forked from admin/french-vocab
138 lines
4.1 KiB
TypeScript
138 lines
4.1 KiB
TypeScript
|
|
import { useState, useRef } from 'react';
|
||
|
|
|
||
|
|
interface ImportModalProps {
|
||
|
|
isOpen: boolean;
|
||
|
|
onClose: () => void;
|
||
|
|
onImport: (words: any[]) => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function ImportModal({ isOpen, onClose, onImport }: ImportModalProps) {
|
||
|
|
const [content, setContent] = useState('');
|
||
|
|
const [error, setError] = useState('');
|
||
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||
|
|
|
||
|
|
if (!isOpen) return null;
|
||
|
|
|
||
|
|
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
|
|
const file = e.target.files?.[0];
|
||
|
|
if (!file) return;
|
||
|
|
|
||
|
|
const reader = new FileReader();
|
||
|
|
reader.onload = (event) => {
|
||
|
|
const text = event.target?.result as string;
|
||
|
|
setContent(text);
|
||
|
|
parseContent(text);
|
||
|
|
};
|
||
|
|
reader.readAsText(file);
|
||
|
|
};
|
||
|
|
|
||
|
|
const parseContent = (text: string) => {
|
||
|
|
try {
|
||
|
|
// Try JSON first
|
||
|
|
const json = JSON.parse(text);
|
||
|
|
if (Array.isArray(json)) {
|
||
|
|
onImport(json);
|
||
|
|
onClose();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (json.words && Array.isArray(json.words)) {
|
||
|
|
onImport(json.words);
|
||
|
|
onClose();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
} catch {
|
||
|
|
// Not JSON, try CSV
|
||
|
|
const lines = text.trim().split('\n');
|
||
|
|
const words = lines.map((line, index) => {
|
||
|
|
const parts = line.split(',').map(p => p.trim());
|
||
|
|
return {
|
||
|
|
id: `imported-${Date.now()}-${index}`,
|
||
|
|
french: parts[0] || '',
|
||
|
|
english: parts[1] || '',
|
||
|
|
pronunciation: parts[2] || '',
|
||
|
|
category: parts[3] || 'General',
|
||
|
|
difficulty: 'beginner',
|
||
|
|
};
|
||
|
|
}).filter(w => w.french && w.english);
|
||
|
|
|
||
|
|
if (words.length > 0) {
|
||
|
|
onImport(words);
|
||
|
|
onClose();
|
||
|
|
} else {
|
||
|
|
setError('无法解析文件格式');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleSubmit = () => {
|
||
|
|
if (!content.trim()) {
|
||
|
|
setError('请输入内容');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
parseContent(content);
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
|
||
|
|
<div className="bg-white rounded-3xl shadow-2xl max-w-lg w-full p-6">
|
||
|
|
<h2 className="text-2xl font-bold text-gray-800 mb-4">导入词库</h2>
|
||
|
|
|
||
|
|
<div className="space-y-4">
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium text-gray-600 mb-2">
|
||
|
|
上传文件 (CSV 或 JSON)
|
||
|
|
</label>
|
||
|
|
<input
|
||
|
|
ref={fileInputRef}
|
||
|
|
type="file"
|
||
|
|
accept=".csv,.json"
|
||
|
|
onChange={handleFileUpload}
|
||
|
|
className="w-full px-4 py-2 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="relative">
|
||
|
|
<div className="absolute inset-0 flex items-center">
|
||
|
|
<div className="w-full border-t border-gray-200"></div>
|
||
|
|
</div>
|
||
|
|
<div className="relative flex justify-center text-sm">
|
||
|
|
<span className="px-2 bg-white text-gray-500">或粘贴内容</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<textarea
|
||
|
|
value={content}
|
||
|
|
onChange={(e) => setContent(e.target.value)}
|
||
|
|
placeholder={`CSV 格式:
|
||
|
|
french,english,pronunciation,category
|
||
|
|
Bonjour,Hello,bɔ̃ʒuʁ,Greetings
|
||
|
|
|
||
|
|
JSON 格式:
|
||
|
|
[{"french": "Bonjour", "english": "Hello"}]`}
|
||
|
|
className="w-full h-40 px-4 py-3 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono text-sm"
|
||
|
|
/>
|
||
|
|
|
||
|
|
{error && (
|
||
|
|
<p className="text-red-500 text-sm">{error}</p>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<div className="flex gap-3 pt-2">
|
||
|
|
<button
|
||
|
|
onClick={onClose}
|
||
|
|
className="flex-1 px-4 py-3 border border-gray-200 rounded-xl text-gray-600 hover:bg-gray-50 transition-colors"
|
||
|
|
>
|
||
|
|
取消
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
onClick={handleSubmit}
|
||
|
|
className="flex-1 px-4 py-3 bg-gradient-to-r from-blue-500 to-indigo-600 text-white rounded-xl hover:shadow-lg transition-all"
|
||
|
|
>
|
||
|
|
导入
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|