主界面修改及代理模块初始化

This commit is contained in:
zhongluofeng
2026-07-15 18:22:07 +08:00
parent 29a5f456cb
commit fb361aff9a
39 changed files with 4768 additions and 396 deletions
+299 -24
View File
@@ -1,48 +1,286 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { ref, computed } from 'vue'
import { getCurrentWindow, Effect, EffectState } from '@tauri-apps/api/window'
import { enable, isEnabled, disable } from '@tauri-apps/plugin-autostart'
import { moduleRegistry } from '@/modules/registry'
import { useSearchStore } from '@/stores/searchStore'
import { useProcessStore } from '@/stores/processStore'
import { toast } from 'vue-sonner'
import { createLogger } from '@/lib/logger'
import type { ModuleCategory } from '@/types/module'
const logger = createLogger('app')
export type Theme = 'light' | 'dark' | 'system'
export type EffectType = 'normal' | 'mica' | 'acrylic'
export interface ModuleInfo {
id: string
name: string
icon: string
enabled: boolean
description: string
category: ModuleCategory
hasProcess: boolean
builtin: boolean
}
/** localStorage 版本号 —— 结构变更时递增,自动清除旧数据 */
const SETTINGS_VERSION = 4
const STORAGE_KEY = 'thing_app_settings'
/** 从模块注册表初始化模块元信息 */
const initModulesFromRegistry = (): ModuleInfo[] => {
return moduleRegistry.getAllMetas().map(meta => ({
id: meta.id,
name: meta.name,
icon: meta.icon,
enabled: meta.enabled,
description: meta.description,
category: meta.category,
hasProcess: meta.hasProcess,
builtin: meta.builtin
}))
}
/** 从注册表初始化模块排序(仅用户模块,按 order 字段排序) */
const initModuleOrder = (): string[] => {
return moduleRegistry
.getAllMetas()
.filter(m => !m.builtin)
.sort((a, b) => a.order - b.order)
.map(m => m.id)
}
export const useAppStore = defineStore('app', () => {
const theme = ref<Theme>('system')
const effect = ref<EffectType>('mica')
const isAutoStart = ref(false)
const isInitialized = ref(false)
const modules = ref<ModuleInfo[]>(initModulesFromRegistry())
const moduleOrder = ref<string[]>(initModuleOrder())
const loadSettings = () => {
/** 正在处理切换的模块 ID 集合(防止重复点击) */
const togglingModules = ref<Set<string>>(new Set())
const loadSettings = async () => {
try {
const saved = localStorage.getItem(STORAGE_KEY)
if (saved) {
const settings = JSON.parse(saved)
// 版本不匹配,清除旧数据
if (settings.version !== SETTINGS_VERSION) {
console.warn('[appStore] Settings version mismatch, clearing old data')
localStorage.removeItem(STORAGE_KEY)
saveSettings()
return
}
if (settings.theme) theme.value = settings.theme
if (settings.effect) effect.value = settings.effect
if (settings.isAutoStart !== undefined) isAutoStart.value = settings.isAutoStart
if (settings.modules) {
const savedModules = settings.modules as Array<{ id: string; enabled: boolean }>
savedModules.forEach(sm => {
const m = modules.value.find(mod => mod.id === sm.id)
if (m) {
m.enabled = sm.enabled
}
})
}
// 恢复模块排序:保留已保存的顺序,追加新增模块到末尾
if (settings.moduleOrder) {
const savedOrder = settings.moduleOrder as string[]
const allUserIds = moduleRegistry
.getAllMetas()
.filter(m => !m.builtin)
.map(m => m.id)
const known = savedOrder.filter(id => allUserIds.includes(id))
const newlyAdded = allUserIds.filter(id => !savedOrder.includes(id))
moduleOrder.value = [...known, ...newlyAdded]
}
}
} catch {
console.error('Failed to load settings from localStorage')
localStorage.removeItem(STORAGE_KEY)
}
}
const saveSettings = () => {
try {
const modulesData = modules.value.map(m => ({
id: m.id,
enabled: m.enabled
}))
localStorage.setItem(STORAGE_KEY, JSON.stringify({
version: SETTINGS_VERSION,
theme: theme.value,
effect: effect.value,
isAutoStart: isAutoStart.value
isAutoStart: isAutoStart.value,
modules: modulesData,
moduleOrder: moduleOrder.value
}))
} catch {
console.error('Failed to save settings to localStorage')
}
}
const enabledModules = computed(() => modules.value.filter(m => m.enabled))
const getModule = (id: string) => modules.value.find(m => m.id === id)
/** 重新排序模块 */
const reorderModules = (newOrder: string[]) => {
moduleOrder.value = newOrder
saveSettings()
}
const toggleModule = async (moduleId: string, enabled?: boolean) => {
const moduleIndex = modules.value.findIndex(m => m.id === moduleId)
if (moduleIndex === -1) {
console.warn(`[toggleModule] Module "${moduleId}" not found`)
return false
}
const moduleInfo = modules.value[moduleIndex]
// 内置模块不可禁用
if (moduleInfo.builtin && enabled === false) {
toast.warning('内置模块无法禁用')
return false
}
// 防止重复操作
if (togglingModules.value.has(moduleId)) {
console.log(`[toggleModule] Module "${moduleId}" is already being toggled`)
return false
}
const targetState = enabled !== undefined ? enabled : !moduleInfo.enabled
if (targetState === moduleInfo.enabled) {
console.log(`[toggleModule] Module "${moduleId}" is already ${targetState ? 'enabled' : 'disabled'}`)
return false
}
console.log(`[toggleModule] Toggling "${moduleId}" from ${moduleInfo.enabled} to ${targetState}`)
togglingModules.value.add(moduleId)
try {
if (!targetState) {
// ===== 禁用模块 =====
// 1. 先更新状态(让开关立即响应)
modules.value[moduleIndex] = { ...moduleInfo, enabled: false }
saveSettings()
// 2. 清理搜索项
try {
useSearchStore().unregisterModule(moduleId)
} catch (e) {
console.error(`[toggleModule] Failed to unregister search items for "${moduleId}":`, e)
logger.error(`禁用模块 "${moduleId}" 时清理搜索项失败: ${e}`)
}
// 3. 停止进程
if (moduleInfo.hasProcess) {
try {
const processStore = useProcessStore()
const status = processStore.getProcessStatus(moduleId)
if (status && status.status === 'running') {
toast.loading(`正在停止 ${moduleInfo.name} 后台进程...`, { id: `stop-${moduleId}` })
await processStore.stopByModule(moduleId)
toast.success(`${moduleInfo.name} 进程已停止`, { id: `stop-${moduleId}` })
}
} catch (e) {
console.error(`[toggleModule] Failed to stop process for "${moduleId}":`, e)
logger.error(`停止模块 "${moduleId}" 进程失败: ${e}`)
toast.error(`停止 ${moduleInfo.name} 进程失败`, { id: `stop-${moduleId}` })
}
}
// 4. 调用生命周期钩子
try {
const config = moduleRegistry.getConfig(moduleId)
await config?.lifecycle?.onDisable?.()
} catch (e) {
console.error(`[toggleModule] Module onDisable hook failed for "${moduleId}":`, e)
logger.error(`模块 "${moduleId}" onDisable 钩子失败: ${e}`)
}
// 5. 清理组件缓存,释放内存
moduleRegistry.clearComponentCache(moduleId)
logger.info(`已禁用模块: ${moduleInfo.name}`)
toast.success(`已禁用 ${moduleInfo.name}`)
} else {
// ===== 启用模块 =====
// 1. 先更新状态(让开关立即响应)
modules.value[moduleIndex] = { ...moduleInfo, enabled: true }
saveSettings()
// 2. 调用生命周期钩子
try {
const config = moduleRegistry.getConfig(moduleId)
await config?.lifecycle?.onEnable?.()
} catch (e) {
console.error(`[toggleModule] Module onEnable hook failed for "${moduleId}":`, e)
logger.error(`模块 "${moduleId}" onEnable 钩子失败: ${e}`)
}
// 3. 恢复搜索项
try {
const searchStore = useSearchStore()
const config = moduleRegistry.getConfig(moduleId)
if (config?.searchItems) {
config.searchItems.forEach((item, index) => {
searchStore.registerItem({
id: `${moduleId}-search-${index}`,
moduleId,
title: item.title,
description: item.description,
keywords: item.keywords
})
})
}
} catch (e) {
console.error(`[toggleModule] Failed to register search items for "${moduleId}":`, e)
logger.error(`启用模块 "${moduleId}" 时注册搜索项失败: ${e}`)
}
// 4. 如果配置了 autoStart,启动进程
if (moduleInfo.hasProcess) {
const config = moduleRegistry.getConfig(moduleId)
if (config?.process?.autoStart) {
try {
const processStore = useProcessStore()
toast.loading(`正在启动 ${moduleInfo.name} 后台进程...`, { id: `start-${moduleId}` })
await processStore.startByModule(moduleId)
toast.success(`${moduleInfo.name} 进程已启动`, { id: `start-${moduleId}` })
} catch (e) {
console.error(`[toggleModule] Failed to start process for "${moduleId}":`, e)
logger.error(`启动模块 "${moduleId}" 进程失败: ${e}`)
toast.error(`启动 ${moduleInfo.name} 进程失败`, { id: `start-${moduleId}` })
}
}
}
logger.info(`已启用模块: ${moduleInfo.name}`)
toast.success(`已启用 ${moduleInfo.name}`)
}
return true
} catch (e) {
console.error(`[toggleModule] Failed to toggle module "${moduleId}":`, e)
logger.error(`模块 "${moduleId}" 切换失败: ${(e as Error).message}`)
toast.error(`操作失败: ${(e as Error).message}`)
return false
} finally {
togglingModules.value.delete(moduleId)
}
}
const setTheme = async (newTheme: Theme) => {
theme.value = newTheme
// (仅系统级主题广播或更换效果时才刷新)。因此亚克力限制为仅"跟随系统"可用。
// 切到非系统主题时若当前为亚克力,自动回退到云母,避免深浅色不同步。
if (newTheme !== 'system' && effect.value === 'acrylic') {
effect.value = 'mica'
}
@@ -56,9 +294,23 @@ export const useAppStore = defineStore('app', () => {
saveSettings()
}
const toggleAutoStart = () => {
isAutoStart.value = !isAutoStart.value
saveSettings()
const toggleAutoStart = async (checked?: boolean) => {
const targetState = checked !== undefined ? checked : !isAutoStart.value
const previousState = isAutoStart.value
try {
isAutoStart.value = targetState
if (targetState) {
await enable()
} else {
await disable()
}
saveSettings()
} catch (e) {
isAutoStart.value = previousState
console.error('Failed to toggle auto-start:', e)
throw e
}
}
const applyTheme = async () => {
@@ -80,7 +332,7 @@ export const useAppStore = defineStore('app', () => {
const tauriWindow = getCurrentWindow()
await tauriWindow.setTheme(isDark ? 'dark' : 'light')
} catch (e) {
console.error('Failed to set window theme:', e)
// 非 Tauri 环境下忽略
}
await applyEffect()
@@ -95,25 +347,19 @@ export const useAppStore = defineStore('app', () => {
const tauriWindow = getCurrentWindow()
const isDark = root.classList.contains('dark')
// 先清除旧效果
await tauriWindow.clearEffects()
if (effect.value === 'normal') {
// 普通模式:不使用原生效果,用不透明背景色
await tauriWindow.setBackgroundColor(isDark ? '#0f172a' : '#ffffff')
} else if (effect.value === 'mica') {
// 浅色用 micaLight,深色用 micaDark。
// 注意:micaDark 仅在系统处于深色模式时才会渲染为深色(Windows 限制)。
const micaEffect = (isDark ? 'micaDark' : 'micaLight') as unknown as Effect
await tauriWindow.setEffects({
effects: [micaEffect],
state: EffectState.FollowsWindowActiveState,
color: isDark ? [30, 41, 59, 0] : [248, 250, 252, 0]
})
// 窗口背景必须透明,原生效果才能显示
await tauriWindow.setBackgroundColor('#00000000')
} else if (effect.value === 'acrylic') {
// Acrylic:亚克力效果,color 使用半透明 RGBA
await tauriWindow.setEffects({
effects: [Effect.Acrylic],
state: EffectState.FollowsWindowActiveState,
@@ -122,7 +368,7 @@ export const useAppStore = defineStore('app', () => {
await tauriWindow.setBackgroundColor('#00000000')
}
} catch (e) {
console.error('Failed to set window effects:', e)
// 非 Tauri 环境下忽略
}
}
@@ -138,8 +384,8 @@ export const useAppStore = defineStore('app', () => {
try {
const tauriWindow = getCurrentWindow()
await tauriWindow.setTheme(e.matches ? 'dark' : 'light')
} catch (err) {
console.error('Failed to update window theme on system change:', err)
} catch (e) {
// 非 Tauri 环境下忽略
}
await applyEffect()
@@ -148,8 +394,29 @@ export const useAppStore = defineStore('app', () => {
const init = async () => {
try {
loadSettings()
// applyTheme 内部已调用 applyEffect,无需重复调用
await loadSettings()
try {
const saved = localStorage.getItem(STORAGE_KEY)
const savedAutoStart = saved ? JSON.parse(saved).isAutoStart : false
const systemAutoStart = await isEnabled()
isAutoStart.value = systemAutoStart
if (savedAutoStart !== systemAutoStart) {
if (savedAutoStart) {
await enable()
isAutoStart.value = true
} else {
await disable()
isAutoStart.value = false
}
saveSettings()
}
} catch (e) {
console.error('Failed to sync auto-start during init:', e)
}
await applyTheme()
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
@@ -158,9 +425,10 @@ export const useAppStore = defineStore('app', () => {
isInitialized.value = true
} finally {
try {
await getCurrentWindow().show()
const tauriWindow = getCurrentWindow()
await tauriWindow.show()
} catch (e) {
console.error('Failed to show window:', e)
// 非 Tauri 环境下忽略
}
}
}
@@ -170,6 +438,13 @@ export const useAppStore = defineStore('app', () => {
effect,
isAutoStart,
isInitialized,
modules,
moduleOrder,
enabledModules,
togglingModules,
getModule,
toggleModule,
reorderModules,
setTheme,
setEffect,
toggleAutoStart,
@@ -178,4 +453,4 @@ export const useAppStore = defineStore('app', () => {
init,
loadSettings
}
})
})