diff --git a/backend/app/main.py b/backend/app/main.py index ce06dd8..f5e659e 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -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) diff --git a/backend/云超服昆创园OPC运营中心_0.1.0_x64-setup.exe b/backend/云超服昆创园OPC运营中心_0.1.0_x64-setup.exe new file mode 100644 index 0000000..bdf8904 Binary files /dev/null and b/backend/云超服昆创园OPC运营中心_0.1.0_x64-setup.exe differ diff --git a/src/components/PageHeader.jsx b/src/components/PageHeader.jsx index 46dbb48..47db753 100644 --- a/src/components/PageHeader.jsx +++ b/src/components/PageHeader.jsx @@ -141,9 +141,19 @@ export default function PageHeader() { {/* Logo */} - - - + window.dispatchEvent(new CustomEvent('dpm:check-update'))} + onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') window.dispatchEvent(new CustomEvent('dpm:check-update')); }} + > + + + +

昆明市大学生创业园 · OPC 智能园区数字运营中心

云南省首家政府主办大学生创业孵化园区 · 空间+孵化+融资+政策+资源+AI赋能+综合服务
diff --git a/src/components/ScreenLayout.jsx b/src/components/ScreenLayout.jsx index b1e73e4..0f284f0 100644 --- a/src/components/ScreenLayout.jsx +++ b/src/components/ScreenLayout.jsx @@ -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() { +
); } diff --git a/src/components/UpdateNotify.jsx b/src/components/UpdateNotify.jsx new file mode 100644 index 0000000..a2004c0 --- /dev/null +++ b/src/components/UpdateNotify.jsx @@ -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 && ( +
+
+
+ 发现新版本 v{upd.latest} + 当前 v{upd.current} · 点击下载更新安装包(Windows) +
+ + +
+ )} + {msg && ( +
+
+
{msg}
+
+ )} + + ); +} diff --git a/src/styles/datascreen.css b/src/styles/datascreen.css index c1d9918..70959c7 100644 --- a/src/styles/datascreen.css +++ b/src/styles/datascreen.css @@ -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; diff --git a/src/utils/updateCheck.js b/src/utils/updateCheck.js new file mode 100644 index 0000000..63fa397 --- /dev/null +++ b/src/utils/updateCheck.js @@ -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'); + } +}