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
+40 -65
View File
@@ -10,18 +10,24 @@
title="检测更新"
@click="toggleUpdatePanel"
></button>
<button
v-if="isDesktopRuntime"
type="button"
class="header-update-text"
@click="toggleUpdatePanel"
>更新版本</button>
<div class="header-update-panel" v-show="updatePanel">
<div class="header-update-title"><span></span> 软件更新</div>
<div class="update-block">
<span class="update-version-text">当前版本: {{ versionText }}</span>
<button type="button" class="btn-check-update" @click="checkUpdate">
<span></span> 检测
<span class="update-version-text">当前版本: {{ currentVersion || '未知' }}</span>
<button type="button" class="btn-check-update" :disabled="checking" @click="runCheck">
<span></span> {{ checking ? '检测中...' : '检测' }}
</button>
</div>
<div class="update-hint">{{ updateHint }}</div>
<div style="margin-top: 4px;">
<button type="button" class="btn-download-update" v-if="updateReady" @click="doUpdate">
立即更新
<div class="update-hint">{{ hint }}</div>
<div v-if="hasUpdate || canDownload" style="margin-top: 4px;">
<button type="button" class="btn-download-update" :disabled="updating" @click="doUpdate">
{{ updating ? '更新中...' : '立即更新' }}
</button>
</div>
</div>
@@ -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;