Add PWA install badge for desktop and tablet (manifest, SW, Add to Home Screen).
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
BeforeInstallPromptEvent,
|
||||
isAppleTouchDevice,
|
||||
isStandaloneDisplay,
|
||||
} from "../pwa/install";
|
||||
|
||||
/**
|
||||
* Desktop/tablet install affordance:
|
||||
* - Chrome/Edge/Android: native install via beforeinstallprompt
|
||||
* - iPad/iOS: tip to Add to Home Screen
|
||||
* - Already installed: show 已安装标识
|
||||
*/
|
||||
export default function InstallBadge() {
|
||||
const [standalone, setStandalone] = useState(isStandaloneDisplay);
|
||||
const [deferred, setDeferred] = useState<BeforeInstallPromptEvent | null>(
|
||||
null,
|
||||
);
|
||||
const [tipOpen, setTipOpen] = useState(false);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const apple = isAppleTouchDevice();
|
||||
|
||||
useEffect(() => {
|
||||
const mq = window.matchMedia("(display-mode: standalone)");
|
||||
const sync = () => setStandalone(isStandaloneDisplay());
|
||||
mq.addEventListener?.("change", sync);
|
||||
sync();
|
||||
|
||||
const onBip = (e: Event) => {
|
||||
e.preventDefault();
|
||||
setDeferred(e as BeforeInstallPromptEvent);
|
||||
};
|
||||
const onInstalled = () => {
|
||||
setDeferred(null);
|
||||
setStandalone(true);
|
||||
setTipOpen(false);
|
||||
};
|
||||
window.addEventListener("beforeinstallprompt", onBip);
|
||||
window.addEventListener("appinstalled", onInstalled);
|
||||
return () => {
|
||||
mq.removeEventListener?.("change", sync);
|
||||
window.removeEventListener("beforeinstallprompt", onBip);
|
||||
window.removeEventListener("appinstalled", onInstalled);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (standalone) {
|
||||
return (
|
||||
<span className="install-badge installed" title="已安装为独立应用">
|
||||
已安装
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const canNativeInstall = Boolean(deferred);
|
||||
|
||||
async function onInstall() {
|
||||
if (!deferred) {
|
||||
setTipOpen((v) => !v);
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
try {
|
||||
await deferred.prompt();
|
||||
await deferred.userChoice;
|
||||
setDeferred(null);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="install-wrap">
|
||||
<button
|
||||
type="button"
|
||||
className="btn ghost install-btn"
|
||||
disabled={busy}
|
||||
onClick={() => void onInstall()}
|
||||
title={
|
||||
canNativeInstall
|
||||
? "安装到电脑或平板"
|
||||
: apple
|
||||
? "添加到主屏幕(平板/手机)"
|
||||
: "安装说明"
|
||||
}
|
||||
>
|
||||
{canNativeInstall ? (busy ? "安装中…" : "安装应用") : "安装应用"}
|
||||
</button>
|
||||
{tipOpen && !canNativeInstall ? (
|
||||
<div className="install-tip" role="dialog" aria-label="安装说明">
|
||||
{apple ? (
|
||||
<>
|
||||
<p>iPad / iPhone:用 Safari 打开本站,点分享 →「添加到主屏幕」。</p>
|
||||
<p className="muted">安装后从主屏幕打开即为独立应用窗口。</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p>
|
||||
电脑:Chrome / Edge 地址栏右侧「安装」图标,或菜单 → 安装应用。
|
||||
</p>
|
||||
<p>
|
||||
安卓平板:浏览器菜单 →「安装应用」/「添加到主屏幕」。
|
||||
</p>
|
||||
<p className="muted">需 HTTPS(或本机 localhost)且已加载完成。</p>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="btn ghost"
|
||||
onClick={() => setTipOpen(false)}
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user