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() {
{/*
*/}
-
-
-
+ 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 && (
+
+ )}
+ >
+ );
+}
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');
+ }
+}