diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 4db0100..2ec0fff 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -3,7 +3,8 @@ "allow": [ "Bash(cargo check:*)", "Bash(pnpm build:*)", - "Bash(tree:*)" + "Bash(tree:*)", + "Bash(pnpm add:*)" ] } } diff --git a/docs/开发指南.md b/docs/开发指南.md index 2352103..d1600dc 100644 --- a/docs/开发指南.md +++ b/docs/开发指南.md @@ -13,7 +13,7 @@ ## 架构概述 -### 四层架构 +### 三层架构 ``` ┌─────────────────────────────────────────────────┐ @@ -25,10 +25,6 @@ │ 职责:业务逻辑、流程编排、状态管理 │ │ 文件:src/services/*.rs │ ├─────────────────────────────────────────────────┤ -│ Platform 层 │ -│ 职责:平台差异抽象、统一接口 │ -│ 文件:src/platforms/*.rs │ -├─────────────────────────────────────────────────┤ │ Utils 层 │ │ 职责:纯算法、无状态工具函数 │ │ 文件:src/utils/*.rs │ @@ -38,9 +34,9 @@ ### 依赖原则 - **单向依赖**:上层依赖下层,下层不依赖上层 -- **依赖链**:Command → Service → Platform → Utils +- **依赖链**:Command → Service → Utils - **Utils 完全独立**:只依赖标准库和 models -- **Platform 通过 trait 解耦**:Service 层通过 trait 调用,不关心具体实现 +- **Service 可以直接调用 Windows API** --- @@ -53,8 +49,7 @@ 1. **功能描述**:这个功能做什么? 2. **用户交互**:用户如何触发这个功能? 3. **输入输出**:需要什么参数?返回什么结果? -4. **平台差异**:不同平台是否需要不同实现? -5. **错误场景**:哪些情况下会出错?如何处理? +4. **错误场景**:哪些情况下会出错?如何处理? ### 步骤 2:设计数据模型(如需要) @@ -140,88 +135,7 @@ mod tests { - ✅ 包含单元测试 - ✅ 不依赖任何外部状态 -### 步骤 4:定义平台抽象(如果需要平台特定实现) - -如果功能需要访问平台 API,定义 trait: - -```rust -// src/platforms/new_feature.rs - -use crate::error::AppResult; - -/// 新功能平台抽象 trait -pub trait NewFeatureAccessor { - /// 执行平台特定的操作 - /// - /// # 参数 - /// - /// * `param1` - 参数 1 说明 - /// - /// # 返回 - /// - /// 返回操作结果 - /// - /// # 错误 - /// - /// 操作失败时返回错误 - fn execute_platform_operation(&self, param1: &str) -> AppResult; -} - -/// 平台特定实现类型别名 -#[cfg(windows)] -pub type PlatformNewFeature = crate::platforms::windows::new_feature_impl::WindowsNewFeature; - -#[cfg(not(windows))] -pub type PlatformNewFeature = crate::platforms::windows::new_feature_impl::DummyNewFeature; -``` - -**规范**: -- ✅ 使用 trait 定义接口 -- ✅ 所有方法返回 `AppResult` -- ✅ 方法参数使用引用(`&str` 而不是 `String`) -- ✅ 添加详细的文档注释 - -### 步骤 5:实现平台特定代码 - -```rust -// src/platforms/windows/new_feature_impl.rs - -use crate::error::{AppError, AppResult}; -use crate::platforms::new_feature::NewFeatureAccessor; - -/// Windows 平台实现 -#[cfg(windows)] -pub struct WindowsNewFeature; - -#[cfg(windows)] -impl NewFeatureAccessor for WindowsNewFeature { - fn execute_platform_operation(&self, param1: &str) -> AppResult { - // Windows 特定实现 - Ok(format!("Windows: {}", param1)) - } -} - -/// 其他平台占位实现 -#[cfg(not(windows))] -pub struct DummyNewFeature; - -#[cfg(not(windows))] -impl NewFeatureAccessor for DummyNewFeature { - fn execute_platform_operation(&self, _param1: &str) -> AppResult { - Err(AppError::PlatformNotSupported( - "此平台暂不支持该功能".to_string() - )) - } -} -``` - -**规范**: -- ✅ 使用 `#[cfg(windows)]` 条件编译 -- ✅ 提供其他平台的占位实现 -- ✅ 占位实现返回 `PlatformNotSupported` 错误 -- ✅ 使用中文错误消息 - -### 步骤 6:实现业务逻辑(Service 层) +### 步骤 4:实现业务逻辑(Service 层) ```rust // src/services/new_feature_service.rs @@ -252,7 +166,6 @@ impl NewFeatureService { /// # 错误 /// /// - 配置无效时返回 `AppError::InvalidColorData` - /// - 平台不支持时返回 `AppError::PlatformNotSupported` pub fn execute(&self, config: &NewFeatureConfig) -> AppResult { // 1. 参数验证 if config.option1.is_empty() { @@ -285,10 +198,11 @@ impl NewFeatureService { - ✅ 使用 struct 命名空间(如 `NewFeatureService`) - ✅ 所有方法返回 `AppResult` - ✅ 参数验证放在 Service 层 +- ✅ 可以直接调用 Windows API - ✅ 添加详细的文档注释 - ✅ 包含同步和异步两个版本(如需要) -### 步骤 7:创建 Tauri 命令(Command 层) +### 步骤 5:创建 Tauri 命令(Command 层) ```rust // src/commands/new_feature_commands.rs @@ -366,7 +280,7 @@ pub async fn execute_new_feature_async(config: NewFeatureConfig) -> Result AppResult; -} - -// Windows 实现 -#[cfg(windows)] -pub type PlatformFile = WindowsFile; - -// 占位实现 -#[cfg(not(windows))] -pub type PlatformFile = DummyFile; -``` - ### Service 层规范 **职责**:业务逻辑实现和流程编排 @@ -623,7 +510,7 @@ pub type PlatformFile = DummyFile; - ✅ 使用 struct 命名空间 - ✅ 所有方法返回 `AppResult` - ✅ 参数验证在 Service 层进行 -- ✅ 通过 trait 调用 Platform 层 +- ✅ 可以直接调用 Windows API - ✅ 可以调用 Utils 层函数 - ✅ 提供同步和异步版本(如需要) - ✅ 使用详细的中文档注释 @@ -639,8 +526,8 @@ impl FileService { return Err(AppError::InvalidData("路径不能为空".to_string())); } - // 2. 调用 Platform 层 - let content = PlatformFile::read_file(path)?; + // 2. 调用 Windows API 或第三方库 + let content = std::fs::read_to_string(path)?; // 3. 调用 Utils 层处理 let processed = utils::text::trim_whitespace(&content); @@ -709,55 +596,7 @@ pub struct ScreenshotResult { } ``` -#### 2. 定义平台抽象 - -```rust -// src/platforms/screenshot.rs - -use crate::error::AppResult; -use crate::models::screenshot::ScreenshotConfig; - -/// 屏幕截图 trait -pub trait ScreenshotCapturer { - /// 截取整个屏幕 - /// - /// # 参数 - /// - /// * `config` - 截图配置 - /// - /// # 返回 - /// - /// 返回截图结果,包含 Base64 编码的图片数据 - fn capture_screen(&self, config: &ScreenshotConfig) -> AppResult; - - /// 截取指定区域 - /// - /// # 参数 - /// - /// * `x` - 起始 X 坐标 - /// * `y` - 起始 Y 坐标 - /// * `width` - 宽度 - /// * `height` - 高度 - /// * `config` - 截图配置 - fn capture_region( - &self, - x: u32, - y: u32, - width: u32, - height: u32, - config: &ScreenshotConfig, - ) -> AppResult; -} - -// 类型别名 -#[cfg(windows)] -pub type PlatformScreenshot = crate::platforms::windows::screenshot_impl::WindowsScreenshot; - -#[cfg(not(windows))] -pub type PlatformScreenshot = crate::platforms::windows::screenshot_impl::DummyScreenshot; -``` - -#### 3. 实现 Service +#### 2. 实现 Service ```rust // src/services/screenshot_service.rs @@ -768,7 +607,6 @@ pub type PlatformScreenshot = crate::platforms::windows::screenshot_impl::DummyS use crate::error::{AppError, AppResult}; use crate::models::screenshot::{ScreenshotConfig, ScreenshotResult}; -use crate::platforms::screenshot::ScreenshotCapturer; /// 截图服务 pub struct ScreenshotService; @@ -793,8 +631,30 @@ impl ScreenshotService { )); } - // 调用平台实现 - PlatformScreenshot::capture_screen(config) + // 调用 Windows API 或使用第三方库 + // 例如:使用 screenshots-rs crate + let screenshots = screenshots::Screen::all().map_err(|e| { + AppError::InvalidData(format!("获取屏幕失败: {}", e)) + })?; + + if let Some(screen) = screenshots.first() { + let image = screen.capture().map_err(|e| { + AppError::InvalidData(format!("截图失败: {}", e)) + })?; + + // 转换为 Base64 + let buffer = image.buffer(); + let base64_data = base64::encode(buffer); + + Ok(ScreenshotResult { + data: base64_data, + format: "png".to_string(), + width: image.width(), + height: image.height(), + }) + } else { + Err(AppError::InvalidData("未找到屏幕".to_string())) + } } /// 截取指定区域 @@ -811,12 +671,19 @@ impl ScreenshotService { )); } - PlatformScreenshot::capture_region(x, y, width, height, config) + // 调用 Windows API 实现区域截图 + // ... + Ok(ScreenshotResult { + data: "base64_data".to_string(), + format: "png".to_string(), + width, + height, + }) } } ``` -#### 4. 创建命令 +#### 3. 创建命令 ```rust // src/commands/screenshot_commands.rs @@ -881,7 +748,7 @@ pub fn capture_region( | 函数 | snake_case | `get_pixel_color`, `toggle_window` | | 常量 | SCREAMING_SNAKE_CASE | `MAX_RETRY_COUNT` | | Trait | PascalCase + 能力 | `ScreenAccessor`, `CursorController` | -| 类型别名 | PascalCase + Platform/Type | `PlatformScreen`, `AppResult` | +| 类型别名 | PascalCase + Type | `AppResult`, `JsonResult` | ### 2. 文档注释规范 @@ -975,33 +842,7 @@ impl SomeService { } ``` -### 6. 平台检测规范 - -```rust -// ✅ 使用条件编译和 trait -#[cfg(windows)] -fn platform_specific() -> AppResult<()> { - WindowsImpl::do_something() -} - -#[cfg(not(windows))] -fn platform_specific() -> AppResult<()> { - Err(AppError::PlatformNotSupported( - "此平台暂不支持".to_string() - )) -} - -// ❌ 不推荐:运行时检测 -fn platform_specific() -> AppResult<()> { - if cfg!(windows) { - // Windows 代码 - } else { - Err(AppError::PlatformNotSupported("...".to_string())) - } -} -``` - -### 7. 代码组织规范 +### 6. 代码组织规范 ```rust // ✅ 推荐:按功能分组 @@ -1114,27 +955,23 @@ impl fmt::Display for AppError { } ``` -### Q: 如何在多个平台实现同一个功能? +### Q: 如何调用 Windows API? -A: 为每个平台创建独立的实现文件,并通过条件编译选择: +A: 在 Service 层直接使用 Windows 相关的 crate 或调用 Windows API: ```rust -// platforms/windows/impl.rs -#[cfg(windows)] -pub struct PlatformImpl; -impl PlatformTrait for PlatformImpl { } +// 使用 windows-rs crate +use windows::Win32::UI::WindowsAndMessaging::GetCursorPos; -// platforms/macos/impl.rs -#[cfg(target_os = "macos")] -pub struct PlatformImpl; -impl PlatformTrait for PlatformImpl { } - -// platforms/mod.rs -#[cfg(windows)] -pub type Platform = crate::platforms::windows::impl::PlatformImpl; - -#[cfg(target_os = "macos")] -pub type Platform = crate::platforms::macos::impl::PlatformImpl; +impl CursorService { + pub fn get_cursor_position(&self) -> AppResult<(i32, i32)> { + let mut pos = POINT { x: 0, y: 0 }; + unsafe { + GetCursorPos(&mut pos)?; + } + Ok((pos.x, pos.y)) + } +} ``` ### Q: 如何处理异步操作? @@ -1168,7 +1005,6 @@ pub async fn get_data() -> Result { - [ ] 文档注释包含示例代码 - [ ] 错误消息使用中文 - [ ] 遵循依赖原则(上层依赖下层) -- [ ] 平台特定代码正确使用条件编译 - [ ] 新增类型已导出(如需要) - [ ] 命令已在 `lib.rs` 中注册 diff --git a/docs/快速参考.md b/docs/快速参考.md index 24ee4ef..32d0543 100644 --- a/docs/快速参考.md +++ b/docs/快速参考.md @@ -5,12 +5,10 @@ ``` 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 +3. 实现 Service → src/services/new_feature_service.rs +4. 创建 Command → src/commands/new_feature_commands.rs +5. 注册模块 → 更新 mod.rs 和 lib.rs +6. 测试验证 → cargo check && cargo test ``` ## 📁 文件模板 @@ -64,31 +62,13 @@ pub fn execute_feature(input: FeatureData) -> Result { } ``` -### 4. Platform Trait 模板 - -```rust -// src/platforms/feature.rs -use crate::error::AppResult; - -/// Trait 说明 -pub trait FeatureAccessor { - fn do_something(&self, param: &str) -> AppResult; -} - -#[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` + 能力描述 +- [ ] Trait: `PascalCase` + 能力描述(可选) ### 文档规范 - [ ] 所有公开 API 有 `///` 注释 @@ -103,7 +83,7 @@ pub type PlatformFeature = crate::platforms::windows::feature_impl::DummyFeature - [ ] 使用中文错误消息 - [ ] 参数验证在 Service 层 - [ ] Command 层简洁(仅适配) -- [ ] 使用 `#[cfg(windows)]` 条件编译 +- [ ] Service 层可直接调用 Windows API ## 🔧 常用命令 @@ -148,8 +128,6 @@ cargo doc --open ``` 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 → 注册命令 @@ -160,8 +138,6 @@ 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 @@ -169,19 +145,7 @@ 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: 类型不匹配 +### 错误 1: 类型不匹配 ```rust // ❌ 错误 pub fn toggle_window(window: &WebviewWindow) { } @@ -190,7 +154,7 @@ pub fn toggle_window(window: &WebviewWindow) { } pub fn toggle_window(window: &Window) { } ``` -### 错误 3: 忘记注册命令 +### 错误 2: 忘记注册命令 ```rust // ❌ 错误:命令未注册,前端无法调用 diff --git a/package.json b/package.json index 742a53a..a437b9f 100644 --- a/package.json +++ b/package.json @@ -12,13 +12,16 @@ "dependencies": { "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-dropdown-menu": "^2.0.6", + "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tabs": "^1.0.4", "@tailwindcss/vite": "^4.1.12", "@tauri-apps/api": "^2", + "@tauri-apps/plugin-dialog": "^2.6.0", "@tauri-apps/plugin-global-shortcut": "^2", "@tauri-apps/plugin-opener": "^2", + "@uidotdev/usehooks": "^2.4.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "fuse.js": "^7.1.0", @@ -27,7 +30,8 @@ "react-dom": "^19.1.0", "react-router-dom": "^7.8.2", "tailwind-merge": "^3.3.1", - "tailwindcss": "^4.1.12" + "tailwindcss": "^4.1.12", + "zustand": "^5.0.11" }, "devDependencies": { "@eslint/js": "^9.33.0", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 86b7bee..e95962e 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -17,6 +17,24 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + [[package]] name = "alloc-no-stdlib" version = "2.0.4" @@ -47,6 +65,38 @@ version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -213,6 +263,49 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.18", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47c8fbc0f831f4519fe8b810b6a7a91410ec83031b8233f730a0480029f6a23f" +dependencies = [ + "arrayvec", +] + [[package]] name = "base64" version = "0.21.7" @@ -225,6 +318,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + [[package]] name = "bitflags" version = "1.3.2" @@ -240,6 +339,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "bitstream-io" +version = "4.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757" +dependencies = [ + "core2", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -292,6 +400,12 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "built" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" + [[package]] name = "bumpalo" version = "3.19.1" @@ -310,6 +424,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.11.1" @@ -393,6 +513,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -441,6 +563,12 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "combine" version = "4.6.7" @@ -516,6 +644,15 @@ dependencies = [ "libc", ] +[[package]] +name = "core2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +dependencies = [ + "memchr", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -568,6 +705,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.7" @@ -843,6 +986,26 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -891,12 +1054,47 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "exr" +version = "1.74.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "fdeflate" version = "0.3.7" @@ -1267,6 +1465,16 @@ dependencies = [ "wasip3", ] +[[package]] +name = "gif" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gio" version = "0.18.4" @@ -1433,6 +1641,17 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1604,7 +1823,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" dependencies = [ "byteorder", - "png", + "png 0.17.16", ] [[package]] @@ -1721,6 +1940,46 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.25.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png 0.18.0", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core 0.5.1", + "zune-jpeg 0.5.12", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imgref" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" + [[package]] name = "indexmap" version = "1.9.3" @@ -1753,6 +2012,17 @@ dependencies = [ "cfb", ] +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -1788,6 +2058,15 @@ dependencies = [ "once_cell", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.17" @@ -1839,6 +2118,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.85" @@ -1906,6 +2195,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + [[package]] name = "libappindicator" version = "0.9.0" @@ -1936,6 +2231,16 @@ version = "0.2.181" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" +[[package]] +name = "libfuzzer-sys" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" +dependencies = [ + "arbitrary", + "cc", +] + [[package]] name = "libloading" version = "0.7.4" @@ -1983,6 +2288,15 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + [[package]] name = "mac" version = "0.1.1" @@ -2020,6 +2334,16 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + [[package]] name = "memchr" version = "2.8.0" @@ -2062,6 +2386,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "moxcms" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "muda" version = "0.17.1" @@ -2077,7 +2411,7 @@ dependencies = [ "objc2-core-foundation", "objc2-foundation", "once_cell", - "png", + "png 0.17.16", "serde", "thiserror 2.0.18", "windows-sys 0.60.2", @@ -2125,6 +2459,21 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + [[package]] name = "ntapi" version = "0.4.2" @@ -2134,12 +2483,53 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-conv" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2471,6 +2861,18 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + [[package]] name = "pathdiff" version = "0.2.3" @@ -2672,6 +3074,19 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "png" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" +dependencies = [ + "bitflags 2.10.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "polling" version = "3.11.0" @@ -2794,6 +3209,58 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +dependencies = [ + "quote", + "syn 2.0.114", +] + +[[package]] +name = "pxfm" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" +dependencies = [ + "num-traits", +] + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "qrcode" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d68782463e408eb1e668cf6152704bd856c78c5b6417adaee3203d8f4c1fc9ec" +dependencies = [ + "image", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quick-xml" version = "0.38.4" @@ -2843,6 +3310,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + [[package]] name = "rand_chacha" version = "0.2.2" @@ -2863,6 +3340,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + [[package]] name = "rand_core" version = "0.5.1" @@ -2881,6 +3368,15 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "rand_hc" version = "0.2.0" @@ -2899,6 +3395,56 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand 0.9.2", + "rand_chacha 0.9.0", + "simd_helpers", + "thiserror 2.0.18", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef69c1990ceef18a116855938e74793a5f7496ee907562bd0857b6ac734ab285" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -3028,6 +3574,12 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rgb" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" + [[package]] name = "rustc_version" version = "0.4.1" @@ -3342,6 +3894,15 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + [[package]] name = "siphasher" version = "0.3.11" @@ -3654,6 +4215,9 @@ dependencies = [ name = "tauri-app" version = "0.1.0" dependencies = [ + "base64 0.22.1", + "image", + "qrcode", "serde", "serde_derive", "serde_json", @@ -3699,7 +4263,7 @@ dependencies = [ "ico", "json-patch", "plist", - "png", + "png 0.17.16", "proc-macro2", "quote", "semver", @@ -3948,6 +4512,20 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "tiff" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg 0.4.21", +] + [[package]] name = "time" version = "0.3.47" @@ -4204,7 +4782,7 @@ dependencies = [ "objc2-core-graphics", "objc2-foundation", "once_cell", - "png", + "png 0.17.16", "serde", "thiserror 2.0.18", "windows-sys 0.60.2", @@ -4347,6 +4925,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + [[package]] name = "version-compare" version = "0.2.1" @@ -4624,6 +5213,12 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + [[package]] name = "winapi" version = "0.3.9" @@ -5401,6 +5996,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + [[package]] name = "yoke" version = "0.8.1" @@ -5565,6 +6166,45 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4de98dfa5d5b7fef4ee834d0073d560c9ca7b6c46a71d058c48db7960f8cfaf7" +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +dependencies = [ + "zune-core 0.4.12", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "410e9ecef634c709e3831c2cfdb8d9c32164fae1c67496d5b68fff728eec37fe" +dependencies = [ + "zune-core 0.5.1", +] + [[package]] name = "zvariant" version = "5.9.2" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 52a9ce3..84d2460 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -24,6 +24,9 @@ tauri-plugin-global-shortcut = "2" serde = { version = "1", features = ["derive"] } serde_json = { version = "1", features = ["preserve_order"] } sysinfo = "0.30" +qrcode = "0.14" +image = "0.25" +base64 = "0.22" [target.'cfg(windows)'.dependencies] windows = { version = "0.58", features = [ diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index cc9ad80..25a17dd 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -4,5 +4,6 @@ pub mod json_format_commands; pub mod picker_color_commands; +pub mod qrcode_commands; pub mod system_info_commands; pub mod window_commands; diff --git a/src-tauri/src/commands/qrcode_commands.rs b/src-tauri/src/commands/qrcode_commands.rs new file mode 100644 index 0000000..d1327f9 --- /dev/null +++ b/src-tauri/src/commands/qrcode_commands.rs @@ -0,0 +1,71 @@ +//! 二维码生成命令 +//! +//! 定义二维码生成相关的 Tauri 命令 + +use crate::models::qrcode::{QrConfig, QrResult}; +use crate::services::qrcode_service::QrCodeService; + +/// 生成二维码预览 +/// +/// Tauri 命令,用于从前端调用生成二维码预览 +/// +/// # 参数 +/// +/// * `config` - 二维码配置 +/// +/// # 返回 +/// +/// 返回二维码生成结果,包含 Base64 编码的图片数据 +/// +/// # 前端调用示例 +/// +/// ```typescript +/// import { invoke } from '@tauri-apps/api/core'; +/// +/// const result = await invoke('generate_qr_preview', { +/// config: { +/// content: 'https://example.com', +/// size: 512, +/// margin: 4, +/// errorCorrection: 'M' +/// } +/// }); +/// console.log(result.data); // "data:image/png;base64,..." +/// ``` +#[tauri::command] +pub async fn generate_qr_preview(config: QrConfig) -> Result { + QrCodeService::generate_preview(&config).map_err(|e| e.to_string()) +} + +/// 生成二维码并保存到文件 +/// +/// Tauri 命令,用于将生成的二维码保存为文件 +/// +/// # 参数 +/// +/// * `config` - 二维码配置 +/// * `output_path` - 输出文件路径 +/// +/// # 返回 +/// +/// 成功时返回 Ok(()),失败时返回错误字符串 +/// +/// # 前端调用示例 +/// +/// ```typescript +/// import { invoke } from '@tauri-apps/api/core'; +/// +/// await invoke('generate_qr_file', { +/// config: { +/// content: 'https://example.com', +/// size: 1024, +/// margin: 4, +/// errorCorrection: 'H' +/// }, +/// outputPath: '/path/to/output.png' +/// }); +/// ``` +#[tauri::command] +pub async fn generate_qr_file(config: QrConfig, output_path: String) -> Result<(), String> { + QrCodeService::generate_to_file(&config, &output_path).map_err(|e| e.to_string()) +} diff --git a/src-tauri/src/error.rs b/src-tauri/src/error.rs index edba99f..0746f74 100644 --- a/src-tauri/src/error.rs +++ b/src-tauri/src/error.rs @@ -43,6 +43,21 @@ pub enum AppError { /// /// 表示获取系统信息时失败 SystemInfoFailed(String), + + /// 无效数据 + /// + /// 表示提供的数据无效或不符合要求 + InvalidData(String), + + /// IO 错误 + /// + /// 表示文件或网络 IO 操作失败 + IoError(String), + + /// 二维码生成失败 + /// + /// 表示二维码生成过程失败 + QrCodeGenerationFailed(String), } impl fmt::Display for AppError { @@ -55,6 +70,9 @@ impl fmt::Display for AppError { AppError::InvalidColorData(msg) => write!(f, "颜色数据无效: {}", msg), AppError::ColorConversionFailed(msg) => write!(f, "颜色转换失败: {}", msg), AppError::SystemInfoFailed(msg) => write!(f, "系统信息获取失败: {}", msg), + AppError::InvalidData(msg) => write!(f, "数据无效: {}", msg), + AppError::IoError(msg) => write!(f, "IO 错误: {}", msg), + AppError::QrCodeGenerationFailed(msg) => write!(f, "二维码生成失败: {}", msg), } } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 3e0c317..ec3df1f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -48,6 +48,9 @@ pub fn run() { commands::json_format_commands::compact_json, // 操作系统信息命令 commands::system_info_commands::get_system_info, + // 二维码生成命令 + commands::qrcode_commands::generate_qr_preview, + commands::qrcode_commands::generate_qr_file, ]) .run(tauri::generate_context!()) .expect("运行 Tauri 应用时出错"); diff --git a/src-tauri/src/models/mod.rs b/src-tauri/src/models/mod.rs index 85bf2ad..d88bf93 100644 --- a/src-tauri/src/models/mod.rs +++ b/src-tauri/src/models/mod.rs @@ -4,4 +4,5 @@ pub mod color; pub mod json_format; +pub mod qrcode; pub mod system_info; diff --git a/src-tauri/src/models/qrcode.rs b/src-tauri/src/models/qrcode.rs new file mode 100644 index 0000000..d56fba0 --- /dev/null +++ b/src-tauri/src/models/qrcode.rs @@ -0,0 +1,63 @@ +//! 二维码生成相关数据模型 + +use serde::{Deserialize, Serialize}; + +/// 二维码配置 +/// +/// 定义生成二维码所需的参数 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct QrConfig { + /// 二维码内容 + pub content: String, + /// 输出尺寸(像素) + pub size: u32, + /// 边距(模块数) + pub margin: u32, + /// 容错级别: "L", "M", "Q", "H" + pub error_correction: String, +} + +/// 二维码样式(阶段 2 使用) +/// +/// 定义二维码的视觉样式 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct QrStyle { + /// 点形状: "square", "circle", "rounded" + pub dot_shape: String, + /// 码眼形状: "square", "circle", "rounded" + pub eye_shape: String, + /// 前景色(Hex 颜色代码) + pub foreground_color: String, + /// 背景色(Hex 颜色代码) + pub background_color: String, + /// 是否使用渐变 + pub is_gradient: bool, + /// 渐变颜色列表(如果 is_gradient 为 true) + pub gradient_colors: Option>, +} + +/// Logo 配置(阶段 2 使用) +/// +/// 定义 Logo 的位置和样式 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LogoConfig { + /// Logo 文件路径 + pub path: String, + /// 缩放比例 (0.1 - 0.3) + pub scale: f32, + /// 是否添加边框 + pub has_border: bool, + /// 边框宽度(像素) + pub border_width: u32, +} + +/// 二维码生成结果 +/// +/// 包含生成的二维码图片数据 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct QrResult { + /// Base64 编码的图片数据 + pub data: String, + /// 图片格式(如 "png") + pub format: String, +} diff --git a/src-tauri/src/services/mod.rs b/src-tauri/src/services/mod.rs index 35143f6..a39d1c7 100644 --- a/src-tauri/src/services/mod.rs +++ b/src-tauri/src/services/mod.rs @@ -3,5 +3,6 @@ //! 提供应用的核心业务逻辑实现 pub mod json_format_service; +pub mod qrcode_service; pub mod system_info_service; pub mod window_service; diff --git a/src-tauri/src/services/qrcode_service.rs b/src-tauri/src/services/qrcode_service.rs new file mode 100644 index 0000000..bc35d5d --- /dev/null +++ b/src-tauri/src/services/qrcode_service.rs @@ -0,0 +1,136 @@ +//! 二维码生成服务 +//! +//! 提供二维码生成的核心业务逻辑 + +use crate::error::{AppError, AppResult}; +use crate::models::qrcode::{QrConfig, QrResult}; +use crate::utils::qrcode_renderer::{image_to_base64, render_basic_qr}; + +/// 二维码生成服务 +pub struct QrCodeService; + +impl QrCodeService { + /// 生成二维码预览 + /// + /// 根据配置生成二维码并返回 Base64 编码的图片数据 + /// + /// # 参数 + /// + /// * `config` - 二维码配置 + /// + /// # 返回 + /// + /// 返回二维码生成结果,包含 Base64 编码的图片数据 + /// + /// # 错误 + /// + /// - 配置无效时返回 `AppError::InvalidData` + /// - 图片编码失败时返回 `AppError::IoError` + pub fn generate_preview(config: &QrConfig) -> AppResult { + // 验证尺寸 + if config.size < 128 || config.size > 4096 { + return Err(AppError::InvalidData( + "尺寸必须在 128 到 4096 之间".to_string(), + )); + } + + // 验证边距 + if config.margin > 50 { + return Err(AppError::InvalidData("边距不能超过 50".to_string())); + } + + // 渲染二维码 + let img = render_basic_qr(config)?; + + // 转换为 Base64 + let base64_data = image_to_base64(&img)?; + + Ok(QrResult { + data: base64_data, + format: "png".to_string(), + }) + } + + /// 生成二维码并保存到文件 + /// + /// 根据配置生成二维码并保存为 PNG 文件 + /// + /// # 参数 + /// + /// * `config` - 二维码配置 + /// * `output_path` - 输出文件路径 + /// + /// # 返回 + /// + /// 成功时返回 Ok(()),失败时返回错误 + /// + /// # 错误 + /// + /// - 配置无效时返回 `AppError::InvalidData` + /// - 文件写入失败时返回 `AppError::IoError` + pub fn generate_to_file(config: &QrConfig, output_path: &str) -> AppResult<()> { + // 验证尺寸 + if config.size < 128 || config.size > 4096 { + return Err(AppError::InvalidData( + "尺寸必须在 128 到 4096 之间".to_string(), + )); + } + + // 渲染二维码 + let img = render_basic_qr(config)?; + + // 保存到文件 + img.save(output_path) + .map_err(|e| AppError::IoError(format!("保存文件失败: {}", e)))?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_generate_preview() { + let config = QrConfig { + content: "https://example.com".to_string(), + size: 512, + margin: 4, + error_correction: "M".to_string(), + }; + + let result = QrCodeService::generate_preview(&config); + assert!(result.is_ok()); + + let qr_result = result.unwrap(); + assert!(qr_result.data.starts_with("data:image/png;base64,")); + assert_eq!(qr_result.format, "png"); + } + + #[test] + fn test_generate_preview_invalid_size() { + let config = QrConfig { + content: "https://example.com".to_string(), + size: 50, // 太小 + margin: 4, + error_correction: "M".to_string(), + }; + + let result = QrCodeService::generate_preview(&config); + assert!(result.is_err()); + } + + #[test] + fn test_generate_preview_invalid_margin() { + let config = QrConfig { + content: "https://example.com".to_string(), + size: 512, + margin: 100, // 太大 + error_correction: "M".to_string(), + }; + + let result = QrCodeService::generate_preview(&config); + assert!(result.is_err()); + } +} diff --git a/src-tauri/src/utils/mod.rs b/src-tauri/src/utils/mod.rs index 379fbc0..1221f93 100644 --- a/src-tauri/src/utils/mod.rs +++ b/src-tauri/src/utils/mod.rs @@ -4,5 +4,6 @@ pub mod color_conversion; pub mod json_formatter; +pub mod qrcode_renderer; pub mod screen; pub mod shortcut; diff --git a/src-tauri/src/utils/qrcode_renderer.rs b/src-tauri/src/utils/qrcode_renderer.rs new file mode 100644 index 0000000..2455c93 --- /dev/null +++ b/src-tauri/src/utils/qrcode_renderer.rs @@ -0,0 +1,153 @@ +//! 二维码渲染工具函数 +//! +//! 提供二维码矩阵到图像的渲染功能 + +use crate::error::{AppError, AppResult}; +use crate::models::qrcode::QrConfig; +use base64::Engine; +use image::Luma; +use image::Rgba; +use image::RgbaImage; +use qrcode::QrCode; +use std::io::Cursor; + +/// 渲染基础黑白二维码 +/// +/// 根据配置生成黑白二维码图片 +/// +/// # 参数 +/// +/// * `config` - 二维码配置 +/// +/// # 返回 +/// +/// 返回生成的图片数据 +/// +/// # 错误 +/// +/// - 二维码内容为空时返回 `InvalidData` +/// - 二维码生成失败时返回相应错误 +pub fn render_basic_qr(config: &QrConfig) -> AppResult { + // 验证内容 + if config.content.trim().is_empty() { + return Err(AppError::InvalidData( + "二维码内容不能为空".to_string(), + )); + } + + // 解析容错级别 + 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 级别 + }; + + // 生成二维码 + let qr_code = QrCode::with_error_correction_level(config.content.as_bytes(), ec_level) + .map_err(|e| AppError::InvalidData(format!("二维码生成失败: {}", e)))?; + + // 计算包含边距的尺寸 + 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 (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]); + } + + // 渲染二维码 + 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; + + // 绘制黑色矩形 + 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])); + } + } + } + } + } + + Ok(img) +} + +/// 将图片转换为 Base64 字符串 +/// +/// # 参数 +/// +/// * `img` - 图片数据 +/// +/// # 返回 +/// +/// 返回 Base64 编码的 PNG 图片数据(带 data URL 前缀) +pub fn image_to_base64(img: &RgbaImage) -> AppResult { + let mut bytes = Vec::new(); + + // 写入 PNG 格式 + let mut cursor = Cursor::new(&mut bytes); + img.write_to(&mut cursor, image::ImageFormat::Png) + .map_err(|e| AppError::IoError(format!("图片编码失败: {}", e)))?; + + // Base64 编码 + let base64_str = base64::engine::general_purpose::STANDARD.encode(&bytes); + + Ok(format!("data:image/png;base64,{}", base64_str)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_render_basic_qr() { + let config = QrConfig { + content: "https://example.com".to_string(), + size: 512, + margin: 4, + error_correction: "M".to_string(), + }; + + let result = render_basic_qr(&config); + assert!(result.is_ok()); + + let img = result.unwrap(); + assert_eq!(img.dimensions().0, 512); + assert_eq!(img.dimensions().1, 512); + } + + #[test] + fn test_render_empty_content() { + let config = QrConfig { + content: "".to_string(), + size: 512, + margin: 4, + error_correction: "M".to_string(), + }; + + let result = render_basic_qr(&config); + assert!(result.is_err()); + } +} diff --git a/src/App.tsx b/src/App.tsx index 440e58d..5c90d95 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,6 +7,7 @@ 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"; +import { QrCodeGeneratorPage } from "@/components/features/QrCodeGenerator/QrCodeGeneratorPage"; function App() { return ( @@ -23,6 +24,7 @@ function App() { } /> } /> } /> + } /> } /> diff --git a/src/components/features/QrCodeGenerator/QrCodeGeneratorPage.tsx b/src/components/features/QrCodeGenerator/QrCodeGeneratorPage.tsx new file mode 100644 index 0000000..ee16a73 --- /dev/null +++ b/src/components/features/QrCodeGenerator/QrCodeGeneratorPage.tsx @@ -0,0 +1,46 @@ +/** + * 二维码生成器主页面 + */ + +import { useEffect } from 'react'; +import { useDebounce } from '@uidotdev/usehooks'; +import { useQrStore } from '@/stores/qrcodeStore'; +import { QrConfigPanel } from './QrConfigPanel'; +import { QrPreview } from './QrPreview'; + +export function QrCodeGeneratorPage() { + const { config, updateConfig, generatePreview } = useQrStore(); + + // 防抖配置(300ms) + const debouncedConfig = useDebounce(config, 300); + + // 当配置改变时自动生成预览 + useEffect(() => { + if (debouncedConfig.content.trim()) { + generatePreview(); + } + }, [debouncedConfig]); + + return ( +
+ {/* 左侧配置面板 */} +
+
+

