Files
meijiaka-zy/tauri-app/src/components/UpdateDialog/UpdateDialog.tsx
T
小鱼开发 d195bb9f1b feat: 自动更新改为每天检查一次
- 新增 localStorage 记录上次检查时间戳 (mjk_last_update_check)
- 启动时判断距离上次检查是否超过 24 小时
- 未超过则跳过,避免每次启动都请求后端
- 设置页手动检查不受此限制
2026-06-01 16:56:18 +08:00

215 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 更新对话框组件
* ==============
*
* 应用启动时自动检查更新(每天最多一次),发现新版本后弹出此对话框。
* 支持强制更新(无法跳过)。
*/
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>
);
}