diff --git a/docs/Tauri_Naming_Conventions.md b/docs/Tauri_Naming_Conventions.md new file mode 100644 index 0000000..33d4ef8 --- /dev/null +++ b/docs/Tauri_Naming_Conventions.md @@ -0,0 +1,270 @@ +# Tauri 命名规范文档 + +## 概述 + +本文档定义了 SSH Terminal 项目中 Tauri 应用 Rust 端和前端(TypeScript/JavaScript)之间的数据类型命名规范。通过遵循这些规范,可以确保跨语言的类型安全性和一致性。 + +## 核心原则 + +### 1. 各自遵循语言规范 +- **Rust 端**: 遵循 Rust 命名规范(snake_case 变量/函数,PascalCase 类型/枚举) +- **前端**: 遵循 TypeScript 命名规范(camelCase 变量/属性,PascalCase 类型) +- **通过 serde 自动转换**: 使用 `serde` 的重命名功能自动处理转换 + +### 2. 类型名称保持一致 +- **Rust 端**: PascalCase(如 `SessionConfig`, `AuthMethod`) +- **前端**: PascalCase(如 `SessionConfig`, `AuthMethod`) +- 类型名称在两端保持一致 + +## 详细规范 + +### Struct 字段命名 + +#### Rust 端规范 +- 使用 **snake_case** 命名 +- 添加 `#[serde(rename_all = "camelCase")]` 注解自动转换为 camelCase + +```rust +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionConfig { + pub host: String, // Rust: snake_case + pub port: u16, + pub user_name: String, // Rust: snake_case + pub private_key_path: String, // Rust: snake_case + pub auth_method: AuthMethod, +} +``` + +#### 前端规范 +- 使用 **camelCase** 命名 +- 与 serde 自动转换后的名称一致 + +```typescript +export interface SessionConfig { + host: string; // TS: camelCase + port: number; + userName: string; // TS: camelCase + privateKeyPath: string; // TS: camelCase + authMethod: AuthMethod; +} +``` + +### Enum 变体命名 + +#### Rust 端规范 +- 使用 **PascalCase** 命名 +- 添加 `#[serde(rename_all = "camelCase")]` 注解自动转换为 camelCase + +```rust +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum AuthMethod { + Password { password: String }, // PascalCase + PublicKey { privateKeyPath: String, passphrase: Option }, +} +``` + +#### 前端规范 +- 使用 **PascalCase** 命名(Discriminated Union) +- 与 serde 自动转换后的变体名称一致 + +```typescript +export type AuthMethod = + | { Password: { password: string } } // PascalCase + | { PublicKey: { privateKeyPath: string; passphrase?: string } }; +``` + +### 字段命名对照表 + +| Rust 端 (snake_case) | 前端 (camelCase) | 示例用途 | +|---------------------|------------------|---------| +| `user_name` | `userName` | 用户名 | +| `private_key_path` | `privateKeyPath` | 私钥路径 | +| `auth_method` | `authMethod` | 认证方法 | +| `connection_id` | `connectionId` | 连接 ID | +| `terminal_type` | `terminalType` | 终端类型 | +| `keep_alive_interval` | `keepAliveInterval` | 保活间隔 | +| `strict_host_key_checking` | `strictHostKeyChecking` | 主机密钥检查 | +| `video_file` | `videoFile` | 视频文件 | + +## 特殊情况处理 + +### 1. 保留字段原名 +如果某些字段需要保持原名(通常是已经是 camelCase 的字段),可以使用 `#[serde(skip_serializing_if = "Option::is_none")]` 或单独注解: + +```rust +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionConfig { + pub name: String, // name -> name (保持不变) + #[serde(rename = "id")] // 显式重命名 + pub session_id: String, +} +``` + +### 2. 单词分隔符 +- **snake_case**: 使用下划线 `_` 分隔 +- **camelCase**: 使用首字母大写分隔 + +```rust +// Rust: strict_host_key_checking +// TS: strictHostKeyChecking +``` + +### 3. 缩写处理 +- 缩写词保持原始形式(如 `ID`, `URL`, `SSH`) +- 不要将缩写词转换为小写 + +```rust +pub connection_id: String, // NOT connection_i_d +// TS: connectionId // NOT connectionId +``` + +## 实现示例 + +### Rust 端完整示例 + +```rust +use serde::{Serialize, Deserialize}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct SessionConfig { + pub id: Option, + pub name: String, + pub host: String, + pub port: u16, + pub username: String, + pub authMethod: AuthMethod, + pub terminalType: Option, + pub columns: Option, + pub rows: Option, + #[serde(default = "default_strict_host_key_checking")] + pub strictHostKeyChecking: bool, + #[serde(default = "default_group")] + pub group: String, + #[serde(default = "default_keep_alive_interval")] + pub keepAliveInterval: u64, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub enum AuthMethod { + Password { password: String }, + PublicKey { privateKeyPath: String, passphrase: Option }, +} +``` + +### 前端完整示例 + +```typescript +export interface SessionConfig { + id?: string; + name: string; + host: string; + port: number; + username: string; + authMethod: AuthMethod; + terminalType?: string; + columns?: number; + rows?: number; + strictHostKeyChecking?: boolean; + group?: string; + keepAliveInterval?: number; +} + +export type AuthMethod = + | { Password: { password: string } } + | { PublicKey: { privateKeyPath: string; passphrase?: string } }; +``` + +## 验证和测试 + +### 1. Rust 端验证 +```bash +cd src-tauri +cargo check +cargo clippy +``` + +### 2. 前端验证 +```bash +pnpm tsc --noEmit +``` + +### 3. 交叉验证 +- 测试 Rust 序列化到 JSON +- 验证前端反序列化是否正确 +- 检查字段名是否匹配 + +## 常见错误 + +### 错误 1: 未添加 serde 注解 +```rust +// ❌ 错误 +pub struct SessionConfig { + pub user_name: String, // 序列化为 user_name (snake_case) +} + +// ✅ 正确 +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionConfig { + pub user_name: String, // 序列化为 userName (camelCase) +} +``` + +### 错误 2: 前端类型不匹配 +```typescript +// ❌ 错误 +export interface SessionConfig { + user_name: string; // 应该是 userName +} + +// ✅ 正确 +export interface SessionConfig { + userName: string; // 与 Rust 端序列化后的名称一致 +} +``` + +### 错误 3: Enum 变体命名不一致 +```rust +// ❌ 错误 +pub enum AuthMethod { + Password, // 序列化为 "Password" (PascalCase) + PublicKey, +} + +// ✅ 正确 +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum AuthMethod { + Password, // 序列化为 "password" (camelCase) + PublicKey, // 序列化为 "publicKey" (camelCase) +} +``` + +## 工具和辅助 + +### 自动检查脚本 +可以创建一个脚本来检查 Rust 和前端类型定义的一致性: + +```bash +# 检查 Rust 端是否缺少 serde 注解 +grep -r "pub struct" src-tauri/src | grep -v "#\[serde" + +# 检查前端类型定义 +grep -r "export interface" src/types +``` + +## 相关资源 + +- [serde 文档](https://serde.rs/) +- [Tauri 文档](https://tauri.app/) +- [Rust 命名规范](https://rust-lang.github.io/api-guidelines/naming.html) +- [TypeScript 命名规范](https://typescript-eslint.io/rules/naming-convention/) + +## 更新日志 + +- **2026-01-29**: 初始版本,定义 Tauri 项目命名规范 diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index e95962e..47854b3 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -860,6 +860,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ "bitflags 2.10.0", + "block2", + "libc", "objc2", ] @@ -3574,6 +3576,30 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + [[package]] name = "rgb" version = "0.8.52" @@ -4224,6 +4250,7 @@ dependencies = [ "sysinfo", "tauri", "tauri-build", + "tauri-plugin-dialog", "tauri-plugin-global-shortcut", "tauri-plugin-opener", "windows 0.58.0", @@ -4310,6 +4337,46 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tauri-plugin-dialog" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9204b425d9be8d12aa60c2a83a289cf7d1caae40f57f336ed1155b3a5c0e359b" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.18", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed390cc669f937afeb8b28032ce837bac8ea023d975a2e207375ec05afaf1804" +dependencies = [ + "anyhow", + "dunce", + "glob", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.18", + "toml 0.9.11+spec-1.1.0", + "url", +] + [[package]] name = "tauri-plugin-global-shortcut" version = "2.3.1" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 84d2460..ff736be 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -21,6 +21,7 @@ tauri-build = { version = "2.4", features = [] } tauri = { version = "2.4", features = [] } tauri-plugin-opener = "2.5" tauri-plugin-global-shortcut = "2" +tauri-plugin-dialog = "2" serde = { version = "1", features = ["derive"] } serde_json = { version = "1", features = ["preserve_order"] } sysinfo = "0.30" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index ab10709..04122ac 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -10,6 +10,8 @@ "core:window:allow-close", "core:window:allow-set-focus", "core:window:allow-is-visible", - "opener:default" + "opener:default", + "dialog:allow-save", + "dialog:allow-open" ] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ec3df1f..390052d 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -22,6 +22,7 @@ pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_global_shortcut::Builder::new().build()) + .plugin(tauri_plugin_dialog::init()) .setup(|app| { // 预热取色器窗口:避免第一次取色出现“白屏闪一下” // 窗口会以 hidden 状态创建,不会影响用户体验 diff --git a/src-tauri/src/models/color.rs b/src-tauri/src/models/color.rs index b05c295..b40ed4c 100644 --- a/src-tauri/src/models/color.rs +++ b/src-tauri/src/models/color.rs @@ -9,6 +9,7 @@ use crate::utils::color_conversion; /// /// 包含颜色的完整信息,支持多种颜色格式和屏幕坐标 #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct ColorInfo { /// 十六进制颜色值(格式:#RRGGBB) pub hex: String, @@ -62,6 +63,7 @@ impl ColorInfo { /// /// 表示 RGB 颜色模式的颜色值 #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct RgbInfo { /// 红色分量 (0-255) pub r: u8, @@ -75,6 +77,7 @@ pub struct RgbInfo { /// /// 表示 HSL 颜色模式的颜色值 #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct HslInfo { /// 色相 (0-360) /// diff --git a/src-tauri/src/models/json_format.rs b/src-tauri/src/models/json_format.rs index acb8f91..19a43fe 100644 --- a/src-tauri/src/models/json_format.rs +++ b/src-tauri/src/models/json_format.rs @@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize}; /// JSON 格式化配置 #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct JsonFormatConfig { /// 缩进空格数(默认 2) #[serde(default = "default_indent")] @@ -54,6 +55,7 @@ impl Default for JsonFormatConfig { /// JSON 格式化结果 #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct JsonFormatResult { /// 是否成功 pub success: bool, @@ -70,6 +72,7 @@ pub struct JsonFormatResult { /// JSON 验证结果 #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct JsonValidateResult { /// 是否有效的 JSON pub is_valid: bool, diff --git a/src-tauri/src/models/qrcode.rs b/src-tauri/src/models/qrcode.rs index d56fba0..ab771fb 100644 --- a/src-tauri/src/models/qrcode.rs +++ b/src-tauri/src/models/qrcode.rs @@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize}; /// /// 定义生成二维码所需的参数 #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct QrConfig { /// 二维码内容 pub content: String, @@ -15,12 +16,17 @@ pub struct QrConfig { pub margin: u32, /// 容错级别: "L", "M", "Q", "H" pub error_correction: String, + /// 样式配置 + pub style: Option, + /// Logo 配置 + pub logo: Option, } -/// 二维码样式(阶段 2 使用) +/// 二维码样式 /// /// 定义二维码的视觉样式 #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct QrStyle { /// 点形状: "square", "circle", "rounded" pub dot_shape: String, @@ -36,10 +42,24 @@ pub struct QrStyle { pub gradient_colors: Option>, } -/// Logo 配置(阶段 2 使用) +impl Default for QrStyle { + fn default() -> Self { + Self { + dot_shape: "square".to_string(), + eye_shape: "square".to_string(), + foreground_color: "#000000".to_string(), + background_color: "#FFFFFF".to_string(), + is_gradient: false, + gradient_colors: None, + } + } +} + +/// Logo 配置 /// /// 定义 Logo 的位置和样式 #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct LogoConfig { /// Logo 文件路径 pub path: String, @@ -55,6 +75,7 @@ pub struct LogoConfig { /// /// 包含生成的二维码图片数据 #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct QrResult { /// Base64 编码的图片数据 pub data: String, diff --git a/src-tauri/src/models/system_info.rs b/src-tauri/src/models/system_info.rs index c4709eb..0389cec 100644 --- a/src-tauri/src/models/system_info.rs +++ b/src-tauri/src/models/system_info.rs @@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize}; /// 系统信息(完整版) #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct SystemInfo { /// 操作系统信息 pub os: OsInfo, @@ -29,6 +30,7 @@ pub struct SystemInfo { /// 操作系统信息 #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct OsInfo { /// 操作系统名称 pub name: String, @@ -46,6 +48,7 @@ pub struct OsInfo { /// 硬件信息(主板、BIOS) #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct HardwareInfo { /// 制造商 pub manufacturer: String, @@ -59,6 +62,7 @@ pub struct HardwareInfo { /// CPU 信息 #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct CpuInfo { /// CPU 型号 pub model: String, @@ -74,6 +78,7 @@ pub struct CpuInfo { /// 内存信息 #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct MemoryInfo { /// 总内存 (GB) pub total_gb: f64, @@ -87,6 +92,7 @@ pub struct MemoryInfo { /// GPU 信息 #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct GpuInfo { /// GPU 名称 pub name: String, @@ -98,6 +104,7 @@ pub struct GpuInfo { /// 磁盘信息 #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct DiskInfo { /// 盘符 (如 "C:") pub drive_letter: String, @@ -117,6 +124,7 @@ pub struct DiskInfo { /// 计算机信息 #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct ComputerInfo { /// 计算机名称 pub name: String, @@ -134,6 +142,7 @@ pub struct ComputerInfo { /// 显示器信息 #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct DisplayInfo { /// 屏幕数量 pub monitor_count: u32, @@ -145,6 +154,7 @@ pub struct DisplayInfo { /// 网络信息 #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct NetworkInfo { /// 网络接口列表 pub interfaces: Vec, @@ -156,6 +166,7 @@ pub struct NetworkInfo { /// 网络接口信息 #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct InterfaceInfo { /// 接口名称 pub name: String, diff --git a/src-tauri/src/services/qrcode_service.rs b/src-tauri/src/services/qrcode_service.rs index bc35d5d..ce8b3af 100644 --- a/src-tauri/src/services/qrcode_service.rs +++ b/src-tauri/src/services/qrcode_service.rs @@ -4,7 +4,7 @@ use crate::error::{AppError, AppResult}; use crate::models::qrcode::{QrConfig, QrResult}; -use crate::utils::qrcode_renderer::{image_to_base64, render_basic_qr}; +use crate::utils::qrcode_renderer::{image_to_base64, render_qr}; /// 二维码生成服务 pub struct QrCodeService; @@ -39,8 +39,17 @@ impl QrCodeService { return Err(AppError::InvalidData("边距不能超过 50".to_string())); } + // 验证 Logo 缩放比例 + if let Some(logo) = &config.logo { + if logo.scale < 0.05 || logo.scale > 0.3 { + return Err(AppError::InvalidData( + "Logo 缩放比例必须在 0.05 到 0.3 之间".to_string(), + )); + } + } + // 渲染二维码 - let img = render_basic_qr(config)?; + let img = render_qr(config)?; // 转换为 Base64 let base64_data = image_to_base64(&img)?; @@ -76,8 +85,22 @@ impl QrCodeService { )); } + // 验证边距 + if config.margin > 50 { + return Err(AppError::InvalidData("边距不能超过 50".to_string())); + } + + // 验证 Logo 缩放比例 + if let Some(logo) = &config.logo { + if logo.scale < 0.05 || logo.scale > 0.3 { + return Err(AppError::InvalidData( + "Logo 缩放比例必须在 0.05 到 0.3 之间".to_string(), + )); + } + } + // 渲染二维码 - let img = render_basic_qr(config)?; + let img = render_qr(config)?; // 保存到文件 img.save(output_path) @@ -90,6 +113,7 @@ impl QrCodeService { #[cfg(test)] mod tests { use super::*; + use crate::models::qrcode::{QrStyle}; #[test] fn test_generate_preview() { @@ -98,6 +122,8 @@ mod tests { size: 512, margin: 4, error_correction: "M".to_string(), + style: None, + logo: None, }; let result = QrCodeService::generate_preview(&config); @@ -108,6 +134,30 @@ mod tests { assert_eq!(qr_result.format, "png"); } + #[test] + fn test_generate_preview_with_style() { + let style = QrStyle { + dot_shape: "circle".to_string(), + eye_shape: "square".to_string(), + foreground_color: "#FF0000".to_string(), + background_color: "#FFFFFF".to_string(), + is_gradient: false, + gradient_colors: None, + }; + + let config = QrConfig { + content: "https://example.com".to_string(), + size: 512, + margin: 4, + error_correction: "M".to_string(), + style: Some(style), + logo: None, + }; + + let result = QrCodeService::generate_preview(&config); + assert!(result.is_ok()); + } + #[test] fn test_generate_preview_invalid_size() { let config = QrConfig { @@ -115,6 +165,8 @@ mod tests { size: 50, // 太小 margin: 4, error_correction: "M".to_string(), + style: None, + logo: None, }; let result = QrCodeService::generate_preview(&config); @@ -128,6 +180,8 @@ mod tests { size: 512, margin: 100, // 太大 error_correction: "M".to_string(), + style: None, + logo: None, }; let result = QrCodeService::generate_preview(&config); diff --git a/src-tauri/src/utils/qrcode_renderer.rs b/src-tauri/src/utils/qrcode_renderer.rs index 2455c93..e78e4a2 100644 --- a/src-tauri/src/utils/qrcode_renderer.rs +++ b/src-tauri/src/utils/qrcode_renderer.rs @@ -1,19 +1,19 @@ //! 二维码渲染工具函数 //! -//! 提供二维码矩阵到图像的渲染功能 +//! 提供二维码矩阵到图像的渲染功能,支持颜色、形状和 Logo use crate::error::{AppError, AppResult}; -use crate::models::qrcode::QrConfig; +use crate::models::qrcode::{QrConfig, QrStyle}; use base64::Engine; -use image::Luma; -use image::Rgba; -use image::RgbaImage; -use qrcode::QrCode; +use image::imageops::overlay; +use image::{ImageReader, Luma, Rgba, RgbaImage}; +use qrcode::{QrCode, EcLevel}; use std::io::Cursor; +use std::path::Path; -/// 渲染基础黑白二维码 +/// 渲染二维码 /// -/// 根据配置生成黑白二维码图片 +/// 根据配置生成二维码图片,支持颜色、形状和 Logo /// /// # 参数 /// @@ -27,7 +27,7 @@ use std::io::Cursor; /// /// - 二维码内容为空时返回 `InvalidData` /// - 二维码生成失败时返回相应错误 -pub fn render_basic_qr(config: &QrConfig) -> AppResult { +pub fn render_qr(config: &QrConfig) -> AppResult { // 验证内容 if config.content.trim().is_empty() { return Err(AppError::InvalidData( @@ -37,61 +37,324 @@ pub fn render_basic_qr(config: &QrConfig) -> AppResult { // 解析容错级别 let ec_level = match config.error_correction.as_str() { - "L" => qrcode::EcLevel::L, - "M" => qrcode::EcLevel::M, - "Q" => qrcode::EcLevel::Q, - "H" => qrcode::EcLevel::H, - _ => qrcode::EcLevel::M, // 默认使用 M 级别 + "L" => EcLevel::L, + "M" => EcLevel::M, + "Q" => EcLevel::Q, + "H" => EcLevel::H, + _ => EcLevel::M, }; // 生成二维码 let qr_code = QrCode::with_error_correction_level(config.content.as_bytes(), ec_level) .map_err(|e| AppError::InvalidData(format!("二维码生成失败: {}", e)))?; - // 计算包含边距的尺寸 + // 获取样式配置 + let style = config.style.as_ref().cloned().unwrap_or_default(); + + // 生成基础图像 + let mut img = if style.is_gradient { + render_gradient_qr(&qr_code, config, &style)? + } else { + render_solid_color_qr(&qr_code, config, &style)? + }; + + // 叠加 Logo + if let Some(logo_config) = &config.logo { + overlay_logo(&mut img, logo_config)?; + } + + Ok(img) +} + +/// 渲染纯色二维码 +fn render_solid_color_qr( + qr_code: &QrCode, + config: &QrConfig, + style: &QrStyle, +) -> AppResult { let qr_size = qr_code.width() as u32; let total_size = qr_size + 2 * config.margin; - // 创建图像缓冲区(Luma 格式,用于生成二维码) - let qr_image = qr_code.render::>().quiet_zone(false).min_dimensions(total_size, total_size).max_dimensions(total_size, total_size).build(); + // 创建基础图像 + let qr_image = qr_code + .render::>() + .quiet_zone(false) + .min_dimensions(total_size, total_size) + .max_dimensions(total_size, total_size) + .build(); - // 获取二维码图像尺寸 let (width, height) = qr_image.dimensions(); - - // 计算缩放比例以匹配目标尺寸 let scale = (config.size as f32 / width as f32).max(1.0) as u32; let scaled_width = width * scale; let scaled_height = height * scale; - // 创建 RGBA 图像并填充白色背景 - let mut img = RgbaImage::new(scaled_width, scaled_height); - for pixel in img.pixels_mut() { - *pixel = Rgba([255, 255, 255, 255]); - } + // 解析颜色 + let bg_color = parse_hex_color(&style.background_color); + let fg_color = parse_hex_color(&style.foreground_color); - // 渲染二维码 + // 创建 RGBA 图像 + let mut img = RgbaImage::new(scaled_width, scaled_height); + + // 渲染每个模块 for y in 0..height { for x in 0..width { let pixel = qr_image.get_pixel(x, y); - // 如果是黑色像素 - if pixel[0] == 0 { - // 计算缩放后的区域 - let start_x = x * scale; - let start_y = y * scale; - let end_x = start_x + scale; - let end_y = start_y + scale; + let is_dark = pixel[0] == 0; + let color = if is_dark { fg_color } else { bg_color }; - // 绘制黑色矩形 - for py in start_y..end_y.min(scaled_height) { - for px in start_x..end_x.min(scaled_width) { - img.put_pixel(px, py, Rgba([0, 0, 0, 255])); + // 计算缩放后的区域 + let start_x = x * scale; + let start_y = y * scale; + let end_x = start_x + scale; + let end_y = start_y + scale; + + // 绘制模块 + draw_shape( + &mut img, + start_x, + start_y, + end_x, + end_y, + color, + &style.dot_shape, + ); + } + } + + Ok(img) +} + +/// 渲染渐变二维码 +fn render_gradient_qr( + qr_code: &QrCode, + config: &QrConfig, + style: &QrStyle, +) -> AppResult { + let qr_size = qr_code.width() as u32; + let total_size = qr_size + 2 * config.margin; + + // 创建基础图像 + let qr_image = qr_code + .render::>() + .quiet_zone(false) + .min_dimensions(total_size, total_size) + .max_dimensions(total_size, total_size) + .build(); + + let (width, height) = qr_image.dimensions(); + let scale = (config.size as f32 / width as f32).max(1.0) as u32; + let scaled_width = width * scale; + let scaled_height = height * scale; + + // 解析背景色 + let bg_color = parse_hex_color(&style.background_color); + + // 获取渐变颜色 + let gradient_colors = style.gradient_colors.as_ref(); + let start_color = gradient_colors + .and_then(|colors| colors.first()) + .map(|c| parse_hex_color(c)) + .unwrap_or(parse_hex_color(&style.foreground_color)); + let end_color = gradient_colors + .and_then(|colors| colors.get(1)) + .map(|c| parse_hex_color(c)) + .unwrap_or(start_color); + + // 创建 RGBA 图像 + let mut img = RgbaImage::new(scaled_width, scaled_height); + + // 渲染每个模块 + for y in 0..height { + for x in 0..width { + let pixel = qr_image.get_pixel(x, y); + let is_dark = pixel[0] == 0; + + // 计算渐变颜色 + let progress = (x as f32 / width as f32).max(0.0).min(1.0); + let color = if is_dark { + interpolate_color(start_color, end_color, progress) + } else { + bg_color + }; + + // 计算缩放后的区域 + let start_x = x * scale; + let start_y = y * scale; + let end_x = start_x + scale; + let end_y = start_y + scale; + + // 绘制模块 + draw_shape( + &mut img, + start_x, + start_y, + end_x, + end_y, + color, + &style.dot_shape, + ); + } + } + + Ok(img) +} + +/// 绘制形状模块 +fn draw_shape( + img: &mut RgbaImage, + start_x: u32, + start_y: u32, + end_x: u32, + end_y: u32, + color: [u8; 4], + shape: &str, +) { + let (width, height) = img.dimensions(); + let end_x = end_x.min(width); + let end_y = end_y.min(height); + + match shape { + "circle" => { + // 绘制圆形 + let center_x = (start_x + end_x) as f32 / 2.0; + let center_y = (start_y + end_y) as f32 / 2.0; + let radius = ((end_x - start_x) as f32 / 2.0).min((end_y - start_y) as f32 / 2.0); + + for py in start_y..end_y { + for px in start_x..end_x { + let dx = px as f32 - center_x; + let dy = py as f32 - center_y; + if dx * dx + dy * dy <= radius * radius { + img.put_pixel(px, py, Rgba(color)); } } } } + "rounded" => { + // 绘制圆角矩形 + let radius = ((end_x - start_x) as f32 * 0.3) as u32; + + for py in start_y..end_y { + for px in start_x..end_x { + let mut should_draw = true; + + // 检查四个角 + if px < start_x + radius && py < start_y + radius { + // 左上角 + let dx = (start_x + radius - px) as f32; + let dy = (start_y + radius - py) as f32; + should_draw = dx * dx + dy * dy >= (radius as f32).powi(2) - 1.0; + } else if px >= end_x - radius && py < start_y + radius { + // 右上角 + let dx = (px - (end_x - radius)) as f32; + let dy = (start_y + radius - py) as f32; + should_draw = dx * dx + dy * dy >= (radius as f32).powi(2) - 1.0; + } else if px < start_x + radius && py >= end_y - radius { + // 左下角 + let dx = (start_x + radius - px) as f32; + let dy = (py - (end_y - radius)) as f32; + should_draw = dx * dx + dy * dy >= (radius as f32).powi(2) - 1.0; + } else if px >= end_x - radius && py >= end_y - radius { + // 右下角 + let dx = (px - (end_x - radius)) as f32; + let dy = (py - (end_y - radius)) as f32; + should_draw = dx * dx + dy * dy >= (radius as f32).powi(2) - 1.0; + } + + if should_draw { + img.put_pixel(px, py, Rgba(color)); + } + } + } + } + _ => { + // 默认绘制矩形 + for py in start_y..end_y { + for px in start_x..end_x { + img.put_pixel(px, py, Rgba(color)); + } + } + } + } +} + +/// 叠加 Logo +fn overlay_logo( + img: &mut RgbaImage, + logo_config: &crate::models::qrcode::LogoConfig, +) -> AppResult<()> { + // 读取 Logo 图片 + let logo_path = Path::new(&logo_config.path); + let logo_img = ImageReader::open(logo_path) + .map_err(|e| AppError::IoError(format!("无法读取 Logo 文件: {}", e)))? + .decode() + .map_err(|e| AppError::IoError(format!("Logo 解码失败: {}", e)))?; + + // 计算 Logo 尺寸 + let (img_width, img_height) = img.dimensions(); + let logo_max_size = (img_width.min(img_height) as f32 * logo_config.scale) as u32; + + // 调整 Logo 尺寸 + let logo_resized = logo_img.resize( + logo_max_size, + logo_max_size, + image::imageops::FilterType::Lanczos3, + ); + + // 转换为 RGBA + let logo_rgba = logo_resized.to_rgba8(); + + // 计算居中位置 + let logo_x = ((img_width - logo_max_size) / 2) as i64; + let logo_y = ((img_height - logo_max_size) / 2) as i64; + + // 添加白色边框 + if logo_config.has_border { + let border_size = logo_config.border_width; + let border_color = Rgba([255, 255, 255, 255]); + + // 绘制边框 + let y_start = (logo_y - border_size as i64).max(0) as u32; + let y_end = (logo_y + logo_max_size as i64 + border_size as i64).min(img_height as i64) as u32; + let x_start = (logo_x - border_size as i64).max(0) as u32; + let x_end = (logo_x + logo_max_size as i64 + border_size as i64).min(img_width as i64) as u32; + + for y in y_start..y_end { + for x in x_start..x_end { + let is_border = x < logo_x as u32 + || x >= (logo_x + logo_max_size as i64) as u32 + || y < logo_y as u32 + || y >= (logo_y + logo_max_size as i64) as u32; + if is_border { + img.put_pixel(x, y, border_color); + } + } + } } - Ok(img) + // 叠加 Logo + overlay(img, &logo_rgba, logo_x, logo_y); + + Ok(()) +} + +/// 解析 Hex 颜色 +fn parse_hex_color(hex: &str) -> [u8; 4] { + let hex = hex.trim_start_matches('#'); + let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0); + let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0); + let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0); + [r, g, b, 255] +} + +/// 插值颜色 +fn interpolate_color(start: [u8; 4], end: [u8; 4], progress: f32) -> [u8; 4] { + [ + (start[0] as f32 + (end[0] as f32 - start[0] as f32) * progress) as u8, + (start[1] as f32 + (end[1] as f32 - start[1] as f32) * progress) as u8, + (start[2] as f32 + (end[2] as f32 - start[2] as f32) * progress) as u8, + 255, + ] } /// 将图片转换为 Base64 字符串 @@ -120,6 +383,7 @@ pub fn image_to_base64(img: &RgbaImage) -> AppResult { #[cfg(test)] mod tests { use super::*; + use crate::models::qrcode::{QrConfig, QrStyle}; #[test] fn test_render_basic_qr() { @@ -128,26 +392,46 @@ mod tests { size: 512, margin: 4, error_correction: "M".to_string(), + style: None, + logo: None, }; - let result = render_basic_qr(&config); + let result = render_qr(&config); assert!(result.is_ok()); let img = result.unwrap(); - assert_eq!(img.dimensions().0, 512); - assert_eq!(img.dimensions().1, 512); + assert!(img.dimensions().0 >= 512); + assert!(img.dimensions().1 >= 512); } #[test] - fn test_render_empty_content() { + fn test_render_colored_qr() { + let style = QrStyle { + dot_shape: "circle".to_string(), + eye_shape: "square".to_string(), + foreground_color: "#FF0000".to_string(), + background_color: "#FFFF00".to_string(), + is_gradient: false, + gradient_colors: None, + }; + let config = QrConfig { - content: "".to_string(), + content: "https://example.com".to_string(), size: 512, margin: 4, error_correction: "M".to_string(), + style: Some(style), + logo: None, }; - let result = render_basic_qr(&config); - assert!(result.is_err()); + let result = render_qr(&config); + assert!(result.is_ok()); + } + + #[test] + fn test_parse_hex_color() { + assert_eq!(parse_hex_color("#000000"), [0, 0, 0, 255]); + assert_eq!(parse_hex_color("#FFFFFF"), [255, 255, 255, 255]); + assert_eq!(parse_hex_color("#FF0000"), [255, 0, 0, 255]); } } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 83ad5a8..7d567c9 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,45 +1,45 @@ { - "$schema": "https://schema.tauri.app/config/2", - "productName": "CmdRs", - "version": "0.1.0", - "identifier": "com.shenjianz.tauri-app", - "build": { - "beforeDevCommand": "pnpm dev", - "devUrl": "http://localhost:1420", - "beforeBuildCommand": "pnpm build", - "frontendDist": "../dist" - }, - "app": { - "windows": [ - { - "title": "CmdRs - 功能集合", - "label": "main", - "width": 1200, - "height": 800, - "minWidth": 800, - "minHeight": 600, - "decorations": true, - "transparent": false, - "alwaysOnTop": false, - "skipTaskbar": false, - "visible": false, - "center": true, - "resizable": true - } - ], - "security": { - "csp": null + "$schema": "https://schema.tauri.app/config/2", + "productName": "CmdRs", + "version": "0.1.0", + "identifier": "com.shenjianz.tauri-app", + "build": { + "beforeDevCommand": "pnpm dev", + "devUrl": "http://localhost:1420", + "beforeBuildCommand": "pnpm build", + "frontendDist": "../dist" + }, + "app": { + "windows": [ + { + "title": "CmdRs - 功能集合", + "label": "main", + "width": 800, + "height": 600, + "minWidth": 800, + "minHeight": 600, + "decorations": true, + "transparent": false, + "alwaysOnTop": false, + "skipTaskbar": false, + "visible": false, + "center": true, + "resizable": true + } + ], + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ] } - }, - "bundle": { - "active": true, - "targets": "all", - "icon": [ - "icons/32x32.png", - "icons/128x128.png", - "icons/128x128@2x.png", - "icons/icon.icns", - "icons/icon.ico" - ] - } } diff --git a/src/App.tsx b/src/App.tsx index 5c90d95..51cbda4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,7 +12,7 @@ import { QrCodeGeneratorPage } from "@/components/features/QrCodeGenerator/QrCod function App() { return ( -
+
{/* 全局快捷键监听 */} diff --git a/src/components/features/ColorPicker/ColorPickerPage.tsx b/src/components/features/ColorPicker/ColorPickerPage.tsx index 5c49cd1..86e1537 100644 --- a/src/components/features/ColorPicker/ColorPickerPage.tsx +++ b/src/components/features/ColorPicker/ColorPickerPage.tsx @@ -6,14 +6,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com 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; -} +import type { ColorInfo } from '@/types/color'; interface ColorHistory { color: ColorInfo; @@ -70,9 +63,9 @@ export function ColorPickerPage() { }, []); return ( -
+
{/* 顶部导航栏 */} -
+
{/* 主内容区 */} -
+
{/* 拾色按钮 */} diff --git a/src/components/features/JsonFormatter/JsonFormatterPage.tsx b/src/components/features/JsonFormatter/JsonFormatterPage.tsx index a840dcf..5a59925 100644 --- a/src/components/features/JsonFormatter/JsonFormatterPage.tsx +++ b/src/components/features/JsonFormatter/JsonFormatterPage.tsx @@ -4,27 +4,7 @@ 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; -} +import type { JsonFormatConfig, JsonFormatResult, JsonValidateResult } from '@/types/json'; export function JsonFormatterPage() { const [input, setInput] = useState(''); @@ -32,7 +12,7 @@ export function JsonFormatterPage() { const [validation, setValidation] = useState(null); const [config, setConfig] = useState({ indent: 2, - sort_keys: false, + sortKeys: false, mode: 'pretty', }); const [copied, setCopied] = useState(false); @@ -151,9 +131,9 @@ export function JsonFormatterPage() { }, []); return ( -
+
{/* 顶部导航栏 */} -
+
{/* 主内容区 */} -
+
{/* 配置选项 */} @@ -200,10 +180,10 @@ export function JsonFormatterPage() {
@@ -270,7 +250,7 @@ export function JsonFormatterPage() { {/* 验证状态指示器 */} {validation && (
- {validation.is_valid ? ( + {validation.isValid ? ( 有效 @@ -286,14 +266,14 @@ export function JsonFormatterPage() {
{/* 错误信息 */} - {validation && !validation.is_valid && validation.error_message && ( + {validation && !validation.isValid && validation.errorMessage && (

- {validation.error_message} + {validation.errorMessage}

- {(validation.error_line || validation.error_column) && ( + {(validation.errorLine || validation.errorColumn) && (

- 位置: 行 {validation.error_line}, 列 {validation.error_column} + 位置: 行 {validation.errorLine}, 列 {validation.errorColumn}

)}
@@ -303,7 +283,7 @@ export function JsonFormatterPage() {
+ ) : ( + // 已选择 Logo 时显示配置 +
+ {/* 文件名 */} +
+
+ + + {logo.path.split(/[/\\]/).pop()} + +
+ +
+ + {/* Logo 缩放比例 */} +
+
+ +
+ + updateLogo({ scale: Number(e.target.value) / 100 }) + } + className="w-full" + /> +

+ 建议 10%-20%,过大可能影响扫码 +

+
+ + {/* 边框选项 */} +
+
+ + updateLogo({ hasBorder: e.target.checked }) + } + className="w-4 h-4" + /> + +
+
+ + {/* 边框宽度 */} + {logo.hasBorder && ( +
+ + + updateLogo({ borderWidth: Number(e.target.value) }) + } + className="w-full" + /> +
+ )} +
+ )} + + + ); +} diff --git a/src/components/features/QrCodeGenerator/QrCodeGeneratorPage.tsx b/src/components/features/QrCodeGenerator/QrCodeGeneratorPage.tsx index ee16a73..c4af071 100644 --- a/src/components/features/QrCodeGenerator/QrCodeGeneratorPage.tsx +++ b/src/components/features/QrCodeGenerator/QrCodeGeneratorPage.tsx @@ -4,9 +4,11 @@ import { useEffect } from 'react'; import { useDebounce } from '@uidotdev/usehooks'; +import { Button } from '@/components/ui/button'; import { useQrStore } from '@/stores/qrcodeStore'; import { QrConfigPanel } from './QrConfigPanel'; import { QrPreview } from './QrPreview'; +import { QrCode } from 'lucide-react'; export function QrCodeGeneratorPage() { const { config, updateConfig, generatePreview } = useQrStore(); @@ -22,25 +24,45 @@ export function QrCodeGeneratorPage() { }, [debouncedConfig]); return ( -
- {/* 左侧配置面板 */} -
-
-

二维码生成器

-

- 生成自定义二维码图片 -

+
+ {/* 顶部导航栏 */} +
+
+
+ +
+ +

二维码生成器

+
+
- -
+
- {/* 右侧预览区域 */} -
- + {/* 主内容区 */} +
+
+
+ {/* 左侧配置面板 */} +
+
+ +
+
+ + {/* 右侧预览区域 */} +
+
+ +
+
+
+
); } diff --git a/src/components/features/QrCodeGenerator/QrConfigPanel.tsx b/src/components/features/QrCodeGenerator/QrConfigPanel.tsx index 3850000..db6ef8a 100644 --- a/src/components/features/QrCodeGenerator/QrConfigPanel.tsx +++ b/src/components/features/QrCodeGenerator/QrConfigPanel.tsx @@ -5,13 +5,13 @@ import { Label } from '@/components/ui/label'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; -import { Card, CardContent } from '@/components/ui/card'; import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { useQrStore } from '@/stores/qrcodeStore'; import type { QrConfig } from '@/types/qrcode'; import { ERROR_CORRECTION_OPTIONS, SIZE_PRESETS } from '@/types/qrcode'; import { Download, RotateCcw } from 'lucide-react'; -import { save } from '@tauri-apps/plugin-dialog'; +import { StyleOptions } from './StyleOptions'; +import { LogoUpload } from './LogoUpload'; interface QrConfigPanelProps { config: QrConfig; @@ -23,6 +23,7 @@ export function QrConfigPanel({ config, onConfigChange }: QrConfigPanelProps) { const handleExport = async () => { try { + const { save } = await import('@tauri-apps/plugin-dialog'); const outputPath = await save({ title: '保存二维码', defaultPath: `qrcode-${Date.now()}.png`, @@ -43,83 +44,86 @@ export function QrConfigPanel({ config, onConfigChange }: QrConfigPanelProps) { }; return ( -
+
{/* 基本配置 */} - - - {/* 内容输入 */} -
- - onConfigChange({ content: e.target.value })} - className="font-mono text-sm" - /> -
+
+
+ + onConfigChange({ content: e.target.value })} + className="font-mono text-sm" + /> +
- {/* 尺寸选择 */} -
- - onConfigChange({ size: Number(value) })} - > - - {SIZE_PRESETS.map((preset) => ( - - {preset.label.split(' ')[0]} - - ))} - - -

- 当前: {config.size}px -

-
+ {/* 尺寸选择 */} +
+ + onConfigChange({ size: Number(value) })} + > + + {SIZE_PRESETS.map((preset) => ( + + {preset.label.split(' ')[0]} + + ))} + + +

+ 当前: {config.size}px +

+
- {/* 容错级别 */} -
- - - onConfigChange({ errorCorrection: value as QrConfig['errorCorrection'] }) - } - > - - {ERROR_CORRECTION_OPTIONS.map((option) => ( - - {option.value} - - ))} - - -

- {ERROR_CORRECTION_OPTIONS.find((opt) => opt.value === config.errorCorrection) - ?.description} -

-
+ {/* 容错级别 */} +
+ + + onConfigChange({ errorCorrection: value as QrConfig['errorCorrection'] }) + } + > + + {ERROR_CORRECTION_OPTIONS.map((option) => ( + + {option.value} + + ))} + + +

+ {ERROR_CORRECTION_OPTIONS.find((opt) => opt.value === config.errorCorrection) + ?.description} +

+
- {/* 边距 */} -
- - onConfigChange({ margin: Number(e.target.value) })} - className="w-full" - /> -
- - + {/* 边距 */} +
+ + onConfigChange({ margin: Number(e.target.value) })} + className="w-full" + /> +
+
+ + {/* 样式配置 */} + + + {/* Logo 配置 */} + {/* 操作按钮 */} -
+
+
+ + {/* 预设颜色 */} +
+ {COLOR_PRESETS.map((preset) => ( + + ))} +
+ + {/* 自定义颜色 */} +
+
+ +
+ + updateStyle({ foregroundColor: e.target.value }) + } + className="w-12 h-8 p-0 border-0" + /> + + updateStyle({ foregroundColor: e.target.value }) + } + className="flex-1 font-mono text-xs" + placeholder="#000000" + /> +
+
+
+ +
+ + updateStyle({ backgroundColor: e.target.value }) + } + className="w-12 h-8 p-0 border-0" + /> + + updateStyle({ backgroundColor: e.target.value }) + } + className="flex-1 font-mono text-xs" + placeholder="#FFFFFF" + /> +
+
+
+ + {/* 渐变选项 */} +
+
+ updateStyle({ isGradient: e.target.checked })} + className="w-4 h-4" + /> + +
+ +
+ + {style.isGradient && ( +
+
+ +
+ + updateStyle({ + gradientColors: [ + e.target.value, + style.gradientColors?.[1] || style.foregroundColor, + ], + }) + } + className="w-12 h-8 p-0 border-0" + /> + + updateStyle({ + gradientColors: [ + e.target.value, + style.gradientColors?.[1] || style.foregroundColor, + ], + }) + } + className="flex-1 font-mono text-xs" + /> +
+
+
+ +
+ + updateStyle({ + gradientColors: [ + style.gradientColors?.[0] || + style.foregroundColor, + e.target.value, + ], + }) + } + className="w-12 h-8 p-0 border-0" + /> + + updateStyle({ + gradientColors: [ + style.gradientColors?.[0] || + style.foregroundColor, + e.target.value, + ], + }) + } + className="flex-1 font-mono text-xs" + /> +
+
+
+ )} +
+
+
+ ); +} diff --git a/src/components/features/SystemInfo/SystemInfoPage.tsx b/src/components/features/SystemInfo/SystemInfoPage.tsx index 994f49b..674d0bf 100644 --- a/src/components/features/SystemInfo/SystemInfoPage.tsx +++ b/src/components/features/SystemInfo/SystemInfoPage.tsx @@ -4,87 +4,7 @@ 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; -} +import type { SystemInfo } from '@/types/system'; export function SystemInfoPage() { const [systemInfo, setSystemInfo] = useState(null); @@ -202,9 +122,9 @@ export function SystemInfoPage() { const formatGB = (value: number) => `${value.toFixed(2)} GB`; return ( -
+
{/* 顶部导航栏 */} -
+
@@ -290,7 +210,7 @@ export function SystemInfoPage() {
{/* 主内容区 */} -
+
{error && ( @@ -316,8 +236,8 @@ export function SystemInfoPage() {
系统{systemInfo.os.name}
版本{systemInfo.os.version}
架构{systemInfo.os.arch}
-
主机名{systemInfo.os.host_name}
-
运行时间{systemInfo.os.uptime_readable}
+
主机名{systemInfo.os.hostName}
+
运行时间{systemInfo.os.uptimeReadable}
@@ -366,18 +286,18 @@ export function SystemInfoPage() {
频率
-
{systemInfo.cpu.max_frequency}MHz
+
{systemInfo.cpu.maxFrequency}MHz
使用率 - {systemInfo.cpu.usage_percent.toFixed(1)}% + {systemInfo.cpu.usagePercent.toFixed(1)}%
@@ -398,26 +318,26 @@ export function SystemInfoPage() {
总内存
-
{formatGB(systemInfo.memory.total_gb)}
+
{formatGB(systemInfo.memory.totalGb)}
已用
-
{formatGB(systemInfo.memory.used_gb)}
+
{formatGB(systemInfo.memory.usedGb)}
可用
-
{formatGB(systemInfo.memory.available_gb)}
+
{formatGB(systemInfo.memory.availableGb)}
使用率 - {systemInfo.memory.usage_percent.toFixed(1)}% + {systemInfo.memory.usagePercent.toFixed(1)}%
@@ -445,8 +365,8 @@ export function SystemInfoPage() {
{gpu.name}
- 显存: {gpu.vram_gb.toFixed(1)} GB - 驱动: {gpu.driver_version} + 显存: {gpu.vramGb.toFixed(1)} GB + 驱动: {gpu.driverVersion}
))} @@ -465,9 +385,9 @@ export function SystemInfoPage() {
-
数量{systemInfo.display.monitor_count}
-
主分辨率{systemInfo.display.primary_resolution}
-
所有分辨率{systemInfo.display.all_resolutions.join(', ')}
+
数量{systemInfo.display.monitorCount}
+
主分辨率{systemInfo.display.primaryResolution}
+
所有分辨率{systemInfo.display.allResolutions.join(', ')}
@@ -485,11 +405,11 @@ export function SystemInfoPage() {
总下载
-
{systemInfo.network.total_downloaded_mb.toFixed(2)} MB
+
{systemInfo.network.totalDownloadedMb.toFixed(2)} MB
总上传
-
{systemInfo.network.total_uploaded_mb.toFixed(2)} MB
+
{systemInfo.network.totalUploadedMb.toFixed(2)} MB
@@ -501,14 +421,14 @@ export function SystemInfoPage() { {iface.name}
- ↓{iface.download_speed_kb.toFixed(1)} - ↑{iface.upload_speed_kb.toFixed(1)} + ↓{iface.downloadSpeedKb.toFixed(1)} + ↑{iface.uploadSpeedKb.toFixed(1)}
-
{iface.mac_address}
- {iface.ip_networks.length > 0 && ( +
{iface.macAddress}
+ {iface.ipNetworks.length > 0 && (
- {iface.ip_networks.map((ip, ipIndex) => ( + {iface.ipNetworks.map((ip, ipIndex) => ( {ip} @@ -535,36 +455,36 @@ export function SystemInfoPage() {
- {disk.drive_letter} - {disk.volume_label} + {disk.driveLetter} + {disk.volumeLabel}
- {disk.file_system} + {disk.fileSystem}
总容量
-
{formatGB(disk.total_gb)}
+
{formatGB(disk.totalGb)}
已用
-
{formatGB(disk.used_gb)}
+
{formatGB(disk.usedGb)}
可用
-
{formatGB(disk.available_gb)}
+
{formatGB(disk.availableGb)}
使用率 - {disk.usage_percent.toFixed(1)}% + {disk.usagePercent.toFixed(1)}%
90 ? 'bg-red-500' : 'bg-primary' + disk.usagePercent > 90 ? 'bg-red-500' : 'bg-primary' }`} - style={{ width: `${Math.min(disk.usage_percent, 100)}%` }} + style={{ width: `${Math.min(disk.usagePercent, 100)}%` }} />
diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 43e726a..b81cb8b 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -35,9 +35,9 @@ export function Home() { }, [selectedCategory, searchQuery]); return ( -
+
{/* 顶部导航栏 */} -
+
@@ -83,7 +83,7 @@ export function Home() {
{/* 主内容区 */} -
+
{/* 分类筛选 */}
+
{/* 顶部导航栏 */} -
+