fix(web): 首页只留下载、去登录/在线体验入口; 下载区改二维码; 修复网络版一直提示新版本
- 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:
Generated
+10
@@ -13,6 +13,7 @@
|
|||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
|
"qrcode.react": "^4.2.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-router-dom": "^6.26.0",
|
"react-router-dom": "^6.26.0",
|
||||||
@@ -3473,6 +3474,15 @@
|
|||||||
"node": ">=6"
|
"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": {
|
"node_modules/qs": {
|
||||||
"version": "6.15.3",
|
"version": "6.15.3",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
|
"qrcode.react": "^4.2.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-router-dom": "^6.26.0",
|
"react-router-dom": "^6.26.0",
|
||||||
|
|||||||
+10
-1
@@ -83,13 +83,22 @@ router.get('/ota/bundle/:filename', (req, res) => {
|
|||||||
|
|
||||||
// GET /api/ota/manifest-web - Web 浏览器用的简化版
|
// GET /api/ota/manifest-web - Web 浏览器用的简化版
|
||||||
// 客户端会拿这个跟当前 build_id 比较, 有新版本就提示用户刷新
|
// 客户端会拿这个跟当前 build_id 比较, 有新版本就提示用户刷新
|
||||||
|
// 比对依据: 当前部署的 dist/.build-id (vite 构建时写入), 与浏览器内置 BUILD_ID 同源;
|
||||||
|
// 当服务端换了新 dist, 老标签页拿到的 build_id 变化 → 提示刷新
|
||||||
router.get('/ota/manifest-web', (_req, res) => {
|
router.get('/ota/manifest-web', (_req, res) => {
|
||||||
const latest = readLatest()
|
const latest = readLatest()
|
||||||
if (!latest) return res.json({ available: false })
|
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({
|
res.json({
|
||||||
available: true,
|
available: true,
|
||||||
version: latest.version,
|
version: latest.version,
|
||||||
build_id: latest.build_id || latest.version,
|
build_id: buildId || latest.build_id || latest.version,
|
||||||
note: latest.notes || '',
|
note: latest.notes || '',
|
||||||
size: latest.bundle_size || 0,
|
size: latest.bundle_size || 0,
|
||||||
})
|
})
|
||||||
|
|||||||
+17
-41
@@ -1,6 +1,5 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useNavigate, Link } from "react-router-dom";
|
import { QRCodeSVG } from "qrcode.react";
|
||||||
import { useAppStore } from "../stores/app";
|
|
||||||
import { APP_VERSION, BUILD_ID } from "../version";
|
import { APP_VERSION, BUILD_ID } from "../version";
|
||||||
import { checkOtaUpdate, reloadApp } from "../api/ota";
|
import { checkOtaUpdate, reloadApp } from "../api/ota";
|
||||||
|
|
||||||
@@ -23,17 +22,6 @@ const STATS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
function DownloadSection() {
|
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 (
|
return (
|
||||||
<section style={styles.section}>
|
<section style={styles.section}>
|
||||||
<h2 style={styles.sectionTitle}>下载 Android 版</h2>
|
<h2 style={styles.sectionTitle}>下载 Android 版</h2>
|
||||||
@@ -45,13 +33,10 @@ function DownloadSection() {
|
|||||||
<p style={{ fontSize: 12, color: "var(--text-tertiary)", marginTop: 10 }}>
|
<p style={{ fontSize: 12, color: "var(--text-tertiary)", marginTop: 10 }}>
|
||||||
版本 {APP_VERSION} · Android 8.0+ · 约 30 MB
|
版本 {APP_VERSION} · Android 8.0+ · 约 30 MB
|
||||||
</p>
|
</p>
|
||||||
<div style={{ marginTop: 16 }}>
|
<div style={{ marginTop: 20 }}>
|
||||||
<p style={{ fontSize: 12, color: "var(--text-tertiary)", marginBottom: 6 }}>或复制下载链接(发给手机打开):</p>
|
<p style={{ fontSize: 12, color: "var(--text-tertiary)", marginBottom: 10 }}>手机扫码下载:</p>
|
||||||
<div style={styles.linkRow}>
|
<div style={styles.qrWrap}>
|
||||||
<code style={styles.linkCode}>{APK_URL}</code>
|
<QRCodeSVG value={APK_URL} size={168} level="M" style={{ display: "block" }} />
|
||||||
<button className="btn btn-secondary" style={{ padding: "6px 12px", fontSize: 12 }} onClick={copy}>
|
|
||||||
{copied ? "已复制 ✓" : "复制"}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p style={{ fontSize: 12, color: "var(--text-tertiary)", marginTop: 16, lineHeight: 1.6 }}>
|
<p style={{ fontSize: 12, color: "var(--text-tertiary)", marginTop: 16, lineHeight: 1.6 }}>
|
||||||
@@ -93,23 +78,13 @@ function WebUpdateBanner() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Landing() {
|
export default function Landing() {
|
||||||
const navigate = useNavigate();
|
|
||||||
const user = useAppStore((s) => s.user);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ minHeight: "100vh", background: "var(--bg)", color: "var(--text-primary)", userSelect: "text" }}>
|
<div style={{ minHeight: "100vh", background: "var(--bg)", color: "var(--text-primary)", userSelect: "text" }}>
|
||||||
<header style={styles.header}>
|
<header style={styles.header}>
|
||||||
<Link to="/" style={styles.logo}>
|
<div style={styles.logo}>
|
||||||
<span style={styles.logoEmoji}>👗</span>
|
<span style={styles.logoEmoji}>👗</span>
|
||||||
<span style={{ fontWeight: 700, fontSize: 18 }}>Wardrobe</span>
|
<span style={{ fontWeight: 700, fontSize: 18 }}>Wardrobe</span>
|
||||||
</Link>
|
</div>
|
||||||
<button
|
|
||||||
className="btn btn-primary"
|
|
||||||
style={{ padding: "8px 18px", fontSize: 14 }}
|
|
||||||
onClick={() => (user ? navigate("/home") : navigate("/login"))}
|
|
||||||
>
|
|
||||||
{user ? "进入应用" : "登录"}
|
|
||||||
</button>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div style={styles.container}>
|
<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}>
|
<a className="btn btn-primary" href={APK_URL} download="wardrobe.apk" style={styles.heroBtn}>
|
||||||
⬇️ 下载 Android APK
|
⬇️ 下载 Android APK
|
||||||
</a>
|
</a>
|
||||||
<button className="btn btn-secondary" style={styles.heroBtn} onClick={() => navigate("/login")}>
|
|
||||||
在线体验 →
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -182,14 +154,13 @@ export default function Landing() {
|
|||||||
|
|
||||||
const styles: Record<string, React.CSSProperties> = {
|
const styles: Record<string, React.CSSProperties> = {
|
||||||
header: {
|
header: {
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
maxWidth: 960,
|
maxWidth: 960,
|
||||||
margin: "0 auto",
|
margin: "0 auto",
|
||||||
padding: "16px 20px",
|
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 },
|
logoEmoji: { fontSize: 24 },
|
||||||
container: { maxWidth: 960, margin: "0 auto", padding: "0 20px 60px" },
|
container: { maxWidth: 960, margin: "0 auto", padding: "0 20px 60px" },
|
||||||
hero: { textAlign: "center", padding: "64px 0 48px" },
|
hero: { textAlign: "center", padding: "64px 0 48px" },
|
||||||
@@ -211,8 +182,13 @@ const styles: Record<string, React.CSSProperties> = {
|
|||||||
margin: "0 auto",
|
margin: "0 auto",
|
||||||
},
|
},
|
||||||
downloadBtn: { padding: "14px 32px", fontSize: 16 },
|
downloadBtn: { padding: "14px 32px", fontSize: 16 },
|
||||||
linkRow: { display: "flex", alignItems: "center", gap: 8, justifyContent: "center", flexWrap: "wrap" },
|
qrWrap: {
|
||||||
linkCode: { fontSize: 11, color: "var(--text-secondary)", wordBreak: "break-all" },
|
display: "inline-flex",
|
||||||
|
padding: 10,
|
||||||
|
background: "#fff",
|
||||||
|
border: "1px solid var(--border)",
|
||||||
|
borderRadius: "var(--radius)",
|
||||||
|
},
|
||||||
techRow: { display: "flex", gap: 10, justifyContent: "center", flexWrap: "wrap" },
|
techRow: { display: "flex", gap: 10, justifyContent: "center", flexWrap: "wrap" },
|
||||||
footer: { textAlign: "center", padding: "24px 16px", fontSize: 12, color: "var(--text-tertiary)" },
|
footer: { textAlign: "center", padding: "24px 16px", fontSize: 12, color: "var(--text-tertiary)" },
|
||||||
};
|
};
|
||||||
+15
-3
@@ -1,6 +1,7 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig, type Plugin } from "vite";
|
||||||
import react from "@vitejs/plugin-react";
|
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;
|
const host = process.env.TAURI_DEV_HOST;
|
||||||
|
|
||||||
@@ -18,8 +19,19 @@ function buildId() {
|
|||||||
export default defineConfig(async () => {
|
export default defineConfig(async () => {
|
||||||
const pkg = JSON.parse(readFileSync(new URL("./package.json", import.meta.url), "utf8"));
|
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 {
|
return {
|
||||||
plugins: [react()],
|
plugins: [react(), writeBuildId],
|
||||||
clearScreen: false,
|
clearScreen: false,
|
||||||
server: {
|
server: {
|
||||||
port: 1420,
|
port: 1420,
|
||||||
|
|||||||
Reference in New Issue
Block a user