Files
QRGen/gui/src-frontend/src/components/HistoryList.tsx
T
Serendipity 77fac0e28f feat: i18n 中英双语界面 — i18next + react-i18next
- 安装 i18next / react-i18next / i18next-browser-languagedetector
- 新建 src/i18n.ts 配置(fallback zh)
- 中/英翻译文件各 ~50 条目
- App.tsx 新增 EN/中 语言切换按钮
- ExportPanel + QrPreview + ModePanel + HistoryList + ErrorBoundary
- 全部 7 种模式组件均支持双语
- 12 前端测试通过,tsc 零错误
2026-06-19 21:23:10 +08:00

96 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useTranslation } from 'react-i18next';
import { useQrState } from '../store/qrContext';
import { type HistoryEntry } from '../types';
import { persistHistory } from '../hooks/useQrEncode';
const MODE_I18N: Record<string, string> = {
text: 'mode.text',
url: 'mode.url',
wifi: 'mode.wifi',
vcard: 'mode.vcard',
email: 'mode.email',
phone: 'mode.phone',
sms: 'mode.sms',
};
export default function HistoryList() {
const { t } = useTranslation();
const { state, dispatch } = useQrState();
const handleClick = (entry: HistoryEntry) => {
dispatch({ type: 'SET_MODE', payload: entry.mode });
// 优先使用存储的 formData 恢复表单,否则回退到纯文本
if (entry.formData) {
dispatch({ type: 'SET_FORM_DATA', payload: entry.formData });
} else {
dispatch({ type: 'SET_FORM_DATA', payload: { text: entry.content } });
}
};
const handleDelete = (e: React.MouseEvent, id: string) => {
e.stopPropagation();
const updated = state.history.filter((h) => h.id !== id);
dispatch({ type: 'SET_HISTORY', payload: updated });
persistHistory(updated);
};
const handleClear = () => {
dispatch({ type: 'SET_HISTORY', payload: [] });
persistHistory([]);
};
const formatTime = (ts: number) => {
const d = new Date(ts);
return `${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`;
};
return (
<div className="flex flex-col h-full">
<div className="flex items-center justify-between mb-2">
<span className="text-xs font-semibold text-gray-400 uppercase tracking-wider">
📋 {t('history.title')}
</span>
{state.history.length > 0 && (
<button
onClick={handleClear}
className="text-xs text-red-400 hover:text-red-600 transition-colors"
>
{t('history.clear')}
</button>
)}
</div>
<div className="flex-1 overflow-y-auto space-y-1">
{state.history.length === 0 && (
<p className="text-xs text-gray-400 text-center py-4">{t('history.empty')}</p>
)}
{state.history.map((entry) => (
<div
key={entry.id}
onClick={() => handleClick(entry)}
className="group flex items-center justify-between px-2 py-1.5 rounded-lg text-xs cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800 transition-all"
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-1.5">
<span className="px-1 py-0.5 rounded text-[10px] font-medium bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400">
{t(MODE_I18N[entry.mode] || entry.mode)}
</span>
<span className="text-gray-400">{formatTime(entry.timestamp)}</span>
</div>
<span className="text-gray-500 dark:text-gray-400 truncate block mt-0.5">
{entry.content.length > 20 ? entry.content.slice(0, 20) + '...' : entry.content}
</span>
</div>
<button
onClick={(e) => handleDelete(e, entry.id)}
className="opacity-0 group-hover:opacity-100 text-red-400 hover:text-red-600 ml-1 transition-all text-lg leading-none"
>
×
</button>
</div>
))}
</div>
</div>
);
}