feat: add export all words

This commit is contained in:
Kai
2025-07-06 20:34:29 -07:00
parent e5985e7bc4
commit a7d40577c9
3 changed files with 79 additions and 47 deletions

View File

@@ -1,14 +1,18 @@
import { idDictionaryMap } from '@/resources/dictionary'
import { wordListFetcher } from '@/utils/wordListFetcher'
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import { saveAs } from 'file-saver'
import type { FC } from 'react'
import { useState } from 'react'
import * as XLSX from 'xlsx'
type DropdownProps = {
renderRecords: any
paraphrases: any
}
const DropdownExport: FC<DropdownProps> = ({ renderRecords, paraphrases }) => {
const DropdownExport: FC<DropdownProps> = ({ renderRecords }) => {
const [isExporting, setIsExporting] = useState(false)
const formatTimestamp = (date: any) => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0') // 月份从0开始
@@ -20,37 +24,77 @@ const DropdownExport: FC<DropdownProps> = ({ renderRecords, paraphrases }) => {
return `${year}-${month}-${day} ${hours}-${minutes}-${seconds}`
}
const handleExport = (bookType: string) => {
const ExportData: Array<{ 单词: string; 释义: string; 错误次数: number; 词典: string }> = []
const handleExport = async (bookType: string) => {
setIsExporting(true)
renderRecords.forEach((item: any) => {
const word = paraphrases.find((w: any) => w.name === item.word)
ExportData.push({
单词: item.word,
释义: word ? word.trans.join('') : '',
错误次数: item.wrongCount,
词典: item.dict,
try {
// 获取所有需要的词典数据
const dictUrls: string[] = []
renderRecords.forEach((item: any) => {
const dictInfo = idDictionaryMap[item.dict]
if (dictInfo?.url && !dictUrls.includes(dictInfo.url)) {
dictUrls.push(dictInfo.url)
}
})
})
let blob: Blob
// 并行获取所有词典数据
const dictDataPromises = dictUrls.map(async (url) => {
try {
const data = await wordListFetcher(url)
return { url, data }
} catch (error) {
console.error(`Failed to fetch dictionary data from ${url}:`, error)
return { url, data: [] }
}
})
if (bookType === 'txt') {
const content = ExportData.map((item: any) => `${item.}: ${item.}`).join('\n')
blob = new Blob([content], { type: 'text/plain' })
} else {
const worksheet = XLSX.utils.json_to_sheet(ExportData)
const workbook = XLSX.utils.book_new()
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1')
const excelBuffer = XLSX.write(workbook, { bookType: bookType as XLSX.BookType, type: 'array' })
blob = new Blob([excelBuffer], { type: 'application/octet-stream' })
}
const dictDataResults = await Promise.all(dictDataPromises)
const dictDataMap = new Map(dictDataResults.map((result) => [result.url, result.data]))
const timestamp = formatTimestamp(new Date())
const fileName = `ErrorBook_${timestamp}.${bookType}`
const ExportData: Array<{ 单词: string; 释义: string; 错误次数: number; 词典: string }> = []
if (blob && fileName) {
saveAs(blob, fileName)
renderRecords.forEach((item: any) => {
const dictInfo = idDictionaryMap[item.dict]
let translation = ''
if (dictInfo?.url && dictDataMap.has(dictInfo.url)) {
const wordList = dictDataMap.get(dictInfo.url) || []
const word = wordList.find((w: any) => w.name === item.word)
translation = word ? word.trans.join('') : ''
}
ExportData.push({
单词: item.word,
释义: translation,
错误次数: item.wrongCount,
词典: dictInfo?.name || item.dict,
})
})
let blob: Blob
if (bookType === 'txt') {
const content = ExportData.map((item: any) => `${item.}: ${item.}`).join('\n')
blob = new Blob([content], { type: 'text/plain' })
} else {
const worksheet = XLSX.utils.json_to_sheet(ExportData)
const workbook = XLSX.utils.book_new()
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1')
const excelBuffer = XLSX.write(workbook, { bookType: bookType as XLSX.BookType, type: 'array' })
blob = new Blob([excelBuffer], { type: 'application/octet-stream' })
}
const timestamp = formatTimestamp(new Date())
const fileName = `ErrorBook_${timestamp}.${bookType}`
if (blob && fileName) {
saveAs(blob, fileName)
}
} catch (error) {
console.error('Export failed:', error)
alert('导出失败,请重试')
} finally {
setIsExporting(false)
}
}
@@ -58,18 +102,22 @@ const DropdownExport: FC<DropdownProps> = ({ renderRecords, paraphrases }) => {
<div className="z-10">
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
<button className="my-btn-primary h-8 shadow transition hover:bg-indigo-600"></button>
<button className="my-btn-primary h-8 shadow transition hover:bg-indigo-600 disabled:opacity-50" disabled={isExporting}>
{isExporting ? '导出中...' : '导出'}
</button>
</DropdownMenu.Trigger>
<DropdownMenu.Content className="mt-1 rounded bg-indigo-500 text-white shadow-lg">
<DropdownMenu.Item
className="cursor-pointer rounded px-4 py-2 hover:bg-indigo-400 focus:bg-indigo-600 focus:outline-none"
onClick={() => handleExport('xlsx')}
disabled={isExporting}
>
.xlsx
</DropdownMenu.Item>
<DropdownMenu.Item
className="cursor-pointer rounded px-4 py-2 hover:bg-indigo-600 focus:bg-indigo-600 focus:outline-none"
onClick={() => handleExport('csv')}
disabled={isExporting}
>
.csv
</DropdownMenu.Item>

View File

@@ -7,34 +7,24 @@ import { idDictionaryMap } from '@/resources/dictionary'
import { recordErrorBookAction } from '@/utils'
import { useSetAtom } from 'jotai'
import type { FC } from 'react'
import { useCallback, useEffect, useMemo, useRef } from 'react'
import { useCallback } from 'react'
import DeleteIcon from '~icons/weui/delete-filled'
type IErrorRowProps = {
record: groupedWordRecords
onDelete: () => void
onWordUpdate: (word: any) => void
}
const ErrorRow: FC<IErrorRowProps> = ({ record, onDelete, onWordUpdate }) => {
const ErrorRow: FC<IErrorRowProps> = ({ record, onDelete }) => {
const setCurrentRowDetail = useSetAtom(currentRowDetailAtom)
const dictInfo = idDictionaryMap[record.dict]
const { word, isLoading, hasError } = useGetWord(record.word, dictInfo)
const prevWordRef = useRef<any>()
const stableWord = useMemo(() => word, [word])
const onClick = useCallback(() => {
setCurrentRowDetail(record)
recordErrorBookAction('detail')
}, [record, setCurrentRowDetail])
useEffect(() => {
if (stableWord && stableWord !== prevWordRef.current) {
onWordUpdate(stableWord)
prevWordRef.current = stableWord
}
}, [stableWord, onWordUpdate])
return (
<li
className="opacity-85 flex w-full cursor-pointer items-center justify-between rounded-lg bg-white px-6 py-3 text-black shadow-md dark:bg-gray-800 dark:text-white"

View File

@@ -23,7 +23,6 @@ export function ErrorBook() {
const currentRowDetail = useAtomValue(currentRowDetailAtom)
const { deleteWordRecord } = useDeleteWordRecord()
const [reload, setReload] = useState(false)
const [paraphrases, setParaphrases] = useState<any[]>([])
const onBack = useCallback(() => {
navigate('/')
@@ -95,10 +94,6 @@ export function ErrorBook() {
setReload((prev) => !prev)
}
const handleWordUpdate = (paraphrases: object) => {
setParaphrases((prevWords) => [...prevWords, paraphrases])
}
return (
<>
<div className={`relative flex h-screen w-full flex-col items-center pb-4 ease-in ${currentRowDetail && 'blur-sm'}`}>
@@ -114,7 +109,7 @@ export function ErrorBook() {
<span className="basis-6/12"></span>
<HeadWrongNumber className="basis-1/12" sortType={sortType} setSortType={setSort} />
<span className="basis-1/12"></span>
<DropdownExport renderRecords={renderRecords} paraphrases={paraphrases} />
<DropdownExport renderRecords={sortedRecords} />
</div>
<ScrollArea.Root className="flex-1 overflow-y-auto pt-5">
<ScrollArea.Viewport className="h-full ">
@@ -124,7 +119,6 @@ export function ErrorBook() {
key={`${record.dict}-${record.word}`}
record={record}
onDelete={() => handleDelete(record.word, record.dict)}
onWordUpdate={handleWordUpdate}
/>
))}
</div>