cb97a45051
Co-authored-by: Cursor <cursoragent@cursor.com>
119 lines
3.4 KiB
TypeScript
119 lines
3.4 KiB
TypeScript
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>
|
||
);
|
||
}
|