feat(客户端更新): 登录页/首页更新面板显示更新包下载进度条

- 新增 shared/utils/update-progress.ts:进度事件解析/百分比钳制/文案格式化(纯函数)
- 新增 shared/components/UpdateProgressBar.vue:两页共用,失败态琥珀色并停在断点
  百分比、总大小未知走不确定态
- useVersionUpdate:先挂监听再发桥调用(避免丢首帧),监听 pywebview-update-progress
  (Python 侧 evaluate_js 推送,同 pywebview-download-progress 约定);失败保留上次进度
- 测试:tests/update-progress.test.ts(8 用例)

兼容:老客户端没有该事件源,进度条不出现,其余行为不变(优雅降级)。
注:本仓库 modules-withdraw / modules-image-video 有 3 个既存失败用例(隔离复现),
与本次改动无关;全仓库仅新增测试引用本次新增模块。
This commit is contained in:
2026-09-11 01:59:42 +08:00
parent 47b2758fe8
commit 94a0c287e0
6 changed files with 361 additions and 5 deletions
@@ -6,8 +6,31 @@
* 提供);旧客户端无此桥时降级:本机版本为空 → 只要线上有包就允许手动更新。
*/
import { ref } from 'vue'
import { computed, ref } from 'vue'
import { getPywebviewApi } from '@/shared/bridges/pywebview'
import {
normalizeUpdateProgress,
type UpdateProgress,
} from '@/shared/utils/update-progress'
export type { UpdateProgress, UpdateProgressStatus } from '@/shared/utils/update-progress'
const updateProgress = ref<UpdateProgress | null>(null)
let progressListenerBound = false
/**
* 绑定一次全局监听:下载进度由 Python 侧主动推事件(同 pywebview-download-progress 约定),
* JS 侧不轮询——桥调用虽然并发安全,但轮询会给主进程平白加请求。
*/
function bindProgressListener() {
if (progressListenerBound || typeof window === 'undefined') return
progressListenerBound = true
window.addEventListener('pywebview-update-progress', (event) => {
const progress = normalizeUpdateProgress((event as CustomEvent<unknown>).detail)
if (!progress) return
updateProgress.value = progress
})
}
function normVersion(value: unknown): string {
return String(value ?? '').trim().replace(/^[vV]/, '')
@@ -124,19 +147,57 @@ export function useVersionUpdate() {
updating.value = false
return
}
// 先挂监听再发桥调用:进度事件在下载开始后立刻开推,晚绑会丢最前面的进度
bindProgressListener()
updateProgress.value = {
status: 'downloading',
message: '正在准备下载更新包...',
downloaded: 0,
total: 0,
percent: -1,
}
hint.value = '正在下载并准备更新,程序将自动退出...'
try {
const result = await doUpdateApp!(fileUrl.value)
hint.value = result?.success
? '更新已启动,程序即将退出...'
: (result?.error || '更新启动失败,请重试')
if (result?.success) {
const last = updateProgress.value
updateProgress.value = {
status: 'ready',
message: '安装包已就绪,正在启动更新程序,程序即将退出...',
downloaded: last?.downloaded || 0,
total: last?.total || 0,
percent: 100,
}
hint.value = '更新已启动,程序即将退出...'
} else {
// 失败:进度条停在断点处,重试将从断点继续(Python 侧保留断点文件)
const message = result?.error || '更新启动失败,请重试'
updateProgress.value = {
status: 'failed',
message,
downloaded: updateProgress.value?.downloaded || 0,
total: updateProgress.value?.total || 0,
percent: updateProgress.value?.percent ?? -1,
}
hint.value = message
}
} catch {
updateProgress.value = {
status: 'failed',
message: '请求更新失败,请重试',
downloaded: updateProgress.value?.downloaded || 0,
total: updateProgress.value?.total || 0,
percent: updateProgress.value?.percent ?? -1,
}
hint.value = '请求更新失败,请重试'
} finally {
updating.value = false
}
}
// 进度条的百分比/文案由 UpdateProgressBar 组件从 progress 自行派生(见 shared/utils/update-progress
const progress = computed(() => updateProgress.value)
return {
checking,
updating,
@@ -147,6 +208,7 @@ export function useVersionUpdate() {
canDownload,
hint,
checked,
progress,
loadCurrentVersion,
runCheck,
doUpdate,