feat: 实现二维码高级渲染功能,支持自定义颜色、形状和 Logo 嵌入

This commit is contained in:
2026-02-10 19:59:32 +08:00
parent b2754bdad5
commit 825b650542
28 changed files with 1782 additions and 355 deletions

View 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
View File

@@ -860,6 +860,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
dependencies = [
"bitflags 2.10.0",
"block2",
"libc",
"objc2",
]
@@ -3574,6 +3576,30 @@ dependencies = [
"web-sys",
]
[[package]]
name = "rfd"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
dependencies = [
"block2",
"dispatch2",
"glib-sys",
"gobject-sys",
"gtk-sys",
"js-sys",
"log",
"objc2",
"objc2-app-kit",
"objc2-core-foundation",
"objc2-foundation",
"raw-window-handle",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"windows-sys 0.60.2",
]
[[package]]
name = "rgb"
version = "0.8.52"
@@ -4224,6 +4250,7 @@ dependencies = [
"sysinfo",
"tauri",
"tauri-build",
"tauri-plugin-dialog",
"tauri-plugin-global-shortcut",
"tauri-plugin-opener",
"windows 0.58.0",
@@ -4310,6 +4337,46 @@ dependencies = [
"walkdir",
]
[[package]]
name = "tauri-plugin-dialog"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9204b425d9be8d12aa60c2a83a289cf7d1caae40f57f336ed1155b3a5c0e359b"
dependencies = [
"log",
"raw-window-handle",
"rfd",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"tauri-plugin-fs",
"thiserror 2.0.18",
"url",
]
[[package]]
name = "tauri-plugin-fs"
version = "2.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed390cc669f937afeb8b28032ce837bac8ea023d975a2e207375ec05afaf1804"
dependencies = [
"anyhow",
"dunce",
"glob",
"percent-encoding",
"schemars 0.8.22",
"serde",
"serde_json",
"serde_repr",
"tauri",
"tauri-plugin",
"tauri-utils",
"thiserror 2.0.18",
"toml 0.9.11+spec-1.1.0",
"url",
]
[[package]]
name = "tauri-plugin-global-shortcut"
version = "2.3.1"

View File

@@ -21,6 +21,7 @@ tauri-build = { version = "2.4", features = [] }
tauri = { version = "2.4", features = [] }
tauri-plugin-opener = "2.5"
tauri-plugin-global-shortcut = "2"
tauri-plugin-dialog = "2"
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1", features = ["preserve_order"] }
sysinfo = "0.30"

View File

@@ -10,6 +10,8 @@
"core:window:allow-close",
"core:window:allow-set-focus",
"core:window:allow-is-visible",
"opener:default"
"opener:default",
"dialog:allow-save",
"dialog:allow-open"
]
}

View File

