- 更新链路: 检查记录远端SHA256,下载完成事件回传,安装时带哈希校验 - 续传: 服务器忽略Range返回200时截断重下防追加损坏,文件名去query - 定时器: 自动检查可重入,配置变更免重启生效,关闭即停,下载加互斥 - 前端: 支持跳过此版本并按版本去重,手动检查不受门禁限制,启动未就绪延时重试 - 更新面板: 新增自动检查开关/间隔/地址配置与进度条状态映射 - 版本历史: cache参数强制刷新与加载超时重试兜底 - 配置: 侧边栏分区全链路持久化,保存请求复用api类型防字段丢失 - 侧边栏: 区块定位改显式类名,不再依赖排序位置 - 绑定: 重新生成并与镜像目录同步
331 lines
9.8 KiB
Vue
331 lines
9.8 KiB
Vue
<template>
|
|
<!-- 升级提示弹窗 -->
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, ref, watch, onMounted, onUnmounted, h, nextTick } from 'vue'
|
|
import { Modal, Message, Progress, Button } from '@arco-design/web-vue'
|
|
import { useUpdateStore } from '../stores/update'
|
|
import { parseMarkdown, isMarkedReady, preloadMarked } from '../utils/markedExtensions'
|
|
import { sanitizeHtml } from '@/utils/fileUtils'
|
|
import { GetCurrentVersion } from '../wailsjs/v3-bindings/u-desk/app'
|
|
|
|
const props = defineProps({
|
|
modelValue: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
updateInfo: {
|
|
type: Object,
|
|
default: () => ({})
|
|
}
|
|
})
|
|
|
|
const emit = defineEmits(['update:modelValue'])
|
|
|
|
// 使用更新管理 store
|
|
const updateStore = useUpdateStore()
|
|
|
|
// 模态框实例
|
|
let confirmModalInstance = null
|
|
let progressModalInstance = null
|
|
|
|
// Computed
|
|
// 更新信息未带 current_version 时从后端兜底获取
|
|
const backendVersion = ref('')
|
|
const currentVersion = computed(() => props.updateInfo?.current_version || backendVersion.value)
|
|
const latestVersion = computed(() => props.updateInfo?.latest_version || '')
|
|
const changelog = computed(() => props.updateInfo?.changelog || '')
|
|
const forceUpdate = computed(() => props.updateInfo?.force_update || false)
|
|
|
|
// Watch
|
|
watch(() => props.modelValue, (val) => {
|
|
if (val) {
|
|
showUpdateModal()
|
|
} else {
|
|
closeModals()
|
|
}
|
|
})
|
|
|
|
// Utility functions
|
|
const formatDate = (dateStr) => {
|
|
if (!dateStr) return ''
|
|
try {
|
|
const date = new Date(dateStr)
|
|
if (!isNaN(date.getTime())) {
|
|
return date.toLocaleDateString('zh-CN', {
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit'
|
|
}).replace(/\//g, '-')
|
|
}
|
|
} catch {}
|
|
return dateStr
|
|
}
|
|
|
|
// 显示更新确认弹窗
|
|
const showUpdateModal = () => {
|
|
if (confirmModalInstance) return
|
|
|
|
confirmModalInstance = Modal.confirm({
|
|
title: forceUpdate.value ? '重要更新' : '发现新版本',
|
|
content: () => {
|
|
const elements = [
|
|
h('div', { style: { marginBottom: '8px' } }, [
|
|
h('span', { style: { fontSize: '13px', color: 'var(--color-text-2)' } }, '版本:'),
|
|
h('span', { style: { fontSize: '14px', color: 'var(--color-text-1)', marginLeft: '8px' } }, currentVersion.value),
|
|
h('span', { style: { fontSize: '14px', color: 'var(--color-text-2)', marginLeft: '12px', marginRight: '12px' } }, '→'),
|
|
h('span', { style: { fontSize: '16px', color: 'rgb(var(--primary-6))', fontWeight: '500' } }, latestVersion.value)
|
|
])
|
|
]
|
|
|
|
// 更新日志
|
|
if (changelog.value) {
|
|
const changelogHtml = (() => { try { if (isMarkedReady()) return sanitizeHtml(String(parseMarkdown(changelog.value))); preloadMarked(); return sanitizeHtml(changelog.value) } catch { return changelog.value } })()
|
|
elements.push(
|
|
h('div', { style: { marginBottom: '8px' } }, [
|
|
h('div', { style: { fontSize: '12px', color: 'var(--color-text-2)', marginBottom: '4px' } }, '更新内容:'),
|
|
h('div', {
|
|
style: {
|
|
fontSize: '12px',
|
|
color: 'var(--color-text-2)',
|
|
lineHeight: '1.6',
|
|
padding: '10px 12px',
|
|
background: 'var(--color-fill-1)',
|
|
borderRadius: '4px',
|
|
maxHeight: '240px',
|
|
overflowY: 'auto'
|
|
},
|
|
innerHTML: changelogHtml
|
|
})
|
|
])
|
|
)
|
|
}
|
|
|
|
// 发布日期和文件大小
|
|
const metadata = []
|
|
if (props.updateInfo?.release_date) {
|
|
metadata.push(formatDate(props.updateInfo.release_date))
|
|
}
|
|
if (props.updateInfo?.file_size) {
|
|
metadata.push(updateStore.formatFileSize(props.updateInfo.file_size))
|
|
}
|
|
if (metadata.length > 0) {
|
|
elements.push(
|
|
h('div', { style: { marginBottom: '4px', fontSize: '12px', color: 'var(--color-text-3)' } }, metadata.join(' · '))
|
|
)
|
|
}
|
|
|
|
// 强制更新提示
|
|
if (forceUpdate.value) {
|
|
elements.push(
|
|
h('div', {
|
|
style: {
|
|
marginTop: '12px',
|
|
padding: '12px',
|
|
background: 'var(--color-danger-light-1)',
|
|
border: '1px solid var(--color-danger-light-3)',
|
|
borderRadius: '4px',
|
|
color: 'rgb(var(--danger-6))',
|
|
fontSize: '13px'
|
|
}
|
|
}, '⚠️ 此版本包含重要的安全更新和问题修复,为保障正常使用,请完成更新后再继续。')
|
|
)
|
|
}
|
|
|
|
// 跳过此版本(强制更新不可跳过)
|
|
if (!forceUpdate.value) {
|
|
elements.push(
|
|
h('div', { style: { marginTop: '8px', textAlign: 'right' } }, [
|
|
h(Button, { size: 'small', type: 'text', onClick: handleSkipVersion }, () => '跳过此版本')
|
|
])
|
|
)
|
|
}
|
|
|
|
return elements
|
|
},
|
|
okText: '立即更新',
|
|
cancelText: '稍后提醒',
|
|
closable: !forceUpdate.value,
|
|
maskClosable: !forceUpdate.value,
|
|
onOk: async () => {
|
|
confirmModalInstance = null
|
|
await handleDownload()
|
|
},
|
|
onCancel: () => {
|
|
confirmModalInstance = null
|
|
emit('update:modelValue', false)
|
|
},
|
|
onBeforeCancel: () => {
|
|
if (forceUpdate.value) {
|
|
Message.warning('此版本为强制更新,无法跳过')
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
})
|
|
}
|
|
|
|
// 生成进度弹窗内容
|
|
const getProgressModalContent = () => {
|
|
// 下载中状态
|
|
if (updateStore.downloading) {
|
|
const progressValue = Math.min(100, Math.max(0, updateStore.downloadProgress || 0))
|
|
const finalProgress = progressValue / 100
|
|
|
|
const { downloaded, total, speed } = updateStore.progressInfo
|
|
const sizeText = total > 0
|
|
? `${updateStore.formatFileSize(downloaded)} / ${updateStore.formatFileSize(total)}`
|
|
: updateStore.downloadProgress > 0 ? '计算文件大小...' : '准备下载...'
|
|
|
|
const speedElement = speed > 0
|
|
? h('div', { style: { fontSize: '12px', color: 'var(--color-text-3)', marginTop: '4px' } },
|
|
`下载速度: ${updateStore.formatSpeed(speed)}`
|
|
)
|
|
: null
|
|
|
|
return [
|
|
h('div', { style: { marginBottom: '16px' } }, [
|
|
h('div', { style: { marginBottom: '8px', fontSize: '14px', color: 'var(--color-text-2)' } }, '正在下载更新包...')
|
|
]),
|
|
h('div', { style: { marginBottom: '8px' } }, [
|
|
h(Progress, { percent: finalProgress, showText: true })
|
|
]),
|
|
h('div', { style: { fontSize: '12px', color: 'var(--color-text-3)', marginTop: '8px' } }, sizeText),
|
|
speedElement
|
|
]
|
|
}
|
|
|
|
// 安装中状态:下载完成后进入安装阶段,弹窗保持打开直到进程重启
|
|
return [
|
|
h('div', { style: { marginBottom: '16px' } }, [
|
|
h('div', { style: { marginBottom: '8px', fontSize: '14px', color: 'var(--color-text-2)' } }, '正在安装更新...')
|
|
]),
|
|
h('div', { style: { fontSize: '12px', color: 'var(--color-text-3)' } }, '请稍候,应用将在安装完成后自动重启...')
|
|
]
|
|
}
|
|
|
|
// 更新进度弹窗内容
|
|
const updateProgressModal = async () => {
|
|
if (!progressModalInstance) return
|
|
await nextTick()
|
|
progressModalInstance.update({
|
|
content: () => getProgressModalContent()
|
|
})
|
|
}
|
|
|
|
// 显示进度弹窗
|
|
const showProgressModal = async () => {
|
|
if (progressModalInstance) {
|
|
progressModalInstance.close()
|
|
progressModalInstance = null
|
|
}
|
|
|
|
progressModalInstance = Modal.info({
|
|
title: '更新进度',
|
|
content: () => getProgressModalContent(),
|
|
closable: false,
|
|
maskClosable: false,
|
|
footer: false
|
|
})
|
|
|
|
await nextTick()
|
|
|
|
// 监听 store 状态变化
|
|
const stopWatcher = watch(
|
|
[
|
|
() => updateStore.downloadProgress,
|
|
() => updateStore.downloading,
|
|
() => updateStore.installing,
|
|
() => updateStore.progressInfo.total,
|
|
() => updateStore.progressInfo.downloaded,
|
|
() => updateStore.progressInfo.speed
|
|
],
|
|
async () => {
|
|
await nextTick(updateProgressModal)
|
|
},
|
|
{ immediate: true, flush: 'post' }
|
|
)
|
|
|
|
if (progressModalInstance) {
|
|
progressModalInstance._stopWatcher = stopWatcher
|
|
}
|
|
}
|
|
|
|
// 关闭进度弹窗
|
|
const closeProgressModal = () => {
|
|
if (progressModalInstance) {
|
|
if (progressModalInstance._stopWatcher) {
|
|
progressModalInstance._stopWatcher()
|
|
delete progressModalInstance._stopWatcher
|
|
}
|
|
progressModalInstance.close()
|
|
progressModalInstance = null
|
|
}
|
|
}
|
|
|
|
// 关闭所有弹窗
|
|
const closeModals = () => {
|
|
if (confirmModalInstance) {
|
|
confirmModalInstance.close()
|
|
confirmModalInstance = null
|
|
}
|
|
closeProgressModal()
|
|
}
|
|
|
|
// 跳过此版本
|
|
const handleSkipVersion = async () => {
|
|
try {
|
|
await updateStore.skipVersion(props.updateInfo.latest_version)
|
|
} catch (error) {
|
|
console.error('跳过版本失败:', error)
|
|
}
|
|
closeModals()
|
|
emit('update:modelValue', false)
|
|
}
|
|
|
|
// 下载更新(进度与完成事件由 store 统一处理)
|
|
const handleDownload = async () => {
|
|
await showProgressModal()
|
|
|
|
try {
|
|
await updateStore.downloadUpdate()
|
|
} catch (error) {
|
|
console.error('下载失败:', error)
|
|
closeProgressModal()
|
|
Message.error('下载失败:' + (error.message || error))
|
|
}
|
|
}
|
|
|
|
// 下载/安装状态收尾:下载失败收起进度弹窗;安装流程结束(重启前或失败)关闭弹窗
|
|
watch(
|
|
() => [updateStore.installing, updateStore.downloadStatus],
|
|
([installing, status], [prevInstalling]) => {
|
|
if (status === 'exception') {
|
|
closeProgressModal()
|
|
return
|
|
}
|
|
if (prevInstalling && !installing) {
|
|
closeProgressModal()
|
|
emit('update:modelValue', false)
|
|
}
|
|
}
|
|
)
|
|
|
|
// 生命周期
|
|
onMounted(async () => {
|
|
try {
|
|
const result = await GetCurrentVersion()
|
|
if (result.success) {
|
|
backendVersion.value = result.data?.version || ''
|
|
}
|
|
} catch {
|
|
// 后端未就绪时忽略,留空兜底
|
|
}
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
closeModals()
|
|
})
|
|
</script>
|