二维码生成器

+

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

+
+ +
+ + {/* 右侧预览区域 */} +
+ +
+
+ ); +} diff --git a/src/components/features/QrCodeGenerator/QrConfigPanel.tsx b/src/components/features/QrCodeGenerator/QrConfigPanel.tsx new file mode 100644 index 0000000..3850000 --- /dev/null +++ b/src/components/features/QrCodeGenerator/QrConfigPanel.tsx @@ -0,0 +1,134 @@ +/** + * 二维码配置面板 + */ + +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'; + +interface QrConfigPanelProps { + config: QrConfig; + onConfigChange: (updates: Partial) => void; +} + +export function QrConfigPanel({ config, onConfigChange }: QrConfigPanelProps) { + const { exportToFile, resetConfig, isGenerating } = useQrStore(); + + const handleExport = async () => { + try { + const outputPath = await save({ + title: '保存二维码', + defaultPath: `qrcode-${Date.now()}.png`, + filters: [ + { + name: 'PNG 图片', + extensions: ['png'], + }, + ], + }); + + if (outputPath) { + await exportToFile(outputPath); + } + } catch (err) { + console.error('保存失败:', err); + } + }; + + return ( +
+ {/* 基本配置 */} + + + {/* 内容输入 */} +
+ + 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({ 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" + /> +
+
+
+ + {/* 操作按钮 */} +
+ + +
+
+ ); +} diff --git a/src/components/features/QrCodeGenerator/QrPreview.tsx b/src/components/features/QrCodeGenerator/QrPreview.tsx new file mode 100644 index 0000000..8cb2223 --- /dev/null +++ b/src/components/features/QrCodeGenerator/QrPreview.tsx @@ -0,0 +1,50 @@ +/** + * 二维码预览组件 + */ + +import { Card } from '@/components/ui/card'; +import { useQrStore } from '@/stores/qrcodeStore'; +import { Loader2 } from 'lucide-react'; + +export function QrPreview() { + const { previewUrl, isGenerating, error, config } = useQrStore(); + + return ( +
+ {/* 预览卡片 */} + + {isGenerating ? ( +
+ +
+ ) : previewUrl ? ( + 二维码预览 + ) : ( +
+

输入内容后自动生成预览

+
+ )} +
+ + {/* 错误提示 */} + {error && ( +
+ {error} +
+ )} + + {/* 配置信息 */} + {previewUrl && !error && ( +
+

尺寸: {config.size}px

+

容错级别: {config.errorCorrection}

+

边距: {config.margin}

+
+ )} +
+ ); +} diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx new file mode 100644 index 0000000..683faa7 --- /dev/null +++ b/src/components/ui/label.tsx @@ -0,0 +1,24 @@ +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" +) + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)) +Label.displayName = LabelPrimitive.Root.displayName + +export { Label } diff --git a/src/features/data.ts b/src/features/data.ts index f6f8408..9622928 100644 --- a/src/features/data.ts +++ b/src/features/data.ts @@ -19,6 +19,16 @@ export const featuresData: Feature[] = [ tags: ['颜色', '取色', 'color', 'hex', 'rgb', 'hsl', '拾色器'], implemented: true, }, + { + id: 'qr-generator', + name: '二维码生成器', + description: '生成自定义二维码,支持多种尺寸和容错级别', + icon: 'QrCode', + category: 'tool', + route: '/feature/qr-generator', + tags: ['二维码', 'QR', '生成', 'qrcode', 'generator'], + implemented: true, + }, { id: 'screenshot', name: '截图工具', diff --git a/src/stores/qrcodeStore.ts b/src/stores/qrcodeStore.ts new file mode 100644 index 0000000..3b658f9 --- /dev/null +++ b/src/stores/qrcodeStore.ts @@ -0,0 +1,103 @@ +/** + * 二维码生成器状态管理 + */ + +import { create } from 'zustand'; +import { invoke } from '@tauri-apps/api/core'; +import type { QrConfig, QrResult } from '@/types/qrcode'; +import { DEFAULT_QR_CONFIG } from '@/types/qrcode'; + +interface QrState { + /** 当前配置 */ + config: QrConfig; + /** 预览图片 URL */ + previewUrl: string; + /** 是否正在生成 */ + isGenerating: boolean; + /** 错误信息 */ + error: string | null; + /** 更新配置 */ + updateConfig: (updates: Partial) => void; + /** 重置配置 */ + resetConfig: () => void; + /** 生成预览 */ + generatePreview: () => Promise; + /** 导出到文件 */ + exportToFile: (outputPath: string) => Promise; + /** 清除错误 */ + clearError: () => void; +} + +export const useQrStore = create((set, get) => ({ + config: DEFAULT_QR_CONFIG, + previewUrl: '', + isGenerating: false, + error: null, + + updateConfig: (updates) => { + set((state) => ({ + config: { ...state.config, ...updates }, + })); + }, + + resetConfig: () => { + set({ + config: DEFAULT_QR_CONFIG, + previewUrl: '', + error: null, + }); + }, + + generatePreview: async () => { + const { config } = get(); + + // 验证内容 + if (!config.content.trim()) { + set({ error: '二维码内容不能为空' }); + return; + } + + set({ isGenerating: true, error: null }); + + try { + const result = (await invoke('generate_qr_preview', { + config, + })) as QrResult; + + set({ previewUrl: result.data, error: null }); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : String(err); + set({ error: `生成失败: ${errorMessage}` }); + } finally { + set({ isGenerating: false }); + } + }, + + exportToFile: async (outputPath) => { + const { config } = get(); + + if (!config.content.trim()) { + set({ error: '二维码内容不能为空' }); + return; + } + + set({ isGenerating: true, error: null }); + + try { + await invoke('generate_qr_file', { + config, + outputPath, + }); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : String(err); + set({ error: `导出失败: ${errorMessage}` }); + throw err; + } finally { + set({ isGenerating: false }); + } + }, + + clearError: () => { + set({ error: null }); + }, +})); diff --git a/src/types/qrcode.ts b/src/types/qrcode.ts new file mode 100644 index 0000000..d21881f --- /dev/null +++ b/src/types/qrcode.ts @@ -0,0 +1,99 @@ +/** + * 二维码生成相关类型定义 + */ + +/** + * 二维码配置 + */ +export interface QrConfig { + /** 二维码内容 */ + content: string; + /** 输出尺寸(像素) */ + size: number; + /** 边距(模块数) */ + margin: number; + /** 容错级别 */ + errorCorrection: 'L' | 'M' | 'Q' | 'H'; +} + +/** + * 二维码样式(阶段 2 使用) + */ +export interface QrStyle { + /** 点形状 */ + dotShape: 'square' | 'circle' | 'rounded'; + /** 码眼形状 */ + eyeShape: 'square' | 'circle' | 'rounded'; + /** 前景色(Hex 颜色代码) */ + foregroundColor: string; + /** 背景色(Hex 颜色代码) */ + backgroundColor: string; + /** 是否使用渐变 */ + isGradient: boolean; + /** 渐变颜色列表 */ + gradientColors?: string[]; +} + +/** + * Logo 配置(阶段 2 使用) + */ +export interface LogoConfig { + /** Logo 文件路径 */ + path: string; + /** 缩放比例 (0.1 - 0.3) */ + scale: number; + /** 是否添加边框 */ + hasBorder: boolean; + /** 边框宽度(像素) */ + borderWidth: number; +} + +/** + * 二维码生成结果 + */ +export interface QrResult { + /** Base64 编码的图片数据(带 data URL 前缀) */ + data: string; + /** 图片格式(如 "png") */ + format: string; +} + +/** + * Tauri 命令类型声明 + */ +export type QrCodeCommands = { + /** 生成二维码预览 */ + generate_qr_preview: (config: QrConfig) => Promise; + /** 生成二维码并保存到文件 */ + generate_qr_file: (config: QrConfig, outputPath: string) => Promise; +}; + +/** + * 默认二维码配置 + */ +export const DEFAULT_QR_CONFIG: QrConfig = { + content: 'https://example.com', + size: 512, + margin: 4, + errorCorrection: 'M', +}; + +/** + * 容错级别选项 + */ +export const ERROR_CORRECTION_OPTIONS = [ + { value: 'L', label: 'L (7%)', description: '低容错率' }, + { value: 'M', label: 'M (15%)', description: '中容错率(推荐)' }, + { value: 'Q', label: 'Q (25%)', description: '高容错率' }, + { value: 'H', label: 'H (30%)', description: '最高容错率' }, +] as const; + +/** + * 预设尺寸选项 + */ +export const SIZE_PRESETS = [ + { value: 256, label: '小 (256px)' }, + { value: 512, label: '中 (512px)' }, + { value: 1024, label: '大 (1024px)' }, + { value: 2048, label: '超大 (2048px)' }, +] as const;