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

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
}
})
})
+114
View File
@@ -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<Map<string, ProcessInfo>>(new Map())
let unlistenFn: UnlistenFn | null = null
/** 启动进程监听,接收 Rust 端的进程状态变更事件 */
const initListener = async () => {
if (unlistenFn) return
unlistenFn = await listen<ProcessInfo>('process-status-changed', (event) => {
processes.value.set(event.payload.id, event.payload)
})
}
/** 通过模块 ID 启动进程(自动从注册表读取进程配置) */
const startByModule = async (moduleId: string): Promise<ProcessInfo> => {
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<ProcessInfo>('start_process', { params })
processes.value.set(moduleId, info)
return info
}
/** 通过模块 ID 停止进程 */
const stopByModule = async (moduleId: string): Promise<void> => {
await invoke('stop_process', { id: moduleId })
processes.value.delete(moduleId)
}
/** 获取单个进程状态(从 Rust 端查询最新值) */
const refreshStatus = async (moduleId: string): Promise<ProcessInfo | null> => {
const info = await invoke<ProcessInfo | null>('get_process_status', { id: moduleId })
if (info) {
processes.value.set(moduleId, info)
} else {
processes.value.delete(moduleId)
}
return info
}
/** 刷新所有进程状态 */
const refreshAll = async (): Promise<void> => {
const all = await invoke<ProcessInfo[]>('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<void> => {
await invoke('stop_all_processes')
processes.value.clear()
}
return {
processes,
initListener,
startByModule,
stopByModule,
refreshStatus,
refreshAll,
getProcessStatus,
stopAll
}
})
+256
View File
@@ -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<string, ProxyNode>
}
export interface MihomoVersion {
version: string
meta?: boolean
}
export const useProxyStore = defineStore('proxy', () => {
const kernel = ref<KernelInfo | null>(null)
const status = ref<ProxyStatus>({ running: false, pid: null, restartCount: 0 })
const version = ref<string>('')
const proxies = ref<Record<string, ProxyNode>>({})
const settings = ref<ProxySettings | null>(null)
const systemProxy = ref(false)
/** 内核信息(同时尝试从 resource 提取到 cores/ */
const refreshKernel = async () => {
try {
kernel.value = await invoke<KernelInfo>('proxy_kernel_info')
} catch (e) {
logger.error('获取内核信息失败: ' + e)
}
return kernel.value
}
/** 刷新进程状态 */
const refreshStatus = async () => {
try {
status.value = await invoke<ProxyStatus>('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<MihomoVersion>('proxy_version')
version.value = v.version
} catch {
version.value = ''
}
}
/** 加载节点列表 */
const loadProxies = async () => {
const res = await invoke<ProxiesResponse>('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<number> => {
return await invoke<number>('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<ProxySettings>('proxy_get_settings')
systemProxy.value = await invoke<boolean>('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<ProfileMeta>('proxy_import_profile', { url, name })
await loadSettings()
return meta
}
const updateProfile = async (id: string) => {
const meta = await invoke<ProfileMeta>('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
}
})
-103
View File
@@ -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']
}
]
}
]
+19 -6
View File
@@ -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<SearchItem[]>([])
/**
* 初始化全局搜索索引
* 从模块注册表收集所有模块的搜索项并注册。
* 注意:被禁用的模块搜索项也会被注册,但 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
}
})
})