细节调整及优化(26.8.3)
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { Monitor, Sun, Moon, Sparkles, Layers, LogOut, Package, GripVertical, Info, RefreshCw, Download, Check, Loader2 } from '@lucide/vue'
|
||||
import { Monitor, Sun, Moon, Sparkles, Layers, LogOut, Package, GripVertical, Info, RefreshCw, Download, Check, Loader2, X } from '@lucide/vue'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Label } from '@/components/ui/label'
|
||||
@@ -9,6 +9,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip
|
||||
import { useAppStore, type Theme, type EffectType, type ModuleInfo } from '@/stores/appStore'
|
||||
import { useSearchStore } from '@/stores/searchStore'
|
||||
import { useProcessStore } from '@/stores/processStore'
|
||||
import { useDownloaderStore } from '@/stores/downloaderStore'
|
||||
import { getModuleIcon } from '@/modules/icons'
|
||||
import { commands, type UpdateCheckResult } from '@/lib/bindings'
|
||||
import { EVENTS } from '@/lib/constants'
|
||||
@@ -16,6 +17,7 @@ import { listen, type UnlistenFn } from '@tauri-apps/api/event'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { VueDraggable } from 'vue-draggable-plus'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const searchStore = useSearchStore()
|
||||
@@ -67,23 +69,152 @@ const checkUpdate = async () => {
|
||||
updateResult.value = await commands.updateCheck()
|
||||
} catch (e) {
|
||||
console.error('[updater] 检查更新失败', e)
|
||||
toast.error('检查更新失败', { description: String(e) })
|
||||
} finally {
|
||||
checking.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 下载并应用应用更新(便携版替换 exe / 安装版静默安装),触发应用退出重启 */
|
||||
// ===== 应用更新:复用下载模块下载安装包(重试/限速/进度事件成熟可靠),
|
||||
// 下载完成后调用 update_install 执行安装并退出。与代理模块 mihomo 内核更新同模式。 =====
|
||||
|
||||
const downloaderStore = useDownloaderStore()
|
||||
|
||||
/** 下载中显示取消按钮(下载完成进入 applying 阶段后不可取消) */
|
||||
const downloadCancellable = ref(false)
|
||||
/** 取消下载的唤醒回调(由等待 Promise 设置) */
|
||||
let cancelAppDownload: (() => void) | null = null
|
||||
|
||||
/** 格式化速度 MB/s */
|
||||
const fmtSpeed = (bytesPerSec: number) => `${(bytesPerSec / 1024 / 1024).toFixed(2)} MB/s`
|
||||
|
||||
/** 下载并应用应用更新:下载模块下载 → update_install 安装(触发应用退出重启) */
|
||||
const installUpdate = async () => {
|
||||
if (appUpdating.value) return
|
||||
const result = updateResult.value
|
||||
if (!result) return
|
||||
// 与后端选择逻辑一致:安装版找 -setup.exe,便携版找非 setup 的 .exe
|
||||
const asset = result.installType === 'installed'
|
||||
? result.assets.find(a => a.name.endsWith('-setup.exe'))
|
||||
: result.assets.find(a => a.name.endsWith('.exe') && !a.name.includes('setup'))
|
||||
if (!asset) {
|
||||
toast.error('未找到可用的更新安装包', { description: `安装类型: ${installTypeText.value},请在 release 页手动下载` })
|
||||
return
|
||||
}
|
||||
|
||||
appUpdating.value = true
|
||||
progress.value = {
|
||||
stage: 'downloading',
|
||||
percent: 0,
|
||||
downloadedBytes: 0,
|
||||
totalBytes: asset.size || null,
|
||||
message: '准备开始下载...'
|
||||
}
|
||||
|
||||
let taskId: string | null = null
|
||||
let downloadProgressFn: UnlistenFn | null = null
|
||||
// 用对象持有完成事件解绑函数,避免闭包内赋值导致的 TS 类型收窄问题(同 proxyStore)
|
||||
const completeHolder: { fn: UnlistenFn | null } = { fn: null }
|
||||
let downloadOk = false
|
||||
try {
|
||||
await commands.updateInstall()
|
||||
// 确保下载模块事件监听已注册(下载器 UI 与这里共用事件流)
|
||||
try { await downloaderStore.startEventListeners() } catch { /* 忽略 */ }
|
||||
taskId = await downloaderStore.addTask(asset.browserDownloadUrl, asset.name, undefined, {}, false)
|
||||
|
||||
// 下载进度 → 更新进度条
|
||||
downloadProgressFn = await listen<{
|
||||
id: string; completedSize: number; totalSize: number; speed: number; status: string
|
||||
}>('download-progress', (e) => {
|
||||
if (e.payload.id !== taskId || !appUpdating.value) return
|
||||
const pct = e.payload.totalSize > 0
|
||||
? Math.round((e.payload.completedSize / e.payload.totalSize) * 100)
|
||||
: 0
|
||||
progress.value = {
|
||||
stage: 'downloading',
|
||||
percent: pct,
|
||||
downloadedBytes: e.payload.completedSize,
|
||||
totalBytes: e.payload.totalSize,
|
||||
message: e.payload.speed > 0
|
||||
? `正在下载... ${fmtSpeed(e.payload.speed)}`
|
||||
: '正在下载...'
|
||||
}
|
||||
})
|
||||
|
||||
// 等待下载完成 / 失败 / 取消
|
||||
downloadCancellable.value = true
|
||||
const dlResult = await new Promise<{ ok: boolean; error?: string }>((resolve) => {
|
||||
let settled = false
|
||||
const finish = (r: { ok: boolean; error?: string }) => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
cancelAppDownload = null
|
||||
resolve(r)
|
||||
}
|
||||
// 任务添加后瞬间进入终态(如探测即失败)
|
||||
const initial = downloaderStore.tasks.find(t => t.id === taskId)
|
||||
if (initial) {
|
||||
if (initial.status === 'complete') { finish({ ok: true }); return }
|
||||
if (initial.status === 'error') { finish({ ok: false, error: initial.error || '下载失败' }); return }
|
||||
}
|
||||
cancelAppDownload = () => finish({ ok: false, error: '已取消下载' })
|
||||
listen<{ id: string; status: string; error: string | null }>('download-complete', (e) => {
|
||||
if (e.payload.id === taskId) {
|
||||
if (e.payload.status === 'complete') finish({ ok: true })
|
||||
else finish({ ok: false, error: e.payload.error || '下载失败' })
|
||||
}
|
||||
}).then(fn => { completeHolder.fn = fn })
|
||||
})
|
||||
downloadCancellable.value = false
|
||||
if (!dlResult.ok) throw new Error(dlResult.error || '下载失败')
|
||||
downloadOk = true
|
||||
|
||||
// 取下载文件路径 → 移除任务记录(保留文件,安装命令内部会 copy 到临时目录并清理)
|
||||
const dlTask = downloaderStore.tasks.find(t => t.id === taskId)
|
||||
if (!dlTask) throw new Error('下载任务未找到')
|
||||
const exePath = dlTask.dir + '/' + dlTask.filename
|
||||
try {
|
||||
await downloaderStore.removeTask(taskId, false)
|
||||
taskId = null
|
||||
} catch { /* 任务清理失败不阻断安装 */ }
|
||||
|
||||
// 安装阶段(后端 emit applying 进度 → 执行安装 → app.exit(0))
|
||||
progress.value = {
|
||||
stage: 'applying',
|
||||
percent: 100,
|
||||
downloadedBytes: 0,
|
||||
totalBytes: null,
|
||||
message: installTypeText.value === '安装版' ? '正在启动安装程序...' : '正在替换程序文件...'
|
||||
}
|
||||
await commands.updateInstall(exePath)
|
||||
// 成功路径后端已退出应用,不会执行到这里
|
||||
} catch (e) {
|
||||
console.error('[updater] 应用更新失败', e)
|
||||
const msg = String(e)
|
||||
if (msg.includes('已取消下载')) {
|
||||
toast.info('已取消更新下载')
|
||||
} else {
|
||||
toast.error('应用更新失败', { description: msg })
|
||||
}
|
||||
// 清理下载任务:下载失败/取消时删除半成品文件
|
||||
if (taskId) {
|
||||
try { await downloaderStore.removeTask(taskId, !downloadOk) } catch { /* 忽略 */ }
|
||||
}
|
||||
appUpdating.value = false
|
||||
progress.value = null
|
||||
} finally {
|
||||
downloadCancellable.value = false
|
||||
cancelAppDownload = null
|
||||
if (downloadProgressFn) downloadProgressFn()
|
||||
if (completeHolder.fn) completeHolder.fn()
|
||||
}
|
||||
}
|
||||
|
||||
/** 取消应用更新下载(仅下载阶段可取消) */
|
||||
const cancelUpdateDownload = async () => {
|
||||
if (!downloadCancellable.value) return
|
||||
cancelAppDownload?.()
|
||||
}
|
||||
|
||||
/** 更新 ThingHK 内核:后端先停止监控内核再覆盖文件 */
|
||||
const updateThinghkKernel = async () => {
|
||||
if (kernelUpdating.value) return
|
||||
@@ -91,8 +222,10 @@ const updateThinghkKernel = async () => {
|
||||
try {
|
||||
await commands.updateThinghk()
|
||||
await loadAppInfo()
|
||||
toast.success('ThingHK 内核更新完成')
|
||||
} catch (e) {
|
||||
console.error('[updater] ThingHK 更新失败', e)
|
||||
toast.error('ThingHK 内核更新失败', { description: String(e) })
|
||||
} finally {
|
||||
kernelUpdating.value = false
|
||||
}
|
||||
@@ -110,6 +243,9 @@ onMounted(() => {
|
||||
if (e.payload.stage === 'done') {
|
||||
kernelUpdating.value = false
|
||||
progress.value = null
|
||||
} else if (e.payload.stage === 'error') {
|
||||
// ThingHK 内核更新失败(后端 emit);应用更新失败走命令 reject 路径
|
||||
kernelUpdating.value = false
|
||||
}
|
||||
}).then((fn) => {
|
||||
progressUnlisten = fn
|
||||
@@ -501,7 +637,19 @@ const onDragEnd = () => {
|
||||
<div v-if="appUpdating && progress" class="space-y-1.5 py-1">
|
||||
<div class="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span>{{ progress.message }}</span>
|
||||
<span class="font-mono">{{ progress.percent }}%</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-mono">{{ progress.percent }}%</span>
|
||||
<Button
|
||||
v-if="downloadCancellable"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-5 px-1.5 text-xs"
|
||||
@click="cancelUpdateDownload"
|
||||
>
|
||||
<X class="size-3 mr-0.5" />
|
||||
取消
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Progress :model-value="progress.percent" />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user