Files
DadaoTask/web/src/components/UpdatePrompt.tsx
T
Steven 483b554c3c
CI / backend (push) Canceled after 0s
feat(web): Web 装到桌面(PWA),带安装引导与发版提示
用 vite-plugin-pwa 生成 manifest 与 Service Worker,不自己写缓存逻辑。

- 装到桌面:manifest 名字/主题色/三个图标(any + maskable + apple-touch-icon),
  图标和页头、安卓桌面是同一枚;首页底部给安装引导,能装就弹「安装」,
  iOS 给「分享 → 添加到主屏幕」,关掉记在 localStorage
- 发版提示:registerType=prompt,收到新版本只在底部提示,点「刷新」才换。
  故意不自动刷新——Vditor、图表是懒加载分片,后台换 chunk 会让面板 404
- 缓存边界:只预缓存页面骨架(约 0.8MB),/api/ 一律 NetworkOnly,
  离线宁可报错也不端旧数据;/vditor/ 走 CacheFirst,不拖慢首次安装
- 顺手修:UpdatePrompt 居中改用 flex 包一层,animate-slide-up 的 keyframes
  自带 transform,会和 -translate-x-1/2 抢属性,手机窄屏会把卡片推出屏幕

验证:web/e2e/pwa.mjs 19 项全绿(manifest/SW 激活/预缓存/安装引导/iOS 文案/
发版提示/断网仍能打开);tsc 通过、vite build 通过。

已知限制:Service Worker 只在 https 或 localhost 注册,生产入口还是 http,
所以内网 IP 上装不了——需要域名 + 证书反代或隧道,已写进 DEPLOY.md 第 6 节。
2026-10-31 04:33:43 +08:00

59 lines
2.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from "react";
import { Button } from "./ui";
import { applyServiceWorkerUpdate, PWA_EVENT } from "../lib/pwa";
/**
* 装到手机/桌面以后,发版就不是"让用户重新输网址"了:
* 有新版本时在这里提一句,点"刷新"才换,不点就继续用手上的这版。
*
* 为什么不用自动刷新(registerType: "autoUpdate"):Vditor、图表这些是懒加载分片,
* 后台悄悄换掉之后,用户下一次点开某个面板就是 404 —— 编辑器直接白屏。
*/
export function UpdatePrompt() {
const [ready, setReady] = useState(false);
const [applying, setApplying] = useState(false);
const [dismissed, setDismissed] = useState(false);
useEffect(() => {
const onReady = () => setReady(true);
window.addEventListener(PWA_EVENT.updateReady, onReady);
return () => window.removeEventListener(PWA_EVENT.updateReady, onReady);
}, []);
if (!ready || dismissed) return null;
return (
// 居中用 flex 包一层:animate-slide-up 的 keyframes 自带 transform,
// 会和 -translate-x-1/2 抢同一个属性,手机窄屏上卡片会被推出屏幕右侧。
<div className="pointer-events-none fixed inset-x-0 bottom-6 z-[70] flex justify-center px-4">
<div
role="status"
className="animate-slide-up pointer-events-auto w-full max-w-[26rem] rounded-xl border border-line-200 bg-ink-900 px-4 py-3 text-paper-50 shadow-pop"
>
<p className="text-sm">有新版了。刷新一下就用上,手头的输入会丢,先存了再点。</p>
<div className="mt-2 flex justify-end gap-2">
<Button
variant="ghost"
size="sm"
className="text-paper-300 hover:text-paper-50"
onClick={() => setDismissed(true)}
>
稍后
</Button>
<Button
variant="primary"
size="sm"
disabled={applying}
onClick={() => {
setApplying(true);
void applyServiceWorkerUpdate();
}}
>
{applying ? "正在刷新…" : "刷新"}
</Button>
</div>
</div>
</div>
);
}