77fac0e28f
- 安装 i18next / react-i18next / i18next-browser-languagedetector - 新建 src/i18n.ts 配置(fallback zh) - 中/英翻译文件各 ~50 条目 - App.tsx 新增 EN/中 语言切换按钮 - ExportPanel + QrPreview + ModePanel + HistoryList + ErrorBoundary - 全部 7 种模式组件均支持双语 - 12 前端测试通过,tsc 零错误
96 lines
3.4 KiB
TypeScript
96 lines
3.4 KiB
TypeScript
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>
|
||
);
|
||
}
|