用 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 节。
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Download, X } from "./icons";
|
||||
import { isStandalone } from "../lib/pwa";
|
||||
|
||||
/** Chrome/Edge/安卓上那个「安装」事件,TS 的 DOM 库里还没有类型。 */
|
||||
interface InstallPromptEvent extends Event {
|
||||
prompt: () => Promise<void>;
|
||||
userChoice: Promise<{ outcome: "accepted" | "dismissed" }>;
|
||||
}
|
||||
|
||||
const DISMISS_KEY = "dadao.install-hint-dismissed";
|
||||
|
||||
function isIos(): boolean {
|
||||
const ua = window.navigator.userAgent;
|
||||
return /iPad|iPhone|iPod/.test(ua) || (ua.includes("Macintosh") && "ontouchend" in document);
|
||||
}
|
||||
|
||||
/**
|
||||
* 把达道Task装到桌面/主屏。
|
||||
*
|
||||
* 有安装事件就自己弹一个按钮(浏览器自己那套横幅经常被无视);
|
||||
* iOS 没有安装事件,只能告诉用户「分享 → 添加到主屏幕」这一步。
|
||||
* 从主屏启动(standalone)或者用户关过之后就不再出现。
|
||||
*/
|
||||
export function InstallHint() {
|
||||
const [promptEvent, setPromptEvent] = useState<InstallPromptEvent | null>(null);
|
||||
const [hidden, setHidden] = useState(() => localStorage.getItem(DISMISS_KEY) === "1");
|
||||
const [installing, setInstalling] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const onPrompt = (event: Event) => {
|
||||
event.preventDefault(); // 拦住浏览器自带的横幅,换成我们自己的入口
|
||||
setPromptEvent(event as InstallPromptEvent);
|
||||
};
|
||||
const onInstalled = () => setPromptEvent(null);
|
||||
window.addEventListener("beforeinstallprompt", onPrompt);
|
||||
window.addEventListener("appinstalled", onInstalled);
|
||||
return () => {
|
||||
window.removeEventListener("beforeinstallprompt", onPrompt);
|
||||
window.removeEventListener("appinstalled", onInstalled);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (hidden || isStandalone()) return null;
|
||||
|
||||
const ios = isIos();
|
||||
// 桌面 Safari、Firefox 既不给安装事件,也没有添加主屏,不必打扰
|
||||
if (!promptEvent && !ios) return null;
|
||||
|
||||
function dismiss() {
|
||||
localStorage.setItem(DISMISS_KEY, "1");
|
||||
setHidden(true);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-4 flex items-start gap-3 rounded-xl border border-primary-300/40 bg-primary-50/50 px-4 py-3">
|
||||
<span className="mt-0.5 grid h-8 w-8 shrink-0 place-items-center rounded-lg bg-primary-900 text-paper-50">
|
||||
<Download size={15} />
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium text-ink-800">把达道Task装到桌面</p>
|
||||
<p className="mt-1 text-2xs leading-relaxed text-ink-500">
|
||||
{promptEvent
|
||||
? "装好之后是一个独立窗口,不用再敲网址,也不会被浏览器标签页挤掉。"
|
||||
: "在 Safari 里点底部的「分享」,选「添加到主屏幕」,就会和 App 一样有图标。"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-1.5">
|
||||
{promptEvent ? (
|
||||
<button
|
||||
className="rounded-lg bg-ink-900 px-3 py-1.5 text-xs font-medium text-paper-50 transition hover:bg-ink-800 disabled:opacity-50"
|
||||
disabled={installing}
|
||||
onClick={async () => {
|
||||
setInstalling(true);
|
||||
try {
|
||||
await promptEvent.prompt();
|
||||
await promptEvent.userChoice;
|
||||
} catch {
|
||||
// 用户取消或浏览器拒绝:什么都不用说
|
||||
} finally {
|
||||
setInstalling(false);
|
||||
setPromptEvent(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
安装
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
aria-label="不再提示安装"
|
||||
className="rounded-lg p-1.5 text-ink-400 transition hover:bg-paper-200 hover:text-ink-600"
|
||||
onClick={dismiss}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user