d195bb9f1b
- 新增 localStorage 记录上次检查时间戳 (mjk_last_update_check) - 启动时判断距离上次检查是否超过 24 小时 - 未超过则跳过,避免每次启动都请求后端 - 设置页手动检查不受此限制
215 lines
7.2 KiB
TypeScript
215 lines
7.2 KiB
TypeScript
/**
|
||
* 更新对话框组件
|
||
* ==============
|
||
*
|
||
* 应用启动时自动检查更新(每天最多一次),发现新版本后弹出此对话框。
|
||
* 支持强制更新(无法跳过)。
|
||
*/
|
||
|
||
import { useEffect } from 'react';
|
||
import { useUpdater } from '../../hooks/useUpdater';
|
||
import './UpdateDialog.css';
|
||
|
||
const CURRENT_VERSION = __APP_VERSION__;
|
||
|
||
export default function UpdateDialog() {
|
||
const {
|
||
hasUpdate,
|
||
updateInfo,
|
||
isMandatory,
|
||
checking,
|
||
downloading,
|
||
installing,
|
||
progress,
|
||
downloadedBytes,
|
||
totalBytes,
|
||
error,
|
||
check,
|
||
downloadAndInstall,
|
||
dismiss,
|
||
relaunch,
|
||
} = useUpdater();
|
||
|
||
// 应用启动时自动检查更新(每天最多一次)
|
||
// 延迟 3 秒避免阻塞首屏;silent=true:失败时不弹窗,只打 console 日志
|
||
useEffect(() => {
|
||
const timer = setTimeout(() => {
|
||
const LAST_CHECK_KEY = 'mjk_last_update_check';
|
||
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
|
||
|
||
const lastCheckRaw = localStorage.getItem(LAST_CHECK_KEY);
|
||
const lastCheck = lastCheckRaw ? parseInt(lastCheckRaw, 10) : 0;
|
||
const now = Date.now();
|
||
|
||
if (!lastCheck || now - lastCheck > ONE_DAY_MS) {
|
||
check(true).finally(() => {
|
||
localStorage.setItem(LAST_CHECK_KEY, Date.now().toString());
|
||
});
|
||
}
|
||
}, 3000);
|
||
return () => clearTimeout(timer);
|
||
}, [check]);
|
||
|
||
// 只在有实质内容时显示弹窗:发现更新 / 正在下载 / 安装完成 / 出错
|
||
// checking 阶段不显示,避免一闪而过的白屏/loading
|
||
if (!hasUpdate && !downloading && !installing && !error) {
|
||
return null;
|
||
}
|
||
|
||
const formatBytes = (bytes: number) => {
|
||
if (bytes === 0) {return '0 B';}
|
||
const k = 1024;
|
||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`;
|
||
};
|
||
|
||
const title = checking
|
||
? '正在检查更新...'
|
||
: downloading
|
||
? '正在下载更新...'
|
||
: installing
|
||
? '更新已就绪'
|
||
: error
|
||
? '检查更新失败'
|
||
: `发现新版本 ${updateInfo?.version ?? ''}`;
|
||
|
||
return (
|
||
<div className="update-dialog-overlay">
|
||
<div className="update-dialog">
|
||
{/* 标题 */}
|
||
<div className="update-dialog-header">
|
||
<h3 className="update-dialog-title">{title}</h3>
|
||
{!isMandatory && !downloading && !installing && !error && (
|
||
<button className="update-dialog-close" onClick={dismiss} aria-label="关闭">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||
<line x1="18" y1="6" x2="6" y2="18" />
|
||
<line x1="6" y1="6" x2="18" y2="18" />
|
||
</svg>
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{/* 内容 */}
|
||
<div className="update-dialog-body">
|
||
{/* 检查中 */}
|
||
{checking && (
|
||
<div className="update-dialog-loading">
|
||
<div className="update-dialog-spinner" />
|
||
<p>正在检查是否有新版本...</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* 发现更新 */}
|
||
{!checking && hasUpdate && updateInfo && (
|
||
<>
|
||
<div className="update-dialog-version">
|
||
<span className="update-dialog-current">当前版本: {CURRENT_VERSION}</span>
|
||
<span className="update-dialog-arrow">→</span>
|
||
<span className="update-dialog-new">新版本: {updateInfo.version}</span>
|
||
</div>
|
||
|
||
{updateInfo.body && (
|
||
<div className="update-dialog-notes">
|
||
<div className="update-dialog-notes-title">更新内容:</div>
|
||
<pre className="update-dialog-notes-content">{updateInfo.body}</pre>
|
||
</div>
|
||
)}
|
||
|
||
{isMandatory && (
|
||
<div className="update-dialog-mandatory">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||
<path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" />
|
||
<line x1="12" y1="9" x2="12" y2="13" />
|
||
<line x1="12" y1="17" x2="12.01" y2="17" />
|
||
</svg>
|
||
此版本为强制更新,必须安装后才能继续使用
|
||
</div>
|
||
)}
|
||
</>
|
||
)}
|
||
|
||
{/* 下载进度 */}
|
||
{downloading && (
|
||
<div className="update-dialog-progress">
|
||
<div className="update-dialog-progress-bar">
|
||
<div
|
||
className="update-dialog-progress-fill"
|
||
style={{ width: `${progress}%` }}
|
||
/>
|
||
</div>
|
||
<div className="update-dialog-progress-info">
|
||
<span>{progress}%</span>
|
||
{totalBytes > 0 && (
|
||
<span>
|
||
{formatBytes(downloadedBytes)} / {formatBytes(totalBytes)}
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 安装完成 */}
|
||
{installing && (
|
||
<div className="update-dialog-success">
|
||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||
<path d="M22 11.08V12a10 10 0 11-5.93-9.14" />
|
||
<polyline points="22 4 12 14.01 9 11.01" />
|
||
</svg>
|
||
<p>更新已下载并安装完成</p>
|
||
<p className="update-dialog-hint">重启应用后即可使用新版本</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* 错误 */}
|
||
{error && (
|
||
<div className="update-dialog-error">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||
<circle cx="12" cy="12" r="10" />
|
||
<line x1="12" y1="8" x2="12" y2="12" />
|
||
<line x1="12" y1="16" x2="12.01" y2="16" />
|
||
</svg>
|
||
<span>{error}</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 按钮 */}
|
||
<div className="update-dialog-footer">
|
||
{!checking && !downloading && !installing && (
|
||
<>
|
||
{error && (
|
||
<button className="update-dialog-btn secondary" onClick={dismiss}>
|
||
关闭
|
||
</button>
|
||
)}
|
||
{!error && !isMandatory && (
|
||
<button className="update-dialog-btn secondary" onClick={dismiss}>
|
||
稍后提醒
|
||
</button>
|
||
)}
|
||
{!error && (
|
||
<button className="update-dialog-btn primary" onClick={downloadAndInstall}>
|
||
立即更新
|
||
</button>
|
||
)}
|
||
</>
|
||
)}
|
||
|
||
{downloading && (
|
||
<button className="update-dialog-btn secondary" disabled>
|
||
下载中...
|
||
</button>
|
||
)}
|
||
|
||
{installing && (
|
||
<button className="update-dialog-btn primary" onClick={relaunch}>
|
||
立即重启
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|