diff --git a/AI_DEV_GUIDE.md b/AI_DEV_GUIDE.md index a80af1a..cc9ed03 100644 --- a/AI_DEV_GUIDE.md +++ b/AI_DEV_GUIDE.md @@ -13,13 +13,42 @@ ### 模块系统 -项目采用模块化架构,每个模块包含前端组件和 Rust 后端命令: +项目采用统一的模块注册机制。每个模块通过 `src/modules//index.ts` 导出 `ModuleConfig` 配置,由 `src/modules/registry.ts` 中的 `moduleRegistry` 单例统一管理。 ``` -src/modules// # 前端模块 -src-tauri/src/commands/.rs # Rust 命令 +src/modules// +├── index.ts # 模块配置(ModuleConfig) +├── ModuleComponent.vue # 前端组件 +src-tauri/src/commands/ # Rust 命令(可选) ``` +#### 模块配置类型 + +```typescript +// src/types/module.ts +interface ModuleConfig { + id: string // 唯一标识 + name: string // 显示名称 + icon: string // 图标标识(对应 icons.ts 中的 key) + description: string // 模块描述 + category: ModuleCategory // 分类:'network' | 'tool' | 'system' | 'media' + defaultEnabled?: boolean // 默认是否启用 + builtin?: boolean // 是否内置模块(不可禁用) + loader?: () => Promise<{ default: Component }> // 懒加载 + component?: Component // 直接组件引用(内置模块) + searchItems?: SearchIndexItem[] // 全局搜索项 + process?: ModuleProcessConfig // 进程配置(需要子进程的模块) + lifecycle?: ModuleLifecycle // 生命周期钩子 + order?: number // 排序权重 +} +``` + +#### 注册新模块 + +1. 创建 `src/modules//index.ts`,导出 `moduleConfig` +2. 在 `src/modules/index.ts` 中添加导入 +3. 在 `src/modules/icons.ts` 中添加图标映射 + ### IPC 通信 前端通过 Tauri API 调用 Rust 命令: @@ -75,18 +104,30 @@ export const useModuleStore = defineStore('moduleName', { 创建 `src/modules//` 目录,包含以下文件: ```typescript -// index.ts - 模块注册 -import type { ModuleConfig } from '@/types' -import ModuleComponent from './ModuleComponent.vue' +// index.ts - 模块配置 +import type { ModuleConfig } from '@/types/module' export const moduleConfig: ModuleConfig = { id: 'module-name', name: '模块名称', - icon: 'icon-name', - component: ModuleComponent + icon: 'module-name', // 需在 icons.ts 中添加映射 + description: '模块描述', + category: 'tool', // 'network' | 'tool' | 'system' | 'media' + defaultEnabled: true, + loader: () => import('./ModuleComponent.vue'), + searchItems: [ + { + title: '功能名称', + description: '功能描述', + keywords: ['关键词1', '关键词2'] + } + ], + order: 100 } ``` +> 注册模块时,还需在 `src/modules/index.ts` 中添加导入,在 `src/modules/icons.ts` 中添加图标映射。 + ```vue \ No newline at end of file + + + diff --git a/src/components/layout/TitleBar.vue b/src/components/layout/TitleBar.vue index 66c2cca..7905129 100644 --- a/src/components/layout/TitleBar.vue +++ b/src/components/layout/TitleBar.vue @@ -49,18 +49,24 @@ const handleSettingSelect = (item: SearchItem) => { isSearchFocused.value = false } -const tauriWindow = getCurrentWindow() +let tauriWindow: ReturnType | null = null +try { + tauriWindow = getCurrentWindow() +} catch { + // 非 Tauri 环境(如浏览器调试),窗口控制不可用 + tauriWindow = null +} const minimize = async () => { - await tauriWindow.minimize() + await tauriWindow?.minimize() } const maximize = async () => { - await tauriWindow.toggleMaximize() + await tauriWindow?.toggleMaximize() } const close = async () => { - await tauriWindow.hide() + await tauriWindow?.hide() } const handleBlur = () => { diff --git a/src/lib/logger.ts b/src/lib/logger.ts new file mode 100644 index 0000000..1af97ce --- /dev/null +++ b/src/lib/logger.ts @@ -0,0 +1,104 @@ +/** + * Thing 日志系统 —— 前端 API + * + * 用法: + * import { createLogger, getLogs, clearLogs, getLogInfo } from '@/lib/logger' + * + * const logger = createLogger('proxy') + * logger.info('mihomo 内核启动成功') + * logger.error('连接失败: timeout') + * + * // 查询日志 + * const entries = await getLogs('proxy', 'error', 50) + */ + +import { invoke } from '@tauri-apps/api/core' + +export type LogLevel = 'debug' | 'info' | 'warn' | 'error' + +export interface LogEntry { + timestamp: string + level: LogLevel + module: string + message: string +} + +export interface LogInfo { + log_dir: string + log_files: string[] + total_size_bytes: number + max_file_size_bytes: number + max_files: number +} + +// ===== 每个模块一个 Logger 实例 ===== + +class Logger { + private module: string + + constructor(module: string) { + this.module = module + } + + private write(level: LogLevel, message: string): void { + invoke('log_message', { level, module: this.module, message }).catch(() => { + // 日志写入失败不应阻塞业务逻辑 + }) + } + + debug(message: string): void { + this.write('debug', message) + } + + info(message: string): void { + this.write('info', message) + } + + warn(message: string): void { + this.write('warn', message) + } + + error(message: string): void { + this.write('error', message) + } +} + +/** + * 创建一个绑定到指定模块的日志记录器。 + * + * @param module 模块标识,如 'proxy'、'clipboard'、'app' 等 + */ +export function createLogger(module: string): Logger { + return new Logger(module) +} + +// ===== 静态查询/管理方法 ===== + +/** + * 查询日志。 + * + * @param module 按模块过滤(可选) + * @param level 按级别过滤(可选) + * @param limit 返回条数上限(可选,默认全部) + */ +export async function getLogs( + module?: string, + level?: LogLevel, + limit?: number, +): Promise { + return invoke('get_logs', { module, level, limit }) +} + +/** + * 清空所有日志文件。 + */ +export async function clearLogs(): Promise { + return invoke('clear_logs') +} + +/** + * 获取日志系统信息(目录、文件列表、空间占用)。 + */ +export async function getLogInfo(): Promise { + return invoke('get_log_info') +} diff --git a/src/main.ts b/src/main.ts index 70cb2fd..bcf10e3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,6 +3,23 @@ import { createPinia } from 'pinia' import App from './App.vue' import './style.css' +// 导入模块注册入口 —— 副作用导入,注册所有模块到 moduleRegistry +import './modules' + +import { createLogger } from './lib/logger' +const logger = createLogger('main') + +// 全局未捕获异常日志 +window.addEventListener('error', (event) => { + logger.error(`全局JS错误: ${event.message} @ ${event.filename}:${event.lineno}`) +}) + +window.addEventListener('unhandledrejection', (event) => { + logger.error(`未处理的Promise拒绝: ${event.reason}`) +}) + +logger.info('Thing 应用启动') + const app = createApp(App) const pinia = createPinia() @@ -10,7 +27,22 @@ app.use(pinia) app.mount('#app') -// 应用挂载后再初始化搜索索引,不阻塞首屏渲染 +// 应用挂载后初始化搜索索引和进程监听(不阻塞首屏渲染) void import('./stores/searchStore').then(({ useSearchStore }) => { - useSearchStore().initGlobalIndex() -}) \ No newline at end of file + const searchStore = useSearchStore() + searchStore.initGlobalIndex() + + // 移除已禁用模块的搜索项 + void import('./stores/appStore').then(({ useAppStore }) => { + const appStore = useAppStore() + appStore.modules.forEach(m => { + if (!m.enabled) { + searchStore.unregisterModule(m.id) + } + }) + }) +}) + +void import('./stores/processStore').then(({ useProcessStore }) => { + useProcessStore().initListener().catch(e => console.error('Process listener init error:', e)) +}) diff --git a/src/modules/clipboard/index.ts b/src/modules/clipboard/index.ts new file mode 100644 index 0000000..4909774 --- /dev/null +++ b/src/modules/clipboard/index.ts @@ -0,0 +1,22 @@ +import type { ModuleConfig } from '@/types/module' +import type { SearchIndexItem } from '@/stores/searchIndex' + +const searchItems: SearchIndexItem[] = [ + { + title: '剪贴板历史', + description: '查看和管理剪贴板记录', + keywords: ['剪贴板', '复制', '粘贴', 'clipboard', 'copy', 'paste'] + } +] + +export const moduleConfig: ModuleConfig = { + id: 'clipboard', + name: '剪贴板', + icon: 'clipboard', + description: '剪贴板历史记录、搜索与多格式预览', + category: 'tool', + defaultEnabled: true, + loader: () => import('./ClipboardModule.vue'), + searchItems, + order: 20 +} diff --git a/src/modules/downloader/index.ts b/src/modules/downloader/index.ts new file mode 100644 index 0000000..4f916b0 --- /dev/null +++ b/src/modules/downloader/index.ts @@ -0,0 +1,30 @@ +import type { ModuleConfig } from '@/types/module' +import type { SearchIndexItem } from '@/stores/searchIndex' + +const searchItems: SearchIndexItem[] = [ + { + title: '下载管理', + description: '管理下载任务', + keywords: ['下载', 'download', '文件', 'file'] + } +] + +export const moduleConfig: ModuleConfig = { + id: 'downloader', + name: '下载器', + icon: 'downloader', + description: 'HTTP下载、BT/磁力链接支持', + category: 'network', + defaultEnabled: true, + loader: () => import('./DownloaderModule.vue'), + searchItems, + process: { + name: 'aria2c', + executable: '', + args: ['--enable-rpc', '--rpc-listen-port=6800'], + autoStart: false, + restartOnCrash: true, + maxRestarts: 3 + }, + order: 50 +} diff --git a/src/modules/finder/index.ts b/src/modules/finder/index.ts new file mode 100644 index 0000000..be8f6c9 --- /dev/null +++ b/src/modules/finder/index.ts @@ -0,0 +1,22 @@ +import type { ModuleConfig } from '@/types/module' +import type { SearchIndexItem } from '@/stores/searchIndex' + +const searchItems: SearchIndexItem[] = [ + { + title: '文件搜索', + description: '搜索本地文件', + keywords: ['文件', '搜索', 'finder', 'search', 'file'] + } +] + +export const moduleConfig: ModuleConfig = { + id: 'finder', + name: '文件搜索', + icon: 'finder', + description: '快速文件搜索、拼音模糊匹配', + category: 'tool', + defaultEnabled: true, + loader: () => import('./FinderModule.vue'), + searchItems, + order: 60 +} diff --git a/src/modules/general/GeneralSettings.vue b/src/modules/general/GeneralSettings.vue index 58a80e7..2c4baa8 100644 --- a/src/modules/general/GeneralSettings.vue +++ b/src/modules/general/GeneralSettings.vue @@ -1,18 +1,22 @@ \ No newline at end of file + + + diff --git a/src/modules/general/index.ts b/src/modules/general/index.ts new file mode 100644 index 0000000..9d3142a --- /dev/null +++ b/src/modules/general/index.ts @@ -0,0 +1,53 @@ +import type { ModuleConfig } from '@/types/module' +import type { SearchIndexItem } from '@/stores/searchIndex' +import GeneralSettings from './GeneralSettings.vue' + +const searchItems: SearchIndexItem[] = [ + { + title: '浅色模式', + description: '切换到浅色主题', + keywords: ['浅色', '主题', 'theme', 'light'] + }, + { + title: '深色模式', + description: '切换到深色主题', + keywords: ['深色', '主题', 'theme', 'dark'] + }, + { + title: '跟随系统', + description: '跟随系统主题设置', + keywords: ['系统', '主题', 'theme', 'system'] + }, + { + title: '普通模式', + description: '标准背景效果', + keywords: ['效果', '普通', 'normal', 'effect'] + }, + { + title: 'Win 云母', + description: 'Windows 11 云母效果', + keywords: ['效果', '云母', 'mica', 'effect'] + }, + { + title: 'Win 亚克力', + description: 'Windows 11 亚克力效果', + keywords: ['效果', '亚克力', 'acrylic', 'effect'] + }, + { + title: '开机自启', + description: '启动 Windows 时自动运行应用', + keywords: ['开机', '自启', '自动', 'auto', 'start'] + } +] + +export const moduleConfig: ModuleConfig = { + id: 'settings', + name: '常规设置', + icon: 'settings', + description: '主题、效果、开机自启与模块管理', + category: 'system', + builtin: true, + component: GeneralSettings, + searchItems, + order: 999 +} diff --git a/src/modules/icons.ts b/src/modules/icons.ts new file mode 100644 index 0000000..6a736aa --- /dev/null +++ b/src/modules/icons.ts @@ -0,0 +1,32 @@ +import type { Component } from 'vue' +import { + Settings, + Globe, + ClipboardList, + Camera, + Activity, + Download, + Search +} from '@lucide/vue' + +/** + * 模块图标映射表 + * + * 模块配置中使用字符串标识(如 'proxy'),通过此表转换为实际图标组件。 + * 新增模块时,在对应模块的 index.ts 中使用一致的 icon 字符串, + * 并在此处添加映射。 + */ +export const moduleIconMap: Record = { + settings: Settings, + proxy: Globe, + clipboard: ClipboardList, + screenshot: Camera, + monitor: Activity, + downloader: Download, + finder: Search +} + +/** 获取模块图标组件,未找到时回退到 Settings 图标 */ +export function getModuleIcon(iconName: string): Component { + return moduleIconMap[iconName] ?? Settings +} diff --git a/src/modules/index.ts b/src/modules/index.ts new file mode 100644 index 0000000..cf9dc0d --- /dev/null +++ b/src/modules/index.ts @@ -0,0 +1,26 @@ +import { moduleRegistry } from './registry' +import type { ModuleConfig } from '@/types/module' + +// 导入所有模块配置 —— 新增模块时在此处添加一行 +import { moduleConfig as proxy } from './proxy' +import { moduleConfig as clipboard } from './clipboard' +import { moduleConfig as screenshot } from './screenshot' +import { moduleConfig as monitor } from './monitor' +import { moduleConfig as downloader } from './downloader' +import { moduleConfig as finder } from './finder' +import { moduleConfig as general } from './general' + +const allModules: ModuleConfig[] = [ + proxy, + clipboard, + screenshot, + monitor, + downloader, + finder, + general +] + +// 启动时注册所有模块 +moduleRegistry.registerAll(allModules) + +export { moduleRegistry } diff --git a/src/modules/monitor/index.ts b/src/modules/monitor/index.ts new file mode 100644 index 0000000..4cc3fcb --- /dev/null +++ b/src/modules/monitor/index.ts @@ -0,0 +1,22 @@ +import type { ModuleConfig } from '@/types/module' +import type { SearchIndexItem } from '@/stores/searchIndex' + +const searchItems: SearchIndexItem[] = [ + { + title: '硬件监控', + description: '查看系统硬件状态', + keywords: ['监控', '硬件', 'cpu', '内存', 'monitor', 'hardware'] + } +] + +export const moduleConfig: ModuleConfig = { + id: 'monitor', + name: '硬件监控', + icon: 'monitor', + description: 'CPU、GPU、内存实时监控与可视化', + category: 'system', + defaultEnabled: true, + loader: () => import('./MonitorModule.vue'), + searchItems, + order: 40 +} diff --git a/src/modules/proxy/ProxyModule.vue b/src/modules/proxy/ProxyModule.vue index 5f0cf8a..7bdd03a 100644 --- a/src/modules/proxy/ProxyModule.vue +++ b/src/modules/proxy/ProxyModule.vue @@ -1,24 +1,617 @@ \ No newline at end of file + diff --git a/src/modules/proxy/index.ts b/src/modules/proxy/index.ts new file mode 100644 index 0000000..0a09e16 --- /dev/null +++ b/src/modules/proxy/index.ts @@ -0,0 +1,68 @@ +import type { ModuleConfig } from '@/types/module' +import type { SearchIndexItem } from '@/stores/searchIndex' +import { invoke } from '@tauri-apps/api/core' + +const searchItems: SearchIndexItem[] = [ + { + title: '代理设置', + description: '配置网络代理、端口与控制接口', + keywords: ['代理', 'proxy', '网络', 'network', '端口', 'port'] + }, + { + title: '订阅管理', + description: '导入与更新 Clash/mihomo 订阅', + keywords: ['订阅', 'subscription', 'profile', '导入'] + }, + { + title: '节点选择', + description: '切换代理节点并测试延迟', + keywords: ['节点', 'node', '延迟', 'delay', '测速'] + }, + { + title: '系统代理', + description: '开启或关闭 Windows 系统代理', + keywords: ['系统代理', 'system proxy', '开关', 'toggle'] + } +] + +export const moduleConfig: ModuleConfig = { + id: 'proxy', + name: '代理管理', + icon: 'proxy', + description: '系统代理切换、订阅管理与延迟测速', + category: 'network', + defaultEnabled: true, + loader: () => import('./ProxyModule.vue'), + searchItems, + // 进程由 MihomoManager 通过 ProcessManager 统一管理(id='proxy'), + // executable/args 在运行时由后端确定,此处仅声明 hasProcess 以便禁用时自动停止。 + process: { + name: 'mihomo', + executable: '', + autoStart: false, + restartOnCrash: true, + maxRestarts: 3 + }, + lifecycle: { + onEnable: async () => { + // 若用户在代理设置中开启了"自动启动",则随模块启用而运行 mihomo + try { + const s = await invoke<{ autoStart?: boolean }>('proxy_get_settings') + if (s.autoStart) { + await invoke('proxy_start') + } + } catch { + /* 忽略:可能内核未安装 */ + } + }, + // 禁用模块时一并关闭系统代理,避免代理已停但系统仍指向导致无法上网 + onDisable: async () => { + try { + await invoke('proxy_clear_system_proxy') + } catch { + /* 忽略:可能内核未运行 */ + } + } + }, + order: 10 +} diff --git a/src/modules/registry.ts b/src/modules/registry.ts new file mode 100644 index 0000000..c0b5849 --- /dev/null +++ b/src/modules/registry.ts @@ -0,0 +1,143 @@ +import type { Component } from 'vue' +import { markRaw, shallowRef } from 'vue' +import type { ModuleConfig, ModuleMeta } from '@/types/module' + +/** + * 模块注册表 —— 全局单例 + * + * 负责收集、管理所有模块的配置信息,并提供查询接口。 + * 模块通过 index.ts 导出 ModuleConfig,由 modules/index.ts 统一注册。 + */ +class ModuleRegistry { + private configs = new Map() + private loadedComponents = new Map() + + /** 注册一个模块 */ + register(config: ModuleConfig): void { + if (this.configs.has(config.id)) { + console.warn(`[ModuleRegistry] 模块 "${config.id}" 已注册,跳过重复注册`) + return + } + this.configs.set(config.id, config) + } + + /** 批量注册 */ + registerAll(configs: ModuleConfig[]): void { + configs.forEach(c => this.register(c)) + } + + /** 获取模块配置 */ + getConfig(id: string): ModuleConfig | undefined { + return this.configs.get(id) + } + + /** 获取所有模块配置 */ + getAllConfigs(): ModuleConfig[] { + return Array.from(this.configs.values()).sort( + (a, b) => (a.order ?? 100) - (b.order ?? 100) + ) + } + + /** 获取所有模块的元信息(可序列化) */ + getAllMetas(): ModuleMeta[] { + return this.getAllConfigs().map(c => ({ + id: c.id, + name: c.name, + icon: c.icon, + description: c.description, + category: c.category, + enabled: c.defaultEnabled ?? true, + builtin: c.builtin ?? false, + hasProcess: !!c.process, + order: c.order ?? 100 + })) + } + + /** 获取所有可被用户管理的模块(非内置) */ + getUserConfigs(): ModuleConfig[] { + return this.getAllConfigs().filter(c => !c.builtin) + } + + /** 获取内置模块 */ + getBuiltinConfigs(): ModuleConfig[] { + return this.getAllConfigs().filter(c => c.builtin) + } + + /** 获取需要进程管理的模块配置 */ + getProcessConfigs(): ModuleConfig[] { + return this.getAllConfigs().filter(c => c.process) + } + + /** 获取模块的所有搜索项 */ + getSearchItems(moduleId: string) { + return this.getConfig(moduleId)?.searchItems ?? [] + } + + /** 收集所有模块的搜索项 */ + getAllSearchItems(): Array<{ moduleId: string; items: ReturnType }> { + return this.getAllConfigs().map(c => ({ + moduleId: c.id, + items: c.searchItems ?? [] + })) + } + + /** 异步加载模块组件,结果会被缓存 */ + async loadComponent(id: string): Promise { + // 缓存命中 + const cached = this.loadedComponents.get(id) + if (cached) return cached + + const config = this.configs.get(id) + if (!config) return null + + // 直接组件引用(内置模块) + if (config.component) { + const raw = markRaw(config.component) + this.loadedComponents.set(id, raw) + return raw + } + + // 懒加载 + if (config.loader) { + try { + const mod = await config.loader() + const raw = markRaw(mod.default) + this.loadedComponents.set(id, raw) + return raw + } catch (e) { + console.error(`[ModuleRegistry] 加载模块 "${id}" 组件失败:`, e) + return null + } + } + + return null + } + + /** 获取已加载的组件(同步,未加载返回 null) */ + getLoadedComponent(id: string): Component | null { + return this.loadedComponents.get(id) ?? null + } + + /** 清除组件缓存 */ + clearComponentCache(id?: string): void { + if (id) { + this.loadedComponents.delete(id) + } else { + this.loadedComponents.clear() + } + } +} + +/** 全局模块注册表实例 */ +export const moduleRegistry = new ModuleRegistry() + +/** Vue 组件中使用的响应式引用 */ +export function useModuleComponent(moduleId: string) { + const component = shallowRef(moduleRegistry.getLoadedComponent(moduleId)) + + const load = async () => { + component.value = await moduleRegistry.loadComponent(moduleId) + } + + return { component, load } +} diff --git a/src/modules/screenshot/index.ts b/src/modules/screenshot/index.ts new file mode 100644 index 0000000..d778535 --- /dev/null +++ b/src/modules/screenshot/index.ts @@ -0,0 +1,22 @@ +import type { ModuleConfig } from '@/types/module' +import type { SearchIndexItem } from '@/stores/searchIndex' + +const searchItems: SearchIndexItem[] = [ + { + title: '截图工具', + description: '捕获屏幕截图', + keywords: ['截图', '屏幕', 'screenshot', 'capture'] + } +] + +export const moduleConfig: ModuleConfig = { + id: 'screenshot', + name: '截图', + icon: 'screenshot', + description: '区域截图、窗口截图与图片编辑', + category: 'media', + defaultEnabled: true, + loader: () => import('./ScreenshotModule.vue'), + searchItems, + order: 30 +} diff --git a/src/stores/appStore.ts b/src/stores/appStore.ts index 49ab132..b15da94 100644 --- a/src/stores/appStore.ts +++ b/src/stores/appStore.ts @@ -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('system') const effect = ref('mica') const isAutoStart = ref(false) const isInitialized = ref(false) + const modules = ref(initModulesFromRegistry()) + const moduleOrder = ref(initModuleOrder()) - const loadSettings = () => { + /** 正在处理切换的模块 ID 集合(防止重复点击) */ + const togglingModules = ref>(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 } -}) \ No newline at end of file +}) diff --git a/src/stores/processStore.ts b/src/stores/processStore.ts new file mode 100644 index 0000000..4b10b36 --- /dev/null +++ b/src/stores/processStore.ts @@ -0,0 +1,114 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { invoke } from '@tauri-apps/api/core' +import { listen, type UnlistenFn } from '@tauri-apps/api/event' +import { moduleRegistry } from '@/modules/registry' + +/** 进程状态 */ +export type ProcessStatus = 'running' | 'stopped' | 'crashed' | 'starting' + +/** 进程信息(与 Rust 端 ProcessInfo 对应) */ +export interface ProcessInfo { + id: string + name: string + status: ProcessStatus + pid: number | null + restartCount: number +} + +/** 启动进程参数(与 Rust 端 StartProcessParams 对应,camelCase) */ +export interface StartProcessParams { + id: string + executable: string + args?: string[] + cwd?: string + name: string + restartOnCrash?: boolean + maxRestarts?: number +} + +export const useProcessStore = defineStore('process', () => { + /** 所有已知进程的状态映射(key = 模块 ID) */ + const processes = ref>(new Map()) + + let unlistenFn: UnlistenFn | null = null + + /** 启动进程监听,接收 Rust 端的进程状态变更事件 */ + const initListener = async () => { + if (unlistenFn) return + unlistenFn = await listen('process-status-changed', (event) => { + processes.value.set(event.payload.id, event.payload) + }) + } + + /** 通过模块 ID 启动进程(自动从注册表读取进程配置) */ + const startByModule = async (moduleId: string): Promise => { + const config = moduleRegistry.getConfig(moduleId) + if (!config?.process) { + throw new Error(`模块 "${moduleId}" 没有进程配置`) + } + + const pc = config.process + const params: StartProcessParams = { + id: moduleId, + executable: pc.executable, + args: pc.args, + cwd: pc.cwd, + name: pc.name, + restartOnCrash: pc.restartOnCrash, + maxRestarts: pc.maxRestarts + } + + const info = await invoke('start_process', { params }) + processes.value.set(moduleId, info) + return info + } + + /** 通过模块 ID 停止进程 */ + const stopByModule = async (moduleId: string): Promise => { + await invoke('stop_process', { id: moduleId }) + processes.value.delete(moduleId) + } + + /** 获取单个进程状态(从 Rust 端查询最新值) */ + const refreshStatus = async (moduleId: string): Promise => { + const info = await invoke('get_process_status', { id: moduleId }) + if (info) { + processes.value.set(moduleId, info) + } else { + processes.value.delete(moduleId) + } + return info + } + + /** 刷新所有进程状态 */ + const refreshAll = async (): Promise => { + const all = await invoke('get_all_process_status') + processes.value.clear() + all.forEach((info) => { + processes.value.set(info.id, info) + }) + } + + /** 获取进程状态(从本地缓存读取,不触发 Rust 调用) */ + const getProcessStatus = (moduleId: string): ProcessInfo | null => { + return processes.value.get(moduleId) ?? null + } + + /** 停止所有进程 */ + const stopAll = async (): Promise => { + await invoke('stop_all_processes') + processes.value.clear() + } + + return { + processes, + initListener, + startByModule, + stopByModule, + refreshStatus, + refreshAll, + getProcessStatus, + stopAll + } +}) diff --git a/src/stores/proxyStore.ts b/src/stores/proxyStore.ts new file mode 100644 index 0000000..8e48d4c --- /dev/null +++ b/src/stores/proxyStore.ts @@ -0,0 +1,256 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { invoke } from '@tauri-apps/api/core' +import { createLogger } from '@/lib/logger' + +const logger = createLogger('proxy') + +// ===== 与 Rust 端对应的数据结构(camelCase) ===== + +export interface ProxySettings { + mixedPort: number + externalController: string + secret: string + mode: string + logLevel: string + allowLan: boolean + systemProxy: boolean + autoStart: boolean + currentProfile: string | null + profiles: ProfileMeta[] +} + +export interface ProfileMeta { + id: string + name: string + url: string + addedAt: string + updatedAt: string + size: number +} + +export interface KernelInfo { + path: string + exists: boolean + version: string | null +} + +export interface ProxyStatus { + running: boolean + pid: number | null + restartCount: number +} + +export interface ProxyHistory { + time: string + delay: number +} + +export interface ProxyNode { + name: string + type: string + udp?: boolean + all?: string[] + now?: string + history?: ProxyHistory[] + alive?: boolean +} + +export interface ProxiesResponse { + proxies: Record +} + +export interface MihomoVersion { + version: string + meta?: boolean +} + +export const useProxyStore = defineStore('proxy', () => { + const kernel = ref(null) + const status = ref({ running: false, pid: null, restartCount: 0 }) + const version = ref('') + const proxies = ref>({}) + const settings = ref(null) + const systemProxy = ref(false) + + /** 内核信息(同时尝试从 resource 提取到 cores/) */ + const refreshKernel = async () => { + try { + kernel.value = await invoke('proxy_kernel_info') + } catch (e) { + logger.error('获取内核信息失败: ' + e) + } + return kernel.value + } + + /** 刷新进程状态 */ + const refreshStatus = async () => { + try { + status.value = await invoke('proxy_status') + } catch (e) { + logger.error('获取进程状态失败: ' + e) + } + return status.value + } + + const start = async () => { + await invoke('proxy_start') + await refreshStatus() + } + + const stop = async () => { + await invoke('proxy_stop') + await refreshStatus() + } + + const restart = async () => { + await invoke('proxy_restart') + await refreshStatus() + } + + /** 获取 mihomo 版本(仅运行时可用) */ + const refreshVersion = async () => { + try { + const v = await invoke('proxy_version') + version.value = v.version + } catch { + version.value = '' + } + } + + /** 加载节点列表 */ + const loadProxies = async () => { + const res = await invoke('proxy_get_proxies') + proxies.value = res.proxies ?? {} + return proxies.value + } + + /** 选择节点 */ + const selectProxy = async (group: string, name: string) => { + await invoke('proxy_select_proxy', { group, name }) + // 更新本地状态 + if (proxies.value[group]) { + proxies.value[group].now = name + } + } + + /** 测速,返回延迟 ms(失败抛错) */ + const testDelay = async (name: string): Promise => { + return await invoke('proxy_test_delay', { name }) + } + + /** 批量测速:对一组节点测速,更新 history */ + const testDelayBatch = async (names: string[]) => { + await Promise.all( + names.map(async (name) => { + try { + const delay = await testDelay(name) + const node = proxies.value[name] + if (node) { + node.history = [{ time: new Date().toISOString(), delay }, ...(node.history ?? [])].slice(0, 5) + } + } catch { + const node = proxies.value[name] + if (node) { + node.history = [{ time: new Date().toISOString(), delay: 0 }, ...(node.history ?? [])].slice(0, 5) + } + } + }) + ) + } + + // ---------- 设置 ---------- + const loadSettings = async () => { + settings.value = await invoke('proxy_get_settings') + systemProxy.value = await invoke('proxy_get_system_proxy') + return settings.value + } + + const saveSettings = async (s: ProxySettings) => { + await invoke('proxy_save_settings', { settings: s }) + settings.value = s + } + + // ---------- 订阅 ---------- + const importProfile = async (url: string, name: string) => { + const meta = await invoke('proxy_import_profile', { url, name }) + await loadSettings() + return meta + } + + const updateProfile = async (id: string) => { + const meta = await invoke('proxy_update_profile', { id }) + await loadSettings() + return meta + } + + const deleteProfile = async (id: string) => { + await invoke('proxy_delete_profile', { id }) + await loadSettings() + } + + const activateProfile = async (id: string) => { + await invoke('proxy_activate_profile', { id }) + await loadSettings() + } + + // ---------- 系统代理 ---------- + const setSystemProxy = async () => { + await invoke('proxy_set_system_proxy') + systemProxy.value = true + if (settings.value) { + settings.value.systemProxy = true + } + } + + const clearSystemProxy = async () => { + await invoke('proxy_clear_system_proxy') + systemProxy.value = false + if (settings.value) { + settings.value.systemProxy = false + } + } + + /** 切换系统代理 */ + const toggleSystemProxy = async (on: boolean) => { + if (on) { + await setSystemProxy() + } else { + await clearSystemProxy() + } + } + + return { + // state + kernel, + status, + version, + proxies, + settings, + systemProxy, + // kernel & process + refreshKernel, + refreshStatus, + start, + stop, + restart, + refreshVersion, + // proxies + loadProxies, + selectProxy, + testDelay, + testDelayBatch, + // settings + loadSettings, + saveSettings, + // profiles + importProfile, + updateProfile, + deleteProfile, + activateProfile, + // system proxy + setSystemProxy, + clearSystemProxy, + toggleSystemProxy + } +}) diff --git a/src/stores/searchIndex.ts b/src/stores/searchIndex.ts index 5190250..8d8be6a 100644 --- a/src/stores/searchIndex.ts +++ b/src/stores/searchIndex.ts @@ -8,106 +8,3 @@ export interface SearchIndexConfig { moduleId: string items: SearchIndexItem[] } - -export const searchIndex: SearchIndexConfig[] = [ - { - moduleId: 'settings', - items: [ - { - title: '浅色模式', - description: '切换到浅色主题', - keywords: ['浅色', '主题', 'theme', 'light'] - }, - { - title: '深色模式', - description: '切换到深色主题', - keywords: ['深色', '主题', 'theme', 'dark'] - }, - { - title: '跟随系统', - description: '跟随系统主题设置', - keywords: ['系统', '主题', 'theme', 'system'] - }, - { - title: '普通模式', - description: '标准背景效果', - keywords: ['效果', '普通', 'normal', 'effect'] - }, - { - title: 'Win 云母', - description: 'Windows 11 云母效果', - keywords: ['效果', '云母', 'mica', 'effect'] - }, - { - title: 'Win 亚克力', - description: 'Windows 11 亚克力效果', - keywords: ['效果', '亚克力', 'acrylic', 'effect'] - }, - { - title: '开机自启', - description: '启动 Windows 时自动运行应用', - keywords: ['开机', '自启', '自动', 'auto', 'start'] - } - ] - }, - { - moduleId: 'proxy', - items: [ - { - title: '代理设置', - description: '配置网络代理', - keywords: ['代理', 'proxy', '网络', 'network'] - } - ] - }, - { - moduleId: 'clipboard', - items: [ - { - title: '剪贴板历史', - description: '查看和管理剪贴板记录', - keywords: ['剪贴板', '复制', '粘贴', 'clipboard', 'copy', 'paste'] - } - ] - }, - { - moduleId: 'screenshot', - items: [ - { - title: '截图工具', - description: '捕获屏幕截图', - keywords: ['截图', '屏幕', 'screenshot', 'capture'] - } - ] - }, - { - moduleId: 'monitor', - items: [ - { - title: '硬件监控', - description: '查看系统硬件状态', - keywords: ['监控', '硬件', 'cpu', '内存', 'monitor', 'hardware'] - } - ] - }, - { - moduleId: 'downloader', - items: [ - { - title: '下载管理', - description: '管理下载任务', - keywords: ['下载', 'download', '文件', 'file'] - } - ] - }, - { - moduleId: 'finder', - items: [ - { - title: '文件搜索', - description: '搜索本地文件', - keywords: ['文件', '搜索', 'finder', 'search', 'file'] - } - ] - } -] \ No newline at end of file diff --git a/src/stores/searchStore.ts b/src/stores/searchStore.ts index 518cb2d..81d23d3 100644 --- a/src/stores/searchStore.ts +++ b/src/stores/searchStore.ts @@ -1,6 +1,6 @@ import { defineStore } from 'pinia' import { ref } from 'vue' -import { searchIndex } from './searchIndex' +import { moduleRegistry } from '@/modules/registry' export interface SearchItem { id: string @@ -14,12 +14,19 @@ export interface SearchItem { export const useSearchStore = defineStore('search', () => { const items = ref([]) + /** + * 初始化全局搜索索引 + * 从模块注册表收集所有模块的搜索项并注册。 + * 注意:被禁用的模块搜索项也会被注册,但 appStore.toggleModule + * 在禁用时会调用 unregisterModule 移除,启用时会调用 registerItem 恢复。 + */ const initGlobalIndex = () => { - searchIndex.forEach(config => { - config.items.forEach((item, index) => { + const allSearchItems = moduleRegistry.getAllSearchItems() + allSearchItems.forEach(({ moduleId, items: moduleItems }) => { + moduleItems.forEach((item, index) => { const searchItem: SearchItem = { - id: `${config.moduleId}-search-${index}`, - moduleId: config.moduleId, + id: `${moduleId}-search-${index}`, + moduleId, title: item.title, description: item.description, keywords: item.keywords @@ -62,6 +69,11 @@ export const useSearchStore = defineStore('search', () => { } } + /** 移除指定模块的所有搜索项 */ + const unregisterModule = (moduleId: string) => { + items.value = items.value.filter(i => i.moduleId !== moduleId) + } + const search = (query: string) => { if (!query.trim()) return [] const lowerQuery = query.toLowerCase() @@ -86,7 +98,8 @@ export const useSearchStore = defineStore('search', () => { registerItem, registerItems, unregisterItem, + unregisterModule, search, getItemsByModule } -}) \ No newline at end of file +}) diff --git a/src/types/module.ts b/src/types/module.ts new file mode 100644 index 0000000..9e44b0e --- /dev/null +++ b/src/types/module.ts @@ -0,0 +1,80 @@ +import type { Component } from 'vue' +import type { SearchIndexItem } from '@/stores/searchIndex' + +/** 模块分类 */ +export type ModuleCategory = 'network' | 'tool' | 'system' | 'media' + +/** 模块进程配置 —— 需要管理外部子进程的模块填写 */ +export interface ModuleProcessConfig { + /** 进程标识符(如 'mihomo'、'aria2') */ + name: string + /** 可执行文件路径(运行时由模块自行确定) */ + executable: string + /** 启动参数 */ + args?: string[] + /** 工作目录 */ + cwd?: string + /** 模块启用时是否自动启动进程 */ + autoStart?: boolean + /** 进程崩溃后是否自动重启 */ + restartOnCrash?: boolean + /** 最大重启次数(0 = 不限制) */ + maxRestarts?: number +} + +/** 模块生命周期钩子 */ +export interface ModuleLifecycle { + /** 模块首次加载时调用 */ + onInit?: () => void | Promise + /** 模块组件挂载时调用 */ + onActivate?: () => void | Promise + /** 模块组件卸载时调用 */ + onDeactivate?: () => void | Promise + /** 模块被禁用时调用 */ + onDisable?: () => void | Promise + /** 模块被启用时调用 */ + onEnable?: () => void | Promise +} + +/** 模块配置 —— 每个模块通过 index.ts 导出此结构 */ +export interface ModuleConfig { + /** 模块唯一标识 */ + id: string + /** 显示名称 */ + name: string + /** 图标标识(对应 Sidebar / Settings 的 iconMap key) */ + icon: string + /** 模块描述(显示在设置界面的模块管理中) */ + description: string + /** 模块分类 */ + category: ModuleCategory + /** 模块是否默认启用 */ + defaultEnabled?: boolean + /** 是否为内置模块(不可禁用,如设置模块) */ + builtin?: boolean + /** 懒加载组件的 loader 函数 */ + loader?: () => Promise<{ default: Component }> + /** 直接组件引用(内置模块可用) */ + component?: Component + /** 全局搜索项 */ + searchItems?: SearchIndexItem[] + /** 进程配置(需要管理子进程的模块填写) */ + process?: ModuleProcessConfig + /** 生命周期钩子 */ + lifecycle?: ModuleLifecycle + /** 排序权重(数值越小越靠前) */ + order?: number +} + +/** 运行时模块元信息(去除了组件等不可序列化字段) */ +export interface ModuleMeta { + id: string + name: string + icon: string + description: string + category: ModuleCategory + enabled: boolean + builtin: boolean + hasProcess: boolean + order: number +}