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:
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* 桌面端「更新包下载进度」的解析与文案(纯函数,便于 node --test 直接覆盖)。
|
||||
*
|
||||
* 数据源:Python 侧 do_update_app 通过 evaluate_js 推 pywebview-update-progress 事件
|
||||
* (与 pywebview-download-progress 同一约定),detail 结构见下:
|
||||
* { status, message, downloaded, total, percent, error }
|
||||
* percent = -1 表示总大小未知(服务端未返回 Content-Length),进度条走不确定态。
|
||||
*/
|
||||
|
||||
export type UpdateProgressStatus = 'downloading' | 'verifying' | 'ready' | 'failed'
|
||||
|
||||
export type UpdateProgress = {
|
||||
status: UpdateProgressStatus
|
||||
message: string
|
||||
downloaded: number
|
||||
total: number
|
||||
/** -1 表示总大小未知,进度条走不确定态 */
|
||||
percent: number
|
||||
}
|
||||
|
||||
export type UpdateProgressDetail = {
|
||||
status?: string
|
||||
message?: string
|
||||
downloaded?: number
|
||||
total?: number
|
||||
percent?: number
|
||||
error?: string
|
||||
}
|
||||
|
||||
const KNOWN_STATUS: readonly string[] = ['downloading', 'verifying', 'ready', 'failed']
|
||||
|
||||
export function isUpdateProgressStatus(value: unknown): value is UpdateProgressStatus {
|
||||
return typeof value === 'string' && KNOWN_STATUS.includes(value)
|
||||
}
|
||||
|
||||
function toByteCount(value: unknown): number {
|
||||
const num = Number(value)
|
||||
return Number.isFinite(num) && num > 0 ? num : 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析进度事件 detail;状态缺失/非法返回 null,
|
||||
* 让调用方保持原状态而不是被脏事件打回初始态。
|
||||
*/
|
||||
export function normalizeUpdateProgress(detail: unknown): UpdateProgress | null {
|
||||
if (!detail || typeof detail !== 'object') return null
|
||||
const raw = detail as UpdateProgressDetail
|
||||
if (!isUpdateProgressStatus(raw.status)) return null
|
||||
const percent = Number(raw.percent)
|
||||
return {
|
||||
status: raw.status,
|
||||
message: String(raw.message ?? ''),
|
||||
downloaded: toByteCount(raw.downloaded),
|
||||
total: toByteCount(raw.total),
|
||||
percent: Number.isFinite(percent) ? percent : -1,
|
||||
}
|
||||
}
|
||||
|
||||
/** 进度条宽度百分比(0-100 取整);总大小未知或非下载态返回 -1(不确定态) */
|
||||
export function updateProgressPercent(progress: UpdateProgress | null): number {
|
||||
if (!progress || !Number.isFinite(progress.percent) || progress.percent < 0) return -1
|
||||
return Math.max(0, Math.min(100, Math.round(progress.percent)))
|
||||
}
|
||||
|
||||
export function formatBytes(value: number): string {
|
||||
if (!Number.isFinite(value) || value <= 0) return '0 MB'
|
||||
if (value < 1024 * 1024) return `${(value / 1024).toFixed(0)} KB`
|
||||
return `${(value / 1024 / 1024).toFixed(1)} MB`
|
||||
}
|
||||
|
||||
/**
|
||||
* 进度条文案:下载中给"百分比 + 已下载/总大小",
|
||||
* 其余状态(校验中/已就绪/失败)直接用 Python 侧的中文说明。
|
||||
*/
|
||||
export function formatUpdateProgressText(progress: UpdateProgress | null): string {
|
||||
if (!progress) return ''
|
||||
if (progress.status !== 'downloading') return progress.message
|
||||
if (progress.percent < 0) {
|
||||
// 进度帧还没到(刚开始)或总大小未知:能算出已下载量就显示,否则退回阶段文案
|
||||
return progress.downloaded > 0
|
||||
? `正在下载更新包(已下载 ${formatBytes(progress.downloaded)})`
|
||||
: (progress.message || '正在下载更新包...')
|
||||
}
|
||||
const size = progress.total > 0
|
||||
? `(${formatBytes(progress.downloaded)}/${formatBytes(progress.total)})`
|
||||
: ''
|
||||
return `正在下载更新包 ${Math.round(progress.percent)}%${size}`
|
||||
}
|
||||
Reference in New Issue
Block a user