feat: 添加安装更新功能,支持下载并安装更新包;优化双屏控制逻辑,增强错误处理

This commit is contained in:
Pine
2026-08-19 13:40:00 +08:00
parent c9460c8d6e
commit fd6493f582
4 changed files with 132 additions and 68 deletions
+57 -34
View File
@@ -1,41 +1,51 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import Icon from './Icons';
import { checkForUpdate, openUpdate } from '../utils/updateCheck';
import { checkForUpdate, downloadAndInstall } from '../utils/updateCheck';
/* =========================================================
UpdateNotify —— 检测新版本(Windows
· 启动静默检查一次 + 每 30 分钟一次(仅在有更新时提示)
· 页眉 logo 点击 → dpm:check-update → 主动检测
有更新 → 弹更新提示;无更新 → 临时提示「已是最新版本」
· 页眉 logo 点击 → dpm:check-update → 主动检测
· 「下载更新」→ 下载安装包 → 启动安装程序 → 应用退出
========================================================= */
export default function UpdateNotify() {
const [upd, setUpd] = useState(null); // 更新提示
const [msg, setMsg] = useState(null); // 临时提示(已是最新 / 失败)
const [upd, setUpd] = useState(null); // 更新提示
const [msg, setMsg] = useState(null); // 临时提示(已是最新 / 失败)
const [dismissed, setDismissed] = useState(false);
const [busy, setBusy] = useState(null); // 'downloading' | 'installing'
const [errText, setErrText] = useState('');
const timerRef = useRef(null);
const run = useCallback(async (manual) => {
const u = await checkForUpdate();
if (u) {
if (u.available) {
setUpd(u);
setDismissed(false);
} else if (manual) {
setMsg(`已是最新版本 v${u.latest}`);
if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => setMsg(null), 3000);
}
} else if (manual) {
setMsg('更新检测失败');
if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => setMsg(null), 3000);
}
if (u.available) { setUpd(u); setDismissed(false); }
else if (manual) { setMsg(`已是最新版本 v${u.latest}`); flash(); }
} else if (manual) { setMsg('更新检测失败'); flash(); }
}, []);
const flash = () => {
if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => setMsg(null), 3000);
};
const handleDownload = useCallback(async () => {
if (!upd || busy) return;
setBusy('downloading');
setErrText('');
try {
await downloadAndInstall(upd.url, upd.name);
setBusy('installing'); // 应用即将退出
} catch (e) {
setErrText(String((e && e.message) || e));
setBusy(null);
}
}, [upd, busy]);
useEffect(() => {
const onCheck = () => run(true);
window.addEventListener('dpm:check-update', onCheck);
run(false); // 启动静默检测
run(false);
const iv = setInterval(() => run(false), 30 * 60 * 1000);
return () => {
window.removeEventListener('dpm:check-update', onCheck);
@@ -44,25 +54,38 @@ export default function UpdateNotify() {
};
}, [run]);
return (
<>
{upd && !dismissed && (
<div className="upd-toast" role="status">
<div className="upd-ico"><Icon name="rocket" size={15} /></div>
<div className="upd-body">
<b>发现新版本 v{upd.latest}</b>
<span>当前 v{upd.current} · 点击下载更新安装包Windows</span>
</div>
<button className="upd-btn" onClick={() => openUpdate(upd.url)}>下载更新</button>
<button className="upd-close" onClick={() => setDismissed(true)} aria-label="关闭">×</button>
</div>
)}
{msg && (
if (!upd || dismissed) {
if (msg) {
return (
<div className="upd-toast upd-msg" role="status">
<div className="upd-ico"><Icon name="check" size={15} /></div>
<div className="upd-body"><b>{msg}</b></div>
</div>
);
}
return null;
}
return (
<div className="upd-toast" role="status">
<div className="upd-ico"><Icon name="rocket" size={15} /></div>
<div className="upd-body">
{busy === 'downloading' && <b>正在下载更新 v{upd.latest}</b>}
{busy === 'installing' && <b>正在启动安装程序应用即将退出</b>}
{!busy && <b>发现新版本 v{upd.latest}</b>}
{!busy && <span>当前 v{upd.current} · 点击下载更新并安装Windows</span>}
{errText && <span style={{ color: '#f04438' }}>{errText}</span>}
</div>
<button
className="upd-btn"
disabled={!!busy}
onClick={handleDownload}
>
{busy ? '处理中…' : '下载更新'}
</button>
{!busy && (
<button className="upd-close" onClick={() => setDismissed(true)} aria-label="关闭">×</button>
)}
</>
</div>
);
}
+14 -11
View File
@@ -1,10 +1,10 @@
/* =========================================================
客户端更新检测(仅 Windows)
客户端更新检测与安装(仅 Windows
后端 POST /api/update/check 返回最新版本 + 安装包下载地址;
前端比对后提示,并提供下载(opener 打开安装包下载链接)。
前端 fetch 下载安装包字节 → invoke install_update(写盘 → 启动安装程序 → 退出应用)。
========================================================= */
import { getApiBase } from '../config';
import { openUrl } from '@tauri-apps/plugin-opener';
import { invoke } from '@tauri-apps/api/core';
// 当前版本:发布新版本时同步 package.json 的 version
export const APP_VERSION = '0.1.0';
@@ -23,18 +23,21 @@ export async function checkForUpdate() {
latest: data.latest_version,
available: !!data.update_available,
url: `${getApiBase()}${data.download_url || '/download'}`,
name: data.installer_name || '',
name: data.installer_name || 'dpm_update_setup.exe',
};
}
} catch { /* 后端不可达时忽略 */ }
return null;
}
/** 打开安装包下载地址(优先系统浏览器下载)。 */
export function openUpdate(url) {
try {
openUrl(url);
} catch {
window.open(url, '_blank');
}
/** 下载安装包并交给后端安装(写盘 → 启动安装程序 → 退出应用)。 */
export async function downloadAndInstall(url, filename) {
const res = await fetch(url);
if (!res.ok) throw new Error(`下载失败(HTTP ${res.status}`);
const buf = new Uint8Array(await res.arrayBuffer());
if (buf.length < 1000) throw new Error('安装包内容无效');
return invoke('install_update', {
bytes: Array.from(buf),
filename: filename || 'dpm_update_setup.exe',
});
}
+6 -1
View File
@@ -100,7 +100,12 @@ export function useMqttControl() {
// 双屏:admin 控制第二块屏全屏展示(Tauri 命令)——仅主窗口执行
if (WINDOW_LABEL !== 'main') break;
aiStatus(cmd.params?.on === false ? '正在关闭双屏' : '正在启动双屏', { icon: 'page' });
invoke('set_dual_screen', { on: cmd.params?.on !== false }).catch(() => {});
invoke('set_dual_screen', { on: cmd.params?.on !== false })
.catch((err) => {
const msg = String((err && err.message) || err || '操作失败');
aiStatus(`双屏失败:${msg}`, { state: 'error', icon: 'error' });
console.error('[dual]', err);
});
break;
case 'alert':
// 通知本身即展示,无需再弹「正在发送通知」状态提示