Update: TTS settings modal and settings types

This commit is contained in:
likingcode
2026-03-18 15:18:47 +08:00
parent 71232cf489
commit 4ea96243c2
13 changed files with 1393 additions and 149 deletions

View File

@@ -1,11 +1,42 @@
import { useState, useRef } from 'react';
import type { Word } from '../types/vocabulary';
interface ImportModalProps {
isOpen: boolean;
onClose: () => void;
onImport: (words: any[]) => void;
onImport: (words: Word[]) => void;
}
const normalizeWord = (input: Partial<Word>, index: number): Word | null => {
const french = input.french?.trim();
const english = input.english?.trim();
if (!french || !english) {
return null;
}
return {
id: input.id || `imported-${Date.now()}-${index}`,
french,
english,
pronunciation: input.pronunciation?.trim() || '',
ttsText: input.ttsText?.trim() || french,
chinese: input.chinese?.trim() || '',
partOfSpeech: input.partOfSpeech?.trim() || '',
example: input.example?.trim() || '',
exampleTranslation: input.exampleTranslation?.trim() || '',
examplePronunciation: input.examplePronunciation?.trim() || '',
notes: input.notes?.trim() || '',
audioUrl: input.audioUrl?.trim() || '',
category: input.category?.trim() || 'General',
difficulty: input.difficulty || 'beginner',
tags: input.tags || [],
};
};
const parseCsvLine = (line: string) =>
line.split(',').map(part => part.trim().replace(/^"|"$/g, ''));
export function ImportModal({ isOpen, onClose, onImport }: ImportModalProps) {
const [content, setContent] = useState('');
const [error, setError] = useState('');
@@ -27,40 +58,84 @@ export function ImportModal({ isOpen, onClose, onImport }: ImportModalProps) {
};
const parseContent = (text: string) => {
setError('');
try {
// Try JSON first
const json = JSON.parse(text);
if (Array.isArray(json)) {
onImport(json);
onClose();
return;
const words = json.map((item, index) => normalizeWord(item, index)).filter(Boolean) as Word[];
if (words.length > 0) {
onImport(words);
onClose();
return;
}
}
if (json.words && Array.isArray(json.words)) {
onImport(json.words);
onClose();
return;
const words = json.words.map((item: Partial<Word>, index: number) => normalizeWord(item, index)).filter(Boolean) as Word[];
if (words.length > 0) {
onImport(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);
// fallthrough to CSV
}
if (words.length > 0) {
onImport(words);
onClose();
} else {
setError('无法解析文件格式');
}
const lines = text
.trim()
.split('\n')
.map(line => line.trim())
.filter(Boolean);
if (lines.length === 0) {
setError('没有可导入的内容');
return;
}
const firstLine = parseCsvLine(lines[0]).map(v => v.toLowerCase());
const hasHeader = firstLine.includes('french') || firstLine.includes('english');
const dataLines = hasHeader ? lines.slice(1) : lines;
const headerIndex = hasHeader ? firstLine : [];
const words = dataLines
.map((line, index) => {
const parts = parseCsvLine(line);
if (hasHeader) {
const item: Partial<Word> = {
french: parts[headerIndex.indexOf('french')] || '',
english: parts[headerIndex.indexOf('english')] || '',
pronunciation: parts[headerIndex.indexOf('pronunciation')] || '',
chinese: parts[headerIndex.indexOf('chinese')] || '',
partOfSpeech: parts[headerIndex.indexOf('partofspeech')] || '',
example: parts[headerIndex.indexOf('example')] || '',
exampleTranslation: parts[headerIndex.indexOf('exampletranslation')] || '',
category: parts[headerIndex.indexOf('category')] || 'General',
difficulty: (parts[headerIndex.indexOf('difficulty')] as Word['difficulty']) || 'beginner',
};
return normalizeWord(item, index);
}
return normalizeWord(
{
french: parts[0] || '',
english: parts[1] || '',
pronunciation: parts[2] || '',
category: parts[3] || 'General',
example: parts[4] || '',
exampleTranslation: parts[5] || '',
},
index
);
})
.filter(Boolean) as Word[];
if (words.length > 0) {
onImport(words);
onClose();
} else {
setError('无法解析文件格式,请检查 french 和 english 字段是否存在');
}
};
@@ -74,9 +149,12 @@ export function ImportModal({ isOpen, onClose, onImport }: ImportModalProps) {
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="bg-white rounded-3xl shadow-2xl max-w-2xl w-full p-6">
<h2 className="text-2xl font-bold text-gray-800 mb-2"></h2>
<p className="text-sm text-gray-500 mb-4">
</p>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-600 mb-2">
@@ -103,18 +181,16 @@ export function ImportModal({ isOpen, onClose, onImport }: ImportModalProps) {
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder={`CSV 格式:
french,english,pronunciation,category
Bonjour,Hello,bɔ̃ʒuʁ,Greetings
placeholder={`CSV 表头示例:
french,english,chinese,pronunciation,partOfSpeech,category,example,exampleTranslation,difficulty
bonjour,hello,你好,bɔ̃ʒuʁ,interjection,Greetings,Bonjour tout le monde.,大家好。,beginner
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"
JSON 示例:
[{"french":"bonjour","english":"hello","chinese":"你好","pronunciation":"bɔ̃ʒuʁ","partOfSpeech":"interjection","example":"Bonjour, comment ça va ?","exampleTranslation":"你好,最近怎么样?"}]`}
className="w-full h-56 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>
)}
{error && <p className="text-red-500 text-sm">{error}</p>}
<div className="flex gap-3 pt-2">
<button