2026-03-07 05:42:32 +00:00
import { useState , useRef } from 'react' ;
2026-03-18 15:18:47 +08:00
import type { Word } from '../types/vocabulary' ;
2026-03-07 05:42:32 +00:00
interface ImportModalProps {
isOpen : boolean ;
onClose : ( ) = > void ;
2026-03-18 15:18:47 +08:00
onImport : ( words : Word [ ] ) = > void ;
2026-03-07 05:42:32 +00:00
}
2026-03-18 15:18:47 +08:00
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 , '' ) ) ;
2026-03-07 05:42:32 +00:00
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 ) = > {
2026-03-18 15:18:47 +08:00
setError ( '' ) ;
2026-03-07 05:42:32 +00:00
try {
const json = JSON . parse ( text ) ;
if ( Array . isArray ( json ) ) {
2026-03-18 15:18:47 +08:00
const words = json . map ( ( item , index ) = > normalizeWord ( item , index ) ) . filter ( Boolean ) as Word [ ] ;
if ( words . length > 0 ) {
onImport ( words ) ;
onClose ( ) ;
return ;
}
2026-03-07 05:42:32 +00:00
}
if ( json . words && Array . isArray ( json . words ) ) {
2026-03-18 15:18:47 +08:00
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 ;
}
2026-03-07 05:42:32 +00:00
}
} catch {
2026-03-18 15:18:47 +08:00
// fallthrough to CSV
}
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 字段是否存在' ) ;
2026-03-07 05:42:32 +00:00
}
} ;
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" >
2026-03-18 15:18:47 +08:00
< 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 >
2026-03-07 05:42:32 +00:00
< 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 ) }
2026-03-18 15:18:47 +08:00
placeholder = { ` CSV 表头示例:
french , english , chinese , pronunciation , partOfSpeech , category , example , exampleTranslation , difficulty
bonjour , hello , 你 好 , bɔ̃ʒuʁ , interjection , Greetings , Bonjour tout le monde . , 大 家 好 。 , beginner
2026-03-07 05:42:32 +00:00
2026-03-18 15:18:47 +08:00
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"
2026-03-07 05:42:32 +00:00
/ >
2026-03-18 15:18:47 +08:00
{ error && < p className = "text-red-500 text-sm" > { error } < / p > }
2026-03-07 05:42:32 +00:00
< 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 >
) ;
}