feat: 实现命令面板、颜色取色、JSON格式化和系统信息功能

- 重构项目架构,采用四层架构模式 (Command → Service → Platform → Utils)
  - 实现命令面板功能,支持快捷搜索和特征分类
  - 添加颜色取色功能,支持屏幕像素颜色获取
  - 添加JSON格式化功能,支持JSON格式化和压缩
  - 添加系统信息功能,显示操作系统和硬件信息
  - 移除旧的状态文档和无用配置文件
This commit is contained in:
2026-02-10 18:46:11 +08:00
parent db4978e349
commit 927eaa1e03
62 changed files with 7536 additions and 1958 deletions

View File

@@ -0,0 +1,9 @@
{
"permissions": {
"allow": [
"Bash(cargo check:*)",
"Bash(pnpm build:*)",
"Bash(tree:*)"
]
}
}

View File

@@ -1,5 +0,0 @@
{
"mcpServers": {},
"DEBUG": true,
"DEBUG_MODE": "true"
}

View File

@@ -1,3 +0,0 @@
{
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
}

101
STATUS.md
View File

@@ -1,101 +0,0 @@
# 📊 项目状态
## 🎯 项目完成度
| 功能模块 | 状态 | 说明 |
|---------|------|------|
| **前端框架** | ✅ 完成 | React 19 + TypeScript |
| **UI 组件库** | ✅ 完成 | shadcn/ui + Tailwind CSS |
| **桌面应用** | ✅ 完成 | Tauri 2.0 集成 |
| **开发工具** | ✅ 完成 | ESLint + 热重载 |
| **构建系统** | ✅ 完成 | Vite + Rust |
| **文档** | ✅ 完成 | 完整的使用指南 |
| **CI/CD** | ✅ 完成 | GitHub Actions |
## 🚀 技术栈状态
### 前端技术
- **React 19** - 最新版本,支持现代特性
- **TypeScript 5.8+** - 完整类型支持
- **Tailwind CSS 4.0** - 最新版本,支持 CSS 变量
- **shadcn/ui** - 生产就绪的组件库
### 桌面应用
- **Tauri 2.0** - 跨平台桌面应用框架
- **Rust** - 高性能后端
- **多平台支持** - Windows, macOS, Linux, Android
### 开发工具
- **Vite** - 快速构建工具
- **ESLint** - 代码质量检查
- **pnpm** - 快速包管理器
## 📈 性能指标
| 指标 | 数值 | 说明 |
|------|------|------|
| **启动时间** | < 2s | 冷启动时间 |
| **构建时间** | < 30s | 完整构建时间 |
| **包大小** | < 50MB | 最终应用大小 |
| **内存占用** | < 100MB | 运行时内存使用 |
## 🔧 配置完整性
### TypeScript 配置
- ✅ 严格模式启用
- ✅ 路径别名配置
- ✅ 类型检查完整
### Tailwind CSS 配置
- ✅ 主题系统
- ✅ 响应式设计
- ✅ 深色模式支持
### Tauri 配置
- ✅ 多平台构建
- ✅ 权限管理
- ✅ 应用图标
## 📱 平台兼容性
| 平台 | 状态 | 说明 |
|------|------|------|
| **Windows** | ✅ 完全支持 | x64, x86, ARM64 |
| **macOS** | ✅ 完全支持 | Intel, Apple Silicon |
| **Linux** | ✅ 完全支持 | AppImage, Snap, Flatpak |
| **Android** | 🔄 实验性 | 基础功能支持 |
## 🎨 UI/UX 特性
- ✅ 响应式设计
- ✅ 深色/浅色主题
- ✅ 无障碍访问
- ✅ 现代设计语言
- ✅ 组件动画
- ✅ 交互反馈
## 🔒 安全性
- ✅ 类型安全
- ✅ 代码质量检查
- ✅ 依赖安全扫描
- ✅ 权限最小化
## 📚 文档完整性
- ✅ 快速开始指南
- ✅ API 文档
- ✅ 组件使用示例
- ✅ 部署指南
- ✅ 故障排除
## 🚀 部署就绪
- ✅ 生产构建配置
- ✅ 应用签名支持
- ✅ 自动更新机制
- ✅ 错误监控
---
**🎉 这是一个生产就绪的模板项目,可以直接用于构建企业级桌面应用!**

1182
docs/开发指南.md Normal file

File diff suppressed because it is too large Load Diff

207
docs/快速参考.md Normal file
View File

@@ -0,0 +1,207 @@
# 添加新功能快速参考
## 🚀 快速步骤
```
1. 定义 Models → src/models/new_feature.rs
2. 实现 Utils → src/utils/new_algorithm.rs (可选)
3. 定义 Platform → src/platforms/new_feature.rs
4. 实现平台代码 → src/platforms/windows/new_feature_impl.rs
5. 实现 Service → src/services/new_feature_service.rs
6. 创建 Command → src/commands/new_feature_commands.rs
7. 注册模块 → 更新 mod.rs 和 lib.rs
8. 测试验证 → cargo check && cargo test
```
## 📁 文件模板
### 1. Model 模板
```rust
// src/models/feature.rs
use serde::{Deserialize, Serialize};
/// 数据说明
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FeatureData {
/// 字段说明
pub field: String,
}
```
### 2. Service 模板
```rust
// src/services/feature_service.rs
//! 功能说明
use crate::error::AppResult;
use crate::models::feature::FeatureData;
/// 功能服务
pub struct FeatureService;
impl FeatureService {
/// 功能说明
pub fn execute(&self, input: &FeatureData) -> AppResult<Output> {
// 实现
}
}
```
### 3. Command 模板
```rust
// src/commands/feature_commands.rs
//! 命令说明
use crate::models::feature::FeatureData;
/// 命令说明
#[tauri::command]
pub fn execute_feature(input: FeatureData) -> Result<Output, String> {
FeatureService::execute(&input).map_err(|e| e.to_string())
}
```
### 4. Platform Trait 模板
```rust
// src/platforms/feature.rs
use crate::error::AppResult;
/// Trait 说明
pub trait FeatureAccessor {
fn do_something(&self, param: &str) -> AppResult<String>;
}
#[cfg(windows)]
pub type PlatformFeature = crate::platforms::windows::feature_impl::WindowsFeature;
#[cfg(not(windows))]
pub type PlatformFeature = crate::platforms::windows::feature_impl::DummyFeature;
```
## ✅ 代码规范清单
### 命名规范
- [ ] 模块文件: `snake_case`
- [ ] 结构体: `PascalCase`
- [ ] 函数: `snake_case`
- [ ] Trait: `PascalCase` + 能力描述
### 文档规范
- [ ] 所有公开 API 有 `///` 注释
- [ ] 所有模块有 `//!` 注释
- [ ] 包含参数说明
- [ ] 包含返回值说明
- [ ] 包含错误说明
- [ ] 包含使用示例
### 代码规范
- [ ] 使用 `AppResult<T>` 返回
- [ ] 使用中文错误消息
- [ ] 参数验证在 Service 层
- [ ] Command 层简洁(仅适配)
- [ ] 使用 `#[cfg(windows)]` 条件编译
## 🔧 常用命令
```bash
# 格式化代码
cargo fmt
# 检查代码
cargo check
# 代码检查
cargo clippy
# 运行测试
cargo test
# 构建应用
pnpm build
# 开发运行
pnpm tauri dev
# 生成文档
cargo doc --open
```
## 📋 检查清单
提交前检查:
- [ ] `cargo fmt` 通过
- [ ] `cargo clippy` 无警告
- [ ] `cargo test` 全部通过
- [ ] `cargo check` 编译成功
- [ ] 文档注释完整
- [ ] 错误消息中文化
- [ ] 命令已注册
## 🎯 示例对照
### 参考:颜色取色功能
```
models/color.rs → ColorInfo, RgbInfo, HslInfo
utils/color_conversion.rs → rgb_to_hsl()
platforms/screen.rs → ScreenAccessor trait
platforms/windows/screen_impl.rs → WindowsScreen
services/color_service.rs → ColorService
commands/color_commands.rs → pick_color_interactive
lib.rs → 注册命令
```
### 添加类似功能:截图
```
models/screenshot.rs → ScreenshotConfig, ScreenshotResult
utils/image_utils.rs → (可选) 图像处理工具
platforms/screenshot.rs → ScreenshotCapturer trait
platforms/windows/screenshot_impl.rs → WindowsScreenshot
services/screenshot_service.rs → ScreenshotService
commands/screenshot_commands.rs → capture_screen
lib.rs → 注册 capture_screen
```
## ⚠️ 常见错误
### 错误 1: Trait 方法未找到
```rust
// ❌ 错误
use crate::platforms::screen::PlatformScreen;
PlatformScreen::get_pixel_color(x, y)?; // 找不到方法
// ✅ 正确
use crate::platforms::screen::PlatformScreen;
use crate::platforms::screen::ScreenAccessor; // 导入 trait
PlatformScreen::get_pixel_color(x, y)?;
```
### 错误 2: 类型不匹配
```rust
// ❌ 错误
pub fn toggle_window(window: &WebviewWindow) { }
// ✅ 正确
pub fn toggle_window(window: &Window) { }
```
### 错误 3: 忘记注册命令
```rust
// ❌ 错误:命令未注册,前端无法调用
// ✅ 正确:在 lib.rs 中注册
.invoke_handler(tauri::generate_handler![
commands::new_commands::new_command, // 添加这一行
])
```
## 📚 相关文档
- [完整开发指南](./开发指南.md) - 详细的开发规范和教程
- [Rust 官方文档](https://doc.rust-lang.org/) - Rust 语言参考
- [Tauri 官方文档](https://tauri.app/) - Tauri 框架文档

View File

@@ -17,9 +17,11 @@
"@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-tabs": "^1.0.4",
"@tailwindcss/vite": "^4.1.12", "@tailwindcss/vite": "^4.1.12",
"@tauri-apps/api": "^2", "@tauri-apps/api": "^2",
"@tauri-apps/plugin-global-shortcut": "^2",
"@tauri-apps/plugin-opener": "^2", "@tauri-apps/plugin-opener": "^2",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"fuse.js": "^7.1.0",
"lucide-react": "^0.539.0", "lucide-react": "^0.539.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",

257
picker.html Normal file
View File

@@ -0,0 +1,257 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>取色遮罩</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 100vw;
height: 100vh;
background-color: transparent;
cursor: crosshair; /* 关键CSS 十字光标 */
user-select: none;
-webkit-user-select: none;
}
/* 添加一个极其透明的背景,确保能捕获鼠标事件 */
#overlay {
width: 100%;
height: 100%;
/* 保持完全透明,避免影响底层颜色的合成结果 */
background-color: transparent;
position: relative;
}
#magnifier {
position: fixed;
pointer-events: none;
display: block;
border: 2px solid rgba(255, 255, 255, 0.8);
border-radius: 14px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
background: rgba(20, 20, 20, 0.75);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
padding: 10px;
width: 260px;
height: 300px;
z-index: 9999;
}
#mag-canvas {
width: 240px;
height: 240px;
border-radius: 10px;
display: block;
background: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.12);
}
#mag-info {
margin-top: 10px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
color: rgba(255, 255, 255, 0.92);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace;
font-size: 13px;
}
#mag-swatch {
width: 28px;
height: 28px;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.18);
}
</style>
</head>
<body>
<div id="overlay"></div>
<div id="magnifier">
<canvas id="mag-canvas" width="240" height="240"></canvas>
<div id="mag-info">
<div id="mag-hex">#------</div>
<div id="mag-swatch"></div>
</div>
</div>
<!--
注意:此页面是 Vite 的独立入口,会被打包到 dist/picker.html。
因此可以直接使用 @tauri-apps/api 的 ESM 导入。
-->
<script type="module">
import { invoke } from "@tauri-apps/api/core";
import { emit } from "@tauri-apps/api/event";
const overlay = document.getElementById("overlay");
const magnifier = document.getElementById("magnifier");
const magCanvas = document.getElementById("mag-canvas");
const magHex = document.getElementById("mag-hex");
const magSwatch = document.getElementById("mag-swatch");
const magCtx = magCanvas.getContext("2d", { willReadFrequently: false });
const tmpCanvas = document.createElement("canvas");
const tmpCtx = tmpCanvas.getContext("2d");
// 初始隐藏到屏幕外,避免一开始挡住视线
magnifier.style.transform = "translate(-9999px, -9999px)";
const CAPTURE_SIZE = 21; // 必须是奇数,中心点用于取色
const SCALE = 10; // 放大倍率21*10=210canvas 240 留边)
const DRAW_SIZE = CAPTURE_SIZE * SCALE;
const OFFSET_X = 26;
const OFFSET_Y = 26;
let lastClientX = 0;
let lastClientY = 0;
let lastScreenX = 0;
let lastScreenY = 0;
let pending = false;
let lastCaptureAt = 0;
function clamp(v, min, max) {
return Math.max(min, Math.min(max, v));
}
function positionMagnifier(clientX, clientY) {
const w = magnifier.offsetWidth || 260;
const h = magnifier.offsetHeight || 300;
const maxX = window.innerWidth - w - 8;
const maxY = window.innerHeight - h - 8;
// 放到鼠标右下角,避免把被采样区域遮住(防止递归镜像)
const x = clamp(clientX + OFFSET_X, 8, Math.max(8, maxX));
const y = clamp(clientY + OFFSET_Y, 8, Math.max(8, maxY));
magnifier.style.transform = `translate(${x}px, ${y}px)`;
}
async function captureAndRender() {
pending = false;
if (!magCtx || !tmpCtx) return;
const now = performance.now();
// 节流:最多 30 FPS
if (now - lastCaptureAt < 33) return;
lastCaptureAt = now;
const dpr = window.devicePixelRatio || 1;
const px = Math.round(lastScreenX * dpr);
const py = Math.round(lastScreenY * dpr);
const half = Math.floor(CAPTURE_SIZE / 2);
try {
const res = await invoke("capture_screen_region_rgba", {
x: px - half,
y: py - half,
width: CAPTURE_SIZE,
height: CAPTURE_SIZE,
});
// res.data 是 RGBA Uint8 数组(通过 JSON 传输会变成 number[]
const u8 = new Uint8ClampedArray(res.data);
const imageData = new ImageData(u8, res.width, res.height);
// 先画到临时 canvas原尺寸再按像素风格放大避免每帧创建对象
tmpCanvas.width = res.width;
tmpCanvas.height = res.height;
tmpCtx.putImageData(imageData, 0, 0);
magCtx.clearRect(0, 0, magCanvas.width, magCanvas.height);
magCtx.imageSmoothingEnabled = false;
// 居中绘制
const dx = Math.floor((magCanvas.width - DRAW_SIZE) / 2);
const dy = Math.floor((magCanvas.height - DRAW_SIZE) / 2);
magCtx.drawImage(tmpCanvas, dx, dy, DRAW_SIZE, DRAW_SIZE);
// 网格线(轻微)
magCtx.strokeStyle = "rgba(255,255,255,0.08)";
magCtx.lineWidth = 1;
for (let i = 0; i <= CAPTURE_SIZE; i++) {
const gx = dx + i * SCALE + 0.5;
const gy = dy + i * SCALE + 0.5;
magCtx.beginPath();
magCtx.moveTo(gx, dy);
magCtx.lineTo(gx, dy + DRAW_SIZE);
magCtx.stroke();
magCtx.beginPath();
magCtx.moveTo(dx, gy);
magCtx.lineTo(dx + DRAW_SIZE, gy);
magCtx.stroke();
}
// 中心十字
const cx = dx + half * SCALE;
const cy = dy + half * SCALE;
magCtx.strokeStyle = "rgba(255,255,255,0.9)";
magCtx.lineWidth = 2;
magCtx.strokeRect(cx, cy, SCALE, SCALE);
// 更新颜色信息
magHex.textContent = res.center_hex;
magSwatch.style.backgroundColor = res.center_hex;
} catch (e) {
// 静默失败,避免刷屏
}
}
overlay.addEventListener("mousemove", (e) => {
lastClientX = e.clientX;
lastClientY = e.clientY;
lastScreenX = e.screenX;
lastScreenY = e.screenY;
positionMagnifier(lastClientX, lastClientY);
if (!pending) {
pending = true;
requestAnimationFrame(captureAndRender);
}
});
overlay.addEventListener("click", async (e) => {
try {
// screenX/screenY 通常是 CSS 像素;后端 GetPixel 需要物理像素
const dpr = window.devicePixelRatio || 1;
const x = Math.round(e.screenX * dpr);
const y = Math.round(e.screenY * dpr);
// 取色前先隐藏遮罩窗口,确保拿到“最上层应用”的真实颜色
const color = await invoke("pick_color_at_point_topmost", {
x,
y,
});
await emit("color-picked", color);
await invoke("close_picker_window");
} catch (error) {
console.error("取色失败:", error);
await invoke("close_picker_window");
}
});
document.addEventListener("keydown", async (e) => {
if (e.key === "Escape") {
try {
await invoke("close_picker_window");
await emit("color-picker-cancelled");
} catch (error) {
console.error("关闭窗口失败:", error);
}
}
});
overlay.addEventListener("contextmenu", (e) => {
e.preventDefault();
});
</script>
</body>
</html>

2064
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,12 +15,26 @@ name = "tauri_app_lib"
crate-type = ["staticlib", "cdylib", "rlib"] crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies] [build-dependencies]
tauri-build = { version = "2.4.0", features = [] } tauri-build = { version = "2.4", features = [] }
[dependencies] [dependencies]
tauri = { version = "2.4.0", features = [] } tauri = { version = "2.4", features = [] }
tauri-plugin-opener = "2.5.0" tauri-plugin-opener = "2.5"
tauri-plugin-global-shortcut = "2"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = { version = "1", features = ["preserve_order"] }
sysinfo = "0.30"
[target.'cfg(windows)'.dependencies]
windows = { version = "0.58", features = [
"Win32_Foundation",
"Win32_Graphics_Gdi",
"Win32_UI_HiDpi",
"Win32_UI_WindowsAndMessaging",
"Win32_UI_Input_KeyboardAndMouse",
"Win32_System_StationsAndDesktops",
"Win32_System_SystemInformation",
"Win32_Storage_FileSystem",
] }
wmi = "0.14"
serde_derive = "1.0"

View File

@@ -2,9 +2,14 @@
"$schema": "../gen/schemas/desktop-schema.json", "$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default", "identifier": "default",
"description": "Capability for the main window", "description": "Capability for the main window",
"windows": ["main"], "windows": ["main", "picker_overlay"],
"permissions": [ "permissions": [
"core:default", "core:default",
"core:window:allow-hide",
"core:window:allow-show",
"core:window:allow-close",
"core:window:allow-set-focus",
"core:window:allow-is-visible",
"opener:default" "opener:default"
] ]
} }

View File

@@ -0,0 +1,110 @@
//! JSON 格式化命令
//!
//! 定义 JSON 格式化相关的 Tauri 命令
use crate::models::json_format::{JsonFormatConfig, JsonFormatResult, JsonValidateResult};
use crate::services::json_format_service::JsonFormatService;
/// 格式化 JSON 命令
///
/// Tauri 命令,用于从前端调用 JSON 格式化功能
///
/// # 参数
///
/// * `input` - 输入的 JSON 字符串
/// * `config` - 格式化配置
///
/// # 返回
///
/// 返回格式化结果,包含成功标志、结果字符串和错误信息
///
/// # 前端调用示例
///
/// ```typescript
/// import { invoke } from '@tauri-apps/api/tauri';
///
/// const result = await invoke('format_json', {
/// input: '{"name":"test","value":123}',
/// config: {
/// indent: 2,
/// sort_keys: false,
/// mode: 'pretty'
/// }
/// });
/// console.log(result.success); // true
/// console.log(result.result); // 格式化后的 JSON
/// ```
#[tauri::command]
pub fn format_json(input: String, config: JsonFormatConfig) -> JsonFormatResult {
JsonFormatService::format(&input, &config)
.unwrap_or_else(|e| JsonFormatResult {
success: false,
result: String::new(),
error: Some(e.to_string()),
is_valid: false,
})
}
/// 验证 JSON 命令
///
/// 验证输入的字符串是否为有效的 JSON
///
/// # 参数
///
/// * `input` - 输入的 JSON 字符串
///
/// # 返回
///
/// 返回验证结果
///
/// # 前端调用示例
///
/// ```typescript
/// import { invoke } from '@tauri-apps/api/tauri';
///
/// const result = await invoke('validate_json', {
/// input: '{"valid": true}'
/// });
/// console.log(result.is_valid); // true
/// ```
#[tauri::command]
pub fn validate_json(input: String) -> JsonValidateResult {
JsonFormatService::validate(&input).unwrap_or_else(|e| JsonValidateResult {
is_valid: false,
error_message: Some(e.to_string()),
error_line: None,
error_column: None,
})
}
/// 压缩 JSON 命令
///
/// 去除 JSON 中的所有空格和换行
///
/// # 参数
///
/// * `input` - 输入的 JSON 字符串
///
/// # 返回
///
/// 返回压缩后的 JSON
///
/// # 前端调用示例
///
/// ```typescript
/// import { invoke } from '@tauri-apps/api/tauri';
///
/// const result = await invoke('compact_json', {
/// input: '{ "name" : "test" }'
/// });
/// console.log(result.result); // '{"name":"test"}'
/// ```
#[tauri::command]
pub fn compact_json(input: String) -> JsonFormatResult {
JsonFormatService::compact(&input).unwrap_or_else(|e| JsonFormatResult {
success: false,
result: String::new(),
error: Some(e.to_string()),
is_valid: false,
})
}

