feat(web): 桌面登录页记住密码/自动登录与版本更新,首页版本更新入口与背景修复

- 登录页(仅桌面端)新增"记住密码/自动登录"勾选与"更新版本"按钮:登录成功持久化凭据
  (base64 存桌面 localStorage),打开登录页恢复勾选并静默登录;显式登出/切号后关闭自动登录
- 新增 shared/composables/useVersionUpdate 统一版本检测:/api/version/latest 与本机版本比较,
  通过 pywebview 桥 get_app_version 读本地 exe 版本,缺桥时降级为可手动更新
- 首页保留 ↻ 图标并加显式"更新版本"按钮,接入新检测逻辑
- 首页背景改画到 .home-root,避免被 SPA 全局样式时序覆盖导致图不显示
This commit is contained in:
2026-09-08 22:30:50 +08:00
parent 15222be2b5
commit 7c1c13526e
4 changed files with 490 additions and 66 deletions
@@ -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<Record<string, unknown>> {
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<string, unknown>
} catch {
return {}
}
}
/**
* 读取本机客户端版本号:优先桌面桥;无桥(旧版 exe / 浏览器环境)返回空串。
*/
export async function resolveLocalVersion(): Promise<string> {
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,
}
}