Files
u-desk/frontend/src/components/UpdatePanel.vue
T
lxy c7978a5bfd 修复: 版本更新与设置面板走查问题
- 更新链路: 检查记录远端SHA256,下载完成事件回传,安装时带哈希校验
- 续传: 服务器忽略Range返回200时截断重下防追加损坏,文件名去query
- 定时器: 自动检查可重入,配置变更免重启生效,关闭即停,下载加互斥
- 前端: 支持跳过此版本并按版本去重,手动检查不受门禁限制,启动未就绪延时重试
- 更新面板: 新增自动检查开关/间隔/地址配置与进度条状态映射
- 版本历史: cache参数强制刷新与加载超时重试兜底
- 配置: 侧边栏分区全链路持久化,保存请求复用api类型防字段丢失
- 侧边栏: 区块定位改显式类名,不再依赖排序位置
- 绑定: 重新生成并与镜像目录同步
2026-10-03 10:18:27 +08:00

447 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="update-panel">
<a-space direction="vertical" style="width: 100%" :size="20">
<!-- 当前版本信息 -->
<a-card title="版本信息" :bordered="false">
<template #extra>
<a-button type="text" size="small" @click="$emit('open-version-history')">
<template #icon><icon-history /></template>
版本历史
</a-button>
</template>
<a-row :gutter="16">
<a-col :span="12">
<div class="info-item">
<div class="info-label">当前版本</div>
<div class="info-value">{{ currentVersion }}</div>
</div>
</a-col>
<a-col :span="12">
<div class="info-item">
<div class="info-label">最后检查</div>
<div class="info-value">{{ lastCheckTime }}</div>
</div>
</a-col>
</a-row>
<!-- 更新说明 -->
<div v-if="updateInfo && updateInfo.changelog" class="changelog-section">
<div class="changelog-title">
{{ updateInfo.has_update ? '最新版本更新内容' : '当前版本更新内容' }}
</div>
<div class="changelog" v-html="renderChangelog(updateInfo.changelog)" />
</div>
</a-card>
<!-- 检查更新 -->
<a-card title="检查更新" :bordered="false">
<a-space>
<a-button type="primary" @click="handleCheckUpdate" :loading="checking">
<template #icon>
<icon-search />
</template>
检查更新
</a-button>
</a-space>
<!-- 更新设置 -->
<div class="config-section">
<div class="config-row">
<span class="config-label">自动检查更新</span>
<a-switch v-model="autoCheckEnabled" />
</div>
<div class="config-row">
<span class="config-label">检查间隔(分钟)</span>
<a-input-number v-model="checkIntervalMinutes" :min="5" :max="1440" style="width: 120px" />
</div>
<div class="config-row">
<span class="config-label">检查地址</span>
<a-input
v-model="checkURL"
placeholder="留空使用默认 https://c.1216.top/last-version.json"
style="width: 320px"
allow-clear
/>
</div>
<a-button type="primary" size="small" :loading="savingConfig" @click="handleSaveConfig">
保存设置
</a-button>
</div>
<!-- 更新信息 -->
<a-alert
v-if="updateInfo"
:type="updateInfo.has_update ? 'info' : 'success'"
style="margin-top: 16px"
>
<template #title>
{{ updateInfo.has_update ? '发现新版本' : '已是最新版本' }}
</template>
<div v-if="updateInfo.has_update">
<p><strong>最新版本:</strong>{{ updateInfo.latest_version }}</p>
<p><strong>发布日期:</strong>{{ updateInfo.release_date }}</p>
<a-space style="margin-top: 12px">
<a-button
type="primary"
@click="handleDownload"
:loading="downloading"
:disabled="!!downloadProgress"
>
<template #icon>
<icon-download />
</template>
{{ downloadButtonText }}
</a-button>
<a-button
v-if="downloadedFile"
type="primary"
status="success"
@click="handleInstall"
:loading="installing"
>
<template #icon>
<icon-check-circle />
</template>
立即安装
</a-button>
</a-space>
</div>
</a-alert>
<!-- 下载进度 -->
<div v-if="downloadProgress > 0 || downloading" class="download-progress">
<a-progress
:percent="downloadProgress"
:status="progressBarStatus"
/>
<div class="progress-info">
<span>{{ updateStore.formatFileSize(progressInfo.downloaded) }} / {{ updateStore.formatFileSize(progressInfo.total) }}</span>
<span v-if="progressInfo.speed > 0">{{ updateStore.formatSpeed(progressInfo.speed) }}</span>
</div>
</div>
<!-- 安装结果 -->
<a-alert
v-if="installResult"
:type="installResult.success ? 'success' : 'error'"
style="margin-top: 16px"
>
<template #title>{{ installResult.success ? '安装成功' : '安装失败' }}</template>
<p>{{ installResult.message }}</p>
</a-alert>
</a-card>
</a-space>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { Message, Modal } from '@arco-design/web-vue'
import { storeToRefs } from 'pinia'
import { IconHistory } from '@arco-design/web-vue/es/icon'
import { useUpdateStore } from '../stores/update'
import { parseMarkdown, isMarkedReady, preloadMarked } from '../utils/markedExtensions'
import { sanitizeHtml } from '@/utils/fileUtils'
import { parseWailsEventData } from '@/utils/wailsEvent'
import { GetCurrentVersion, GetUpdateConfig, SetUpdateConfig, InstallUpdate } from '../wailsjs/v3-bindings/u-desk/app'
import { Events } from '@wailsio/runtime'
// Emits
defineEmits(['open-version-history'])
// 使用更新管理 store
const updateStore = useUpdateStore()
// 使用 storeToRefs 解构以保持响应性
const { checking, downloading, installing, downloadProgress, downloadStatus, progressInfo, updateInfo } = storeToRefs(updateStore)
// 本地状态
const currentVersion = ref('-')
const lastCheckTime = ref('-')
const installResult = ref(null)
const downloadedFile = ref(null)
// 更新设置
const autoCheckEnabled = ref(false)
const checkIntervalMinutes = ref(60)
const checkURL = ref('')
const savingConfig = ref(false)
// 下载按钮文案:下载中/安装中/可下载三态
const downloadButtonText = computed(() => {
if (downloading.value) return `下载中...${Math.round(downloadProgress.value)}%`
if (installing.value) return '安装中...'
return '下载更新'
})
// a-progress 状态映射:store 的 active/exception/success → arco 的 normal/danger/success
const progressBarStatus = computed(() => {
if (downloadStatus.value === 'exception') return 'danger'
if (downloadStatus.value === 'success') return 'success'
return 'normal'
})
/** 渲染 changelog(Markdown → HTML) */
function renderChangelog(text: string): string {
if (!text) return ''
try { if (isMarkedReady()) return sanitizeHtml(parseMarkdown(text)); preloadMarked(); return sanitizeHtml(text) } catch { return text }
}
// 加载当前版本
const loadCurrentVersion = async () => {
try {
const result = await GetCurrentVersion()
if (!result.success) return
currentVersion.value = result.data?.version || '-'
} catch (error) {
console.error('获取版本失败:', error)
}
}
// 加载配置
const loadConfig = async () => {
try {
const result = await GetUpdateConfig()
if (!result.success) return
const {
last_check_time = '-',
auto_check_enabled = false,
check_interval_minutes = 60,
check_url = ''
} = result.data || {}
lastCheckTime.value = last_check_time
autoCheckEnabled.value = auto_check_enabled
checkIntervalMinutes.value = check_interval_minutes
checkURL.value = check_url
} catch (error) {
console.error('加载配置失败:', error)
}
}
// 保存设置
const handleSaveConfig = async () => {
savingConfig.value = true
try {
const result = await SetUpdateConfig(autoCheckEnabled.value, checkIntervalMinutes.value, checkURL.value)
if (result.success) {
Message.success('设置已保存')
} else {
Message.error(result.message || '保存设置失败')
}
} catch (error) {
console.error('保存设置失败:', error)
Message.error('保存设置失败:' + (error as Error).message)
} finally {
savingConfig.value = false
}
}
// 检查更新
const handleCheckUpdate = async () => {
installResult.value = null
// 使用 store 的检查方法(非静默模式,显示消息)
await updateStore.checkForUpdates(false)
// 刷新最后检查时间
await loadConfig()
}
// 下载更新
const handleDownload = async () => {
// 使用 store 的下载方法,会自动管理状态和事件监听
await updateStore.downloadUpdate()
}
// 安装更新
const handleInstall = async () => {
if (!downloadedFile.value) {
Message.warning('请先下载更新包')
return
}
Modal.confirm({
title: '确认安装',
content: '安装更新后应用将自动重启,是否继续?',
onOk: async () => {
installing.value = true
installResult.value = null
try {
const result = await InstallUpdate(downloadedFile.value, true)
installResult.value = result.data || result
const success = result.success || result.data?.success
if (!success) {
Message.error(result.message || '安装失败')
return
}
Message.success({
content: '安装成功!应用将在几秒后重启...',
duration: 3000
})
} catch (error) {
console.error('安装失败:', error)
const errorMsg = '安装失败:' + (error.message || error)
installResult.value = { success: false, message: errorMsg }
Message.error(errorMsg)
} finally {
installing.value = false
}
}
})
}
// 监听下载完成事件(本地覆盖:记录下载文件路径)
const onDownloadComplete = (event) => {
const data = parseWailsEventData(event)
if (data.success && data.file_path) {
downloadedFile.value = data.file_path
}
}
onMounted(async () => {
await loadCurrentVersion()
await loadConfig()
// 监听下载完成事件(仅用于记录文件路径)
Events.On('download-complete', onDownloadComplete)
})
onUnmounted(() => {
Events.Off('download-complete')
})
</script>
<style scoped>
.update-panel {
max-width: 900px;
margin: 0 auto;
}
.info-item {
padding: 12px;
background: var(--color-fill-1);
border-radius: 6px;
text-align: center;
}
.info-label {
font-size: 12px;
color: var(--color-text-3);
margin-bottom: 8px;
}
.info-value {
font-size: 16px;
font-weight: 500;
color: var(--color-text-1);
}
.changelog-section {
margin-top: 12px;
}
.changelog-title {
font-size: 13px;
font-weight: 500;
color: var(--color-text-2);
margin-bottom: 6px;
}
.changelog {
background: var(--color-fill-1);
padding: 10px 12px;
border-radius: 4px;
margin: 0;
max-height: 280px;
overflow-y: auto;
font-size: 12px;
line-height: 1.65;
color: var(--color-text-2);
}
.changelog :deep(h4) {
font-size: 12px;
font-weight: 600;
color: var(--color-text-1);
margin: 8px 0 3px;
}
.changelog :deep(h4:first-child) {
margin-top: 0;
}
.changelog :deep(ul) {
list-style: none;
padding: 0;
margin: 1px 0;
}
.changelog :deep(li) {
position: relative;
padding-left: 14px;
margin: 1px 0;
}
.changelog :deep(li::before) {
content: '·';
position: absolute;
left: 4px;
color: var(--color-text-4);
font-weight: bold;
}
.changelog :deep(code) {
background: var(--color-fill-3);
padding: 0 4px;
border-radius: 3px;
font-size: 11px;
}
.changelog :deep(p) {
margin: 2px 0;
}
.download-progress {
margin-top: 16px;
padding: 16px;
background: var(--color-fill-1);
border-radius: 4px;
}
.progress-info {
display: flex;
justify-content: space-between;
margin-top: 8px;
font-size: 12px;
color: var(--color-text-3);
}
.config-section {
margin-top: 16px;
padding: 12px 16px;
background: var(--color-fill-1);
border-radius: 4px;
}
.config-row {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.config-label {
width: 110px;
flex-shrink: 0;
font-size: 13px;
color: var(--color-text-2);
}
</style>