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