主界面修改及代理模块初始化
This commit is contained in:
+109
-25
@@ -13,13 +13,42 @@
|
|||||||
|
|
||||||
### 模块系统
|
### 模块系统
|
||||||
|
|
||||||
项目采用模块化架构,每个模块包含前端组件和 Rust 后端命令:
|
项目采用统一的模块注册机制。每个模块通过 `src/modules/<name>/index.ts` 导出 `ModuleConfig` 配置,由 `src/modules/registry.ts` 中的 `moduleRegistry` 单例统一管理。
|
||||||
|
|
||||||
```
|
```
|
||||||
src/modules/<module-name>/ # 前端模块
|
src/modules/<module-name>/
|
||||||
src-tauri/src/commands/<name>.rs # Rust 命令
|
├── 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/<name>/index.ts`,导出 `moduleConfig`
|
||||||
|
2. 在 `src/modules/index.ts` 中添加导入
|
||||||
|
3. 在 `src/modules/icons.ts` 中添加图标映射
|
||||||
|
|
||||||
### IPC 通信
|
### IPC 通信
|
||||||
|
|
||||||
前端通过 Tauri API 调用 Rust 命令:
|
前端通过 Tauri API 调用 Rust 命令:
|
||||||
@@ -75,18 +104,30 @@ export const useModuleStore = defineStore('moduleName', {
|
|||||||
创建 `src/modules/<module-name>/` 目录,包含以下文件:
|
创建 `src/modules/<module-name>/` 目录,包含以下文件:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// index.ts - 模块注册
|
// index.ts - 模块配置
|
||||||
import type { ModuleConfig } from '@/types'
|
import type { ModuleConfig } from '@/types/module'
|
||||||
import ModuleComponent from './ModuleComponent.vue'
|
|
||||||
|
|
||||||
export const moduleConfig: ModuleConfig = {
|
export const moduleConfig: ModuleConfig = {
|
||||||
id: 'module-name',
|
id: 'module-name',
|
||||||
name: '模块名称',
|
name: '模块名称',
|
||||||
icon: 'icon-name',
|
icon: 'module-name', // 需在 icons.ts 中添加映射
|
||||||
component: ModuleComponent
|
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
|
```vue
|
||||||
<!-- ModuleComponent.vue - 主组件 -->
|
<!-- ModuleComponent.vue - 主组件 -->
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -224,23 +265,67 @@ pub fn run() {
|
|||||||
|
|
||||||
## 进程管理
|
## 进程管理
|
||||||
|
|
||||||
对于需要管理外部进程的模块(如 mihomo、aria2),使用 `tauri-plugin-shell`:
|
对于需要管理外部进程的模块(如 mihomo、aria2),使用内置的 `ProcessManager`:
|
||||||
|
|
||||||
```rust
|
### 架构
|
||||||
use tauri_plugin_shell::ShellExt;
|
|
||||||
|
|
||||||
#[tauri::command]
|
- **Rust 端** (`src-tauri/src/process_manager.rs`):`ProcessManager` 通过 `std::process::Command` 管理子进程,支持启动、停止、崩溃检测和自动重启
|
||||||
pub fn start_process(app: tauri::AppHandle) -> Result<(), String> {
|
- **前端** (`src/stores/processStore.ts`):Pinia store,通过 `invoke` 调用 Rust 命令,通过 `listen` 接收进程状态变更事件
|
||||||
let mut cmd = app.shell().command("path/to/executable");
|
- **模块配置**:在模块的 `index.ts` 中通过 `ModuleProcessConfig` 声明进程信息
|
||||||
cmd.arg("--config").arg("config.yaml");
|
|
||||||
|
|
||||||
let child = cmd.spawn().map_err(|e| e.to_string())?;
|
### 模块配置
|
||||||
|
|
||||||
// 保存进程句柄
|
```typescript
|
||||||
Ok(())
|
// src/modules/proxy/index.ts
|
||||||
|
export const moduleConfig: ModuleConfig = {
|
||||||
|
// ...
|
||||||
|
process: {
|
||||||
|
name: 'mihomo',
|
||||||
|
executable: '', // 运行时确定
|
||||||
|
args: ['-f', 'config.yaml'],
|
||||||
|
autoStart: false, // 模块启用时是否自动启动
|
||||||
|
restartOnCrash: true, // 崩溃后自动重启
|
||||||
|
maxRestarts: 3 // 最大重启次数(0 = 不限制)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 前端 API
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useProcessStore } from '@/stores/processStore'
|
||||||
|
|
||||||
|
const processStore = useProcessStore()
|
||||||
|
|
||||||
|
// 通过模块 ID 启动进程(自动读取模块配置)
|
||||||
|
await processStore.startByModule('proxy')
|
||||||
|
|
||||||
|
// 停止进程
|
||||||
|
await processStore.stopByModule('proxy')
|
||||||
|
|
||||||
|
// 获取进程状态
|
||||||
|
const status = processStore.getProcessStatus('proxy')
|
||||||
|
|
||||||
|
// 监听状态变更(在 main.ts 中已初始化)
|
||||||
|
// processStore.initListener()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rust 命令
|
||||||
|
|
||||||
|
| 命令 | 参数 | 返回值 |
|
||||||
|
|------|------|--------|
|
||||||
|
| `start_process` | `StartProcessParams` | `ProcessInfo` |
|
||||||
|
| `stop_process` | `id: String` | `()` |
|
||||||
|
| `get_process_status` | `id: String` | `Option<ProcessInfo>` |
|
||||||
|
| `get_all_process_status` | - | `Vec<ProcessInfo>` |
|
||||||
|
| `stop_all_processes` | - | `()` |
|
||||||
|
|
||||||
|
### 事件
|
||||||
|
|
||||||
|
| 事件名 | 载荷 | 触发时机 |
|
||||||
|
|--------|------|----------|
|
||||||
|
| `process-status-changed` | `ProcessInfo` | 进程状态变更(崩溃、重启、停止) |
|
||||||
|
|
||||||
## 数据存储
|
## 数据存储
|
||||||
|
|
||||||
### 配置文件
|
### 配置文件
|
||||||
@@ -323,13 +408,12 @@ let path = Path::new("data").join("config.json");
|
|||||||
|
|
||||||
## 开发流程
|
## 开发流程
|
||||||
|
|
||||||
1. **创建模块目录**:`src/modules/<module-name>/` 和 `src-tauri/src/commands/<name>.rs`
|
1. **创建模块目录**:`src/modules/<module-name>/`,编写组件和 `index.ts` 配置
|
||||||
2. **实现前端组件**:创建 Vue 组件和 Pinia store
|
2. **注册模块**:在 `src/modules/index.ts` 中添加导入,在 `src/modules/icons.ts` 中添加图标映射
|
||||||
3. **实现 Rust 命令**:编写后端逻辑和命令
|
3. **实现 Rust 命令**:在 `src-tauri/src/` 中编写后端逻辑,在 `lib.rs` 中注册命令
|
||||||
4. **注册命令**:在 `lib.rs` 中注册新命令
|
4. **配置权限**:更新 `capabilities/default.json`(如需要)
|
||||||
5. **配置权限**:更新 `capabilities/default.json`
|
5. **测试**:运行 `bun run tauri dev` 测试
|
||||||
6. **测试**:运行 `bun run tauri dev` 测试
|
6. **构建**:运行 `bun run tauri build` 构建生产版本
|
||||||
7. **构建**:运行 `bun run tauri build` 构建生产版本
|
|
||||||
|
|
||||||
## 模板代码生成
|
## 模板代码生成
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,322 @@
|
|||||||
|
# 模块开发指南
|
||||||
|
|
||||||
|
本文档详细说明 Thing 应用的模块系统架构,帮助开发者快速理解并创建新模块。
|
||||||
|
|
||||||
|
## 架构概述
|
||||||
|
|
||||||
|
Thing 采用**统一模块注册机制**,每个模块通过 `index.ts` 自描述其全部配置(名称、图标、组件、搜索项、进程配置、生命周期钩子等),由中央注册表 `moduleRegistry` 统一管理。
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────┐
|
||||||
|
│ main.ts │
|
||||||
|
│ import './modules' (触发注册) │
|
||||||
|
└──────────────────────┬──────────────────────────────┘
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────┐
|
||||||
|
│ src/modules/index.ts │
|
||||||
|
│ 聚合入口:导入所有模块配置并注册 │
|
||||||
|
└──────────────────────┬──────────────────────────────┘
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────┐
|
||||||
|
│ src/modules/registry.ts │
|
||||||
|
│ ModuleRegistry 单例:注册 / 查询 / 组件懒加载 │
|
||||||
|
└──────┬───────────────┬───────────────┬──────────────┘
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
App.vue Sidebar.vue GeneralSettings
|
||||||
|
(组件加载) (侧边栏导航) (模块开关管理)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 核心文件说明
|
||||||
|
|
||||||
|
| 文件 | 职责 |
|
||||||
|
|------|------|
|
||||||
|
| `src/types/module.ts` | 模块配置类型定义 |
|
||||||
|
| `src/modules/registry.ts` | 注册表单例:注册、查询、组件懒加载缓存 |
|
||||||
|
| `src/modules/index.ts` | 聚合入口:导入并注册所有模块 |
|
||||||
|
| `src/modules/icons.ts` | 模块图标映射表 |
|
||||||
|
| `src/modules/<name>/index.ts` | 各模块的配置声明 |
|
||||||
|
| `src/modules/<name>/*.vue` | 模块前端组件 |
|
||||||
|
|
||||||
|
## 模块配置类型
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/types/module.ts
|
||||||
|
|
||||||
|
interface ModuleConfig {
|
||||||
|
/** 模块唯一标识(如 'proxy'、'clipboard') */
|
||||||
|
id: string
|
||||||
|
/** 显示名称 */
|
||||||
|
name: string
|
||||||
|
/** 图标标识(对应 icons.ts 中的 key) */
|
||||||
|
icon: string
|
||||||
|
/** 模块描述(显示在设置界面的模块管理中) */
|
||||||
|
description: string
|
||||||
|
/** 模块分类 */
|
||||||
|
category: 'network' | 'tool' | 'system' | 'media'
|
||||||
|
/** 默认是否启用(默认 true) */
|
||||||
|
defaultEnabled?: boolean
|
||||||
|
/** 是否为内置模块(不可禁用,如设置模块) */
|
||||||
|
builtin?: boolean
|
||||||
|
/** 懒加载组件的 loader 函数(推荐) */
|
||||||
|
loader?: () => Promise<{ default: Component }>
|
||||||
|
/** 直接组件引用(内置模块可用,无需懒加载) */
|
||||||
|
component?: Component
|
||||||
|
/** 全局搜索项 */
|
||||||
|
searchItems?: SearchIndexItem[]
|
||||||
|
/** 进程配置(需要管理子进程的模块填写) */
|
||||||
|
process?: ModuleProcessConfig
|
||||||
|
/** 生命周期钩子 */
|
||||||
|
lifecycle?: ModuleLifecycle
|
||||||
|
/** 排序权重(数值越小越靠前,默认 100) */
|
||||||
|
order?: number
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 创建新模块
|
||||||
|
|
||||||
|
### 第 1 步:创建模块目录
|
||||||
|
|
||||||
|
```
|
||||||
|
src/modules/my-module/
|
||||||
|
├── index.ts # 模块配置
|
||||||
|
└── MyModule.vue # 前端组件
|
||||||
|
```
|
||||||
|
|
||||||
|
### 第 2 步:编写模块配置
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/modules/my-module/index.ts
|
||||||
|
import type { ModuleConfig } from '@/types/module'
|
||||||
|
|
||||||
|
export const moduleConfig: ModuleConfig = {
|
||||||
|
id: 'my-module',
|
||||||
|
name: '我的模块',
|
||||||
|
icon: 'my-module', // 需在 icons.ts 中添加映射
|
||||||
|
description: '模块功能描述',
|
||||||
|
category: 'tool',
|
||||||
|
defaultEnabled: true,
|
||||||
|
loader: () => import('./MyModule.vue'),
|
||||||
|
searchItems: [
|
||||||
|
{
|
||||||
|
title: '功能名称',
|
||||||
|
description: '功能描述',
|
||||||
|
keywords: ['关键词1', '关键词2', 'keyword']
|
||||||
|
}
|
||||||
|
],
|
||||||
|
order: 70
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 第 3 步:注册模块
|
||||||
|
|
||||||
|
在 `src/modules/index.ts` 中添加导入:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { moduleConfig as myModule } from './my-module'
|
||||||
|
|
||||||
|
const allModules: ModuleConfig[] = [
|
||||||
|
// ... 已有模块
|
||||||
|
myModule
|
||||||
|
]
|
||||||
|
|
||||||
|
moduleRegistry.registerAll(allModules)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 第 4 步:添加图标映射
|
||||||
|
|
||||||
|
在 `src/modules/icons.ts` 中添加:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Wrench } from '@lucide/vue' // 选择合适的图标
|
||||||
|
|
||||||
|
export const moduleIconMap: Record<string, Component> = {
|
||||||
|
// ... 已有映射
|
||||||
|
'my-module': Wrench
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 第 5 步(可选):添加 Rust 后端命令
|
||||||
|
|
||||||
|
如果模块需要 Rust 后端支持,在 `src-tauri/src/` 中创建命令模块,并在 `lib.rs` 的 `invoke_handler` 中注册。
|
||||||
|
|
||||||
|
## 进程管理
|
||||||
|
|
||||||
|
需要管理外部子进程的模块(如 mihomo、aria2)通过 `ModuleProcessConfig` 声明进程配置。
|
||||||
|
|
||||||
|
### 配置示例
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/modules/proxy/index.ts
|
||||||
|
export const moduleConfig: ModuleConfig = {
|
||||||
|
// ...
|
||||||
|
process: {
|
||||||
|
name: 'mihomo', // 进程标识
|
||||||
|
executable: '', // 可执行文件路径(运行时确定)
|
||||||
|
args: ['-f', 'config.yaml'], // 启动参数
|
||||||
|
autoStart: false, // 模块启用时是否自动启动
|
||||||
|
restartOnCrash: true, // 崩溃后自动重启
|
||||||
|
maxRestarts: 3 // 最大重启次数(0 = 不限制)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 前端 API
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useProcessStore } from '@/stores/processStore'
|
||||||
|
|
||||||
|
const processStore = useProcessStore()
|
||||||
|
|
||||||
|
// 通过模块 ID 启动进程(自动读取模块配置)
|
||||||
|
await processStore.startByModule('proxy')
|
||||||
|
|
||||||
|
// 停止进程
|
||||||
|
await processStore.stopByModule('proxy')
|
||||||
|
|
||||||
|
// 获取进程状态(从缓存读取)
|
||||||
|
const status = processStore.getProcessStatus('proxy')
|
||||||
|
// status.status: 'running' | 'stopped' | 'crashed' | 'starting'
|
||||||
|
|
||||||
|
// 刷新所有进程状态
|
||||||
|
await processStore.refreshAll()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 自动化行为
|
||||||
|
|
||||||
|
模块启用/禁用时,`appStore.toggleModule` 会自动处理:
|
||||||
|
|
||||||
|
| 操作 | 禁用模块 | 启用模块 |
|
||||||
|
|------|----------|----------|
|
||||||
|
| 进程 | 停止运行中的进程 | 若 `autoStart` 为 true,启动进程 |
|
||||||
|
| 搜索项 | 移除该模块的搜索项 | 恢复该模块的搜索项 |
|
||||||
|
| 组件缓存 | 清除组件缓存,释放内存 | 下次访问时重新懒加载 |
|
||||||
|
| 生命周期 | 调用 `onDisable` 钩子 | 调用 `onEnable` 钩子 |
|
||||||
|
| 侧边栏 | 从侧边栏隐藏 | 在侧边栏显示 |
|
||||||
|
| Toast 通知 | 显示"已禁用 {模块名}" | 显示"已启用 {模块名}" |
|
||||||
|
|
||||||
|
## 生命周期钩子
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ModuleLifecycle {
|
||||||
|
/** 模块首次加载时调用 */
|
||||||
|
onInit?: () => void | Promise<void>
|
||||||
|
/** 模块组件挂载时调用(切换到该模块) */
|
||||||
|
onActivate?: () => void | Promise<void>
|
||||||
|
/** 模块组件卸载时调用(切换离开该模块) */
|
||||||
|
onDeactivate?: () => void | Promise<void>
|
||||||
|
/** 模块被禁用时调用 */
|
||||||
|
onDisable?: () => void | Promise<void>
|
||||||
|
/** 模块被启用时调用 */
|
||||||
|
onEnable?: () => void | Promise<void>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
使用示例:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const moduleConfig: ModuleConfig = {
|
||||||
|
// ...
|
||||||
|
lifecycle: {
|
||||||
|
onEnable: async () => {
|
||||||
|
console.log('模块已启用')
|
||||||
|
// 初始化资源、建立连接等
|
||||||
|
},
|
||||||
|
onDisable: async () => {
|
||||||
|
console.log('模块已禁用')
|
||||||
|
// 释放资源、关闭连接等
|
||||||
|
},
|
||||||
|
onActivate: () => {
|
||||||
|
console.log('用户切换到本模块')
|
||||||
|
// 开始实时数据更新等
|
||||||
|
},
|
||||||
|
onDeactivate: () => {
|
||||||
|
console.log('用户离开本模块')
|
||||||
|
// 暂停实时更新以节省资源
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 全局搜索
|
||||||
|
|
||||||
|
模块通过 `searchItems` 声明可被全局搜索的功能项。用户在标题栏搜索框输入关键词时,匹配的搜索项会显示在结果列表中。
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
searchItems: [
|
||||||
|
{
|
||||||
|
title: '代理设置', // 显示标题
|
||||||
|
description: '配置网络代理', // 显示描述
|
||||||
|
keywords: ['代理', 'proxy', '网络'] // 匹配关键词
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
如果搜索项需要执行特定操作(如跳转到子页面、触发命令),在模块组件挂载时通过 `searchStore.registerAction` 注册:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useSearchStore } from '@/stores/searchStore'
|
||||||
|
|
||||||
|
const searchStore = useSearchStore()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 注册第 0 个搜索项的 action
|
||||||
|
searchStore.registerAction('my-module', 0, () => {
|
||||||
|
// 跳转到特定子页面或执行操作
|
||||||
|
activeTab.value = 'settings'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 模块分类
|
||||||
|
|
||||||
|
| 分类 | 说明 | 适用场景 |
|
||||||
|
|------|------|----------|
|
||||||
|
| `network` | 网络相关 | 代理、下载器、网络工具 |
|
||||||
|
| `tool` | 实用工具 | 剪贴板、文件搜索、文本处理 |
|
||||||
|
| `system` | 系统相关 | 硬件监控、系统设置 |
|
||||||
|
| `media` | 媒体相关 | 截图、录屏、图片处理 |
|
||||||
|
|
||||||
|
分类目前用于元信息标记,未来可用于设置界面的分组展示。
|
||||||
|
|
||||||
|
## 模块禁用机制详解
|
||||||
|
|
||||||
|
当用户在设置页面切换模块开关时:
|
||||||
|
|
||||||
|
```
|
||||||
|
用户点击开关
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
appStore.toggleModule(moduleId, enabled)
|
||||||
|
│
|
||||||
|
├─ 禁用时:
|
||||||
|
│ ├─ searchStore.unregisterModule(moduleId) // 移除搜索项
|
||||||
|
│ ├─ processStore.stopByModule(moduleId) // 停止进程
|
||||||
|
│ ├─ config.lifecycle?.onDisable?.() // 生命周期钩子
|
||||||
|
│ ├─ moduleRegistry.clearComponentCache(id) // 清除组件缓存
|
||||||
|
│ ├─ modules[id].enabled = false // 更新状态
|
||||||
|
│ └─ toast.success('已禁用 {模块名}') // 通知用户
|
||||||
|
│
|
||||||
|
└─ 启用时:
|
||||||
|
├─ modules[id].enabled = true // 更新状态
|
||||||
|
├─ config.lifecycle?.onEnable?.() // 生命周期钩子
|
||||||
|
├─ searchStore.registerItem(...) // 恢复搜索项
|
||||||
|
├─ processStore.startByModule(moduleId) // 启动进程(若 autoStart)
|
||||||
|
└─ toast.success('已启用 {模块名}') // 通知用户
|
||||||
|
```
|
||||||
|
|
||||||
|
禁用后的效果:
|
||||||
|
- 模块从侧边栏导航中隐藏
|
||||||
|
- 模块的搜索项从全局搜索中移除
|
||||||
|
- 模块的后台进程被停止
|
||||||
|
- 模块的组件缓存被清除,释放内存
|
||||||
|
- 如果当前正在查看被禁用的模块,自动切换到第一个可用模块
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **模块 ID 必须唯一**:重复注册会被忽略并输出警告
|
||||||
|
2. **图标必须映射**:模块配置中的 `icon` 字符串必须在 `icons.ts` 中有对应映射,否则回退到 Settings 图标
|
||||||
|
3. **懒加载优先**:使用 `loader` 而非 `component`,避免首屏加载所有模块代码
|
||||||
|
4. **避免循环依赖**:模块的 `index.ts` 只导出配置,不导入其他模块的 store
|
||||||
|
5. **进程配置的 executable**:通常留空,由模块组件在运行时根据用户设置确定实际路径
|
||||||
|
6. **内置模块**:设置 `builtin: true` 的模块不可被用户禁用,开关处于禁用状态
|
||||||
@@ -66,12 +66,12 @@ Thing/
|
|||||||
### 第一阶段:主体架构(当前阶段)
|
### 第一阶段:主体架构(当前阶段)
|
||||||
|
|
||||||
- [x] 项目初始化
|
- [x] 项目初始化
|
||||||
- [ ] 主界面布局(左侧模块导航 + 右侧内容区域)
|
- [x] 主界面布局(左侧模块导航 + 右侧内容区域)
|
||||||
- [ ] 基础设置模块(主题切换、开机自启、语言设置)
|
- [x] 基础设置模块(主题切换、开机自启)
|
||||||
- [ ] 系统托盘(Tray)
|
- [x] 系统托盘基础(Tray)
|
||||||
- [ ] 日志系统(Log)
|
- [x] 日志系统(Log)
|
||||||
- [ ] 进程管理架构(子进程生命周期管理)
|
- [x] 进程管理架构(子进程生命周期管理)
|
||||||
- [ ] 模块注册机制
|
- [x] 模块注册机制
|
||||||
|
|
||||||
### 第二阶段:核心模块开发
|
### 第二阶段:核心模块开发
|
||||||
|
|
||||||
@@ -127,11 +127,57 @@ Thing/
|
|||||||
|
|
||||||
## 模块管理架构
|
## 模块管理架构
|
||||||
|
|
||||||
项目采用模块化架构,每个模块独立开发,通过统一的注册机制接入主界面:
|
项目采用统一的模块注册机制,每个模块通过 `index.ts` 自描述其配置,由中央注册表统一管理:
|
||||||
|
|
||||||
1. **左侧导航栏**:显示所有已注册模块的名称和图标
|
### 核心文件
|
||||||
2. **右侧内容区域**:显示当前选中模块的详细设置和功能界面
|
|
||||||
3. **模块注册**:通过配置文件或代码注册模块信息(名称、图标、组件路径、权限等)
|
| 文件 | 职责 |
|
||||||
|
|------|------|
|
||||||
|
| `src/types/module.ts` | 模块配置类型定义(`ModuleConfig`、`ModuleMeta`、`ModuleProcessConfig` 等) |
|
||||||
|
| `src/modules/registry.ts` | 模块注册表单例,提供注册、查询、组件懒加载等功能 |
|
||||||
|
| `src/modules/index.ts` | 模块聚合入口,导入并注册所有模块 |
|
||||||
|
| `src/modules/icons.ts` | 模块图标映射表 |
|
||||||
|
| `src/modules/<name>/index.ts` | 各模块的配置声明(名称、图标、组件、搜索项、进程配置等) |
|
||||||
|
|
||||||
|
### 模块配置示例
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/modules/proxy/index.ts
|
||||||
|
export const moduleConfig: ModuleConfig = {
|
||||||
|
id: 'proxy',
|
||||||
|
name: '代理管理',
|
||||||
|
icon: 'proxy',
|
||||||
|
description: '系统代理切换、订阅管理与延迟测速',
|
||||||
|
category: 'network',
|
||||||
|
loader: () => import('./ProxyModule.vue'),
|
||||||
|
searchItems: [...],
|
||||||
|
process: {
|
||||||
|
name: 'mihomo',
|
||||||
|
executable: '',
|
||||||
|
autoStart: false,
|
||||||
|
restartOnCrash: true,
|
||||||
|
maxRestarts: 3
|
||||||
|
},
|
||||||
|
order: 10
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 进程管理架构
|
||||||
|
|
||||||
|
需要管理外部子进程的模块(如代理的 mihomo、下载器的 aria2)通过 `ModuleProcessConfig` 声明进程配置,由 Rust 端的 `ProcessManager` 统一管理生命周期:
|
||||||
|
|
||||||
|
- **Rust 端** (`src-tauri/src/process_manager.rs`):管理子进程的启动、停止、重启和崩溃检测
|
||||||
|
- **前端** (`src/stores/processStore.ts`):Pinia store,通过 Tauri IPC 调用 Rust 命令,监听进程状态变更事件
|
||||||
|
- **后台监控线程**:每 3 秒检查进程状态,崩溃时自动重启(可配置),并通过 Tauri 事件通知前端
|
||||||
|
- **应用退出清理**:退出时自动停止所有子进程
|
||||||
|
|
||||||
|
### 新增模块流程
|
||||||
|
|
||||||
|
1. 创建 `src/modules/<name>/` 目录,编写 `ModuleComponent.vue` 和 `index.ts`
|
||||||
|
2. 在 `index.ts` 中导出 `moduleConfig: ModuleConfig`
|
||||||
|
3. 在 `src/modules/index.ts` 中添加导入
|
||||||
|
4. 在 `src/modules/icons.ts` 中添加图标映射
|
||||||
|
5. 如需 Rust 命令,在 `src-tauri/src/` 中创建对应模块
|
||||||
|
|
||||||
## 构建与运行
|
## 构建与运行
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"@lucide/vue": "^1.24.0",
|
"@lucide/vue": "^1.24.0",
|
||||||
"@tailwindcss/vite": "^4.3.2",
|
"@tailwindcss/vite": "^4.3.2",
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
|
"@tauri-apps/plugin-autostart": "~2",
|
||||||
"@tauri-apps/plugin-opener": "^2",
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
"@vueuse/core": "^14.3.0",
|
"@vueuse/core": "^14.3.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
@@ -227,6 +228,8 @@
|
|||||||
|
|
||||||
"@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.11.4", "https://registry.npmmirror.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.4.tgz", { "os": "win32", "cpu": "x64" }, "sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw=="],
|
"@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.11.4", "https://registry.npmmirror.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.4.tgz", { "os": "win32", "cpu": "x64" }, "sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw=="],
|
||||||
|
|
||||||
|
"@tauri-apps/plugin-autostart": ["@tauri-apps/plugin-autostart@2.5.1", "https://registry.npmmirror.com/@tauri-apps/plugin-autostart/-/plugin-autostart-2.5.1.tgz", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-zS/xx7yzveCcotkA+8TqkI2lysmG2wvQXv2HGAVExITmnFfHAdj1arGsbbfs3o6EktRHf6l34pJxc3YGG2mg7w=="],
|
||||||
|
|
||||||
"@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.4", "https://registry.npmmirror.com/@tauri-apps/plugin-opener/-/plugin-opener-2.5.4.tgz", { "dependencies": { "@tauri-apps/api": "^2.11.0" } }, "sha512-1HnPkb+AmgO29HBazm4uPLKB+r7zzcTBW1d0fyYp1uP+jwtpoiNDGKMMzz58SFp49nOIrxdE3aUJtT57lfO9CQ=="],
|
"@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.4", "https://registry.npmmirror.com/@tauri-apps/plugin-opener/-/plugin-opener-2.5.4.tgz", { "dependencies": { "@tauri-apps/api": "^2.11.0" } }, "sha512-1HnPkb+AmgO29HBazm4uPLKB+r7zzcTBW1d0fyYp1uP+jwtpoiNDGKMMzz58SFp49nOIrxdE3aUJtT57lfO9CQ=="],
|
||||||
|
|
||||||
"@types/estree": ["@types/estree@1.0.9", "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
|
"@types/estree": ["@types/estree@1.0.9", "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
|
||||||
|
|||||||
Generated
+34
-78
@@ -11,6 +11,7 @@
|
|||||||
"@lucide/vue": "^1.24.0",
|
"@lucide/vue": "^1.24.0",
|
||||||
"@tailwindcss/vite": "^4.3.2",
|
"@tailwindcss/vite": "^4.3.2",
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
|
"@tauri-apps/plugin-autostart": "~2",
|
||||||
"@tauri-apps/plugin-opener": "^2",
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
"@vueuse/core": "^14.3.0",
|
"@vueuse/core": "^14.3.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
"tailwind-merge": "^3.6.0",
|
"tailwind-merge": "^3.6.0",
|
||||||
"tailwindcss": "^4.3.2",
|
"tailwindcss": "^4.3.2",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
|
"vue-draggable-plus": "^0.6.1",
|
||||||
"vue-sonner": "^2.0.9"
|
"vue-sonner": "^2.0.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -693,9 +695,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -709,9 +708,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -725,9 +721,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -741,9 +734,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -757,9 +747,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -773,9 +760,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -789,9 +773,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -805,9 +786,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -821,9 +799,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -837,9 +812,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -853,9 +825,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -869,9 +838,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -885,9 +851,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1098,9 +1061,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1117,9 +1077,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1136,9 +1093,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1155,9 +1109,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1359,9 +1310,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0 OR MIT",
|
"license": "Apache-2.0 OR MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1379,9 +1327,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0 OR MIT",
|
"license": "Apache-2.0 OR MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1399,9 +1344,6 @@
|
|||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0 OR MIT",
|
"license": "Apache-2.0 OR MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1419,9 +1361,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0 OR MIT",
|
"license": "Apache-2.0 OR MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1439,9 +1378,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0 OR MIT",
|
"license": "Apache-2.0 OR MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1500,6 +1436,15 @@
|
|||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tauri-apps/plugin-autostart": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@tauri-apps/plugin-autostart/-/plugin-autostart-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-zS/xx7yzveCcotkA+8TqkI2lysmG2wvQXv2HGAVExITmnFfHAdj1arGsbbfs3o6EktRHf6l34pJxc3YGG2mg7w==",
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^2.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tauri-apps/plugin-opener": {
|
"node_modules/@tauri-apps/plugin-opener": {
|
||||||
"version": "2.5.4",
|
"version": "2.5.4",
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
@@ -1521,6 +1466,12 @@
|
|||||||
"undici-types": "~8.3.0"
|
"undici-types": "~8.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/sortablejs": {
|
||||||
|
"version": "1.15.9",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@types/sortablejs/-/sortablejs-1.15.9.tgz",
|
||||||
|
"integrity": "sha512-7HP+rZGE2p886PKV9c9OJzLBI6BBJu1O7lJGYnPyG3fS4/duUCcngkNCjsLwIMV+WMqANe3tt4irrXHSIe68OQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/web-bluetooth": {
|
"node_modules/@types/web-bluetooth": {
|
||||||
"version": "0.0.21",
|
"version": "0.0.21",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
|
||||||
@@ -2108,9 +2059,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2131,9 +2079,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2154,9 +2099,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2177,9 +2119,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2628,6 +2567,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vue-draggable-plus": {
|
||||||
|
"version": "0.6.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/vue-draggable-plus/-/vue-draggable-plus-0.6.1.tgz",
|
||||||
|
"integrity": "sha512-FbtQ/fuoixiOfTZzG3yoPl4JAo9HJXRHmBQZFB9x2NYCh6pq0TomHf7g5MUmpaDYv+LU2n6BPq2YN9sBO+FbIg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/sortablejs": "^1.15.8"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/sortablejs": "^1.15.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@vue/composition-api": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vue-sonner": {
|
"node_modules/vue-sonner": {
|
||||||
"version": "2.0.9",
|
"version": "2.0.9",
|
||||||
"resolved": "https://registry.npmmirror.com/vue-sonner/-/vue-sonner-2.0.9.tgz",
|
"resolved": "https://registry.npmmirror.com/vue-sonner/-/vue-sonner-2.0.9.tgz",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"@lucide/vue": "^1.24.0",
|
"@lucide/vue": "^1.24.0",
|
||||||
"@tailwindcss/vite": "^4.3.2",
|
"@tailwindcss/vite": "^4.3.2",
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
|
"@tauri-apps/plugin-autostart": "~2",
|
||||||
"@tauri-apps/plugin-opener": "^2",
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
"@vueuse/core": "^14.3.0",
|
"@vueuse/core": "^14.3.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
@@ -22,6 +23,7 @@
|
|||||||
"tailwind-merge": "^3.6.0",
|
"tailwind-merge": "^3.6.0",
|
||||||
"tailwindcss": "^4.3.2",
|
"tailwindcss": "^4.3.2",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
|
"vue-draggable-plus": "^0.6.1",
|
||||||
"vue-sonner": "^2.0.9"
|
"vue-sonner": "^2.0.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
Generated
+550
-13
@@ -207,6 +207,17 @@ version = "1.1.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "auto-launch"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1f012b8cc0c850f34117ec8252a44418f2e34a2cf501de89e29b241ae5f79471"
|
||||||
|
dependencies = [
|
||||||
|
"dirs 4.0.0",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"winreg 0.10.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.5.1"
|
version = "1.5.1"
|
||||||
@@ -460,8 +471,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
|
checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"serde",
|
"serde",
|
||||||
|
"wasm-bindgen",
|
||||||
"windows-link 0.2.1",
|
"windows-link 0.2.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -494,6 +507,16 @@ dependencies = [
|
|||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation"
|
||||||
|
version = "0.9.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
|
||||||
|
dependencies = [
|
||||||
|
"core-foundation-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
@@ -517,9 +540,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97"
|
checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.0",
|
"bitflags 2.13.0",
|
||||||
"core-foundation",
|
"core-foundation 0.10.1",
|
||||||
"core-graphics-types",
|
"core-graphics-types",
|
||||||
"foreign-types",
|
"foreign-types 0.5.0",
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -530,7 +553,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
|
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.0",
|
"bitflags 2.13.0",
|
||||||
"core-foundation",
|
"core-foundation 0.10.1",
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -701,13 +724,33 @@ dependencies = [
|
|||||||
"crypto-common",
|
"crypto-common",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs"
|
||||||
|
version = "4.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
|
||||||
|
dependencies = [
|
||||||
|
"dirs-sys 0.3.7",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dirs"
|
name = "dirs"
|
||||||
version = "6.0.0"
|
version = "6.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
|
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dirs-sys",
|
"dirs-sys 0.5.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs-sys"
|
||||||
|
version = "0.3.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"redox_users 0.4.6",
|
||||||
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -718,7 +761,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"option-ext",
|
"option-ext",
|
||||||
"redox_users",
|
"redox_users 0.5.2",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -845,7 +888,7 @@ dependencies = [
|
|||||||
"rustc_version",
|
"rustc_version",
|
||||||
"toml 1.1.2+spec-1.1.0",
|
"toml 1.1.2+spec-1.1.0",
|
||||||
"vswhom",
|
"vswhom",
|
||||||
"winreg",
|
"winreg 0.55.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -854,6 +897,15 @@ version = "1.2.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
|
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "encoding_rs"
|
||||||
|
version = "0.8.35"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "endi"
|
name = "endi"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
@@ -982,6 +1034,15 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foreign-types"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
||||||
|
dependencies = [
|
||||||
|
"foreign-types-shared 0.1.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "foreign-types"
|
name = "foreign-types"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -989,7 +1050,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
|
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"foreign-types-macros",
|
"foreign-types-macros",
|
||||||
"foreign-types-shared",
|
"foreign-types-shared 0.3.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1003,6 +1064,12 @@ dependencies = [
|
|||||||
"syn 2.0.118",
|
"syn 2.0.118",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foreign-types-shared"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "foreign-types-shared"
|
name = "foreign-types-shared"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@@ -1393,6 +1460,25 @@ dependencies = [
|
|||||||
"syn 2.0.118",
|
"syn 2.0.118",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "h2"
|
||||||
|
version = "0.4.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155"
|
||||||
|
dependencies = [
|
||||||
|
"atomic-waker",
|
||||||
|
"bytes",
|
||||||
|
"fnv",
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
"http",
|
||||||
|
"indexmap 2.14.0",
|
||||||
|
"slab",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.12.3"
|
version = "0.12.3"
|
||||||
@@ -1488,6 +1574,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"h2",
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"httparse",
|
"httparse",
|
||||||
@@ -1498,6 +1585,37 @@ dependencies = [
|
|||||||
"want",
|
"want",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper-rustls"
|
||||||
|
version = "0.27.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
|
||||||
|
dependencies = [
|
||||||
|
"http",
|
||||||
|
"hyper",
|
||||||
|
"hyper-util",
|
||||||
|
"rustls",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
|
"tower-service",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper-tls"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"http-body-util",
|
||||||
|
"hyper",
|
||||||
|
"hyper-util",
|
||||||
|
"native-tls",
|
||||||
|
"tokio",
|
||||||
|
"tokio-native-tls",
|
||||||
|
"tower-service",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-util"
|
name = "hyper-util"
|
||||||
version = "0.1.20"
|
version = "0.1.20"
|
||||||
@@ -1516,9 +1634,11 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"socket2",
|
"socket2",
|
||||||
|
"system-configuration",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"windows-registry",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1997,6 +2117,23 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "native-tls"
|
||||||
|
version = "0.2.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"openssl",
|
||||||
|
"openssl-probe",
|
||||||
|
"openssl-sys",
|
||||||
|
"schannel",
|
||||||
|
"security-framework",
|
||||||
|
"security-framework-sys",
|
||||||
|
"tempfile",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ndk"
|
name = "ndk"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -2276,6 +2413,49 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl"
|
||||||
|
version = "0.10.81"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.13.0",
|
||||||
|
"cfg-if",
|
||||||
|
"foreign-types 0.3.2",
|
||||||
|
"libc",
|
||||||
|
"openssl-macros",
|
||||||
|
"openssl-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-macros"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.118",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-probe"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-sys"
|
||||||
|
version = "0.9.117"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"pkg-config",
|
||||||
|
"vcpkg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "option-ext"
|
name = "option-ext"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -2609,6 +2789,17 @@ dependencies = [
|
|||||||
"bitflags 2.13.0",
|
"bitflags 2.13.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_users"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.2.17",
|
||||||
|
"libredox",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_users"
|
name = "redox_users"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
@@ -2669,6 +2860,46 @@ version = "0.8.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "reqwest"
|
||||||
|
version = "0.12.28"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"bytes",
|
||||||
|
"encoding_rs",
|
||||||
|
"futures-core",
|
||||||
|
"h2",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"hyper",
|
||||||
|
"hyper-rustls",
|
||||||
|
"hyper-tls",
|
||||||
|
"hyper-util",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"mime",
|
||||||
|
"native-tls",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project-lite",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_urlencoded",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tokio",
|
||||||
|
"tokio-native-tls",
|
||||||
|
"tower",
|
||||||
|
"tower-http",
|
||||||
|
"tower-service",
|
||||||
|
"url",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
version = "0.13.4"
|
version = "0.13.4"
|
||||||
@@ -2703,6 +2934,20 @@ dependencies = [
|
|||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ring"
|
||||||
|
version = "0.17.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"cfg-if",
|
||||||
|
"getrandom 0.2.17",
|
||||||
|
"libc",
|
||||||
|
"untrusted",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
version = "2.1.3"
|
version = "2.1.3"
|
||||||
@@ -2731,12 +2976,51 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls"
|
||||||
|
version = "0.23.42"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"rustls-webpki",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-pki-types"
|
||||||
|
version = "1.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046"
|
||||||
|
dependencies = [
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-webpki"
|
||||||
|
version = "0.103.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
|
||||||
|
dependencies = [
|
||||||
|
"ring",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"untrusted",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.23"
|
version = "1.0.23"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "same-file"
|
name = "same-file"
|
||||||
version = "1.0.6"
|
version = "1.0.6"
|
||||||
@@ -2746,6 +3030,15 @@ dependencies = [
|
|||||||
"winapi-util",
|
"winapi-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "schannel"
|
||||||
|
version = "0.1.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "schemars"
|
name = "schemars"
|
||||||
version = "0.8.22"
|
version = "0.8.22"
|
||||||
@@ -2803,6 +3096,29 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "security-framework"
|
||||||
|
version = "3.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.13.0",
|
||||||
|
"core-foundation 0.10.1",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"libc",
|
||||||
|
"security-framework-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "security-framework-sys"
|
||||||
|
version = "2.17.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
|
||||||
|
dependencies = [
|
||||||
|
"core-foundation-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "selectors"
|
name = "selectors"
|
||||||
version = "0.36.1"
|
version = "0.36.1"
|
||||||
@@ -2927,6 +3243,18 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_urlencoded"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||||
|
dependencies = [
|
||||||
|
"form_urlencoded",
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_with"
|
name = "serde_with"
|
||||||
version = "3.21.0"
|
version = "3.21.0"
|
||||||
@@ -2959,6 +3287,19 @@ dependencies = [
|
|||||||
"syn 2.0.118",
|
"syn 2.0.118",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_yaml"
|
||||||
|
version = "0.9.34+deprecated"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap 2.14.0",
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
"unsafe-libyaml",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serialize-to-javascript"
|
name = "serialize-to-javascript"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@@ -3135,6 +3476,12 @@ version = "0.11.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "subtle"
|
||||||
|
version = "2.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "swift-rs"
|
name = "swift-rs"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
@@ -3187,6 +3534,27 @@ dependencies = [
|
|||||||
"syn 2.0.118",
|
"syn 2.0.118",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "system-configuration"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.13.0",
|
||||||
|
"core-foundation 0.9.4",
|
||||||
|
"system-configuration-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "system-configuration-sys"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
|
||||||
|
dependencies = [
|
||||||
|
"core-foundation-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "system-deps"
|
name = "system-deps"
|
||||||
version = "6.2.2"
|
version = "6.2.2"
|
||||||
@@ -3208,7 +3576,7 @@ checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.0",
|
"bitflags 2.13.0",
|
||||||
"block2",
|
"block2",
|
||||||
"core-foundation",
|
"core-foundation 0.10.1",
|
||||||
"core-graphics",
|
"core-graphics",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"dbus",
|
"dbus",
|
||||||
@@ -3266,7 +3634,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
"cookie",
|
"cookie",
|
||||||
"dirs",
|
"dirs 6.0.0",
|
||||||
"dunce",
|
"dunce",
|
||||||
"embed_plist",
|
"embed_plist",
|
||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
@@ -3287,7 +3655,7 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"plist",
|
"plist",
|
||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
"reqwest",
|
"reqwest 0.13.4",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_repr",
|
"serde_repr",
|
||||||
@@ -3316,7 +3684,7 @@ checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"cargo_toml",
|
"cargo_toml",
|
||||||
"dirs",
|
"dirs 6.0.0",
|
||||||
"glob",
|
"glob",
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"json-patch",
|
"json-patch",
|
||||||
@@ -3386,6 +3754,20 @@ dependencies = [
|
|||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-autostart"
|
||||||
|
version = "2.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "459383cebc193cdd03d1ba4acc40f2c408a7abce419d64bdcd2d745bc2886f70"
|
||||||
|
dependencies = [
|
||||||
|
"auto-launch",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-opener"
|
name = "tauri-plugin-opener"
|
||||||
version = "2.5.4"
|
version = "2.5.4"
|
||||||
@@ -3534,11 +3916,17 @@ dependencies = [
|
|||||||
name = "thing"
|
name = "thing"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"reqwest 0.12.28",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"serde_yaml",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
|
"tauri-plugin-autostart",
|
||||||
"tauri-plugin-opener",
|
"tauri-plugin-opener",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
"winreg 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3650,6 +4038,26 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-native-tls"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
|
||||||
|
dependencies = [
|
||||||
|
"native-tls",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-rustls"
|
||||||
|
version = "0.26.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
|
||||||
|
dependencies = [
|
||||||
|
"rustls",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.7.18"
|
version = "0.7.18"
|
||||||
@@ -3866,7 +4274,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "65ba1e5f6b9ef9fd87e21b9c6f351554dbd717960089168fcfdef854686961dc"
|
checksum = "65ba1e5f6b9ef9fd87e21b9c6f351554dbd717960089168fcfdef854686961dc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"dirs",
|
"dirs 6.0.0",
|
||||||
"libappindicator",
|
"libappindicator",
|
||||||
"muda",
|
"muda",
|
||||||
"objc2",
|
"objc2",
|
||||||
@@ -3963,6 +4371,18 @@ version = "1.13.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
|
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unsafe-libyaml"
|
||||||
|
version = "0.2.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "untrusted"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.5.8"
|
version = "2.5.8"
|
||||||
@@ -4006,6 +4426,12 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vcpkg"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version-compare"
|
name = "version-compare"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -4391,6 +4817,17 @@ dependencies = [
|
|||||||
"windows-link 0.1.3",
|
"windows-link 0.1.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-registry"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link 0.2.1",
|
||||||
|
"windows-result 0.4.1",
|
||||||
|
"windows-strings 0.5.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-result"
|
name = "windows-result"
|
||||||
version = "0.3.4"
|
version = "0.3.4"
|
||||||
@@ -4436,6 +4873,24 @@ dependencies = [
|
|||||||
"windows-targets 0.42.2",
|
"windows-targets 0.42.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.48.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets 0.48.5",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.52.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.59.0"
|
version = "0.59.0"
|
||||||
@@ -4469,6 +4924,21 @@ dependencies = [
|
|||||||
"windows_x86_64_msvc 0.42.2",
|
"windows_x86_64_msvc 0.42.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-targets"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
|
||||||
|
dependencies = [
|
||||||
|
"windows_aarch64_gnullvm 0.48.5",
|
||||||
|
"windows_aarch64_msvc 0.48.5",
|
||||||
|
"windows_i686_gnu 0.48.5",
|
||||||
|
"windows_i686_msvc 0.48.5",
|
||||||
|
"windows_x86_64_gnu 0.48.5",
|
||||||
|
"windows_x86_64_gnullvm 0.48.5",
|
||||||
|
"windows_x86_64_msvc 0.48.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-targets"
|
name = "windows-targets"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
@@ -4509,6 +4979,12 @@ version = "0.42.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
|
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_gnullvm"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_aarch64_gnullvm"
|
name = "windows_aarch64_gnullvm"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
@@ -4521,6 +4997,12 @@ version = "0.42.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
|
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_msvc"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_aarch64_msvc"
|
name = "windows_aarch64_msvc"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
@@ -4533,6 +5015,12 @@ version = "0.42.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
|
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnu"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_gnu"
|
name = "windows_i686_gnu"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
@@ -4551,6 +5039,12 @@ version = "0.42.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
|
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_msvc"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_msvc"
|
name = "windows_i686_msvc"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
@@ -4563,6 +5057,12 @@ version = "0.42.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
|
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnu"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnu"
|
name = "windows_x86_64_gnu"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
@@ -4575,6 +5075,12 @@ version = "0.42.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
|
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnullvm"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnullvm"
|
name = "windows_x86_64_gnullvm"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
@@ -4587,6 +5093,12 @@ version = "0.42.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
|
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_msvc"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_msvc"
|
name = "windows_x86_64_msvc"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
@@ -4617,6 +5129,25 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winreg"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
|
||||||
|
dependencies = [
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winreg"
|
||||||
|
version = "0.52.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"windows-sys 0.48.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winreg"
|
name = "winreg"
|
||||||
version = "0.55.0"
|
version = "0.55.0"
|
||||||
@@ -4649,7 +5180,7 @@ dependencies = [
|
|||||||
"block2",
|
"block2",
|
||||||
"cookie",
|
"cookie",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"dirs",
|
"dirs 6.0.0",
|
||||||
"dom_query",
|
"dom_query",
|
||||||
"dpi",
|
"dpi",
|
||||||
"dunce",
|
"dunce",
|
||||||
@@ -4809,6 +5340,12 @@ dependencies = [
|
|||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zeroize"
|
||||||
|
version = "1.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerotrie"
|
name = "zerotrie"
|
||||||
version = "0.2.4"
|
version = "0.2.4"
|
||||||
|
|||||||
@@ -22,4 +22,14 @@ tauri = { version = "2", features = ["tray-icon"] }
|
|||||||
tauri-plugin-opener = "2"
|
tauri-plugin-opener = "2"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
serde_yaml = "0.9"
|
||||||
|
chrono = "0.4"
|
||||||
|
reqwest = { version = "0.12", features = ["json"] }
|
||||||
|
|
||||||
|
[target.'cfg(windows)'.dependencies]
|
||||||
|
winreg = "0.52"
|
||||||
|
windows-sys = { version = "0.52", features = ["Win32_Networking_WinInet", "Win32_Foundation"] }
|
||||||
|
|
||||||
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
|
tauri-plugin-autostart = "2"
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -15,6 +15,7 @@
|
|||||||
"core:window:allow-set-focus",
|
"core:window:allow-set-focus",
|
||||||
"core:window:allow-start-dragging",
|
"core:window:allow-start-dragging",
|
||||||
"core:window:allow-set-effects",
|
"core:window:allow-set-effects",
|
||||||
"core:window:allow-set-background-color"
|
"core:window:allow-set-background-color",
|
||||||
|
"core:window:allow-set-theme"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"identifier": "desktop-capability",
|
||||||
|
"platforms": [
|
||||||
|
"macOS",
|
||||||
|
"windows",
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"windows": [
|
||||||
|
"main"
|
||||||
|
],
|
||||||
|
"permissions": [
|
||||||
|
"autostart:default"
|
||||||
|
]
|
||||||
|
}
|
||||||
+85
-3
@@ -1,21 +1,92 @@
|
|||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
|
|
||||||
|
mod logger;
|
||||||
|
mod mihomo_manager;
|
||||||
|
mod process_manager;
|
||||||
|
|
||||||
|
use logger::{
|
||||||
|
clear_logs, get_log_info, get_logs, log_message, LogManager,
|
||||||
|
};
|
||||||
|
use mihomo_manager::{
|
||||||
|
proxy_activate_profile, proxy_clear_system_proxy, proxy_close_connection, proxy_delete_profile,
|
||||||
|
proxy_get_connections, proxy_get_proxies, proxy_get_settings, proxy_get_system_proxy,
|
||||||
|
proxy_import_profile, proxy_kernel_info, proxy_patch_configs, proxy_restart, proxy_save_settings,
|
||||||
|
proxy_select_proxy, proxy_set_system_proxy, proxy_start, proxy_status, proxy_stop,
|
||||||
|
proxy_test_delay, proxy_update_profile, proxy_version, MihomoManager,
|
||||||
|
};
|
||||||
|
use process_manager::{
|
||||||
|
get_all_process_status, get_process_status, start_monitoring_thread, start_process,
|
||||||
|
stop_all_processes, stop_process, ProcessManager,
|
||||||
|
};
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn greet(name: &str) -> String {
|
fn greet(name: &str) -> String {
|
||||||
format!("Hello, {}! You've been greeted from Rust!", name)
|
format!("Hello, {}! You've been greeted from Rust!", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn quit_app() {
|
fn quit_app(state: tauri::State<'_, ProcessManager>) {
|
||||||
|
// 退出前停止所有子进程
|
||||||
|
state.stop_all();
|
||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
|
.plugin(tauri_plugin_autostart::Builder::new().build())
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.invoke_handler(tauri::generate_handler![greet, quit_app])
|
.manage(ProcessManager::new())
|
||||||
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
greet,
|
||||||
|
quit_app,
|
||||||
|
start_process,
|
||||||
|
stop_process,
|
||||||
|
get_process_status,
|
||||||
|
get_all_process_status,
|
||||||
|
stop_all_processes,
|
||||||
|
log_message,
|
||||||
|
get_logs,
|
||||||
|
clear_logs,
|
||||||
|
get_log_info,
|
||||||
|
proxy_get_settings,
|
||||||
|
proxy_save_settings,
|
||||||
|
proxy_kernel_info,
|
||||||
|
proxy_status,
|
||||||
|
proxy_start,
|
||||||
|
proxy_stop,
|
||||||
|
proxy_restart,
|
||||||
|
proxy_version,
|
||||||
|
proxy_get_proxies,
|
||||||
|
proxy_select_proxy,
|
||||||
|
proxy_test_delay,
|
||||||
|
proxy_get_connections,
|
||||||
|
proxy_close_connection,
|
||||||
|
proxy_patch_configs,
|
||||||
|
proxy_import_profile,
|
||||||
|
proxy_update_profile,
|
||||||
|
proxy_delete_profile,
|
||||||
|
proxy_activate_profile,
|
||||||
|
proxy_set_system_proxy,
|
||||||
|
proxy_clear_system_proxy,
|
||||||
|
proxy_get_system_proxy
|
||||||
|
])
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
|
// 初始化日志系统,日志目录: {app_data_dir}/logs/
|
||||||
|
let log_dir = app
|
||||||
|
.path()
|
||||||
|
.app_data_dir()
|
||||||
|
.unwrap_or_else(|_| std::path::PathBuf::from("."))
|
||||||
|
.join("logs");
|
||||||
|
app.manage(LogManager::new(log_dir));
|
||||||
|
|
||||||
|
// 初始化 MihomoManager,数据目录: {app_data_dir}/proxy/
|
||||||
|
let app_data_dir = app
|
||||||
|
.path()
|
||||||
|
.app_data_dir()
|
||||||
|
.unwrap_or_else(|_| std::path::PathBuf::from("."));
|
||||||
|
app.manage(MihomoManager::new(app_data_dir));
|
||||||
|
|
||||||
let open = tauri::menu::MenuItem::with_id(app, "open", "设置", true, None::<&str>)?;
|
let open = tauri::menu::MenuItem::with_id(app, "open", "设置", true, None::<&str>)?;
|
||||||
let quit = tauri::menu::MenuItem::with_id(app, "quit", "退出", true, None::<&str>)?;
|
let quit = tauri::menu::MenuItem::with_id(app, "quit", "退出", true, None::<&str>)?;
|
||||||
let menu = tauri::menu::Menu::with_items(app, &[&open, &quit])?;
|
let menu = tauri::menu::Menu::with_items(app, &[&open, &quit])?;
|
||||||
@@ -32,12 +103,20 @@ pub fn run() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"quit" => {
|
"quit" => {
|
||||||
|
// 退出前停止所有子进程
|
||||||
|
if let Some(pm) = app.try_state::<ProcessManager>() {
|
||||||
|
pm.stop_all();
|
||||||
|
}
|
||||||
app.exit(0);
|
app.exit(0);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
})
|
})
|
||||||
.on_tray_icon_event(|tray, event| {
|
.on_tray_icon_event(|tray, event| {
|
||||||
if let tauri::tray::TrayIconEvent::Click { button: tauri::tray::MouseButton::Left, .. } = event {
|
if let tauri::tray::TrayIconEvent::Click {
|
||||||
|
button: tauri::tray::MouseButton::Left,
|
||||||
|
..
|
||||||
|
} = event
|
||||||
|
{
|
||||||
let app = tray.app_handle();
|
let app = tray.app_handle();
|
||||||
if let Some(window) = app.get_webview_window("main") {
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
window.show().ok();
|
window.show().ok();
|
||||||
@@ -47,6 +126,9 @@ pub fn run() {
|
|||||||
})
|
})
|
||||||
.build(app)?;
|
.build(app)?;
|
||||||
|
|
||||||
|
// 启动进程监控线程
|
||||||
|
start_monitoring_thread(app.handle().clone());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.on_window_event(|window, event| {
|
.on_window_event(|window, event| {
|
||||||
|
|||||||
@@ -0,0 +1,305 @@
|
|||||||
|
use chrono::Local;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fs::{self, File, OpenOptions};
|
||||||
|
use std::io::{BufRead, BufReader, Write};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// 日志级别
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum LogLevel {
|
||||||
|
Debug,
|
||||||
|
Info,
|
||||||
|
Warn,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 单条日志记录
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct LogEntry {
|
||||||
|
pub timestamp: String,
|
||||||
|
pub level: LogLevel,
|
||||||
|
pub module: String,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 日志文件信息
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct LogInfo {
|
||||||
|
pub log_dir: String,
|
||||||
|
pub log_files: Vec<String>,
|
||||||
|
pub total_size_bytes: u64,
|
||||||
|
pub max_file_size_bytes: u64,
|
||||||
|
pub max_files: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 日志管理器 —— 负责文件轮转、写入、查询
|
||||||
|
pub struct LogManager {
|
||||||
|
log_dir: PathBuf,
|
||||||
|
max_file_size: u64,
|
||||||
|
max_files: u32,
|
||||||
|
base_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LogManager {
|
||||||
|
pub fn new(log_dir: PathBuf) -> Self {
|
||||||
|
fs::create_dir_all(&log_dir).ok();
|
||||||
|
Self {
|
||||||
|
log_dir,
|
||||||
|
max_file_size: 5 * 1024 * 1024,
|
||||||
|
max_files: 5,
|
||||||
|
base_name: "thing".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 写入一条日志
|
||||||
|
pub fn log(&self, level: LogLevel, module: &str, message: &str) {
|
||||||
|
let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
|
||||||
|
let level_str = match level {
|
||||||
|
LogLevel::Debug => "DEBUG",
|
||||||
|
LogLevel::Info => "INFO",
|
||||||
|
LogLevel::Warn => "WARN",
|
||||||
|
LogLevel::Error => "ERROR",
|
||||||
|
};
|
||||||
|
let line = format!(
|
||||||
|
"[{}] [{}] [{}] {}\n",
|
||||||
|
timestamp, level_str, module, message
|
||||||
|
);
|
||||||
|
|
||||||
|
let current_log = self.log_dir.join(format!("{}.log", self.base_name));
|
||||||
|
|
||||||
|
// 检查是否需要轮转
|
||||||
|
if let Ok(meta) = fs::metadata(¤t_log) {
|
||||||
|
if meta.len() >= self.max_file_size {
|
||||||
|
self.rotate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 追加写入
|
||||||
|
if let Ok(mut file) = OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.append(true)
|
||||||
|
.open(¤t_log)
|
||||||
|
{
|
||||||
|
let _ = file.write_all(line.as_bytes());
|
||||||
|
let _ = file.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 日志文件轮转: thing.log → thing.1.log, thing.1.log → thing.2.log, …
|
||||||
|
fn rotate(&self) {
|
||||||
|
// 删除最旧的文件
|
||||||
|
let oldest = self
|
||||||
|
.log_dir
|
||||||
|
.join(format!("{}.{}.log", self.base_name, self.max_files));
|
||||||
|
fs::remove_file(&oldest).ok();
|
||||||
|
|
||||||
|
// 依次重命名
|
||||||
|
for i in (1..self.max_files).rev() {
|
||||||
|
let src = self
|
||||||
|
.log_dir
|
||||||
|
.join(format!("{}.{}.log", self.base_name, i));
|
||||||
|
let dst = self
|
||||||
|
.log_dir
|
||||||
|
.join(format!("{}.{}.log", self.base_name, i + 1));
|
||||||
|
fs::rename(&src, &dst).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
// thing.log → thing.1.log
|
||||||
|
let current = self.log_dir.join(format!("{}.log", self.base_name));
|
||||||
|
let first_rotated = self
|
||||||
|
.log_dir
|
||||||
|
.join(format!("{}.1.log", self.base_name));
|
||||||
|
fs::rename(¤t, &first_rotated).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 读取日志(支持按模块/级别过滤、条数限制)
|
||||||
|
pub fn get_logs(
|
||||||
|
&self,
|
||||||
|
module: Option<&str>,
|
||||||
|
level: Option<LogLevel>,
|
||||||
|
limit: Option<usize>,
|
||||||
|
) -> Vec<LogEntry> {
|
||||||
|
let mut entries: Vec<LogEntry> = Vec::new();
|
||||||
|
|
||||||
|
// 收集所有日志文件(包括轮转文件)
|
||||||
|
let mut log_files: Vec<PathBuf> = Vec::new();
|
||||||
|
let current = self.log_dir.join(format!("{}.log", self.base_name));
|
||||||
|
if current.exists() {
|
||||||
|
log_files.push(current);
|
||||||
|
}
|
||||||
|
for i in 1..=self.max_files {
|
||||||
|
let rotated = self
|
||||||
|
.log_dir
|
||||||
|
.join(format!("{}.{}.log", self.base_name, i));
|
||||||
|
if rotated.exists() {
|
||||||
|
log_files.push(rotated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for path in &log_files {
|
||||||
|
if let Ok(file) = File::open(path) {
|
||||||
|
for line in BufReader::new(file).lines().flatten() {
|
||||||
|
if let Some(entry) = Self::parse_line(&line) {
|
||||||
|
if let Some(ref m) = module {
|
||||||
|
if entry.module != *m {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(ref l) = level {
|
||||||
|
if entry.level != *l {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entries.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按时间戳降序(最新在前)
|
||||||
|
entries.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
|
||||||
|
|
||||||
|
if let Some(n) = limit {
|
||||||
|
entries.truncate(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
entries
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 解析一行日志: `[YYYY-MM-DD HH:MM:SS] [LEVEL] [MODULE] message`
|
||||||
|
fn parse_line(line: &str) -> Option<LogEntry> {
|
||||||
|
let line = line.trim();
|
||||||
|
if line.len() < 22 || !line.starts_with('[') {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ts_end = line[1..].find(']')? + 1;
|
||||||
|
let timestamp = line[1..ts_end].to_string();
|
||||||
|
|
||||||
|
let rest = line[ts_end + 1..].trim();
|
||||||
|
if !rest.starts_with('[') {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let lv_end = rest[1..].find(']')? + 1;
|
||||||
|
let level_str = &rest[1..lv_end];
|
||||||
|
let level = match level_str {
|
||||||
|
"DEBUG" => LogLevel::Debug,
|
||||||
|
"INFO" => LogLevel::Info,
|
||||||
|
"WARN" => LogLevel::Warn,
|
||||||
|
"ERROR" => LogLevel::Error,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let rest = rest[lv_end + 1..].trim();
|
||||||
|
if !rest.starts_with('[') {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mod_end = rest[1..].find(']')? + 1;
|
||||||
|
let module = rest[1..mod_end].to_string();
|
||||||
|
|
||||||
|
let message = rest[mod_end + 1..].trim().to_string();
|
||||||
|
|
||||||
|
Some(LogEntry {
|
||||||
|
timestamp,
|
||||||
|
level,
|
||||||
|
module,
|
||||||
|
message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 清空所有日志文件
|
||||||
|
pub fn clear_logs(&self) -> Result<(), String> {
|
||||||
|
let current = self.log_dir.join(format!("{}.log", self.base_name));
|
||||||
|
fs::remove_file(¤t).map_err(|e| e.to_string())?;
|
||||||
|
for i in 1..=self.max_files {
|
||||||
|
let rotated = self
|
||||||
|
.log_dir
|
||||||
|
.join(format!("{}.{}.log", self.base_name, i));
|
||||||
|
fs::remove_file(&rotated).ok();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取日志系统信息
|
||||||
|
pub fn get_info(&self) -> LogInfo {
|
||||||
|
let mut log_files: Vec<String> = Vec::new();
|
||||||
|
let mut total_size: u64 = 0;
|
||||||
|
|
||||||
|
let current = self.log_dir.join(format!("{}.log", self.base_name));
|
||||||
|
if current.exists() {
|
||||||
|
if let Ok(meta) = fs::metadata(¤t) {
|
||||||
|
total_size += meta.len();
|
||||||
|
}
|
||||||
|
log_files.push(format!("{}.log", self.base_name));
|
||||||
|
}
|
||||||
|
for i in 1..=self.max_files {
|
||||||
|
let rotated = self
|
||||||
|
.log_dir
|
||||||
|
.join(format!("{}.{}.log", self.base_name, i));
|
||||||
|
if rotated.exists() {
|
||||||
|
if let Ok(meta) = fs::metadata(&rotated) {
|
||||||
|
total_size += meta.len();
|
||||||
|
}
|
||||||
|
log_files.push(format!("{}.{}.log", self.base_name, i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LogInfo {
|
||||||
|
log_dir: self.log_dir.to_string_lossy().to_string(),
|
||||||
|
log_files,
|
||||||
|
total_size_bytes: total_size,
|
||||||
|
max_file_size_bytes: self.max_file_size,
|
||||||
|
max_files: self.max_files,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Tauri 命令 =====
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn log_message(
|
||||||
|
state: tauri::State<'_, LogManager>,
|
||||||
|
level: String,
|
||||||
|
module: String,
|
||||||
|
message: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let level = match level.as_str() {
|
||||||
|
"debug" => LogLevel::Debug,
|
||||||
|
"info" => LogLevel::Info,
|
||||||
|
"warn" => LogLevel::Warn,
|
||||||
|
"error" => LogLevel::Error,
|
||||||
|
_ => return Err(format!("无效的日志级别: {}", level)),
|
||||||
|
};
|
||||||
|
state.log(level, &module, &message);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_logs(
|
||||||
|
state: tauri::State<'_, LogManager>,
|
||||||
|
module: Option<String>,
|
||||||
|
level: Option<String>,
|
||||||
|
limit: Option<usize>,
|
||||||
|
) -> Vec<LogEntry> {
|
||||||
|
let level = level.and_then(|l| match l.as_str() {
|
||||||
|
"debug" => Some(LogLevel::Debug),
|
||||||
|
"info" => Some(LogLevel::Info),
|
||||||
|
"warn" => Some(LogLevel::Warn),
|
||||||
|
"error" => Some(LogLevel::Error),
|
||||||
|
_ => None,
|
||||||
|
});
|
||||||
|
state.get_logs(module.as_deref(), level, limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn clear_logs(state: tauri::State<'_, LogManager>) -> Result<(), String> {
|
||||||
|
state.clear_logs()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_log_info(state: tauri::State<'_, LogManager>) -> LogInfo {
|
||||||
|
state.get_info()
|
||||||
|
}
|
||||||
@@ -0,0 +1,705 @@
|
|||||||
|
use chrono::Local;
|
||||||
|
use reqwest::Client;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_yaml::Value as YamlValue;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tauri::{AppHandle, Manager};
|
||||||
|
use tauri::path::BaseDirectory;
|
||||||
|
|
||||||
|
use crate::process_manager::{ProcessInfo, ProcessManager, StartProcessParams};
|
||||||
|
|
||||||
|
// ===================== 数据结构 =====================
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ProxySettings {
|
||||||
|
pub mixed_port: u16,
|
||||||
|
pub external_controller: String,
|
||||||
|
pub secret: String,
|
||||||
|
pub mode: String,
|
||||||
|
pub log_level: String,
|
||||||
|
pub allow_lan: bool,
|
||||||
|
pub system_proxy: bool,
|
||||||
|
pub auto_start: bool,
|
||||||
|
pub current_profile: Option<String>,
|
||||||
|
pub profiles: Vec<ProfileMeta>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ProxySettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
mixed_port: 7890,
|
||||||
|
external_controller: "127.0.0.1:9090".into(),
|
||||||
|
secret: String::new(),
|
||||||
|
mode: "rule".into(),
|
||||||
|
log_level: "info".into(),
|
||||||
|
allow_lan: false,
|
||||||
|
system_proxy: false,
|
||||||
|
auto_start: false,
|
||||||
|
current_profile: None,
|
||||||
|
profiles: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ProfileMeta {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub url: String,
|
||||||
|
pub added_at: String,
|
||||||
|
pub updated_at: String,
|
||||||
|
pub size: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct KernelInfo {
|
||||||
|
pub path: String,
|
||||||
|
pub exists: bool,
|
||||||
|
pub version: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ProxyStatus {
|
||||||
|
pub running: bool,
|
||||||
|
pub pid: Option<u32>,
|
||||||
|
pub restart_count: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== MihomoManager =====================
|
||||||
|
|
||||||
|
pub struct MihomoManager {
|
||||||
|
root: PathBuf,
|
||||||
|
client: Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MihomoManager {
|
||||||
|
pub fn new(app_data_dir: PathBuf) -> Self {
|
||||||
|
let root = app_data_dir.join("proxy");
|
||||||
|
for d in ["cores", "mihomo", "profiles", "logs"] {
|
||||||
|
fs::create_dir_all(root.join(d)).ok();
|
||||||
|
}
|
||||||
|
Self {
|
||||||
|
root,
|
||||||
|
client: Client::builder()
|
||||||
|
.build()
|
||||||
|
.unwrap_or_else(|_| Client::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cores_dir(&self) -> PathBuf {
|
||||||
|
self.root.join("cores")
|
||||||
|
}
|
||||||
|
pub fn kernel_path(&self) -> PathBuf {
|
||||||
|
self.cores_dir().join("mihomo.exe")
|
||||||
|
}
|
||||||
|
fn mihomo_dir(&self) -> PathBuf {
|
||||||
|
self.root.join("mihomo")
|
||||||
|
}
|
||||||
|
fn config_path(&self) -> PathBuf {
|
||||||
|
self.mihomo_dir().join("config.yaml")
|
||||||
|
}
|
||||||
|
fn profiles_dir(&self) -> PathBuf {
|
||||||
|
self.root.join("profiles")
|
||||||
|
}
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn logs_dir(&self) -> PathBuf {
|
||||||
|
self.root.join("logs")
|
||||||
|
}
|
||||||
|
fn settings_path(&self) -> PathBuf {
|
||||||
|
self.root.join("settings.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 设置 ----------
|
||||||
|
pub fn load_settings(&self) -> ProxySettings {
|
||||||
|
fs::read_to_string(self.settings_path())
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| serde_json::from_str::<ProxySettings>(&s).ok())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_settings(&self, settings: &ProxySettings) -> Result<(), String> {
|
||||||
|
let s = serde_json::to_string_pretty(settings).map_err(|e| e.to_string())?;
|
||||||
|
fs::write(self.settings_path(), s).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 内核 ----------
|
||||||
|
pub fn kernel_info(&self) -> KernelInfo {
|
||||||
|
let path = self.kernel_path();
|
||||||
|
let exists = path.exists();
|
||||||
|
let version = if exists {
|
||||||
|
std::process::Command::new(&path)
|
||||||
|
.arg("-v")
|
||||||
|
.output()
|
||||||
|
.ok()
|
||||||
|
.and_then(|o| String::from_utf8(o.stdout).ok())
|
||||||
|
.and_then(|s| {
|
||||||
|
s.lines()
|
||||||
|
.find(|l| l.contains("Mihomo Meta") || l.contains("mihomo"))
|
||||||
|
.map(|l| l.trim().to_string())
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
KernelInfo {
|
||||||
|
path: path.to_string_lossy().to_string(),
|
||||||
|
exists,
|
||||||
|
version,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 确保内核就位:若 cores/ 无内核,尝试从 resource 目录复制
|
||||||
|
pub fn prepare_kernel(&self, app: &AppHandle) -> Result<KernelInfo, String> {
|
||||||
|
let kernel = self.kernel_path();
|
||||||
|
if !kernel.exists() {
|
||||||
|
if let Ok(res) = app.path().resolve("binaries/mihomo.exe", BaseDirectory::Resource) {
|
||||||
|
if res.exists() {
|
||||||
|
fs::copy(&res, &kernel).map_err(|e| format!("复制内核失败: {}", e))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(self.kernel_info())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 配置生成 ----------
|
||||||
|
/// 合并 profile + 控制器设置,生成运行时 config.yaml
|
||||||
|
pub fn generate_config(&self) -> Result<(), String> {
|
||||||
|
let settings = self.load_settings();
|
||||||
|
let mut value: YamlValue = if let Some(id) = &settings.current_profile {
|
||||||
|
let path = self.profiles_dir().join(format!("{}.yaml", id));
|
||||||
|
if path.exists() {
|
||||||
|
let content = fs::read_to_string(&path).map_err(|e| e.to_string())?;
|
||||||
|
serde_yaml::from_str(&content).unwrap_or(YamlValue::Mapping(serde_yaml::Mapping::new()))
|
||||||
|
} else {
|
||||||
|
YamlValue::Mapping(serde_yaml::Mapping::new())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
YamlValue::Mapping(serde_yaml::Mapping::new())
|
||||||
|
};
|
||||||
|
|
||||||
|
if !value.is_mapping() {
|
||||||
|
value = YamlValue::Mapping(serde_yaml::Mapping::new());
|
||||||
|
}
|
||||||
|
let m = value.as_mapping_mut().unwrap();
|
||||||
|
m.insert(YamlValue::String("mixed-port".into()), YamlValue::Number(settings.mixed_port.into()));
|
||||||
|
m.insert(
|
||||||
|
YamlValue::String("external-controller".into()),
|
||||||
|
YamlValue::String(settings.external_controller.clone()),
|
||||||
|
);
|
||||||
|
if !settings.secret.is_empty() {
|
||||||
|
m.insert(YamlValue::String("secret".into()), YamlValue::String(settings.secret.clone()));
|
||||||
|
}
|
||||||
|
m.insert(YamlValue::String("mode".into()), YamlValue::String(settings.mode.clone()));
|
||||||
|
m.insert(
|
||||||
|
YamlValue::String("log-level".into()),
|
||||||
|
YamlValue::String(settings.log_level.clone()),
|
||||||
|
);
|
||||||
|
m.insert(YamlValue::String("allow-lan".into()), YamlValue::Bool(settings.allow_lan));
|
||||||
|
|
||||||
|
let yaml = serde_yaml::to_string(&value).map_err(|e| e.to_string())?;
|
||||||
|
fs::write(self.config_path(), yaml).map_err(|e| e.to_string())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 构建启动 mihomo 所需的进程参数(含 prepare + config 生成)
|
||||||
|
pub fn prepare_for_start(&self, app: &AppHandle) -> Result<StartProcessParams, String> {
|
||||||
|
let info = self.prepare_kernel(app)?;
|
||||||
|
if !info.exists {
|
||||||
|
return Err(format!(
|
||||||
|
"mihomo 内核未安装。请将 mihomo.exe 放置到 src-tauri/binaries/ 后重新运行,或直接放到:\n{}",
|
||||||
|
self.cores_dir().to_string_lossy()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
self.generate_config()?;
|
||||||
|
Ok(StartProcessParams {
|
||||||
|
id: "proxy".into(),
|
||||||
|
executable: self.kernel_path().to_string_lossy().to_string(),
|
||||||
|
args: vec![
|
||||||
|
"-d".into(),
|
||||||
|
self.mihomo_dir().to_string_lossy().to_string(),
|
||||||
|
"-f".into(),
|
||||||
|
self.config_path().to_string_lossy().to_string(),
|
||||||
|
],
|
||||||
|
cwd: Some(self.mihomo_dir().to_string_lossy().to_string()),
|
||||||
|
name: "mihomo".into(),
|
||||||
|
restart_on_crash: true,
|
||||||
|
max_restarts: 3,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 订阅管理 ----------
|
||||||
|
pub async fn import_profile(&self, url: &str, name: &str) -> Result<ProfileMeta, String> {
|
||||||
|
let resp = self
|
||||||
|
.client
|
||||||
|
.get(url)
|
||||||
|
.header("User-Agent", "clash.meta/thing")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("下载订阅失败: {}", e))?;
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
return Err(format!("订阅下载失败: HTTP {}", resp.status()));
|
||||||
|
}
|
||||||
|
let content = resp.text().await.map_err(|e| e.to_string())?;
|
||||||
|
if !content.contains("proxies") && !content.contains("Proxy") {
|
||||||
|
return Err("订阅内容不像有效的 Clash/mihomo 配置".into());
|
||||||
|
}
|
||||||
|
let id = format!("profile-{}", Local::now().format("%Y%m%d%H%M%S"));
|
||||||
|
let path = self.profiles_dir().join(format!("{}.yaml", id));
|
||||||
|
fs::write(&path, &content).map_err(|e| e.to_string())?;
|
||||||
|
let now = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
|
||||||
|
let meta = ProfileMeta {
|
||||||
|
id: id.clone(),
|
||||||
|
name: name.to_string(),
|
||||||
|
url: url.to_string(),
|
||||||
|
added_at: now.clone(),
|
||||||
|
updated_at: now,
|
||||||
|
size: content.len() as u64,
|
||||||
|
};
|
||||||
|
let mut settings = self.load_settings();
|
||||||
|
settings.profiles.push(meta.clone());
|
||||||
|
if settings.current_profile.is_none() {
|
||||||
|
settings.current_profile = Some(id);
|
||||||
|
}
|
||||||
|
self.save_settings(&settings)?;
|
||||||
|
Ok(meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_profile(&self, id: &str) -> Result<ProfileMeta, String> {
|
||||||
|
let mut settings = self.load_settings();
|
||||||
|
let meta = settings
|
||||||
|
.profiles
|
||||||
|
.iter()
|
||||||
|
.find(|p| p.id == id)
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| "订阅不存在".to_string())?;
|
||||||
|
let resp = self
|
||||||
|
.client
|
||||||
|
.get(&meta.url)
|
||||||
|
.header("User-Agent", "clash.meta/thing")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("更新订阅失败: {}", e))?;
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
return Err(format!("更新订阅失败: HTTP {}", resp.status()));
|
||||||
|
}
|
||||||
|
let content = resp.text().await.map_err(|e| e.to_string())?;
|
||||||
|
let path = self.profiles_dir().join(format!("{}.yaml", id));
|
||||||
|
fs::write(&path, &content).map_err(|e| e.to_string())?;
|
||||||
|
let now = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
|
||||||
|
let size = content.len() as u64;
|
||||||
|
if let Some(p) = settings.profiles.iter_mut().find(|p| p.id == id) {
|
||||||
|
p.updated_at = now.clone();
|
||||||
|
p.size = size;
|
||||||
|
}
|
||||||
|
self.save_settings(&settings)?;
|
||||||
|
Ok(ProfileMeta {
|
||||||
|
id: id.to_string(),
|
||||||
|
name: meta.name,
|
||||||
|
url: meta.url,
|
||||||
|
added_at: meta.added_at,
|
||||||
|
updated_at: now,
|
||||||
|
size,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_profile(&self, id: &str) -> Result<(), String> {
|
||||||
|
let path = self.profiles_dir().join(format!("{}.yaml", id));
|
||||||
|
fs::remove_file(&path).ok();
|
||||||
|
let mut settings = self.load_settings();
|
||||||
|
settings.profiles.retain(|p| p.id != id);
|
||||||
|
if settings.current_profile.as_deref() == Some(id) {
|
||||||
|
settings.current_profile = settings.profiles.first().map(|p| p.id.clone());
|
||||||
|
}
|
||||||
|
self.save_settings(&settings)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn activate_profile(&self, id: &str) -> Result<(), String> {
|
||||||
|
let mut settings = self.load_settings();
|
||||||
|
if !settings.profiles.iter().any(|p| p.id == id) {
|
||||||
|
return Err("订阅不存在".into());
|
||||||
|
}
|
||||||
|
settings.current_profile = Some(id.to_string());
|
||||||
|
self.save_settings(&settings)?;
|
||||||
|
self.generate_config()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- mihomo API ----------
|
||||||
|
fn api_url(&self, path: &str) -> String {
|
||||||
|
let s = self.load_settings();
|
||||||
|
format!("http://{}{}", s.external_controller, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn api_bearer(&self) -> Option<String> {
|
||||||
|
let s = self.load_settings();
|
||||||
|
if s.secret.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(format!("Bearer {}", s.secret))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn api_get(&self, path: &str) -> Result<serde_json::Value, String> {
|
||||||
|
let mut req = self.client.get(self.api_url(path));
|
||||||
|
if let Some(b) = self.api_bearer() {
|
||||||
|
req = req.header("Authorization", b);
|
||||||
|
}
|
||||||
|
let resp = req.send().await.map_err(|e| format!("请求 mihomo 失败: {}", e))?;
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
return Err(format!("mihomo API 错误: {}", resp.status()));
|
||||||
|
}
|
||||||
|
resp.json().await.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn api_request(
|
||||||
|
&self,
|
||||||
|
method: reqwest::Method,
|
||||||
|
path: &str,
|
||||||
|
body: Option<serde_json::Value>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let mut req = self.client.request(method, self.api_url(path));
|
||||||
|
if let Some(b) = self.api_bearer() {
|
||||||
|
req = req.header("Authorization", b);
|
||||||
|
}
|
||||||
|
if let Some(b) = body {
|
||||||
|
req = req.json(&b);
|
||||||
|
}
|
||||||
|
let resp = req.send().await.map_err(|e| format!("请求 mihomo 失败: {}", e))?;
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
return Err(format!("mihomo API 错误: {}", resp.status()));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_version(&self) -> Result<serde_json::Value, String> {
|
||||||
|
self.api_get("/version").await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_proxies(&self) -> Result<serde_json::Value, String> {
|
||||||
|
self.api_get("/proxies").await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn select_proxy(&self, group: &str, name: &str) -> Result<(), String> {
|
||||||
|
self.api_request(
|
||||||
|
reqwest::Method::PUT,
|
||||||
|
&format!("/proxies/{}", url_encode(group)),
|
||||||
|
Some(serde_json::json!({ "name": name })),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn test_delay(&self, name: &str, url: &str, timeout: u32) -> Result<u32, String> {
|
||||||
|
let path = format!(
|
||||||
|
"/proxies/{}/delay?timeout={}&url={}",
|
||||||
|
url_encode(name),
|
||||||
|
timeout,
|
||||||
|
url_encode(url)
|
||||||
|
);
|
||||||
|
let v = self.api_get(&path).await?;
|
||||||
|
v.get("delay")
|
||||||
|
.and_then(|d| d.as_u64())
|
||||||
|
.map(|d| d as u32)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
v.get("message")
|
||||||
|
.and_then(|m| m.as_str())
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.unwrap_or_else(|| "测速失败".into())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub async fn get_rules(&self) -> Result<serde_json::Value, String> {
|
||||||
|
self.api_get("/rules").await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_connections(&self) -> Result<serde_json::Value, String> {
|
||||||
|
self.api_get("/connections").await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn close_connection(&self, id: &str) -> Result<(), String> {
|
||||||
|
self.api_request(
|
||||||
|
reqwest::Method::DELETE,
|
||||||
|
&format!("/connections/{}", url_encode(id)),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn patch_configs(&self, body: serde_json::Value) -> Result<(), String> {
|
||||||
|
self.api_request(reqwest::Method::PATCH, "/configs", Some(body)).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn url_encode(s: &str) -> String {
|
||||||
|
// 仅对路径段做最小编码,避免引入额外依赖
|
||||||
|
let mut out = String::with_capacity(s.len());
|
||||||
|
for b in s.bytes() {
|
||||||
|
match b {
|
||||||
|
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
|
||||||
|
out.push(b as char);
|
||||||
|
}
|
||||||
|
_ => out.push_str(&format!("%{:02X}", b)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== 系统代理(Windows) =====================
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn set_system_proxy_windows(addr: &str) -> Result<(), String> {
|
||||||
|
use winreg::enums::*;
|
||||||
|
use winreg::RegKey;
|
||||||
|
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||||
|
let (settings, _) = hkcu
|
||||||
|
.create_subkey("Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings")
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
settings
|
||||||
|
.set_value("ProxyEnable", &1u32)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
settings
|
||||||
|
.set_value("ProxyServer", &addr)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
notify_wininet();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn clear_system_proxy_windows() -> Result<(), String> {
|
||||||
|
use winreg::enums::*;
|
||||||
|
use winreg::RegKey;
|
||||||
|
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||||
|
let (settings, _) = hkcu
|
||||||
|
.create_subkey("Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings")
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
settings
|
||||||
|
.set_value("ProxyEnable", &0u32)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
notify_wininet();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn get_system_proxy_windows() -> bool {
|
||||||
|
use winreg::enums::*;
|
||||||
|
use winreg::RegKey;
|
||||||
|
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||||
|
hkcu.open_subkey("Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings")
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| s.get_value::<u32, _>("ProxyEnable").ok())
|
||||||
|
.map(|v| v != 0)
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn notify_wininet() {
|
||||||
|
unsafe {
|
||||||
|
use windows_sys::Win32::Networking::WinInet::*;
|
||||||
|
InternetSetOptionW(std::ptr::null(), INTERNET_OPTION_SETTINGS_CHANGED, std::ptr::null(), 0);
|
||||||
|
InternetSetOptionW(std::ptr::null(), INTERNET_OPTION_REFRESH, std::ptr::null(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
fn set_system_proxy_windows(_addr: &str) -> Result<(), String> {
|
||||||
|
Err("系统代理仅支持 Windows".into())
|
||||||
|
}
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
fn clear_system_proxy_windows() -> Result<(), String> {
|
||||||
|
Err("系统代理仅支持 Windows".into())
|
||||||
|
}
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
fn get_system_proxy_windows() -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== Tauri 命令 =====================
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn proxy_get_settings(state: tauri::State<'_, MihomoManager>) -> ProxySettings {
|
||||||
|
state.load_settings()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn proxy_save_settings(
|
||||||
|
state: tauri::State<'_, MihomoManager>,
|
||||||
|
settings: ProxySettings,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
state.save_settings(&settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn proxy_kernel_info(
|
||||||
|
state: tauri::State<'_, MihomoManager>,
|
||||||
|
app: AppHandle,
|
||||||
|
) -> Result<KernelInfo, String> {
|
||||||
|
state.prepare_kernel(&app)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn proxy_status(pm: tauri::State<'_, ProcessManager>) -> ProxyStatus {
|
||||||
|
match pm.get_status("proxy") {
|
||||||
|
Some(p) => ProxyStatus {
|
||||||
|
running: matches!(p.status, crate::process_manager::ProcessStatus::Running),
|
||||||
|
pid: p.pid,
|
||||||
|
restart_count: p.restart_count,
|
||||||
|
},
|
||||||
|
None => ProxyStatus {
|
||||||
|
running: false,
|
||||||
|
pid: None,
|
||||||
|
restart_count: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn proxy_start(
|
||||||
|
state: tauri::State<'_, MihomoManager>,
|
||||||
|
pm: tauri::State<'_, ProcessManager>,
|
||||||
|
app: AppHandle,
|
||||||
|
) -> Result<ProcessInfo, String> {
|
||||||
|
let params = state.prepare_for_start(&app)?;
|
||||||
|
pm.start(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn proxy_stop(pm: tauri::State<'_, ProcessManager>) -> Result<(), String> {
|
||||||
|
pm.stop("proxy")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn proxy_restart(
|
||||||
|
state: tauri::State<'_, MihomoManager>,
|
||||||
|
pm: tauri::State<'_, ProcessManager>,
|
||||||
|
app: AppHandle,
|
||||||
|
) -> Result<ProcessInfo, String> {
|
||||||
|
let _ = pm.stop("proxy");
|
||||||
|
let params = state.prepare_for_start(&app)?;
|
||||||
|
pm.start(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn proxy_version(state: tauri::State<'_, MihomoManager>) -> Result<serde_json::Value, String> {
|
||||||
|
state.get_version().await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn proxy_get_proxies(state: tauri::State<'_, MihomoManager>) -> Result<serde_json::Value, String> {
|
||||||
|
state.get_proxies().await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn proxy_select_proxy(
|
||||||
|
state: tauri::State<'_, MihomoManager>,
|
||||||
|
group: String,
|
||||||
|
name: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
state.select_proxy(&group, &name).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn proxy_test_delay(
|
||||||
|
state: tauri::State<'_, MihomoManager>,
|
||||||
|
name: String,
|
||||||
|
url: Option<String>,
|
||||||
|
timeout: Option<u32>,
|
||||||
|
) -> Result<u32, String> {
|
||||||
|
state
|
||||||
|
.test_delay(
|
||||||
|
&name,
|
||||||
|
url.as_deref().unwrap_or("https://www.gstatic.com/generate_204"),
|
||||||
|
timeout.unwrap_or(5000),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn proxy_get_connections(
|
||||||
|
state: tauri::State<'_, MihomoManager>,
|
||||||
|
) -> Result<serde_json::Value, String> {
|
||||||
|
state.get_connections().await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn proxy_close_connection(
|
||||||
|
state: tauri::State<'_, MihomoManager>,
|
||||||
|
id: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
state.close_connection(&id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn proxy_patch_configs(
|
||||||
|
state: tauri::State<'_, MihomoManager>,
|
||||||
|
body: serde_json::Value,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
state.patch_configs(body).await
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 订阅 ----------
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn proxy_import_profile(
|
||||||
|
state: tauri::State<'_, MihomoManager>,
|
||||||
|
url: String,
|
||||||
|
name: String,
|
||||||
|
) -> Result<ProfileMeta, String> {
|
||||||
|
state.import_profile(&url, &name).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn proxy_update_profile(
|
||||||
|
state: tauri::State<'_, MihomoManager>,
|
||||||
|
id: String,
|
||||||
|
) -> Result<ProfileMeta, String> {
|
||||||
|
state.update_profile(&id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn proxy_delete_profile(
|
||||||
|
state: tauri::State<'_, MihomoManager>,
|
||||||
|
id: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
state.delete_profile(&id)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn proxy_activate_profile(
|
||||||
|
state: tauri::State<'_, MihomoManager>,
|
||||||
|
id: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
state.activate_profile(&id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 系统代理 ----------
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn proxy_set_system_proxy(
|
||||||
|
state: tauri::State<'_, MihomoManager>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let settings = state.load_settings();
|
||||||
|
let addr = format!("127.0.0.1:{}", settings.mixed_port);
|
||||||
|
set_system_proxy_windows(&addr)?;
|
||||||
|
let mut settings = settings;
|
||||||
|
settings.system_proxy = true;
|
||||||
|
state.save_settings(&settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn proxy_clear_system_proxy(
|
||||||
|
state: tauri::State<'_, MihomoManager>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
clear_system_proxy_windows()?;
|
||||||
|
let mut settings = state.load_settings();
|
||||||
|
settings.system_proxy = false;
|
||||||
|
state.save_settings(&settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn proxy_get_system_proxy() -> bool {
|
||||||
|
get_system_proxy_windows()
|
||||||
|
}
|
||||||
@@ -0,0 +1,358 @@
|
|||||||
|
use serde::Serialize;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::process::{Child, Command, Stdio};
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tauri::{AppHandle, Emitter, Manager};
|
||||||
|
|
||||||
|
/// 进程状态枚举
|
||||||
|
#[derive(Serialize, Clone, Debug)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum ProcessStatus {
|
||||||
|
Running,
|
||||||
|
Stopped,
|
||||||
|
Crashed,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
Starting,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 进程信息(返回给前端)
|
||||||
|
#[derive(Serialize, Clone)]
|
||||||
|
pub struct ProcessInfo {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub status: ProcessStatus,
|
||||||
|
pub pid: Option<u32>,
|
||||||
|
pub restart_count: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 进程启动参数(从前端传入)
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct StartProcessParams {
|
||||||
|
pub id: String,
|
||||||
|
pub executable: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub args: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub cwd: Option<String>,
|
||||||
|
pub name: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub restart_on_crash: bool,
|
||||||
|
#[serde(default = "default_max_restarts")]
|
||||||
|
pub max_restarts: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_max_restarts() -> u32 {
|
||||||
|
3
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 内部进程条目
|
||||||
|
struct ProcessEntry {
|
||||||
|
child: Child,
|
||||||
|
name: String,
|
||||||
|
restart_count: u32,
|
||||||
|
max_restarts: u32,
|
||||||
|
restart_on_crash: bool,
|
||||||
|
executable: String,
|
||||||
|
args: Vec<String>,
|
||||||
|
cwd: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 进程管理器 —— 管理子进程的完整生命周期
|
||||||
|
pub struct ProcessManager {
|
||||||
|
processes: Mutex<HashMap<String, ProcessEntry>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProcessManager {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
processes: Mutex::new(HashMap::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 启动一个子进程
|
||||||
|
pub fn start(&self, params: StartProcessParams) -> Result<ProcessInfo, String> {
|
||||||
|
let mut processes = self.processes.lock().map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
// 如果已有同名进程且仍在运行,返回错误
|
||||||
|
if let Some(entry) = processes.get_mut(¶ms.id) {
|
||||||
|
if entry.child.try_wait().map_err(|e| e.to_string())?.is_none() {
|
||||||
|
return Err(format!("进程 '{}' 已在运行中", params.id));
|
||||||
|
}
|
||||||
|
// 进程已退出,移除旧记录
|
||||||
|
processes.remove(¶ms.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut cmd = Command::new(¶ms.executable);
|
||||||
|
cmd.args(¶ms.args);
|
||||||
|
if let Some(ref dir) = params.cwd {
|
||||||
|
cmd.current_dir(dir);
|
||||||
|
}
|
||||||
|
// 子进程的 stdout/stderr/stdin 不继承主进程
|
||||||
|
cmd.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.stdin(Stdio::null());
|
||||||
|
|
||||||
|
let child = cmd
|
||||||
|
.spawn()
|
||||||
|
.map_err(|e| format!("启动进程 '{}' 失败: {}", params.id, e))?;
|
||||||
|
let pid = child.id();
|
||||||
|
|
||||||
|
let entry = ProcessEntry {
|
||||||
|
child,
|
||||||
|
name: params.name.clone(),
|
||||||
|
restart_count: 0,
|
||||||
|
max_restarts: params.max_restarts,
|
||||||
|
restart_on_crash: params.restart_on_crash,
|
||||||
|
executable: params.executable,
|
||||||
|
args: params.args,
|
||||||
|
cwd: params.cwd,
|
||||||
|
};
|
||||||
|
|
||||||
|
processes.insert(params.id.clone(), entry);
|
||||||
|
|
||||||
|
Ok(ProcessInfo {
|
||||||
|
id: params.id,
|
||||||
|
name: params.name,
|
||||||
|
status: ProcessStatus::Running,
|
||||||
|
pid: Some(pid),
|
||||||
|
restart_count: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 停止指定进程
|
||||||
|
pub fn stop(&self, id: &str) -> Result<(), String> {
|
||||||
|
let mut processes = self.processes.lock().map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
if let Some(mut entry) = processes.remove(id) {
|
||||||
|
entry
|
||||||
|
.child
|
||||||
|
.kill()
|
||||||
|
.map_err(|e| format!("终止进程 '{}' 失败: {}", id, e))?;
|
||||||
|
let _ = entry.child.wait();
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(format!("进程 '{}' 不存在", id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 停止所有进程(应用退出时调用)
|
||||||
|
pub fn stop_all(&self) {
|
||||||
|
if let Ok(mut processes) = self.processes.lock() {
|
||||||
|
for (id, mut entry) in processes.drain() {
|
||||||
|
let _ = entry.child.kill();
|
||||||
|
let _ = entry.child.wait();
|
||||||
|
println!("[ProcessManager] 已停止进程: {}", id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取单个进程状态
|
||||||
|
pub fn get_status(&self, id: &str) -> Option<ProcessInfo> {
|
||||||
|
let mut processes = self.processes.lock().ok()?;
|
||||||
|
let entry = processes.get_mut(id)?;
|
||||||
|
|
||||||
|
let (status, pid) = match entry.child.try_wait() {
|
||||||
|
Ok(None) => (ProcessStatus::Running, Some(entry.child.id())),
|
||||||
|
Ok(Some(_)) => (ProcessStatus::Crashed, None),
|
||||||
|
Err(_) => (ProcessStatus::Stopped, None),
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(ProcessInfo {
|
||||||
|
id: id.to_string(),
|
||||||
|
name: entry.name.clone(),
|
||||||
|
status,
|
||||||
|
pid,
|
||||||
|
restart_count: entry.restart_count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取所有进程状态
|
||||||
|
pub fn get_all_status(&self) -> Vec<ProcessInfo> {
|
||||||
|
let mut result = Vec::new();
|
||||||
|
|
||||||
|
if let Ok(mut processes) = self.processes.lock() {
|
||||||
|
for (id, entry) in processes.iter_mut() {
|
||||||
|
let (status, pid) = match entry.child.try_wait() {
|
||||||
|
Ok(None) => (ProcessStatus::Running, Some(entry.child.id())),
|
||||||
|
Ok(Some(_)) => (ProcessStatus::Crashed, None),
|
||||||
|
Err(_) => (ProcessStatus::Stopped, None),
|
||||||
|
};
|
||||||
|
|
||||||
|
result.push(ProcessInfo {
|
||||||
|
id: id.clone(),
|
||||||
|
name: entry.name.clone(),
|
||||||
|
status,
|
||||||
|
pid,
|
||||||
|
restart_count: entry.restart_count,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 检查所有进程,处理崩溃的进程(自动重启或移除)
|
||||||
|
/// 返回状态发生变化的进程列表
|
||||||
|
pub fn check_and_cleanup(&self) -> Vec<ProcessInfo> {
|
||||||
|
let mut changes = Vec::new();
|
||||||
|
|
||||||
|
let mut processes = match self.processes.lock() {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(_) => return changes,
|
||||||
|
};
|
||||||
|
|
||||||
|
let ids: Vec<String> = processes.keys().cloned().collect();
|
||||||
|
|
||||||
|
for id in ids {
|
||||||
|
if let Some(entry) = processes.get_mut(&id) {
|
||||||
|
match entry.child.try_wait() {
|
||||||
|
Ok(None) => {
|
||||||
|
// 仍在运行,无需处理
|
||||||
|
}
|
||||||
|
Ok(Some(_)) => {
|
||||||
|
// 进程已退出
|
||||||
|
if entry.restart_on_crash
|
||||||
|
&& (entry.max_restarts == 0
|
||||||
|
|| entry.restart_count < entry.max_restarts)
|
||||||
|
{
|
||||||
|
// 自动重启
|
||||||
|
let restart_count = entry.restart_count + 1;
|
||||||
|
let executable = entry.executable.clone();
|
||||||
|
let args = entry.args.clone();
|
||||||
|
let cwd = entry.cwd.clone();
|
||||||
|
let name = entry.name.clone();
|
||||||
|
|
||||||
|
// 先终止旧进程
|
||||||
|
let _ = entry.child.kill();
|
||||||
|
let _ = entry.child.wait();
|
||||||
|
|
||||||
|
// 重新启动
|
||||||
|
let mut cmd = Command::new(&executable);
|
||||||
|
cmd.args(&args);
|
||||||
|
if let Some(ref dir) = cwd {
|
||||||
|
cmd.current_dir(dir);
|
||||||
|
}
|
||||||
|
cmd.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.stdin(Stdio::null());
|
||||||
|
|
||||||
|
match cmd.spawn() {
|
||||||
|
Ok(new_child) => {
|
||||||
|
let pid = new_child.id();
|
||||||
|
entry.child = new_child;
|
||||||
|
entry.restart_count = restart_count;
|
||||||
|
|
||||||
|
changes.push(ProcessInfo {
|
||||||
|
id: id.clone(),
|
||||||
|
name: name.clone(),
|
||||||
|
status: ProcessStatus::Running,
|
||||||
|
pid: Some(pid),
|
||||||
|
restart_count,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!(
|
||||||
|
"[ProcessManager] 重启进程 '{}' 失败: {}",
|
||||||
|
id, e
|
||||||
|
);
|
||||||
|
processes.remove(&id);
|
||||||
|
changes.push(ProcessInfo {
|
||||||
|
id: id.clone(),
|
||||||
|
name,
|
||||||
|
status: ProcessStatus::Crashed,
|
||||||
|
pid: None,
|
||||||
|
restart_count,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 不自动重启,移除记录
|
||||||
|
let name = entry.name.clone();
|
||||||
|
let restart_count = entry.restart_count;
|
||||||
|
processes.remove(&id);
|
||||||
|
changes.push(ProcessInfo {
|
||||||
|
id: id.clone(),
|
||||||
|
name,
|
||||||
|
status: ProcessStatus::Stopped,
|
||||||
|
pid: None,
|
||||||
|
restart_count,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// try_wait 出错
|
||||||
|
let name = entry.name.clone();
|
||||||
|
let restart_count = entry.restart_count;
|
||||||
|
processes.remove(&id);
|
||||||
|
changes.push(ProcessInfo {
|
||||||
|
id: id.clone(),
|
||||||
|
name,
|
||||||
|
status: ProcessStatus::Stopped,
|
||||||
|
pid: None,
|
||||||
|
restart_count,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
changes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Tauri 命令 =====
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn start_process(
|
||||||
|
state: tauri::State<'_, ProcessManager>,
|
||||||
|
params: StartProcessParams,
|
||||||
|
) -> Result<ProcessInfo, String> {
|
||||||
|
state.start(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn stop_process(
|
||||||
|
state: tauri::State<'_, ProcessManager>,
|
||||||
|
id: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
state.stop(&id)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_process_status(
|
||||||
|
state: tauri::State<'_, ProcessManager>,
|
||||||
|
id: String,
|
||||||
|
) -> Option<ProcessInfo> {
|
||||||
|
state.get_status(&id)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_all_process_status(
|
||||||
|
state: tauri::State<'_, ProcessManager>,
|
||||||
|
) -> Vec<ProcessInfo> {
|
||||||
|
state.get_all_status()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn stop_all_processes(state: tauri::State<'_, ProcessManager>) {
|
||||||
|
state.stop_all()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 启动后台监控线程,定期检查进程状态并向前端发送事件
|
||||||
|
pub fn start_monitoring_thread(app: AppHandle) {
|
||||||
|
thread::spawn(move || {
|
||||||
|
loop {
|
||||||
|
thread::sleep(Duration::from_secs(3));
|
||||||
|
|
||||||
|
let state = app.state::<ProcessManager>();
|
||||||
|
let changes = state.check_and_cleanup();
|
||||||
|
|
||||||
|
for change in changes {
|
||||||
|
let _ = app.emit("process-status-changed", &change);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
+62
-41
@@ -1,64 +1,68 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, shallowRef, markRaw, type Component } from 'vue'
|
import { ref, onMounted, shallowRef, computed, watch, type Component } from 'vue'
|
||||||
import TitleBar from '@/components/layout/TitleBar.vue'
|
import TitleBar from '@/components/layout/TitleBar.vue'
|
||||||
import Sidebar from '@/components/layout/Sidebar.vue'
|
import Sidebar from '@/components/layout/Sidebar.vue'
|
||||||
import ModuleContainer from '@/components/layout/ModuleContainer.vue'
|
import ModuleContainer from '@/components/layout/ModuleContainer.vue'
|
||||||
import GeneralSettings from '@/modules/general/GeneralSettings.vue'
|
import { Toaster } from '@/components/ui/sonner'
|
||||||
import { useAppStore } from '@/stores/appStore'
|
import { useAppStore } from '@/stores/appStore'
|
||||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||||
|
import { moduleRegistry } from '@/modules/registry'
|
||||||
|
import type { ModuleMeta } from '@/types/module'
|
||||||
|
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
|
|
||||||
interface ModuleMeta {
|
/** 侧边栏 / 标题栏需要的模块信息(id + name + icon) */
|
||||||
|
interface NavModule {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
icon: string
|
icon: string
|
||||||
// 同步加载的模块直接传 Component;懒加载的传 import 工厂
|
|
||||||
loader?: () => Promise<{ default: Component }>
|
|
||||||
component?: Component
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 常规设置是默认可见且轻量的,直接同步引入
|
/** 从注册表元信息转换为导航用的精简结构 */
|
||||||
// 其他业务模块较大且首屏不一定需要,懒加载
|
const toNavModule = (meta: ModuleMeta): NavModule => ({
|
||||||
const modules: ModuleMeta[] = [
|
id: meta.id,
|
||||||
{ id: 'proxy', name: '代理管理', icon: 'proxy', loader: () => import('@/modules/proxy/ProxyModule.vue') },
|
name: meta.name,
|
||||||
{ id: 'clipboard', name: '剪贴板', icon: 'clipboard', loader: () => import('@/modules/clipboard/ClipboardModule.vue') },
|
icon: meta.icon
|
||||||
{ id: 'screenshot', name: '截图', icon: 'screenshot', loader: () => import('@/modules/screenshot/ScreenshotModule.vue') },
|
})
|
||||||
{ id: 'monitor', name: '硬件监控', icon: 'monitor', loader: () => import('@/modules/monitor/MonitorModule.vue') },
|
|
||||||
{ id: 'downloader', name: '下载器', icon: 'downloader', loader: () => import('@/modules/downloader/DownloaderModule.vue') },
|
|
||||||
{ id: 'finder', name: '文件搜索', icon: 'finder', loader: () => import('@/modules/finder/FinderModule.vue') },
|
|
||||||
{ id: 'settings', name: '常规设置', icon: 'settings', component: markRaw(GeneralSettings) }
|
|
||||||
]
|
|
||||||
|
|
||||||
const activeModule = ref('proxy')
|
const activeModule = ref('proxy')
|
||||||
|
|
||||||
// 当前激活的组件实例(shallowRef 适合大组件)
|
|
||||||
const activeComponent = shallowRef<Component | null>(null)
|
const activeComponent = shallowRef<Component | null>(null)
|
||||||
|
|
||||||
// 加载模块组件
|
/** 当前可显示的模块(内置模块 + 已启用的用户模块),按用户排序显示 */
|
||||||
|
const availableModules = computed<NavModule[]>(() => {
|
||||||
|
const enabledIds = appStore.enabledModules.map(m => m.id)
|
||||||
|
const allModules = moduleRegistry
|
||||||
|
.getAllMetas()
|
||||||
|
.filter(m => m.builtin || enabledIds.includes(m.id))
|
||||||
|
.map(toNavModule)
|
||||||
|
|
||||||
|
// 按 moduleOrder 排序,settings 始终在末尾
|
||||||
|
return allModules.sort((a, b) => {
|
||||||
|
if (a.id === 'settings') return 1
|
||||||
|
if (b.id === 'settings') return -1
|
||||||
|
const aIdx = appStore.moduleOrder.indexOf(a.id)
|
||||||
|
const bIdx = appStore.moduleOrder.indexOf(b.id)
|
||||||
|
if (aIdx === -1) return 1
|
||||||
|
if (bIdx === -1) return -1
|
||||||
|
return aIdx - bIdx
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const loadModule = async (moduleId: string) => {
|
const loadModule = async (moduleId: string) => {
|
||||||
const m = modules.find(mod => mod.id === moduleId)
|
const component = await moduleRegistry.loadComponent(moduleId)
|
||||||
if (!m) {
|
activeComponent.value = component
|
||||||
activeComponent.value = null
|
|
||||||
return
|
// 调用模块的 onActivate 生命周期钩子
|
||||||
}
|
const config = moduleRegistry.getConfig(moduleId)
|
||||||
if (m.component) {
|
config?.lifecycle?.onActivate?.()
|
||||||
activeComponent.value = m.component
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (m.loader) {
|
|
||||||
try {
|
|
||||||
const mod = await m.loader()
|
|
||||||
// 缓存到 component,避免重复加载
|
|
||||||
m.component = markRaw(mod.default)
|
|
||||||
activeComponent.value = m.component
|
|
||||||
} catch (e) {
|
|
||||||
console.error(`Failed to load module ${moduleId}:`, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleModuleChange = (moduleId: string) => {
|
const handleModuleChange = (moduleId: string) => {
|
||||||
|
// 调用上一个模块的 onDeactivate 钩子
|
||||||
|
const prevConfig = moduleRegistry.getConfig(activeModule.value)
|
||||||
|
prevConfig?.lifecycle?.onDeactivate?.()
|
||||||
|
|
||||||
activeModule.value = moduleId
|
activeModule.value = moduleId
|
||||||
loadModule(moduleId)
|
loadModule(moduleId)
|
||||||
}
|
}
|
||||||
@@ -68,8 +72,24 @@ const handleSearch = (moduleId: string) => {
|
|||||||
loadModule(moduleId)
|
loadModule(moduleId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getFallbackModule = () => {
|
||||||
|
const enabledIds = appStore.enabledModules.map(m => m.id)
|
||||||
|
const fallback = moduleRegistry.getAllMetas().find(
|
||||||
|
m => !m.builtin && enabledIds.includes(m.id)
|
||||||
|
)
|
||||||
|
return fallback?.id || 'settings'
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => appStore.enabledModules.length, () => {
|
||||||
|
const enabledIds = appStore.enabledModules.map(m => m.id)
|
||||||
|
if (activeModule.value !== 'settings' && !enabledIds.includes(activeModule.value)) {
|
||||||
|
const fallback = getFallbackModule()
|
||||||
|
activeModule.value = fallback
|
||||||
|
loadModule(fallback)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 首次加载默认模块
|
|
||||||
loadModule(activeModule.value)
|
loadModule(activeModule.value)
|
||||||
appStore.init().catch(e => console.error('App init error:', e))
|
appStore.init().catch(e => console.error('App init error:', e))
|
||||||
})
|
})
|
||||||
@@ -78,15 +98,16 @@ onMounted(() => {
|
|||||||
<template>
|
<template>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<div class="flex flex-col h-screen w-screen overflow-hidden">
|
<div class="flex flex-col h-screen w-screen overflow-hidden">
|
||||||
<TitleBar :modules="modules" @search="handleSearch" />
|
<TitleBar :modules="availableModules" @search="handleSearch" />
|
||||||
<div class="flex-1 flex overflow-hidden">
|
<div class="flex-1 flex overflow-hidden">
|
||||||
<Sidebar
|
<Sidebar
|
||||||
:modules="modules"
|
:modules="availableModules"
|
||||||
:active-module="activeModule"
|
:active-module="activeModule"
|
||||||
@change="handleModuleChange"
|
@change="handleModuleChange"
|
||||||
/>
|
/>
|
||||||
<ModuleContainer :active-component="activeComponent" :active-module="activeModule" />
|
<ModuleContainer :active-component="activeComponent" :active-module="activeModule" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Toaster position="bottom-right" rich-colors close-button />
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</template>
|
</template>
|
||||||
@@ -1,17 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import {
|
import { Settings } from '@lucide/vue'
|
||||||
Settings,
|
|
||||||
Globe,
|
|
||||||
ClipboardList,
|
|
||||||
Camera,
|
|
||||||
Activity,
|
|
||||||
Download,
|
|
||||||
Search
|
|
||||||
} from '@lucide/vue'
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { ButtonGroup } from '@/components/ui/button-group'
|
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||||
|
import { getModuleIcon } from '@/modules/icons'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modules: Array<{ id: string; name: string; icon: string }>
|
modules: Array<{ id: string; name: string; icon: string }>
|
||||||
@@ -22,20 +14,6 @@ const emit = defineEmits<{
|
|||||||
(e: 'change', moduleId: string): void
|
(e: 'change', moduleId: string): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const iconMap: Record<string, typeof Settings> = {
|
|
||||||
settings: Settings,
|
|
||||||
proxy: Globe,
|
|
||||||
clipboard: ClipboardList,
|
|
||||||
screenshot: Camera,
|
|
||||||
monitor: Activity,
|
|
||||||
downloader: Download,
|
|
||||||
finder: Search
|
|
||||||
}
|
|
||||||
|
|
||||||
const getIcon = (iconName: string) => {
|
|
||||||
return iconMap[iconName] || Settings
|
|
||||||
}
|
|
||||||
|
|
||||||
const displayModules = computed(() => {
|
const displayModules = computed(() => {
|
||||||
return props.modules.filter(m => m.id !== 'settings')
|
return props.modules.filter(m => m.id !== 'settings')
|
||||||
})
|
})
|
||||||
@@ -43,11 +21,13 @@ const displayModules = computed(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<aside
|
<aside
|
||||||
class="w-16 flex flex-col items-center py-4 border-r border-border transition-all duration-300"
|
class="w-17 flex flex-col items-center py-4 border-r border-border transition-all duration-300"
|
||||||
:style="{ backgroundColor: 'var(--effect-bg)', backdropFilter: 'var(--effect-blur)' }"
|
:style="{ backgroundColor: 'var(--effect-bg)', backdropFilter: 'var(--effect-blur)' }"
|
||||||
>
|
>
|
||||||
<ButtonGroup orientation="vertical" class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<Tooltip v-for="module in displayModules" :key="module.id">
|
<TransitionGroup name="module-flip">
|
||||||
|
<div v-for="module in displayModules" :key="module.id">
|
||||||
|
<Tooltip>
|
||||||
<TooltipTrigger as-child>
|
<TooltipTrigger as-child>
|
||||||
<Button
|
<Button
|
||||||
:variant="activeModule === module.id ? 'default' : 'ghost'"
|
:variant="activeModule === module.id ? 'default' : 'ghost'"
|
||||||
@@ -57,16 +37,19 @@ const displayModules = computed(() => {
|
|||||||
'bg-primary text-primary-foreground shadow-md': activeModule === module.id,
|
'bg-primary text-primary-foreground shadow-md': activeModule === module.id,
|
||||||
'hover:bg-secondary/50': activeModule !== module.id
|
'hover:bg-secondary/50': activeModule !== module.id
|
||||||
}"
|
}"
|
||||||
|
:title="module.name"
|
||||||
@click="emit('change', module.id)"
|
@click="emit('change', module.id)"
|
||||||
>
|
>
|
||||||
<component :is="getIcon(module.icon)" class="h-5 w-5" />
|
<component :is="getModuleIcon(module.icon)" class="h-5 w-5" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="right" class="w-fit">
|
<TooltipContent side="right" class="w-fit">
|
||||||
<p>{{ module.name }}</p>
|
<p>{{ module.name }}</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ButtonGroup>
|
</div>
|
||||||
|
</TransitionGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex-1"></div>
|
<div class="flex-1"></div>
|
||||||
|
|
||||||
@@ -80,6 +63,7 @@ const displayModules = computed(() => {
|
|||||||
'bg-primary text-primary-foreground shadow-md': activeModule === 'settings',
|
'bg-primary text-primary-foreground shadow-md': activeModule === 'settings',
|
||||||
'hover:bg-secondary/50': activeModule !== 'settings'
|
'hover:bg-secondary/50': activeModule !== 'settings'
|
||||||
}"
|
}"
|
||||||
|
title="常规设置"
|
||||||
@click="emit('change', 'settings')"
|
@click="emit('change', 'settings')"
|
||||||
>
|
>
|
||||||
<Settings class="h-5 w-5" />
|
<Settings class="h-5 w-5" />
|
||||||
@@ -91,3 +75,9 @@ const displayModules = computed(() => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</aside>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.module-flip-move {
|
||||||
|
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -49,18 +49,24 @@ const handleSettingSelect = (item: SearchItem) => {
|
|||||||
isSearchFocused.value = false
|
isSearchFocused.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const tauriWindow = getCurrentWindow()
|
let tauriWindow: ReturnType<typeof getCurrentWindow> | null = null
|
||||||
|
try {
|
||||||
|
tauriWindow = getCurrentWindow()
|
||||||
|
} catch {
|
||||||
|
// 非 Tauri 环境(如浏览器调试),窗口控制不可用
|
||||||
|
tauriWindow = null
|
||||||
|
}
|
||||||
|
|
||||||
const minimize = async () => {
|
const minimize = async () => {
|
||||||
await tauriWindow.minimize()
|
await tauriWindow?.minimize()
|
||||||
}
|
}
|
||||||
|
|
||||||
const maximize = async () => {
|
const maximize = async () => {
|
||||||
await tauriWindow.toggleMaximize()
|
await tauriWindow?.toggleMaximize()
|
||||||
}
|
}
|
||||||
|
|
||||||
const close = async () => {
|
const close = async () => {
|
||||||
await tauriWindow.hide()
|
await tauriWindow?.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleBlur = () => {
|
const handleBlur = () => {
|
||||||
|
|||||||
@@ -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<LogEntry[]> {
|
||||||
|
return invoke('get_logs', { module, level, limit })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空所有日志文件。
|
||||||
|
*/
|
||||||
|
export async function clearLogs(): Promise<void> {
|
||||||
|
return invoke('clear_logs')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取日志系统信息(目录、文件列表、空间占用)。
|
||||||
|
*/
|
||||||
|
export async function getLogInfo(): Promise<LogInfo> {
|
||||||
|
return invoke('get_log_info')
|
||||||
|
}
|
||||||
+34
-2
@@ -3,6 +3,23 @@ import { createPinia } from 'pinia'
|
|||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import './style.css'
|
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 app = createApp(App)
|
||||||
const pinia = createPinia()
|
const pinia = createPinia()
|
||||||
|
|
||||||
@@ -10,7 +27,22 @@ app.use(pinia)
|
|||||||
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
|
||||||
// 应用挂载后再初始化搜索索引,不阻塞首屏渲染
|
// 应用挂载后初始化搜索索引和进程监听(不阻塞首屏渲染)
|
||||||
void import('./stores/searchStore').then(({ useSearchStore }) => {
|
void import('./stores/searchStore').then(({ useSearchStore }) => {
|
||||||
useSearchStore().initGlobalIndex()
|
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))
|
||||||
})
|
})
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -1,18 +1,22 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Monitor, Sun, Moon, Sparkles, Layers, LogOut } from '@lucide/vue'
|
import { Monitor, Sun, Moon, Sparkles, Layers, LogOut, Package, GripVertical } from '@lucide/vue'
|
||||||
import { Switch } from '@/components/ui/switch'
|
import { Switch } from '@/components/ui/switch'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { useAppStore, type Theme, type EffectType } from '@/stores/appStore'
|
import { useAppStore, type Theme, type EffectType, type ModuleInfo } from '@/stores/appStore'
|
||||||
import { useSearchStore } from '@/stores/searchStore'
|
import { useSearchStore } from '@/stores/searchStore'
|
||||||
|
import { useProcessStore } from '@/stores/processStore'
|
||||||
|
import { getModuleIcon } from '@/modules/icons'
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
|
import { VueDraggable } from 'vue-draggable-plus'
|
||||||
|
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
const searchStore = useSearchStore()
|
const searchStore = useSearchStore()
|
||||||
|
const processStore = useProcessStore()
|
||||||
|
|
||||||
// 始终反映系统真实的深浅色偏好,用于“跟随系统”卡片色块
|
// 始终反映系统真实的深浅色偏好,用于"跟随系统"卡片色块
|
||||||
const systemDark = ref(window.matchMedia('(prefers-color-scheme: dark)').matches)
|
const systemDark = ref(window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||||
const systemMediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
const systemMediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||||
const handleSystemMediaChange = (e: MediaQueryListEvent) => {
|
const handleSystemMediaChange = (e: MediaQueryListEvent) => {
|
||||||
@@ -82,6 +86,51 @@ const getThemeColor = (themeId: Theme) => {
|
|||||||
const quitApp = async () => {
|
const quitApp = async () => {
|
||||||
await invoke('quit_app')
|
await invoke('quit_app')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 判断模块开关是否处于处理中状态 */
|
||||||
|
const isModuleToggling = (moduleId: string): boolean => {
|
||||||
|
return appStore.togglingModules.has(moduleId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取模块的进程状态文本 */
|
||||||
|
const getProcessStatusText = (moduleId: string): string | null => {
|
||||||
|
const module = appStore.modules.find(m => m.id === moduleId)
|
||||||
|
if (!module?.hasProcess) return null
|
||||||
|
const status = processStore.getProcessStatus(module.id)
|
||||||
|
if (!status) return '未启动'
|
||||||
|
switch (status.status) {
|
||||||
|
case 'running': return '运行中'
|
||||||
|
case 'stopped': return '已停止'
|
||||||
|
case 'crashed': return '已崩溃'
|
||||||
|
case 'starting': return '启动中...'
|
||||||
|
default: return '未知'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 模块拖拽排序 =====
|
||||||
|
|
||||||
|
/** 可拖拽的模块列表(仅用户模块,按 moduleOrder 排序)—— 浅拷贝以支持 VueDraggable 原地修改 */
|
||||||
|
const dragList = ref<ModuleInfo[]>(
|
||||||
|
appStore.moduleOrder
|
||||||
|
.map(id => appStore.getModule(id))
|
||||||
|
.filter((m): m is ModuleInfo => !!m && !m.builtin)
|
||||||
|
.map(m => ({ ...m }))
|
||||||
|
)
|
||||||
|
|
||||||
|
/** 监听 store 中模块状态变化,同步 enabled 到本地拖拽列表 */
|
||||||
|
watch(() => appStore.modules, () => {
|
||||||
|
dragList.value.forEach(item => {
|
||||||
|
const storeModule = appStore.getModule(item.id)
|
||||||
|
if (storeModule) {
|
||||||
|
item.enabled = storeModule.enabled
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
|
/** 拖拽结束时,将新顺序同步到 store */
|
||||||
|
const onDragEnd = () => {
|
||||||
|
appStore.reorderModules(dragList.value.map(m => m.id))
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -101,8 +150,8 @@ const quitApp = async () => {
|
|||||||
<p class="text-sm text-muted-foreground">启动 Windows 时自动运行应用</p>
|
<p class="text-sm text-muted-foreground">启动 Windows 时自动运行应用</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
:checked="appStore.isAutoStart"
|
:model-value="appStore.isAutoStart"
|
||||||
@update:checked="appStore.toggleAutoStart"
|
@update:model-value="(checked: boolean) => appStore.toggleAutoStart(checked)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -199,6 +248,72 @@ const quitApp = async () => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle class="flex items-center gap-2">
|
||||||
|
<Package class="h-5 w-5 text-primary" />
|
||||||
|
模块管理
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<VueDraggable
|
||||||
|
v-model="dragList"
|
||||||
|
:animation="200"
|
||||||
|
:force-fallback="true"
|
||||||
|
handle=".drag-handle"
|
||||||
|
ghost-class="opacity-40"
|
||||||
|
chosen-class="drag-chosen"
|
||||||
|
class="space-y-2"
|
||||||
|
@end="onDragEnd"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="module in dragList"
|
||||||
|
:key="module.id"
|
||||||
|
class="flex items-center justify-between py-2 px-3 rounded-lg border border-border/50 hover:bg-secondary/30 transition-colors group"
|
||||||
|
:class="{ 'opacity-60': isModuleToggling(module.id) }"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div
|
||||||
|
class="drag-handle cursor-grab active:cursor-grabbing text-muted-foreground/40 hover:text-muted-foreground transition-colors"
|
||||||
|
title="拖拽排序"
|
||||||
|
>
|
||||||
|
<GripVertical class="h-4 w-4 no-native-drag" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="w-9 h-9 rounded-lg flex items-center justify-center"
|
||||||
|
:class="module.enabled ? 'bg-primary/10 text-primary' : 'bg-muted text-muted-foreground'"
|
||||||
|
>
|
||||||
|
<component :is="getModuleIcon(module.icon)" class="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="font-medium text-sm flex items-center gap-2">
|
||||||
|
{{ module.name }}
|
||||||
|
<span
|
||||||
|
v-if="getProcessStatusText(module.id)"
|
||||||
|
class="text-xs px-1.5 py-0.5 rounded-full"
|
||||||
|
:class="module.enabled ? 'bg-green-500/10 text-green-600 dark:text-green-400' : 'bg-muted text-muted-foreground'"
|
||||||
|
>
|
||||||
|
{{ getProcessStatusText(module.id) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-muted-foreground">
|
||||||
|
{{ module.description }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
:model-value="module.enabled"
|
||||||
|
:disabled="module.builtin || isModuleToggling(module.id)"
|
||||||
|
@update:model-value="(checked: boolean) => appStore.toggleModule(module.id, checked)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</VueDraggable>
|
||||||
|
<p class="mt-4 text-xs text-muted-foreground">
|
||||||
|
拖拽手柄可调整模块顺序,禁用模块将从侧边栏隐藏并停止后台进程以减少内存占用。更改后立即生效。
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="flex items-center gap-2">
|
<CardTitle class="flex items-center gap-2">
|
||||||
@@ -220,3 +335,14 @@ const quitApp = async () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.no-native-drag {
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-chosen {
|
||||||
|
box-shadow: 0 0 0 2px hsl(var(--primary) / 0.3);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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<string, Component> = {
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -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 }
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -1,24 +1,617 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Globe } from '@lucide/vue'
|
import {
|
||||||
|
Globe, Play, Square, RotateCw, Power, Zap, Plus, RefreshCw, Trash2,
|
||||||
|
Check, AlertCircle, Server, Settings as SettingsIcon, ListChecks,
|
||||||
|
Upload, Link2, Loader2
|
||||||
|
} from '@lucide/vue'
|
||||||
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
|
import { toast } from 'vue-sonner'
|
||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
import { useProxyStore, type ProxyNode } from '@/stores/proxyStore'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Switch } from '@/components/ui/switch'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
|
import { Separator } from '@/components/ui/separator'
|
||||||
|
|
||||||
|
const store = useProxyStore()
|
||||||
|
|
||||||
|
const activeTab = ref('overview')
|
||||||
|
const starting = ref(false)
|
||||||
|
const stopping = ref(false)
|
||||||
|
const restarting = ref(false)
|
||||||
|
const sysProxyLoading = ref(false)
|
||||||
|
const importUrl = ref('')
|
||||||
|
const importName = ref('')
|
||||||
|
const importing = ref(false)
|
||||||
|
const testingGroups = ref<Set<string>>(new Set())
|
||||||
|
|
||||||
|
// 进程状态轮询
|
||||||
|
let statusTimer: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
|
const running = computed(() => store.status.running)
|
||||||
|
|
||||||
|
// 代理组(Selector/URLTest/Fallback/LoadBalance)
|
||||||
|
const GROUP_TYPES = ['Selector', 'URLTest', 'Fallback', 'LoadBalance']
|
||||||
|
const groups = computed<Array<[string, ProxyNode]>>(() => {
|
||||||
|
return Object.entries(store.proxies).filter(([, n]) => GROUP_TYPES.includes(n.type))
|
||||||
|
})
|
||||||
|
|
||||||
|
const modeOptions = [
|
||||||
|
{ value: 'rule', label: '规则' },
|
||||||
|
{ value: 'global', label: '全局' },
|
||||||
|
{ value: 'direct', label: '直连' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const currentProfile = computed(() =>
|
||||||
|
store.settings?.profiles.find(p => p.id === store.settings?.currentProfile) ?? null
|
||||||
|
)
|
||||||
|
|
||||||
|
const formatSize = (bytes: number) => {
|
||||||
|
if (bytes < 1024) return `${bytes} B`
|
||||||
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
||||||
|
return `${(bytes / 1024 / 1024).toFixed(2)} MB`
|
||||||
|
}
|
||||||
|
|
||||||
|
const delayColor = (delay: number | undefined) => {
|
||||||
|
if (!delay) return 'text-muted-foreground'
|
||||||
|
if (delay < 150) return 'text-emerald-500'
|
||||||
|
if (delay < 400) return 'text-amber-500'
|
||||||
|
return 'text-red-500'
|
||||||
|
}
|
||||||
|
|
||||||
|
const delayText = (delay: number | undefined) => {
|
||||||
|
if (delay === undefined) return '—'
|
||||||
|
if (delay === 0) return '超时'
|
||||||
|
return `${delay}ms`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 生命周期 =====
|
||||||
|
const init = async () => {
|
||||||
|
await Promise.all([store.loadSettings(), store.refreshKernel(), store.refreshStatus()])
|
||||||
|
if (running.value) {
|
||||||
|
store.refreshVersion()
|
||||||
|
store.loadProxies().catch(() => {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
init()
|
||||||
|
statusTimer = setInterval(async () => {
|
||||||
|
await store.refreshStatus()
|
||||||
|
}, 3000)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (statusTimer) clearInterval(statusTimer)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 状态从停止→运行时,加载节点与版本
|
||||||
|
watch(running, async (val, old) => {
|
||||||
|
if (val && !old) {
|
||||||
|
await store.refreshVersion()
|
||||||
|
await store.loadProxies().catch(() => {})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// ===== 进程控制 =====
|
||||||
|
const handleStart = async () => {
|
||||||
|
starting.value = true
|
||||||
|
try {
|
||||||
|
await store.start()
|
||||||
|
toast.success('mihomo 已启动')
|
||||||
|
await store.refreshVersion()
|
||||||
|
await store.loadProxies().catch(() => {})
|
||||||
|
} catch (e) {
|
||||||
|
toast.error('启动失败', { description: String(e) })
|
||||||
|
} finally {
|
||||||
|
starting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStop = async () => {
|
||||||
|
stopping.value = true
|
||||||
|
try {
|
||||||
|
await store.stop()
|
||||||
|
toast.success('mihomo 已停止')
|
||||||
|
} catch (e) {
|
||||||
|
toast.error('停止失败', { description: String(e) })
|
||||||
|
} finally {
|
||||||
|
stopping.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRestart = async () => {
|
||||||
|
restarting.value = true
|
||||||
|
try {
|
||||||
|
await store.restart()
|
||||||
|
toast.success('mihomo 已重启')
|
||||||
|
await store.refreshVersion()
|
||||||
|
await store.loadProxies().catch(() => {})
|
||||||
|
} catch (e) {
|
||||||
|
toast.error('重启失败', { description: String(e) })
|
||||||
|
} finally {
|
||||||
|
restarting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 系统代理 =====
|
||||||
|
const onToggleSystemProxy = async (on: boolean) => {
|
||||||
|
sysProxyLoading.value = true
|
||||||
|
try {
|
||||||
|
await store.toggleSystemProxy(on)
|
||||||
|
toast.success(on ? '系统代理已开启' : '系统代理已关闭')
|
||||||
|
} catch (e) {
|
||||||
|
toast.error('操作失败', { description: String(e) })
|
||||||
|
} finally {
|
||||||
|
sysProxyLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 模式切换 =====
|
||||||
|
const changeMode = async (mode: string) => {
|
||||||
|
if (!store.settings || store.settings.mode === mode) return
|
||||||
|
const prev = store.settings.mode
|
||||||
|
store.settings.mode = mode
|
||||||
|
try {
|
||||||
|
await store.saveSettings({ ...store.settings })
|
||||||
|
if (running.value) {
|
||||||
|
await invokePatchConfigs({ mode })
|
||||||
|
}
|
||||||
|
toast.success(`已切换为${modeOptions.find(m => m.value === mode)?.label}模式`)
|
||||||
|
} catch (e) {
|
||||||
|
if (store.settings) store.settings.mode = prev
|
||||||
|
toast.error('模式切换失败', { description: String(e) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const invokePatchConfigs = (body: Record<string, unknown>) =>
|
||||||
|
invoke('proxy_patch_configs', { body })
|
||||||
|
|
||||||
|
// ===== 节点 =====
|
||||||
|
const selectNode = async (group: string, name: string) => {
|
||||||
|
// 仅 Selector 允许手动选择
|
||||||
|
if (store.proxies[group]?.type !== 'Selector') return
|
||||||
|
try {
|
||||||
|
await store.selectProxy(group, name)
|
||||||
|
} catch (e) {
|
||||||
|
toast.error('切换节点失败', { description: String(e) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const testGroup = async (groupName: string) => {
|
||||||
|
const group = store.proxies[groupName]
|
||||||
|
if (!group?.all?.length) return
|
||||||
|
testingGroups.value.add(groupName)
|
||||||
|
try {
|
||||||
|
await store.testDelayBatch(group.all)
|
||||||
|
toast.success(`「${groupName}」测速完成`)
|
||||||
|
} catch (e) {
|
||||||
|
toast.error('测速失败', { description: String(e) })
|
||||||
|
} finally {
|
||||||
|
testingGroups.value.delete(groupName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeDelay = (name: string): number | undefined => {
|
||||||
|
return store.proxies[name]?.history?.[0]?.delay
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 订阅 =====
|
||||||
|
const doImport = async () => {
|
||||||
|
if (!importUrl.value.trim()) {
|
||||||
|
toast.warning('请输入订阅地址')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
importing.value = true
|
||||||
|
try {
|
||||||
|
const name = importName.value.trim() || `订阅 ${new Date().toLocaleString()}`
|
||||||
|
await store.importProfile(importUrl.value.trim(), name)
|
||||||
|
toast.success('订阅导入成功')
|
||||||
|
importUrl.value = ''
|
||||||
|
importName.value = ''
|
||||||
|
} catch (e) {
|
||||||
|
toast.error('导入失败', { description: String(e) })
|
||||||
|
} finally {
|
||||||
|
importing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const doUpdate = async (id: string) => {
|
||||||
|
try {
|
||||||
|
await store.updateProfile(id)
|
||||||
|
toast.success('订阅已更新')
|
||||||
|
} catch (e) {
|
||||||
|
toast.error('更新失败', { description: String(e) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const doDelete = async (id: string, name: string) => {
|
||||||
|
if (!confirm(`确定删除订阅「${name}」?`)) return
|
||||||
|
try {
|
||||||
|
await store.deleteProfile(id)
|
||||||
|
toast.success('已删除订阅')
|
||||||
|
} catch (e) {
|
||||||
|
toast.error('删除失败', { description: String(e) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const doActivate = async (id: string) => {
|
||||||
|
try {
|
||||||
|
await store.activateProfile(id)
|
||||||
|
toast.success('已切换订阅,配置已重新生成')
|
||||||
|
if (running.value) {
|
||||||
|
await handleRestart()
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
toast.error('切换失败', { description: String(e) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 设置 =====
|
||||||
|
const localSettings = ref({
|
||||||
|
mixedPort: 7890,
|
||||||
|
externalController: '127.0.0.1:9090',
|
||||||
|
secret: '',
|
||||||
|
logLevel: 'info',
|
||||||
|
allowLan: false,
|
||||||
|
autoStart: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const syncLocalSettings = () => {
|
||||||
|
if (store.settings) {
|
||||||
|
localSettings.value = {
|
||||||
|
mixedPort: store.settings.mixedPort,
|
||||||
|
externalController: store.settings.externalController,
|
||||||
|
secret: store.settings.secret,
|
||||||
|
logLevel: store.settings.logLevel,
|
||||||
|
allowLan: store.settings.allowLan,
|
||||||
|
autoStart: store.settings.autoStart
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => store.settings, syncLocalSettings, { immediate: true })
|
||||||
|
|
||||||
|
const savingSettings = ref(false)
|
||||||
|
const saveSettingsForm = async () => {
|
||||||
|
if (!store.settings) return
|
||||||
|
savingSettings.value = true
|
||||||
|
try {
|
||||||
|
await store.saveSettings({
|
||||||
|
...store.settings,
|
||||||
|
...localSettings.value
|
||||||
|
})
|
||||||
|
toast.success('设置已保存')
|
||||||
|
} catch (e) {
|
||||||
|
toast.error('保存失败', { description: String(e) })
|
||||||
|
} finally {
|
||||||
|
savingSettings.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-full p-6 overflow-y-auto">
|
<div class="h-full p-6">
|
||||||
|
<Tabs v-model="activeTab" class="h-full flex flex-col">
|
||||||
|
<TabsList class="grid w-full grid-cols-4 max-w-md">
|
||||||
|
<TabsTrigger value="overview" class="gap-1.5"><Globe class="size-3.5" />概览</TabsTrigger>
|
||||||
|
<TabsTrigger value="proxies" class="gap-1.5"><Server class="size-3.5" />节点</TabsTrigger>
|
||||||
|
<TabsTrigger value="profiles" class="gap-1.5"><ListChecks class="size-3.5" />订阅</TabsTrigger>
|
||||||
|
<TabsTrigger value="settings" class="gap-1.5"><SettingsIcon class="size-3.5" />设置</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<!-- 概览 -->
|
||||||
|
<TabsContent value="overview" class="flex-1 mt-4 overflow-y-auto">
|
||||||
|
<div class="grid gap-4 md:grid-cols-2">
|
||||||
|
<!-- 内核状态 -->
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="flex items-center gap-2">
|
<CardTitle class="flex items-center gap-2 text-base">
|
||||||
<Globe class="h-5 w-5 text-primary" />
|
<Server class="size-4 text-primary" />内核
|
||||||
代理管理模块
|
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent class="space-y-3 text-sm">
|
||||||
<div class="flex flex-col items-center justify-center h-64 text-muted-foreground">
|
<div class="flex items-center justify-between">
|
||||||
<Globe class="h-16 w-16 mb-4 opacity-50" />
|
<span class="text-muted-foreground">状态</span>
|
||||||
<p>代理管理功能开发中...</p>
|
<span v-if="store.kernel?.exists" class="flex items-center gap-1 text-emerald-500">
|
||||||
<p class="text-sm mt-2">支持系统代理切换、规则配置、延迟测速等功能</p>
|
<Check class="size-3.5" />已安装
|
||||||
|
</span>
|
||||||
|
<span v-else class="flex items-center gap-1 text-red-500">
|
||||||
|
<AlertCircle class="size-3.5" />未安装
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-muted-foreground">版本</span>
|
||||||
|
<span class="font-mono text-xs">{{ store.kernel?.version ?? '—' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between gap-3">
|
||||||
|
<span class="text-muted-foreground shrink-0">路径</span>
|
||||||
|
<span class="font-mono text-xs text-right break-all">{{ store.kernel?.path ?? '—' }}</span>
|
||||||
|
</div>
|
||||||
|
<p v-if="!store.kernel?.exists" class="text-xs text-amber-600 dark:text-amber-500 leading-relaxed">
|
||||||
|
请将 mihomo.exe 放到 <code class="px-1 bg-muted rounded">src-tauri/binaries/</code> 后重启应用,或直接放到上述 cores 目录。
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- 运行状态 -->
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle class="flex items-center gap-2 text-base">
|
||||||
|
<Zap class="size-4 text-primary" />运行状态
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="space-y-3 text-sm">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-muted-foreground">mihomo</span>
|
||||||
|
<span v-if="running" class="flex items-center gap-1 text-emerald-500">
|
||||||
|
<span class="size-2 rounded-full bg-emerald-500" />运行中
|
||||||
|
</span>
|
||||||
|
<span v-else class="flex items-center gap-1 text-muted-foreground">
|
||||||
|
<span class="size-2 rounded-full bg-muted-foreground" />已停止
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-muted-foreground">PID</span>
|
||||||
|
<span class="font-mono text-xs">{{ store.status.pid ?? '—' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-muted-foreground">API 版本</span>
|
||||||
|
<span class="font-mono text-xs">{{ store.version || '—' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-muted-foreground">重启次数</span>
|
||||||
|
<span class="font-mono text-xs">{{ store.status.restartCount }}</span>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Button v-if="!running" size="sm" :disabled="starting" @click="handleStart">
|
||||||
|
<Loader2 v-if="starting" class="size-3.5 animate-spin" />
|
||||||
|
<Play v-else class="size-3.5" />启动
|
||||||
|
</Button>
|
||||||
|
<template v-else>
|
||||||
|
<Button size="sm" variant="destructive" :disabled="stopping" @click="handleStop">
|
||||||
|
<Loader2 v-if="stopping" class="size-3.5 animate-spin" />
|
||||||
|
<Square v-else class="size-3.5" />停止
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="outline" :disabled="restarting" @click="handleRestart">
|
||||||
|
<Loader2 v-if="restarting" class="size-3.5 animate-spin" />
|
||||||
|
<RotateCw v-else class="size-3.5" />重启
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- 系统代理 -->
|
||||||
|
<Card :class="{ 'opacity-60': sysProxyLoading }">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle class="flex items-center gap-2 text-base">
|
||||||
|
<Power class="size-4 text-primary" />系统代理
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="flex items-center justify-between">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-sm">Windows 系统代理</p>
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
{{ store.systemProxy ? `指向 127.0.0.1:${store.settings?.mixedPort ?? 7890}` : '已关闭' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
:model-value="store.systemProxy"
|
||||||
|
:disabled="sysProxyLoading"
|
||||||
|
@update:model-value="onToggleSystemProxy"
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- 当前订阅 & 模式 -->
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle class="flex items-center gap-2 text-base">
|
||||||
|
<ListChecks class="size-4 text-primary" />订阅与模式
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="space-y-3 text-sm">
|
||||||
|
<div class="flex items-center justify-between gap-3">
|
||||||
|
<span class="text-muted-foreground shrink-0">当前订阅</span>
|
||||||
|
<span class="text-right truncate">{{ currentProfile?.name ?? '无' }}</span>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-muted-foreground">运行模式</span>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<Button
|
||||||
|
v-for="m in modeOptions" :key="m.value"
|
||||||
|
size="xs"
|
||||||
|
:variant="store.settings?.mode === m.value ? 'default' : 'outline'"
|
||||||
|
:disabled="!running && store.settings?.mode !== m.value"
|
||||||
|
@click="changeMode(m.value)"
|
||||||
|
>{{ m.label }}</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<!-- 节点 -->
|
||||||
|
<TabsContent value="proxies" class="flex-1 mt-4 min-h-0">
|
||||||
|
<div v-if="!running" class="h-full flex flex-col items-center justify-center text-muted-foreground gap-2">
|
||||||
|
<Server class="size-12 opacity-30" />
|
||||||
|
<p class="text-sm">mihomo 未运行,请先在概览页启动</p>
|
||||||
|
</div>
|
||||||
|
<ScrollArea v-else class="h-full pr-3">
|
||||||
|
<div v-if="!groups.length" class="text-center text-sm text-muted-foreground py-12">
|
||||||
|
暂无代理组,请先在订阅页导入并激活配置
|
||||||
|
</div>
|
||||||
|
<div class="space-y-4 pb-4">
|
||||||
|
<Card v-for="[gname, group] in groups" :key="gname">
|
||||||
|
<CardHeader class="pb-3">
|
||||||
|
<CardTitle class="flex items-center justify-between text-base">
|
||||||
|
<span class="flex items-center gap-2">
|
||||||
|
<Server class="size-4 text-primary" />{{ gname }}
|
||||||
|
<span class="text-xs font-normal text-muted-foreground">{{ group.type }}</span>
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
size="xs" variant="outline"
|
||||||
|
:disabled="testingGroups.has(gname)"
|
||||||
|
@click="testGroup(gname)"
|
||||||
|
>
|
||||||
|
<Loader2 v-if="testingGroups.has(gname)" class="size-3 animate-spin" />
|
||||||
|
<Zap v-else class="size-3" />测速
|
||||||
|
</Button>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-3 gap-1.5">
|
||||||
|
<button
|
||||||
|
v-for="node in group.all" :key="node"
|
||||||
|
type="button"
|
||||||
|
class="flex items-center justify-between gap-2 rounded-md border px-2.5 py-1.5 text-xs transition-colors hover:bg-accent"
|
||||||
|
:class="group.now === node ? 'border-primary bg-primary/10' : 'border-border'"
|
||||||
|
@click="selectNode(gname, node)"
|
||||||
|
>
|
||||||
|
<span class="truncate text-left">{{ store.proxies[node]?.name ?? node }}</span>
|
||||||
|
<span class="font-mono shrink-0" :class="delayColor(nodeDelay(node))">
|
||||||
|
{{ delayText(nodeDelay(node)) }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<!-- 订阅 -->
|
||||||
|
<TabsContent value="profiles" class="flex-1 mt-4 overflow-y-auto">
|
||||||
|
<div class="space-y-4 max-w-3xl">
|
||||||
|
<!-- 导入 -->
|
||||||
|
<Card>
|
||||||
|
<CardHeader class="pb-3">
|
||||||
|
<CardTitle class="flex items-center gap-2 text-base">
|
||||||
|
<Plus class="size-4 text-primary" />导入订阅
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="space-y-3">
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<Label for="sub-url">订阅地址</Label>
|
||||||
|
<Input id="sub-url" v-model="importUrl" placeholder="https://example.com/sub.yaml" />
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<Label for="sub-name">名称(可选)</Label>
|
||||||
|
<Input id="sub-name" v-model="importName" placeholder="我的订阅" />
|
||||||
|
</div>
|
||||||
|
<Button size="sm" :disabled="importing" @click="doImport">
|
||||||
|
<Loader2 v-if="importing" class="size-3.5 animate-spin" />
|
||||||
|
<Upload v-else class="size-3.5" />导入
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- 列表 -->
|
||||||
|
<Card>
|
||||||
|
<CardHeader class="pb-3">
|
||||||
|
<CardTitle class="text-base">订阅列表</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div v-if="!store.settings?.profiles.length" class="text-center text-sm text-muted-foreground py-8">
|
||||||
|
暂无订阅
|
||||||
|
</div>
|
||||||
|
<div v-else class="space-y-2">
|
||||||
|
<div
|
||||||
|
v-for="p in store.settings.profiles" :key="p.id"
|
||||||
|
class="flex items-center gap-3 rounded-md border p-3"
|
||||||
|
:class="store.settings.currentProfile === p.id ? 'border-primary bg-primary/5' : 'border-border'"
|
||||||
|
>
|
||||||
|
<div class="flex-1 min-w-0 space-y-1">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Link2 class="size-3.5 text-muted-foreground shrink-0" />
|
||||||
|
<span class="font-medium text-sm truncate">{{ p.name }}</span>
|
||||||
|
<span v-if="store.settings.currentProfile === p.id" class="text-xs text-primary">当前</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-muted-foreground truncate">{{ p.url }}</p>
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
{{ formatSize(p.size) }} · 更新于 {{ p.updatedAt }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-1 shrink-0">
|
||||||
|
<Button size="icon-sm" variant="ghost" title="更新" @click="doUpdate(p.id)">
|
||||||
|
<RefreshCw class="size-3.5" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
v-if="store.settings.currentProfile !== p.id"
|
||||||
|
size="icon-sm" variant="ghost" title="切换" @click="doActivate(p.id)"
|
||||||
|
>
|
||||||
|
<Check class="size-3.5" />
|
||||||
|
</Button>
|
||||||
|
<Button size="icon-sm" variant="ghost" title="删除" @click="doDelete(p.id, p.name)">
|
||||||
|
<Trash2 class="size-3.5 text-destructive" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<!-- 设置 -->
|
||||||
|
<TabsContent value="settings" class="flex-1 mt-4 overflow-y-auto">
|
||||||
|
<Card class="max-w-2xl">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle class="flex items-center gap-2 text-base">
|
||||||
|
<SettingsIcon class="size-4 text-primary" />基础设置
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="space-y-4">
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<Label for="mixed-port">混合代理端口</Label>
|
||||||
|
<Input id="mixed-port" v-model.number="localSettings.mixedPort" type="number" />
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<Label for="api-addr">控制接口地址</Label>
|
||||||
|
<Input id="api-addr" v-model="localSettings.externalController" placeholder="127.0.0.1:9090" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<Label for="secret">API 密钥(留空则不鉴权)</Label>
|
||||||
|
<Input id="secret" v-model="localSettings.secret" placeholder="可选" />
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<Label for="log-level">日志级别</Label>
|
||||||
|
<Input id="log-level" v-model="localSettings.logLevel" placeholder="info" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between rounded-md border p-3">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm">允许局域网连接</p>
|
||||||
|
<p class="text-xs text-muted-foreground">允许其他设备通过本机代理上网</p>
|
||||||
|
</div>
|
||||||
|
<Switch v-model="localSettings.allowLan" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between rounded-md border p-3">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm">模块启用时自动启动</p>
|
||||||
|
<p class="text-xs text-muted-foreground">在设置中开启代理模块时自动运行 mihomo</p>
|
||||||
|
</div>
|
||||||
|
<Switch v-model="localSettings.autoStart" />
|
||||||
|
</div>
|
||||||
|
<Button size="sm" :disabled="savingSettings" @click="saveSettingsForm">
|
||||||
|
<Loader2 v-if="savingSettings" class="size-3.5 animate-spin" />
|
||||||
|
<Check v-else class="size-3.5" />保存设置
|
||||||
|
</Button>
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
修改端口/接口/密钥后需重启 mihomo 生效。
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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<string, ModuleConfig>()
|
||||||
|
private loadedComponents = new Map<string, Component>()
|
||||||
|
|
||||||
|
/** 注册一个模块 */
|
||||||
|
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<ModuleRegistry['getSearchItems']> }> {
|
||||||
|
return this.getAllConfigs().map(c => ({
|
||||||
|
moduleId: c.id,
|
||||||
|
items: c.searchItems ?? []
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 异步加载模块组件,结果会被缓存 */
|
||||||
|
async loadComponent(id: string): Promise<Component | null> {
|
||||||
|
// 缓存命中
|
||||||
|
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<Component | null>(moduleRegistry.getLoadedComponent(moduleId))
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
component.value = await moduleRegistry.loadComponent(moduleId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { component, load }
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
+297
-22
@@ -1,48 +1,286 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { getCurrentWindow, Effect, EffectState } from '@tauri-apps/api/window'
|
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 Theme = 'light' | 'dark' | 'system'
|
||||||
export type EffectType = 'normal' | 'mica' | 'acrylic'
|
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 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', () => {
|
export const useAppStore = defineStore('app', () => {
|
||||||
const theme = ref<Theme>('system')
|
const theme = ref<Theme>('system')
|
||||||
const effect = ref<EffectType>('mica')
|
const effect = ref<EffectType>('mica')
|
||||||
const isAutoStart = ref(false)
|
const isAutoStart = ref(false)
|
||||||
const isInitialized = 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 {
|
try {
|
||||||
const saved = localStorage.getItem(STORAGE_KEY)
|
const saved = localStorage.getItem(STORAGE_KEY)
|
||||||
if (saved) {
|
if (saved) {
|
||||||
const settings = JSON.parse(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.theme) theme.value = settings.theme
|
||||||
if (settings.effect) effect.value = settings.effect
|
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 {
|
} catch {
|
||||||
console.error('Failed to load settings from localStorage')
|
console.error('Failed to load settings from localStorage')
|
||||||
|
localStorage.removeItem(STORAGE_KEY)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveSettings = () => {
|
const saveSettings = () => {
|
||||||
try {
|
try {
|
||||||
|
const modulesData = modules.value.map(m => ({
|
||||||
|
id: m.id,
|
||||||
|
enabled: m.enabled
|
||||||
|
}))
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify({
|
localStorage.setItem(STORAGE_KEY, JSON.stringify({
|
||||||
|
version: SETTINGS_VERSION,
|
||||||
theme: theme.value,
|
theme: theme.value,
|
||||||
effect: effect.value,
|
effect: effect.value,
|
||||||
isAutoStart: isAutoStart.value
|
isAutoStart: isAutoStart.value,
|
||||||
|
modules: modulesData,
|
||||||
|
moduleOrder: moduleOrder.value
|
||||||
}))
|
}))
|
||||||
} catch {
|
} catch {
|
||||||
console.error('Failed to save settings to localStorage')
|
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) => {
|
const setTheme = async (newTheme: Theme) => {
|
||||||
theme.value = newTheme
|
theme.value = newTheme
|
||||||
// (仅系统级主题广播或更换效果时才刷新)。因此亚克力限制为仅"跟随系统"可用。
|
|
||||||
// 切到非系统主题时若当前为亚克力,自动回退到云母,避免深浅色不同步。
|
|
||||||
if (newTheme !== 'system' && effect.value === 'acrylic') {
|
if (newTheme !== 'system' && effect.value === 'acrylic') {
|
||||||
effect.value = 'mica'
|
effect.value = 'mica'
|
||||||
}
|
}
|
||||||
@@ -56,9 +294,23 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
saveSettings()
|
saveSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleAutoStart = () => {
|
const toggleAutoStart = async (checked?: boolean) => {
|
||||||
isAutoStart.value = !isAutoStart.value
|
const targetState = checked !== undefined ? checked : !isAutoStart.value
|
||||||
|
const previousState = isAutoStart.value
|
||||||
|
|
||||||
|
try {
|
||||||
|
isAutoStart.value = targetState
|
||||||
|
if (targetState) {
|
||||||
|
await enable()
|
||||||
|
} else {
|
||||||
|
await disable()
|
||||||
|
}
|
||||||
saveSettings()
|
saveSettings()
|
||||||
|
} catch (e) {
|
||||||
|
isAutoStart.value = previousState
|
||||||
|
console.error('Failed to toggle auto-start:', e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const applyTheme = async () => {
|
const applyTheme = async () => {
|
||||||
@@ -80,7 +332,7 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
const tauriWindow = getCurrentWindow()
|
const tauriWindow = getCurrentWindow()
|
||||||
await tauriWindow.setTheme(isDark ? 'dark' : 'light')
|
await tauriWindow.setTheme(isDark ? 'dark' : 'light')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to set window theme:', e)
|
// 非 Tauri 环境下忽略
|
||||||
}
|
}
|
||||||
|
|
||||||
await applyEffect()
|
await applyEffect()
|
||||||
@@ -95,25 +347,19 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
const tauriWindow = getCurrentWindow()
|
const tauriWindow = getCurrentWindow()
|
||||||
const isDark = root.classList.contains('dark')
|
const isDark = root.classList.contains('dark')
|
||||||
|
|
||||||
// 先清除旧效果
|
|
||||||
await tauriWindow.clearEffects()
|
await tauriWindow.clearEffects()
|
||||||
|
|
||||||
if (effect.value === 'normal') {
|
if (effect.value === 'normal') {
|
||||||
// 普通模式:不使用原生效果,用不透明背景色
|
|
||||||
await tauriWindow.setBackgroundColor(isDark ? '#0f172a' : '#ffffff')
|
await tauriWindow.setBackgroundColor(isDark ? '#0f172a' : '#ffffff')
|
||||||
} else if (effect.value === 'mica') {
|
} else if (effect.value === 'mica') {
|
||||||
// 浅色用 micaLight,深色用 micaDark。
|
|
||||||
// 注意:micaDark 仅在系统处于深色模式时才会渲染为深色(Windows 限制)。
|
|
||||||
const micaEffect = (isDark ? 'micaDark' : 'micaLight') as unknown as Effect
|
const micaEffect = (isDark ? 'micaDark' : 'micaLight') as unknown as Effect
|
||||||
await tauriWindow.setEffects({
|
await tauriWindow.setEffects({
|
||||||
effects: [micaEffect],
|
effects: [micaEffect],
|
||||||
state: EffectState.FollowsWindowActiveState,
|
state: EffectState.FollowsWindowActiveState,
|
||||||
color: isDark ? [30, 41, 59, 0] : [248, 250, 252, 0]
|
color: isDark ? [30, 41, 59, 0] : [248, 250, 252, 0]
|
||||||
})
|
})
|
||||||
// 窗口背景必须透明,原生效果才能显示
|
|
||||||
await tauriWindow.setBackgroundColor('#00000000')
|
await tauriWindow.setBackgroundColor('#00000000')
|
||||||
} else if (effect.value === 'acrylic') {
|
} else if (effect.value === 'acrylic') {
|
||||||
// Acrylic:亚克力效果,color 使用半透明 RGBA
|
|
||||||
await tauriWindow.setEffects({
|
await tauriWindow.setEffects({
|
||||||
effects: [Effect.Acrylic],
|
effects: [Effect.Acrylic],
|
||||||
state: EffectState.FollowsWindowActiveState,
|
state: EffectState.FollowsWindowActiveState,
|
||||||
@@ -122,7 +368,7 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
await tauriWindow.setBackgroundColor('#00000000')
|
await tauriWindow.setBackgroundColor('#00000000')
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to set window effects:', e)
|
// 非 Tauri 环境下忽略
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,8 +384,8 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
try {
|
try {
|
||||||
const tauriWindow = getCurrentWindow()
|
const tauriWindow = getCurrentWindow()
|
||||||
await tauriWindow.setTheme(e.matches ? 'dark' : 'light')
|
await tauriWindow.setTheme(e.matches ? 'dark' : 'light')
|
||||||
} catch (err) {
|
} catch (e) {
|
||||||
console.error('Failed to update window theme on system change:', err)
|
// 非 Tauri 环境下忽略
|
||||||
}
|
}
|
||||||
|
|
||||||
await applyEffect()
|
await applyEffect()
|
||||||
@@ -148,8 +394,29 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
|
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
try {
|
try {
|
||||||
loadSettings()
|
await loadSettings()
|
||||||
// applyTheme 内部已调用 applyEffect,无需重复调用
|
|
||||||
|
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()
|
await applyTheme()
|
||||||
|
|
||||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||||
@@ -158,9 +425,10 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
isInitialized.value = true
|
isInitialized.value = true
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
try {
|
||||||
await getCurrentWindow().show()
|
const tauriWindow = getCurrentWindow()
|
||||||
|
await tauriWindow.show()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to show window:', e)
|
// 非 Tauri 环境下忽略
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,6 +438,13 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
effect,
|
effect,
|
||||||
isAutoStart,
|
isAutoStart,
|
||||||
isInitialized,
|
isInitialized,
|
||||||
|
modules,
|
||||||
|
moduleOrder,
|
||||||
|
enabledModules,
|
||||||
|
togglingModules,
|
||||||
|
getModule,
|
||||||
|
toggleModule,
|
||||||
|
reorderModules,
|
||||||
setTheme,
|
setTheme,
|
||||||
setEffect,
|
setEffect,
|
||||||
toggleAutoStart,
|
toggleAutoStart,
|
||||||
|
|||||||
@@ -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
|
moduleId: string
|
||||||
items: SearchIndexItem[]
|
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 { defineStore } from 'pinia'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { searchIndex } from './searchIndex'
|
import { moduleRegistry } from '@/modules/registry'
|
||||||
|
|
||||||
export interface SearchItem {
|
export interface SearchItem {
|
||||||
id: string
|
id: string
|
||||||
@@ -14,12 +14,19 @@ export interface SearchItem {
|
|||||||
export const useSearchStore = defineStore('search', () => {
|
export const useSearchStore = defineStore('search', () => {
|
||||||
const items = ref<SearchItem[]>([])
|
const items = ref<SearchItem[]>([])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化全局搜索索引
|
||||||
|
* 从模块注册表收集所有模块的搜索项并注册。
|
||||||
|
* 注意:被禁用的模块搜索项也会被注册,但 appStore.toggleModule
|
||||||
|
* 在禁用时会调用 unregisterModule 移除,启用时会调用 registerItem 恢复。
|
||||||
|
*/
|
||||||
const initGlobalIndex = () => {
|
const initGlobalIndex = () => {
|
||||||
searchIndex.forEach(config => {
|
const allSearchItems = moduleRegistry.getAllSearchItems()
|
||||||
config.items.forEach((item, index) => {
|
allSearchItems.forEach(({ moduleId, items: moduleItems }) => {
|
||||||
|
moduleItems.forEach((item, index) => {
|
||||||
const searchItem: SearchItem = {
|
const searchItem: SearchItem = {
|
||||||
id: `${config.moduleId}-search-${index}`,
|
id: `${moduleId}-search-${index}`,
|
||||||
moduleId: config.moduleId,
|
moduleId,
|
||||||
title: item.title,
|
title: item.title,
|
||||||
description: item.description,
|
description: item.description,
|
||||||
keywords: item.keywords
|
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) => {
|
const search = (query: string) => {
|
||||||
if (!query.trim()) return []
|
if (!query.trim()) return []
|
||||||
const lowerQuery = query.toLowerCase()
|
const lowerQuery = query.toLowerCase()
|
||||||
@@ -86,6 +98,7 @@ export const useSearchStore = defineStore('search', () => {
|
|||||||
registerItem,
|
registerItem,
|
||||||
registerItems,
|
registerItems,
|
||||||
unregisterItem,
|
unregisterItem,
|
||||||
|
unregisterModule,
|
||||||
search,
|
search,
|
||||||
getItemsByModule
|
getItemsByModule
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<void>
|
||||||
|
/** 模块组件挂载时调用 */
|
||||||
|
onActivate?: () => void | Promise<void>
|
||||||
|
/** 模块组件卸载时调用 */
|
||||||
|
onDeactivate?: () => void | Promise<void>
|
||||||
|
/** 模块被禁用时调用 */
|
||||||
|
onDisable?: () => void | Promise<void>
|
||||||
|
/** 模块被启用时调用 */
|
||||||
|
onEnable?: () => void | Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 模块配置 —— 每个模块通过 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user