fix: 前端 HIGH/MEDIUM — timer 清理 + 历史持久化 + Error Boundary + console 移除
This commit is contained in:
+1
-1
@@ -74,7 +74,7 @@ fn main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
match ext.as_str() {
|
match ext.as_str() {
|
||||||
"png" => {
|
"png" => {
|
||||||
let bytes = qr.to_png_bytes(args.size);
|
let bytes = qr.to_png_bytes(args.size)?;
|
||||||
std::fs::write(&path, bytes)?;
|
std::fs::write(&path, bytes)?;
|
||||||
println!(
|
println!(
|
||||||
"已生成: {} (版本 {}, {}×{} 模块, {} 级纠错)",
|
"已生成: {} (版本 {}, {}×{} 模块, {} 级纠错)",
|
||||||
|
|||||||
+13
-19
@@ -1,11 +1,9 @@
|
|||||||
/// GF(2⁸) Galois 域运算
|
/// GF(2⁸) Galois 域运算
|
||||||
/// 本原多项式: x⁸ + x⁴ + x³ + x² + 1 = 0x11D
|
/// 本原多项式: x⁸ + x⁴ + x³ + x² + 1 = 0x11D
|
||||||
/// 生成元 α = 0x02
|
/// 生成元 α = 0x02
|
||||||
use std::sync::OnceLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
fn exp_table() -> &'static [u8; 512] {
|
static EXP_TABLE: LazyLock<[u8; 512]> = LazyLock::new(|| {
|
||||||
static TABLE: OnceLock<[u8; 512]> = OnceLock::new();
|
|
||||||
TABLE.get_or_init(|| {
|
|
||||||
let mut table = [0u8; 512];
|
let mut table = [0u8; 512];
|
||||||
let mut x = 1u8;
|
let mut x = 1u8;
|
||||||
for i in 0..255 {
|
for i in 0..255 {
|
||||||
@@ -21,12 +19,9 @@ fn exp_table() -> &'static [u8; 512] {
|
|||||||
table[510] = table[255];
|
table[510] = table[255];
|
||||||
table[511] = table[256];
|
table[511] = table[256];
|
||||||
table
|
table
|
||||||
})
|
});
|
||||||
}
|
|
||||||
|
|
||||||
fn log_table() -> &'static [u8; 256] {
|
static LOG_TABLE: LazyLock<[u8; 256]> = LazyLock::new(|| {
|
||||||
static TABLE: OnceLock<[u8; 256]> = OnceLock::new();
|
|
||||||
TABLE.get_or_init(|| {
|
|
||||||
let mut table = [0u8; 256];
|
let mut table = [0u8; 256];
|
||||||
let mut x = 1u8;
|
let mut x = 1u8;
|
||||||
for i in 0..255 {
|
for i in 0..255 {
|
||||||
@@ -39,8 +34,7 @@ fn log_table() -> &'static [u8; 256] {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
table
|
table
|
||||||
})
|
});
|
||||||
}
|
|
||||||
|
|
||||||
/// GF(2⁸) 加法 = 异或
|
/// GF(2⁸) 加法 = 异或
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -60,9 +54,9 @@ pub fn mul(a: u8, b: u8) -> u8 {
|
|||||||
if a == 0 || b == 0 {
|
if a == 0 || b == 0 {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
let log_a = log_table()[a as usize] as usize;
|
let log_a = LOG_TABLE[a as usize] as usize;
|
||||||
let log_b = log_table()[b as usize] as usize;
|
let log_b = LOG_TABLE[b as usize] as usize;
|
||||||
exp_table()[log_a + log_b]
|
EXP_TABLE[log_a + log_b]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// GF(2⁸) 除法:a / b,b == 0 时返回 None
|
/// GF(2⁸) 除法:a / b,b == 0 时返回 None
|
||||||
@@ -74,10 +68,10 @@ pub fn div(a: u8, b: u8) -> Option<u8> {
|
|||||||
if b == 0 {
|
if b == 0 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let log_a = log_table()[a as usize] as usize;
|
let log_a = LOG_TABLE[a as usize] as usize;
|
||||||
let log_b = log_table()[b as usize] as usize;
|
let log_b = LOG_TABLE[b as usize] as usize;
|
||||||
let diff = (log_a + 255 - log_b) % 255;
|
let diff = (log_a + 255 - log_b) % 255;
|
||||||
Some(exp_table()[diff])
|
Some(EXP_TABLE[diff])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// GF(2⁸) 幂运算:base^exp
|
/// GF(2⁸) 幂运算:base^exp
|
||||||
@@ -89,8 +83,8 @@ pub fn pow(base: u8, exp: usize) -> u8 {
|
|||||||
if base == 0 {
|
if base == 0 {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
let log_b = log_table()[base as usize] as usize;
|
let log_b = LOG_TABLE[base as usize] as usize;
|
||||||
exp_table()[(log_b * exp) % 255]
|
EXP_TABLE[(log_b * exp) % 255]
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -163,8 +163,8 @@ fn unicode_to_shift_jis(c: char) -> Option<u16> {
|
|||||||
let sjis = ((hi << 8) | lo) as u16;
|
let sjis = ((hi << 8) | lo) as u16;
|
||||||
// 映射到 13-bit 码字(内层 if/else 已区分两个 Shift-JIS 区间)
|
// 映射到 13-bit 码字(内层 if/else 已区分两个 Shift-JIS 区间)
|
||||||
let val = {
|
let val = {
|
||||||
let h = (sjis >> 8);
|
let h = sjis >> 8;
|
||||||
let l = (sjis & 0xFF);
|
let l = sjis & 0xFF;
|
||||||
if (0x81..=0x9F).contains(&h) {
|
if (0x81..=0x9F).contains(&h) {
|
||||||
(h - 0x81) * 0xBC + (l - 0x40)
|
(h - 0x81) * 0xBC + (l - 0x40)
|
||||||
} else {
|
} else {
|
||||||
@@ -277,6 +277,37 @@ mod tests {
|
|||||||
assert!(!is_kanji('A'));
|
assert!(!is_kanji('A'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_kanji_encode_basic() {
|
||||||
|
// 常用汉字 "中文" 应能编码且长度正确
|
||||||
|
let bits = encode_kanji("中文");
|
||||||
|
// 2 个汉字,每个 13 bit = 26 bit
|
||||||
|
assert_eq!(bits.len(), 26);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_unicode_to_shift_jis_known() {
|
||||||
|
// 基本汉字应返回 Some
|
||||||
|
assert!(unicode_to_shift_jis('中').is_some());
|
||||||
|
assert!(unicode_to_shift_jis('文').is_some());
|
||||||
|
assert!(unicode_to_shift_jis('你').is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_unicode_to_shift_jis_ascii_returns_none() {
|
||||||
|
// ASCII 字符不是汉字
|
||||||
|
assert!(unicode_to_shift_jis('A').is_none());
|
||||||
|
assert!(unicode_to_shift_jis('1').is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_kanji_mode_fallback() {
|
||||||
|
// 非 CJK 字符会被降级为 UTF-8 字节编码
|
||||||
|
let bits = encode_kanji("A");
|
||||||
|
// 1 个 ASCII 字符 = 8 bit
|
||||||
|
assert_eq!(bits.len(), 8);
|
||||||
|
}
|
||||||
|
|
||||||
fn bits_to_u16(bits: &[bool]) -> u16 {
|
fn bits_to_u16(bits: &[bool]) -> u16 {
|
||||||
bits.iter().fold(0, |acc, &b| (acc << 1) | (b as u16))
|
bits.iter().fold(0, |acc, &b| (acc << 1) | (b as u16))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -185,19 +185,19 @@ fn score_rule4(matrix: &Matrix) -> u32 {
|
|||||||
pub fn best_mask(matrix: &Matrix) -> (u8, Matrix) {
|
pub fn best_mask(matrix: &Matrix) -> (u8, Matrix) {
|
||||||
let mut best_idx = 0u8;
|
let mut best_idx = 0u8;
|
||||||
let mut best_score = u32::MAX;
|
let mut best_score = u32::MAX;
|
||||||
let mut best_matrix = matrix.clone();
|
let mut best_matrix: Option<Matrix> = None;
|
||||||
|
|
||||||
for i in 0..8u8 {
|
for i in 0..8u8 {
|
||||||
let masked = apply_mask(matrix, i);
|
let masked = apply_mask(matrix, i);
|
||||||
let s = score(&masked);
|
let s = score(&masked);
|
||||||
if s < best_score {
|
if best_matrix.is_none() || s < best_score {
|
||||||
best_score = s;
|
best_score = s;
|
||||||
best_idx = i;
|
best_idx = i;
|
||||||
best_matrix = masked;
|
best_matrix = Some(masked);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(best_idx, best_matrix)
|
(best_idx, best_matrix.unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -24,6 +24,15 @@ pub fn place_finder_patterns(matrix: &mut Matrix) {
|
|||||||
matrix.reserve(x, y);
|
matrix.reserve(x, y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 定位图案分隔符(1 模块宽的白色边框,在 finder 周围)
|
||||||
|
for i in 0..8u8 {
|
||||||
|
if fx + 7 < matrix.size {
|
||||||
|
matrix.reserve(fx + 7, fy + i); // 右侧分隔列
|
||||||
|
}
|
||||||
|
if fy + 7 < matrix.size {
|
||||||
|
matrix.reserve(fx + i, fy + 7); // 底部隔行
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,11 +68,7 @@ fn place_single_alignment(matrix: &mut Matrix, cx: u8, cy: u8) {
|
|||||||
let y0 = cy - 2;
|
let y0 = cy - 2;
|
||||||
for dy in 0..5u8 {
|
for dy in 0..5u8 {
|
||||||
for dx in 0..5u8 {
|
for dx in 0..5u8 {
|
||||||
let is_dark = match (dx, dy) {
|
let is_dark = matches!((dx, dy), (0, _) | (4, _) | (_, 0) | (_, 4) | (2, 2));
|
||||||
(0, _) | (4, _) | (_, 0) | (_, 4) => true,
|
|
||||||
(2, 2) => true,
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
let x = x0 + dx;
|
let x = x0 + dx;
|
||||||
let y = y0 + dy;
|
let y = y0 + dy;
|
||||||
matrix.set(x, y, is_dark);
|
matrix.set(x, y, is_dark);
|
||||||
|
|||||||
+1
-1
@@ -143,7 +143,7 @@ impl QrCode {
|
|||||||
crate::render::ascii::render_ascii(self, invert)
|
crate::render::ascii::render_ascii(self, invert)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_png_bytes(&self, module_size: u8) -> Vec<u8> {
|
pub fn to_png_bytes(&self, module_size: u8) -> Result<Vec<u8>, image::ImageError> {
|
||||||
crate::render::png::render_png(self, module_size)
|
crate::render::png::render_png(self, module_size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::qr::QrCode;
|
use crate::qr::QrCode;
|
||||||
use image::{ImageBuffer, Luma};
|
use image::{ImageBuffer, Luma};
|
||||||
|
|
||||||
pub fn render_png(qr: &QrCode, module_size: u8) -> Vec<u8> {
|
pub fn render_png(qr: &QrCode, module_size: u8) -> Result<Vec<u8>, image::ImageError> {
|
||||||
let matrix_size = qr.size() as u32;
|
let matrix_size = qr.size() as u32;
|
||||||
let margin = qr.margin as u32;
|
let margin = qr.margin as u32;
|
||||||
let total_size = matrix_size + 2 * margin;
|
let total_size = matrix_size + 2 * margin;
|
||||||
@@ -34,7 +34,6 @@ pub fn render_png(qr: &QrCode, module_size: u8) -> Vec<u8> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
img.write_to(&mut std::io::Cursor::new(&mut buf), image::ImageFormat::Png)
|
img.write_to(&mut std::io::Cursor::new(&mut buf), image::ImageFormat::Png)?;
|
||||||
.expect("PNG 编码失败");
|
Ok(buf)
|
||||||
buf
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ fn test_ascii_output() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_png_output() {
|
fn test_png_output() {
|
||||||
let qr = QrCode::encode("TEST", QrConfig::default()).unwrap();
|
let qr = QrCode::encode("TEST", QrConfig::default()).unwrap();
|
||||||
let png = qr.to_png_bytes(4);
|
let png = qr.to_png_bytes(4).unwrap();
|
||||||
assert!(!png.is_empty());
|
assert!(!png.is_empty());
|
||||||
// PNG 文件应以 8 字节魔术签名开头
|
// PNG 文件应以 8 字节魔术签名开头
|
||||||
assert_eq!(&png[..8], &[137, 80, 78, 71, 13, 10, 26, 10]);
|
assert_eq!(&png[..8], &[137, 80, 78, 71, 13, 10, 26, 10]);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { QrProvider, useQrState } from './store/qrContext';
|
import { QrProvider, useQrState } from './store/qrContext';
|
||||||
|
import ErrorBoundary from './components/ErrorBoundary';
|
||||||
import ModePanel from './components/ModePanel';
|
import ModePanel from './components/ModePanel';
|
||||||
import QrPreview from './components/QrPreview';
|
import QrPreview from './components/QrPreview';
|
||||||
import ExportPanel from './components/ExportPanel';
|
import ExportPanel from './components/ExportPanel';
|
||||||
@@ -60,8 +61,10 @@ function BottomInput() {
|
|||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
|
<ErrorBoundary>
|
||||||
<QrProvider>
|
<QrProvider>
|
||||||
<AppLayout />
|
<AppLayout />
|
||||||
</QrProvider>
|
</QrProvider>
|
||||||
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import React, { Component, type ReactNode } from 'react';
|
||||||
|
|
||||||
|
interface Props { children: ReactNode; }
|
||||||
|
interface State { hasError: boolean; error: Error | null; }
|
||||||
|
|
||||||
|
export default class ErrorBoundary extends Component<Props, State> {
|
||||||
|
state: State = { hasError: false, error: null };
|
||||||
|
|
||||||
|
static getDerivedStateFromError(error: Error) { return { hasError: true, error }; }
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
return (
|
||||||
|
<div className="h-screen flex flex-col items-center justify-center gap-3 bg-gray-50 dark:bg-gray-950 text-gray-600 dark:text-gray-400">
|
||||||
|
<span className="text-4xl">⚠</span>
|
||||||
|
<h2 className="text-lg font-semibold">应用发生错误</h2>
|
||||||
|
<p className="text-sm max-w-md text-center">{this.state.error?.message}</p>
|
||||||
|
<button onClick={() => window.location.reload()}
|
||||||
|
className="px-4 py-2 rounded-lg bg-blue-500 text-white text-sm hover:bg-blue-600 transition-all">
|
||||||
|
重新加载
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,7 +45,7 @@ export default function ExportPanel() {
|
|||||||
});
|
});
|
||||||
await writeFile(filePath, new Uint8Array(bytes));
|
await writeFile(filePath, new Uint8Array(bytes));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('导出 PNG 失败:', e);
|
console.warn('导出 PNG 失败:', e);
|
||||||
}
|
}
|
||||||
setExporting(false);
|
setExporting(false);
|
||||||
};
|
};
|
||||||
@@ -60,7 +60,7 @@ export default function ExportPanel() {
|
|||||||
if (!filePath) return;
|
if (!filePath) return;
|
||||||
await writeFile(filePath, new TextEncoder().encode(state.preview.svg));
|
await writeFile(filePath, new TextEncoder().encode(state.preview.svg));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('导出 SVG 失败:', e);
|
console.warn('导出 SVG 失败:', e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export default function QrPreview() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-3">
|
||||||
|
{/* SVG 由 Rust 端 qr-core 生成,仅含 <rect> 和固定颜色,无用户文本嵌入 */}
|
||||||
<div
|
<div
|
||||||
className="w-64 h-64 border-2 border-dashed border-gray-300 dark:border-gray-700 rounded-2xl p-4 flex items-center justify-center bg-white dark:bg-white qr-preview"
|
className="w-64 h-64 border-2 border-dashed border-gray-300 dark:border-gray-700 rounded-2xl p-4 flex items-center justify-center bg-white dark:bg-white qr-preview"
|
||||||
dangerouslySetInnerHTML={{ __html: state.preview.svg }}
|
dangerouslySetInnerHTML={{ __html: state.preview.svg }}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { useCallback, useRef } from 'react';
|
import { useCallback, useRef, useEffect } from 'react';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
import { Store } from '@tauri-apps/plugin-store';
|
||||||
import { useQrState } from '../store/qrContext';
|
import { useQrState } from '../store/qrContext';
|
||||||
|
import type { HistoryEntry } from '../types';
|
||||||
|
|
||||||
|
const HISTORY_KEY = 'qr-history';
|
||||||
|
|
||||||
interface QrResponse {
|
interface QrResponse {
|
||||||
svg: string;
|
svg: string;
|
||||||
@@ -15,6 +19,13 @@ export function useQrEncode() {
|
|||||||
const modeRef = useRef(state.mode);
|
const modeRef = useRef(state.mode);
|
||||||
modeRef.current = state.mode;
|
modeRef.current = state.mode;
|
||||||
|
|
||||||
|
// 组件卸载时清理定时器
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (timerRef.current) clearTimeout(timerRef.current);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const encode = useCallback((text: string) => {
|
const encode = useCallback((text: string) => {
|
||||||
if (!text.trim()) {
|
if (!text.trim()) {
|
||||||
dispatch({ type: 'SET_PREVIEW', payload: null });
|
dispatch({ type: 'SET_PREVIEW', payload: null });
|
||||||
@@ -33,18 +44,26 @@ export function useQrEncode() {
|
|||||||
});
|
});
|
||||||
dispatch({ type: 'SET_PREVIEW', payload: result });
|
dispatch({ type: 'SET_PREVIEW', payload: result });
|
||||||
|
|
||||||
// 保存到历史
|
// 保存到历史(内存 + 持久化)
|
||||||
dispatch({
|
const entryId = Date.now().toString();
|
||||||
type: 'ADD_HISTORY',
|
const entry: HistoryEntry = {
|
||||||
payload: {
|
id: entryId,
|
||||||
id: Date.now().toString(),
|
|
||||||
mode: modeRef.current,
|
mode: modeRef.current,
|
||||||
content: text,
|
content: text,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
},
|
};
|
||||||
});
|
dispatch({ type: 'ADD_HISTORY', payload: entry });
|
||||||
|
|
||||||
|
// 持久化到 tauri-plugin-store
|
||||||
|
try {
|
||||||
|
const store = await Store.load('history.json');
|
||||||
|
const current = await store.get<HistoryEntry[]>(HISTORY_KEY) || [];
|
||||||
|
const updated = [entry, ...current].slice(0, 50);
|
||||||
|
await store.set(HISTORY_KEY, updated);
|
||||||
|
await store.save();
|
||||||
|
} catch { /* store 不可用时静默忽略 */ }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('QR 编码失败:', e);
|
// 编码失败已在 dispatch SET_PREVIEW(null) 中处理,无需额外日志
|
||||||
dispatch({ type: 'SET_PREVIEW', payload: null });
|
dispatch({ type: 'SET_PREVIEW', payload: null });
|
||||||
}
|
}
|
||||||
}, 200);
|
}, 200);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { createContext, useContext, useReducer, type ReactNode } from 'react';
|
import React, { createContext, useContext, useReducer, useEffect, type ReactNode } from 'react';
|
||||||
import type { QrState, QrAction } from '../types';
|
import { Store } from '@tauri-apps/plugin-store';
|
||||||
|
import type { QrState, QrAction, HistoryEntry } from '../types';
|
||||||
|
|
||||||
const initialState: QrState = {
|
const initialState: QrState = {
|
||||||
mode: 'text',
|
mode: 'text',
|
||||||
@@ -42,6 +43,18 @@ const QrContext = createContext<{
|
|||||||
|
|
||||||
export function QrProvider({ children }: { children: ReactNode }) {
|
export function QrProvider({ children }: { children: ReactNode }) {
|
||||||
const [state, dispatch] = useReducer(qrReducer, initialState);
|
const [state, dispatch] = useReducer(qrReducer, initialState);
|
||||||
|
|
||||||
|
// 启动时从 store 加载持久化的历史记录
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const store = await Store.load('history.json');
|
||||||
|
const history = await store.get<HistoryEntry[]>('qr-history') || [];
|
||||||
|
dispatch({ type: 'SET_HISTORY', payload: history });
|
||||||
|
} catch { /* store 不可用时忽略 */ }
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return <QrContext.Provider value={{ state, dispatch }}>{children}</QrContext.Provider>;
|
return <QrContext.Provider value={{ state, dispatch }}>{children}</QrContext.Provider>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+9
-1
@@ -29,6 +29,10 @@ struct AppState {
|
|||||||
/// 编码 QR 码,返回 SVG + 元信息
|
/// 编码 QR 码,返回 SVG + 元信息
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn encode_qr(text: String, level: String, margin: u8) -> Result<QrResponse, String> {
|
fn encode_qr(text: String, level: String, margin: u8) -> Result<QrResponse, String> {
|
||||||
|
if margin > 100 {
|
||||||
|
return Err("边距过大(最大 100)".into());
|
||||||
|
}
|
||||||
|
|
||||||
let ec_level = match level.to_uppercase().as_str() {
|
let ec_level = match level.to_uppercase().as_str() {
|
||||||
"L" => EcLevel::L,
|
"L" => EcLevel::L,
|
||||||
"M" => EcLevel::M,
|
"M" => EcLevel::M,
|
||||||
@@ -58,6 +62,10 @@ fn encode_qr(text: String, level: String, margin: u8) -> Result<QrResponse, Stri
|
|||||||
/// 导出 PNG bytes
|
/// 导出 PNG bytes
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn export_png(text: String, level: String, margin: u8, module_size: u8) -> Result<Vec<u8>, String> {
|
fn export_png(text: String, level: String, margin: u8, module_size: u8) -> Result<Vec<u8>, String> {
|
||||||
|
if margin > 100 {
|
||||||
|
return Err("边距过大(最大 100)".into());
|
||||||
|
}
|
||||||
|
|
||||||
let ec_level = match level.to_uppercase().as_str() {
|
let ec_level = match level.to_uppercase().as_str() {
|
||||||
"L" => EcLevel::L,
|
"L" => EcLevel::L,
|
||||||
"M" => EcLevel::M,
|
"M" => EcLevel::M,
|
||||||
@@ -74,7 +82,7 @@ fn export_png(text: String, level: String, margin: u8, module_size: u8) -> Resul
|
|||||||
|
|
||||||
let qr = QrCode::encode(&text, config).map_err(|e| format!("编码失败: {}", e))?;
|
let qr = QrCode::encode(&text, config).map_err(|e| format!("编码失败: {}", e))?;
|
||||||
|
|
||||||
Ok(qr.to_png_bytes(module_size))
|
qr.to_png_bytes(module_size).map_err(|e| format!("PNG 导出失败: {}", e))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 保存历史记录条目
|
/// 保存历史记录条目
|
||||||
|
|||||||
Reference in New Issue
Block a user