粘贴图根修(Ctrl+V直读系统剪贴板,不再依赖不可靠的paste事件)/SFTP串台双刀(拨号期禁回退本机+盘符路径自动回根)/PDF导出目录设置与文案带落盘路径/性能批(连接保存下载防重/md渲染防抖+预热/hljs按需加载/审计锁外置/快捷方式解析超时/webview-watch可停)/前端62类型错清零(含keyPath蛇形字段炸弹)/目录图标字段错位修复
323 lines
9.0 KiB
TypeScript
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
|
|
}
|