Files
u-desk/frontend/src/stores/update.ts
T
lxy 75a42c0b6a 修复: 粘贴图失效与SFTP串台回退等一批问题
粘贴图根修(Ctrl+V直读系统剪贴板,不再依赖不可靠的paste事件)/SFTP串台双刀(拨号期禁回退本机+盘符路径自动回根)/PDF导出目录设置与文案带落盘路径/性能批(连接保存下载防重/md渲染防抖+预热/hljs按需加载/审计锁外置/快捷方式解析超时/webview-watch可停)/前端62类型错清零(含keyPath蛇形字段炸弹)/目录图标字段错位修复
2026-10-24 13:48:54 +08:00

323 lines
9.0 KiB
TypeScript

import { defineStore } from 'pinia'
import { ref } from 'vue'
import { Message } from '@arco-design/web-vue'
import { formatBytes as formatFileSize } from '@/utils/fileUtils'
import { parseWailsEventData } from '@/utils/wailsEvent'
import {
CheckUpdate, GetUpdateConfig, DownloadUpdate, InstallUpdate, InstallUpdateWithHash
} from '../wailsjs/v3-bindings/u-desk/app'
import { Events } from '@wailsio/runtime'
const { On, Off } = Events
// 本次应用运行内已自动弹过的版本(升级后自动恢复提醒)
let sessionNotifiedVersion = ''
// 下载完成事件携带的远端声明哈希,安装时用于完整性校验
let lastExpectedSha256 = ''
/**
* 更新管理 Store
* 统一管理版本检查、下载、安装等更新相关逻辑
*/
export const useUpdateStore = defineStore('update', () => {
// ==================== 状态 ====================
const updateInfo = ref<UpdateInfo | null>(null)
const showUpdate = ref(false)
const checking = ref(false)
const downloading = ref(false)
const installing = ref(false)
const downloadProgress = ref(0)
const downloadStatus = ref<'active' | 'exception' | 'success'>('active')
const progressInfo = ref({
speed: 0,
downloaded: 0,
total: 0
})
// 节流:防止过度更新
let lastUpdateTime = 0
const UPDATE_THROTTLE = 100 // 100ms 最小更新间隔
// 最小显示时间:确保进度条至少显示 5 秒
let downloadStartTime = 0
const MIN_DISPLAY_TIME = 5000 // 5 秒最小显示时间
// ==================== 工具函数 ====================
const formatSpeed = (bytesPerSecond: number): string => {
return formatFileSize(bytesPerSecond) + '/s'
}
// ==================== 核心方法 ====================
// 用户显式跳过的版本(localStorage):该版本不再自动弹窗,手动检查不受限
const SKIPPED_VERSION_KEY = 'udesk-update-skipped-version'
/**
* 展示更新弹窗。自动路径(静默检查/后端广播)按版本去重(用户已跳过或本次运行已弹过则不弹),手动检查强制弹
*/
const tryShowUpdate = (data: UpdateInfo, force = false) => {
const version = String(data?.latest_version || '')
if (!force && version) {
if (localStorage.getItem(SKIPPED_VERSION_KEY) === version) return
if (sessionNotifiedVersion === version) return
}
updateInfo.value = data
showUpdate.value = true
if (version) {
sessionNotifiedVersion = version
}
}
/**
* 稍后提醒:记录用户显式跳过的版本,该版本不再自动弹窗(手动检查仍强制弹)
*/
const skipVersion = (version: string) => {
if (version) {
localStorage.setItem(SKIPPED_VERSION_KEY, version)
}
showUpdate.value = false
}
/**
* 检查更新
* @param silent 是否静默模式(不显示消息)
* @param manual 是否手动触发(跳过 auto_check_enabled 门禁)
* @returns 检查是否实际完成(false=后端未就绪或网络失败,调用方可重试)
*/
const checkForUpdates = async (silent = false, manual = false): Promise<boolean> => {
if (checking.value) return true
checking.value = true
try {
const configResult = await GetUpdateConfig()
if (!configResult.success) return false
const { auto_check_enabled } = configResult.data || {}
if (!manual && !auto_check_enabled) return true
const result = await CheckUpdate()
if (result.success && result.data?.has_update) {
tryShowUpdate(result.data, !silent)
if (!silent) {
Message.success('发现新版本!')
}
} else if (!silent) {
Message.success('已是最新版本')
}
return result.success === true
} catch (error) {
if (!silent) {
console.error('检查更新失败:', error)
Message.error('检查更新失败:' + (error as Error).message)
}
return false
} finally {
checking.value = false
}
}
/**
* 下载更新
*/
const downloadUpdate = async () => {
const url = updateInfo.value?.download_url
if (!url) {
Message.warning('下载地址不存在')
return
}
// 记录开始时间
downloadStartTime = Date.now()
// 重置下载状态
downloading.value = true
downloadProgress.value = 1 // 设置为 1 而不是 0,确保进度条显示
downloadStatus.value = 'active'
progressInfo.value = { speed: 0, downloaded: 0, total: 0 }
try {
const result = await DownloadUpdate(url)
if (!result.success) {
downloadStatus.value = 'exception'
downloading.value = false
Message.error(result.message || '下载启动失败')
return
}
Message.success('下载请求已发送,等待后端发送进度事件...')
} catch (error) {
console.error('下载失败:', error)
downloadStatus.value = 'exception'
downloading.value = false
Message.error('下载失败:' + (error as Error).message)
}
}
/**
* 安装更新
*/
const installUpdate = async (filePath: string) => {
if (!filePath) {
Message.warning('请先下载更新包')
return
}
installing.value = true
try {
// 优先带哈希校验安装(期望哈希来自下载完成事件的远端声明)
const result = lastExpectedSha256
? await InstallUpdateWithHash(filePath, true, lastExpectedSha256, 'sha256')
: await InstallUpdate(filePath, true)
if (result.success) {
Message.success({
content: '安装成功!应用将在几秒后重启...',
duration: 3000
})
return
}
Message.error(result.message || '安装失败')
} catch (error) {
Message.error('安装失败:' + (error as Error).message)
} finally {
installing.value = false
}
}
/**
* 下载进度处理
*/
const onDownloadProgress = (event: unknown) => {
const now = Date.now()
if (now - lastUpdateTime < UPDATE_THROTTLE) {
return
}
lastUpdateTime = now
const data = parseWailsEventData(event)
progressInfo.value = {
speed: (data.speed as number) || 0,
downloaded: (data.downloaded as number) || 0,
total: (data.total as number) || 0
}
const rawProgress = Number(data.progress) || 0
const safeProgress = Math.min(100, Math.max(0, Math.round(rawProgress)))
downloadProgress.value = safeProgress
}
/**
* 下载完成处理
*/
const onDownloadComplete = (event: unknown) => {
const data = parseWailsEventData(event)
// 错误处理
if (data.error) {
console.error('下载失败:', data.error)
downloadStatus.value = 'exception'
downloading.value = false
Message.error('下载失败:' + data.error)
return
}
// 数据验证
if (!data.success || !data.file_path) {
console.error('下载数据不完整:', data)
downloadStatus.value = 'exception'
downloading.value = false
Message.error('下载完成但数据不完整')
return
}
// 完成下载
downloadProgress.value = 100
downloadStatus.value = 'success'
// 记录远端声明的期望哈希,安装时用于校验
lastExpectedSha256 = String(data.expected_sha256 || '')
const fileSize = (data.file_size as number) || 0
progressInfo.value = {
speed: 0,
downloaded: fileSize,
total: fileSize
}
// 计算已经显示的时间
const elapsed = Date.now() - downloadStartTime
const remainingTime = Math.max(0, MIN_DISPLAY_TIME - elapsed)
// 确保进度条至少显示 3 秒
setTimeout(() => {
downloading.value = false // 安装前才关闭下载状态
installUpdate(data.file_path as string)
}, remainingTime)
}
/**
* 后端广播"发现新版本"处理(启动即查与周期检查命中时后端都会发)
*/
const onUpdateAvailable = (event: unknown) => {
const data = parseWailsEventData(event) as UpdateInfo
if (!data?.latest_version) {
return
}
tryShowUpdate(data)
}
/**
* 设置事件监听
*/
const setupEventListeners = () => {
On('download-progress', onDownloadProgress)
On('download-complete', onDownloadComplete)
On('update-available', onUpdateAvailable)
}
/**
* 移除事件监听
*/
const removeEventListeners = () => {
Off('download-progress')
Off('download-complete')
Off('update-available')
}
// ==================== 返回 ====================
return {
// 状态
updateInfo,
showUpdate,
checking,
downloading,
installing,
downloadProgress,
downloadStatus,
progressInfo,
// 方法
checkForUpdates,
downloadUpdate,
installUpdate,
skipVersion,
setupEventListeners,
removeEventListeners,
formatFileSize,
formatSpeed
}
})
// ==================== 类型定义 ====================
interface UpdateInfo {
has_update: boolean
current_version: string
latest_version: string
download_url: string
changelog: string
force_update: boolean
release_date: string
file_size: number
}