View File

@@ -0,0 +1,8 @@
//! Tauri 命令处理层
//!
//! 定义与前端交互的 Tauri 命令,作为前端和业务逻辑之间的适配器
pub mod json_format_commands;
pub mod picker_color_commands;
pub mod system_info_commands;
pub mod window_commands;

View File

@@ -0,0 +1,311 @@
//! 取色器命令
//!
//! 提供完整的屏幕取色功能(使用透明遮罩窗口方案)
//!
//! # 架构设计
//!
//! - **后端Rust**:负责窗口创建/销毁和屏幕取色
//! - **前端HTML/CSS/JS**:负责光标样式和用户交互
//!
//! # 优势
//!
//! 使用透明全屏遮罩 + CSS 光标,完美解决 Windows 系统光标竞争问题,
//! 避免了传统的 SetCursor API 与系统的 race condition。
use std::thread;
use std::time::Duration;
use serde::Serialize;
use tauri::{AppHandle, Manager};
#[derive(Serialize)]
pub struct ScreenRegionRgba {
pub width: i32,
pub height: i32,
/// RGBA 字节数组(长度 = width * height * 4
pub data: Vec<u8>,
/// 中心点颜色(从 data 直接计算),便于前端展示
pub center: crate::models::color::RgbInfo,
pub center_hex: String,
}
/// 预热取色器窗口(隐藏创建)
///
/// 目的:避免第一次显示 WebView 时的“白屏闪一下”WebView 首帧默认白底/初始化抖动)。
pub(crate) fn prewarm_picker_window(app: &AppHandle) -> Result<(), String> {
use tauri::WebviewWindowBuilder;
if app.get_webview_window("picker_overlay").is_some() {
return Ok(());
}
WebviewWindowBuilder::new(
app,
"picker_overlay",
tauri::WebviewUrl::App("picker.html".into()),
)
.title("取色器")
.fullscreen(true)
.transparent(true)
.always_on_top(true)
.decorations(false)
.skip_taskbar(true)
.resizable(false)
// 关键:先不可见创建,等真正开始取色时再 show
.visible(false)
// 尽可能早地把背景设为透明,降低首帧白底概率
.initialization_script(
r#"
try {
document.documentElement.style.background = 'transparent';
document.body && (document.body.style.background = 'transparent');
} catch (_) {}
"#,
)
.build()
.map_err(|e| format!("预热取色器窗口失败: {}", e))?;
Ok(())
}
/// 启动取色器(推荐使用 ⭐)
///
/// 打开透明全屏遮罩窗口,光标由前端 CSS 控制。
///
/// # 工作流程
///
/// 1. 后端隐藏主窗口
/// 2. 后端创建全屏透明遮罩窗口
/// 3. **前端通过 CSS 设置 `cursor: crosshair` 控制光标**
/// 4. 用户点击任意位置,前端调用 `pick_color_at_point` 取色
/// 5. 取色完成后前端调用 `close_picker_window` 关闭遮罩窗口
///
/// # 参数
///
/// * `app` - Tauri 应用句柄
///
/// # 前端实现示例
///
/// picker.html:
/// ```html
/// <style>
/// body {
/// width: 100vw;
/// height: 100vh;
/// background-color: transparent;
/// cursor: crosshair; /* 关键!前端控制光标 */
/// }
/// </style>
/// <script>
/// async function handleClick(e) {
/// const color = await invoke('pick_color_at_point', {
/// x: e.clientX,
/// y: e.clientY
/// });
/// console.log('HEX:', color.hex);
/// await invoke('close_picker_window');
/// }
/// document.addEventListener('click', handleClick);
/// </script>
/// ```
#[tauri::command]
pub async fn start_color_picker(app: AppHandle) -> Result<(), String> {
// 先隐藏主窗口
if let Some(main_window) = app.get_webview_window("main") {
main_window.hide().map_err(|e| e.to_string())?;
}
// 等待窗口完全隐藏
thread::sleep(Duration::from_millis(150));
// 打开透明遮罩窗口
open_picker_window(app).await?;
Ok(())
}
/// 打开取色器遮罩窗口(内部辅助函数)
///
/// # 参数
///
/// * `app` - Tauri 应用句柄
///
/// # 返回
///
/// 返回成功或错误信息
pub(crate) async fn open_picker_window(app: AppHandle) -> Result<(), String> {
use tauri::WebviewWindowBuilder;
// 检查窗口是否已存在
if let Some(existing) = app.get_webview_window("picker_overlay") {
// 复用已存在窗口:避免频繁 close/build 引起的白屏闪烁
existing
.show()
.and_then(|_| existing.set_focus())
.map_err(|e| format!("显示取色器窗口失败: {}", e))?;
return Ok(());
}
// 创建全屏透明遮罩窗口
let picker_window = WebviewWindowBuilder::new(
&app,
"picker_overlay",
tauri::WebviewUrl::App("picker.html".into()),
)
.title("取色器")
.fullscreen(true)
.transparent(true)
.always_on_top(true)
.decorations(false)
.skip_taskbar(true)
.resizable(false)
// 先不可见创建,再显式 show降低首帧白底闪烁
.visible(false)
.initialization_script(
r#"
try {
document.documentElement.style.background = 'transparent';
document.body && (document.body.style.background = 'transparent');
} catch (_) {}
"#,
)
.build()
.map_err(|e| format!("创建取色器窗口失败: {}", e))?;
// 显式 show + focus确保在某些系统上立即可见
picker_window
.show()
.and_then(|_| picker_window.set_focus())
.map_err(|e| format!("显示取色器窗口失败: {}", e))?;
Ok(())
}
/// 关闭取色器遮罩窗口
///
/// # 前端调用示例
///
/// ```typescript
/// import { invoke } from '@tauri-apps/api/tauri';
///
/// await invoke('close_picker_window');
/// ```
#[tauri::command]
pub async fn close_picker_window(app: AppHandle) -> Result<(), String> {
if let Some(window) = app.get_webview_window("picker_overlay") {
// 不 close只 hide避免窗口销毁/重建导致的白屏闪烁
// 先隐藏遮罩窗口,再恢复主窗口,过渡更自然
window
.hide()
.map_err(|e| format!("隐藏取色器窗口失败: {}", e))?;
// 恢复主窗口
if let Some(main_window) = app.get_webview_window("main") {
main_window
.show()
.and_then(|_| main_window.set_focus())
.map_err(|e| format!("显示主窗口失败: {}", e))?;
}
}
Ok(())
}
/// 在指定坐标取色
///
/// # 参数
///
/// * `x` - 屏幕 X 坐标
/// * `y` - 屏幕 Y 坐标
///
/// # 返回
///
/// 返回颜色信息
#[tauri::command]
pub async fn pick_color_at_point(
x: i32,
y: i32,
) -> Result<crate::models::color::ColorInfo, String> {
let (r, g, b) = crate::utils::screen::WindowsScreen::get_pixel_color(x, y)
.map_err(|e| e.to_string())?;
Ok(crate::models::color::ColorInfo::new(r, g, b, x, y))
}
/// 获取“最上层应用”的颜色(排除取色遮罩自身的影响)
///
/// 在 Windows 上,如果遮罩窗口位于最顶层,`GetPixel` 读到的是**合成后的颜色**
/// 即可能包含遮罩的透明叠加,从而导致取色偏暗/偏差。
///
/// 这里的策略是:先隐藏遮罩窗口,等待一帧左右让桌面合成刷新,再读取屏幕像素。
#[tauri::command]
pub async fn pick_color_at_point_topmost(
app: AppHandle,
x: i32,
y: i32,
) -> Result<crate::models::color::ColorInfo, String> {
// 先隐藏遮罩窗口(不 close避免白屏闪烁
if let Some(overlay) = app.get_webview_window("picker_overlay") {
let _ = overlay.hide();
}
// 给桌面合成一点时间刷新(过短可能还会读到遮罩叠加结果)
thread::sleep(Duration::from_millis(35));
let (r, g, b) = crate::utils::screen::WindowsScreen::get_pixel_color(x, y)
.map_err(|e| e.to_string())?;
Ok(crate::models::color::ColorInfo::new(r, g, b, x, y))
}
/// 捕获屏幕区域像素(用于前端放大镜)
#[tauri::command]
pub async fn capture_screen_region_rgba(
x: i32,
y: i32,
width: i32,
height: i32,
) -> Result<ScreenRegionRgba, String> {
let data = crate::utils::screen::WindowsScreen::capture_region_rgba(x, y, width, height)
.map_err(|e| e.to_string())?;
let cx = width / 2;
let cy = height / 2;
let idx = ((cy as usize) * (width as usize) + (cx as usize)) * 4;
let r = data[idx];
let g = data[idx + 1];
let b = data[idx + 2];
Ok(ScreenRegionRgba {
width,
height,
data,
center: crate::models::color::RgbInfo { r, g, b },
center_hex: format!("#{:02X}{:02X}{:02X}", r, g, b),
})
}
/// RGB 转 HSL 命令
///
/// 将 RGB 颜色值转换为 HSL 颜色值
///
/// # 参数
///
/// * `r` - 红色分量 (0-255)
/// * `g` - 绿色分量 (0-255)
/// * `b` - 蓝色分量 (0-255)
///
/// # 返回
///
/// 返回 HSL 颜色值
///
/// # 前端调用示例
///
/// ```typescript
/// import { invoke } from '@tauri-apps/api/tauri';
///
/// const hsl = await invoke('rgb_to_hsl', { r: 255, g: 0, b: 0 });
/// console.log(hsl); // { h: 0, s: 100, l: 50 }
/// ```
#[tauri::command]
pub fn rgb_to_hsl(r: u8, g: u8, b: u8) -> crate::models::color::HslInfo {
crate::utils::color_conversion::rgb_to_hsl(r, g, b)
}

View File

@@ -0,0 +1,29 @@
//! 系统信息命令
//!
//! 定义系统信息相关的 Tauri 命令
use crate::models::system_info::SystemInfo;
use crate::services::system_info_service::SystemInfoService;
/// 获取系统信息命令
///
/// Tauri 命令,用于从前端调用系统信息查询功能
///
/// # 返回
///
/// 返回包含所有系统信息的结构体
///
/// # 前端调用示例
///
/// ```typescript
/// import { invoke } from '@tauri-apps/api/tauri';
///
/// const info = await invoke('get_system_info');
/// console.log(info.os.name); // "Windows"
/// console.log(info.cpu.model); // "Intel Core i7..."
/// console.log(info.memory.total_gb); // 16.0
/// ```
#[tauri::command]
pub fn get_system_info() -> Result<SystemInfo, String> {
SystemInfoService::get_system_info().map_err(|e| e.to_string())
}

View File

@@ -0,0 +1,70 @@
//! 窗口命令
//!
//! 定义窗口管理相关的 Tauri 命令
use tauri::Window;
use crate::services::window_service::WindowService;
/// 切换窗口显示/隐藏命令
///
/// 根据窗口当前状态切换显示或隐藏
///
/// # 参数
///
/// * `window` - Tauri 窗口对象,自动由框架注入
///
/// # 前端调用示例
///
/// ```typescript
/// import { invoke } from '@tauri-apps/api/tauri';
///
/// await invoke('toggle_window');
/// ```
#[tauri::command]
pub fn toggle_window(window: Window) -> Result<(), String> {
WindowService::toggle_window(&window)
.map_err(|e| e.to_string())
}
/// 隐藏窗口命令
///
/// 将窗口隐藏,使其不再可见
///
/// # 参数
///
/// * `window` - Tauri 窗口对象,自动由框架注入
///
/// # 前端调用示例
///
/// ```typescript
/// import { invoke } from '@tauri-apps/api/tauri';
///
/// await invoke('hide_window');
/// ```
#[tauri::command]
pub fn hide_window(window: Window) -> Result<(), String> {
WindowService::hide_window(&window)
.map_err(|e| e.to_string())
}
/// 显示窗口命令
///
/// 显示窗口并将其设置为焦点窗口
///
/// # 参数
///
/// * `window` - Tauri 窗口对象,自动由框架注入
///
/// # 前端调用示例
///
/// ```typescript
/// import { invoke } from '@tauri-apps/api/tauri';
///
/// await invoke('show_window');
/// ```
#[tauri::command]
pub fn show_window(window: Window) -> Result<(), String> {
WindowService::show_window(&window)
.map_err(|e| e.to_string())
}

76
src-tauri/src/error.rs Normal file
View File

@@ -0,0 +1,76 @@
//! 错误处理模块
//!
//! 提供统一的错误类型定义和错误处理机制
use std::fmt;
/// 应用统一错误类型
///
/// 定义了应用中可能出现的所有错误类型,每个错误都携带详细的错误信息
#[derive(Debug)]
pub enum AppError {
/// 平台不支持
///
/// 表示当前平台不支持某项功能
PlatformNotSupported(String),
/// 屏幕访问失败
///
/// 表示无法获取或访问屏幕设备
ScreenAccessFailed(String),
/// 窗口操作失败
///
/// 表示窗口显示、隐藏或聚焦等操作失败
WindowOperationFailed(String),
/// 光标操作失败
///
/// 表示光标设置或恢复操作失败
CursorOperationFailed(String),
/// 无效的颜色数据
///
/// 表示提供的颜色数据格式不正确或超出范围
InvalidColorData(String),
/// 颜色转换失败
///
/// 表示颜色空间转换(如 RGB 到 HSL失败
ColorConversionFailed(String),
/// 系统信息获取失败
///
/// 表示获取系统信息时失败
SystemInfoFailed(String),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::PlatformNotSupported(msg) => write!(f, "平台不支持: {}", msg),
AppError::ScreenAccessFailed(msg) => write!(f, "屏幕访问失败: {}", msg),
AppError::WindowOperationFailed(msg) => write!(f, "窗口操作失败: {}", msg),
AppError::CursorOperationFailed(msg) => write!(f, "光标操作失败: {}", msg),
AppError::InvalidColorData(msg) => write!(f, "颜色数据无效: {}", msg),
AppError::ColorConversionFailed(msg) => write!(f, "颜色转换失败: {}", msg),
AppError::SystemInfoFailed(msg) => write!(f, "系统信息获取失败: {}", msg),
}
}
}
impl std::error::Error for AppError {}
/// 应用统一返回类型
///
/// 用于所有可能返回错误的函数,简化错误处理代码
pub type AppResult<T> = Result<T, AppError>;
/// 为 Tauri 实现自动转换
///
/// 允许 `AppError` 自动转换为 `String`,以满足 Tauri 命令的要求
impl From<AppError> for String {
fn from(error: AppError) -> String {
error.to_string()
}
}

View File

@@ -1,14 +1,54 @@
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ //! Tauri 应用入口
#[tauri::command] //!
fn greet(name: &str) -> String { //! 提供应用初始化和模块组装功能
format!("Hello, {}! You've been greeted from Rust!", name)
}
// 模块声明
mod commands;
mod error;
mod models;
mod platforms;
mod services;
mod utils;
// 重新导出常用类型
pub use error::{AppError, AppResult};
/// 运行 Tauri 应用
///
/// 初始化应用、注册插件、设置全局快捷键并启动应用
///
#[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_opener::init()) .plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![greet]) .plugin(tauri_plugin_global_shortcut::Builder::new().build())
.setup(|app| {
// 预热取色器窗口:避免第一次取色出现“白屏闪一下”
// 窗口会以 hidden 状态创建,不会影响用户体验
let _ = commands::picker_color_commands::prewarm_picker_window(app.handle());
utils::shortcut::register_global_shortcuts(app)?;
Ok(())
})
.invoke_handler(tauri::generate_handler![
// window 窗口操作
commands::window_commands::toggle_window,
commands::window_commands::hide_window,
commands::window_commands::show_window,
// 取色器命令
commands::picker_color_commands::rgb_to_hsl,
commands::picker_color_commands::start_color_picker,
commands::picker_color_commands::close_picker_window,
commands::picker_color_commands::pick_color_at_point,
commands::picker_color_commands::pick_color_at_point_topmost,
commands::picker_color_commands::capture_screen_region_rgba,
// Json格式化命令
commands::json_format_commands::format_json,
commands::json_format_commands::validate_json,
commands::json_format_commands::compact_json,
// 操作系统信息命令
commands::system_info_commands::get_system_info,
])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("运行 Tauri 应用时出错");
} }

View File

@@ -0,0 +1,91 @@
//! 颜色数据模型
//!
//! 定义颜色相关的数据结构,包括 RGB、HSL 和完整的颜色信息
use serde::{Deserialize, Serialize};
use crate::utils::color_conversion;
/// 颜色信息
///
/// 包含颜色的完整信息,支持多种颜色格式和屏幕坐标
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ColorInfo {
/// 十六进制颜色值(格式:#RRGGBB
pub hex: String,
/// RGB 颜色值
pub rgb: RgbInfo,
/// HSL 颜色值
pub hsl: HslInfo,
/// 屏幕坐标 X像素
pub x: i32,
/// 屏幕坐标 Y像素
pub y: i32,
}
impl ColorInfo {
/// 从 RGB 值创建颜色信息
///
/// # 参数
///
/// * `r` - 红色分量 (0-255)
/// * `g` - 绿色分量 (0-255)
/// * `b` - 蓝色分量 (0-255)
/// * `x` - 屏幕坐标 X像素
/// * `y` - 屏幕坐标 Y像素
///
/// # 返回
///
/// 返回包含完整颜色信息的 `ColorInfo` 实例
///
/// # 示例
///
/// ```no_run
/// use crate::models::color::ColorInfo;
///
/// let color = ColorInfo::new(255, 0, 0, 100, 200);
/// assert_eq!(color.hex, "#FF0000");
/// ```
pub fn new(r: u8, g: u8, b: u8, x: i32, y: i32) -> Self {
let hex = format!("#{:02X}{:02X}{:02X}", r, g, b);
Self {
hex,
rgb: RgbInfo { r, g, b },
hsl: color_conversion::rgb_to_hsl(r, g, b),
x,
y,
}
}
}
/// RGB 颜色
///
/// 表示 RGB 颜色模式的颜色值
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RgbInfo {
/// 红色分量 (0-255)
pub r: u8,
/// 绿色分量 (0-255)
pub g: u8,
/// 蓝色分量 (0-255)
pub b: u8,
}
/// HSL 颜色
///
/// 表示 HSL 颜色模式的颜色值
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HslInfo {
/// 色相 (0-360)
///
/// 表示颜色在色轮上的角度0° 为红色120° 为绿色240° 为蓝色
pub h: u16,
/// 饱和度 (0-100)
///
/// 表示颜色的鲜艳程度0% 为灰色100% 为完全饱和
pub s: u8,
/// 亮度 (0-100)
///
/// 表示颜色的明暗程度0% 为黑色100% 为白色
pub l: u8,
}

View File

@@ -0,0 +1,85 @@
//! JSON 格式化相关数据模型
//!
//! 定义 JSON 格式化工具使用的数据结构
use serde::{Deserialize, Serialize};
/// JSON 格式化配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonFormatConfig {
/// 缩进空格数(默认 2
#[serde(default = "default_indent")]
pub indent: u32,
/// 是否对 key 进行排序
#[serde(default)]
pub sort_keys: bool,
/// 格式化模式
#[serde(default)]
pub mode: FormatMode,
}
/// 默认缩进空格数
fn default_indent() -> u32 {
2
}
/// JSON 格式化模式
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum FormatMode {
/// 标准格式化(美化)
#[serde(rename = "pretty")]
Pretty,
/// 压缩格式(去除空格和换行)
#[serde(rename = "compact")]
Compact,
}
impl Default for FormatMode {
fn default() -> Self {
Self::Pretty
}
}
impl Default for JsonFormatConfig {
fn default() -> Self {
Self {
indent: default_indent(),
sort_keys: false,
mode: FormatMode::default(),
}
}
}
/// JSON 格式化结果
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonFormatResult {
/// 是否成功
pub success: bool,
/// 格式化后的 JSON 字符串
pub result: String,
/// 错误信息(如果失败)
pub error: Option<String>,
/// 原始 JSON 是否有效
pub is_valid: bool,
}
/// JSON 验证结果
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonValidateResult {
/// 是否有效的 JSON
pub is_valid: bool,
/// 错误信息(如果无效)
pub error_message: Option<String>,
/// 错误位置(行号,从 1 开始)
pub error_line: Option<usize>,
/// 错误位置(列号,从 1 开始)
pub error_column: Option<usize>,
}

View File

@@ -0,0 +1,7 @@
//! 数据模型模块
//!
//! 定义应用中使用的数据结构
pub mod color;
pub mod json_format;
pub mod system_info;

