feat: CLI 编码模式 + 批量生成 + text_builder

- 新增 core/src/text_builder.rs — Rust 版文本构造(WiFi/vCard/Email/Phone/SMS)
- CLI 新增 --mode 模式参数(wifi/vcard/email/phone/sms/url/batch)
- CLI 新增 --ssid/--password/--name/--phone/--to 等模式专属参数
- CLI 新增 --batch <file> 批量生成(JSON 数组 / CSV)
- 批量支持自动检测 JSON/CSV 格式并自动编号输出
- 新增 6 个 text_builder 单元测试(80 tests total)
This commit is contained in:
2026-06-19 21:15:55 +08:00
parent 38be82973e
commit c7d5252651
6 changed files with 480 additions and 59 deletions
+1
View File
@@ -4,4 +4,5 @@ pub mod encoder;
pub mod matrix;
pub mod qr;
pub mod render;
pub mod text_builder;
pub mod version;
+11 -9
View File
@@ -26,8 +26,7 @@ fn fill_module(
/// 在 QR 码 PNG 缓冲区中央叠加 logo
fn overlay_logo(img: &mut RgbaImage, logo_bytes: &[u8], logo_size_pct: f32) -> Result<(), String> {
let logo =
image::load_from_memory(logo_bytes).map_err(|e| format!("Logo 加载失败: {e}"))?;
let logo = image::load_from_memory(logo_bytes).map_err(|e| format!("Logo 加载失败: {e}"))?;
let logo = logo.to_rgba8();
let img_w = img.width();
@@ -39,12 +38,7 @@ fn overlay_logo(img: &mut RgbaImage, logo_bytes: &[u8], logo_size_pct: f32) -> R
return Ok(()); // 太小,跳过
}
let resized = imageops::resize(
&logo,
logo_size,
logo_size,
imageops::FilterType::Lanczos3,
);
let resized = imageops::resize(&logo, logo_size, logo_size, imageops::FilterType::Lanczos3);
let x = (img_w - logo_size) / 2;
let y = (img_h - logo_size) / 2;
@@ -79,7 +73,15 @@ pub fn render_png(
false
};
fill_module(&mut img, x, y, module_size as u32, is_dark, &qr.fg_color, &qr.bg_color);
fill_module(
&mut img,
x,
y,
module_size as u32,
is_dark,
&qr.fg_color,
&qr.bg_color,
);
}
}
+97
View File
@@ -0,0 +1,97 @@
//! QR 编码文本构造工具
//!
//! 集中管理各模式的文本格式(与 gui 前端 `utils/qrText.ts` 功能一致)
/// 构造 WiFi 连接字符串
pub fn build_wifi_text(ssid: &str, password: &str, encryption: &str, hidden: bool) -> String {
let h = if hidden { "H:true;" } else { "" };
format!("WIFI:T:{encryption};S:{ssid};P:{password};{h};")
}
/// 构造 vCard 字符串
pub fn build_vcard_text(
name: &str,
phone: &str,
email: &str,
company: &str,
address: &str,
) -> String {
format!("BEGIN:VCARD\nVERSION:3.0\nFN:{name}\nTEL:{phone}\nEMAIL:{email}\nORG:{company}\nADR:{address}\nEND:VCARD")
}
/// 构造 mailto 链接
pub fn build_email_text(to: &str, subject: &str, body: &str) -> String {
let subject_enc = urlencoding(subject);
let body_enc = urlencoding(body);
format!("mailto:{to}?subject={subject_enc}&body={body_enc}")
}
/// 构造电话链接
pub fn build_phone_text(number: &str) -> String {
format!("tel:{number}")
}
/// 构造短信链接
pub fn build_sms_text(number: &str, message: &str) -> String {
format!("smsto:{number}:{message}")
}
/// 简易 URL 编码(仅编码特殊字符)
fn urlencoding(s: &str) -> String {
s.chars()
.map(|c| match c {
' ' => "%20".into(),
'&' => "%26".into(),
'=' => "%3D".into(),
'#' => "%23".into(),
'%' => "%25".into(),
'+' => "%2B".into(),
'\n' => "%0A".into(),
'\r' => "%0D".into(),
_ => c.to_string(),
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_wifi_text() {
let text = build_wifi_text("MyWiFi", "pass123", "WPA", false);
assert!(text.contains("WIFI:T:WPA;S:MyWiFi;P:pass123;"));
}
#[test]
fn test_build_wifi_hidden() {
let text = build_wifi_text("HiddenNet", "secret", "WPA2", true);
assert!(text.contains("H:true;"));
}
#[test]
fn test_build_vcard_text() {
let text = build_vcard_text("张三", "13800138000", "a@b.com", "公司", "北京");
assert!(text.contains("BEGIN:VCARD"));
assert!(text.contains("FN:张三"));
assert!(text.contains("END:VCARD"));
}
#[test]
fn test_build_email_text() {
let text = build_email_text("a@b.com", "Hello World", "Test body");
assert!(text.starts_with("mailto:a@b.com"));
assert!(text.contains("Hello%20World"));
}
#[test]
fn test_build_phone_text() {
assert_eq!(build_phone_text("13800138000"), "tel:13800138000");
}
#[test]
fn test_build_sms_text() {
let text = build_sms_text("13800138000", "Hi");
assert_eq!(text, "smsto:13800138000:Hi");
}
}