feat: 添加客户端更新检测功能,支持版本检查和更新提示;更新相关组件和样式
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
from contextlib import asynccontextmanager
|
||||
@@ -15,6 +16,7 @@ from fastapi import FastAPI, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse, JSONResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .admin import admin_login, admin_logout, admin_page
|
||||
from .config import settings
|
||||
@@ -120,6 +122,52 @@ async def download_installer():
|
||||
return JSONResponse({"detail": "安装包不存在"}, status_code=404)
|
||||
|
||||
|
||||
# ---------- 客户端更新检测(仅 Windows:比对版本 → 下发安装包下载地址) ----------
|
||||
|
||||
def _latest_installer():
|
||||
"""返回 backend/ 下最新(按文件名排序取最大)的 *.setup.exe。"""
|
||||
for f in sorted(_DOWNLOAD_ROOT.glob("*setup.exe"), reverse=True):
|
||||
if f.is_file():
|
||||
return f
|
||||
return None
|
||||
|
||||
|
||||
def _version_from_name(name: str) -> str:
|
||||
"""从安装包文件名解析版本,如 `..._0.1.0_x64-setup.exe` → `0.1.0`。"""
|
||||
m = re.search(r"_(\d+\.\d+(?:\.\d+)?)", name or "")
|
||||
return m.group(1) if m else ""
|
||||
|
||||
|
||||
def _version_key(v: str):
|
||||
return tuple(int(x) for x in v.split(".") if x.isdigit())
|
||||
|
||||
|
||||
class UpdateCheckBody(BaseModel):
|
||||
current: str = "" # 前端上报的当前版本,如 0.1.0
|
||||
|
||||
|
||||
@app.post("/api/update/check", include_in_schema=False)
|
||||
async def update_check(body: UpdateCheckBody):
|
||||
"""检测更新:返回最新版本号、安装包下载地址、是否需要更新(仅 Windows)。"""
|
||||
inst = _latest_installer()
|
||||
if not inst:
|
||||
return {"ok": False, "error": "installer not found"}
|
||||
latest = _version_from_name(inst.name)
|
||||
current = (body.current or "").strip()
|
||||
update_available = False
|
||||
if latest and current and current != latest:
|
||||
update_available = _version_key(latest) > _version_key(current)
|
||||
elif latest and not current:
|
||||
update_available = True
|
||||
return {
|
||||
"ok": True,
|
||||
"latest_version": latest,
|
||||
"installer_name": inst.name,
|
||||
"download_url": "/download",
|
||||
"update_available": update_available,
|
||||
}
|
||||
|
||||
|
||||
# ---------- 前端静态托管 + SPA 回退(浏览器直接访问 :8000 即可) ----------
|
||||
|
||||
@app.get("/{path:path}", include_in_schema=False)
|
||||
|
||||
Binary file not shown.
@@ -141,9 +141,19 @@ export default function PageHeader() {
|
||||
{/* <span className="bd-logo">
|
||||
<Image src="/logo.png" alt="Logo" />
|
||||
</span> */}
|
||||
<Avatar size="lg">
|
||||
<AvatarImage src="/logo.png" alt="" />
|
||||
</Avatar>
|
||||
<span
|
||||
className="bd-logo-update"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
title="点击检测更新"
|
||||
aria-label="点击检测更新"
|
||||
onClick={() => window.dispatchEvent(new CustomEvent('dpm:check-update'))}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') window.dispatchEvent(new CustomEvent('dpm:check-update')); }}
|
||||
>
|
||||
<Avatar size="lg">
|
||||
<AvatarImage src="/logo.png" alt="" />
|
||||
</Avatar>
|
||||
</span>
|
||||
<div className="bd-header-titles">
|
||||
<h1 className="bd-header-title">昆明市大学生创业园 <span className="bd-header-title-accent">· OPC 智能园区数字运营中心</span></h1>
|
||||
<div className="bd-header-sub">云南省首家政府主办大学生创业孵化园区 · <span className="bd-header-content—highlights">空间+孵化+融资+政策+资源+AI赋能+综合服务</span></div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Outlet } from 'react-router-dom';
|
||||
import PageHeader from './PageHeader';
|
||||
import DpmOverlays from './DpmOverlays';
|
||||
import ToolStatusToast from './ToolStatusToast';
|
||||
import UpdateNotify from './UpdateNotify';
|
||||
import GlobalVision from './GlobalVision';
|
||||
import { usePageNav } from '../utils/pageNav';
|
||||
import { useMqttControl } from '../utils/useMqttControl';
|
||||
@@ -29,6 +30,7 @@ export default function ScreenLayout() {
|
||||
<GlobalVision />
|
||||
<DpmOverlays />
|
||||
<ToolStatusToast />
|
||||
<UpdateNotify />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import Icon from './Icons';
|
||||
import { checkForUpdate, openUpdate } from '../utils/updateCheck';
|
||||
|
||||
/* =========================================================
|
||||
UpdateNotify —— 检测新版本(Windows)
|
||||
· 启动静默检查一次 + 每 30 分钟一次(仅在有更新时提示)
|
||||
· 页眉 logo 点击 → dpm:check-update → 主动检测:
|
||||
有更新 → 弹更新提示;无更新 → 临时提示「已是最新版本」
|
||||
========================================================= */
|
||||
export default function UpdateNotify() {
|
||||
const [upd, setUpd] = useState(null); // 更新提示
|
||||
const [msg, setMsg] = useState(null); // 临时提示(已是最新 / 失败)
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
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);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const onCheck = () => run(true);
|
||||
window.addEventListener('dpm:check-update', onCheck);
|
||||
run(false); // 启动静默检测
|
||||
const iv = setInterval(() => run(false), 30 * 60 * 1000);
|
||||
return () => {
|
||||
window.removeEventListener('dpm:check-update', onCheck);
|
||||
clearInterval(iv);
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
};
|
||||
}, [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 && (
|
||||
<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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2201,6 +2201,86 @@
|
||||
color: #fff;
|
||||
background: linear-gradient(140deg, var(--bd-blue), var(--bd-cyan));
|
||||
}
|
||||
/* ---- 页眉 logo:点击检测更新 ---- */
|
||||
.bd-logo-update {
|
||||
display: inline-flex;
|
||||
cursor: pointer;
|
||||
border-radius: 12px;
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.bd-logo-update:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(29, 93, 206, 0.25);
|
||||
}
|
||||
.bd-logo-update:active { transform: translateY(0); }
|
||||
.bd-logo-update:focus-visible { outline: 2px solid var(--bd-blue); outline-offset: 2px; }
|
||||
|
||||
/* ---- 底部:更新提示(Windows 检测到新版本) ---- */
|
||||
.upd-toast {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 3200;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
max-width: 540px;
|
||||
padding: 12px 16px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--bd-line);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 12px 30px rgba(29, 44, 68, 0.16);
|
||||
animation: bdOverlayIn 0.3s ease;
|
||||
}
|
||||
.upd-ico {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 8px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
background: linear-gradient(140deg, var(--bd-blue), var(--bd-cyan));
|
||||
}
|
||||
.upd-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
.upd-body b { font-size: 13px; color: var(--bd-ink); }
|
||||
.upd-body span { font-size: 11.5px; color: var(--bd-sub); }
|
||||
.upd-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 7px 15px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(140deg, var(--bd-blue), var(--bd-cyan));
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.upd-btn:hover { opacity: 0.9; }
|
||||
.upd-msg .upd-ico { background: linear-gradient(140deg, var(--bd-green), #4ade80); }
|
||||
.upd-close {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--bd-sub);
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.upd-close:hover { color: var(--bd-red); background: rgba(240, 68, 56, 0.1); }
|
||||
|
||||
/* ---- 右上角:工具 / 操作进行中状态提示 ---- */
|
||||
.ts-toast {
|
||||
position: fixed;
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/* =========================================================
|
||||
客户端更新检测(仅 Windows)
|
||||
后端 POST /api/update/check 返回最新版本 + 安装包下载地址;
|
||||
前端比对后提示,并提供下载(opener 打开安装包下载链接)。
|
||||
========================================================= */
|
||||
import { getApiBase } from '../config';
|
||||
import { openUrl } from '@tauri-apps/plugin-opener';
|
||||
|
||||
// 当前版本:发布新版本时同步 package.json 的 version
|
||||
export const APP_VERSION = '0.1.0';
|
||||
|
||||
export async function checkForUpdate() {
|
||||
try {
|
||||
const res = await fetch(`${getApiBase()}/api/update/check`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ current: APP_VERSION }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data && data.ok) {
|
||||
return {
|
||||
current: APP_VERSION,
|
||||
latest: data.latest_version,
|
||||
available: !!data.update_available,
|
||||
url: `${getApiBase()}${data.download_url || '/download'}`,
|
||||
name: data.installer_name || '',
|
||||
};
|
||||
}
|
||||
} catch { /* 后端不可达时忽略 */ }
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 打开安装包下载地址(优先系统浏览器下载)。 */
|
||||
export function openUpdate(url) {
|
||||
try {
|
||||
openUrl(url);
|
||||
} catch {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user