diff --git a/frontend-vue/src/pages/home/DesktopHomePage.vue b/frontend-vue/src/pages/home/DesktopHomePage.vue
index f7ce2cbd..ababb6bc 100644
--- a/frontend-vue/src/pages/home/DesktopHomePage.vue
+++ b/frontend-vue/src/pages/home/DesktopHomePage.vue
@@ -10,18 +10,24 @@
title="检测更新"
@click="toggleUpdatePanel"
>↻
+
+
+
+
+
+
+
+
+
+
+
+
软件更新
+
当前版本{{ currentVersion || '—' }}
+
最新版本{{ latestVersion || '—' }}
+
+
+
+
+
{{ hint }}
+
+
@@ -233,6 +409,7 @@ body {
}
.login-box {
+ position: relative;
background: #fff;
border: 1px solid #b8d4e3;
border-radius: 12px;
@@ -332,6 +509,32 @@ body {
text-align: center;
}
+/* 记住密码 / 自动登录 */
+.login-options {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16px;
+ margin: -4px 0 16px;
+}
+
+.check-label {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 13px;
+ color: #555;
+ cursor: pointer;
+ user-select: none;
+}
+
+.check-input {
+ width: 15px;
+ height: 15px;
+ accent-color: #3498db;
+ cursor: pointer;
+}
+
.btn-login {
width: 100%;
padding: 14px;
@@ -353,4 +556,104 @@ body {
opacity: 0.6;
cursor: not-allowed;
}
+
+/* 版本更新入口与面板 */
+.login-update {
+ margin-top: 14px;
+ text-align: center;
+}
+
+.link-update {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ padding: 4px 8px;
+ font-size: 13px;
+ color: #71808d;
+ background: transparent;
+ border: 1px solid transparent;
+ border-radius: 8px;
+ cursor: pointer;
+}
+
+.link-update svg {
+ width: 14px;
+ height: 14px;
+}
+
+.link-update:hover {
+ color: #3498db;
+ background: #f1f7fb;
+ border-color: #d5e6f2;
+}
+
+.update-panel {
+ margin-top: 10px;
+ padding: 12px 14px;
+ background: #f6fafd;
+ border: 1px solid #d5e6f2;
+ border-radius: 8px;
+ text-align: left;
+}
+
+.update-panel-title {
+ font-size: 13px;
+ font-weight: 600;
+ color: #333;
+ margin-bottom: 8px;
+}
+
+.update-row {
+ display: flex;
+ justify-content: space-between;
+ gap: 12px;
+ font-size: 13px;
+ color: #555;
+ line-height: 1.7;
+}
+
+.update-row b {
+ font-weight: 600;
+ color: #333;
+}
+
+.update-actions {
+ display: flex;
+ gap: 10px;
+ margin-top: 10px;
+}
+
+.btn-mini {
+ padding: 5px 12px;
+ font-size: 13px;
+ color: #fff;
+ background: #3498db;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+}
+
+.btn-mini:hover:not(:disabled) {
+ opacity: 0.9;
+}
+
+.btn-mini:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+.btn-mini-green {
+ background: #28a745;
+}
+
+.btn-mini-green:hover:not(:disabled) {
+ opacity: 0.9;
+}
+
+.update-hint {
+ font-size: 12px;
+ color: #666;
+ margin-top: 8px;
+ min-height: 16px;
+}
diff --git a/frontend-vue/src/shared/bridges/pywebview.ts b/frontend-vue/src/shared/bridges/pywebview.ts
index 5555c772..b2c8e8e7 100644
--- a/frontend-vue/src/shared/bridges/pywebview.ts
+++ b/frontend-vue/src/shared/bridges/pywebview.ts
@@ -135,6 +135,8 @@ export interface PywebviewApi {
open_external_url?: (url: string) => Promise<{ success: boolean; error?: string }>;
/** 版本更新(桌面专属):下载更新包并启动 update.exe 后退出主程序(原 Flask /api/update/do 桥化) */
do_update_app?: (fileUrl: string) => Promise<{ success: boolean; error?: string }>;
+ /** 桌面客户端本地版本号(与 /api/version/latest 比较检测更新;新版客户端提供) */
+ get_app_version?: () => Promise<{ success: boolean; version?: string; error?: string }>;
launch_yaoayanui?: () => Promise<{
success: boolean;
path?: string;
diff --git a/frontend-vue/src/shared/composables/useVersionUpdate.ts b/frontend-vue/src/shared/composables/useVersionUpdate.ts
new file mode 100644
index 00000000..5420b465
--- /dev/null
+++ b/frontend-vue/src/shared/composables/useVersionUpdate.ts
@@ -0,0 +1,144 @@
+/**
+ * 版本检测 / 更新共享逻辑(登录页、首页共用)
+ *
+ * 服务端化后桌面页面直载线上 Web:检测"是否有新版"需把线上 /api/version/latest
+ * 与"本机 exe 版本"比较。本机版本经 pywebview 桥 get_app_version() 获取(新版客户端
+ * 提供);旧客户端无此桥时降级:本机版本为空 → 只要线上有包就允许手动更新。
+ */
+
+import { ref } from 'vue'
+import { getPywebviewApi } from '@/shared/bridges/pywebview'
+
+function normVersion(value: unknown): string {
+ return String(value ?? '').trim().replace(/^[vV]/, '')
+}
+
+async function fetchJson(url: string): Promise> {
+ const resp = await window.fetch(url, { credentials: 'same-origin' })
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
+ const text = await resp.text()
+ if (!text) return {}
+ try {
+ return JSON.parse(text) as Record
+ } catch {
+ return {}
+ }
+}
+
+/**
+ * 读取本机客户端版本号:优先桌面桥;无桥(旧版 exe / 浏览器环境)返回空串。
+ */
+export async function resolveLocalVersion(): Promise {
+ const bridge = getPywebviewApi()
+ if (bridge?.get_app_version) {
+ try {
+ const res = await bridge.get_app_version()
+ if (res?.success && res.version) {
+ return normVersion(res.version)
+ }
+ } catch {
+ /* 忽略,走空版本降级 */
+ }
+ }
+ return ''
+}
+
+/** 拉取线上最新客户端版本(公开接口,无需登录;web_config 无记录时 version 为空)。 */
+export async function fetchLatestVersion(): Promise<{ version: string; fileUrl: string }> {
+ const data = await fetchJson('/api/version/latest')
+ return { version: normVersion(data.version), fileUrl: String(data.file_url ?? '') }
+}
+
+export function useVersionUpdate() {
+ const checking = ref(false)
+ const updating = ref(false)
+ const currentVersion = ref('')
+ const latestVersion = ref('')
+ const fileUrl = ref('')
+ const hasUpdate = ref(false)
+ /** 本地版本未知但线上有包时也允许"立即更新",此标志标记可下载 */
+ const canDownload = ref(false)
+ const hint = ref('')
+ const checked = ref(false)
+
+ async function loadCurrentVersion() {
+ if (currentVersion.value) return
+ currentVersion.value = await resolveLocalVersion()
+ }
+
+ async function runCheck() {
+ if (checking.value) return
+ checking.value = true
+ hint.value = '正在检测更新...'
+ try {
+ const local = await resolveLocalVersion()
+ currentVersion.value = local
+ const latest = await fetchLatestVersion()
+ latestVersion.value = latest.version
+ fileUrl.value = latest.fileUrl
+ checked.value = true
+
+ if (!latest.version) {
+ hasUpdate.value = false
+ canDownload.value = false
+ hint.value = '暂无版本更新信息'
+ return
+ }
+ if (!local) {
+ // 旧客户端无本机版本桥:无法精确比较,线上有包即可手动更新
+ hasUpdate.value = false
+ canDownload.value = Boolean(latest.fileUrl)
+ hint.value = latest.fileUrl
+ ? `线上最新版本 v${latest.version},可点击下方按钮手动更新`
+ : `线上最新版本 v${latest.version},暂无下载地址`
+ return
+ }
+ hasUpdate.value = local !== latest.version
+ canDownload.value = hasUpdate.value && Boolean(latest.fileUrl)
+ hint.value = hasUpdate.value
+ ? `发现新版本 v${latest.version}${latest.fileUrl ? '' : ',暂无下载地址'}`
+ : `已是最新版本 v${local}`
+ } catch {
+ hasUpdate.value = false
+ canDownload.value = false
+ hint.value = '检测更新失败,请稍后重试'
+ } finally {
+ checking.value = false
+ }
+ }
+
+ async function doUpdate() {
+ if (updating.value || !fileUrl.value) return
+ if (!window.confirm('有更新,是否现在更新?\n更新将下载安装包并重启程序。')) return
+ updating.value = true
+ hint.value = '正在下载并准备更新,程序将自动退出...'
+ try {
+ const bridge = getPywebviewApi()
+ const result = bridge?.do_update_app
+ ? await bridge.do_update_app(fileUrl.value)
+ : undefined
+ hint.value = result?.success
+ ? '更新已启动,程序即将退出...'
+ : (result?.error || '当前环境不支持自动更新')
+ } catch {
+ hint.value = '请求更新失败,请重试'
+ } finally {
+ updating.value = false
+ }
+ }
+
+ return {
+ checking,
+ updating,
+ currentVersion,
+ latestVersion,
+ fileUrl,
+ hasUpdate,
+ canDownload,
+ hint,
+ checked,
+ loadCurrentVersion,
+ runCheck,
+ doUpdate,
+ }
+}