CI / backend (push) Canceled after 0s
用 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 节。
100 lines
3.7 KiB
TypeScript
100 lines
3.7 KiB
TypeScript
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>
|
|
);
|
|
}
|