版本管理,优化

This commit is contained in:
zhongluofeng
2026-08-11 17:19:36 +08:00
parent 2f20161010
commit 6c7897bf47
33 changed files with 1361 additions and 39 deletions
+223 -4
View File
@@ -1,27 +1,119 @@
<script setup lang="ts">
import { Monitor, Sun, Moon, Sparkles, Layers, LogOut, Package, GripVertical } from '@lucide/vue'
import { Monitor, Sun, Moon, Sparkles, Layers, LogOut, Package, GripVertical, Info, RefreshCw, Download, Check, Loader2 } from '@lucide/vue'
import { Switch } from '@/components/ui/switch'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Label } from '@/components/ui/label'
import { Button } from '@/components/ui/button'
import { Progress } from '@/components/ui/progress'
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 { getModuleIcon } from '@/modules/icons'
import { commands, type UpdateCheckResult } from '@/lib/bindings'
import { EVENTS } from '@/lib/constants'
import { listen, type UnlistenFn } from '@tauri-apps/api/event'
import { invoke } from '@tauri-apps/api/core'
import { computed, onMounted, ref, watch } from 'vue'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { VueDraggable } from 'vue-draggable-plus'
const appStore = useAppStore()
const searchStore = useSearchStore()
const processStore = useProcessStore()
// ===== 关于 / 更新 =====
/** 更新进度事件载荷(与 Rust UpdateProgress 对应) */
interface UpdateProgress {
stage: string
percent: number
downloadedBytes: number
totalBytes: number | null
message: string
}
/** 当前应用版本(启动时读取) */
const currentVersion = ref('')
/** 检查更新的结果 */
const updateResult = ref<UpdateCheckResult | null>(null)
const checking = ref(false)
/** 应用本体更新中 */
const appUpdating = ref(false)
/** ThingHK 内核更新中 */
const kernelUpdating = ref(false)
const progress = ref<UpdateProgress | null>(null)
const thinghkExists = ref(false)
let progressUnlisten: UnlistenFn | null = null
const installTypeText = computed(() =>
updateResult.value?.installType === 'installed' ? '安装版' : '便携版',
)
const loadAppInfo = async () => {
try {
currentVersion.value = await commands.appVersion()
} catch { /* 忽略:后端未就绪 */ }
try {
const info = await invoke('monitor_kernel_info') as { exists?: boolean }
thinghkExists.value = info?.exists ?? false
} catch { /* 忽略:内核未就绪 */ }
}
/** 检查 Gitea 最新 release */
const checkUpdate = async () => {
if (checking.value || appUpdating.value) return
checking.value = true
try {
updateResult.value = await commands.updateCheck()
} catch (e) {
console.error('[updater] 检查更新失败', e)
} finally {
checking.value = false
}
}
/** 下载并应用应用更新(便携版替换 exe / 安装版静默安装),触发应用退出重启 */
const installUpdate = async () => {
if (appUpdating.value) return
appUpdating.value = true
try {
await commands.updateInstall()
} catch (e) {
console.error('[updater] 应用更新失败', e)
appUpdating.value = false
}
}
/** 更新 ThingHK 内核:后端先停止监控内核再覆盖文件 */
const updateThinghkKernel = async () => {
if (kernelUpdating.value) return
kernelUpdating.value = true
try {
await commands.updateThinghk()
await loadAppInfo()
} catch (e) {
console.error('[updater] ThingHK 更新失败', e)
} finally {
kernelUpdating.value = false
}
}
// 系统真实深浅色偏好,来自 appStore(应用启动时初始化,仅通过 onThemeChanged 更新,
// 不受 setTheme 污染),用于"跟随系统"卡片色块。
const systemDark = computed(() => appStore.systemDark)
onMounted(() => {
loadAppInfo()
// 监听更新进度事件(应用更新与 ThingHK 内核更新共用)
listen<UpdateProgress>(EVENTS.updateProgress, (e) => {
progress.value = e.payload
if (e.payload.stage === 'done') {
kernelUpdating.value = false
progress.value = null
}
}).then((fn) => {
progressUnlisten = fn
})
searchStore.registerAction('settings', 0, () => appStore.setTheme('light'))
searchStore.registerAction('settings', 1, () => appStore.setTheme('dark'))
searchStore.registerAction('settings', 2, () => appStore.setTheme('system'))
@@ -29,10 +121,21 @@ onMounted(() => {
searchStore.registerAction('settings', 4, () => appStore.setEffect('mica'))
searchStore.registerAction('settings', 5, () => appStore.setEffect('acrylic'))
searchStore.registerAction('settings', 6, () => appStore.toggleAutoStart())
// 新增设置项(模块管理/关于/退出):仅定位滚动到对应卡片
searchStore.registerAction('settings', 7, () => scrollToCard('settings-card-modules'))
searchStore.registerAction('settings', 8, () => scrollToCard('settings-card-about'))
searchStore.registerAction('settings', 9, () => scrollToCard('settings-card-about'))
searchStore.registerAction('settings', 10, () => scrollToCard('settings-card-about'))
searchStore.registerAction('settings', 11, () => scrollToCard('settings-card-quit'))
// 主动刷新所有进程状态,确保内核 badge 显示当前真实状态(而非过期缓存)
processStore.refreshAll().catch(() => { /* 忽略:后端可能未就绪 */ })
})
onUnmounted(() => {
progressUnlisten?.()
progressUnlisten = null
})
const themes: Array<{ id: Theme; name: string; color: string; icon: typeof Sun }> = [
{ id: 'light', name: '浅色模式', color: '#f8fafc', icon: Sun },
{ id: 'dark', name: '深色模式', color: '#1e293b', icon: Moon },
@@ -82,6 +185,25 @@ const quitApp = async () => {
await invoke('quit_app')
}
/** 滚动到指定卡片(搜索导航定位用)。
* 模块为异步加载,若卡片尚未渲染则短暂重试,直到模块挂载完成。 */
const scrollToCard = (id: string) => {
const tryScroll = (): boolean => {
const el = document.getElementById(id)
if (!el) return false
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
return true
}
if (tryScroll()) return
let attempts = 0
const timer = window.setInterval(() => {
attempts++
if (tryScroll() || attempts >= 20) {
window.clearInterval(timer)
}
}, 100)
}
/** 判断模块开关是否处于处理中状态 */
const isModuleToggling = (moduleId: string): boolean => {
return appStore.togglingModules.has(moduleId)
@@ -243,7 +365,7 @@ const onDragEnd = () => {
</CardContent>
</Card>
<Card>
<Card id="settings-card-modules">
<CardHeader>
<CardTitle class="flex items-center gap-2">
<Package class="size-5 text-primary" />
@@ -313,7 +435,104 @@ const onDragEnd = () => {
</CardContent>
</Card>
<Card>
<Card id="settings-card-about">
<CardHeader>
<CardTitle class="flex items-center gap-2">
<Info class="size-5 text-primary" />
关于
</CardTitle>
</CardHeader>
<CardContent class="space-y-1">
<!-- 应用版本 + 检查更新 -->
<div class="flex items-center justify-between py-2">
<div class="space-y-1">
<Label class="text-base font-medium">应用版本</Label>
<p class="text-sm text-muted-foreground">
Thing v{{ currentVersion || '…' }}
<span v-if="updateResult" class="ml-1 text-xs px-1.5 py-0.5 rounded-full bg-muted">
{{ installTypeText }}
</span>
</p>
</div>
<Button
variant="outline"
size="sm"
:disabled="checking || appUpdating"
@click="checkUpdate"
>
<RefreshCw v-if="!checking" class="size-3.5 mr-1.5" />
<Loader2 v-else class="size-3.5 mr-1.5 animate-spin" />
{{ checking ? '检查中...' : '检查更新' }}
</Button>
</div>
<!-- 更新结果 -->
<div v-if="updateResult" class="rounded-lg border border-border/50 p-3 space-y-2">
<div v-if="updateResult.hasUpdate" class="space-y-2">
<div class="flex items-center justify-between">
<span class="text-sm font-medium">
发现新版本 v{{ updateResult.latestVersion }}
</span>
<span class="text-xs text-muted-foreground">当前 v{{ updateResult.currentVersion }}</span>
</div>
<p
v-if="updateResult.releaseBody"
class="text-xs text-muted-foreground whitespace-pre-wrap max-h-20 overflow-y-auto"
>
{{ updateResult.releaseBody }}
</p>
<div class="flex items-center gap-2">
<Button size="sm" :disabled="appUpdating" @click="installUpdate">
<Download class="size-3.5 mr-1.5" />
{{ appUpdating ? '更新中...' : '下载并更新' }}
</Button>
<span class="text-xs text-muted-foreground">
{{ installTypeText === '安装版' ? '将静默安装新版并重启' : '将替换程序文件并重启' }}
</span>
</div>
</div>
<div v-else class="flex items-center gap-1.5 text-sm text-muted-foreground">
<Check class="size-4 text-green-500" />
已是最新版本
</div>
</div>
<!-- 应用更新进度 -->
<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>
<Progress :model-value="progress.percent" />
</div>
<!-- ThingHK 内核 -->
<div class="flex items-center justify-between py-2 border-t border-border/50">
<div class="space-y-1">
<Label class="text-base font-medium">ThingHK 内核</Label>
<p class="text-sm text-muted-foreground">
{{ thinghkExists ? '已安装' : '未安装' }} · 更新前请先停用监控模块
</p>
</div>
<Button variant="outline" size="sm" :disabled="kernelUpdating" @click="updateThinghkKernel">
<Loader2 v-if="kernelUpdating && !progress" class="size-3.5 mr-1.5 animate-spin" />
<Package v-else class="size-3.5 mr-1.5" />
{{ kernelUpdating ? '更新中...' : '更新内核' }}
</Button>
</div>
<!-- ThingHK 更新进度 -->
<div v-if="kernelUpdating && 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>
<Progress :model-value="progress.percent" />
</div>
</CardContent>
</Card>
<Card id="settings-card-quit">
<CardHeader>
<CardTitle class="flex items-center gap-2">
<LogOut class="size-5 text-destructive" />