View File

@@ -0,0 +1,170 @@
//! 系统信息相关数据模型
//!
//! 定义系统信息工具使用的数据结构
use serde::{Deserialize, Serialize};
/// 系统信息(完整版)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemInfo {
/// 操作系统信息
pub os: OsInfo,
/// 硬件信息主板、BIOS
pub hardware: HardwareInfo,
/// CPU 信息
pub cpu: CpuInfo,
/// 内存信息
pub memory: MemoryInfo,
/// GPU 信息列表
pub gpu: Vec<GpuInfo>,
/// 磁盘信息列表
pub disks: Vec<DiskInfo>,
/// 计算机信息
pub computer: ComputerInfo,
/// 显示器信息
pub display: DisplayInfo,
/// 网络信息
pub network: NetworkInfo,
}
/// 操作系统信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OsInfo {
/// 操作系统名称
pub name: String,
/// 操作系统版本
pub version: String,
/// 系统架构
pub arch: String,
/// 内核版本
pub kernel_version: String,
/// 主机名
pub host_name: String,
/// 运行时间(可读格式)
pub uptime_readable: String,
}
/// 硬件信息主板、BIOS
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HardwareInfo {
/// 制造商
pub manufacturer: String,
/// 型号
pub model: String,
/// BIOS 版本
pub bios_version: String,
/// BIOS 序列号
pub bios_serial: String,
}
/// CPU 信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CpuInfo {
/// CPU 型号
pub model: String,
/// 物理核心数
pub cores: usize,
/// 逻辑处理器数
pub processors: usize,
/// 最大频率 (MHz)
pub max_frequency: u32,
/// 当前使用率 (0-100)
pub usage_percent: f32,
}
/// 内存信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryInfo {
/// 总内存 (GB)
pub total_gb: f64,
/// 可用内存 (GB)
pub available_gb: f64,
/// 已用内存 (GB)
pub used_gb: f64,
/// 使用率 (0-100)
pub usage_percent: f64,
}
/// GPU 信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GpuInfo {
/// GPU 名称
pub name: String,
/// 显存 (GB)
pub vram_gb: f64,
/// 驱动版本
pub driver_version: String,
}
/// 磁盘信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiskInfo {
/// 盘符 (如 "C:")
pub drive_letter: String,
/// 卷标
pub volume_label: String,
/// 文件系统类型
pub file_system: String,
/// 总容量 (GB)
pub total_gb: f64,
/// 可用空间 (GB)
pub available_gb: f64,
/// 已用空间 (GB)
pub used_gb: f64,
/// 使用率 (0-100)
pub usage_percent: f64,
}
/// 计算机信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComputerInfo {
/// 计算机名称
pub name: String,
/// 用户名
pub username: String,
/// 域名/工作组
pub domain: String,
/// 制造商
pub manufacturer: String,
/// 型号
pub model: String,
/// 序列号
pub serial_number: String,
}
/// 显示器信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DisplayInfo {
/// 屏幕数量
pub monitor_count: u32,
/// 主显示器分辨率
pub primary_resolution: String,
/// 所有显示器分辨率列表
pub all_resolutions: Vec<String>,
}
/// 网络信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkInfo {
/// 网络接口列表
pub interfaces: Vec<InterfaceInfo>,
/// 总下载 (MB)
pub total_downloaded_mb: f64,
/// 总上传 (MB)
pub total_uploaded_mb: f64,
}
/// 网络接口信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InterfaceInfo {
/// 接口名称
pub name: String,
/// MAC 地址
pub mac_address: String,
/// IP 地址列表
pub ip_networks: Vec<String>,
/// 上传速度 (KB/s)
pub upload_speed_kb: f64,
/// 下载速度 (KB/s)
pub download_speed_kb: f64,
}

View File

@@ -0,0 +1,9 @@
//! 平台相关模块
//!
//! 定义不同平台的特定实现
pub mod system_info;
// Windows 平台实现
#[cfg(windows)]
pub mod windows;

View File

@@ -0,0 +1,22 @@
//! 系统信息平台抽象
//!
//! 定义获取系统信息的平台相关接口
use crate::error::AppResult;
use crate::models::system_info::SystemInfo;
/// 系统信息获取 trait
///
/// 定义获取系统信息的接口,不同平台需要实现此 trait
pub trait SystemInfoAccessor {
/// 获取完整的系统信息
///
/// # 返回
///
/// 返回包含所有系统信息的结构体
///
/// # 错误
///
/// 平台不支持或获取信息失败时返回错误
fn get_system_info(&self) -> AppResult<SystemInfo>;
}

View File

@@ -0,0 +1,3 @@
//! Windows 平台特定实现
pub mod system_info_impl;

View File

