fix: 前端 HIGH/MEDIUM — timer 清理 + 历史持久化 + Error Boundary + console 移除

This commit is contained in:
2026-06-17 09:03:38 +08:00
parent feb5ae709f
commit 91bdf9ecc3
15 changed files with 184 additions and 84 deletions
+6 -3
View File
@@ -1,4 +1,5 @@
import { QrProvider, useQrState } from './store/qrContext';
import ErrorBoundary from './components/ErrorBoundary';
import ModePanel from './components/ModePanel';
import QrPreview from './components/QrPreview';
import ExportPanel from './components/ExportPanel';
@@ -60,8 +61,10 @@ function BottomInput() {
export default function App() {
return (
<QrProvider>
<AppLayout />
</QrProvider>
<ErrorBoundary>
<QrProvider>
<AppLayout />
</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));
} catch (e) {
console.error('导出 PNG 失败:', e);
console.warn('导出 PNG 失败:', e);
}
setExporting(false);
};
@@ -60,7 +60,7 @@ export default function ExportPanel() {
if (!filePath) return;
await writeFile(filePath, new TextEncoder().encode(state.preview.svg));
} catch (e) {
console.error('导出 SVG 失败:', e);
console.warn('导出 SVG 失败:', e);
}
};
@@ -15,6 +15,7 @@ export default function QrPreview() {
return (
<div className="flex flex-col items-center gap-3">
{/* SVG 由 Rust 端 qr-core 生成,仅含 <rect> 和固定颜色,无用户文本嵌入 */}
<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"
dangerouslySetInnerHTML={{ __html: state.preview.svg }}
+31 -12
View File
@@ -1,6 +1,10 @@
import { useCallback, useRef } from 'react';
import { useCallback, useRef, useEffect } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { Store } from '@tauri-apps/plugin-store';
import { useQrState } from '../store/qrContext';
import type { HistoryEntry } from '../types';
const HISTORY_KEY = 'qr-history';
interface QrResponse {
svg: string;
@@ -15,6 +19,13 @@ export function useQrEncode() {
const modeRef = useRef(state.mode);
modeRef.current = state.mode;
// 组件卸载时清理定时器
useEffect(() => {
return () => {
if (timerRef.current) clearTimeout(timerRef.current);
};
}, []);
const encode = useCallback((text: string) => {
if (!text.trim()) {
dispatch({ type: 'SET_PREVIEW', payload: null });
@@ -33,18 +44,26 @@ export function useQrEncode() {
});
dispatch({ type: 'SET_PREVIEW', payload: result });
// 保存到历史
dispatch({
type: 'ADD_HISTORY',
payload: {
id: Date.now().toString(),
mode: modeRef.current,
content: text,
timestamp: Date.now(),
},
});
// 保存到历史(内存 + 持久化)
const entryId = Date.now().toString();
const entry: HistoryEntry = {
id: entryId,
mode: modeRef.current,
content: text,
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) {
console.error('QR 编码失败:', e);
// 编码失败已在 dispatch SET_PREVIEW(null) 中处理,无需额外日志
dispatch({ type: 'SET_PREVIEW', payload: null });
}
}, 200);
+15 -2
View File
@@ -1,5 +1,6 @@
import React, { createContext, useContext, useReducer, type ReactNode } from 'react';
import type { QrState, QrAction } from '../types';
import React, { createContext, useContext, useReducer, useEffect, type ReactNode } from 'react';
import { Store } from '@tauri-apps/plugin-store';
import type { QrState, QrAction, HistoryEntry } from '../types';
const initialState: QrState = {
mode: 'text',
@@ -42,6 +43,18 @@ const QrContext = createContext<{
export function QrProvider({ children }: { children: ReactNode }) {
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>;
}
+9 -1
View File
@@ -29,6 +29,10 @@ struct AppState {
/// 编码 QR 码,返回 SVG + 元信息
#[tauri::command]
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() {
"L" => EcLevel::L,
"M" => EcLevel::M,
@@ -58,6 +62,10 @@ fn encode_qr(text: String, level: String, margin: u8) -> Result<QrResponse, Stri
/// 导出 PNG bytes
#[tauri::command]
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() {
"L" => EcLevel::L,
"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))?;
Ok(qr.to_png_bytes(module_size))
qr.to_png_bytes(module_size).map_err(|e| format!("PNG 导出失败: {}", e))
}
/// 保存历史记录条目