fix(web): 首页只留下载、去登录/在线体验入口; 下载区改二维码; 修复网络版一直提示新版本
Android Build & Publish / build-android (push) Failing after 6s
Docker Build and Push / build-image (push) Successful in 2m25s

- Landing: 移除 header 登录/进入应用按钮、hero 在线体验入口
- 下载区: 复制链接行改为 qrcode.react 二维码 (扫码下载 APK)
- 修复 '持续提示新版本可用' bug: vite 构建时把 BUILD_ID 写入 dist/.build-id,
  manifest-web 返回当前部署 dist 的 build_id, 与浏览器内置值同源比对;
  此前返回 OTA 包 build_id (seq-<ts>) 与页面 BUILD_ID 永不一致 → 横幅常驻
This commit is contained in:
jif2.zhang
2026-09-14 09:49:39 +08:00
parent 47db85d01d
commit b2db988834
5 changed files with 54 additions and 46 deletions
+10
View File
@@ -13,6 +13,7 @@
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.21.2",
"qrcode.react": "^4.2.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.26.0",
@@ -3473,6 +3474,15 @@
"node": ">=6"
}
},
"node_modules/qrcode.react": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz",
"integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==",
"license": "ISC",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/qs": {
"version": "6.15.3",
"license": "BSD-3-Clause",
+2 -1
View File
@@ -23,6 +23,7 @@
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.21.2",
"qrcode.react": "^4.2.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.26.0",
@@ -41,4 +42,4 @@
"vite": "^5.3.3",
"vite-plugin-singlefile": "^2.0.1"
}
}
}
+10 -1
View File
@@ -83,13 +83,22 @@ router.get('/ota/bundle/:filename', (req, res) => {
// GET /api/ota/manifest-web - Web 浏览器用的简化版
// 客户端会拿这个跟当前 build_id 比较, 有新版本就提示用户刷新
// 比对依据: 当前部署的 dist/.build-id (vite 构建时写入), 与浏览器内置 BUILD_ID 同源;
// 当服务端换了新 dist, 老标签页拿到的 build_id 变化 → 提示刷新
router.get('/ota/manifest-web', (_req, res) => {
const latest = readLatest()
if (!latest) return res.json({ available: false })
// 优先用"当前托管 dist"的 build_id (部署时 dist/.build-id 与浏览器页面同一次构建)
let buildId = null
try {
buildId = fs
.readFileSync(path.join(path.resolve(process.cwd(), 'dist'), '.build-id'), 'utf8')
.trim()
} catch { /* dist 未构建或无该文件时忽略 */ }
res.json({
available: true,
version: latest.version,
build_id: latest.build_id || latest.version,
build_id: buildId || latest.build_id || latest.version,
note: latest.notes || '',
size: latest.bundle_size || 0,
})
+17 -41
View File
@@ -1,6 +1,5 @@
import React, { useEffect, useState } from "react";
import { useNavigate, Link } from "react-router-dom";
import { useAppStore } from "../stores/app";
import { QRCodeSVG } from "qrcode.react";
import { APP_VERSION, BUILD_ID } from "../version";
import { checkOtaUpdate, reloadApp } from "../api/ota";
@@ -23,17 +22,6 @@ const STATS = [
];
function DownloadSection() {
const [copied, setCopied] = useState(false);
const copy = async () => {
try {
await navigator.clipboard.writeText(APK_URL);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
setCopied(false);
}
};
return (
<section style={styles.section}>
<h2 style={styles.sectionTitle}>下载 Android 版</h2>
@@ -45,13 +33,10 @@ function DownloadSection() {
<p style={{ fontSize: 12, color: "var(--text-tertiary)", marginTop: 10 }}>
版本 {APP_VERSION} · Android 8.0+ · 约 30 MB
</p>
<div style={{ marginTop: 16 }}>
<p style={{ fontSize: 12, color: "var(--text-tertiary)", marginBottom: 6 }}>或复制下载链接(发给手机打开):</p>
<div style={styles.linkRow}>
<code style={styles.linkCode}>{APK_URL}</code>
<button className="btn btn-secondary" style={{ padding: "6px 12px", fontSize: 12 }} onClick={copy}>
{copied ? "已复制 ✓" : "复制"}
</button>
<div style={{ marginTop: 20 }}>
<p style={{ fontSize: 12, color: "var(--text-tertiary)", marginBottom: 10 }}>手机扫码下载:</p>
<div style={styles.qrWrap}>
<QRCodeSVG value={APK_URL} size={168} level="M" style={{ display: "block" }} />
</div>
</div>
<p style={{ fontSize: 12, color: "var(--text-tertiary)", marginTop: 16, lineHeight: 1.6 }}>
@@ -93,23 +78,13 @@ function WebUpdateBanner() {
}
export default function Landing() {
const navigate = useNavigate();
const user = useAppStore((s) => s.user);
return (
<div style={{ minHeight: "100vh", background: "var(--bg)", color: "var(--text-primary)", userSelect: "text" }}>
<header style={styles.header}>
<Link to="/" style={styles.logo}>
<div style={styles.logo}>
<span style={styles.logoEmoji}>👗</span>
<span style={{ fontWeight: 700, fontSize: 18 }}>Wardrobe</span>
</Link>
<button
className="btn btn-primary"
style={{ padding: "8px 18px", fontSize: 14 }}
onClick={() => (user ? navigate("/home") : navigate("/login"))}
>
{user ? "进入应用" : "登录"}
</button>
</div>
</header>
<div style={styles.container}>
@@ -127,9 +102,6 @@ export default function Landing() {
<a className="btn btn-primary" href={APK_URL} download="wardrobe.apk" style={styles.heroBtn}>
⬇️ 下载 Android APK
</a>
<button className="btn btn-secondary" style={styles.heroBtn} onClick={() => navigate("/login")}>
在线体验 →
</button>
</div>
</section>
@@ -182,14 +154,13 @@ export default function Landing() {
const styles: Record<string, React.CSSProperties> = {
header: {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
maxWidth: 960,
margin: "0 auto",
padding: "16px 20px",
display: "flex",
alignItems: "center",
},
logo: { display: "flex", alignItems: "center", gap: 8, textDecoration: "none", color: "var(--text-primary)" },
logo: { display: "flex", alignItems: "center", gap: 8 },
logoEmoji: { fontSize: 24 },
container: { maxWidth: 960, margin: "0 auto", padding: "0 20px 60px" },
hero: { textAlign: "center", padding: "64px 0 48px" },
@@ -211,8 +182,13 @@ const styles: Record<string, React.CSSProperties> = {
margin: "0 auto",
},
downloadBtn: { padding: "14px 32px", fontSize: 16 },
linkRow: { display: "flex", alignItems: "center", gap: 8, justifyContent: "center", flexWrap: "wrap" },
linkCode: { fontSize: 11, color: "var(--text-secondary)", wordBreak: "break-all" },
qrWrap: {
display: "inline-flex",
padding: 10,
background: "#fff",
border: "1px solid var(--border)",
borderRadius: "var(--radius)",
},
techRow: { display: "flex", gap: 10, justifyContent: "center", flexWrap: "wrap" },
footer: { textAlign: "center", padding: "24px 16px", fontSize: 12, color: "var(--text-tertiary)" },
};
+15 -3
View File
@@ -1,6 +1,7 @@
import { defineConfig } from "vite";
import { defineConfig, type Plugin } from "vite";
import react from "@vitejs/plugin-react";
import { readFileSync } from "node:fs";
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
const host = process.env.TAURI_DEV_HOST;
@@ -18,8 +19,19 @@ function buildId() {
export default defineConfig(async () => {
const pkg = JSON.parse(readFileSync(new URL("./package.json", import.meta.url), "utf8"));
// 构建完成后把 BUILD_ID 写进 dist/.build-id, 供 server 的 /api/ota/manifest-web
// 返回"当前部署版本"的 build_id, 让 web 端升级提示只在真是新部署时出现
const writeBuildId: Plugin = {
name: "wardrobe:write-build-id",
closeBundle() {
const dir = path.resolve(process.cwd(), "dist");
mkdirSync(dir, { recursive: true });
writeFileSync(path.join(dir, ".build-id"), buildId(), "utf8");
},
};
return {
plugins: [react()],
plugins: [react(), writeBuildId],
clearScreen: false,
server: {
port: 1420,