fix: 前端 HIGH/MEDIUM — timer 清理 + 历史持久化 + Error Boundary + console 移除
This commit is contained in:
@@ -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 }}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user