mirror of
https://github.com/RealKai42/qwerty-learner.git
synced 2026-04-04 22:09:04 +08:00
Merge pull request #1005 from RealKai42/add-export-btn
feat: add export all words
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user