From 7c1c13526e59b970f21431483eb65a129d465f24 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com>
Date: Tue, 8 Sep 2026 22:30:50 +0800
Subject: [PATCH] =?UTF-8?q?feat(web):=20=E6=A1=8C=E9=9D=A2=E7=99=BB?=
=?UTF-8?q?=E5=BD=95=E9=A1=B5=E8=AE=B0=E4=BD=8F=E5=AF=86=E7=A0=81/?=
=?UTF-8?q?=E8=87=AA=E5=8A=A8=E7=99=BB=E5=BD=95=E4=B8=8E=E7=89=88=E6=9C=AC?=
=?UTF-8?q?=E6=9B=B4=E6=96=B0=EF=BC=8C=E9=A6=96=E9=A1=B5=E7=89=88=E6=9C=AC?=
=?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=85=A5=E5=8F=A3=E4=B8=8E=E8=83=8C=E6=99=AF?=
=?UTF-8?q?=E4=BF=AE=E5=A4=8D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 登录页(仅桌面端)新增"记住密码/自动登录"勾选与"更新版本"按钮:登录成功持久化凭据
(base64 存桌面 localStorage),打开登录页恢复勾选并静默登录;显式登出/切号后关闭自动登录
- 新增 shared/composables/useVersionUpdate 统一版本检测:/api/version/latest 与本机版本比较,
通过 pywebview 桥 get_app_version 读本地 exe 版本,缺桥时降级为可手动更新
- 首页保留 ↻ 图标并加显式"更新版本"按钮,接入新检测逻辑
- 首页背景改画到 .home-root,避免被 SPA 全局样式时序覆盖导致图不显示
---
.../src/pages/home/DesktopHomePage.vue | 105 +++---
.../src/pages/login/DesktopLoginPage.vue | 305 +++++++++++++++++-
frontend-vue/src/shared/bridges/pywebview.ts | 2 +
.../shared/composables/useVersionUpdate.ts | 144 +++++++++
4 files changed, 490 insertions(+), 66 deletions(-)
create mode 100644 frontend-vue/src/shared/composables/useVersionUpdate.ts
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,
+ }
+}