@@ -22,6 +22,7 @@ pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
.plugin(tauri_plugin_dialog::init())
.setup(|app| {
// 预热取色器窗口:避免第一次取色出现“白屏闪一下”
// 窗口会以 hidden 状态创建,不会影响用户体验

View File

@@ -9,6 +9,7 @@ use crate::utils::color_conversion;
///
/// 包含颜色的完整信息,支持多种颜色格式和屏幕坐标
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ColorInfo {
/// 十六进制颜色值(格式:#RRGGBB
pub hex: String,
@@ -62,6 +63,7 @@ impl ColorInfo {
///
/// 表示 RGB 颜色模式的颜色值
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RgbInfo {
/// 红色分量 (0-255)
pub r: u8,
@@ -75,6 +77,7 @@ pub struct RgbInfo {
///
/// 表示 HSL 颜色模式的颜色值
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HslInfo {
/// 色相 (0-360)
///

View File

@@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize};
/// JSON 格式化配置
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct JsonFormatConfig {
/// 缩进空格数(默认 2
#[serde(default = "default_indent")]
@@ -54,6 +55,7 @@ impl Default for JsonFormatConfig {
/// JSON 格式化结果
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct JsonFormatResult {
/// 是否成功
pub success: bool,
@@ -70,6 +72,7 @@ pub struct JsonFormatResult {
/// JSON 验证结果
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct JsonValidateResult {
/// 是否有效的 JSON
pub is_valid: bool,

View File

@@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize};
///
/// 定义生成二维码所需的参数
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct QrConfig {
/// 二维码内容
pub content: String,
@@ -15,12 +16,17 @@ pub struct QrConfig {
pub margin: u32,
/// 容错级别: "L", "M", "Q", "H"
pub error_correction: String,
/// 样式配置
pub style: Option<QrStyle>,
/// Logo 配置
pub logo: Option<LogoConfig>,
}
/// 二维码样式(阶段 2 使用)
/// 二维码样式
///
/// 定义二维码的视觉样式
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct QrStyle {
/// 点形状: "square", "circle", "rounded"
pub dot_shape: String,
@@ -36,10 +42,24 @@ pub struct QrStyle {
pub gradient_colors: Option<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 的位置和样式
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LogoConfig {
/// Logo 文件路径
pub path: String,
@@ -55,6 +75,7 @@ pub struct LogoConfig {
///
/// 包含生成的二维码图片数据
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct QrResult {
/// Base64 编码的图片数据
pub data: String,

View File

@@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize};
/// 系统信息(完整版)
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SystemInfo {
/// 操作系统信息
pub os: OsInfo,
@@ -29,6 +30,7 @@ pub struct SystemInfo {
/// 操作系统信息
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OsInfo {
/// 操作系统名称
pub name: String,
@@ -46,6 +48,7 @@ pub struct OsInfo {
/// 硬件信息主板、BIOS
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HardwareInfo {
/// 制造商
pub manufacturer: String,
@@ -59,6 +62,7 @@ pub struct HardwareInfo {
/// CPU 信息
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CpuInfo {
/// CPU 型号
pub model: String,
@@ -74,6 +78,7 @@ pub struct CpuInfo {
/// 内存信息
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MemoryInfo {
/// 总内存 (GB)
pub total_gb: f64,
@@ -87,6 +92,7 @@ pub struct MemoryInfo {
/// GPU 信息
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GpuInfo {
/// GPU 名称
pub name: String,
@@ -98,6 +104,7 @@ pub struct GpuInfo {
/// 磁盘信息
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DiskInfo {
/// 盘符 (如 "C:")
pub drive_letter: String,
@@ -117,6 +124,7 @@ pub struct DiskInfo {
/// 计算机信息
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ComputerInfo {
/// 计算机名称
pub name: String,
@@ -134,6 +142,7 @@ pub struct ComputerInfo {
/// 显示器信息
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DisplayInfo {
/// 屏幕数量
pub monitor_count: u32,
@@ -145,6 +154,7 @@ pub struct DisplayInfo {
/// 网络信息
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NetworkInfo {
/// 网络接口列表
pub interfaces: Vec<InterfaceInfo>,
@@ -156,6 +166,7 @@ pub struct NetworkInfo {
/// 网络接口信息
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InterfaceInfo {
/// 接口名称
pub name: String,

View File

@@ -4,7 +4,7 @@
use crate::error::{AppError, AppResult};
use crate::models::qrcode::{QrConfig, QrResult};
use crate::utils::qrcode_renderer::{image_to_base64, render_basic_qr};
use crate::utils::qrcode_renderer::{image_to_base64, render_qr};
/// 二维码生成服务
pub struct QrCodeService;
@@ -39,8 +39,17 @@ impl QrCodeService {
return Err(AppError::InvalidData("边距不能超过 50".to_string()));
}
// 验证 Logo 缩放比例
if let Some(logo) = &config.logo {
if logo.scale < 0.05 || logo.scale > 0.3 {
return Err(AppError::InvalidData(
"Logo 缩放比例必须在 0.05 到 0.3 之间".to_string(),
));
}
}
// 渲染二维码
let img = render_basic_qr(config)?;
let img = render_qr(config)?;
// 转换为 Base64
let base64_data = image_to_base64(&img)?;
@@ -76,8 +85,22 @@ impl QrCodeService {
));
}
// 验证边距
if config.margin > 50 {
return Err(AppError::InvalidData("边距不能超过 50".to_string()));
}
// 验证 Logo 缩放比例
if let Some(logo) = &config.logo {
if logo.scale < 0.05 || logo.scale > 0.3 {
return Err(AppError::InvalidData(
"Logo 缩放比例必须在 0.05 到 0.3 之间".to_string(),
));
}
}
// 渲染二维码
let img = render_basic_qr(config)?;
let img = render_qr(config)?;
// 保存到文件
img.save(output_path)
@@ -90,6 +113,7 @@ impl QrCodeService {
#[cfg(test)]
mod tests {
use super::*;
use crate::models::qrcode::{QrStyle};
#[test]
fn test_generate_preview() {
@@ -98,6 +122,8 @@ mod tests {
size: 512,
margin: 4,
error_correction: "M".to_string(),
style: None,
logo: None,
};
let result = QrCodeService::generate_preview(&config);
@@ -108,6 +134,30 @@ mod tests {
assert_eq!(qr_result.format, "png");
}
#[test]
fn test_generate_preview_with_style() {
let style = QrStyle {
dot_shape: "circle".to_string(),
eye_shape: "square".to_string(),
foreground_color: "#FF0000".to_string(),
background_color: "#FFFFFF".to_string(),
is_gradient: false,
gradient_colors: None,
};
let config = QrConfig {
content: "https://example.com".to_string(),
size: 512,
margin: 4,
error_correction: "M".to_string(),
style: Some(style),
logo: None,
};
let result = QrCodeService::generate_preview(&config);
assert!(result.is_ok());
}
#[test]
fn test_generate_preview_invalid_size() {
let config = QrConfig {
@@ -115,6 +165,8 @@ mod tests {
size: 50, // 太小
margin: 4,
error_correction: "M".to_string(),
style: None,
logo: None,
};
let result = QrCodeService::generate_preview(&config);
@@ -128,6 +180,8 @@ mod tests {
size: 512,
margin: 100, // 太大
error_correction: "M".to_string(),
style: None,
logo: None,
};
let result = QrCodeService::generate_preview(&config);

View File

@@ -1,19 +1,19 @@
//! 二维码渲染工具函数
//!
//! 提供二维码矩阵到图像的渲染功能
//! 提供二维码矩阵到图像的渲染功能,支持颜色、形状和 Logo
use crate::error::{AppError, AppResult};
use crate::models::qrcode::QrConfig;
use crate::models::qrcode::{QrConfig, QrStyle};
use base64::Engine;
use image::Luma;
use image::Rgba;
use image::RgbaImage;
use qrcode::QrCode;
use image::imageops::overlay;
use image::{ImageReader, Luma, Rgba, RgbaImage};
use qrcode::{QrCode, EcLevel};
use std::io::Cursor;
use std::path::Path;
/// 渲染基础黑白二维码
/// 渲染二维码
///
/// 根据配置生成黑白二维码图片
/// 根据配置生成二维码图片,支持颜色、形状和 Logo
///
/// # 参数
///
@@ -27,7 +27,7 @@ use std::io::Cursor;
///
/// - 二维码内容为空时返回 `InvalidData`
/// - 二维码生成失败时返回相应错误
pub fn render_basic_qr(config: &QrConfig) -> AppResult<RgbaImage> {
pub fn render_qr(config: &QrConfig) -> AppResult<RgbaImage> {
// 验证内容
if config.content.trim().is_empty() {
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() {
"L" => qrcode::EcLevel::L,
"M" => qrcode::EcLevel::M,
"Q" => qrcode::EcLevel::Q,
"H" => qrcode::EcLevel::H,
_ => qrcode::EcLevel::M, // 默认使用 M 级别
"L" => EcLevel::L,
"M" => EcLevel::M,
"Q" => EcLevel::Q,
"H" => EcLevel::H,
_ => EcLevel::M,
};
// 生成二维码
let qr_code = QrCode::with_error_correction_level(config.content.as_bytes(), ec_level)
.map_err(|e| AppError::InvalidData(format!("二维码生成失败: {}", e)))?;
// 计算包含边距的尺寸
// 获取样式配置
let style = config.style.as_ref().cloned().unwrap_or_default();
// 生成基础图像
let mut img = if style.is_gradient {
render_gradient_qr(&qr_code, config, &style)?
} else {
render_solid_color_qr(&qr_code, config, &style)?
};
// 叠加 Logo
if let Some(logo_config) = &config.logo {
overlay_logo(&mut img, logo_config)?;
}
Ok(img)
}
/// 渲染纯色二维码
fn render_solid_color_qr(
qr_code: &QrCode,
config: &QrConfig,
style: &QrStyle,
) -> AppResult<RgbaImage> {
let qr_size = qr_code.width() as u32;
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 scale = (config.size as f32 / width as f32).max(1.0) as u32;
let scaled_width = width * scale;
let scaled_height = height * scale;
// 创建 RGBA 图像并填充白色背景
let mut img = RgbaImage::new(scaled_width, scaled_height);
for pixel in img.pixels_mut() {
*pixel = Rgba([255, 255, 255, 255]);
}
// 解析颜色
let bg_color = parse_hex_color(&style.background_color);
let fg_color = parse_hex_color(&style.foreground_color);
// 渲染二维码
// 创建 RGBA 图像
let mut img = RgbaImage::new(scaled_width, scaled_height);
// 渲染每个模块
for y in 0..height {
for x in 0..width {
let pixel = qr_image.get_pixel(x, y);
// 如果是黑色像素
if pixel[0] == 0 {
let is_dark = pixel[0] == 0;
let color = if is_dark { fg_color } 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;
// 绘制黑色矩形
for py in start_y..end_y.min(scaled_height) {
for px in start_x..end_x.min(scaled_width) {
img.put_pixel(px, py, Rgba([0, 0, 0, 255]));
// 绘制模块
draw_shape(
&mut img,
start_x,
start_y,
end_x,
end_y,
color,
&style.dot_shape,
);
}
}
Ok(img)
}
/// 渲染渐变二维码
fn render_gradient_qr(
qr_code: &QrCode,
config: &QrConfig,
style: &QrStyle,
) -> AppResult<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));
}
}
}
}
}
/// 叠加 Logo
fn overlay_logo(
img: &mut RgbaImage,
logo_config: &crate::models::qrcode::LogoConfig,
) -> AppResult<()> {
// 读取 Logo 图片
let logo_path = Path::new(&logo_config.path);
let logo_img = ImageReader::open(logo_path)
.map_err(|e| AppError::IoError(format!("无法读取 Logo 文件: {}", e)))?
.decode()
.map_err(|e| AppError::IoError(format!("Logo 解码失败: {}", e)))?;
// 计算 Logo 尺寸
let (img_width, img_height) = img.dimensions();
let logo_max_size = (img_width.min(img_height) as f32 * logo_config.scale) as u32;
// 调整 Logo 尺寸
let logo_resized = logo_img.resize(
logo_max_size,
logo_max_size,
image::imageops::FilterType::Lanczos3,
);
// 转换为 RGBA
let logo_rgba = logo_resized.to_rgba8();
// 计算居中位置
let logo_x = ((img_width - logo_max_size) / 2) as i64;
let logo_y = ((img_height - logo_max_size) / 2) as i64;
// 添加白色边框
if logo_config.has_border {
let border_size = logo_config.border_width;
let border_color = Rgba([255, 255, 255, 255]);
// 绘制边框
let y_start = (logo_y - border_size as i64).max(0) as u32;
let y_end = (logo_y + logo_max_size as i64 + border_size as i64).min(img_height as i64) as u32;
let x_start = (logo_x - border_size as i64).max(0) as u32;
let x_end = (logo_x + logo_max_size as i64 + border_size as i64).min(img_width as i64) as u32;
for y in y_start..y_end {
for x in x_start..x_end {
let is_border = x < logo_x as u32
|| x >= (logo_x + logo_max_size as i64) as u32
|| y < logo_y as u32
|| y >= (logo_y + logo_max_size as i64) as u32;
if is_border {
img.put_pixel(x, y, border_color);
}
}
}
}
Ok(img)
// 叠加 Logo
overlay(img, &logo_rgba, logo_x, logo_y);
Ok(())
}
/// 解析 Hex 颜色
fn parse_hex_color(hex: &str) -> [u8; 4] {
let hex = hex.trim_start_matches('#');
let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0);
let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0);
let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0);
[r, g, b, 255]
}
/// 插值颜色
fn interpolate_color(start: [u8; 4], end: [u8; 4], progress: f32) -> [u8; 4] {
[
(start[0] as f32 + (end[0] as f32 - start[0] as f32) * progress) as u8,
(start[1] as f32 + (end[1] as f32 - start[1] as f32) * progress) as u8,
(start[2] as f32 + (end[2] as f32 - start[2] as f32) * progress) as u8,
255,
]
}
/// 将图片转换为 Base64 字符串
@@ -120,6 +383,7 @@ pub fn image_to_base64(img: &RgbaImage) -> AppResult<String> {
#[cfg(test)]
mod tests {
use super::*;
use crate::models::qrcode::{QrConfig, QrStyle};
#[test]
fn test_render_basic_qr() {
@@ -128,26 +392,46 @@ mod tests {
size: 512,
margin: 4,
error_correction: "M".to_string(),
style: None,
logo: None,
};
let result = render_basic_qr(&config);
let result = render_qr(&config);
assert!(result.is_ok());
let img = result.unwrap();
assert_eq!(img.dimensions().0, 512);
assert_eq!(img.dimensions().1, 512);
assert!(img.dimensions().0 >= 512);
assert!(img.dimensions().1 >= 512);
}
#[test]
fn test_render_empty_content() {
fn test_render_colored_qr() {
let style = QrStyle {
dot_shape: "circle".to_string(),
eye_shape: "square".to_string(),
foreground_color: "#FF0000".to_string(),
background_color: "#FFFF00".to_string(),
is_gradient: false,
gradient_colors: None,
};
let config = QrConfig {
content: "".to_string(),
content: "https://example.com".to_string(),
size: 512,
margin: 4,
error_correction: "M".to_string(),
style: Some(style),
logo: None,
};
let result = render_basic_qr(&config);
assert!(result.is_err());
let result = render_qr(&config);
assert!(result.is_ok());
}
#[test]
fn test_parse_hex_color() {
assert_eq!(parse_hex_color("#000000"), [0, 0, 0, 255]);
assert_eq!(parse_hex_color("#FFFFFF"), [255, 255, 255, 255]);
assert_eq!(parse_hex_color("#FF0000"), [255, 0, 0, 255]);
}
}

View File

@@ -14,8 +14,8 @@
{
"title": "CmdRs - 功能集合",
"label": "main",
"width": 1200,
"height": 800,
"width": 800,
"height": 600,
"minWidth": 800,
"minHeight": 600,
"decorations": true,

View File

@@ -12,7 +12,7 @@ import { QrCodeGeneratorPage } from "@/components/features/QrCodeGenerator/QrCod
function App() {
return (
<ThemeProvider defaultTheme="system" storageKey="vite-ui-theme">
<div className="w-screen h-screen">
<div className="w-full h-screen overflow-hidden">
{/* 全局快捷键监听 */}
<CommandPalette />

View File

@@ -6,14 +6,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import { Copy, Check, Droplet, RefreshCw } from 'lucide-react';
interface ColorInfo {
hex: string;
rgb: { r: number; g: number; b: number };
hsl: { h: number; s: number; l: number };
x: number;
y: number;
}
import type { ColorInfo } from '@/types/color';
interface ColorHistory {
color: ColorInfo;
@@ -70,9 +63,9 @@ export function ColorPickerPage() {
}, []);
return (
<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="flex items-center gap-3">
<Button variant="ghost" size="sm" onClick={() => window.history.back()}>
@@ -87,7 +80,7 @@ export function ColorPickerPage() {
</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">
{/* 拾色按钮 */}
<Card>

View File

@@ -4,27 +4,7 @@ import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Copy, Check, FileCode, Sparkles, Minimize2, CheckCircle2, XCircle, Upload } from 'lucide-react';
// 类型定义
interface JsonFormatConfig {
indent: number;
sort_keys: boolean;
mode: 'pretty' | 'compact';
}
interface JsonFormatResult {
success: boolean;
result: string;
error: string | null;
is_valid: boolean;
}
interface JsonValidateResult {
is_valid: boolean;
error_message: string | null;
error_line: number | null;
error_column: number | null;
}
import type { JsonFormatConfig, JsonFormatResult, JsonValidateResult } from '@/types/json';
export function JsonFormatterPage() {
const [input, setInput] = useState('');
@@ -32,7 +12,7 @@ export function JsonFormatterPage() {
const [validation, setValidation] = useState<JsonValidateResult | null>(null);
const [config, setConfig] = useState<JsonFormatConfig>({
indent: 2,
sort_keys: false,
sortKeys: false,
mode: 'pretty',
});
const [copied, setCopied] = useState(false);
@@ -151,9 +131,9 @@ export function JsonFormatterPage() {
}, []);
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="flex items-center gap-3">
<Button variant="ghost" size="sm" onClick={() => window.history.back()}>
@@ -168,7 +148,7 @@ export function JsonFormatterPage() {
</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">
{/* 配置选项 */}
<Card>
@@ -200,10 +180,10 @@ export function JsonFormatterPage() {
<label className="text-sm font-medium"> Keys:</label>
<Button
size="sm"
variant={config.sort_keys ? 'default' : 'outline'}
onClick={() => setConfig({ ...config, sort_keys: !config.sort_keys })}
variant={config.sortKeys ? 'default' : 'outline'}
onClick={() => setConfig({ ...config, sortKeys: !config.sortKeys })}
>
{config.sort_keys ? '开启' : '关闭'}
{config.sortKeys ? '开启' : '关闭'}
</Button>
</div>
@@ -270,7 +250,7 @@ export function JsonFormatterPage() {
{/* 验证状态指示器 */}
{validation && (
<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">
<CheckCircle2 className="w-3 h-3" />
@@ -286,14 +266,14 @@ export function JsonFormatterPage() {
</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">
<p className="text-sm text-destructive font-medium">
{validation.error_message}
{validation.errorMessage}
</p>
{(validation.error_line || validation.error_column) && (
{(validation.errorLine || validation.errorColumn) && (
<p className="text-xs text-destructive/80 mt-1">
位置: {validation.error_line}, {validation.error_column}
位置: {validation.errorLine}, {validation.errorColumn}
</p>
)}
</div>
@@ -303,7 +283,7 @@ export function JsonFormatterPage() {
<div className="flex gap-2 mt-4">
<Button
onClick={formatJson}
disabled={!input.trim() || isProcessing || !validation?.is_valid}
disabled={!input.trim() || isProcessing || !validation?.isValid}
className="flex-1 gap-2"
>
{isProcessing ? (
@@ -321,7 +301,7 @@ export function JsonFormatterPage() {
<Button
onClick={compactJson}
variant="outline"
disabled={!input.trim() || isProcessing || !validation?.is_valid}
disabled={!input.trim() || isProcessing || !validation?.isValid}
>
<Minimize2 className="w-4 h-4 mr-1" />

View 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>
);
}

View File

@@ -4,9 +4,11 @@
import { useEffect } from 'react';
import { useDebounce } from '@uidotdev/usehooks';
import { Button } from '@/components/ui/button';
import { useQrStore } from '@/stores/qrcodeStore';
import { QrConfigPanel } from './QrConfigPanel';
import { QrPreview } from './QrPreview';
import { QrCode } from 'lucide-react';
export function QrCodeGeneratorPage() {
const { config, updateConfig, generatePreview } = useQrStore();
@@ -22,25 +24,45 @@ export function QrCodeGeneratorPage() {
}, [debouncedConfig]);
return (
<div className="flex h-full bg-background">
{/* 左侧配置面板 */}
<div className="w-96 border-r border-border bg-card p-6 overflow-y-auto">
<div className="mb-6">
<h1 className="text-2xl font-bold"></h1>
<p className="text-sm text-muted-foreground mt-1">
</p>
<div className="flex flex-col h-screen bg-background">
{/* 顶部导航栏 */}
<header className="border-b bg-background/95 backdrop-blur flex-shrink-0">
<div className="container mx-auto px-4 py-4">
<div className="flex items-center gap-3">
<Button variant="ghost" size="sm" onClick={() => window.history.back()}>
</Button>
<div className="flex items-center gap-2">
<QrCode className="w-6 h-6 text-primary" />
<h1 className="text-xl font-bold"></h1>
</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
config={config}
onConfigChange={updateConfig}
/>
</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 />
</div>
</div>
</div>
</div>
</main>
</div>
);
}

View File

@@ -5,13 +5,13 @@
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { useQrStore } from '@/stores/qrcodeStore';
import type { QrConfig } from '@/types/qrcode';
import { ERROR_CORRECTION_OPTIONS, SIZE_PRESETS } from '@/types/qrcode';
import { Download, RotateCcw } from 'lucide-react';
import { save } from '@tauri-apps/plugin-dialog';
import { StyleOptions } from './StyleOptions';
import { LogoUpload } from './LogoUpload';
interface QrConfigPanelProps {
config: QrConfig;
@@ -23,6 +23,7 @@ export function QrConfigPanel({ config, onConfigChange }: QrConfigPanelProps) {
const handleExport = async () => {
try {
const { save } = await import('@tauri-apps/plugin-dialog');
const outputPath = await save({
title: '保存二维码',
defaultPath: `qrcode-${Date.now()}.png`,
@@ -43,11 +44,9 @@ export function QrConfigPanel({ config, onConfigChange }: QrConfigPanelProps) {
};
return (
<div className="space-y-6">
<div className="space-y-4">
{/* 基本配置 */}
<Card>
<CardContent className="pt-6 space-y-4">
{/* 内容输入 */}
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="content"></Label>
<Input
@@ -115,11 +114,16 @@ export function QrConfigPanel({ config, onConfigChange }: QrConfigPanelProps) {
className="w-full"
/>
</div>
</CardContent>
</Card>
</div>
{/* 样式配置 */}
<StyleOptions />
{/* Logo 配置 */}
<LogoUpload />
{/* 操作按钮 */}
<div className="flex gap-2">
<div className="flex gap-2 pt-2">
<Button onClick={handleExport} disabled={isGenerating} className="flex-1">
<Download className="w-4 h-4 mr-2" />
PNG

View 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>
);
}

View File

@@ -4,87 +4,7 @@ import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Monitor, Cpu, HardDrive, Database, Computer, RefreshCw, Clock, Play, Pause, Network, Wifi } from 'lucide-react';
// 类型定义
interface SystemInfo {
os: OsInfo;
cpu: CpuInfo;
memory: MemoryInfo;
gpu: GpuInfo[];
disks: DiskInfo[];
computer: ComputerInfo;
display: DisplayInfo;
network: NetworkInfo;
}
interface OsInfo {
name: string;
version: string;
arch: string;
kernel_version: string;
host_name: string;
uptime_readable: string;
}
interface CpuInfo {
model: string;
cores: number;
processors: number;
max_frequency: number;
usage_percent: number;
}
interface MemoryInfo {
total_gb: number;
available_gb: number;
used_gb: number;
usage_percent: number;
}
interface GpuInfo {
name: string;
vram_gb: number;
driver_version: string;
}
interface DiskInfo {
drive_letter: string;
volume_label: string;
file_system: string;
total_gb: number;
available_gb: number;
used_gb: number;
usage_percent: number;
}
interface ComputerInfo {
name: string;
username: string;
domain: string;
manufacturer: string;
model: string;
serial_number: string;
}
interface DisplayInfo {
monitor_count: number;
primary_resolution: string;
all_resolutions: string[];
}
interface NetworkInfo {
interfaces: InterfaceInfo[];
total_downloaded_mb: number;
total_uploaded_mb: number;
}
interface InterfaceInfo {
name: string;
mac_address: string;
ip_networks: string[];
upload_speed_kb: number;
download_speed_kb: number;
}
import type { SystemInfo } from '@/types/system';
export function SystemInfoPage() {
const [systemInfo, setSystemInfo] = useState<SystemInfo | null>(null);
@@ -202,9 +122,9 @@ export function SystemInfoPage() {
const formatGB = (value: number) => `${value.toFixed(2)} GB`;
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="flex items-center justify-between">
<div className="flex items-center gap-3">
@@ -290,7 +210,7 @@ export function SystemInfoPage() {
</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 && (
<Card className="mb-4 border-destructive">
<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.version}</span></div>
<div className="flex justify-between"><span className="text-sm text-muted-foreground"></span><span className="text-sm font-medium">{systemInfo.os.arch}</span></div>
<div className="flex justify-between"><span className="text-sm text-muted-foreground"></span><span className="text-sm font-medium">{systemInfo.os.host_name}</span></div>
<div className="flex justify-between"><span className="text-sm text-muted-foreground"></span><span className="text-sm font-medium">{systemInfo.os.uptime_readable}</span></div>
<div 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.uptimeReadable}</span></div>
</div>
</CardContent>
</Card>
@@ -366,18 +286,18 @@ export function SystemInfoPage() {
</div>
<div className="bg-muted/50 rounded p-2">
<div className="text-xs text-muted-foreground"></div>
<div className="font-semibold text-sm">{systemInfo.cpu.max_frequency}MHz</div>
<div className="font-semibold text-sm">{systemInfo.cpu.maxFrequency}MHz</div>
</div>
</div>
<div>
<div className="flex justify-between mb-1">
<span className="text-sm text-muted-foreground">使</span>
<span className="text-sm font-medium">{systemInfo.cpu.usage_percent.toFixed(1)}%</span>
<span className="text-sm font-medium">{systemInfo.cpu.usagePercent.toFixed(1)}%</span>
</div>
<div className="w-full bg-muted rounded-full h-2">
<div
className="bg-primary h-2 rounded-full transition-all duration-300"
style={{ width: `${systemInfo.cpu.usage_percent}%` }}
style={{ width: `${systemInfo.cpu.usagePercent}%` }}
/>
</div>
</div>
@@ -398,26 +318,26 @@ export function SystemInfoPage() {
<div className="grid grid-cols-3 gap-2 text-center">
<div className="bg-muted/50 rounded p-2">
<div className="text-xs text-muted-foreground"></div>
<div className="font-semibold text-sm">{formatGB(systemInfo.memory.total_gb)}</div>
<div className="font-semibold text-sm">{formatGB(systemInfo.memory.totalGb)}</div>
</div>
<div className="bg-muted/50 rounded p-2">
<div className="text-xs text-muted-foreground"></div>
<div className="font-semibold text-sm">{formatGB(systemInfo.memory.used_gb)}</div>
<div className="font-semibold text-sm">{formatGB(systemInfo.memory.usedGb)}</div>
</div>
<div className="bg-muted/50 rounded p-2">
<div className="text-xs text-muted-foreground"></div>
<div className="font-semibold text-sm text-green-600">{formatGB(systemInfo.memory.available_gb)}</div>
<div className="font-semibold text-sm text-green-600">{formatGB(systemInfo.memory.availableGb)}</div>
</div>
</div>
<div>
<div className="flex justify-between mb-1">
<span className="text-sm text-muted-foreground">使</span>
<span className="text-sm font-medium">{systemInfo.memory.usage_percent.toFixed(1)}%</span>
<span className="text-sm font-medium">{systemInfo.memory.usagePercent.toFixed(1)}%</span>
</div>
<div className="w-full bg-muted rounded-full h-2">
<div
className="bg-primary h-2 rounded-full transition-all duration-300"
style={{ width: `${systemInfo.memory.usage_percent}%` }}
style={{ width: `${systemInfo.memory.usagePercent}%` }}
/>
</div>
</div>
@@ -445,8 +365,8 @@ export function SystemInfoPage() {
<div key={index} className="border-l-2 border-primary pl-3 py-1">
<div className="text-sm font-medium mb-1">{gpu.name}</div>
<div className="flex gap-3 text-sm">
<span className="text-muted-foreground">: <span className="font-medium">{gpu.vram_gb.toFixed(1)} GB</span></span>
<span className="text-muted-foreground">: <span className="font-medium">{gpu.driver_version}</span></span>
<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.driverVersion}</span></span>
</div>
</div>
))}
@@ -465,9 +385,9 @@ export function SystemInfoPage() {
</CardHeader>
<CardContent className="py-3">
<div className="space-y-2">
<div className="flex justify-between"><span className="text-sm text-muted-foreground"></span><span className="text-sm font-medium">{systemInfo.display.monitor_count}</span></div>
<div className="flex justify-between"><span className="text-sm text-muted-foreground"></span><span className="text-sm font-medium">{systemInfo.display.primary_resolution}</span></div>
<div className="flex justify-between"><span className="text-sm text-muted-foreground"></span><span className="text-sm font-medium">{systemInfo.display.all_resolutions.join(', ')}</span></div>
<div 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.primaryResolution}</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>
</CardContent>
</Card>
@@ -485,11 +405,11 @@ export function SystemInfoPage() {
<div className="flex gap-4 mb-3">
<div className="flex-1 bg-muted/50 rounded p-2 text-center">
<div className="text-xs text-muted-foreground"></div>
<div className="font-semibold text-base text-green-600">{systemInfo.network.total_downloaded_mb.toFixed(2)} MB</div>
<div className="font-semibold text-base text-green-600">{systemInfo.network.totalDownloadedMb.toFixed(2)} MB</div>
</div>
<div className="flex-1 bg-muted/50 rounded p-2 text-center">
<div className="text-xs text-muted-foreground"></div>
<div className="font-semibold text-base text-blue-600">{systemInfo.network.total_uploaded_mb.toFixed(2)} MB</div>
<div className="font-semibold text-base text-blue-600">{systemInfo.network.totalUploadedMb.toFixed(2)} MB</div>
</div>
</div>
<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>
</div>
<div className="text-sm">
<span className="text-green-600">{iface.download_speed_kb.toFixed(1)}</span>
<span className="text-blue-600 ml-1">{iface.upload_speed_kb.toFixed(1)}</span>
<span className="text-green-600">{iface.downloadSpeedKb.toFixed(1)}</span>
<span className="text-blue-600 ml-1">{iface.uploadSpeedKb.toFixed(1)}</span>
</div>
</div>
<div className="text-sm text-muted-foreground">{iface.mac_address}</div>
{iface.ip_networks.length > 0 && (
<div className="text-sm text-muted-foreground">{iface.macAddress}</div>
{iface.ipNetworks.length > 0 && (
<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">
{ip}
</Badge>
@@ -535,36 +455,36 @@ export function SystemInfoPage() {
<div key={index} className="border rounded p-3">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<Badge variant="outline" className="text-sm">{disk.drive_letter}</Badge>
<span className="text-base font-medium">{disk.volume_label}</span>
<Badge variant="outline" className="text-sm">{disk.driveLetter}</Badge>
<span className="text-base font-medium">{disk.volumeLabel}</span>
</div>
<span className="text-sm text-muted-foreground">{disk.file_system}</span>
<span className="text-sm text-muted-foreground">{disk.fileSystem}</span>
</div>
<div className="grid grid-cols-3 gap-2 mb-2 text-center">
<div>
<div className="text-xs text-muted-foreground"></div>
<div className="text-sm font-medium">{formatGB(disk.total_gb)}</div>
<div className="text-sm font-medium">{formatGB(disk.totalGb)}</div>
</div>
<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 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 className="flex justify-between mb-1">
<span className="text-sm text-muted-foreground">使</span>
<span className="text-sm font-medium">{disk.usage_percent.toFixed(1)}%</span>
<span className="text-sm font-medium">{disk.usagePercent.toFixed(1)}%</span>
</div>
<div className="w-full bg-muted rounded-full h-2">
<div
className={`h-2 rounded-full transition-all duration-300 ${
disk.usage_percent > 90 ? 'bg-red-500' : 'bg-primary'
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>

View File

@@ -35,9 +35,9 @@ export function Home() {
}, [selectedCategory, searchQuery]);
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="flex items-center justify-between gap-4">
<div className="flex items-center gap-2">
@@ -83,7 +83,7 @@ export function Home() {
</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">
<CategoryFilter

View File

@@ -69,9 +69,9 @@ export function Search() {
}, []);
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="flex items-center gap-3 mb-4">
<Button
@@ -100,7 +100,7 @@ export function Search() {
</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">
<p className="text-sm text-muted-foreground">

View File

@@ -2,7 +2,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
export default function Settings() {
return (
<div className="container mx-auto p-6">
<div className="container mx-auto p-6 h-screen overflow-y-auto">
<Card>
<CardHeader>
<CardTitle></CardTitle>

View File

@@ -4,7 +4,8 @@
import { create } from 'zustand';
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';
interface QrState {
@@ -18,6 +19,14 @@ interface QrState {
error: string | null;
/** 更新配置 */
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;
/** 生成预览 */
@@ -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: () => {
set({
config: DEFAULT_QR_CONFIG,

43
src/types/color.ts Normal file
View 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
View 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;
}

View File

@@ -14,10 +14,14 @@ export interface QrConfig {
margin: number;
/** 容错级别 */
errorCorrection: 'L' | 'M' | 'Q' | 'H';
/** 样式配置 */
style: QrStyle;
/** Logo 配置 */
logo?: LogoConfig;
}
/**
* 二维码样式(阶段 2 使用)
* 二维码样式
*/
export interface QrStyle {
/** 点形状 */
@@ -35,12 +39,12 @@ export interface QrStyle {
}
/**
* Logo 配置(阶段 2 使用)
* Logo 配置
*/
export interface LogoConfig {
/** Logo 文件路径 */
path: string;
/** 缩放比例 (0.1 - 0.3) */
/** 缩放比例 (0.05 - 0.3) */
scale: number;
/** 是否添加边框 */
hasBorder: boolean;
@@ -68,6 +72,18 @@ export type QrCodeCommands = {
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,
margin: 4,
errorCorrection: 'M',
style: DEFAULT_QR_STYLE,
};
/**
@@ -97,3 +114,57 @@ export const SIZE_PRESETS = [
{ value: 1024, label: '大 (1024px)' },
{ value: 2048, label: '超大 (2048px)' },
] 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
View 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;
}