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" >↻ +
软件更新
- 当前版本: {{ versionText }} -
-
{{ updateHint }}
-
-
@@ -64,16 +70,15 @@ import { computed, onMounted, ref, watch } from 'vue' import { restoreLoginUser } from '@/shared/auth/ensure-auth' import { getCurrentUserAppColumnRaw, type PermissionMenuItem } from '@/shared/api/permission' -import { getPywebviewApi, isDesktopRuntime } from '@/shared/bridges/pywebview' +import { isDesktopRuntime } from '@/shared/bridges/pywebview' +import { useVersionUpdate } from '@/shared/composables/useVersionUpdate' import { resolvePageHref } from '@/shared/page-prefix' const username = ref('') -const versionText = ref('—') const updatePanel = ref(false) -const updateHint = ref('') -const updateReady = ref(false) -const updateFileUrl = ref('') const toastText = ref('') +const { checking, updating, currentVersion, hasUpdate, canDownload, hint, checked, runCheck, doUpdate } = + useVersionUpdate() // 桌面端原 Flask /logout 已随瘦身下线:统一跳登录页并清本地 token(/login?logout=1) const logoutHref = computed(() => `${resolvePageHref('/new_web_source/login.html')}?logout=1`) @@ -162,69 +167,21 @@ async function loadCurrentUser() { function toggleUpdatePanel() { updatePanel.value = !updatePanel.value -} - -async function fetchVersion() { - try { - const resp = await window.fetch('/api/version', { credentials: 'same-origin' }) - const data = await resp.json().catch(() => ({})) - const current = String((data && data.version) || '').replace(/^v/i, '') - if (current) versionText.value = current || '—' - return data || {} - } catch { - return {} - } -} - -async function checkUpdate() { - updateHint.value = '正在检测更新...' - updateReady.value = false - try { - const data = await fetchVersion() - if (data.has_update && data.latest_version) { - updateHint.value = - `发现新版本 v${String(data.latest_version).replace(/^v/i, '')}` + - (data.desc ? `:${data.desc}` : '') - if (data.file_url) { - updateReady.value = true - updateFileUrl.value = data.file_url - } else { - updateHint.value = '暂无下载地址,请关注官方渠道。' - } - } else { - updateHint.value = '已是最新版本' - } - } catch { - updateHint.value = '检测更新失败' - } -} - -async function doUpdate() { - if (!updateFileUrl.value) return - if (!window.confirm('有更新,是否现在更新?\n更新将下载安装包并重启程序。')) return - updateHint.value = '正在下载并准备更新,程序将自动退出...' - updateReady.value = false - try { - // 版本更新是桌面客户端专属动作:经浏览器桥调用本地 update 下载/启动(服务端化后无 /api/update/do) - const bridge = getPywebviewApi() - const result = bridge?.do_update_app ? await bridge.do_update_app(updateFileUrl.value) : undefined - updateHint.value = result?.success - ? '更新已启动,程序即将退出...' - : (result?.error || '当前环境不支持自动更新') - } catch { - updateHint.value = '请求更新失败,请重试' + // 首次展开面板即触发一次检测(版本逻辑在 shared/composables/useVersionUpdate) + if (updatePanel.value && !checked.value && !checking.value) { + void runCheck() } } onMounted(() => { void loadCurrentUser() void loadPermissions() - // 版本检测/更新仅桌面客户端(本地 Flask image 蓝图)提供;桌面桥注入后立即拉取 + // 版本检测/更新仅桌面客户端:桌面桥注入后立即检测一次(逻辑见 useVersionUpdate) watch( isDesktopRuntime, (runtime) => { if (runtime) { - void fetchVersion().catch(() => undefined) + void runCheck() } }, { immediate: true }, @@ -247,7 +204,10 @@ body { } .home-root { + /* 背景直接落到根容器,避免依赖 body 被 SPA 全局样式覆盖导致图不显示 */ min-height: 100vh; + background: linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5)), + url("/bg.jpg") center/cover no-repeat; display: flex; flex-direction: column; align-items: center; @@ -316,6 +276,21 @@ body { background: rgba(102, 126, 234, 0.2); } +.header-update-text { + padding: 5px 12px; + font-size: 13px; + color: #4c5bd4; + background: rgba(102, 126, 234, 0.12); + border: 1px solid rgba(102, 126, 234, 0.35); + border-radius: 14px; + cursor: pointer; + white-space: nowrap; +} + +.header-update-text:hover { + background: rgba(102, 126, 234, 0.2); +} + .header-update-panel { position: absolute; top: 44px; diff --git a/frontend-vue/src/pages/login/DesktopLoginPage.vue b/frontend-vue/src/pages/login/DesktopLoginPage.vue index cf549409..e00010a2 100644 --- a/frontend-vue/src/pages/login/DesktopLoginPage.vue +++ b/frontend-vue/src/pages/login/DesktopLoginPage.vue @@ -43,29 +43,140 @@ + +
+ + +
+ + +
+ +
+
软件更新
+
当前版本{{ 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, + } +}