@@ -0,0 +1,509 @@
//! Windows 平台系统信息实现
//!
//! 使用 WMI 和 sysinfo 获取系统信息
use crate::error::{AppError, AppResult};
use crate::models::system_info::{
ComputerInfo, CpuInfo, DisplayInfo, DiskInfo, GpuInfo, HardwareInfo, InterfaceInfo,
MemoryInfo, NetworkInfo, OsInfo, SystemInfo,
};
use crate::platforms::system_info::SystemInfoAccessor;
use serde::Deserialize;
use std::time::Duration;
use sysinfo::System;
/// Windows 平台系统信息实现
#[cfg(windows)]
pub struct WindowsSystemInfo;
#[cfg(windows)]
impl SystemInfoAccessor for WindowsSystemInfo {
fn get_system_info(&self) -> AppResult<SystemInfo> {
// 使用 sysinfo 获取基础信息
let mut sys = System::new_all();
// 等待一小会儿以收集CPU使用率和网络速率
std::thread::sleep(Duration::from_millis(200));
sys.refresh_all();
sys.refresh_cpu();
// 使用 WMI 获取详细硬件信息
let wmi_result = Self::get_wmi_info();
// 解构 WMI 结果,提供默认值
let (hw_info, gpu_infos, disk_labels, net_ips) = match wmi_result {
Ok((hw, gpus, labels, ips)) => (hw, gpus, labels, ips),
Err(_) => (
HardwareInfo {
manufacturer: "Unknown".to_string(),
model: "Unknown".to_string(),
bios_version: "Unknown".to_string(),
bios_serial: "Unknown".to_string(),
},
vec![],
std::collections::HashMap::new(),
std::collections::HashMap::new(),
)
};
Ok(SystemInfo {
os: Self::get_os_info(&sys)?,
hardware: hw_info,
cpu: Self::get_cpu_info(&sys)?,
memory: Self::get_memory_info(&sys)?,
gpu: gpu_infos,
disks: Self::get_disk_info(&sys, &disk_labels)?,
computer: Self::get_computer_info()?,
display: Self::get_display_info()?,
network: Self::get_network_info(&sys, &net_ips)?,
})
}
}
/// WMI 结构体映射
/// 这些结构体名称必须匹配 Windows WMI 类名(包含下划线)
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
#[allow(non_camel_case_types)]
struct Win32_VideoController {
name: String,
driver_version: Option<String>,
adapter_ram: Option<u64>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
#[allow(non_camel_case_types)]
struct Win32_ComputerSystem {
manufacturer: Option<String>,
model: Option<String>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
#[allow(non_camel_case_types)]
struct Win32_BaseBoard {
manufacturer: Option<String>,
product: Option<String>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
#[allow(non_camel_case_types)]
struct Win32_Bios {
#[allow(dead_code)]
manufacturer: Option<String>,
version: Option<String>,
serial_number: Option<String>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
#[allow(non_camel_case_types)]
struct Win32_LogicalDisk {
device_id: String,
volume_name: Option<String>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
#[allow(non_camel_case_types)]
struct Win32_NetworkAdapterConfiguration {
mac_address: Option<String>,
ip_address: Option<Vec<String>>,
ip_enabled: Option<bool>,
}
/// WMI 信息返回类型
type WmiInfoResult = Result<(
HardwareInfo,
Vec<GpuInfo>,
std::collections::HashMap<String, String>,
std::collections::HashMap<String, Vec<String>>,
), String>;
#[cfg(windows)]
impl WindowsSystemInfo {
/// 获取 WMI 硬件信息(容错版本,某个查询失败不影响其他查询)
fn get_wmi_info() -> WmiInfoResult {
use wmi::{COMLibrary, WMIConnection};
use std::collections::HashMap;
let com = COMLibrary::new()
.map_err(|e| format!("初始化 COM 失败: {:?}", e))?;
let wmi_con = WMIConnection::new(com)
.map_err(|e| format!("连接 WMI 失败: {:?}", e))?;
// 1. 获取硬件信息(结合 ComputerSystem 和 BaseBoard
let hw_info = {
let sys_query: Result<Vec<Win32_ComputerSystem>, _> = wmi_con.query();
let board_query: Result<Vec<Win32_BaseBoard>, _> = wmi_con.query();
let bios_query: Result<Vec<Win32_Bios>, _> = wmi_con.query();
let sys_result = sys_query.ok();
let board_result = board_query.ok();
let bios_result = bios_query.ok();
let sys = sys_result.as_ref().and_then(|v| v.first());
let board = board_result.as_ref().and_then(|v| v.first());
let bios = bios_result.as_ref().and_then(|v| v.first());
// 优先使用主板信息,回退到系统信息
let manufacturer = board
.and_then(|b| b.manufacturer.clone())
.or_else(|| sys.and_then(|s| s.manufacturer.clone()))
.unwrap_or_else(|| "Unknown".to_string());
let model = board
.and_then(|b| b.product.clone())
.or_else(|| sys.and_then(|s| s.model.clone()))
.unwrap_or_else(|| "Unknown".to_string());
HardwareInfo {
manufacturer,
model,
bios_version: bios
.and_then(|b| b.version.clone())
.unwrap_or_else(|| "Unknown".to_string()),
bios_serial: bios
.and_then(|b| b.serial_number.clone())
.unwrap_or_else(|| "Unknown".to_string()),
}
};
// 2. 获取显卡信息(容错)
let gpu_infos = {
let gpu_query: Result<Vec<Win32_VideoController>, _> = wmi_con.query();
gpu_query
.unwrap_or_default()
.into_iter()
.map(|g| GpuInfo {
name: g.name,
vram_gb: g.adapter_ram.unwrap_or(0) as f64 / (1024.0 * 1024.0 * 1024.0),
driver_version: g.driver_version.unwrap_or_else(|| "Unknown".to_string()),
})
.collect()
};
// 3. 获取磁盘卷标
let mut disk_labels = HashMap::new();
if let Ok(disk_query_result) = wmi_con.query::<Win32_LogicalDisk>() {
for disk in disk_query_result {
if let Some(vol) = disk.volume_name {
if !vol.is_empty() {
disk_labels.insert(disk.device_id, vol);
}
}
}
}
// 4. 获取网络 IP修复 MAC 大小写匹配)
let mut net_ips = HashMap::new();
if let Ok(net_query_result) = wmi_con.query::<Win32_NetworkAdapterConfiguration>() {
for net in net_query_result {
if let (Some(true), Some(mac), Some(ips)) = (net.ip_enabled, net.mac_address, net.ip_address) {
// WMI 返回大写 MACsysinfo 返回小写,统一转为小写
net_ips.insert(mac.to_lowercase(), ips);
}
}
}
Ok((hw_info, gpu_infos, disk_labels, net_ips))
}
/// 获取操作系统信息
fn get_os_info(_sys: &System) -> AppResult<OsInfo> {
use windows::Win32::System::SystemInformation::GetNativeSystemInfo;
let mut sys_info = unsafe { std::mem::zeroed() };
unsafe {
GetNativeSystemInfo(&mut sys_info);
}
let arch = unsafe {
match sys_info.Anonymous.Anonymous.wProcessorArchitecture.0 {
0 => "x86 (32-bit)".to_string(),
9 => "x64 (64-bit)".to_string(),
5 => "ARM".to_string(),
12 => "ARM64".to_string(),
_ => "Unknown".to_string(),
}
};
Ok(OsInfo {
name: "Windows".to_string(),
version: System::os_version().unwrap_or_else(|| "Unknown".to_string()),
arch,
kernel_version: System::kernel_version().unwrap_or_else(|| "Unknown".to_string()),
host_name: System::host_name().unwrap_or_else(|| "Unknown".to_string()),
uptime_readable: Self::format_uptime(System::uptime()),
})
}
/// 获取 CPU 信息
fn get_cpu_info(sys: &System) -> AppResult<CpuInfo> {
let cpus = sys.cpus();
let cpu = cpus.first().ok_or_else(|| {
AppError::SystemInfoFailed("无法获取 CPU 信息".to_string())
})?;
let physical_cores = sys.physical_core_count().unwrap_or(1);
let usage = sys.global_cpu_info().cpu_usage();
Ok(CpuInfo {
model: cpu.brand().to_string(),
cores: physical_cores,
processors: cpus.len(),
max_frequency: cpu.frequency() as u32,
usage_percent: usage,
})
}
/// 获取内存信息
fn get_memory_info(sys: &System) -> AppResult<MemoryInfo> {
let total = sys.total_memory() as f64;
let available = sys.available_memory() as f64;
let used = total - available;
Ok(MemoryInfo {
total_gb: Self::bytes_to_gb(total),
available_gb: Self::bytes_to_gb(available),
used_gb: Self::bytes_to_gb(used),
usage_percent: if total > 0.0 { (used / total) * 100.0 } else { 0.0 },
})
}
/// 获取磁盘信息
fn get_disk_info(_sys: &System, disk_labels: &std::collections::HashMap<String, String>) -> AppResult<Vec<DiskInfo>> {
let mut disk_infos = Vec::new();
// 在 sysinfo 0.30 中使用新的 API
use sysinfo::Disks;
let disks = Disks::new_with_refreshed_list();
for disk in disks.list() {
let total = disk.total_space() as f64;
let available = disk.available_space() as f64;
let used = total - available;
// 获取盘符,处理 "C:\" -> "C:" 的情况
let name = disk.name().to_string_lossy();
let drive_letter = name.trim_end_matches('\\').to_string();
// 从 WMI 查询结果中获取真实卷标
let volume_label = disk_labels
.get(&drive_letter)
.cloned()
.unwrap_or_else(|| "Local Disk".to_string());
disk_infos.push(DiskInfo {
drive_letter,
volume_label,
file_system: disk.file_system().to_string_lossy().to_string(),
total_gb: Self::bytes_to_gb(total),
available_gb: Self::bytes_to_gb(available),
used_gb: Self::bytes_to_gb(used),
usage_percent: if total > 0.0 { (used / total) * 100.0 } else { 0.0 },
});
}
Ok(disk_infos)
}
/// 获取计算机信息
fn get_computer_info() -> AppResult<ComputerInfo> {
use windows::Win32::System::SystemInformation::{
ComputerNamePhysicalDnsHostname, GetComputerNameExW,
};
use windows::core::PWSTR;
let mut computer_name = [0u16; 256];
let mut size = computer_name.len() as u32;
unsafe {
let _ = GetComputerNameExW(
ComputerNamePhysicalDnsHostname,
PWSTR(computer_name.as_mut_ptr()),
&mut size,
);
}
let name = String::from_utf16_lossy(&computer_name[..size as usize]);
Ok(ComputerInfo {
name: name.clone(),
username: std::env::var("USERNAME").unwrap_or_else(|_| "Unknown".to_string()),
domain: "WORKGROUP".to_string(),
manufacturer: name.clone(),
model: "PC".to_string(),
serial_number: "Unknown".to_string(),
})
}
/// 获取显示器信息
fn get_display_info() -> AppResult<DisplayInfo> {
use windows::Win32::UI::WindowsAndMessaging::{GetSystemMetrics, SM_CXSCREEN, SM_CYSCREEN};
let width = unsafe { GetSystemMetrics(SM_CXSCREEN) };
let height = unsafe { GetSystemMetrics(SM_CYSCREEN) };
let resolution = format!("{}x{}", width, height);
Ok(DisplayInfo {
monitor_count: 1,
primary_resolution: resolution.clone(),
all_resolutions: vec![resolution],
})
}
/// 获取网络信息
fn get_network_info(_sys: &System, net_ips: &std::collections::HashMap<String, Vec<String>>) -> AppResult<NetworkInfo> {
use sysinfo::Networks;
let networks = Networks::new_with_refreshed_list();
let mut interfaces = Vec::new();
let mut total_down = 0.0;
let mut total_up = 0.0;
for (name, data) in networks.list() {
total_down += data.total_received() as f64;
total_up += data.total_transmitted() as f64;
// 过滤掉回环接口
if name == "LO" {
continue;
}
// 修复 MAC 地址匹配:统一转为小写
let mac = data.mac_address().to_string().to_lowercase();
let ip_list = net_ips.get(&mac).cloned().unwrap_or_default();
// 只显示有 IP 或有流量的接口
if !ip_list.is_empty() || data.total_received() > 0 {
interfaces.push(InterfaceInfo {
name: name.clone(),
mac_address: mac,
ip_networks: ip_list,
upload_speed_kb: data.transmitted() as f64 / 1024.0,
download_speed_kb: data.received() as f64 / 1024.0,
});
}
}
Ok(NetworkInfo {
interfaces,
total_downloaded_mb: total_down / 1024.0 / 1024.0,
total_uploaded_mb: total_up / 1024.0 / 1024.0,
})
}
/// 字节转换为 GB
fn bytes_to_gb(bytes: f64) -> f64 {
bytes / 1024.0 / 1024.0 / 1024.0
}
/// 格式化运行时间为人类可读格式
fn format_uptime(seconds: u64) -> String {
let days = seconds / 86400;
let hours = (seconds % 86400) / 3600;
let minutes = (seconds % 3600) / 60;
if days > 0 {
format!("{}{}小时 {}分钟", days, hours, minutes)
} else if hours > 0 {
format!("{}小时 {}分钟", hours, minutes)
} else {
format!("{}分钟", minutes)
}
}
}
/// 其他平台占位实现
#[cfg(not(windows))]
pub struct DummySystemInfo;
#[cfg(not(windows))]
impl SystemInfoAccessor for DummySystemInfo {
fn get_system_info(&self) -> AppResult<SystemInfo> {
use sysinfo::System;
let mut sys = System::new_all();
sys.refresh_all();
Ok(SystemInfo {
os: OsInfo {
name: System::name().unwrap_or_else(|| "Unknown".to_string()),
version: System::os_version().unwrap_or_default(),
arch: std::env::consts::ARCH.to_string(),
kernel_version: System::kernel_version().unwrap_or_default(),
host_name: System::host_name().unwrap_or_default(),
uptime_readable: format!("{} seconds", System::uptime()),
},
hardware: HardwareInfo {
manufacturer: "Unknown".to_string(),
model: "Unknown".to_string(),
bios_version: "Unknown".to_string(),
bios_serial: "Unknown".to_string(),
},
cpu: {
let cpus = sys.cpus();
let cpu = cpus.first().unwrap();
CpuInfo {
model: cpu.brand().to_string(),
cores: sys.physical_core_count().unwrap_or(1),
processors: cpus.len(),
max_frequency: cpu.frequency(),
usage_percent: sys.global_cpu_info().cpu_usage(),
}
},
memory: {
let total = sys.total_memory() as f64;
let available = sys.available_memory() as f64;
let used = total - available;
MemoryInfo {
total_gb: total / 1024.0 / 1024.0 / 1024.0,
available_gb: available / 1024.0 / 1024.0 / 1024.0,
used_gb: used / 1024.0 / 1024.0 / 1024.0,
usage_percent: if total > 0.0 { (used / total) * 100.0 } else { 0.0 },
}
},
gpu: vec![],
disks: sys
.disks()
.iter()
.map(|disk| {
let total = disk.total_space() as f64;
let available = disk.available_space() as f64;
let used = total - available;
DiskInfo {
drive_letter: disk.name().to_string_lossy().into_owned(),
volume_label: "Local Disk".to_string(),
file_system: disk.file_system().to_string_lossy().into_owned(),
total_gb: total / 1024.0 / 1024.0 / 1024.0,
available_gb: available / 1024.0 / 1024.0 / 1024.0,
used_gb: used / 1024.0 / 1024.0 / 1024.0,
usage_percent: if total > 0.0 { (used / total) * 100.0 } else { 0.0 },
}
})
.collect(),
computer: ComputerInfo {
name: System::host_name().unwrap_or_default(),
username: std::env::var("USERNAME").unwrap_or_default(),
domain: "WORKGROUP".to_string(),
manufacturer: "Unknown".to_string(),
model: "Unknown".to_string(),
serial_number: "Unknown".to_string(),
},
display: DisplayInfo {
monitor_count: 1,
primary_resolution: "Unknown".to_string(),
all_resolutions: vec![],
},
network: NetworkInfo {
interfaces: vec![],
total_downloaded_mb: 0.0,
total_uploaded_mb: 0.0,
},
})
}
}

View File

@@ -0,0 +1,200 @@
//! JSON 格式化服务
//!
//! 提供 JSON 格式化功能的核心业务逻辑
use crate::error::AppResult;
use crate::models::json_format::{JsonFormatConfig, JsonFormatResult, JsonValidateResult};
use crate::utils::json_formatter;
/// JSON 格式化服务
pub struct JsonFormatService;
impl JsonFormatService {
/// 格式化 JSON 字符串
///
/// 根据配置对输入的 JSON 字符串进行格式化
///
/// # 参数
///
/// * `input` - 输入的 JSON 字符串
/// * `config` - 格式化配置
///
/// # 返回
///
/// 返回格式化结果
///
/// # 错误
///
/// - 输入为空时返回 `AppError::InvalidData`
pub fn format(input: &str, config: &JsonFormatConfig) -> AppResult<JsonFormatResult> {
// 参数验证
if input.trim().is_empty() {
return Ok(JsonFormatResult {
success: false,
result: String::new(),
error: Some("输入内容不能为空".to_string()),
is_valid: false,
});
}
// 调用工具函数进行格式化
match json_formatter::format_json(input, config) {
Ok(formatted) => Ok(JsonFormatResult {
success: true,
result: formatted,
error: None,
is_valid: true,
}),
Err(err) => Ok(JsonFormatResult {
success: false,
result: String::new(),
error: Some(err),
is_valid: false,
}),
}
}
/// 验证 JSON 字符串
///
/// 检查输入的字符串是否为有效的 JSON
///
/// # 参数
///
/// * `input` - 输入的 JSON 字符串
///
/// # 返回
///
/// 返回验证结果
pub fn validate(input: &str) -> AppResult<JsonValidateResult> {
// 参数验证
if input.trim().is_empty() {
return Ok(JsonValidateResult {
is_valid: false,
error_message: Some("输入内容不能为空".to_string()),
error_line: None,
error_column: None,
});
}
// 调用工具函数进行验证
let result = json_formatter::validate_json(input);
Ok(JsonValidateResult {
is_valid: result.is_valid,
error_message: result.error_message,
error_line: result.error_line,
error_column: result.error_column,
})
}
/// 压缩 JSON 字符串
///
/// 去除 JSON 中的所有空格和换行
///
/// # 参数
///
/// * `input` - 输入的 JSON 字符串
///
/// # 返回
///
/// 返回格式化结果
pub fn compact(input: &str) -> AppResult<JsonFormatResult> {
// 参数验证
if input.trim().is_empty() {
return Ok(JsonFormatResult {
success: false,
result: String::new(),
error: Some("输入内容不能为空".to_string()),
is_valid: false,
});
}
// 调用工具函数进行压缩
match json_formatter::compact_json(input) {
Ok(compacted) => Ok(JsonFormatResult {
success: true,
result: compacted,
error: None,
is_valid: true,
}),
Err(err) => Ok(JsonFormatResult {
success: false,
result: String::new(),
error: Some(err),
is_valid: false,
}),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_valid_json() {
let service = JsonFormatService;
let input = r#"{"name":"test","value":123}"#;
let config = JsonFormatConfig::default();
let result = service.format(input, &config).unwrap();
assert!(result.success);
assert!(result.is_valid);
assert!(result.error.is_none());
assert!(result.result.contains('\n'));
}
#[test]
fn test_format_invalid_json() {
let service = JsonFormatService;
let input = r#"{"invalid": }"#;
let config = JsonFormatConfig::default();
let result = service.format(input, &config).unwrap();
assert!(!result.success);
assert!(!result.is_valid);
assert!(result.error.is_some());
}
#[test]
fn test_format_empty_input() {
let service = JsonFormatService;
let input = "";
let config = JsonFormatConfig::default();
let result = service.format(input, &config).unwrap();
assert!(!result.success);
assert!(!result.is_valid);
assert!(result.error.is_some());
}
#[test]
fn test_validate_valid_json() {
let service = JsonFormatService;
let input = r#"{"valid": true}"#;
let result = service.validate(input).unwrap();
assert!(result.is_valid);
assert!(result.error_message.is_none());
}
#[test]
fn test_validate_invalid_json() {
let service = JsonFormatService;
let input = r#"{"invalid": }"#;
let result = service.validate(input).unwrap();
assert!(!result.is_valid);
assert!(result.error_message.is_some());
}
#[test]
fn test_compact_json() {
let service = JsonFormatService;
let input = r#"{ "name" : "test" }"#;
let result = service.compact(input).unwrap();
assert!(result.success);
assert!(result.is_valid);
assert_eq!(result.result, r#"{"name":"test"}"#);
}
}

View File

@@ -0,0 +1,7 @@
//! 业务逻辑层
//!
//! 提供应用的核心业务逻辑实现
pub mod json_format_service;
pub mod system_info_service;
pub mod window_service;

View File

@@ -0,0 +1,38 @@
//! 系统信息服务
//!
//! 提供系统信息查询功能的核心业务逻辑
use crate::error::AppResult;
use crate::models::system_info::SystemInfo;
use crate::platforms::system_info::SystemInfoAccessor;
/// 系统信息服务
pub struct SystemInfoService;
impl SystemInfoService {
/// 获取系统信息
///
/// 查询并返回当前系统的完整信息
///
/// # 返回
///
/// 返回包含所有系统信息的结构体
///
/// # 错误
///
/// 平台不支持或获取信息失败时返回错误
pub fn get_system_info() -> AppResult<SystemInfo> {
// 调用平台实现获取系统信息
#[cfg(windows)]
{
let accessor = crate::platforms::windows::system_info_impl::WindowsSystemInfo;
accessor.get_system_info()
}
#[cfg(not(windows))]
{
let accessor = crate::platforms::windows::system_info_impl::DummySystemInfo;
accessor.get_system_info()
}
}
}

View File

@@ -0,0 +1,81 @@
//! 窗口服务
//!
//! 提供窗口管理相关的业务逻辑
use tauri::Window;
use crate::error::{AppError, AppResult};
/// 窗口服务
///
/// 提供窗口显示、隐藏和切换等管理功能
pub struct WindowService;
impl WindowService {
/// 切换窗口显示/隐藏
///
/// 根据窗口当前状态切换显示或隐藏
///
/// # 参数
///
/// * `window` - Tauri 窗口引用
///
/// # 行为
///
/// - 如果窗口当前可见,则隐藏窗口
/// - 如果窗口当前隐藏,则显示窗口并聚焦
///
/// # 错误
///
/// 窗口操作失败时返回 `AppError::WindowOperationFailed`
pub fn toggle_window(window: &Window) -> AppResult<()> {
let is_visible = window.is_visible()
.map_err(|e| AppError::WindowOperationFailed(e.to_string()))?;
if is_visible {
Self::hide_window(window)?;
} else {
Self::show_window(window)?;
}
Ok(())
}
/// 隐藏窗口
///
/// 将窗口隐藏,使其不再可见
///
/// # 参数
///
/// * `window` - Tauri 窗口引用
///
/// # 错误
///
/// 窗口操作失败时返回 `AppError::WindowOperationFailed`
pub fn hide_window(window: &Window) -> AppResult<()> {
window.hide()
.map_err(|e| AppError::WindowOperationFailed(e.to_string()))
}
/// 显示窗口并聚焦
///
/// 显示窗口并将其设置为焦点窗口
///
/// # 参数
///
/// * `window` - Tauri 窗口引用
///
/// # 行为
///
/// - 显示窗口
/// - 将窗口设置为焦点窗口,用户可以直接与之交互
///
/// # 错误
///
/// 窗口操作失败时返回 `AppError::WindowOperationFailed`
pub fn show_window(window: &Window) -> AppResult<()> {
window.show()
.and_then(|_| window.set_focus())
.map_err(|e| AppError::WindowOperationFailed(e.to_string()))
}
}

View File

@@ -0,0 +1,93 @@
//! 颜色转换工具
//!
//! 提供颜色空间转换算法实现
use crate::models::color::HslInfo;
/// RGB 转 HSL
///
/// 将 RGB 颜色值转换为 HSL 颜色值
///
/// # 参数
///
/// * `r` - 红色分量 (0-255)
/// * `g` - 绿色分量 (0-255)
/// * `b` - 蓝色分量 (0-255)
///
/// # 返回
///
/// 返回 HSL 颜色信息
///
/// # 算法说明
///
/// 该函数使用标准的 RGB 到 HSL 转换算法:
/// 1. 将 RGB 值归一化到 [0, 1] 范围
/// 2. 计算最大值和最小值
/// 3. 根据最大值计算色相H
/// 4. 根据最大值和最小值之差计算饱和度S
/// 5. 亮度为最大值和最小值的平均值
///
/// # 示例
///
/// ```
/// use crate::utils::color_conversion::rgb_to_hsl;
///
/// let hsl = rgb_to_hsl(255, 0, 0);
/// assert_eq!(hsl.h, 0); // 红色
/// assert_eq!(hsl.s, 100); // 完全饱和
/// assert_eq!(hsl.l, 50); // 中等亮度
/// ```
pub fn rgb_to_hsl(r: u8, g: u8, b: u8) -> HslInfo {
let r = r as f64 / 255.0;
let g = g as f64 / 255.0;
let b = b as f64 / 255.0;
let max = r.max(g).max(b);
let min = r.min(g).min(b);
let mut h = 0.0;
let mut s = 0.0;
let l = (max + min) / 2.0;
if max != min {
let d = max - min;
s = if l > 0.5 {
d / (2.0 - max - min)
} else {
d / (max + min)
};
h = match max {
x if x == r => (g - b) / d + if g < b { 6.0 } else { 0.0 },
x if x == g => (b - r) / d + 2.0,
_ => (r - g) / d + 4.0,
};
h /= 6.0;
}
HslInfo {
h: (h * 360.0).round() as u16,
s: (s * 100.0).round() as u8,
l: (l * 100.0).round() as u8,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_red_color() {
let hsl = rgb_to_hsl(255, 0, 0);
assert_eq!(hsl.h, 0);
assert_eq!(hsl.s, 100);
assert_eq!(hsl.l, 50);
}
#[test]
fn test_gray_color() {
let hsl = rgb_to_hsl(128, 128, 128);
assert_eq!(hsl.s, 0);
assert_eq!(hsl.l, 50);
}
}

View File

@@ -0,0 +1,290 @@
//! JSON 格式化工具函数
//!
//! 提供纯函数的 JSON 处理算法
use crate::models::json_format::{FormatMode, JsonFormatConfig};
use serde_json::{self, Value};
/// 格式化 JSON 字符串
///
/// 对输入的 JSON 字符串进行格式化,支持美化和压缩模式
///
/// # 参数
///
/// * `input` - 输入的 JSON 字符串
/// * `config` - 格式化配置
///
/// # 返回
///
/// 返回格式化后的 JSON 字符串
///
/// # 错误
///
/// 当输入不是有效的 JSON 时返回错误
///
/// # 示例
///
/// ```
/// use crate::utils::json_formatter::format_json;
/// use crate::models::json_format::{JsonFormatConfig, FormatMode};
///
/// let input = r#"{"name":"test","value":123}"#;
/// let config = JsonFormatConfig::default();
/// let result = format_json(input, &config).unwrap();
/// assert!(result.contains('\n'));
/// ```
pub fn format_json(input: &str, config: &JsonFormatConfig) -> Result<String, String> {
// 解析 JSON
let mut value: Value = serde_json::from_str(input)
.map_err(|e| format!("JSON 解析失败: {}", e))?;
// 如果需要排序 key
if config.sort_keys {
sort_keys(&mut value);
}
// 根据模式格式化
match config.mode {
FormatMode::Pretty => {
let indent_str = " ".repeat(config.indent as usize);
serde_json::to_string_pretty(&value)
.map_err(|e| format!("JSON 格式化失败: {}", e))
.map(|s| replace_indent(&s, &indent_str))
}
FormatMode::Compact => {
serde_json::to_string(&value)
.map_err(|e| format!("JSON 序列化失败: {}", e))
}
}
}
/// 替换缩进空格数
///
/// serde_json 默认使用 2 空格缩进,此函数将其替换为配置的缩进数
fn replace_indent(json: &str, indent: &str) -> String {
if indent == " " {
return json.to_string();
}
json.lines()
.map(|line| {
let trimmed = line.trim_start();
if trimmed.is_empty() {
return String::new();
}
let leading_spaces = line.len() - trimmed.len();
if leading_spaces > 0 {
let indent_level = leading_spaces / 2;
format!("{}{}", indent.repeat(indent_level), trimmed)
} else {
line.to_string()
}
})
.collect::<Vec<_>>()
.join("\n")
}
/// 对 JSON 对象的 key 进行排序
///
/// 递归遍历 JSON 结构,对所有对象的 key 按字母顺序排序
fn sort_keys(value: &mut Value) {
match value {
Value::Object(map) => {
// 收集所有 key-value 对
let mut entries: Vec<(String, Value)> = map
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
// 排序 key
entries.sort_by(|a, b| a.0.cmp(&b.0));
// 递归处理每个值
for (_, v) in &mut entries {
sort_keys(v);
}
// 清空并重新插入
map.clear();
for (k, v) in entries {
map.insert(k, v);
}
}
Value::Array(arr) => {
// 递归处理数组中的每个元素
for v in arr {
sort_keys(v);
}
}
_ => {}
}
}
/// 验证 JSON 字符串是否有效
///
/// 检查输入的字符串是否为有效的 JSON
///
/// # 参数
///
/// * `input` - 输入的 JSON 字符串
///
/// # 返回
///
/// 返回验证结果,包含是否有效和错误信息
///
/// # 示例
///
/// ```
/// use crate::utils::json_formatter::validate_json;
///
/// let result = validate_json(r#"{"valid": true}"#);
/// assert!(result.is_valid);
///
/// let result = validate_json(r#"{"invalid": }"#);
/// assert!(!result.is_valid);
/// ```
pub fn validate_json(input: &str) -> JsonValidateResult {
match serde_json::from_str::<Value>(input) {
Ok(_) => JsonValidateResult {
is_valid: true,
error_message: None,
error_line: None,
error_column: None,
},
Err(e) => {
// 解析错误信息以获取行号和列号
let error_msg = e.to_string();
let (line, column) = parse_error_position(&error_msg);
JsonValidateResult {
is_valid: false,
error_message: Some(error_msg),
error_line: line,
error_column: column,
}
}
}
}
/// JSON 验证结果结构
#[derive(Debug, Clone)]
pub struct JsonValidateResult {
pub is_valid: bool,
pub error_message: Option<String>,
pub error_line: Option<usize>,
pub error_column: Option<usize>,
}
/// 从错误信息中解析行号和列号
fn parse_error_position(error_msg: &str) -> (Option<usize>, Option<usize>) {
// serde_json 的错误格式通常是 "line X, column Y"
if let Some(line_pos) = error_msg.find("line ") {
let after_line = &error_msg[line_pos + 5..];
if let Some(comma_pos) = after_line.find(',') {
if let Ok(line) = after_line[..comma_pos].parse::<usize>() {
if let Some(col_pos) = after_line.find("column ") {
let after_col = &after_line[col_pos + 7..];
if let Some(end_pos) = after_col.find(|c: char| !c.is_ascii_digit()) {
if let Ok(col) = after_col[..end_pos].parse::<usize>() {
return (Some(line), Some(col));
}
}
}
}
}
}
(None, None)
}
/// 压缩 JSON 字符串
///
/// 去除所有空格和换行,生成最紧凑的 JSON 格式
///
/// # 参数
///
/// * `input` - 输入的 JSON 字符串
///
/// # 返回
///
/// 返回压缩后的 JSON 字符串
///
/// # 错误
///
/// 当输入不是有效的 JSON 时返回错误
pub fn compact_json(input: &str) -> Result<String, String> {
let value: Value = serde_json::from_str(input)
.map_err(|e| format!("JSON 解析失败: {}", e))?;
serde_json::to_string(&value)
.map_err(|e| format!("JSON 序列化失败: {}", e))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_json_pretty() {
let input = r#"{"name":"test","value":123}"#;
let config = JsonFormatConfig::default();
let result = format_json(input, &config).unwrap();
assert!(result.contains('\n'));
assert!(result.contains(" "));
}
#[test]
fn test_format_json_compact() {
let input = r#"{ "name" : "test" , "value" : 123 }"#;
let config = JsonFormatConfig {
mode: FormatMode::Compact,
..Default::default()
};
let result = format_json(input, &config).unwrap();
assert!(!result.contains('\n'));
assert!(!result.contains(' '));
}
#[test]
fn test_format_json_invalid() {
let input = r#"{"invalid": }"#;
let config = JsonFormatConfig::default();
assert!(format_json(input, &config).is_err());
}
#[test]
fn test_format_json_with_sort_keys() {
let input = r#"{"z":1,"a":2,"m":3}"#;
let config = JsonFormatConfig {
sort_keys: true,
..Default::default()
};
let result = format_json(input, &config).unwrap();
// 验证 key 已排序
let a_pos = result.find("\"a\"").unwrap();
let m_pos = result.find("\"m\"").unwrap();
let z_pos = result.find("\"z\"").unwrap();
assert!(a_pos < m_pos);
assert!(m_pos < z_pos);
}
#[test]
fn test_validate_json_valid() {
let result = validate_json(r#"{"valid": true}"#);
assert!(result.is_valid);
assert!(result.error_message.is_none());
}
#[test]
fn test_validate_json_invalid() {
let result = validate_json(r#"{"invalid": }"#);
assert!(!result.is_valid);
assert!(result.error_message.is_some());
}
#[test]
fn test_compact_json() {
let input = r#"{ "name" : "test" }"#;
let result = compact_json(input).unwrap();
assert_eq!(result, r#"{"name":"test"}"#);
}
}

View File

@@ -0,0 +1,8 @@
//! 工具函数模块
//!
//! 提供纯函数算法实现,无副作用
pub mod color_conversion;
pub mod json_formatter;
pub mod screen;
pub mod shortcut;

View File

@@ -0,0 +1,177 @@
//! Windows 屏幕访问模块
//!
//! 提供屏幕像素颜色获取功能
use crate::error::{AppError, AppResult};
use windows::Win32::Foundation::HWND;
use windows::Win32::Graphics::Gdi::{
BitBlt, CreateCompatibleBitmap, CreateCompatibleDC, DeleteDC, DeleteObject, GetDC, GetDIBits,
GetPixel, ReleaseDC, SelectObject, BITMAPINFO, BITMAPINFOHEADER, BI_RGB, DIB_RGB_COLORS,
HBITMAP, HGDIOBJ, SRCCOPY,
};
use windows::Win32::UI::WindowsAndMessaging::{
GetSystemMetrics, SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, SM_XVIRTUALSCREEN, SM_YVIRTUALSCREEN,
};
/// Windows 屏幕访问器
pub struct WindowsScreen;
impl WindowsScreen {
/// 获取屏幕指定像素的 RGB 颜色
///
/// # 参数
///
/// * `x` - 屏幕横坐标(像素)
/// * `y` - 屏幕纵坐标(像素)
///
/// # 返回
///
/// 返回 RGB 三个分量的值,每个分量范围是 0-255
///
/// # 错误
///
/// 如果无法访问屏幕或坐标无效,返回错误
pub fn get_pixel_color(x: i32, y: i32) -> AppResult<(u8, u8, u8)> {
unsafe {
let screen_dc = GetDC(HWND::default());
if screen_dc.is_invalid() {
return Err(AppError::ScreenAccessFailed("无法获取屏幕设备上下文".to_string()));
}
let color = GetPixel(screen_dc, x, y);
ReleaseDC(HWND::default(), screen_dc);
// COLORREF 是一个 newtype包含 u32 值
// 格式: 0x00BBGGRR (蓝、绿、红)
let color_value = color.0;
if color_value == 0xFFFFFFFF {
// GetPixel 在失败时返回 CLR_INVALID (0xFFFFFFFF)
return Err(AppError::ScreenAccessFailed("无法获取像素颜色".to_string()));
}
let r = (color_value & 0xFF) as u8;
let g = ((color_value >> 8) & 0xFF) as u8;
let b = ((color_value >> 16) & 0xFF) as u8;
Ok((r, g, b))
}
}
/// 捕获屏幕指定区域像素RGBA行优先左上角开始
///
/// 该函数用于前端放大镜实时预览。
pub fn capture_region_rgba(
x: i32,
y: i32,
width: i32,
height: i32,
) -> AppResult<Vec<u8>> {
if width <= 0 || height <= 0 {
return Err(AppError::ScreenAccessFailed("无效的捕获区域尺寸".to_string()));
}
// 将捕获区域 clamp 到“虚拟屏幕”范围,避免在屏幕边缘 BitBlt 失败
let v_left = unsafe { GetSystemMetrics(SM_XVIRTUALSCREEN) };
let v_top = unsafe { GetSystemMetrics(SM_YVIRTUALSCREEN) };
let v_w = unsafe { GetSystemMetrics(SM_CXVIRTUALSCREEN) };
let v_h = unsafe { GetSystemMetrics(SM_CYVIRTUALSCREEN) };
// 如果请求区域比虚拟屏幕还大,直接报错(避免溢出/异常)
if width > v_w || height > v_h {
return Err(AppError::ScreenAccessFailed("捕获区域超出屏幕范围".to_string()));
}
let max_x = v_left + v_w - width;
let max_y = v_top + v_h - height;
let x = x.clamp(v_left, max_x);
let y = y.clamp(v_top, max_y);
unsafe {
// 屏幕 DC
let screen_dc = GetDC(HWND::default());
if screen_dc.is_invalid() {
return Err(AppError::ScreenAccessFailed("无法获取屏幕设备上下文".to_string()));
}
// 内存 DC + 位图
let mem_dc = CreateCompatibleDC(screen_dc);
if mem_dc.is_invalid() {
ReleaseDC(HWND::default(), screen_dc);
return Err(AppError::ScreenAccessFailed("无法创建兼容设备上下文".to_string()));
}
let bitmap: HBITMAP = CreateCompatibleBitmap(screen_dc, width, height);
if bitmap.is_invalid() {
let _ = DeleteDC(mem_dc);
ReleaseDC(HWND::default(), screen_dc);
return Err(AppError::ScreenAccessFailed("无法创建兼容位图".to_string()));
}
let old_obj: HGDIOBJ = SelectObject(mem_dc, bitmap);
// 拷贝屏幕到位图
let ok = BitBlt(mem_dc, 0, 0, width, height, screen_dc, x, y, SRCCOPY);
// 释放 screen dc尽早
ReleaseDC(HWND::default(), screen_dc);
if ok.is_err() {
// 恢复/清理
let _ = SelectObject(mem_dc, old_obj);
let _ = DeleteObject(bitmap);
let _ = DeleteDC(mem_dc);
return Err(AppError::ScreenAccessFailed("BitBlt 捕获失败".to_string()));
}
// 准备 BITMAPINFO32-bit BGRA并用负高度得到“自顶向下”顺序
let mut bmi = BITMAPINFO {
bmiHeader: BITMAPINFOHEADER {
biSize: std::mem::size_of::<BITMAPINFOHEADER>() as u32,
biWidth: width,
biHeight: -height, // top-down
biPlanes: 1,
biBitCount: 32,
biCompression: BI_RGB.0 as u32,
biSizeImage: 0,
biXPelsPerMeter: 0,
biYPelsPerMeter: 0,
biClrUsed: 0,
biClrImportant: 0,
},
bmiColors: [Default::default(); 1],
};
let mut bgra = vec![0u8; (width as usize) * (height as usize) * 4];
let lines = GetDIBits(
mem_dc,
bitmap,
0,
height as u32,
Some(bgra.as_mut_ptr() as *mut _),
&mut bmi,
DIB_RGB_COLORS,
);
// 恢复/清理 GDI 对象
let _ = SelectObject(mem_dc, old_obj);
let _ = DeleteObject(bitmap);
let _ = DeleteDC(mem_dc);
if lines == 0 {
return Err(AppError::ScreenAccessFailed("GetDIBits 读取失败".to_string()));
}
// BGRA -> RGBA给前端 canvas 更直接)
for px in bgra.chunks_exact_mut(4) {
let b = px[0];
let r = px[2];
px[0] = r;
px[2] = b;
// px[1] = g, px[3] = a 保持
}
Ok(bgra)
}
}
}

View File

@@ -0,0 +1,54 @@
//! 全局快捷键工具
//!
//! 将快捷键注册逻辑从 `lib.rs` 拆分出来,避免入口文件过于拥挤。
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use tauri::Manager;
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, Modifiers, Shortcut, ShortcutState};
/// 注册全局快捷键
///
/// - `Alt+Space`: 切换主窗口显示/隐藏
pub fn register_global_shortcuts(app: &tauri::App) -> Result<(), String> {
let shortcut = Shortcut::new(Some(Modifiers::ALT), Code::Space);
let app_handle = app.handle().clone();
let is_processing = Arc::new(AtomicBool::new(false));
let is_processing_clone = is_processing.clone();
app.global_shortcut()
.on_shortcut(shortcut, move |_app_handle, _shortcut, event| {
// 忽略按键释放事件
if event.state == ShortcutState::Released {
return;
}
// 防止重复触发
if is_processing_clone.load(Ordering::SeqCst) {
return;
}
is_processing_clone.store(true, Ordering::SeqCst);
if let Some(window) = app_handle.get_webview_window("main") {
let _ = window.is_visible().and_then(|is_visible| {
if is_visible {
window.hide()
} else {
window.show().and_then(|_| window.set_focus())
}
});
}
// 延迟重置处理标志,防止快速重复触发
let is_processing_reset = is_processing_clone.clone();
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(500));
is_processing_reset.store(false, Ordering::SeqCst);
});
})
.map_err(|e| format!("注册全局快捷键失败: {}", e))?;
println!("全局快捷键 Alt+Space 注册成功");
Ok(())
}

View File

@@ -1,6 +1,6 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "tauri-app", "productName": "CmdRs",
"version": "0.1.0", "version": "0.1.0",
"identifier": "com.shenjianz.tauri-app", "identifier": "com.shenjianz.tauri-app",
"build": { "build": {
@@ -12,11 +12,19 @@
"app": { "app": {
"windows": [ "windows": [
{ {
"title": "tauri-app", "title": "CmdRs - 功能集合",
"width": 800, "label": "main",
"height": 600, "width": 1200,
"minWidth": 600, "height": 800,
"minHeight": 500 "minWidth": 800,
"minHeight": 600,
"decorations": true,
"transparent": false,
"alwaysOnTop": false,
"skipTaskbar": false,
"visible": false,
"center": true,
"resizable": true
} }
], ],
"security": { "security": {

View File

@@ -1,24 +1,32 @@
import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import { Routes, Route, Navigate } from "react-router-dom";
import { MainLayout } from "@/components/layout/MainLayout";
import { Dashboard } from "@/pages/Dashboard";
import { Features } from "@/pages/Features";
import { Components } from "@/pages/Components";
import { ThemeProvider } from "@/components/theme-provider"; import { ThemeProvider } from "@/components/theme-provider";
import { Home } from "@/pages/Home";
import { Search } from "@/pages/Search";
import Settings from "@/pages/Settings";
import { CommandPalette } from "@/components/command-palette/CommandPalette";
import { ColorPickerPage } from "@/components/features/ColorPicker/ColorPickerPage";
import { JsonFormatterPage } from "@/components/features/JsonFormatter/JsonFormatterPage";
import { SystemInfoPage } from "@/components/features/SystemInfo/SystemInfoPage";
function App() { function App() {
return ( return (
<Router> <ThemeProvider defaultTheme="system" storageKey="vite-ui-theme">
<ThemeProvider defaultTheme="system" storageKey="vite-ui-theme"> <div className="w-screen h-screen">
<MainLayout> {/* 全局快捷键监听 */}
<Routes> <CommandPalette />
<Route path="/" element={<Dashboard />} />
<Route path="/features" element={<Features />} /> {/* 路由配置 */}
<Route path="/components" element={<Components />} /> <Routes>
<Route path="/layout" element={<Dashboard />} /> <Route path="/" element={<Home />} />
</Routes> <Route path="/search" element={<Search />} />
</MainLayout> <Route path="/settings" element={<Settings />} />
</ThemeProvider> <Route path="/feature/color-picker" element={<ColorPickerPage />} />
</Router> <Route path="/feature/json-formatter" element={<JsonFormatterPage />} />
<Route path="/feature/system-info" element={<SystemInfoPage />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</div>
</ThemeProvider>
); );
} }

View File

@@ -0,0 +1,32 @@
import { useEffect } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
/**
* 全局快捷键处理组件
* 负责监听全局快捷键并控制窗口显示/隐藏
*/
export function CommandPalette() {
useEffect(() => {
// 监听热键事件
const unlistenPromise = listen('hotkey-pressed', async () => {
await invoke('toggle_window');
});
// 清理监听器
return () => {
unlistenPromise.then((unlisten) => unlisten());
};
}, []);
// 这个组件不渲染任何可见 UI
return null;
}
// 保留 SearchResult 类型以供其他组件使用
export interface SearchResult {
id: string;
title: string;
description?: string;
icon?: string;
}

View File

@@ -0,0 +1,45 @@
import type { SearchResult } from './CommandPalette';
import { AppWindow } from 'lucide-react';
interface ResultItemProps {
result: SearchResult;
isSelected: boolean;
onClick: () => void;
}
export function ResultItem({ result, isSelected, onClick }: ResultItemProps) {
const IconComponent = AppWindow; // 默认图标
return (
<div
onClick={onClick}
className={`flex items-center gap-3 px-4 py-3 cursor-pointer transition-colors ${
isSelected
? 'bg-primary/20 border-l-4 border-primary'
: 'hover:bg-muted/50 border-l-4 border-transparent'
}`}
>
{/* 图标 */}
<div className="flex-shrink-0">
<IconComponent className="w-5 h-5 text-muted-foreground" />
</div>
{/* 内容 */}
<div className="flex-1 min-w-0">
<div className="font-medium truncate">{result.title}</div>
{result.description && (
<div className="text-sm text-muted-foreground truncate">
{result.description}
</div>
)}
</div>
{/* 快捷键提示(可选) */}
{result.icon && (
<div className="flex-shrink-0 text-xs text-muted-foreground bg-muted px-2 py-1 rounded">
{result.icon}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,31 @@
import { ResultItem } from './ResultItem';
import type { SearchResult } from './CommandPalette';
interface ResultListProps {
results: SearchResult[];
selectedIndex: number;
onSelect: (index: number) => void;
}
export function ResultList({ results, selectedIndex, onSelect }: ResultListProps) {
if (results.length === 0) {
return (
<div className="flex items-center justify-center h-full text-muted-foreground">
<p>...</p>
</div>
);
}
return (
<div className="py-2">
{results.map((result, index) => (
<ResultItem
key={result.id}
result={result}
isSelected={index === selectedIndex}
onClick={() => onSelect(index)}
/>
))}
</div>
);
}

View File

@@ -0,0 +1,39 @@
import { Search } from 'lucide-react';
import { forwardRef, useEffect } from 'react';
interface SearchInputProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
}
export const SearchInput = forwardRef<HTMLInputElement, SearchInputProps>(
({ value, onChange, placeholder = '搜索...' }, ref) => {
// 内部 ref 用于自动聚焦
const innerRef = ref || useEffect;
useEffect(() => {
// 自动聚焦到输入框
(innerRef as any)?.current?.focus();
}, []);
return (
<div className="relative">
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground">
<Search className="w-5 h-5" />
</div>
<input
ref={innerRef as any}
type="text"
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
className="w-full pl-10 pr-4 py-3 bg-muted/50 rounded-lg border border-border focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"
autoComplete="off"
/>
</div>
);
}
);
SearchInput.displayName = 'SearchInput';

View File

@@ -0,0 +1,237 @@
import { useState, useCallback, useEffect } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import { Copy, Check, Droplet, RefreshCw } from 'lucide-react';
interface ColorInfo {
hex: string;
rgb: { r: number; g: number; b: number };
hsl: { h: number; s: number; l: number };
x: number;
y: number;
}
interface ColorHistory {
color: ColorInfo;
timestamp: number;
}
export function ColorPickerPage() {
const [currentColor, setCurrentColor] = useState<ColorInfo | null>(null);
const [history, setHistory] = useState<ColorHistory[]>([]);
const [copied, setCopied] = useState<string>('');
const [isPicking, setIsPicking] = useState(false);
// 监听从取色器窗口返回的颜色
useEffect(() => {
const unlistenPicked = listen<ColorInfo>('color-picked', (event) => {
setCurrentColor(event.payload);
setHistory(prev => [{ color: event.payload, timestamp: Date.now() }, ...prev].slice(0, 10));
setIsPicking(false);
});
const unlistenCancelled = listen('color-picker-cancelled', () => {
setIsPicking(false);
});
return () => {
unlistenPicked.then(fn => fn());
unlistenCancelled.then(fn => fn());
};
}, []);
// 开始拾色
const pickColor = useCallback(async () => {
try {
setIsPicking(true);
// 调用启动取色器命令,打开透明遮罩窗口
await invoke('start_color_picker');
} catch (error) {
console.error('拾色失败:', error);
alert('取色失败: ' + String(error));
setIsPicking(false);
}
}, []);
// 复制到剪贴板
const copyToClipboard = useCallback(async (text: string, type: string) => {
try {
await navigator.clipboard.writeText(text);
setCopied(type);
setTimeout(() => setCopied(''), 2000);
} catch (error) {
console.error('复制失败:', error);
}
}, []);
return (
<div className="min-h-screen bg-background">
{/* 顶部导航栏 */}
<header className="border-b bg-background/95 backdrop-blur">
<div className="container mx-auto px-4 py-4">
<div className="flex items-center gap-3">
<Button variant="ghost" size="sm" onClick={() => window.history.back()}>
</Button>
<div className="flex items-center gap-2">
<Droplet className="w-6 h-6 text-primary" />
<h1 className="text-xl font-bold"></h1>
</div>
</div>
</div>
</header>
{/* 主内容区 */}
<main className="container mx-auto px-4 py-6">
<div className="max-w-4xl mx-auto space-y-6">
{/* 拾色按钮 */}
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold mb-2"></h3>
<p className="text-sm text-muted-foreground">
</p>
</div>
<Button
size="lg"
onClick={pickColor}
disabled={isPicking}
className="gap-2"
>
{isPicking ? (
<>
<RefreshCw className="w-5 h-5 animate-spin" />
...
</>
) : (
<>
<Droplet className="w-5 h-5" />
</>
)}
</Button>
</div>
</CardContent>
</Card>
{/* 当前颜色 */}
{currentColor && (
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription>
: ({currentColor.x}, {currentColor.y})
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{/* 颜色预览 */}
<div className="flex items-center gap-4">
<div
className="w-32 h-32 rounded-lg shadow-lg border-4 border-border"
style={{ backgroundColor: currentColor.hex }}
/>
<div className="flex-1 space-y-3">
{/* HEX */}
<div className="flex items-center gap-2">
<Badge variant="outline">HEX</Badge>
<Input
value={currentColor.hex}
readOnly
className="font-mono"
/>
<Button
size="sm"
variant="outline"
onClick={() => copyToClipboard(currentColor.hex, 'hex')}
>
{copied === 'hex' ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
</Button>
</div>
{/* RGB */}
<div className="flex items-center gap-2">
<Badge variant="outline">RGB</Badge>
<Input
value={`rgb(${currentColor.rgb.r}, ${currentColor.rgb.g}, ${currentColor.rgb.b})`}
readOnly
className="font-mono"
/>
<Button
size="sm"
variant="outline"
onClick={() => copyToClipboard(`rgb(${currentColor.rgb.r}, ${currentColor.rgb.g}, ${currentColor.rgb.b})`, 'rgb')}
>
{copied === 'rgb' ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
</Button>
</div>
{/* HSL */}
<div className="flex items-center gap-2">
<Badge variant="outline">HSL</Badge>
<Input
value={`hsl(${currentColor.hsl.h}, ${currentColor.hsl.s}%, ${currentColor.hsl.l}%)`}
readOnly
className="font-mono"
/>
<Button
size="sm"
variant="outline"
onClick={() => copyToClipboard(`hsl(${currentColor.hsl.h}, ${currentColor.hsl.s}%, ${currentColor.hsl.l}%)`, 'hsl')}
>
{copied === 'hsl' ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
</Button>
</div>
</div>
</div>
</CardContent>
</Card>
)}
{/* 历史记录 */}
{history.length > 0 && (
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-5 sm:grid-cols-10 gap-3">
{history.map((item, index) => (
<button
key={index}
onClick={() => setCurrentColor(item.color)}
className="w-12 h-12 rounded-lg shadow-md border-2 border-border hover:border-primary transition-colors"
style={{ backgroundColor: item.color.hex }}
title={item.color.hex}
/>
))}
</div>
</CardContent>
</Card>
)}
{/* 使用说明 */}
<Card>
<CardHeader>
<CardTitle>使</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm text-muted-foreground">
<p>1. "开始拾色"</p>
<p>2. </p>
<p>3. </p>
<p>4. </p>
<p>5. </p>
</CardContent>
</Card>
</div>
</main>
</div>
);
}

View File

@@ -0,0 +1,402 @@
import { useState, useCallback, useEffect } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Copy, Check, FileCode, Sparkles, Minimize2, CheckCircle2, XCircle, Upload } from 'lucide-react';
// 类型定义
interface JsonFormatConfig {
indent: number;
sort_keys: boolean;
mode: 'pretty' | 'compact';
}
interface JsonFormatResult {
success: boolean;
result: string;
error: string | null;
is_valid: boolean;
}
interface JsonValidateResult {
is_valid: boolean;
error_message: string | null;
error_line: number | null;
error_column: number | null;
}
export function JsonFormatterPage() {
const [input, setInput] = useState('');
const [output, setOutput] = useState('');
const [validation, setValidation] = useState<JsonValidateResult | null>(null);
const [config, setConfig] = useState<JsonFormatConfig>({
indent: 2,
sort_keys: false,
mode: 'pretty',
});
const [copied, setCopied] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
// 监听输入变化,自动验证
useEffect(() => {
if (input.trim()) {
validateJson();
} else {
setValidation(null);
}
}, [input]);
// 验证 JSON
const validateJson = useCallback(async () => {
if (!input.trim()) {
setValidation(null);
return;
}
try {
const result = await invoke<JsonValidateResult>('validate_json', {
input,
});
setValidation(result);
} catch (error) {
console.error('验证失败:', error);
}
}, [input]);
// 格式化 JSON
const formatJson = useCallback(async () => {
if (!input.trim()) {
return;
}
setIsProcessing(true);
try {
const result = await invoke<JsonFormatResult>('format_json', {
input,
config,
});
if (result.success) {
setOutput(result.result);
} else {
setOutput(result.error || '格式化失败');
}
} catch (error) {
console.error('格式化失败:', error);
setOutput('错误: ' + String(error));
} finally {
setIsProcessing(false);
}
}, [input, config]);
// 压缩 JSON
const compactJson = useCallback(async () => {
if (!input.trim()) {
return;
}
setIsProcessing(true);
try {
const result = await invoke<JsonFormatResult>('compact_json', {
input,
});
if (result.success) {
setOutput(result.result);
} else {
setOutput(result.error || '压缩失败');
}
} catch (error) {
console.error('压缩失败:', error);
setOutput('错误: ' + String(error));
} finally {
setIsProcessing(false);
}
}, [input]);
// 复制到剪贴板
const copyToClipboard = useCallback(async () => {
if (!output) return;
try {
await navigator.clipboard.writeText(output);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (error) {
console.error('复制失败:', error);
}
}, [output]);
// 清空输入
const clearInput = useCallback(() => {
setInput('');
setOutput('');
setValidation(null);
}, []);
// 使用示例
const loadExample = useCallback(() => {
const example = {
"name": "JSON 格式化工具",
"version": "1.0.0",
"features": ["格式化", "验证", "压缩"],
"config": {
"indent": 2,
"sortKeys": false
},
"active": true
};
setInput(JSON.stringify(example));
}, []);
return (
<div className="min-h-screen bg-background">
{/* 顶部导航栏 */}
<header className="border-b bg-background/95 backdrop-blur">
<div className="container mx-auto px-4 py-4">
<div className="flex items-center gap-3">
<Button variant="ghost" size="sm" onClick={() => window.history.back()}>
</Button>
<div className="flex items-center gap-2">
<FileCode className="w-6 h-6 text-primary" />
<h1 className="text-xl font-bold">JSON </h1>
</div>
</div>
</div>
</header>
{/* 主内容区 */}
<main className="container mx-auto px-4 py-6">
<div className="max-w-6xl mx-auto space-y-6">
{/* 配置选项 */}
<Card>
<CardHeader>
<CardTitle className="text-lg"></CardTitle>
<CardDescription> JSON </CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-wrap items-center gap-6">
{/* 缩进空格数 */}
<div className="flex items-center gap-3">
<label className="text-sm font-medium">:</label>
<div className="flex gap-1">
{[2, 4].map((spaces) => (
<Button
key={spaces}
size="sm"
variant={config.indent === spaces ? 'default' : 'outline'}
onClick={() => setConfig({ ...config, indent: spaces })}
>
{spaces}
</Button>
))}
</div>
</div>
{/* 排序 Keys */}
<div className="flex items-center gap-3">
<label className="text-sm font-medium"> Keys:</label>
<Button
size="sm"
variant={config.sort_keys ? 'default' : 'outline'}
onClick={() => setConfig({ ...config, sort_keys: !config.sort_keys })}
>
{config.sort_keys ? '开启' : '关闭'}
</Button>
</div>
{/* 格式化模式 */}
<div className="flex items-center gap-3">
<label className="text-sm font-medium">:</label>
<div className="flex gap-1">
{(['pretty', 'compact'] as const).map((mode) => (
<Button
key={mode}
size="sm"
variant={config.mode === mode ? 'default' : 'outline'}
onClick={() => setConfig({ ...config, mode })}
>
{mode === 'pretty' ? '美化' : '压缩'}
</Button>
))}
</div>
</div>
</div>
</CardContent>
</Card>
{/* 输入输出区域 */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* 输入区域 */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-lg"> JSON</CardTitle>
<CardDescription> JSON </CardDescription>
</div>
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={loadExample}
>
<Upload className="w-4 h-4 mr-1" />
</Button>
{input && (
<Button
size="sm"
variant="ghost"
onClick={clearInput}
>
</Button>
)}
</div>
</div>
</CardHeader>
<CardContent>
<div className="relative">
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
className="w-full h-96 p-4 font-mono text-sm bg-muted rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-primary"
placeholder="在此输入 JSON..."
spellCheck={false}
/>
{/* 验证状态指示器 */}
{validation && (
<div className="absolute top-2 right-2">
{validation.is_valid ? (
<Badge variant="default" className="gap-1 bg-green-500 hover:bg-green-600">
<CheckCircle2 className="w-3 h-3" />
</Badge>
) : (
<Badge variant="destructive" className="gap-1">
<XCircle className="w-3 h-3" />
</Badge>
)}
</div>
)}
</div>
{/* 错误信息 */}
{validation && !validation.is_valid && validation.error_message && (
<div className="mt-3 p-3 bg-destructive/10 border border-destructive/20 rounded-lg">
<p className="text-sm text-destructive font-medium">
{validation.error_message}
</p>
{(validation.error_line || validation.error_column) && (
<p className="text-xs text-destructive/80 mt-1">
位置: {validation.error_line}, {validation.error_column}
</p>
)}
</div>
)}
{/* 操作按钮 */}
<div className="flex gap-2 mt-4">
<Button
onClick={formatJson}
disabled={!input.trim() || isProcessing || !validation?.is_valid}
className="flex-1 gap-2"
>
{isProcessing ? (
<>
<Sparkles className="w-4 h-4 animate-spin" />
...
</>
) : (
<>
<Sparkles className="w-4 h-4" />
</>
)}
</Button>
<Button
onClick={compactJson}
variant="outline"
disabled={!input.trim() || isProcessing || !validation?.is_valid}
>
<Minimize2 className="w-4 h-4 mr-1" />
</Button>
</div>
</CardContent>
</Card>
{/* 输出区域 */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-lg"></CardTitle>
<CardDescription> JSON</CardDescription>
</div>
{output && (
<Button
size="sm"
variant="outline"
onClick={copyToClipboard}
className="gap-2"
>
{copied ? (
<>
<Check className="w-4 h-4" />
</>
) : (
<>
<Copy className="w-4 h-4" />
</>
)}
</Button>
)}
</div>
</CardHeader>
<CardContent>
<div className="relative">
<pre className="w-full h-96 p-4 font-mono text-sm bg-muted rounded-lg overflow-auto">
{output || (
<span className="text-muted-foreground">
...
</span>
)}
</pre>
</div>
{/* 统计信息 */}
{output && (
<div className="flex gap-4 mt-4 text-sm text-muted-foreground">
<span>: {output.length}</span>
<span>: {output.split('\n').length}</span>
</div>
)}
</CardContent>
</Card>
</div>
{/* 使用说明 */}
<Card>
<CardHeader>
<CardTitle>使</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm text-muted-foreground">
<p>1. JSON </p>
<p>2. JSON </p>
<p>3. Keys</p>
<p>4. "格式化" JSON"压缩"</p>
<p>5. "复制"</p>
</CardContent>
</Card>
</div>
</main>
</div>
);
}

View File

@@ -0,0 +1,581 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Monitor, Cpu, HardDrive, Database, Computer, RefreshCw, Clock, Play, Pause, Network, Wifi } from 'lucide-react';
// 类型定义
interface SystemInfo {
os: OsInfo;
cpu: CpuInfo;
memory: MemoryInfo;
gpu: GpuInfo[];
disks: DiskInfo[];
computer: ComputerInfo;
display: DisplayInfo;
network: NetworkInfo;
}
interface OsInfo {
name: string;
version: string;
arch: string;
kernel_version: string;
host_name: string;
uptime_readable: string;
}
interface CpuInfo {
model: string;
cores: number;
processors: number;
max_frequency: number;
usage_percent: number;
}
interface MemoryInfo {
total_gb: number;
available_gb: number;
used_gb: number;
usage_percent: number;
}
interface GpuInfo {
name: string;
vram_gb: number;
driver_version: string;
}
interface DiskInfo {
drive_letter: string;
volume_label: string;
file_system: string;
total_gb: number;
available_gb: number;
used_gb: number;
usage_percent: number;
}
interface ComputerInfo {
name: string;
username: string;
domain: string;
manufacturer: string;
model: string;
serial_number: string;
}
interface DisplayInfo {
monitor_count: number;
primary_resolution: string;
all_resolutions: string[];
}
interface NetworkInfo {
interfaces: InterfaceInfo[];
total_downloaded_mb: number;
total_uploaded_mb: number;
}
interface InterfaceInfo {
name: string;
mac_address: string;
ip_networks: string[];
upload_speed_kb: number;
download_speed_kb: number;
}
export function SystemInfoPage() {
const [systemInfo, setSystemInfo] = useState<SystemInfo | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// 自动刷新相关状态
const [autoRefresh, setAutoRefresh] = useState(false);
const [refreshInterval, setRefreshInterval] = useState(3); // 默认3秒
const [nextRefreshIn, setNextRefreshIn] = useState(0);
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const countdownRef = useRef<NodeJS.Timeout | null>(null);
// 刷新间隔选项(秒)
const intervalOptions = [1, 3, 5, 10, 30];
// 获取系统信息
const fetchSystemInfo = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const info = await invoke<SystemInfo>('get_system_info');
setSystemInfo(info);
// 重置倒计时
setNextRefreshIn(refreshInterval);
} catch (err) {
console.error('获取系统信息失败:', err);
setError(String(err));
} finally {
setIsLoading(false);
}
}, [refreshInterval]);
// 启动自动刷新
const startAutoRefresh = useCallback(() => {
// 清除现有定时器
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
if (countdownRef.current) {
clearInterval(countdownRef.current);
}
// 立即刷新一次
fetchSystemInfo();
// 设置刷新定时器
intervalRef.current = setInterval(() => {
fetchSystemInfo();
}, refreshInterval * 1000);
// 设置倒计时定时器(每秒更新一次)
setNextRefreshIn(refreshInterval);
countdownRef.current = setInterval(() => {
setNextRefreshIn((prev) => {
if (prev <= 1) {
return refreshInterval;
}
return prev - 1;
});
}, 1000);
}, [refreshInterval, fetchSystemInfo]);
// 停止自动刷新
const stopAutoRefresh = useCallback(() => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
if (countdownRef.current) {
clearInterval(countdownRef.current);
countdownRef.current = null;
}
setNextRefreshIn(0);
}, []);
// 切换自动刷新
const toggleAutoRefresh = useCallback(() => {
if (autoRefresh) {
stopAutoRefresh();
setAutoRefresh(false);
} else {
startAutoRefresh();
setAutoRefresh(true);
}
}, [autoRefresh, startAutoRefresh, stopAutoRefresh]);
// 当刷新间隔改变时,重启自动刷新
useEffect(() => {
if (autoRefresh) {
startAutoRefresh();
}
return () => {
if (intervalRef.current) clearInterval(intervalRef.current);
if (countdownRef.current) clearInterval(countdownRef.current);
};
}, [refreshInterval, autoRefresh, startAutoRefresh]);
// 初始加载
useEffect(() => {
if (!autoRefresh) {
fetchSystemInfo();
}
}, [autoRefresh, fetchSystemInfo]);
// 组件卸载时清理
useEffect(() => {
return () => {
if (intervalRef.current) clearInterval(intervalRef.current);
if (countdownRef.current) clearInterval(countdownRef.current);
};
}, []);
// 格式化显示
const formatGB = (value: number) => `${value.toFixed(2)} GB`;
return (
<div className="min-h-screen bg-background">
{/* 顶部导航栏 */}
<header className="border-b bg-background/95 backdrop-blur">
<div className="container mx-auto px-4 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Button variant="ghost" size="sm" onClick={() => window.history.back()}>
</Button>
<div className="flex items-center gap-2">
<Monitor className="w-6 h-6 text-primary" />
<h1 className="text-xl font-bold"></h1>
</div>
</div>
<div className="flex items-center gap-2">
{/* 自动刷新控制 */}
{autoRefresh && (
<Badge variant="outline" className="gap-1">
<Clock className="w-3 h-3" />
{nextRefreshIn}s
</Badge>
)}
<Button
size="sm"
variant={autoRefresh ? "default" : "outline"}
onClick={toggleAutoRefresh}
className="gap-2"
>
{autoRefresh ? (
<>
<Pause className="w-4 h-4" />
</>
) : (
<>
<Play className="w-4 h-4" />
</>
)}
</Button>
{/* 刷新间隔选择 */}
{autoRefresh && (
<div className="flex items-center gap-1 border rounded-md px-2">
<span className="text-xs text-muted-foreground">:</span>
{intervalOptions.map((interval) => (
<button
key={interval}
onClick={() => setRefreshInterval(interval)}
className={`px-2 py-1 text-xs rounded transition-colors ${
refreshInterval === interval
? 'bg-primary text-primary-foreground'
: 'hover:bg-muted'
}`}
>
{interval}s
</button>
))}
</div>
)}
<Button
size="sm"
variant="outline"
onClick={fetchSystemInfo}
disabled={isLoading}
className="gap-2"
>
{isLoading ? (
<>
<RefreshCw className="w-4 h-4 animate-spin" />
...
</>
) : (
<>
<RefreshCw className="w-4 h-4" />
</>
)}
</Button>
</div>
</div>
</div>
</header>
{/* 主内容区 */}
<main className="container mx-auto px-4 py-4">
{error && (
<Card className="mb-4 border-destructive">
<CardContent className="p-4">
<p className="text-destructive">{error}</p>
</CardContent>
</Card>
)}
{systemInfo && (
<div className="max-w-7xl mx-auto">
{/* 顶部2列系统、计算机 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
{/* 操作系统 */}
<Card>
<CardHeader className="py-3">
<div className="flex items-center gap-2">
<Monitor className="w-5 h-5 text-primary" />
<CardTitle className="text-lg"></CardTitle>
</div>
</CardHeader>
<CardContent className="py-3">
<div className="space-y-2">
<div className="flex justify-between"><span className="text-sm text-muted-foreground"></span><span className="text-sm font-medium">{systemInfo.os.name}</span></div>
<div className="flex justify-between"><span className="text-sm text-muted-foreground"></span><span className="text-sm font-medium">{systemInfo.os.version}</span></div>
<div className="flex justify-between"><span className="text-sm text-muted-foreground"></span><span className="text-sm font-medium">{systemInfo.os.arch}</span></div>
<div className="flex justify-between"><span className="text-sm text-muted-foreground"></span><span className="text-sm font-medium">{systemInfo.os.host_name}</span></div>
<div className="flex justify-between"><span className="text-sm text-muted-foreground"></span><span className="text-sm font-medium">{systemInfo.os.uptime_readable}</span></div>
</div>
</CardContent>
</Card>
{/* 计算机信息 */}
<Card>
<CardHeader className="py-3">
<div className="flex items-center gap-2">
<Computer className="w-5 h-5 text-primary" />
<CardTitle className="text-lg"></CardTitle>
</div>
</CardHeader>
<CardContent className="py-3">
<div className="space-y-2">
<div className="flex justify-between"><span className="text-sm text-muted-foreground"></span><span className="text-sm font-medium">{systemInfo.computer.name}</span></div>
<div className="flex justify-between"><span className="text-sm text-muted-foreground"></span><span className="text-sm font-medium">{systemInfo.computer.username}</span></div>
<div className="flex justify-between"><span className="text-sm text-muted-foreground">/</span><span className="text-sm font-medium">{systemInfo.computer.domain}</span></div>
<div className="flex justify-between"><span className="text-sm text-muted-foreground"></span><span className="text-sm font-medium">{systemInfo.computer.manufacturer}</span></div>
<div className="flex justify-between"><span className="text-sm text-muted-foreground"></span><span className="text-sm font-medium">{systemInfo.computer.model}</span></div>
</div>
</CardContent>
</Card>
</div>
{/* 中部2列CPU 和 内存 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
{/* CPU */}
<Card>
<CardHeader className="py-3">
<div className="flex items-center gap-2">
<Cpu className="w-5 h-5 text-primary" />
<CardTitle className="text-lg"></CardTitle>
</div>
</CardHeader>
<CardContent className="py-3">
<div className="space-y-3">
<div className="text-base font-medium">{systemInfo.cpu.model}</div>
<div className="grid grid-cols-3 gap-2 text-center">
<div className="bg-muted/50 rounded p-2">
<div className="text-xs text-muted-foreground"></div>
<div className="font-semibold text-base">{systemInfo.cpu.cores}</div>
</div>
<div className="bg-muted/50 rounded p-2">
<div className="text-xs text-muted-foreground">线</div>
<div className="font-semibold text-base">{systemInfo.cpu.processors}</div>
</div>
<div className="bg-muted/50 rounded p-2">
<div className="text-xs text-muted-foreground"></div>
<div className="font-semibold text-sm">{systemInfo.cpu.max_frequency}MHz</div>
</div>
</div>
<div>
<div className="flex justify-between mb-1">
<span className="text-sm text-muted-foreground">使</span>
<span className="text-sm font-medium">{systemInfo.cpu.usage_percent.toFixed(1)}%</span>
</div>
<div className="w-full bg-muted rounded-full h-2">
<div
className="bg-primary h-2 rounded-full transition-all duration-300"
style={{ width: `${systemInfo.cpu.usage_percent}%` }}
/>
</div>
</div>
</div>
</CardContent>
</Card>
{/* 内存 */}
<Card>
<CardHeader className="py-3">
<div className="flex items-center gap-2">
<Database className="w-5 h-5 text-primary" />
<CardTitle className="text-lg"></CardTitle>
</div>
</CardHeader>
<CardContent className="py-3">
<div className="space-y-3">
<div className="grid grid-cols-3 gap-2 text-center">
<div className="bg-muted/50 rounded p-2">
<div className="text-xs text-muted-foreground"></div>
<div className="font-semibold text-sm">{formatGB(systemInfo.memory.total_gb)}</div>
</div>
<div className="bg-muted/50 rounded p-2">
<div className="text-xs text-muted-foreground"></div>
<div className="font-semibold text-sm">{formatGB(systemInfo.memory.used_gb)}</div>
</div>
<div className="bg-muted/50 rounded p-2">
<div className="text-xs text-muted-foreground"></div>
<div className="font-semibold text-sm text-green-600">{formatGB(systemInfo.memory.available_gb)}</div>
</div>
</div>
<div>
<div className="flex justify-between mb-1">
<span className="text-sm text-muted-foreground">使</span>
<span className="text-sm font-medium">{systemInfo.memory.usage_percent.toFixed(1)}%</span>
</div>
<div className="w-full bg-muted rounded-full h-2">
<div
className="bg-primary h-2 rounded-full transition-all duration-300"
style={{ width: `${systemInfo.memory.usage_percent}%` }}
/>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
{/* 中下部GPU、网络 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
{/* GPU */}
<Card>
<CardHeader className="py-3">
<div className="flex items-center gap-2">
<Monitor className="w-5 h-5 text-primary" />
<CardTitle className="text-lg"> (GPU)</CardTitle>
</div>
</CardHeader>
<CardContent className="py-3">
{systemInfo.gpu.length === 0 ? (
<p className="text-sm text-muted-foreground"></p>
) : (
<div className="space-y-3">
{systemInfo.gpu.map((gpu, index) => (
<div key={index} className="border-l-2 border-primary pl-3 py-1">
<div className="text-sm font-medium mb-1">{gpu.name}</div>
<div className="flex gap-3 text-sm">
<span className="text-muted-foreground">: <span className="font-medium">{gpu.vram_gb.toFixed(1)} GB</span></span>
<span className="text-muted-foreground">: <span className="font-medium">{gpu.driver_version}</span></span>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
{/* 显示器 */}
<Card>
<CardHeader className="py-3">
<div className="flex items-center gap-2">
<Monitor className="w-5 h-5 text-primary" />
<CardTitle className="text-lg"></CardTitle>
</div>
</CardHeader>
<CardContent className="py-3">
<div className="space-y-2">
<div className="flex justify-between"><span className="text-sm text-muted-foreground"></span><span className="text-sm font-medium">{systemInfo.display.monitor_count}</span></div>
<div className="flex justify-between"><span className="text-sm text-muted-foreground"></span><span className="text-sm font-medium">{systemInfo.display.primary_resolution}</span></div>
<div className="flex justify-between"><span className="text-sm text-muted-foreground"></span><span className="text-sm font-medium">{systemInfo.display.all_resolutions.join(', ')}</span></div>
</div>
</CardContent>
</Card>
</div>
{/* 网络信息 */}
<Card className="mb-4">
<CardHeader className="py-3">
<div className="flex items-center gap-2">
<Network className="w-5 h-5 text-primary" />
<CardTitle className="text-lg"></CardTitle>
</div>
</CardHeader>
<CardContent className="py-3">
<div className="flex gap-4 mb-3">
<div className="flex-1 bg-muted/50 rounded p-2 text-center">
<div className="text-xs text-muted-foreground"></div>
<div className="font-semibold text-base text-green-600">{systemInfo.network.total_downloaded_mb.toFixed(2)} MB</div>
</div>
<div className="flex-1 bg-muted/50 rounded p-2 text-center">
<div className="text-xs text-muted-foreground"></div>
<div className="font-semibold text-base text-blue-600">{systemInfo.network.total_uploaded_mb.toFixed(2)} MB</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2">
{systemInfo.network.interfaces.map((iface, index) => (
<div key={index} className="border rounded p-2">
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-1">
<Wifi className="w-4 h-4 text-primary" />
<span className="text-sm font-medium">{iface.name}</span>
</div>
<div className="text-sm">
<span className="text-green-600">{iface.download_speed_kb.toFixed(1)}</span>
<span className="text-blue-600 ml-1">{iface.upload_speed_kb.toFixed(1)}</span>
</div>
</div>
<div className="text-sm text-muted-foreground">{iface.mac_address}</div>
{iface.ip_networks.length > 0 && (
<div className="flex flex-wrap gap-1 mt-1">
{iface.ip_networks.map((ip, ipIndex) => (
<Badge key={ipIndex} variant="outline" className="text-xs px-2 py-0 h-5">
{ip}
</Badge>
))}
</div>
)}
</div>
))}
</div>
</CardContent>
</Card>
{/* 磁盘信息 */}
<Card>
<CardHeader className="py-3">
<div className="flex items-center gap-2">
<HardDrive className="w-5 h-5 text-primary" />
<CardTitle className="text-lg"></CardTitle>
</div>
</CardHeader>
<CardContent className="py-3">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{systemInfo.disks.map((disk, index) => (
<div key={index} className="border rounded p-3">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<Badge variant="outline" className="text-sm">{disk.drive_letter}</Badge>
<span className="text-base font-medium">{disk.volume_label}</span>
</div>
<span className="text-sm text-muted-foreground">{disk.file_system}</span>
</div>
<div className="grid grid-cols-3 gap-2 mb-2 text-center">
<div>
<div className="text-xs text-muted-foreground"></div>
<div className="text-sm font-medium">{formatGB(disk.total_gb)}</div>
</div>
<div>
<div className="text-xs text-muted-foreground"></div>
<div className="text-sm font-medium">{formatGB(disk.used_gb)}</div>
</div>
<div>
<div className="text-xs text-muted-foreground"></div>
<div className="text-sm font-medium text-green-600">{formatGB(disk.available_gb)}</div>
</div>
</div>
<div>
<div className="flex justify-between mb-1">
<span className="text-sm text-muted-foreground">使</span>
<span className="text-sm font-medium">{disk.usage_percent.toFixed(1)}%</span>
</div>
<div className="w-full bg-muted rounded-full h-2">
<div
className={`h-2 rounded-full transition-all duration-300 ${
disk.usage_percent > 90 ? 'bg-red-500' : 'bg-primary'
}`}
style={{ width: `${Math.min(disk.usage_percent, 100)}%` }}
/>
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
)}
</main>
</div>
);
}

View File

@@ -0,0 +1,32 @@
import { Button } from "@/components/ui/button";
import { categories, type FeatureCategory } from "@/features/data";
import * as LucideIcons from "lucide-react";
interface CategoryFilterProps {
selectedCategory: FeatureCategory | 'all';
onCategoryChange: (category: FeatureCategory | 'all') => void;
}
export function CategoryFilter({ selectedCategory, onCategoryChange }: CategoryFilterProps) {
return (
<div className="flex gap-2 overflow-x-auto pb-2">
{categories.map((category) => {
const IconComponent = (LucideIcons as any)[category.icon] || LucideIcons.AppWindow;
const isSelected = selectedCategory === category.id;
return (
<Button
key={category.id}
variant={isSelected ? "default" : "outline"}
size="sm"
onClick={() => onCategoryChange(category.id as FeatureCategory | 'all')}
className="flex-shrink-0"
>
<IconComponent className="w-4 h-4 mr-2" />
{category.name}
</Button>
);
})}
</div>
);
}

View File

@@ -0,0 +1,55 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Feature } from "@/features/types";
import * as LucideIcons from "lucide-react";
import { useNavigate } from "react-router-dom";
interface FeatureCardProps {
feature: Feature;
}
export function FeatureCard({ feature }: FeatureCardProps) {
const navigate = useNavigate();
// 动态获取图标组件
const IconComponent = (LucideIcons as any)[feature.icon] || LucideIcons.AppWindow;
const handleClick = () => {
if (feature.implemented) {
navigate(feature.route);
}
};
return (
<Card
className={`group hover:shadow-lg transition-all duration-300 cursor-pointer ${
!feature.implemented ? 'opacity-60' : ''
}`}
onClick={handleClick}
>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-base font-semibold">
{feature.name}
</CardTitle>
<div className="w-10 h-10 bg-gradient-to-br from-primary to-primary/60 rounded-lg flex items-center justify-center group-hover:scale-110 transition-transform">
<IconComponent className="w-5 h-5 text-white" />
</div>
</CardHeader>
<CardContent>
<CardDescription className="text-sm mb-3 line-clamp-2">
{feature.description}
</CardDescription>
<div className="flex items-center justify-between">
<Badge variant={feature.implemented ? "default" : "secondary"}>
{feature.implemented ? "已实现" : "开发中"}
</Badge>
{feature.shortcut && (
<span className="text-xs text-muted-foreground bg-muted px-2 py-1 rounded">
{feature.shortcut}
</span>
)}
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,30 @@
import { FeatureCard } from "./FeatureCard";
import { Feature } from "@/features/types";
interface FeatureGridProps {
features: Feature[];
}
export function FeatureGrid({ features }: FeatureGridProps) {
if (features.length === 0) {
return (
<div className="flex flex-col items-center justify-center py-16 text-center">
<div className="w-16 h-16 bg-muted rounded-full flex items-center justify-center mb-4">
<span className="text-3xl">🔍</span>
</div>
<h3 className="text-lg font-semibold mb-2"></h3>
<p className="text-sm text-muted-foreground">
使
</p>
</div>
);
}
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{features.map((feature) => (
<FeatureCard key={feature.id} feature={feature} />
))}
</div>
);
}

View File

@@ -1,27 +0,0 @@
import { Sidebar } from "./Sidebar";
import { TopBar } from "./TopBar";
interface MainLayoutProps {
children: React.ReactNode;
}
export function MainLayout({ children }: MainLayoutProps) {
return (
<div className="flex h-screen bg-background">
{/* Sidebar */}
<Sidebar />
{/* Main Content */}
<div className="flex-1 flex flex-col overflow-hidden">
{/* Top Bar */}
<TopBar />
{/* Page Content */}
<main className="flex-1 overflow-y-auto bg-muted/20 custom-scrollbar">
<div className="p-6">
{children}
</div>
</main>
</div>
</div>
);
}

View File

@@ -1,108 +0,0 @@
import { NavLink } from "react-router-dom";
import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
import {
Home,
Star,
Palette,
Layout,
CheckCircle2,
Zap,
LucideIcon
} from "lucide-react";
interface NavigationItem {
name: string;
path: string;
icon: LucideIcon;
}
interface NavigationSection {
title: string;
items: NavigationItem[];
}
const navigationItems: NavigationSection[] = [
{
title: "Overview",
items: [
{ name: "Dashboard", path: "/", icon: Home },
{ name: "Features", path: "/features", icon: Star },
]
},
{
title: "Components",
items: [
{ name: "UI Components", path: "/components", icon: Palette },
{ name: "Layout", path: "/layout", icon: Layout },
// Removed: Forms, Navigation
]
},
// Removed: Development and Resources sections per request
];
export function Sidebar() {
return (
<aside className="w-64 bg-card border-r border-border h-screen overflow-y-auto sticky top-0 custom-scrollbar">
{/* Logo Section */}
<div className="p-6 border-b border-border">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-primary to-primary/60 rounded-xl flex items-center justify-center">
<span className="text-white font-bold text-xl">T</span>
</div>
<div>
<h1 className="font-bold text-lg">Tauri Template</h1>
<Badge variant="secondary" className="text-xs">v2.0.0</Badge>
</div>
</div>
</div>
{/* Navigation */}
<nav className="p-4 space-y-6">
{navigationItems.map((section, sectionIndex) => (
<div key={sectionIndex}>
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3 px-2">
{section.title}
</h3>
<ul className="space-y-1">
{section.items.map((item, itemIndex) => (
<li key={itemIndex}>
<NavLink
to={item.path}
className={({ isActive }) =>
`flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors border ${
isActive
? "border-2 border-black text-foreground"
: "border-transparent text-muted-foreground hover:text-foreground hover:bg-muted/40"
}`
}
>
<item.icon className="w-5 h-5" />
{item.name}
</NavLink>
</li>
))}
</ul>
{sectionIndex < navigationItems.length - 1 && (
<Separator className="my-4" />
)}
</div>
))}
</nav>
{/* Status Section */}
<div className="p-4 border-t border-border mt-auto">
<div className="space-y-3">
<div className="flex items-center gap-2 text-sm">
<CheckCircle2 className="w-4 h-4 text-green-500" />
<span className="text-muted-foreground">Ready for Development</span>
</div>
<div className="flex items-center gap-2 text-sm">
<Zap className="w-4 h-4 text-blue-500" />
<span className="text-muted-foreground">Hot Reload Active</span>
</div>
</div>
</div>
</aside>
);
}

View File

@@ -1,84 +0,0 @@
import { useLocation } from "react-router-dom";
import { Badge } from "@/components/ui/badge";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { ModeToggle } from "@/components/mode-toggle";
const getBreadcrumbs = (pathname: string) => {
const segments = pathname.split('/').filter(Boolean);
if (segments.length === 0) return [{ name: 'Dashboard', path: '/' }];
const breadcrumbs = [{ name: 'Home', path: '/' }];
let currentPath = '';
segments.forEach((segment) => {
currentPath += `/${segment}`;
const name = segment.charAt(0).toUpperCase() + segment.slice(1).replace(/-/g, ' ');
breadcrumbs.push({ name, path: currentPath });
});
return breadcrumbs;
};
export function TopBar() {
const location = useLocation();
const breadcrumbs = getBreadcrumbs(location.pathname);
return (
<header className="h-16 bg-background border-b border-border px-6 flex items-center justify-between">
{/* Breadcrumbs */}
<nav className="flex items-center space-x-2">
{breadcrumbs.map((breadcrumb, idx) => (
<div key={idx} className="flex items-center">
{idx > 0 && (
<span className="text-muted-foreground mx-2">/</span>
)}
<span className="text-sm font-medium text-muted-foreground">
{breadcrumb.name}
</span>
</div>
))}
</nav>
{/* Right Section */}
<div className="flex items-center gap-4">
{/* Status Badges */}
<div className="flex gap-2">
<Badge variant="outline" className="text-xs">
<div className="w-2 h-2 bg-green-500 rounded-full mr-2 animate-pulse"></div>
Development
</Badge>
<Badge variant="secondary" className="text-xs">
Tauri 2.0
</Badge>
</div>
{/* Actions */}
<div className="flex gap-2 items-center">
<ModeToggle />
<Button variant="outline" size="sm" className="dark:bg-transparent dark:hover:bg-accent">
<span className="mr-2">📖</span>
Docs
</Button>
<Button variant="outline" size="sm" className="dark:bg-transparent dark:hover:bg-accent">
<span className="mr-2">🔧</span>
Settings
</Button>
</div>
{/* User */}
<div className="flex items-center gap-3 pl-4 border-l border-border">
<div className="text-right">
<p className="text-sm font-medium">Developer</p>
<p className="text-xs text-muted-foreground">admin@example.com</p>
</div>
<Avatar className="h-8 w-8">
<AvatarFallback className="bg-primary/10 text-primary text-sm font-medium">
Dev
</AvatarFallback>
</Avatar>
</div>
</div>
</header>
);
}

View File

@@ -0,0 +1,78 @@
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Feature } from "@/features/types";
import * as LucideIcons from "lucide-react";
import { useNavigate } from "react-router-dom";
interface SearchResultProps {
feature: Feature;
isHighlighted?: boolean;
onClick?: () => void;
}
export function SearchResult({ feature, isHighlighted, onClick }: SearchResultProps) {
const navigate = useNavigate();
// 动态获取图标组件
const IconComponent = (LucideIcons as any)[feature.icon] || LucideIcons.AppWindow;
const handleClick = () => {
if (onClick) {
onClick();
} else if (feature.implemented) {
navigate(feature.route);
}
};
return (
<Card
className={`cursor-pointer transition-all duration-200 ${
isHighlighted
? 'bg-primary/10 border-primary'
: 'hover:bg-muted/50'
} ${!feature.implemented ? 'opacity-60' : ''}`}
onClick={handleClick}
>
<CardContent className="p-4">
<div className="flex items-start gap-3">
{/* 图标 */}
<div className="flex-shrink-0 w-12 h-12 bg-gradient-to-br from-primary to-primary/60 rounded-lg flex items-center justify-center">
<IconComponent className="w-6 h-6 text-white" />
</div>
{/* 内容 */}
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-1">
<h3 className="font-semibold text-base">{feature.name}</h3>
<Badge variant={feature.implemented ? "default" : "secondary"}>
{feature.implemented ? "可用" : "开发中"}
</Badge>
</div>
<p className="text-sm text-muted-foreground line-clamp-2 mb-2">
{feature.description}
</p>
{/* 标签和快捷键 */}
<div className="flex items-center justify-between gap-2">
<div className="flex flex-wrap gap-1">
{feature.tags.slice(0, 3).map((tag) => (
<span
key={tag}
className="text-xs text-muted-foreground bg-muted px-2 py-0.5 rounded"
>
{tag}
</span>
))}
</div>
{feature.shortcut && (
<span className="text-xs text-muted-foreground bg-muted px-2 py-1 rounded flex-shrink-0">
{feature.shortcut}
</span>
)}
</div>
</div>
</div>
</CardContent>
</Card>
);
}

201
src/features/data.ts Normal file
View File

@@ -0,0 +1,201 @@
import { Feature, FeatureCategory } from './types';
// 重新导出 FeatureCategory 类型
export type { FeatureCategory };
/**
* 功能数据列表
*/
export const featuresData: Feature[] = [
// 工具类
{
id: 'color-picker',
name: '取色器',
description: '快速拾取屏幕颜色,支持 HEX/RGB/HSL 格式',
icon: 'Droplet',
category: 'tool',
route: '/feature/color-picker',
shortcut: 'Ctrl+Alt+C',
tags: ['颜色', '取色', 'color', 'hex', 'rgb', 'hsl', '拾色器'],
implemented: true,
},
{
id: 'screenshot',
name: '截图工具',
description: '截取屏幕区域,支持延迟截图和标注',
icon: 'Camera',
category: 'tool',
route: '/feature/screenshot',
shortcut: 'Ctrl+Alt+S',
tags: ['截图', '屏幕', '捕获', 'screenshot', 'capture', '区域截图'],
implemented: false,
},
{
id: 'clipboard',
name: '剪贴板管理',
description: '管理剪贴板历史记录,支持快速粘贴',
icon: 'Clipboard',
category: 'tool',
route: '/feature/clipboard',
shortcut: 'Ctrl+Alt+V',
tags: ['剪贴板', '粘贴', '历史', 'clipboard', 'paste', 'history'],
implemented: false,
},
{
id: 'screen-ruler',
name: '屏幕标尺',
description: '测量屏幕元素尺寸,像素级精度',
icon: 'Ruler',
category: 'tool',
route: '/feature/screen-ruler',
tags: ['标尺', '测量', '尺寸', 'ruler', 'measure', 'pixel'],
implemented: false,
},
// 系统类
{
id: 'search',
name: '搜索工具',
description: '文件搜索、网页搜索、应用启动',
icon: 'Search',
category: 'system',
route: '/feature/search',
tags: ['搜索', '文件', '应用', 'search', 'file', 'launcher'],
implemented: false,
},
{
id: 'volume-control',
name: '音量控制',
description: '快速调整系统音量',
icon: 'Volume2',
category: 'system',
route: '/feature/volume-control',
tags: ['音量', '声音', 'volume', 'audio', 'sound'],
implemented: false,
},
{
id: 'brightness-control',
name: '亮度控制',
description: '调整屏幕亮度',
icon: 'Sun',
category: 'system',
route: '/feature/brightness-control',
tags: ['亮度', '屏幕', 'brightness', 'screen'],
implemented: false,
},
{
id: 'system-info',
name: '系统信息',
description: '查看系统硬件和软件信息',
icon: 'Monitor',
category: 'system',
route: '/feature/system-info',
tags: ['系统', '信息', '硬件', 'system', 'info', 'hardware'],
implemented: true,
},
// 媒体类
{
id: 'image-viewer',
name: '图片查看器',
description: '快速浏览图片文件',
icon: 'Image',
category: 'media',
route: '/feature/image-viewer',
tags: ['图片', '查看', 'image', 'viewer', 'photo'],
implemented: false,
},
{
id: 'audio-recorder',
name: '录音工具',
description: '录制音频并保存',
icon: 'Mic',
category: 'media',
route: '/feature/audio-recorder',
tags: ['录音', '音频', 'record', 'audio', 'microphone'],
implemented: false,
},
{
id: 'video-converter',
name: '视频转换',
description: '转换视频格式',
icon: 'Video',
category: 'media',
route: '/feature/video-converter',
tags: ['视频', '转换', 'video', 'converter', 'format'],
implemented: false,
},
// 开发类
{
id: 'json-formatter',
name: 'JSON 格式化',
description: '格式化和验证 JSON 数据',
icon: 'Braces',
category: 'dev',
route: '/feature/json-formatter',
tags: ['json', '格式化', '验证', 'format', 'validate'],
implemented: true,
},
{
id: 'base64-tool',
name: 'Base64 编解码',
description: 'Base64 编码和解码工具',
icon: 'FileCode',
category: 'dev',
route: '/feature/base64-tool',
tags: ['base64', '编码', '解码', 'encode', 'decode'],
implemented: false,
},
{
id: 'regex-tester',
name: '正则测试',
description: '测试和调试正则表达式',
icon: 'Regex',
category: 'dev',
route: '/feature/regex-tester',
tags: ['正则', 'regex', '表达式', 'regular', 'expression'],
implemented: false,
},
{
id: 'markdown-preview',
name: 'Markdown 预览',
description: '实时预览 Markdown 文档',
icon: 'FileText',
category: 'dev',
route: '/feature/markdown-preview',
tags: ['markdown', '预览', 'md', 'preview', '文档'],
implemented: false,
},
{
id: 'color-converter',
name: '颜色转换',
description: 'HEX/RGB/HSL 颜色格式转换',
icon: 'Palette',
category: 'dev',
route: '/feature/color-converter',
tags: ['颜色', '转换', 'hex', 'rgb', 'hsl', 'color', 'convert'],
implemented: false,
},
{
id: 'timestamp-converter',
name: '时间戳转换',
description: 'Unix 时间戳与日期时间转换',
icon: 'Clock',
category: 'dev',
route: '/feature/timestamp-converter',
tags: ['时间戳', '日期', 'timestamp', 'date', 'time', 'unix'],
implemented: false,
},
];
/**
* 分类信息
*/
export const categories = [
{ id: 'all' as const, name: '全部', icon: 'AppWindow' },
{ id: 'tool' as const, name: '工具', icon: 'Wrench' },
{ id: 'system' as const, name: '系统', icon: 'Settings' },
{ id: 'media' as const, name: '媒体', icon: 'PlayCircle' },
{ id: 'dev' as const, name: '开发', icon: 'Code' },
];

11
src/features/registry.ts Normal file
View File

@@ -0,0 +1,11 @@
/**
* 功能注册表
* 导出所有可用功能
*/
export { featuresData, categories } from './data';
export type { Feature, FeatureCategory } from './types';
// 重新导出功能列表,方便使用
import { featuresData } from './data';
export const features = featuresData;

28
src/features/types.ts Normal file
View File

@@ -0,0 +1,28 @@
/**
* 功能分类
*/
export type FeatureCategory = 'tool' | 'system' | 'media' | 'dev';
/**
* 功能接口
*/
export interface Feature {
/** 功能唯一标识 */
id: string;
/** 功能名称 */
name: string;
/** 功能描述 */
description: string;
/** 图标名称lucide-react */
icon: string;
/** 功能分类 */
category: FeatureCategory;
/** 路由路径 */
route: string;
/** 快捷键(可选) */
shortcut?: string;
/** 搜索标签 */
tags: string[];
/** 是否已实现 */
implemented: boolean;
}

View File

@@ -1,10 +1,13 @@
import React from "react"; import React from "react";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App"; import App from "./App";
import "./index.css"; import "./index.css";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode> <React.StrictMode>
<App /> <BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>, </React.StrictMode>,
); );

View File

@@ -1,342 +0,0 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Separator } from "@/components/ui/separator";
import { Input } from "@/components/ui/input";
import { ModeToggle } from "@/components/mode-toggle";
const componentCategories = [
{
id: "buttons",
name: "Buttons",
description: "Interactive button components with various styles",
icon: "🔘"
},
{
id: "badges",
name: "Badges",
description: "Status indicators and labels",
icon: "🏷️"
},
{
id: "cards",
name: "Cards",
description: "Content containers with headers and actions",
icon: "🃏"
},
{
id: "forms",
name: "Forms",
description: "Input fields and form controls",
icon: "📝"
}
];
export function Components() {
return (
<div className="space-y-8">
{/* Header */}
<div className="text-center space-y-4">
<div className="flex justify-between items-center">
<div></div>
<ModeToggle />
</div>
<h1 className="text-4xl font-bold text-foreground">UI Components</h1>
<p className="text-xl text-muted-foreground max-w-3xl mx-auto">
Explore the beautiful and accessible components available in this template.
Each component is built with accessibility and customization in mind.
</p>
</div>
{/* Component Categories */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{componentCategories.map((category) => (
<Card key={category.id} className="text-center hover:shadow-lg transition-shadow cursor-pointer">
<CardHeader className="pb-3">
<div className="w-12 h-12 bg-primary/10 rounded-xl flex items-center justify-center mx-auto mb-3">
<span className="text-2xl">{category.icon}</span>
</div>
<CardTitle className="text-lg">{category.name}</CardTitle>
<CardDescription className="text-sm">
{category.description}
</CardDescription>
</CardHeader>
</Card>
))}
</div>
{/* Component Showcase */}
<Card>
<CardHeader>
<CardTitle className="text-2xl flex items-center gap-2">
<span>🎨</span>
Component Showcase
</CardTitle>
<CardDescription>
Interactive examples of all available components
</CardDescription>
</CardHeader>
<CardContent>
<Tabs defaultValue="buttons" className="w-full">
<TabsList className="grid w-full grid-cols-4 mb-8">
<TabsTrigger value="buttons">Buttons</TabsTrigger>
<TabsTrigger value="badges">Badges</TabsTrigger>
<TabsTrigger value="cards">Cards</TabsTrigger>
<TabsTrigger value="forms">Forms</TabsTrigger>
</TabsList>
<TabsContent value="buttons" className="space-y-8">
{/* Button Variants */}
<div className="space-y-4">
<h3 className="text-lg font-semibold">Button Variants</h3>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
<div className="space-y-2">
<Button className="w-full">Default</Button>
<p className="text-xs text-center text-muted-foreground">Default</p>
</div>
<div className="space-y-2">
<Button variant="secondary" className="w-full">
Secondary
</Button>
<p className="text-xs text-center text-muted-foreground">Secondary</p>
</div>
<div className="space-y-2">
<Button variant="destructive" className="w-full">
Destructive
</Button>
<p className="text-xs text-center text-muted-foreground">Destructive</p>
</div>
<div className="space-y-2">
<Button variant="outline" className="w-full">
Outline
</Button>
<p className="text-xs text-center text-muted-foreground">Outline</p>
</div>
<div className="space-y-2">
<Button variant="ghost" className="w-full">
Ghost
</Button>
<p className="text-xs text-center text-muted-foreground">Ghost</p>
</div>
<div className="space-y-2">
<Button variant="link" className="w-full">
Link
</Button>
<p className="text-xs text-center text-muted-foreground">Link</p>
</div>
</div>
</div>
<Separator />
{/* Button Sizes */}
<div className="space-y-4">
<h3 className="text-lg font-semibold">Button Sizes</h3>
<div className="flex flex-wrap gap-4 items-center">
<div className="space-y-2">
<Button size="sm">Small</Button>
<p className="text-xs text-center text-muted-foreground">Small</p>
</div>
<div className="space-y-2">
<Button>Default</Button>
<p className="text-xs text-center text-muted-foreground">Default</p>
</div>
<div className="space-y-2">
<Button size="lg">Large</Button>
<p className="text-xs text-center text-muted-foreground">Large</p>
</div>
<div className="space-y-2">
<Button size="icon">
🔍
</Button>
<p className="text-xs text-center text-muted-foreground">Icon</p>
</div>
</div>
</div>
<Separator />
{/* Interactive Button Demo */}
<div className="space-y-4">
<h3 className="text-lg font-semibold">Interactive Demo</h3>
<div className="p-4 border rounded-lg bg-card">
<div className="flex flex-wrap gap-4 items-center justify-center">
<Button
variant="outline"
onClick={() => alert('Outline button clicked!')}
>
Click Me!
</Button>
<Button
variant="secondary"
onClick={() => alert('Secondary button clicked!')}
>
Try Me!
</Button>
<Button
variant="destructive"
onClick={() => alert('Destructive button clicked!')}
>
Danger!
</Button>
</div>
</div>
</div>
<Separator />
{/* Theme Color Demo */}
<div className="space-y-4">
<h3 className="text-lg font-semibold">Theme Colors Demo</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="p-4 rounded-lg border bg-card text-card-foreground">
<h4 className="font-medium mb-2">Card Background</h4>
<p className="text-sm text-muted-foreground">
This card uses the card background and foreground colors
</p>
</div>
<div className="p-4 rounded-lg border bg-muted text-muted-foreground">
<h4 className="font-medium mb-2">Muted Background</h4>
<p className="text-sm">
This card uses the muted background and foreground colors
</p>
</div>
</div>
</div>
</TabsContent>
<TabsContent value="badges" className="space-y-8">
{/* Badge Variants */}
<div className="space-y-4">
<h3 className="text-lg font-semibold">Badge Variants</h3>
<div className="flex flex-wrap gap-4">
<div className="space-y-2">
<Badge variant="default">
Default
</Badge>
<p className="text-xs text-center text-muted-foreground">Default</p>
</div>
<div className="space-y-2">
<Badge variant="secondary">
Secondary
</Badge>
<p className="text-xs text-center text-muted-foreground">Secondary</p>
</div>
<div className="space-y-2">
<Badge variant="destructive">
Destructive
</Badge>
<p className="text-xs text-center text-muted-foreground">Destructive</p>
</div>
<div className="space-y-2">
<Badge variant="outline">
Outline
</Badge>
<p className="text-xs text-center text-muted-foreground">Outline</p>
</div>
</div>
</div>
<Separator />
{/* Custom Badges */}
<div className="space-y-4">
<h3 className="text-lg font-semibold">Custom Badges</h3>
<div className="flex flex-wrap gap-4">
<Badge className="bg-green-500 hover:bg-green-600">
Success
</Badge>
<Badge className="bg-yellow-500 hover:bg-yellow-600">
Warning
</Badge>
<Badge className="bg-blue-500 hover:bg-blue-600">
Info
</Badge>
<Badge className="bg-purple-500 hover:bg-purple-600">
🔮 New
</Badge>
</div>
</div>
</TabsContent>
<TabsContent value="cards" className="space-y-8">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Basic Card */}
<Card>
<CardHeader>
<CardTitle>Basic Card</CardTitle>
<CardDescription>
A simple card with header and content
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
This is a basic card component that demonstrates the standard layout.
</p>
</CardContent>
</Card>
{/* Interactive Card */}
<Card>
<CardHeader>
<CardTitle>Interactive Card</CardTitle>
<CardDescription>
Card with interactive elements
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Button className="w-full">Action Button</Button>
<div className="flex gap-2">
<Badge variant="outline">
Tag 1
</Badge>
<Badge variant="outline">
Tag 2
</Badge>
</div>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="forms" className="space-y-8">
<div className="max-w-2xl space-y-6">
<h3 className="text-lg font-semibold">Form Elements</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium">Email Address</label>
<Input
type="email"
placeholder="Enter your email"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Password</label>
<Input
type="password"
placeholder="Enter your password"
/>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Message</label>
<textarea
placeholder="Enter your message"
rows={4}
className="w-full px-3 py-2 border border-input bg-background rounded-md focus:outline-none focus:ring-2 focus:ring-ring resize-none dark:border-input dark:bg-background dark:focus:ring-ring"
/>
</div>
<Button className="w-full">Submit Form</Button>
</div>
</TabsContent>
</Tabs>
</CardContent>
</Card>
</div>
);
}

View File

@@ -1,212 +0,0 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Palette,
Star,
BookOpen,
Zap,
BarChart3,
Plus,
Settings,
Play,
LucideIcon
} from "lucide-react";
interface Stat {
title: string;
value: string;
description: string;
icon: LucideIcon;
color: string;
}
const stats: Stat[] = [
{
title: "Total Components",
value: "24",
description: "Available UI components",
icon: Palette,
color: "bg-primary"
},
{
title: "Features",
value: "12",
description: "Core functionality",
icon: Star,
color: "bg-secondary"
},
{
title: "Documentation",
value: "100%",
description: "Complete coverage",
icon: BookOpen,
color: "bg-primary"
},
{
title: "Performance",
value: "A+",
description: "Lighthouse score",
icon: Zap,
color: "bg-secondary"
}
];
const recentActivity = [
{ action: "Component added", component: "DataTable", time: "2 minutes ago", status: "success" },
{ action: "Feature updated", component: "Navigation", time: "1 hour ago", status: "info" },
{ action: "Bug fixed", component: "Form validation", time: "3 hours ago", status: "success" },
{ action: "Documentation", component: "API Reference", time: "1 day ago", status: "warning" }
];
export function Dashboard() {
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-foreground">Dashboard</h1>
<p className="text-muted-foreground">Welcome to your Tauri development workspace</p>
</div>
<div className="flex gap-3">
<Button variant="outline">
<BarChart3 className="w-4 h-4 mr-2" />
View Reports
</Button>
<Button>
<Plus className="w-4 h-4 mr-2" />
New Project
</Button>
</div>
</div>
{/* Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{stats.map((stat, index) => (
<Card key={index} className="group hover:shadow-lg transition-all duration-300">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
{stat.title}
</CardTitle>
<div className={`w-8 h-8 bg-gradient-to-br ${stat.color} rounded-lg flex items-center justify-center group-hover:scale-110 transition-transform`}>
<stat.icon className="w-5 h-5 text-white" />
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-foreground">{stat.value}</div>
<p className="text-xs text-muted-foreground">{stat.description}</p>
</CardContent>
</Card>
))}
</div>
{/* Main Content Grid */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Quick Actions */}
<Card className="lg:col-span-1">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Zap className="w-5 h-5" />
Quick Actions
</CardTitle>
<CardDescription>Common development tasks</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<Button variant="outline" className="w-full justify-start">
<Play className="w-4 h-4 mr-2" />
Run Development Server
</Button>
<Button variant="outline" className="w-full justify-start">
<span className="mr-2">📦</span>
Build Application
</Button>
<Button variant="outline" className="w-full justify-start">
<span className="mr-2">🧪</span>
Run Tests
</Button>
<Button variant="outline" className="w-full justify-start">
<span className="mr-2">📖</span>
View Documentation
</Button>
</CardContent>
</Card>
{/* Recent Activity */}
<Card className="lg:col-span-2">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<span>📋</span>
Recent Activity
</CardTitle>
<CardDescription>Latest development updates</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{recentActivity.map((activity, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-muted/50 rounded-lg">
<div className="flex items-center gap-3">
<div className="w-2 h-2 bg-primary rounded-full"></div>
<div>
<p className="text-sm font-medium">{activity.action}</p>
<p className="text-xs text-muted-foreground">{activity.component}</p>
</div>
</div>
<div className="text-right">
<p className="text-xs text-muted-foreground">{activity.time}</p>
<Badge className="text-xs border border-input bg-background hover:bg-accent hover:text-accent-foreground">
{activity.status}
</Badge>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
{/* Project Status */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<span>📊</span>
Project Status
</CardTitle>
<CardDescription>Current development progress</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Frontend</span>
<Badge className="bg-primary text-primary-foreground hover:bg-primary/80">Complete</Badge>
</div>
<div className="w-full bg-muted rounded-full h-2">
<div className="bg-primary h-2 rounded-full" style={{ width: '100%' }}></div>
</div>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Backend</span>
<Badge className="bg-secondary text-secondary-foreground hover:bg-secondary/80">In Progress</Badge>
</div>
<div className="w-full bg-muted rounded-full h-2">
<div className="bg-secondary h-2 rounded-full" style={{ width: '75%' }}></div>
</div>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Testing</span>
<Badge className="border border-input bg-background hover:bg-accent hover:text-accent-foreground">Pending</Badge>
</div>
<div className="w-full bg-muted rounded-full h-2">
<div className="bg-muted-foreground h-2 rounded-full" style={{ width: '25%' }}></div>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
);
}

View File

@@ -1,202 +0,0 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
import {
Palette,
Rainbow,
FileText,
Zap,
Settings,
Smartphone,
Moon,
Accessibility,
LucideIcon
} from "lucide-react";
interface Feature {
name: string;
description: string;
icon: LucideIcon;
status: string;
color: string;
details: string[];
}
interface FeatureCategory {
category: string;
items: Feature[];
}
const features: FeatureCategory[] = [
{
category: "Core Framework",
items: [
{
name: "Tauri 2.0",
description: "Cross-platform desktop applications with Rust backend",
icon: Zap,
status: "Latest",
color: "from-blue-500 to-cyan-500",
details: ["Rust backend", "Cross-platform", "Small bundle size", "Native performance"]
},
{
name: "React 19",
description: "Latest React with modern features and performance",
icon: FileText,
status: "New",
color: "from-purple-500 to-pink-500",
details: ["Concurrent features", "Server components", "Improved performance", "Modern hooks"]
}
]
},
{
category: "UI & Design",
items: [
{
name: "shadcn/ui",
description: "Beautiful, accessible components built with Radix UI",
icon: Palette,
status: "Premium",
color: "from-emerald-500 to-teal-500",
details: ["Accessible", "Customizable", "Type-safe", "Modern design"]
},
{
name: "Tailwind CSS 4.0",
description: "Utility-first CSS framework with modern features",
icon: Rainbow,
status: "Latest",
color: "from-indigo-500 to-purple-500",
details: ["Utility-first", "Responsive", "Dark mode", "CSS variables"]
}
]
},
{
category: "Development Tools",
items: [
{
name: "TypeScript",
description: "Full type safety and modern development experience",
icon: FileText,
status: "Stable",
color: "from-orange-500 to-red-500",
details: ["Type safety", "Modern ES features", "IntelliSense", "Error prevention"]
},
{
name: "Vite",
description: "Lightning fast build tool and development server",
icon: Zap,
status: "Fast",
color: "from-green-500 to-emerald-500",
details: ["Fast HMR", "ES modules", "Plugin system", "Optimized builds"]
}
]
}
];
export function Features() {
return (
<div className="space-y-8">
{/* Header */}
<div className="text-center space-y-4">
<h1 className="text-4xl font-bold text-foreground">Features</h1>
<p className="text-xl text-muted-foreground max-w-3xl mx-auto">
Discover the powerful features that make this template the perfect starting point
for your next desktop application
</p>
</div>
{/* Features by Category */}
{features.map((category, categoryIndex) => (
<div key={categoryIndex} className="space-y-6">
<div className="text-center">
<h2 className="text-2xl font-semibold text-foreground mb-2">
{category.category}
</h2>
<Separator className="w-24 mx-auto" />
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{category.items.map((feature, featureIndex) => (
<Card key={featureIndex} className="group hover:shadow-lg transition-all duration-300 hover:-translate-y-1">
<CardHeader>
<div className="flex items-start justify-between">
<div className="flex items-center gap-4">
<div className={`w-16 h-16 bg-gradient-to-br ${feature.color} rounded-2xl flex items-center justify-center group-hover:scale-110 transition-transform duration-300`}>
<feature.icon className="w-8 h-8 text-white" />
</div>
<div>
<div className="flex items-center gap-2 mb-2">
<CardTitle className="text-xl">{feature.name}</CardTitle>
<Badge className="text-xs">
{feature.status}
</Badge>
</div>
<CardDescription className="text-base">
{feature.description}
</CardDescription>
</div>
</div>
</div>
</CardHeader>
<CardContent>
<div className="space-y-3">
<h4 className="font-medium text-sm text-muted-foreground">Key Benefits:</h4>
<ul className="space-y-2">
{feature.details.map((detail, detailIndex) => (
<li key={detailIndex} className="flex items-center gap-2 text-sm">
<div className="w-1.5 h-1.5 bg-primary rounded-full"></div>
{detail}
</li>
))}
</ul>
</div>
</CardContent>
</Card>
))}
</div>
</div>
))}
{/* Additional Features */}
<Card className="bg-gradient-to-r from-primary/5 to-primary/10 border-primary/20">
<CardHeader className="text-center">
<CardTitle className="text-2xl flex items-center justify-center gap-2">
<Settings className="w-6 h-6" />
Additional Features
</CardTitle>
<CardDescription>
More tools and capabilities to enhance your development experience
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="text-center space-y-2">
<div className="w-12 h-12 bg-blue-500/20 rounded-lg flex items-center justify-center mx-auto">
<Smartphone className="w-6 h-6 text-blue-600" />
</div>
<h4 className="font-medium">Responsive Design</h4>
<p className="text-sm text-muted-foreground">Works on all screen sizes</p>
</div>
<div className="text-center space-y-2">
<div className="w-12 h-12 bg-green-500/20 rounded-lg flex items-center justify-center mx-auto">
<Moon className="w-6 h-6 text-green-600" />
</div>
<h4 className="font-medium">Dark Mode</h4>
<p className="text-sm text-muted-foreground">Built-in theme support</p>
</div>
<div className="text-center space-y-2">
<div className="w-12 h-12 bg-purple-500/20 rounded-lg flex items-center justify-center mx-auto">
<Accessibility className="w-6 h-6 text-purple-600" />
</div>
<h4 className="font-medium">Accessibility</h4>
<p className="text-sm text-muted-foreground">WCAG compliant</p>
</div>
</div>
</CardContent>
</Card>
</div>
);
}

105
src/pages/Home.tsx Normal file
View File

@@ -0,0 +1,105 @@
import { useState, useMemo } from "react";
import { useNavigate } from "react-router-dom";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Search, Settings } from "lucide-react";
import { FeatureGrid } from "@/components/home/FeatureGrid";
import { CategoryFilter } from "@/components/home/CategoryFilter";
import { features, type FeatureCategory } from "@/features/registry";
export function Home() {
const navigate = useNavigate();
const [selectedCategory, setSelectedCategory] = useState<FeatureCategory | 'all'>('all');
const [searchQuery, setSearchQuery] = useState('');
// 过滤和搜索功能
const filteredFeatures = useMemo(() => {
let result = features;
// 分类过滤
if (selectedCategory !== 'all') {
result = result.filter(f => f.category === selectedCategory);
}
// 搜索过滤
if (searchQuery.trim()) {
const query = searchQuery.toLowerCase();
result = result.filter(f =>
f.name.toLowerCase().includes(query) ||
f.description.toLowerCase().includes(query) ||
f.tags.some(tag => tag.toLowerCase().includes(query))
);
}
return result;
}, [selectedCategory, searchQuery]);
return (
<div className="min-h-screen bg-background">
{/* 顶部导航栏 */}
<header className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 sticky top-0 z-50">
<div className="container mx-auto px-4 py-4">
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-2">
<span className="text-2xl"></span>
<h1 className="text-2xl font-bold">CmdRs</h1>
<span className="text-sm text-muted-foreground"></span>
</div>
<div className="flex items-center gap-3">
<Button
variant="outline"
size="sm"
onClick={() => navigate('/search')}
className="hidden sm:flex"
>
<Search className="w-4 h-4 mr-2" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => navigate('/settings')}
>
<Settings className="w-4 h-4" />
</Button>
</div>
</div>
{/* 搜索框 */}
<div className="mt-4">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
type="text"
placeholder="搜索功能..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10"
/>
</div>
</div>
</div>
</header>
{/* 主内容区 */}
<main className="container mx-auto px-4 py-6">
{/* 分类筛选 */}
<div className="mb-6">
<CategoryFilter
selectedCategory={selectedCategory}
onCategoryChange={setSelectedCategory}
/>
</div>
{/* 功能网格 */}
<div className="mb-4">
<h2 className="text-lg font-semibold mb-4">
<span className="text-muted-foreground font-normal">({filteredFeatures.length})</span>
</h2>
</div>
<FeatureGrid features={filteredFeatures} />
</main>
</div>
);
}

156
src/pages/Search.tsx Normal file
View File

@@ -0,0 +1,156 @@
import { useState, useEffect, useMemo, useCallback } from "react";
import { useNavigate } from "react-router-dom";
import Fuse from "fuse.js";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { ArrowLeft, Keyboard } from "lucide-react";
import { SearchResult } from "@/components/search/SearchResult";
import { features } from "@/features/registry";
export function Search() {
const navigate = useNavigate();
const [query, setQuery] = useState("");
const [selectedIndex, setSelectedIndex] = useState(0);
// 使用 Fuse.js 进行模糊搜索
const fuse = useMemo(() => {
return new Fuse(features, {
keys: [
{ name: 'name', weight: 2 },
{ name: 'description', weight: 1.5 },
{ name: 'tags', weight: 1 },
],
threshold: 0.4,
ignoreLocation: true,
});
}, []);
// 搜索结果
const searchResults = useMemo(() => {
if (!query.trim()) {
return features;
}
return fuse.search(query).map(result => result.item);
}, [query, fuse]);
// 键盘导航
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'ArrowDown') {
e.preventDefault();
setSelectedIndex((prev) =>
prev < searchResults.length - 1 ? prev + 1 : prev
);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : 0));
} else if (e.key === 'Enter' && searchResults.length > 0) {
e.preventDefault();
const selected = searchResults[selectedIndex];
if (selected.implemented) {
navigate(selected.route);
}
} else if (e.key === 'Escape') {
navigate('/');
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [searchResults, selectedIndex, navigate]);
// 重置选中索引
useEffect(() => {
setSelectedIndex(0);
}, [query]);
const handleResultClick = useCallback((index: number) => {
setSelectedIndex(index);
}, []);
return (
<div className="min-h-screen bg-background">
{/* 顶部导航栏 */}
<header className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 sticky top-0 z-50">
<div className="container mx-auto px-4 py-4">
<div className="flex items-center gap-3 mb-4">
<Button
variant="ghost"
size="sm"
onClick={() => navigate('/')}
>
<ArrowLeft className="w-4 h-4" />
</Button>
<h1 className="text-xl font-bold"></h1>
</div>
{/* 大搜索框 */}
<div className="relative">
<Keyboard className="absolute left-4 top-1/2 transform -translate-y-1/2 w-5 h-5 text-muted-foreground" />
<Input
type="text"
placeholder="输入关键词搜索功能..."
value={query}
onChange={(e) => setQuery(e.target.value)}
className="pl-12 h-14 text-lg"
autoFocus
/>
</div>
</div>
</header>
{/* 主内容区 */}
<main className="container mx-auto px-4 py-6">
{/* 结果统计 */}
<div className="mb-4">
<p className="text-sm text-muted-foreground">
{query.trim() ? (
<>
<span className="font-semibold text-foreground">{searchResults.length}</span>
</>
) : (
<>
<span className="font-semibold text-foreground">{features.length}</span>
</>
)}
</p>
</div>
{/* 搜索结果列表 */}
<div className="space-y-3 max-h-[calc(100vh-250px)] overflow-y-auto">
{searchResults.length > 0 ? (
searchResults.map((feature, index) => (
<SearchResult
key={feature.id}
feature={feature}
isHighlighted={index === selectedIndex}
onClick={() => handleResultClick(index)}
/>
))
) : (
<div className="flex flex-col items-center justify-center py-16 text-center">
<div className="w-16 h-16 bg-muted rounded-full flex items-center justify-center mb-4">
<span className="text-3xl">🔍</span>
</div>
<h3 className="text-lg font-semibold mb-2"></h3>
<p className="text-sm text-muted-foreground">
使
</p>
</div>
)}
</div>
{/* 键盘提示 */}
{searchResults.length > 0 && (
<div className="mt-4 p-3 bg-muted/50 rounded-lg">
<p className="text-xs text-muted-foreground text-center">
<span className="font-semibold"></span>
<span className="font-semibold ml-2">Enter</span>
<span className="font-semibold ml-2">Esc</span>
</p>
</div>
)}
</main>
</div>
);
}

16
src/pages/Settings.tsx Normal file
View File

@@ -0,0 +1,16 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
export default function Settings() {
return (
<div className="container mx-auto p-6">
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<p className="text-muted-foreground">...</p>
</CardContent>
</Card>
</div>
);
}

View File

@@ -16,6 +16,16 @@ export default defineConfig(async () => ({
}, },
}, },
// 取色器遮罩是一个独立 HTML 入口Tauri 子窗口加载)
build: {
rollupOptions: {
input: {
main: path.resolve(__dirname, "index.html"),
picker: path.resolve(__dirname, "picker.html"),
},
},
},
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
// //
// 1. prevent Vite from obscuring rust errors // 1. prevent Vite from obscuring rust errors