Files
Thing/src/App.vue
T
2026-08-06 10:33:16 +08:00

215 lines
7.7 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.
<script setup lang="ts">
import { ref, onMounted, onUnmounted, shallowRef, computed, watch, type Component } from 'vue'
import { listen, type UnlistenFn } from '@tauri-apps/api/event'
import { getCurrentWindow } from '@tauri-apps/api/window'
import TitleBar from '@/components/layout/TitleBar.vue'
import Sidebar from '@/components/layout/Sidebar.vue'
import ModuleContainer from '@/components/layout/ModuleContainer.vue'
import { Toaster } from '@/components/ui/sonner'
import { useAppStore } from '@/stores/appStore'
import { useScreenshotStore } from '@/stores/screenshotStore'
import { useQuickPanelStore } from '@/stores/quickpanelStore'
import { useMonitorStore } from '@/stores/monitorStore'
import { TooltipProvider } from '@/components/ui/tooltip'
import { moduleRegistry } from '@/modules/registry'
import type { ModuleMeta } from '@/types/module'
import { pendingNewDownload } from '@/lib/trayEvents'
import { EVENTS, STORAGE_KEYS } from '@/lib/constants'
const appStore = useAppStore()
const screenshotStore = useScreenshotStore()
const quickpanelStore = useQuickPanelStore()
const monitorStore = useMonitorStore()
/** 侧边栏 / 标题栏需要的模块信息(id + name + icon */
interface NavModule {
id: string
name: string
icon: string
}
/** 从注册表元信息转换为导航用的精简结构 */
const toNavModule = (meta: ModuleMeta): NavModule => ({
id: meta.id,
name: meta.name,
icon: meta.icon
})
/** 上次激活的模块 IDlocalStorage 持久化) */
const LAST_MODULE_KEY = STORAGE_KEYS.lastModule
const activeModule = ref('')
const activeComponent = shallowRef<Component | null>(null)
/** 当前可显示的模块(内置模块 + 已启用的用户模块),按用户排序显示 */
const availableModules = computed<NavModule[]>(() => {
const enabledIds = appStore.enabledModules.map(m => m.id)
const allModules = moduleRegistry
.getAllMetas()
.filter(m => m.builtin || enabledIds.includes(m.id))
.map(toNavModule)
// 按 moduleOrder 排序,settings 始终在末尾
return allModules.sort((a, b) => {
if (a.id === 'settings') return 1
if (b.id === 'settings') return -1
const aIdx = appStore.moduleOrder.indexOf(a.id)
const bIdx = appStore.moduleOrder.indexOf(b.id)
if (aIdx === -1) return 1
if (bIdx === -1) return -1
return aIdx - bIdx
})
})
/** 模块加载请求序号:快速切换模块时丢弃过期加载结果,避免旧组件覆盖新组件 */
let moduleLoadSeq = 0
const loadModule = async (moduleId: string) => {
const seq = ++moduleLoadSeq
const component = await moduleRegistry.loadComponent(moduleId)
// 过期请求(期间用户又切换了模块)直接丢弃,不覆盖 activeComponent 也不触发钩子
if (seq !== moduleLoadSeq) return
activeComponent.value = component
// 调用模块的 onActivate 生命周期钩子
const config = moduleRegistry.getConfig(moduleId)
config?.lifecycle?.onActivate?.()
}
const handleModuleChange = (moduleId: string) => {
// 调用上一个模块的 onDeactivate 钩子
const prevConfig = moduleRegistry.getConfig(activeModule.value)
prevConfig?.lifecycle?.onDeactivate?.()
activeModule.value = moduleId
localStorage.setItem(LAST_MODULE_KEY, moduleId)
loadModule(moduleId)
}
const handleSearch = (moduleId: string) => {
activeModule.value = moduleId
localStorage.setItem(LAST_MODULE_KEY, moduleId)
loadModule(moduleId)
}
const getFallbackModule = () => {
const enabledIds = appStore.enabledModules.map(m => m.id)
const fallback = moduleRegistry.getAllMetas().find(
m => !m.builtin && enabledIds.includes(m.id)
)
return fallback?.id || 'settings'
}
watch(() => appStore.enabledModules.length, () => {
// 启动期间 activeModule 尚未确定,跳过
if (!activeModule.value) return
const enabledIds = appStore.enabledModules.map(m => m.id)
if (activeModule.value !== 'settings' && !enabledIds.includes(activeModule.value)) {
const fallback = getFallbackModule()
activeModule.value = fallback
loadModule(fallback)
}
// 模块启用/禁用变化时重新同步快速面板命令缓存
quickpanelStore.syncCommands()
})
/** 计算启动时应打开的默认模块:优先上次记忆,其次排序第一个 */
const resolveDefaultModule = (): string => {
const enabledIds = appStore.enabledModules.map(m => m.id)
const allMetas = moduleRegistry.getAllMetas()
// 尝试上次记忆的模块
const lastModule = localStorage.getItem(LAST_MODULE_KEY)
if (lastModule) {
const meta = allMetas.find(m => m.id === lastModule)
if (meta && (meta.builtin || enabledIds.includes(lastModule))) {
return lastModule
}
}
// 回退到排序第一个可用模块(settings 除外)
const first = availableModules.value.find(m => m.id !== 'settings')
return first?.id || 'settings'
}
onMounted(async () => {
await appStore.init().catch(e => console.error('App init error:', e))
// 初始化监控 store:订阅后端 monitor-data / monitor-network 等事件,
// 使 OSD 窗口在应用启动后即可接收数据流,不依赖用户手动打开监控模块。
// init() 幂等:MonitorModule 挂载时再次调用不会重复订阅。
monitorStore.init()
// 显式初始化 OSD
// 使 OSD 窗口在应用启动时创建(若配置已开启),且不依赖监控模块挂载/卸载
monitorStore.initOsd()
const defaultModule = resolveDefaultModule()
activeModule.value = defaultModule
loadModule(defaultModule)
// 快速面板:同步命令缓存与设置到 localStorage,供独立窗口读取
quickpanelStore.syncCommands()
quickpanelStore.syncSettings()
// 监听快速面板执行命令事件:显示主窗口 + 切换模块
trayUnlisteners.push(
await listen<{ moduleId: string }>('quickpanel-execute-command', async (e) => {
const win = getCurrentWindow()
try {
await win.show()
await win.unminimize()
await win.setFocus()
} catch {
/* 忽略窗口操作失败 */
}
handleSearch(e.payload.moduleId)
})
)
// 监听托盘菜单事件
// tray:toggle-osd 由 monitorStore.initOsd() 注册的监听处理(与监控模块生命周期解耦)
trayUnlisteners.push(
await listen(EVENTS.trayNewDownload, () => {
// 设置标志位,DownloaderModule 挂载后消费
pendingNewDownload.value = true
// 切换到下载模块(如果未启用则切换到设置)
const enabledIds = appStore.enabledModules.map(m => m.id)
if (enabledIds.includes('downloader') || moduleRegistry.getConfig('downloader')?.builtin) {
handleModuleChange('downloader')
}
})
)
trayUnlisteners.push(
await listen(EVENTS.trayOpenSettings, () => {
handleModuleChange('settings')
})
)
// 截图导出监听(应用级常驻,确保任何来源的截图都记录到历史)
screenshotStore.initExportListener().catch(e => console.error('[screenshot] 导出监听初始化失败:', e))
})
const trayUnlisteners: UnlistenFn[] = []
onUnmounted(() => {
trayUnlisteners.forEach(fn => fn())
screenshotStore.destroyExportListener()
// 释放 OSD 事件监听与配置 watcher
monitorStore.disposeOsd()
})
</script>
<template>
<TooltipProvider>
<div class="flex flex-col h-screen w-screen overflow-hidden">
<TitleBar :modules="availableModules" @search="handleSearch" />
<div class="flex-1 flex overflow-hidden">
<Sidebar
:modules="availableModules"
:active-module="activeModule"
@change="handleModuleChange"
/>
<ModuleContainer :active-component="activeComponent" :active-module="activeModule" />
</div>
</div>
<Toaster position="bottom-right" rich-colors close-button :style="{ zIndex: 99999 }" />
</TooltipProvider>
</template>