feat: 实现二维码高级渲染功能,支持自定义颜色、形状和 Logo 嵌入
This commit is contained in:
270
docs/Tauri_Naming_Conventions.md
Normal file
270
docs/Tauri_Naming_Conventions.md
Normal file
@@ -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<String> },
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 前端规范
|
||||||
|
- 使用 **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<String>,
|
||||||
|
pub name: String,
|
||||||
|
pub host: String,
|
||||||
|
pub port: u16,
|
||||||
|
pub username: String,
|
||||||
|
pub authMethod: AuthMethod,
|
||||||
|
pub terminalType: Option<String>,
|
||||||
|
pub columns: Option<u16>,
|
||||||
|
pub rows: Option<u16>,
|
||||||
|
#[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<String> },
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 前端完整示例
|
||||||
|
|
||||||
|
```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 项目命名规范
|
||||||
67
src-tauri/Cargo.lock
generated
67
src-tauri/Cargo.lock
generated
@@ -860,6 +860,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
|
checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.10.0",
|
"bitflags 2.10.0",
|
||||||
|
"block2",
|
||||||
|
"libc",
|
||||||
"objc2",
|
"objc2",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3574,6 +3576,30 @@ dependencies = [
|
|||||||
"web-sys",
|
"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]]
|
[[package]]
|
||||||
name = "rgb"
|
name = "rgb"
|
||||||
version = "0.8.52"
|
version = "0.8.52"
|
||||||
@@ -4224,6 +4250,7 @@ dependencies = [
|
|||||||
"sysinfo",
|
"sysinfo",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
|
"tauri-plugin-dialog",
|
||||||
"tauri-plugin-global-shortcut",
|
"tauri-plugin-global-shortcut",
|
||||||
"tauri-plugin-opener",
|
"tauri-plugin-opener",
|
||||||
"windows 0.58.0",
|
"windows 0.58.0",
|
||||||
@@ -4310,6 +4337,46 @@ dependencies = [
|
|||||||
"walkdir",
|
"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]]
|
[[package]]
|
||||||
name = "tauri-plugin-global-shortcut"
|
name = "tauri-plugin-global-shortcut"
|
||||||
version = "2.3.1"
|
version = "2.3.1"
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ tauri-build = { version = "2.4", features = [] }
|
|||||||
tauri = { version = "2.4", features = [] }
|
tauri = { version = "2.4", features = [] }
|
||||||
tauri-plugin-opener = "2.5"
|
tauri-plugin-opener = "2.5"
|
||||||
tauri-plugin-global-shortcut = "2"
|
tauri-plugin-global-shortcut = "2"
|
||||||
|
tauri-plugin-dialog = "2"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = { version = "1", features = ["preserve_order"] }
|
serde_json = { version = "1", features = ["preserve_order"] }
|
||||||
sysinfo = "0.30"
|
sysinfo = "0.30"
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
"core:window:allow-close",
|
"core:window:allow-close",
|
||||||
"core:window:allow-set-focus",
|
"core:window:allow-set-focus",
|
||||||
"core:window:allow-is-visible",
|
"core:window:allow-is-visible",
|
||||||
"opener:default"
|
"opener:default",
|
||||||
|
"dialog:allow-save",
|
||||||
|
"dialog:allow-open"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ pub fn run() {
|
|||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
||||||
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
// 预热取色器窗口:避免第一次取色出现“白屏闪一下”
|
// 预热取色器窗口:避免第一次取色出现“白屏闪一下”
|
||||||
// 窗口会以 hidden 状态创建,不会影响用户体验
|
// 窗口会以 hidden 状态创建,不会影响用户体验
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use crate::utils::color_conversion;
|
|||||||
///
|
///
|
||||||
/// 包含颜色的完整信息,支持多种颜色格式和屏幕坐标
|
/// 包含颜色的完整信息,支持多种颜色格式和屏幕坐标
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ColorInfo {
|
pub struct ColorInfo {
|
||||||
/// 十六进制颜色值(格式:#RRGGBB)
|
/// 十六进制颜色值(格式:#RRGGBB)
|
||||||
pub hex: String,
|
pub hex: String,
|
||||||
@@ -62,6 +63,7 @@ impl ColorInfo {
|
|||||||
///
|
///
|
||||||
/// 表示 RGB 颜色模式的颜色值
|
/// 表示 RGB 颜色模式的颜色值
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct RgbInfo {
|
pub struct RgbInfo {
|
||||||
/// 红色分量 (0-255)
|
/// 红色分量 (0-255)
|
||||||
pub r: u8,
|
pub r: u8,
|
||||||
@@ -75,6 +77,7 @@ pub struct RgbInfo {
|
|||||||
///
|
///
|
||||||
/// 表示 HSL 颜色模式的颜色值
|
/// 表示 HSL 颜色模式的颜色值
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct HslInfo {
|
pub struct HslInfo {
|
||||||
/// 色相 (0-360)
|
/// 色相 (0-360)
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
/// JSON 格式化配置
|
/// JSON 格式化配置
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct JsonFormatConfig {
|
pub struct JsonFormatConfig {
|
||||||
/// 缩进空格数(默认 2)
|
/// 缩进空格数(默认 2)
|
||||||
#[serde(default = "default_indent")]
|
#[serde(default = "default_indent")]
|
||||||
@@ -54,6 +55,7 @@ impl Default for JsonFormatConfig {
|
|||||||
|
|
||||||
/// JSON 格式化结果
|
/// JSON 格式化结果
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct JsonFormatResult {
|
pub struct JsonFormatResult {
|
||||||
/// 是否成功
|
/// 是否成功
|
||||||
pub success: bool,
|
pub success: bool,
|
||||||
@@ -70,6 +72,7 @@ pub struct JsonFormatResult {
|
|||||||
|
|
||||||
/// JSON 验证结果
|
/// JSON 验证结果
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct JsonValidateResult {
|
pub struct JsonValidateResult {
|
||||||
/// 是否有效的 JSON
|
/// 是否有效的 JSON
|
||||||
pub is_valid: bool,
|
pub is_valid: bool,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
///
|
///
|
||||||
/// 定义生成二维码所需的参数
|
/// 定义生成二维码所需的参数
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct QrConfig {
|
pub struct QrConfig {
|
||||||
/// 二维码内容
|
/// 二维码内容
|
||||||
pub content: String,
|
pub content: String,
|
||||||
@@ -15,12 +16,17 @@ pub struct QrConfig {
|
|||||||
pub margin: u32,
|
pub margin: u32,
|
||||||
/// 容错级别: "L", "M", "Q", "H"
|
/// 容错级别: "L", "M", "Q", "H"
|
||||||
pub error_correction: String,
|
pub error_correction: String,
|
||||||
|
/// 样式配置
|
||||||
|
pub style: Option<QrStyle>,
|
||||||
|
/// Logo 配置
|
||||||
|
pub logo: Option<LogoConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 二维码样式(阶段 2 使用)
|
/// 二维码样式
|
||||||
///
|
///
|
||||||
/// 定义二维码的视觉样式
|
/// 定义二维码的视觉样式
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct QrStyle {
|
pub struct QrStyle {
|
||||||
/// 点形状: "square", "circle", "rounded"
|
/// 点形状: "square", "circle", "rounded"
|
||||||
pub dot_shape: String,
|
pub dot_shape: String,
|
||||||
@@ -36,10 +42,24 @@ pub struct QrStyle {
|
|||||||
pub gradient_colors: Option<Vec<String>>,
|
pub gradient_colors: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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 的位置和样式
|
/// 定义 Logo 的位置和样式
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct LogoConfig {
|
pub struct LogoConfig {
|
||||||
/// Logo 文件路径
|
/// Logo 文件路径
|
||||||
pub path: String,
|
pub path: String,
|
||||||
@@ -55,6 +75,7 @@ pub struct LogoConfig {
|
|||||||
///
|
///
|
||||||
/// 包含生成的二维码图片数据
|
/// 包含生成的二维码图片数据
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct QrResult {
|
pub struct QrResult {
|
||||||
/// Base64 编码的图片数据
|
/// Base64 编码的图片数据
|
||||||
pub data: String,
|
pub data: String,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
/// 系统信息(完整版)
|
/// 系统信息(完整版)
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct SystemInfo {
|
pub struct SystemInfo {
|
||||||
/// 操作系统信息
|
/// 操作系统信息
|
||||||
pub os: OsInfo,
|
pub os: OsInfo,
|
||||||
@@ -29,6 +30,7 @@ pub struct SystemInfo {
|
|||||||
|
|
||||||
/// 操作系统信息
|
/// 操作系统信息
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct OsInfo {
|
pub struct OsInfo {
|
||||||
/// 操作系统名称
|
/// 操作系统名称
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -46,6 +48,7 @@ pub struct OsInfo {
|
|||||||
|
|
||||||
/// 硬件信息(主板、BIOS)
|
/// 硬件信息(主板、BIOS)
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct HardwareInfo {
|
pub struct HardwareInfo {
|
||||||
/// 制造商
|
/// 制造商
|
||||||
pub manufacturer: String,
|
pub manufacturer: String,
|
||||||
@@ -59,6 +62,7 @@ pub struct HardwareInfo {
|
|||||||
|
|
||||||
/// CPU 信息
|
/// CPU 信息
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct CpuInfo {
|
pub struct CpuInfo {
|
||||||
/// CPU 型号
|
/// CPU 型号
|
||||||
pub model: String,
|
pub model: String,
|
||||||
@@ -74,6 +78,7 @@ pub struct CpuInfo {
|
|||||||
|
|
||||||
/// 内存信息
|
/// 内存信息
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct MemoryInfo {
|
pub struct MemoryInfo {
|
||||||
/// 总内存 (GB)
|
/// 总内存 (GB)
|
||||||
pub total_gb: f64,
|
pub total_gb: f64,
|
||||||
@@ -87,6 +92,7 @@ pub struct MemoryInfo {
|
|||||||
|
|
||||||
/// GPU 信息
|
/// GPU 信息
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct GpuInfo {
|
pub struct GpuInfo {
|
||||||
/// GPU 名称
|
/// GPU 名称
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -98,6 +104,7 @@ pub struct GpuInfo {
|
|||||||
|
|
||||||
/// 磁盘信息
|
/// 磁盘信息
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct DiskInfo {
|
pub struct DiskInfo {
|
||||||
/// 盘符 (如 "C:")
|
/// 盘符 (如 "C:")
|
||||||
pub drive_letter: String,
|
pub drive_letter: String,
|
||||||
@@ -117,6 +124,7 @@ pub struct DiskInfo {
|
|||||||
|
|
||||||
/// 计算机信息
|
/// 计算机信息
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ComputerInfo {
|
pub struct ComputerInfo {
|
||||||
/// 计算机名称
|
/// 计算机名称
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -134,6 +142,7 @@ pub struct ComputerInfo {
|
|||||||
|
|
||||||
/// 显示器信息
|
/// 显示器信息
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct DisplayInfo {
|
pub struct DisplayInfo {
|
||||||
/// 屏幕数量
|
/// 屏幕数量
|
||||||
pub monitor_count: u32,
|
pub monitor_count: u32,
|
||||||
@@ -145,6 +154,7 @@ pub struct DisplayInfo {
|
|||||||
|
|
||||||
/// 网络信息
|
/// 网络信息
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct NetworkInfo {
|
pub struct NetworkInfo {
|
||||||
/// 网络接口列表
|
/// 网络接口列表
|
||||||
pub interfaces: Vec<InterfaceInfo>,
|
pub interfaces: Vec<InterfaceInfo>,
|
||||||
@@ -156,6 +166,7 @@ pub struct NetworkInfo {
|
|||||||
|
|
||||||
/// 网络接口信息
|
/// 网络接口信息
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct InterfaceInfo {
|
pub struct InterfaceInfo {
|
||||||
/// 接口名称
|
/// 接口名称
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
use crate::error::{AppError, AppResult};
|
use crate::error::{AppError, AppResult};
|
||||||
use crate::models::qrcode::{QrConfig, QrResult};
|
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;
|
pub struct QrCodeService;
|
||||||
@@ -39,8 +39,17 @@ impl QrCodeService {
|
|||||||
return Err(AppError::InvalidData("边距不能超过 50".to_string()));
|
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
|
// 转换为 Base64
|
||||||
let base64_data = image_to_base64(&img)?;
|
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)
|
img.save(output_path)
|
||||||
@@ -90,6 +113,7 @@ impl QrCodeService {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::models::qrcode::{QrStyle};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_generate_preview() {
|
fn test_generate_preview() {
|
||||||
@@ -98,6 +122,8 @@ mod tests {
|
|||||||
size: 512,
|
size: 512,
|
||||||
margin: 4,
|
margin: 4,
|
||||||
error_correction: "M".to_string(),
|
error_correction: "M".to_string(),
|
||||||
|
style: None,
|
||||||
|
logo: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = QrCodeService::generate_preview(&config);
|
let result = QrCodeService::generate_preview(&config);
|
||||||
@@ -108,6 +134,30 @@ mod tests {
|
|||||||
assert_eq!(qr_result.format, "png");
|
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]
|
#[test]
|
||||||
fn test_generate_preview_invalid_size() {
|
fn test_generate_preview_invalid_size() {
|
||||||
let config = QrConfig {
|
let config = QrConfig {
|
||||||
@@ -115,6 +165,8 @@ mod tests {
|
|||||||
size: 50, // 太小
|
size: 50, // 太小
|
||||||
margin: 4,
|
margin: 4,
|
||||||
error_correction: "M".to_string(),
|
error_correction: "M".to_string(),
|
||||||
|
style: None,
|
||||||
|
logo: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = QrCodeService::generate_preview(&config);
|
let result = QrCodeService::generate_preview(&config);
|
||||||
@@ -128,6 +180,8 @@ mod tests {
|
|||||||
size: 512,
|
size: 512,
|
||||||
margin: 100, // 太大
|
margin: 100, // 太大
|
||||||
error_correction: "M".to_string(),
|
error_correction: "M".to_string(),
|
||||||
|
style: None,
|
||||||
|
logo: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = QrCodeService::generate_preview(&config);
|
let result = QrCodeService::generate_preview(&config);
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
//! 二维码渲染工具函数
|
//! 二维码渲染工具函数
|
||||||
//!
|
//!
|
||||||
//! 提供二维码矩阵到图像的渲染功能
|
//! 提供二维码矩阵到图像的渲染功能,支持颜色、形状和 Logo
|
||||||
|
|
||||||
use crate::error::{AppError, AppResult};
|
use crate::error::{AppError, AppResult};
|
||||||
use crate::models::qrcode::QrConfig;
|
use crate::models::qrcode::{QrConfig, QrStyle};
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use image::Luma;
|
use image::imageops::overlay;
|
||||||
use image::Rgba;
|
use image::{ImageReader, Luma, Rgba, RgbaImage};
|
||||||
use image::RgbaImage;
|
use qrcode::{QrCode, EcLevel};
|
||||||
use qrcode::QrCode;
|
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
/// 渲染基础黑白二维码
|
/// 渲染二维码
|
||||||
///
|
///
|
||||||
/// 根据配置生成黑白二维码图片
|
/// 根据配置生成二维码图片,支持颜色、形状和 Logo
|
||||||
///
|
///
|
||||||
/// # 参数
|
/// # 参数
|
||||||
///
|
///
|
||||||
@@ -27,7 +27,7 @@ use std::io::Cursor;
|
|||||||
///
|
///
|
||||||
/// - 二维码内容为空时返回 `InvalidData`
|
/// - 二维码内容为空时返回 `InvalidData`
|
||||||
/// - 二维码生成失败时返回相应错误
|
/// - 二维码生成失败时返回相应错误
|
||||||
pub fn render_basic_qr(config: &QrConfig) -> AppResult<RgbaImage> {
|
pub fn render_qr(config: &QrConfig) -> AppResult<RgbaImage> {
|
||||||
// 验证内容
|
// 验证内容
|
||||||
if config.content.trim().is_empty() {
|
if config.content.trim().is_empty() {
|
||||||
return Err(AppError::InvalidData(
|
return Err(AppError::InvalidData(
|
||||||
@@ -37,61 +37,324 @@ pub fn render_basic_qr(config: &QrConfig) -> AppResult<RgbaImage> {
|
|||||||
|
|
||||||
// 解析容错级别
|
// 解析容错级别
|
||||||
let ec_level = match config.error_correction.as_str() {
|
let ec_level = match config.error_correction.as_str() {
|
||||||
"L" => qrcode::EcLevel::L,
|
"L" => EcLevel::L,
|
||||||
"M" => qrcode::EcLevel::M,
|
"M" => EcLevel::M,
|
||||||
"Q" => qrcode::EcLevel::Q,
|
"Q" => EcLevel::Q,
|
||||||
"H" => qrcode::EcLevel::H,
|
"H" => EcLevel::H,
|
||||||
_ => qrcode::EcLevel::M, // 默认使用 M 级别
|
_ => EcLevel::M,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 生成二维码
|
// 生成二维码
|
||||||
let qr_code = QrCode::with_error_correction_level(config.content.as_bytes(), ec_level)
|
let qr_code = QrCode::with_error_correction_level(config.content.as_bytes(), ec_level)
|
||||||
.map_err(|e| AppError::InvalidData(format!("二维码生成失败: {}", e)))?;
|
.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<RgbaImage> {
|
||||||
let qr_size = qr_code.width() as u32;
|
let qr_size = qr_code.width() as u32;
|
||||||
let total_size = qr_size + 2 * config.margin;
|
let total_size = qr_size + 2 * config.margin;
|
||||||
|
|
||||||
// 创建图像缓冲区(Luma 格式,用于生成二维码)
|
// 创建基础图像
|
||||||
let qr_image = qr_code.render::<Luma<u8>>().quiet_zone(false).min_dimensions(total_size, total_size).max_dimensions(total_size, total_size).build();
|
let qr_image = qr_code
|
||||||
|
.render::<Luma<u8>>()
|
||||||
|
.quiet_zone(false)
|
||||||
|
.min_dimensions(total_size, total_size)
|
||||||
|
.max_dimensions(total_size, total_size)
|
||||||
|
.build();
|
||||||
|
|
||||||
// 获取二维码图像尺寸
|
|
||||||
let (width, height) = qr_image.dimensions();
|
let (width, height) = qr_image.dimensions();
|
||||||
|
|
||||||
// 计算缩放比例以匹配目标尺寸
|
|
||||||
let scale = (config.size as f32 / width as f32).max(1.0) as u32;
|
let scale = (config.size as f32 / width as f32).max(1.0) as u32;
|
||||||
let scaled_width = width * scale;
|
let scaled_width = width * scale;
|
||||||
let scaled_height = height * scale;
|
let scaled_height = height * scale;
|
||||||
|
|
||||||
// 创建 RGBA 图像并填充白色背景
|
// 解析颜色
|
||||||
let mut img = RgbaImage::new(scaled_width, scaled_height);
|
let bg_color = parse_hex_color(&style.background_color);
|
||||||
for pixel in img.pixels_mut() {
|
let fg_color = parse_hex_color(&style.foreground_color);
|
||||||
*pixel = Rgba([255, 255, 255, 255]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 渲染二维码
|
// 创建 RGBA 图像
|
||||||
|
let mut img = RgbaImage::new(scaled_width, scaled_height);
|
||||||
|
|
||||||
|
// 渲染每个模块
|
||||||
for y in 0..height {
|
for y in 0..height {
|
||||||
for x in 0..width {
|
for x in 0..width {
|
||||||
let pixel = qr_image.get_pixel(x, y);
|
let pixel = qr_image.get_pixel(x, y);
|
||||||
// 如果是黑色像素
|
let is_dark = pixel[0] == 0;
|
||||||
if pixel[0] == 0 {
|
let color = if is_dark { fg_color } else { bg_color };
|
||||||
|
|
||||||
// 计算缩放后的区域
|
// 计算缩放后的区域
|
||||||
let start_x = x * scale;
|
let start_x = x * scale;
|
||||||
let start_y = y * scale;
|
let start_y = y * scale;
|
||||||
let end_x = start_x + scale;
|
let end_x = start_x + scale;
|
||||||
let end_y = start_y + scale;
|
let end_y = start_y + scale;
|
||||||
|
|
||||||
// 绘制黑色矩形
|
// 绘制模块
|
||||||
for py in start_y..end_y.min(scaled_height) {
|
draw_shape(
|
||||||
for px in start_x..end_x.min(scaled_width) {
|
&mut img,
|
||||||
img.put_pixel(px, py, Rgba([0, 0, 0, 255]));
|
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<RgbaImage> {
|
||||||
|
let qr_size = qr_code.width() as u32;
|
||||||
|
let total_size = qr_size + 2 * config.margin;
|
||||||
|
|
||||||
|
// 创建基础图像
|
||||||
|
let qr_image = qr_code
|
||||||
|
.render::<Luma<u8>>()
|
||||||
|
.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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(img)
|
/// 叠加 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 叠加 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 字符串
|
/// 将图片转换为 Base64 字符串
|
||||||
@@ -120,6 +383,7 @@ pub fn image_to_base64(img: &RgbaImage) -> AppResult<String> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::models::qrcode::{QrConfig, QrStyle};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_render_basic_qr() {
|
fn test_render_basic_qr() {
|
||||||
@@ -128,26 +392,46 @@ mod tests {
|
|||||||
size: 512,
|
size: 512,
|
||||||
margin: 4,
|
margin: 4,
|
||||||
error_correction: "M".to_string(),
|
error_correction: "M".to_string(),
|
||||||
|
style: None,
|
||||||
|
logo: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = render_basic_qr(&config);
|
let result = render_qr(&config);
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
|
|
||||||
let img = result.unwrap();
|
let img = result.unwrap();
|
||||||
assert_eq!(img.dimensions().0, 512);
|
assert!(img.dimensions().0 >= 512);
|
||||||
assert_eq!(img.dimensions().1, 512);
|
assert!(img.dimensions().1 >= 512);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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 {
|
let config = QrConfig {
|
||||||
content: "".to_string(),
|
content: "https://example.com".to_string(),
|
||||||
size: 512,
|
size: 512,
|
||||||
margin: 4,
|
margin: 4,
|
||||||
error_correction: "M".to_string(),
|
error_correction: "M".to_string(),
|
||||||
|
style: Some(style),
|
||||||
|
logo: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = render_basic_qr(&config);
|
let result = render_qr(&config);
|
||||||
assert!(result.is_err());
|
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]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,8 @@
|
|||||||
{
|
{
|
||||||
"title": "CmdRs - 功能集合",
|
"title": "CmdRs - 功能集合",
|
||||||
"label": "main",
|
"label": "main",
|
||||||
"width": 1200,
|
"width": 800,
|
||||||
"height": 800,
|
"height": 600,
|
||||||
"minWidth": 800,
|
"minWidth": 800,
|
||||||
"minHeight": 600,
|
"minHeight": 600,
|
||||||
"decorations": true,
|
"decorations": true,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { QrCodeGeneratorPage } from "@/components/features/QrCodeGenerator/QrCod
|
|||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider defaultTheme="system" storageKey="vite-ui-theme">
|
<ThemeProvider defaultTheme="system" storageKey="vite-ui-theme">
|
||||||
<div className="w-screen h-screen">
|
<div className="w-full h-screen overflow-hidden">
|
||||||
{/* 全局快捷键监听 */}
|
{/* 全局快捷键监听 */}
|
||||||
<CommandPalette />
|
<CommandPalette />
|
||||||
|
|
||||||
|
|||||||
@@ -6,14 +6,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
|||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Copy, Check, Droplet, RefreshCw } from 'lucide-react';
|
import { Copy, Check, Droplet, RefreshCw } from 'lucide-react';
|
||||||
|
import type { ColorInfo } from '@/types/color';
|
||||||
interface ColorInfo {
|
|
||||||
hex: string;
|
|
||||||
rgb: { r: number; g: number; b: number };
|
|
||||||
hsl: { h: number; s: number; l: number };
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ColorHistory {
|
interface ColorHistory {
|
||||||
color: ColorInfo;
|
color: ColorInfo;
|
||||||
@@ -70,9 +63,9 @@ export function ColorPickerPage() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<div className="flex flex-col h-screen bg-background">
|
||||||
{/* 顶部导航栏 */}
|
{/* 顶部导航栏 */}
|
||||||
<header className="border-b bg-background/95 backdrop-blur">
|
<header className="border-b bg-background/95 backdrop-blur flex-shrink-0">
|
||||||
<div className="container mx-auto px-4 py-4">
|
<div className="container mx-auto px-4 py-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Button variant="ghost" size="sm" onClick={() => window.history.back()}>
|
<Button variant="ghost" size="sm" onClick={() => window.history.back()}>
|
||||||
@@ -87,7 +80,7 @@ export function ColorPickerPage() {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* 主内容区 */}
|
{/* 主内容区 */}
|
||||||
<main className="container mx-auto px-4 py-6">
|
<main className="flex-1 container mx-auto px-4 py-6 overflow-y-auto">
|
||||||
<div className="max-w-4xl mx-auto space-y-6">
|
<div className="max-w-4xl mx-auto space-y-6">
|
||||||
{/* 拾色按钮 */}
|
{/* 拾色按钮 */}
|
||||||
<Card>
|
<Card>
|
||||||
|
|||||||
@@ -4,27 +4,7 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Copy, Check, FileCode, Sparkles, Minimize2, CheckCircle2, XCircle, Upload } from 'lucide-react';
|
import { Copy, Check, FileCode, Sparkles, Minimize2, CheckCircle2, XCircle, Upload } from 'lucide-react';
|
||||||
|
import type { JsonFormatConfig, JsonFormatResult, JsonValidateResult } from '@/types/json';
|
||||||
// 类型定义
|
|
||||||
interface JsonFormatConfig {
|
|
||||||
indent: number;
|
|
||||||
sort_keys: boolean;
|
|
||||||
mode: 'pretty' | 'compact';
|
|
||||||
}
|
|
||||||
|
|
||||||
interface JsonFormatResult {
|
|
||||||
success: boolean;
|
|
||||||
result: string;
|
|
||||||
error: string | null;
|
|
||||||
is_valid: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface JsonValidateResult {
|
|
||||||
is_valid: boolean;
|
|
||||||
error_message: string | null;
|
|
||||||
error_line: number | null;
|
|
||||||
error_column: number | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function JsonFormatterPage() {
|
export function JsonFormatterPage() {
|
||||||
const [input, setInput] = useState('');
|
const [input, setInput] = useState('');
|
||||||
@@ -32,7 +12,7 @@ export function JsonFormatterPage() {
|
|||||||
const [validation, setValidation] = useState<JsonValidateResult | null>(null);
|
const [validation, setValidation] = useState<JsonValidateResult | null>(null);
|
||||||
const [config, setConfig] = useState<JsonFormatConfig>({
|
const [config, setConfig] = useState<JsonFormatConfig>({
|
||||||
indent: 2,
|
indent: 2,
|
||||||
sort_keys: false,
|
sortKeys: false,
|
||||||
mode: 'pretty',
|
mode: 'pretty',
|
||||||
});
|
});
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
@@ -151,9 +131,9 @@ export function JsonFormatterPage() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<div className="flex flex-col h-screen bg-background">
|
||||||
{/* 顶部导航栏 */}
|
{/* 顶部导航栏 */}
|
||||||
<header className="border-b bg-background/95 backdrop-blur">
|
<header className="border-b bg-background/95 backdrop-blur flex-shrink-0">
|
||||||
<div className="container mx-auto px-4 py-4">
|
<div className="container mx-auto px-4 py-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Button variant="ghost" size="sm" onClick={() => window.history.back()}>
|
<Button variant="ghost" size="sm" onClick={() => window.history.back()}>
|
||||||
@@ -168,7 +148,7 @@ export function JsonFormatterPage() {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* 主内容区 */}
|
{/* 主内容区 */}
|
||||||
<main className="container mx-auto px-4 py-6">
|
<main className="flex-1 container mx-auto px-4 py-6 overflow-y-auto">
|
||||||
<div className="max-w-6xl mx-auto space-y-6">
|
<div className="max-w-6xl mx-auto space-y-6">
|
||||||
{/* 配置选项 */}
|
{/* 配置选项 */}
|
||||||
<Card>
|
<Card>
|
||||||
@@ -200,10 +180,10 @@ export function JsonFormatterPage() {
|
|||||||
<label className="text-sm font-medium">排序 Keys:</label>
|
<label className="text-sm font-medium">排序 Keys:</label>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant={config.sort_keys ? 'default' : 'outline'}
|
variant={config.sortKeys ? 'default' : 'outline'}
|
||||||
onClick={() => setConfig({ ...config, sort_keys: !config.sort_keys })}
|
onClick={() => setConfig({ ...config, sortKeys: !config.sortKeys })}
|
||||||
>
|
>
|
||||||
{config.sort_keys ? '开启' : '关闭'}
|
{config.sortKeys ? '开启' : '关闭'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -270,7 +250,7 @@ export function JsonFormatterPage() {
|
|||||||
{/* 验证状态指示器 */}
|
{/* 验证状态指示器 */}
|
||||||
{validation && (
|
{validation && (
|
||||||
<div className="absolute top-2 right-2">
|
<div className="absolute top-2 right-2">
|
||||||
{validation.is_valid ? (
|
{validation.isValid ? (
|
||||||
<Badge variant="default" className="gap-1 bg-green-500 hover:bg-green-600">
|
<Badge variant="default" className="gap-1 bg-green-500 hover:bg-green-600">
|
||||||
<CheckCircle2 className="w-3 h-3" />
|
<CheckCircle2 className="w-3 h-3" />
|
||||||
有效
|
有效
|
||||||
@@ -286,14 +266,14 @@ export function JsonFormatterPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 错误信息 */}
|
{/* 错误信息 */}
|
||||||
{validation && !validation.is_valid && validation.error_message && (
|
{validation && !validation.isValid && validation.errorMessage && (
|
||||||
<div className="mt-3 p-3 bg-destructive/10 border border-destructive/20 rounded-lg">
|
<div className="mt-3 p-3 bg-destructive/10 border border-destructive/20 rounded-lg">
|
||||||
<p className="text-sm text-destructive font-medium">
|
<p className="text-sm text-destructive font-medium">
|
||||||
{validation.error_message}
|
{validation.errorMessage}
|
||||||
</p>
|
</p>
|
||||||
{(validation.error_line || validation.error_column) && (
|
{(validation.errorLine || validation.errorColumn) && (
|
||||||
<p className="text-xs text-destructive/80 mt-1">
|
<p className="text-xs text-destructive/80 mt-1">
|
||||||
位置: 行 {validation.error_line}, 列 {validation.error_column}
|
位置: 行 {validation.errorLine}, 列 {validation.errorColumn}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -303,7 +283,7 @@ export function JsonFormatterPage() {
|
|||||||
<div className="flex gap-2 mt-4">
|
<div className="flex gap-2 mt-4">
|
||||||
<Button
|
<Button
|
||||||
onClick={formatJson}
|
onClick={formatJson}
|
||||||
disabled={!input.trim() || isProcessing || !validation?.is_valid}
|
disabled={!input.trim() || isProcessing || !validation?.isValid}
|
||||||
className="flex-1 gap-2"
|
className="flex-1 gap-2"
|
||||||
>
|
>
|
||||||
{isProcessing ? (
|
{isProcessing ? (
|
||||||
@@ -321,7 +301,7 @@ export function JsonFormatterPage() {
|
|||||||
<Button
|
<Button
|
||||||
onClick={compactJson}
|
onClick={compactJson}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={!input.trim() || isProcessing || !validation?.is_valid}
|
disabled={!input.trim() || isProcessing || !validation?.isValid}
|
||||||
>
|
>
|
||||||
<Minimize2 className="w-4 h-4 mr-1" />
|
<Minimize2 className="w-4 h-4 mr-1" />
|
||||||
压缩
|
压缩
|
||||||
|
|||||||
115
src/components/features/QrCodeGenerator/LogoUpload.tsx
Normal file
115
src/components/features/QrCodeGenerator/LogoUpload.tsx
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
/**
|
||||||
|
* Logo 上传组件
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { useQrStore } from '@/stores/qrcodeStore';
|
||||||
|
import { Image as ImageIcon, Upload, X } from 'lucide-react';
|
||||||
|
|
||||||
|
export function LogoUpload() {
|
||||||
|
const { config, selectLogoFile, clearLogo, updateLogo } = useQrStore();
|
||||||
|
const logo = config.logo;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-base">
|
||||||
|
<ImageIcon className="w-4 h-4" />
|
||||||
|
Logo 配置
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{!logo ? (
|
||||||
|
// 未选择 Logo 时显示上传按钮
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full"
|
||||||
|
onClick={selectLogoFile}
|
||||||
|
>
|
||||||
|
<Upload className="w-4 h-4 mr-2" />
|
||||||
|
选择 Logo 图片
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
// 已选择 Logo 时显示配置
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 文件名 */}
|
||||||
|
<div className="flex items-center justify-between p-2 bg-muted rounded-md">
|
||||||
|
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||||
|
<ImageIcon className="w-4 h-4 text-muted-foreground flex-shrink-0" />
|
||||||
|
<span className="text-sm truncate">
|
||||||
|
{logo.path.split(/[/\\]/).pop()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 w-7 p-0"
|
||||||
|
onClick={clearLogo}
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Logo 缩放比例 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label className="text-xs">缩放比例: {(logo.scale * 100).toFixed(0)}%</Label>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
type="range"
|
||||||
|
min="5"
|
||||||
|
max="30"
|
||||||
|
value={logo.scale * 100}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateLogo({ scale: Number(e.target.value) / 100 })
|
||||||
|
}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
建议 10%-20%,过大可能影响扫码
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 边框选项 */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="logo-border"
|
||||||
|
checked={logo.hasBorder}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateLogo({ hasBorder: e.target.checked })
|
||||||
|
}
|
||||||
|
className="w-4 h-4"
|
||||||
|
/>
|
||||||
|
<Label htmlFor="logo-border" className="text-xs cursor-pointer">
|
||||||
|
添加白色边框
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 边框宽度 */}
|
||||||
|
{logo.hasBorder && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-xs">边框宽度: {logo.borderWidth}px</Label>
|
||||||
|
<Input
|
||||||
|
type="range"
|
||||||
|
min="1"
|
||||||
|
max="20"
|
||||||
|
value={logo.borderWidth}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateLogo({ borderWidth: Number(e.target.value) })
|
||||||
|
}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,9 +4,11 @@
|
|||||||
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useDebounce } from '@uidotdev/usehooks';
|
import { useDebounce } from '@uidotdev/usehooks';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
import { useQrStore } from '@/stores/qrcodeStore';
|
import { useQrStore } from '@/stores/qrcodeStore';
|
||||||
import { QrConfigPanel } from './QrConfigPanel';
|
import { QrConfigPanel } from './QrConfigPanel';
|
||||||
import { QrPreview } from './QrPreview';
|
import { QrPreview } from './QrPreview';
|
||||||
|
import { QrCode } from 'lucide-react';
|
||||||
|
|
||||||
export function QrCodeGeneratorPage() {
|
export function QrCodeGeneratorPage() {
|
||||||
const { config, updateConfig, generatePreview } = useQrStore();
|
const { config, updateConfig, generatePreview } = useQrStore();
|
||||||
@@ -22,25 +24,45 @@ export function QrCodeGeneratorPage() {
|
|||||||
}, [debouncedConfig]);
|
}, [debouncedConfig]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full bg-background">
|
<div className="flex flex-col h-screen bg-background">
|
||||||
{/* 左侧配置面板 */}
|
{/* 顶部导航栏 */}
|
||||||
<div className="w-96 border-r border-border bg-card p-6 overflow-y-auto">
|
<header className="border-b bg-background/95 backdrop-blur flex-shrink-0">
|
||||||
<div className="mb-6">
|
<div className="container mx-auto px-4 py-4">
|
||||||
<h1 className="text-2xl font-bold">二维码生成器</h1>
|
<div className="flex items-center gap-3">
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
<Button variant="ghost" size="sm" onClick={() => window.history.back()}>
|
||||||
生成自定义二维码图片
|
← 返回
|
||||||
</p>
|
</Button>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<QrCode className="w-6 h-6 text-primary" />
|
||||||
|
<h1 className="text-xl font-bold">二维码生成器</h1>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* 主内容区 */}
|
||||||
|
<main className="flex-1 container mx-auto px-4 py-6 overflow-y-auto">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
{/* 左侧配置面板 */}
|
||||||
|
<div className="lg:col-span-1">
|
||||||
|
<div className="sticky top-6">
|
||||||
<QrConfigPanel
|
<QrConfigPanel
|
||||||
config={config}
|
config={config}
|
||||||
onConfigChange={updateConfig}
|
onConfigChange={updateConfig}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 右侧预览区域 */}
|
{/* 右侧预览区域 */}
|
||||||
<div className="flex-1 flex items-center justify-center p-8 bg-muted/20">
|
<div className="lg:col-span-2">
|
||||||
|
<div className="flex items-center justify-center min-h-[500px] bg-muted/20 rounded-lg border-2 border-dashed border-border">
|
||||||
<QrPreview />
|
<QrPreview />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,13 @@
|
|||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { useQrStore } from '@/stores/qrcodeStore';
|
import { useQrStore } from '@/stores/qrcodeStore';
|
||||||
import type { QrConfig } from '@/types/qrcode';
|
import type { QrConfig } from '@/types/qrcode';
|
||||||
import { ERROR_CORRECTION_OPTIONS, SIZE_PRESETS } from '@/types/qrcode';
|
import { ERROR_CORRECTION_OPTIONS, SIZE_PRESETS } from '@/types/qrcode';
|
||||||
import { Download, RotateCcw } from 'lucide-react';
|
import { Download, RotateCcw } from 'lucide-react';
|
||||||
import { save } from '@tauri-apps/plugin-dialog';
|
import { StyleOptions } from './StyleOptions';
|
||||||
|
import { LogoUpload } from './LogoUpload';
|
||||||
|
|
||||||
interface QrConfigPanelProps {
|
interface QrConfigPanelProps {
|
||||||
config: QrConfig;
|
config: QrConfig;
|
||||||
@@ -23,6 +23,7 @@ export function QrConfigPanel({ config, onConfigChange }: QrConfigPanelProps) {
|
|||||||
|
|
||||||
const handleExport = async () => {
|
const handleExport = async () => {
|
||||||
try {
|
try {
|
||||||
|
const { save } = await import('@tauri-apps/plugin-dialog');
|
||||||
const outputPath = await save({
|
const outputPath = await save({
|
||||||
title: '保存二维码',
|
title: '保存二维码',
|
||||||
defaultPath: `qrcode-${Date.now()}.png`,
|
defaultPath: `qrcode-${Date.now()}.png`,
|
||||||
@@ -43,11 +44,9 @@ export function QrConfigPanel({ config, onConfigChange }: QrConfigPanelProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-4">
|
||||||
{/* 基本配置 */}
|
{/* 基本配置 */}
|
||||||
<Card>
|
<div className="space-y-4">
|
||||||
<CardContent className="pt-6 space-y-4">
|
|
||||||
{/* 内容输入 */}
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="content">二维码内容</Label>
|
<Label htmlFor="content">二维码内容</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -115,11 +114,16 @@ export function QrConfigPanel({ config, onConfigChange }: QrConfigPanelProps) {
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
|
||||||
|
{/* 样式配置 */}
|
||||||
|
<StyleOptions />
|
||||||
|
|
||||||
|
{/* Logo 配置 */}
|
||||||
|
<LogoUpload />
|
||||||
|
|
||||||
{/* 操作按钮 */}
|
{/* 操作按钮 */}
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2 pt-2">
|
||||||
<Button onClick={handleExport} disabled={isGenerating} className="flex-1">
|
<Button onClick={handleExport} disabled={isGenerating} className="flex-1">
|
||||||
<Download className="w-4 h-4 mr-2" />
|
<Download className="w-4 h-4 mr-2" />
|
||||||
导出 PNG
|
导出 PNG
|
||||||
|
|||||||
264
src/components/features/QrCodeGenerator/StyleOptions.tsx
Normal file
264
src/components/features/QrCodeGenerator/StyleOptions.tsx
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
/**
|
||||||
|
* 样式配置组件
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
import { useQrStore } from '@/stores/qrcodeStore';
|
||||||
|
import {
|
||||||
|
DOT_SHAPE_OPTIONS,
|
||||||
|
EYE_SHAPE_OPTIONS,
|
||||||
|
COLOR_PRESETS,
|
||||||
|
} from '@/types/qrcode';
|
||||||
|
import { Palette, Sparkles, Trash2 } from 'lucide-react';
|
||||||
|
|
||||||
|
export function StyleOptions() {
|
||||||
|
const { config, updateStyle } = useQrStore();
|
||||||
|
const style = config.style!;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-base">
|
||||||
|
<Palette className="w-4 h-4" />
|
||||||
|
样式配置
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{/* 点形状 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>点形状</Label>
|
||||||
|
<Tabs
|
||||||
|
value={style.dotShape}
|
||||||
|
onValueChange={(value) => updateStyle({ dotShape: value as any })}
|
||||||
|
>
|
||||||
|
<TabsList className="grid w-full grid-cols-3">
|
||||||
|
{DOT_SHAPE_OPTIONS.map((option) => (
|
||||||
|
<TabsTrigger key={option.value} value={option.value}>
|
||||||
|
<span className="mr-1">{option.icon}</span>
|
||||||
|
{option.label}
|
||||||
|
</TabsTrigger>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 码眼形状 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>码眼形状</Label>
|
||||||
|
<Tabs
|
||||||
|
value={style.eyeShape}
|
||||||
|
onValueChange={(value) => updateStyle({ eyeShape: value as any })}
|
||||||
|
>
|
||||||
|
<TabsList className="grid w-full grid-cols-3">
|
||||||
|
{EYE_SHAPE_OPTIONS.map((option) => (
|
||||||
|
<TabsTrigger key={option.value} value={option.value}>
|
||||||
|
<span className="mr-1">{option.icon}</span>
|
||||||
|
{option.label}
|
||||||
|
</TabsTrigger>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 颜色配置 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label>颜色</Label>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 text-xs"
|
||||||
|
onClick={() =>
|
||||||
|
updateStyle({
|
||||||
|
foregroundColor: '#000000',
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
isGradient: false,
|
||||||
|
gradientColors: undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-3 h-3 mr-1" />
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 预设颜色 */}
|
||||||
|
<div className="grid grid-cols-3 gap-2">
|
||||||
|
{COLOR_PRESETS.map((preset) => (
|
||||||
|
<Button
|
||||||
|
key={preset.name}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 text-xs"
|
||||||
|
onClick={() =>
|
||||||
|
updateStyle({
|
||||||
|
foregroundColor: preset.foreground,
|
||||||
|
backgroundColor: preset.background,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<div
|
||||||
|
className="w-3 h-3 rounded-full border"
|
||||||
|
style={{ backgroundColor: preset.foreground }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="w-3 h-3 rounded-full border"
|
||||||
|
style={{ backgroundColor: preset.background }}
|
||||||
|
/>
|
||||||
|
<span>{preset.name}</span>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 自定义颜色 */}
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs">前景色</Label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
type="color"
|
||||||
|
value={style.foregroundColor}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateStyle({ foregroundColor: e.target.value })
|
||||||
|
}
|
||||||
|
className="w-12 h-8 p-0 border-0"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={style.foregroundColor}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateStyle({ foregroundColor: e.target.value })
|
||||||
|
}
|
||||||
|
className="flex-1 font-mono text-xs"
|
||||||
|
placeholder="#000000"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs">背景色</Label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
type="color"
|
||||||
|
value={style.backgroundColor}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateStyle({ backgroundColor: e.target.value })
|
||||||
|
}
|
||||||
|
className="w-12 h-8 p-0 border-0"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={style.backgroundColor}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateStyle({ backgroundColor: e.target.value })
|
||||||
|
}
|
||||||
|
className="flex-1 font-mono text-xs"
|
||||||
|
placeholder="#FFFFFF"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 渐变选项 */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="gradient"
|
||||||
|
checked={style.isGradient}
|
||||||
|
onChange={(e) => updateStyle({ isGradient: e.target.checked })}
|
||||||
|
className="w-4 h-4"
|
||||||
|
/>
|
||||||
|
<Label htmlFor="gradient" className="text-xs cursor-pointer">
|
||||||
|
启用渐变
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<Sparkles className="w-4 h-4 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{style.isGradient && (
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs">渐变起始色</Label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
type="color"
|
||||||
|
value={
|
||||||
|
style.gradientColors?.[0] || style.foregroundColor
|
||||||
|
}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateStyle({
|
||||||
|
gradientColors: [
|
||||||
|
e.target.value,
|
||||||
|
style.gradientColors?.[1] || style.foregroundColor,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="w-12 h-8 p-0 border-0"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={
|
||||||
|
style.gradientColors?.[0] || style.foregroundColor
|
||||||
|
}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateStyle({
|
||||||
|
gradientColors: [
|
||||||
|
e.target.value,
|
||||||
|
style.gradientColors?.[1] || style.foregroundColor,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="flex-1 font-mono text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs">渐变结束色</Label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
type="color"
|
||||||
|
value={
|
||||||
|
style.gradientColors?.[1] || style.foregroundColor
|
||||||
|
}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateStyle({
|
||||||
|
gradientColors: [
|
||||||
|
style.gradientColors?.[0] ||
|
||||||
|
style.foregroundColor,
|
||||||
|
e.target.value,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="w-12 h-8 p-0 border-0"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={
|
||||||
|
style.gradientColors?.[1] || style.foregroundColor
|
||||||
|
}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateStyle({
|
||||||
|
gradientColors: [
|
||||||
|
style.gradientColors?.[0] ||
|
||||||
|
style.foregroundColor,
|
||||||
|
e.target.value,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="flex-1 font-mono text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,87 +4,7 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Monitor, Cpu, HardDrive, Database, Computer, RefreshCw, Clock, Play, Pause, Network, Wifi } from 'lucide-react';
|
import { Monitor, Cpu, HardDrive, Database, Computer, RefreshCw, Clock, Play, Pause, Network, Wifi } from 'lucide-react';
|
||||||
|
import type { SystemInfo } from '@/types/system';
|
||||||
// 类型定义
|
|
||||||
interface SystemInfo {
|
|
||||||
os: OsInfo;
|
|
||||||
cpu: CpuInfo;
|
|
||||||
memory: MemoryInfo;
|
|
||||||
gpu: GpuInfo[];
|
|
||||||
disks: DiskInfo[];
|
|
||||||
computer: ComputerInfo;
|
|
||||||
display: DisplayInfo;
|
|
||||||
network: NetworkInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OsInfo {
|
|
||||||
name: string;
|
|
||||||
version: string;
|
|
||||||
arch: string;
|
|
||||||
kernel_version: string;
|
|
||||||
host_name: string;
|
|
||||||
uptime_readable: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CpuInfo {
|
|
||||||
model: string;
|
|
||||||
cores: number;
|
|
||||||
processors: number;
|
|
||||||
max_frequency: number;
|
|
||||||
usage_percent: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MemoryInfo {
|
|
||||||
total_gb: number;
|
|
||||||
available_gb: number;
|
|
||||||
used_gb: number;
|
|
||||||
usage_percent: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GpuInfo {
|
|
||||||
name: string;
|
|
||||||
vram_gb: number;
|
|
||||||
driver_version: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DiskInfo {
|
|
||||||
drive_letter: string;
|
|
||||||
volume_label: string;
|
|
||||||
file_system: string;
|
|
||||||
total_gb: number;
|
|
||||||
available_gb: number;
|
|
||||||
used_gb: number;
|
|
||||||
usage_percent: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ComputerInfo {
|
|
||||||
name: string;
|
|
||||||
username: string;
|
|
||||||
domain: string;
|
|
||||||
manufacturer: string;
|
|
||||||
model: string;
|
|
||||||
serial_number: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DisplayInfo {
|
|
||||||
monitor_count: number;
|
|
||||||
primary_resolution: string;
|
|
||||||
all_resolutions: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface NetworkInfo {
|
|
||||||
interfaces: InterfaceInfo[];
|
|
||||||
total_downloaded_mb: number;
|
|
||||||
total_uploaded_mb: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface InterfaceInfo {
|
|
||||||
name: string;
|
|
||||||
mac_address: string;
|
|
||||||
ip_networks: string[];
|
|
||||||
upload_speed_kb: number;
|
|
||||||
download_speed_kb: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SystemInfoPage() {
|
export function SystemInfoPage() {
|
||||||
const [systemInfo, setSystemInfo] = useState<SystemInfo | null>(null);
|
const [systemInfo, setSystemInfo] = useState<SystemInfo | null>(null);
|
||||||
@@ -202,9 +122,9 @@ export function SystemInfoPage() {
|
|||||||
const formatGB = (value: number) => `${value.toFixed(2)} GB`;
|
const formatGB = (value: number) => `${value.toFixed(2)} GB`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<div className="flex flex-col h-screen bg-background">
|
||||||
{/* 顶部导航栏 */}
|
{/* 顶部导航栏 */}
|
||||||
<header className="border-b bg-background/95 backdrop-blur">
|
<header className="border-b bg-background/95 backdrop-blur flex-shrink-0">
|
||||||
<div className="container mx-auto px-4 py-4">
|
<div className="container mx-auto px-4 py-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -290,7 +210,7 @@ export function SystemInfoPage() {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* 主内容区 */}
|
{/* 主内容区 */}
|
||||||
<main className="container mx-auto px-4 py-4">
|
<main className="flex-1 container mx-auto px-4 py-4 overflow-y-auto">
|
||||||
{error && (
|
{error && (
|
||||||
<Card className="mb-4 border-destructive">
|
<Card className="mb-4 border-destructive">
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
@@ -316,8 +236,8 @@ export function SystemInfoPage() {
|
|||||||
<div className="flex justify-between"><span className="text-sm text-muted-foreground">系统</span><span className="text-sm font-medium">{systemInfo.os.name}</span></div>
|
<div className="flex justify-between"><span className="text-sm text-muted-foreground">系统</span><span className="text-sm font-medium">{systemInfo.os.name}</span></div>
|
||||||
<div className="flex justify-between"><span className="text-sm text-muted-foreground">版本</span><span className="text-sm font-medium">{systemInfo.os.version}</span></div>
|
<div className="flex justify-between"><span className="text-sm text-muted-foreground">版本</span><span className="text-sm font-medium">{systemInfo.os.version}</span></div>
|
||||||
<div className="flex justify-between"><span className="text-sm text-muted-foreground">架构</span><span className="text-sm font-medium">{systemInfo.os.arch}</span></div>
|
<div className="flex justify-between"><span className="text-sm text-muted-foreground">架构</span><span className="text-sm font-medium">{systemInfo.os.arch}</span></div>
|
||||||
<div className="flex justify-between"><span className="text-sm text-muted-foreground">主机名</span><span className="text-sm font-medium">{systemInfo.os.host_name}</span></div>
|
<div className="flex justify-between"><span className="text-sm text-muted-foreground">主机名</span><span className="text-sm font-medium">{systemInfo.os.hostName}</span></div>
|
||||||
<div className="flex justify-between"><span className="text-sm text-muted-foreground">运行时间</span><span className="text-sm font-medium">{systemInfo.os.uptime_readable}</span></div>
|
<div className="flex justify-between"><span className="text-sm text-muted-foreground">运行时间</span><span className="text-sm font-medium">{systemInfo.os.uptimeReadable}</span></div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -366,18 +286,18 @@ export function SystemInfoPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="bg-muted/50 rounded p-2">
|
<div className="bg-muted/50 rounded p-2">
|
||||||
<div className="text-xs text-muted-foreground">频率</div>
|
<div className="text-xs text-muted-foreground">频率</div>
|
||||||
<div className="font-semibold text-sm">{systemInfo.cpu.max_frequency}MHz</div>
|
<div className="font-semibold text-sm">{systemInfo.cpu.maxFrequency}MHz</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-between mb-1">
|
<div className="flex justify-between mb-1">
|
||||||
<span className="text-sm text-muted-foreground">使用率</span>
|
<span className="text-sm text-muted-foreground">使用率</span>
|
||||||
<span className="text-sm font-medium">{systemInfo.cpu.usage_percent.toFixed(1)}%</span>
|
<span className="text-sm font-medium">{systemInfo.cpu.usagePercent.toFixed(1)}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full bg-muted rounded-full h-2">
|
<div className="w-full bg-muted rounded-full h-2">
|
||||||
<div
|
<div
|
||||||
className="bg-primary h-2 rounded-full transition-all duration-300"
|
className="bg-primary h-2 rounded-full transition-all duration-300"
|
||||||
style={{ width: `${systemInfo.cpu.usage_percent}%` }}
|
style={{ width: `${systemInfo.cpu.usagePercent}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -398,26 +318,26 @@ export function SystemInfoPage() {
|
|||||||
<div className="grid grid-cols-3 gap-2 text-center">
|
<div className="grid grid-cols-3 gap-2 text-center">
|
||||||
<div className="bg-muted/50 rounded p-2">
|
<div className="bg-muted/50 rounded p-2">
|
||||||
<div className="text-xs text-muted-foreground">总内存</div>
|
<div className="text-xs text-muted-foreground">总内存</div>
|
||||||
<div className="font-semibold text-sm">{formatGB(systemInfo.memory.total_gb)}</div>
|
<div className="font-semibold text-sm">{formatGB(systemInfo.memory.totalGb)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-muted/50 rounded p-2">
|
<div className="bg-muted/50 rounded p-2">
|
||||||
<div className="text-xs text-muted-foreground">已用</div>
|
<div className="text-xs text-muted-foreground">已用</div>
|
||||||
<div className="font-semibold text-sm">{formatGB(systemInfo.memory.used_gb)}</div>
|
<div className="font-semibold text-sm">{formatGB(systemInfo.memory.usedGb)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-muted/50 rounded p-2">
|
<div className="bg-muted/50 rounded p-2">
|
||||||
<div className="text-xs text-muted-foreground">可用</div>
|
<div className="text-xs text-muted-foreground">可用</div>
|
||||||
<div className="font-semibold text-sm text-green-600">{formatGB(systemInfo.memory.available_gb)}</div>
|
<div className="font-semibold text-sm text-green-600">{formatGB(systemInfo.memory.availableGb)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-between mb-1">
|
<div className="flex justify-between mb-1">
|
||||||
<span className="text-sm text-muted-foreground">使用率</span>
|
<span className="text-sm text-muted-foreground">使用率</span>
|
||||||
<span className="text-sm font-medium">{systemInfo.memory.usage_percent.toFixed(1)}%</span>
|
<span className="text-sm font-medium">{systemInfo.memory.usagePercent.toFixed(1)}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full bg-muted rounded-full h-2">
|
<div className="w-full bg-muted rounded-full h-2">
|
||||||
<div
|
<div
|
||||||
className="bg-primary h-2 rounded-full transition-all duration-300"
|
className="bg-primary h-2 rounded-full transition-all duration-300"
|
||||||
style={{ width: `${systemInfo.memory.usage_percent}%` }}
|
style={{ width: `${systemInfo.memory.usagePercent}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -445,8 +365,8 @@ export function SystemInfoPage() {
|
|||||||
<div key={index} className="border-l-2 border-primary pl-3 py-1">
|
<div key={index} className="border-l-2 border-primary pl-3 py-1">
|
||||||
<div className="text-sm font-medium mb-1">{gpu.name}</div>
|
<div className="text-sm font-medium mb-1">{gpu.name}</div>
|
||||||
<div className="flex gap-3 text-sm">
|
<div className="flex gap-3 text-sm">
|
||||||
<span className="text-muted-foreground">显存: <span className="font-medium">{gpu.vram_gb.toFixed(1)} GB</span></span>
|
<span className="text-muted-foreground">显存: <span className="font-medium">{gpu.vramGb.toFixed(1)} GB</span></span>
|
||||||
<span className="text-muted-foreground">驱动: <span className="font-medium">{gpu.driver_version}</span></span>
|
<span className="text-muted-foreground">驱动: <span className="font-medium">{gpu.driverVersion}</span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -465,9 +385,9 @@ export function SystemInfoPage() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="py-3">
|
<CardContent className="py-3">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex justify-between"><span className="text-sm text-muted-foreground">数量</span><span className="text-sm font-medium">{systemInfo.display.monitor_count}</span></div>
|
<div className="flex justify-between"><span className="text-sm text-muted-foreground">数量</span><span className="text-sm font-medium">{systemInfo.display.monitorCount}</span></div>
|
||||||
<div className="flex justify-between"><span className="text-sm text-muted-foreground">主分辨率</span><span className="text-sm font-medium">{systemInfo.display.primary_resolution}</span></div>
|
<div className="flex justify-between"><span className="text-sm text-muted-foreground">主分辨率</span><span className="text-sm font-medium">{systemInfo.display.primaryResolution}</span></div>
|
||||||
<div className="flex justify-between"><span className="text-sm text-muted-foreground">所有分辨率</span><span className="text-sm font-medium">{systemInfo.display.all_resolutions.join(', ')}</span></div>
|
<div className="flex justify-between"><span className="text-sm text-muted-foreground">所有分辨率</span><span className="text-sm font-medium">{systemInfo.display.allResolutions.join(', ')}</span></div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -485,11 +405,11 @@ export function SystemInfoPage() {
|
|||||||
<div className="flex gap-4 mb-3">
|
<div className="flex gap-4 mb-3">
|
||||||
<div className="flex-1 bg-muted/50 rounded p-2 text-center">
|
<div className="flex-1 bg-muted/50 rounded p-2 text-center">
|
||||||
<div className="text-xs text-muted-foreground">总下载</div>
|
<div className="text-xs text-muted-foreground">总下载</div>
|
||||||
<div className="font-semibold text-base text-green-600">{systemInfo.network.total_downloaded_mb.toFixed(2)} MB</div>
|
<div className="font-semibold text-base text-green-600">{systemInfo.network.totalDownloadedMb.toFixed(2)} MB</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 bg-muted/50 rounded p-2 text-center">
|
<div className="flex-1 bg-muted/50 rounded p-2 text-center">
|
||||||
<div className="text-xs text-muted-foreground">总上传</div>
|
<div className="text-xs text-muted-foreground">总上传</div>
|
||||||
<div className="font-semibold text-base text-blue-600">{systemInfo.network.total_uploaded_mb.toFixed(2)} MB</div>
|
<div className="font-semibold text-base text-blue-600">{systemInfo.network.totalUploadedMb.toFixed(2)} MB</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2">
|
||||||
@@ -501,14 +421,14 @@ export function SystemInfoPage() {
|
|||||||
<span className="text-sm font-medium">{iface.name}</span>
|
<span className="text-sm font-medium">{iface.name}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
<span className="text-green-600">↓{iface.download_speed_kb.toFixed(1)}</span>
|
<span className="text-green-600">↓{iface.downloadSpeedKb.toFixed(1)}</span>
|
||||||
<span className="text-blue-600 ml-1">↑{iface.upload_speed_kb.toFixed(1)}</span>
|
<span className="text-blue-600 ml-1">↑{iface.uploadSpeedKb.toFixed(1)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-muted-foreground">{iface.mac_address}</div>
|
<div className="text-sm text-muted-foreground">{iface.macAddress}</div>
|
||||||
{iface.ip_networks.length > 0 && (
|
{iface.ipNetworks.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-1 mt-1">
|
<div className="flex flex-wrap gap-1 mt-1">
|
||||||
{iface.ip_networks.map((ip, ipIndex) => (
|
{iface.ipNetworks.map((ip, ipIndex) => (
|
||||||
<Badge key={ipIndex} variant="outline" className="text-xs px-2 py-0 h-5">
|
<Badge key={ipIndex} variant="outline" className="text-xs px-2 py-0 h-5">
|
||||||
{ip}
|
{ip}
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -535,36 +455,36 @@ export function SystemInfoPage() {
|
|||||||
<div key={index} className="border rounded p-3">
|
<div key={index} className="border rounded p-3">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Badge variant="outline" className="text-sm">{disk.drive_letter}</Badge>
|
<Badge variant="outline" className="text-sm">{disk.driveLetter}</Badge>
|
||||||
<span className="text-base font-medium">{disk.volume_label}</span>
|
<span className="text-base font-medium">{disk.volumeLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm text-muted-foreground">{disk.file_system}</span>
|
<span className="text-sm text-muted-foreground">{disk.fileSystem}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 gap-2 mb-2 text-center">
|
<div className="grid grid-cols-3 gap-2 mb-2 text-center">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs text-muted-foreground">总容量</div>
|
<div className="text-xs text-muted-foreground">总容量</div>
|
||||||
<div className="text-sm font-medium">{formatGB(disk.total_gb)}</div>
|
<div className="text-sm font-medium">{formatGB(disk.totalGb)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs text-muted-foreground">已用</div>
|
<div className="text-xs text-muted-foreground">已用</div>
|
||||||
<div className="text-sm font-medium">{formatGB(disk.used_gb)}</div>
|
<div className="text-sm font-medium">{formatGB(disk.usedGb)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs text-muted-foreground">可用</div>
|
<div className="text-xs text-muted-foreground">可用</div>
|
||||||
<div className="text-sm font-medium text-green-600">{formatGB(disk.available_gb)}</div>
|
<div className="text-sm font-medium text-green-600">{formatGB(disk.availableGb)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-between mb-1">
|
<div className="flex justify-between mb-1">
|
||||||
<span className="text-sm text-muted-foreground">使用率</span>
|
<span className="text-sm text-muted-foreground">使用率</span>
|
||||||
<span className="text-sm font-medium">{disk.usage_percent.toFixed(1)}%</span>
|
<span className="text-sm font-medium">{disk.usagePercent.toFixed(1)}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full bg-muted rounded-full h-2">
|
<div className="w-full bg-muted rounded-full h-2">
|
||||||
<div
|
<div
|
||||||
className={`h-2 rounded-full transition-all duration-300 ${
|
className={`h-2 rounded-full transition-all duration-300 ${
|
||||||
disk.usage_percent > 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)}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ export function Home() {
|
|||||||
}, [selectedCategory, searchQuery]);
|
}, [selectedCategory, searchQuery]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<div className="flex flex-col h-screen bg-background overflow-hidden">
|
||||||
{/* 顶部导航栏 */}
|
{/* 顶部导航栏 */}
|
||||||
<header className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 sticky top-0 z-50">
|
<header className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 flex-shrink-0">
|
||||||
<div className="container mx-auto px-4 py-4">
|
<div className="container mx-auto px-4 py-4">
|
||||||
<div className="flex items-center justify-between gap-4">
|
<div className="flex items-center justify-between gap-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -83,7 +83,7 @@ export function Home() {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* 主内容区 */}
|
{/* 主内容区 */}
|
||||||
<main className="container mx-auto px-4 py-6">
|
<main className="flex-1 container mx-auto px-4 py-6 overflow-y-auto">
|
||||||
{/* 分类筛选 */}
|
{/* 分类筛选 */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<CategoryFilter
|
<CategoryFilter
|
||||||
|
|||||||
@@ -69,9 +69,9 @@ export function Search() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<div className="flex flex-col h-screen bg-background overflow-hidden">
|
||||||
{/* 顶部导航栏 */}
|
{/* 顶部导航栏 */}
|
||||||
<header className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 sticky top-0 z-50">
|
<header className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 flex-shrink-0">
|
||||||
<div className="container mx-auto px-4 py-4">
|
<div className="container mx-auto px-4 py-4">
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<div className="flex items-center gap-3 mb-4">
|
||||||
<Button
|
<Button
|
||||||
@@ -100,7 +100,7 @@ export function Search() {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* 主内容区 */}
|
{/* 主内容区 */}
|
||||||
<main className="container mx-auto px-4 py-6">
|
<main className="flex-1 container mx-auto px-4 py-6 overflow-y-auto">
|
||||||
{/* 结果统计 */}
|
{/* 结果统计 */}
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
|
|
||||||
export default function Settings() {
|
export default function Settings() {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto p-6">
|
<div className="container mx-auto p-6 h-screen overflow-y-auto">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>设置</CardTitle>
|
<CardTitle>设置</CardTitle>
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
|
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import type { QrConfig, QrResult } from '@/types/qrcode';
|
import { open } from '@tauri-apps/plugin-dialog';
|
||||||
|
import type { QrConfig, QrStyle, LogoConfig, QrResult } from '@/types/qrcode';
|
||||||
import { DEFAULT_QR_CONFIG } from '@/types/qrcode';
|
import { DEFAULT_QR_CONFIG } from '@/types/qrcode';
|
||||||
|
|
||||||
interface QrState {
|
interface QrState {
|
||||||
@@ -18,6 +19,14 @@ interface QrState {
|
|||||||
error: string | null;
|
error: string | null;
|
||||||
/** 更新配置 */
|
/** 更新配置 */
|
||||||
updateConfig: (updates: Partial<QrConfig>) => void;
|
updateConfig: (updates: Partial<QrConfig>) => void;
|
||||||
|
/** 更新样式 */
|
||||||
|
updateStyle: (updates: Partial<QrStyle>) => void;
|
||||||
|
/** 更新 Logo 配置 */
|
||||||
|
updateLogo: (updates: Partial<LogoConfig>) => void;
|
||||||
|
/** 清除 Logo */
|
||||||
|
clearLogo: () => void;
|
||||||
|
/** 选择 Logo 文件 */
|
||||||
|
selectLogoFile: () => Promise<void>;
|
||||||
/** 重置配置 */
|
/** 重置配置 */
|
||||||
resetConfig: () => void;
|
resetConfig: () => void;
|
||||||
/** 生成预览 */
|
/** 生成预览 */
|
||||||
@@ -40,6 +49,68 @@ export const useQrStore = create<QrState>((set, get) => ({
|
|||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateStyle: (updates) => {
|
||||||
|
set((state) => ({
|
||||||
|
config: {
|
||||||
|
...state.config,
|
||||||
|
style: { ...state.config.style, ...updates },
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
updateLogo: (updates) => {
|
||||||
|
set((state) => {
|
||||||
|
const currentLogo = state.config.logo;
|
||||||
|
return {
|
||||||
|
config: {
|
||||||
|
...state.config,
|
||||||
|
logo: { ...currentLogo, ...updates } as LogoConfig,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
clearLogo: () => {
|
||||||
|
set((state) => ({
|
||||||
|
config: {
|
||||||
|
...state.config,
|
||||||
|
logo: undefined,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
selectLogoFile: async () => {
|
||||||
|
try {
|
||||||
|
const selected = await open({
|
||||||
|
title: '选择 Logo 图片',
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
name: '图片',
|
||||||
|
extensions: ['png', 'jpg', 'jpeg', 'webp', 'svg'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (selected && typeof selected === 'string') {
|
||||||
|
// 初始化 Logo 配置
|
||||||
|
set((state) => ({
|
||||||
|
config: {
|
||||||
|
...state.config,
|
||||||
|
logo: {
|
||||||
|
path: selected,
|
||||||
|
scale: 0.2,
|
||||||
|
hasBorder: true,
|
||||||
|
borderWidth: 4,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||||
|
set({ error: `选择 Logo 失败: ${errorMessage}` });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
resetConfig: () => {
|
resetConfig: () => {
|
||||||
set({
|
set({
|
||||||
config: DEFAULT_QR_CONFIG,
|
config: DEFAULT_QR_CONFIG,
|
||||||
|
|||||||
43
src/types/color.ts
Normal file
43
src/types/color.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* 颜色相关类型定义
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 颜色信息
|
||||||
|
*/
|
||||||
|
export interface ColorInfo {
|
||||||
|
/** 十六进制颜色值(格式:#RRGGBB) */
|
||||||
|
hex: string;
|
||||||
|
/** RGB 颜色值 */
|
||||||
|
rgb: RgbInfo;
|
||||||
|
/** HSL 颜色值 */
|
||||||
|
hsl: HslInfo;
|
||||||
|
/** 屏幕坐标 X(像素) */
|
||||||
|
x: number;
|
||||||
|
/** 屏幕坐标 Y(像素) */
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RGB 颜色
|
||||||
|
*/
|
||||||
|
export interface RgbInfo {
|
||||||
|
/** 红色分量 (0-255) */
|
||||||
|
r: number;
|
||||||
|
/** 绿色分量 (0-255) */
|
||||||
|
g: number;
|
||||||
|
/** 蓝色分量 (0-255) */
|
||||||
|
b: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HSL 颜色
|
||||||
|
*/
|
||||||
|
export interface HslInfo {
|
||||||
|
/** 色相 (0-360) */
|
||||||
|
h: number;
|
||||||
|
/** 饱和度 (0-100) */
|
||||||
|
s: number;
|
||||||
|
/** 亮度 (0-100) */
|
||||||
|
l: number;
|
||||||
|
}
|
||||||
48
src/types/json.ts
Normal file
48
src/types/json.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* JSON 格式化相关类型定义
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON 格式化配置
|
||||||
|
*/
|
||||||
|
export interface JsonFormatConfig {
|
||||||
|
/** 缩进空格数(默认 2) */
|
||||||
|
indent?: number;
|
||||||
|
/** 是否对 key 进行排序 */
|
||||||
|
sortKeys?: boolean;
|
||||||
|
/** 格式化模式 */
|
||||||
|
mode?: FormatMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON 格式化模式
|
||||||
|
*/
|
||||||
|
export type FormatMode = 'pretty' | 'compact';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON 格式化结果
|
||||||
|
*/
|
||||||
|
export interface JsonFormatResult {
|
||||||
|
/** 是否成功 */
|
||||||
|
success: boolean;
|
||||||
|
/** 格式化后的 JSON 字符串 */
|
||||||
|
result: string;
|
||||||
|
/** 错误信息(如果失败) */
|
||||||
|
error?: string;
|
||||||
|
/** 原始 JSON 是否有效 */
|
||||||
|
isValid: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON 验证结果
|
||||||
|
*/
|
||||||
|
export interface JsonValidateResult {
|
||||||
|
/** 是否有效的 JSON */
|
||||||
|
isValid: boolean;
|
||||||
|
/** 错误信息(如果无效) */
|
||||||
|
errorMessage?: string;
|
||||||
|
/** 错误位置(行号,从 1 开始) */
|
||||||
|
errorLine?: number;
|
||||||
|
/** 错误位置(列号,从 1 开始) */
|
||||||
|
errorColumn?: number;
|
||||||
|
}
|
||||||
@@ -14,10 +14,14 @@ export interface QrConfig {
|
|||||||
margin: number;
|
margin: number;
|
||||||
/** 容错级别 */
|
/** 容错级别 */
|
||||||
errorCorrection: 'L' | 'M' | 'Q' | 'H';
|
errorCorrection: 'L' | 'M' | 'Q' | 'H';
|
||||||
|
/** 样式配置 */
|
||||||
|
style: QrStyle;
|
||||||
|
/** Logo 配置 */
|
||||||
|
logo?: LogoConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 二维码样式(阶段 2 使用)
|
* 二维码样式
|
||||||
*/
|
*/
|
||||||
export interface QrStyle {
|
export interface QrStyle {
|
||||||
/** 点形状 */
|
/** 点形状 */
|
||||||
@@ -35,12 +39,12 @@ export interface QrStyle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logo 配置(阶段 2 使用)
|
* Logo 配置
|
||||||
*/
|
*/
|
||||||
export interface LogoConfig {
|
export interface LogoConfig {
|
||||||
/** Logo 文件路径 */
|
/** Logo 文件路径 */
|
||||||
path: string;
|
path: string;
|
||||||
/** 缩放比例 (0.1 - 0.3) */
|
/** 缩放比例 (0.05 - 0.3) */
|
||||||
scale: number;
|
scale: number;
|
||||||
/** 是否添加边框 */
|
/** 是否添加边框 */
|
||||||
hasBorder: boolean;
|
hasBorder: boolean;
|
||||||
@@ -68,6 +72,18 @@ export type QrCodeCommands = {
|
|||||||
generate_qr_file: (config: QrConfig, outputPath: string) => Promise<void>;
|
generate_qr_file: (config: QrConfig, outputPath: string) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认样式配置
|
||||||
|
*/
|
||||||
|
export const DEFAULT_QR_STYLE: QrStyle = {
|
||||||
|
dotShape: 'square',
|
||||||
|
eyeShape: 'square',
|
||||||
|
foregroundColor: '#000000',
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
isGradient: false,
|
||||||
|
gradientColors: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 默认二维码配置
|
* 默认二维码配置
|
||||||
*/
|
*/
|
||||||
@@ -76,6 +92,7 @@ export const DEFAULT_QR_CONFIG: QrConfig = {
|
|||||||
size: 512,
|
size: 512,
|
||||||
margin: 4,
|
margin: 4,
|
||||||
errorCorrection: 'M',
|
errorCorrection: 'M',
|
||||||
|
style: DEFAULT_QR_STYLE,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -97,3 +114,57 @@ export const SIZE_PRESETS = [
|
|||||||
{ value: 1024, label: '大 (1024px)' },
|
{ value: 1024, label: '大 (1024px)' },
|
||||||
{ value: 2048, label: '超大 (2048px)' },
|
{ value: 2048, label: '超大 (2048px)' },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 点形状选项
|
||||||
|
*/
|
||||||
|
export const DOT_SHAPE_OPTIONS = [
|
||||||
|
{ value: 'square', label: '方块', icon: '⬜' },
|
||||||
|
{ value: 'circle', label: '圆点', icon: '⚫' },
|
||||||
|
{ value: 'rounded', label: '圆角', icon: '▢' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 码眼形状选项
|
||||||
|
*/
|
||||||
|
export const EYE_SHAPE_OPTIONS = [
|
||||||
|
{ value: 'square', label: '方块', icon: '⬜' },
|
||||||
|
{ value: 'circle', label: '圆点', icon: '⚫' },
|
||||||
|
{ value: 'rounded', label: '圆角', icon: '▢' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预设颜色方案
|
||||||
|
*/
|
||||||
|
export const COLOR_PRESETS = [
|
||||||
|
{
|
||||||
|
name: '经典',
|
||||||
|
foreground: '#000000',
|
||||||
|
background: '#FFFFFF',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '蓝白',
|
||||||
|
foreground: '#1E40AF',
|
||||||
|
background: '#DBEAFE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '红白',
|
||||||
|
foreground: '#DC2626',
|
||||||
|
background: '#FEE2E2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '绿白',
|
||||||
|
foreground: '#059669',
|
||||||
|
background: '#D1FAE5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '紫白',
|
||||||
|
foreground: '#7C3AED',
|
||||||
|
background: '#EDE9FE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '橙白',
|
||||||
|
foreground: '#EA580C',
|
||||||
|
background: '#FFEDD5',
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|||||||
179
src/types/system.ts
Normal file
179
src/types/system.ts
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
/**
|
||||||
|
* 系统信息相关类型定义
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统信息(完整版)
|
||||||
|
*/
|
||||||
|
export interface SystemInfo {
|
||||||
|
/** 操作系统信息 */
|
||||||
|
os: OsInfo;
|
||||||
|
/** 硬件信息(主板、BIOS) */
|
||||||
|
hardware: HardwareInfo;
|
||||||
|
/** CPU 信息 */
|
||||||
|
cpu: CpuInfo;
|
||||||
|
/** 内存信息 */
|
||||||
|
memory: MemoryInfo;
|
||||||
|
/** GPU 信息列表 */
|
||||||
|
gpu: GpuInfo[];
|
||||||
|
/** 磁盘信息列表 */
|
||||||
|
disks: DiskInfo[];
|
||||||
|
/** 计算机信息 */
|
||||||
|
computer: ComputerInfo;
|
||||||
|
/** 显示器信息 */
|
||||||
|
display: DisplayInfo;
|
||||||
|
/** 网络信息 */
|
||||||
|
network: NetworkInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 操作系统信息
|
||||||
|
*/
|
||||||
|
export interface OsInfo {
|
||||||
|
/** 操作系统名称 */
|
||||||
|
name: string;
|
||||||
|
/** 操作系统版本 */
|
||||||
|
version: string;
|
||||||
|
/** 系统架构 */
|
||||||
|
arch: string;
|
||||||
|
/** 内核版本 */
|
||||||
|
kernelVersion: string;
|
||||||
|
/** 主机名 */
|
||||||
|
hostName: string;
|
||||||
|
/** 运行时间(可读格式) */
|
||||||
|
uptimeReadable: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 硬件信息(主板、BIOS)
|
||||||
|
*/
|
||||||
|
export interface HardwareInfo {
|
||||||
|
/** 制造商 */
|
||||||
|
manufacturer: string;
|
||||||
|
/** 型号 */
|
||||||
|
model: string;
|
||||||
|
/** BIOS 版本 */
|
||||||
|
biosVersion: string;
|
||||||
|
/** BIOS 序列号 */
|
||||||
|
biosSerial: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CPU 信息
|
||||||
|
*/
|
||||||
|
export interface CpuInfo {
|
||||||
|
/** CPU 型号 */
|
||||||
|
model: string;
|
||||||
|
/** 物理核心数 */
|
||||||
|
cores: number;
|
||||||
|
/** 逻辑处理器数 */
|
||||||
|
processors: number;
|
||||||
|
/** 最大频率 (MHz) */
|
||||||
|
maxFrequency: number;
|
||||||
|
/** 当前使用率 (0-100) */
|
||||||
|
usagePercent: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内存信息
|
||||||
|
*/
|
||||||
|
export interface MemoryInfo {
|
||||||
|
/** 总内存 (GB) */
|
||||||
|
totalGb: number;
|
||||||
|
/** 可用内存 (GB) */
|
||||||
|
availableGb: number;
|
||||||
|
/** 已用内存 (GB) */
|
||||||
|
usedGb: number;
|
||||||
|
/** 使用率 (0-100) */
|
||||||
|
usagePercent: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GPU 信息
|
||||||
|
*/
|
||||||
|
export interface GpuInfo {
|
||||||
|
/** GPU 名称 */
|
||||||
|
name: string;
|
||||||
|
/** 显存 (GB) */
|
||||||
|
vramGb: number;
|
||||||
|
/** 驱动版本 */
|
||||||
|
driverVersion: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 磁盘信息
|
||||||
|
*/
|
||||||
|
export interface DiskInfo {
|
||||||
|
/** 盘符 (如 "C:") */
|
||||||
|
driveLetter: string;
|
||||||
|
/** 卷标 */
|
||||||
|
volumeLabel: string;
|
||||||
|
/** 文件系统类型 */
|
||||||
|
fileSystem: string;
|
||||||
|
/** 总容量 (GB) */
|
||||||
|
totalGb: number;
|
||||||
|
/** 可用空间 (GB) */
|
||||||
|
availableGb: number;
|
||||||
|
/** 已用空间 (GB) */
|
||||||
|
usedGb: number;
|
||||||
|
/** 使用率 (0-100) */
|
||||||
|
usagePercent: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算机信息
|
||||||
|
*/
|
||||||
|
export interface ComputerInfo {
|
||||||
|
/** 计算机名称 */
|
||||||
|
name: string;
|
||||||
|
/** 用户名 */
|
||||||
|
username: string;
|
||||||
|
/** 域名/工作组 */
|
||||||
|
domain: string;
|
||||||
|
/** 制造商 */
|
||||||
|
manufacturer: string;
|
||||||
|
/** 型号 */
|
||||||
|
model: string;
|
||||||
|
/** 序列号 */
|
||||||
|
serialNumber: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示器信息
|
||||||
|
*/
|
||||||
|
export interface DisplayInfo {
|
||||||
|
/** 屏幕数量 */
|
||||||
|
monitorCount: number;
|
||||||
|
/** 主显示器分辨率 */
|
||||||
|
primaryResolution: string;
|
||||||
|
/** 所有显示器分辨率列表 */
|
||||||
|
allResolutions: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 网络信息
|
||||||
|
*/
|
||||||
|
export interface NetworkInfo {
|
||||||
|
/** 网络接口列表 */
|
||||||
|
interfaces: InterfaceInfo[];
|
||||||
|
/** 总下载 (MB) */
|
||||||
|
totalDownloadedMb: number;
|
||||||
|
/** 总上传 (MB) */
|
||||||
|
totalUploadedMb: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 网络接口信息
|
||||||
|
*/
|
||||||
|
export interface InterfaceInfo {
|
||||||
|
/** 接口名称 */
|
||||||
|
name: string;
|
||||||
|
/** MAC 地址 */
|
||||||
|
macAddress: string;
|
||||||
|
/** IP 地址列表 */
|
||||||
|
ipNetworks: string[];
|
||||||
|
/** 上传速度 (KB/s) */
|
||||||
|
uploadSpeedKb: number;
|
||||||
|
/** 下载速度 (KB/s) */
|
||||||
|
downloadSpeedKb: number;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user