主界面修改及代理模块初始化
This commit is contained in:
+299
-24
@@ -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
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
@@ -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']
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user