77 lines
3.3 KiB
TypeScript
77 lines
3.3 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useState } from "react";
|
||
import { Button } from "@/components/ui/button";
|
||
import { AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter } from "@/components/ui/alert-dialog";
|
||
import { preparePageRefresh } from "@/lib/client/page-refresh";
|
||
|
||
export function AppUpdateNotice() {
|
||
const [availableVersion, setAvailableVersion] = useState("");
|
||
const [refreshing, setRefreshing] = useState(false);
|
||
const [error, setError] = useState("");
|
||
useEffect(() => {
|
||
let disposed = false;
|
||
let controller: AbortController | undefined;
|
||
let checking = false;
|
||
const check = async () => {
|
||
if (disposed || checking || document.visibilityState !== "visible") return;
|
||
checking = true;
|
||
controller = new AbortController();
|
||
const current = controller;
|
||
const timeout = window.setTimeout(() => current.abort(), 8000);
|
||
try {
|
||
const response = await fetch("/api/app-version", { cache: "no-store", signal: current.signal });
|
||
if (!response.ok) return;
|
||
const data = await response.json();
|
||
if (!disposed && !current.signal.aborted && typeof data.version === "string" && data.version)
|
||
setAvailableVersion(data.version !== (process.env.NEXT_PUBLIC_APP_RELEASE || "development") ? data.version : "");
|
||
} catch {
|
||
// A failed version read leaves the current workspace available.
|
||
} finally {
|
||
window.clearTimeout(timeout);
|
||
checking = false;
|
||
}
|
||
};
|
||
void check();
|
||
const interval = window.setInterval(check, 60_000);
|
||
window.addEventListener("focus", check);
|
||
window.addEventListener("online", check);
|
||
document.addEventListener("visibilitychange", check);
|
||
return () => {
|
||
disposed = true;
|
||
controller?.abort();
|
||
clearInterval(interval);
|
||
window.removeEventListener("focus", check);
|
||
window.removeEventListener("online", check);
|
||
document.removeEventListener("visibilitychange", check);
|
||
};
|
||
}, []);
|
||
|
||
const refresh = async () => {
|
||
if (refreshing) return;
|
||
setRefreshing(true);
|
||
setError("");
|
||
try {
|
||
await preparePageRefresh();
|
||
window.location.reload();
|
||
setRefreshing(false);
|
||
} catch (cause) {
|
||
setError(cause instanceof Error ? cause.message : "当前内容同步失败,请保留此页并重试。");
|
||
setRefreshing(false);
|
||
}
|
||
};
|
||
return <AlertDialog open={Boolean(availableVersion)}>
|
||
<AlertDialogContent className="z-[10001]" overlayClassName="z-[10000]" onEscapeKeyDown={event => event.preventDefault()}>
|
||
<AlertDialogHeader>
|
||
<AlertDialogTitle>平台已更新</AlertDialogTitle>
|
||
<AlertDialogDescription>需要刷新页面后继续使用。点击后会先同步当前画布并保留输入;正在处理的任务完成后再刷新。</AlertDialogDescription>
|
||
</AlertDialogHeader>
|
||
{error ? <p role="alert" className="text-sm text-destructive">{error}</p> : null}
|
||
<AlertDialogFooter>
|
||
{error ? <Button variant="outline" disabled={refreshing} onClick={() => { setAvailableVersion(""); setError(""); }}>返回处理</Button> : null}
|
||
<Button disabled={refreshing} onClick={() => void refresh()}>{refreshing ? "正在同步内容…" : "同步并刷新"}</Button>
|
||
</AlertDialogFooter>
|
||
</AlertDialogContent>
|
||
</AlertDialog>;
|
||
}
|