mirror of
https://github.com/RealKai42/qwerty-learner.git
synced 2024-11-25 16:22:53 +08:00
feat: add recite to error book (#587)
This commit is contained in:
parent
d5b9f29eb9
commit
1e66000ff1
@ -52,5 +52,6 @@ module.exports = {
|
||||
rules: {
|
||||
'sort-imports': ['error', { ignoreDeclarationSort: true }],
|
||||
'@typescript-eslint/consistent-type-imports': 1,
|
||||
'react/prop-types': 'off',
|
||||
},
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
"@radix-ui/react-slider": "^1.1.1",
|
||||
"canvas-confetti": "^1.6.0",
|
||||
"classnames": "^2.3.2",
|
||||
"daisyui": "^3.5.1",
|
||||
"dayjs": "^1.11.8",
|
||||
"dexie": "^3.2.3",
|
||||
"dexie-export-import": "^4.0.7",
|
||||
|
@ -14,7 +14,7 @@ const Header: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
<img src={logo} className="mr-3 h-16 w-16" alt="Qwerty Learner Logo" />
|
||||
<h1>Qwerty Learner</h1>
|
||||
</NavLink>
|
||||
<nav className="card on element flex w-auto content-center items-center justify-end space-x-3 rounded-xl bg-white p-4 transition-colors duration-300 dark:bg-gray-800">
|
||||
<nav className="my-card on element flex w-auto content-center items-center justify-end space-x-3 rounded-xl bg-white p-4 transition-colors duration-300 dark:bg-gray-800">
|
||||
{children}
|
||||
</nav>
|
||||
</div>
|
||||
|
@ -27,7 +27,7 @@ body,
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.card {
|
||||
.my-card {
|
||||
box-shadow: 0px 100px 80px rgba(50, 46, 129, 0.07), 0px 41.7776px 33.4221px rgba(50, 46, 129, 0.0503198),
|
||||
0px 22.3363px 17.869px rgba(50, 46, 129, 0.0417275), 0px 12.5216px 10.0172px rgba(50, 46, 129, 0.035),
|
||||
0px 6.6501px 5.32008px rgba(50, 46, 129, 0.0282725), 0px 2.76726px 2.21381px rgba(50, 46, 129, 0.0196802);
|
||||
|
@ -1,22 +1,33 @@
|
||||
import { LoadingWordUI } from './LoadingWordUI'
|
||||
import useGetWord from './hooks/useGetWord'
|
||||
import { currentRowDetailAtom } from './store'
|
||||
import type { groupedWordRecords } from './type'
|
||||
import { LoadingUI } from '@/components/Loading'
|
||||
import { idDictionaryMap } from '@/resources/dictionary'
|
||||
import { useSetAtom } from 'jotai'
|
||||
import type { FC } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
type IErrorRowProps = {
|
||||
record: groupedWordRecords
|
||||
}
|
||||
|
||||
const ErrorRow: FC<IErrorRowProps> = ({ record }) => {
|
||||
const setCurrentRowDetail = useSetAtom(currentRowDetailAtom)
|
||||
const dictInfo = idDictionaryMap[record.dict]
|
||||
const { word, isLoading } = useGetWord(record.word, dictInfo)
|
||||
const { word, isLoading, hasError } = useGetWord(record.word, dictInfo)
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
setCurrentRowDetail(record)
|
||||
}, [record, setCurrentRowDetail])
|
||||
|
||||
return (
|
||||
<li className="opacity-85 flex w-full items-center justify-between rounded-lg bg-white px-6 py-3 text-black shadow-md dark:bg-gray-800 dark:text-white">
|
||||
<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"
|
||||
onClick={onClick}
|
||||
>
|
||||
<span className="basis-2/12 break-normal">{record.word}</span>
|
||||
<span className="basis-6/12 break-normal">
|
||||
{!isLoading && word ? word.trans.join(', ') : <LoadingUI className="h-4 w-4 !border-2" />}
|
||||
{word ? word.trans.join(';') : <LoadingWordUI isLoading={isLoading} hasError={hasError} />}
|
||||
</span>
|
||||
<span className="basis-1/12 break-normal">{record.wrongCount}</span>
|
||||
<span className="basis-2/12 break-normal">{dictInfo.name}</span>
|
||||
|
23
src/pages/ErrorBook/LoadingWordUI.tsx
Normal file
23
src/pages/ErrorBook/LoadingWordUI.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { LoadingUI } from '@/components/Loading'
|
||||
import type { FC } from 'react'
|
||||
import ErrorIcon from '~icons/ic/outline-error'
|
||||
|
||||
type LoadingWordUIProps = {
|
||||
className?: string
|
||||
isLoading: boolean
|
||||
hasError: boolean
|
||||
}
|
||||
|
||||
export const LoadingWordUI: FC<LoadingWordUIProps> = ({ className, isLoading, hasError }) => {
|
||||
return (
|
||||
<div className={`${className}`}>
|
||||
{hasError ? (
|
||||
<div className="tooltip !bg-transparent" data-tip="数据加载失败">
|
||||
<ErrorIcon className="text-red-500" />
|
||||
</div>
|
||||
) : (
|
||||
isLoading && <LoadingUI />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -7,11 +7,12 @@ type IPaginationProps = {
|
||||
className?: string
|
||||
page: number
|
||||
setPage: (page: number) => void
|
||||
totalPages: number
|
||||
}
|
||||
|
||||
export const ITEM_PER_PAGE = 20
|
||||
|
||||
const Pagination: FC<IPaginationProps> = ({ className, page, setPage }) => {
|
||||
const Pagination: FC<IPaginationProps> = ({ className, page, setPage, totalPages }) => {
|
||||
const nextPage = useCallback(() => {
|
||||
setPage(page + 1)
|
||||
}, [page, setPage])
|
||||
@ -28,7 +29,7 @@ const Pagination: FC<IPaginationProps> = ({ className, page, setPage }) => {
|
||||
>
|
||||
<PrevIcon />
|
||||
</button>
|
||||
<span className="text-black dark:text-white">{page}</span>
|
||||
<span className="text-black dark:text-white">{`${page} / ${totalPages}`}</span>
|
||||
<button
|
||||
className="cursor-pointer rounded-full bg-white p-2 text-indigo-500 shadow-md dark:bg-gray-800 dark:text-indigo-300"
|
||||
onClick={nextPage}
|
||||
|
25
src/pages/ErrorBook/RowDetail/DataTag.tsx
Normal file
25
src/pages/ErrorBook/RowDetail/DataTag.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import type React from 'react'
|
||||
|
||||
interface DataTagProps {
|
||||
icon: React.ElementType
|
||||
name: string
|
||||
data: number | string
|
||||
}
|
||||
|
||||
const DataTag: React.FC<DataTagProps> = ({ icon, name, data }) => {
|
||||
const IconComponent = icon
|
||||
|
||||
return (
|
||||
<div className="g flex h-10 w-40 flex-1 select-none items-center justify-between rounded-md border-gray-400 bg-gray-100 px-3 py-5 shadow dark:border-gray-600 dark:bg-gray-800">
|
||||
<div className="flex items-center space-x-1 ">
|
||||
<IconComponent className="h-4 w-4 text-gray-700 dark:text-gray-300" />
|
||||
<span className="break-keep text-base font-normal text-gray-500 dark:text-gray-300">{name}</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-base font-normal text-gray-800 dark:text-gray-200">{data}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DataTag
|
83
src/pages/ErrorBook/RowDetail/RowPagination.tsx
Normal file
83
src/pages/ErrorBook/RowDetail/RowPagination.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
import { currentRowDetailAtom } from '../store'
|
||||
import type { groupedWordRecords } from '../type'
|
||||
import { useAtom } from 'jotai'
|
||||
import type { FC } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import NextIcon from '~icons/ooui/next-ltr'
|
||||
import PrevIcon from '~icons/ooui/next-rtl'
|
||||
|
||||
type IRowPaginationProps = {
|
||||
className?: string
|
||||
allRecords: groupedWordRecords[]
|
||||
}
|
||||
|
||||
export const ITEM_PER_PAGE = 20
|
||||
|
||||
const RowPagination: FC<IRowPaginationProps> = ({ className, allRecords }) => {
|
||||
const [currentRowDetail, setCurrentRowDetail] = useAtom(currentRowDetailAtom)
|
||||
const currentIndex = useMemo(() => {
|
||||
if (!currentRowDetail) return -1
|
||||
return allRecords.findIndex((record) => record.word === currentRowDetail.word && record.dict === currentRowDetail.dict)
|
||||
}, [currentRowDetail, allRecords])
|
||||
|
||||
const nextRowDetail = useCallback(() => {
|
||||
if (!currentRowDetail) return
|
||||
|
||||
const index = currentIndex
|
||||
if (index === -1) return
|
||||
const nextIndex = index + 1
|
||||
if (nextIndex >= allRecords.length) return
|
||||
setCurrentRowDetail(allRecords[nextIndex])
|
||||
}, [currentRowDetail, currentIndex, allRecords, setCurrentRowDetail])
|
||||
|
||||
const prevRowDetail = useCallback(() => {
|
||||
if (!currentRowDetail) return
|
||||
|
||||
const index = currentIndex
|
||||
if (index === -1) return
|
||||
const prevIndex = index - 1
|
||||
if (prevIndex < 0) return
|
||||
setCurrentRowDetail(allRecords[prevIndex])
|
||||
}, [currentRowDetail, currentIndex, setCurrentRowDetail, allRecords])
|
||||
|
||||
useHotkeys(
|
||||
'left',
|
||||
(e) => {
|
||||
prevRowDetail()
|
||||
e.stopPropagation()
|
||||
},
|
||||
{
|
||||
preventDefault: true,
|
||||
},
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'right',
|
||||
(e) => {
|
||||
nextRowDetail()
|
||||
e.stopPropagation()
|
||||
},
|
||||
{
|
||||
preventDefault: true,
|
||||
},
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={`-gap-1 flex select-none items-center ${className}`}>
|
||||
<button
|
||||
className="d cursor-pointer rounded-full p-1 text-indigo-500 focus:outline-none dark:text-indigo-300"
|
||||
onClick={prevRowDetail}
|
||||
>
|
||||
<PrevIcon />
|
||||
</button>
|
||||
<span className="text-sm text-black dark:text-white">{`${currentIndex + 1} / ${allRecords.length}`}</span>
|
||||
<button className="cursor-pointer rounded-full p-1 text-indigo-500 focus:outline-none dark:text-indigo-300" onClick={nextRowDetail}>
|
||||
<NextIcon />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RowPagination
|
96
src/pages/ErrorBook/RowDetail/index.tsx
Normal file
96
src/pages/ErrorBook/RowDetail/index.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
import { LoadingWordUI } from '../LoadingWordUI'
|
||||
import useGetWord from '../hooks/useGetWord'
|
||||
import { currentRowDetailAtom } from '../store'
|
||||
import type { groupedWordRecords } from '../type'
|
||||
import DataTag from './DataTag'
|
||||
import RowPagination from './RowPagination'
|
||||
import Phonetic from '@/pages/Typing/components/WordPanel/components/Phonetic'
|
||||
import Letter from '@/pages/Typing/components/WordPanel/components/Word/Letter'
|
||||
import { idDictionaryMap } from '@/resources/dictionary'
|
||||
import { useSetAtom } from 'jotai'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import HashtagIcon from '~icons/heroicons/chart-pie-20-solid'
|
||||
import CheckCircle from '~icons/heroicons/check-circle-20-solid'
|
||||
import ClockIcon from '~icons/heroicons/clock-20-solid'
|
||||
import XCircle from '~icons/heroicons/x-circle-20-solid'
|
||||
import IconX from '~icons/tabler/x'
|
||||
|
||||
type RowDetailProps = {
|
||||
currentRowDetail: groupedWordRecords
|
||||
allRecords: groupedWordRecords[]
|
||||
}
|
||||
|
||||
const RowDetail: React.FC<RowDetailProps> = ({ currentRowDetail, allRecords }) => {
|
||||
const setCurrentRowDetail = useSetAtom(currentRowDetailAtom)
|
||||
const dictInfo = idDictionaryMap[currentRowDetail.dict]
|
||||
const { word, isLoading, hasError } = useGetWord(currentRowDetail.word, dictInfo)
|
||||
|
||||
const rowDetailData: RowDetailData = useMemo(() => {
|
||||
const time =
|
||||
currentRowDetail.records.length > 0
|
||||
? currentRowDetail.records.reduce((acc, cur) => acc + cur.totalTime, 0) / currentRowDetail.records.length
|
||||
: 0
|
||||
const timeStr = (time / 1000).toFixed(2)
|
||||
const correctCount = currentRowDetail.records.length
|
||||
const wrongCount = currentRowDetail.wrongCount
|
||||
const sumCount = correctCount + wrongCount
|
||||
return { time: timeStr, sumCount, correctCount, wrongCount }
|
||||
}, [currentRowDetail.records, currentRowDetail.wrongCount])
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
setCurrentRowDetail(null)
|
||||
}, [setCurrentRowDetail])
|
||||
|
||||
useHotkeys(
|
||||
'esc',
|
||||
(e) => {
|
||||
onClose()
|
||||
e.stopPropagation()
|
||||
},
|
||||
{ preventDefault: true },
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center ">
|
||||
<div className="my-card relative z-10 flex h-[32rem] min-w-[26rem] select-text flex-col items-center justify-around rounded-2xl bg-white px-3 py-10 dark:bg-gray-900">
|
||||
<IconX className="absolute right-3 top-3 h-6 w-6 cursor-pointer text-gray-400" onClick={onClose} />
|
||||
<div className="flex flex-col items-center justify-start">
|
||||
<div>
|
||||
{currentRowDetail.word.split('').map((t, index) => (
|
||||
<Letter key={`${index}-${t}`} letter={t} visible state="normal" />
|
||||
))}
|
||||
</div>
|
||||
<div>{word ? <Phonetic word={word} /> : <LoadingWordUI isLoading={isLoading} hasError={hasError} />}</div>
|
||||
<div className="flex max-w-[24rem] items-center">
|
||||
<span className={`max-w-4xl text-center font-sans transition-colors duration-300 dark:text-white dark:text-opacity-80`}>
|
||||
{word ? word.trans.join(';') : <LoadingWordUI isLoading={isLoading} hasError={hasError} />}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="item flex flex-col gap-4">
|
||||
<div className="flex gap-6">
|
||||
<DataTag icon={ClockIcon} name="平均用时" data={rowDetailData.time} />
|
||||
<DataTag icon={HashtagIcon} name="练习次数" data={rowDetailData.sumCount} />
|
||||
</div>
|
||||
<div className="flex gap-6">
|
||||
<DataTag icon={CheckCircle} name="正确次数" data={rowDetailData.correctCount} />
|
||||
<DataTag icon={XCircle} name="错误次数" data={rowDetailData.wrongCount} />
|
||||
</div>
|
||||
</div>
|
||||
<RowPagination className="absolute bottom-6 mt-10" allRecords={allRecords} />
|
||||
</div>
|
||||
<div className="absolute inset-0 z-0 cursor-pointer bg-transparent" onClick={onClose}></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type RowDetailData = {
|
||||
time: string
|
||||
sumCount: number
|
||||
correctCount: number
|
||||
wrongCount: number
|
||||
}
|
||||
|
||||
export default RowDetail
|
@ -1,16 +1,27 @@
|
||||
import type { Dictionary, Word } from '@/typings'
|
||||
import { wordListFetcher } from '@/utils/wordListFetcher'
|
||||
import { useMemo } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import useSWR from 'swr'
|
||||
|
||||
export default function useGetWord(name: string, dict: Dictionary) {
|
||||
const { data: wordList, error, isLoading } = useSWR(dict.url, wordListFetcher)
|
||||
const { data: wordList, error, isLoading } = useSWR(dict?.url, wordListFetcher)
|
||||
const [hasError, setHasError] = useState(false)
|
||||
|
||||
const word: Word | undefined = useMemo(() => {
|
||||
if (!wordList) return undefined
|
||||
|
||||
return wordList.find((word) => word.name === name)
|
||||
const word = wordList.find((word) => word.name === name)
|
||||
if (word) {
|
||||
return word
|
||||
} else {
|
||||
setHasError(true)
|
||||
return undefined
|
||||
}
|
||||
}, [wordList, name])
|
||||
|
||||
return { word, isLoading, error }
|
||||
useEffect(() => {
|
||||
if (error) setHasError(true)
|
||||
}, [error])
|
||||
|
||||
return { word, isLoading, hasError }
|
||||
}
|
||||
|
@ -2,9 +2,13 @@ import ErrorRow from './ErrorRow'
|
||||
import type { ISortType } from './HeadWrongNumber'
|
||||
import HeadWrongNumber from './HeadWrongNumber'
|
||||
import Pagination, { ITEM_PER_PAGE } from './Pagination'
|
||||
import RowDetail from './RowDetail'
|
||||
import { currentRowDetailAtom } from './store'
|
||||
import type { groupedWordRecords } from './type'
|
||||
import { db } from '@/utils/db'
|
||||
import type { WordRecord } from '@/utils/db/record'
|
||||
import * as ScrollArea from '@radix-ui/react-scroll-area'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import IconX from '~icons/tabler/x'
|
||||
@ -15,6 +19,7 @@ export function ErrorBook() {
|
||||
const totalPages = useMemo(() => Math.ceil(groupedRecords.length / ITEM_PER_PAGE), [groupedRecords.length])
|
||||
const [sortType, setSortType] = useState<ISortType>('asc')
|
||||
const navigate = useNavigate()
|
||||
const currentRowDetail = useAtomValue(currentRowDetailAtom)
|
||||
|
||||
const onBack = useCallback(() => {
|
||||
navigate('/')
|
||||
@ -47,6 +52,12 @@ export function ErrorBook() {
|
||||
})
|
||||
}, [groupedRecords, sortType])
|
||||
|
||||
const renderRecords = useMemo(() => {
|
||||
const start = (currentPage - 1) * ITEM_PER_PAGE
|
||||
const end = start + ITEM_PER_PAGE
|
||||
return sortedRecords.slice(start, end)
|
||||
}, [currentPage, sortedRecords])
|
||||
|
||||
useEffect(() => {
|
||||
db.wordRecords
|
||||
.where('wrongCount')
|
||||
@ -61,7 +72,7 @@ export function ErrorBook() {
|
||||
group = { word: record.word, dict: record.dict, records: [], wrongCount: 0 }
|
||||
groups.push(group)
|
||||
}
|
||||
group.records.push(record)
|
||||
group.records.push(record as WordRecord)
|
||||
})
|
||||
|
||||
groups.forEach((group) => {
|
||||
@ -75,36 +86,37 @@ export function ErrorBook() {
|
||||
})
|
||||
}, [])
|
||||
|
||||
const renderRecords = useMemo(() => {
|
||||
const start = (currentPage - 1) * ITEM_PER_PAGE
|
||||
const end = start + ITEM_PER_PAGE
|
||||
return sortedRecords.slice(start, end)
|
||||
}, [currentPage, sortedRecords])
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-full flex-col items-center pb-4">
|
||||
<IconX className="absolute right-10 top-5 mr-2 h-7 w-7 cursor-pointer text-gray-400" onClick={onBack} />
|
||||
<div className="flex w-full flex-1 select-text items-start justify-center overflow-hidden">
|
||||
<div className="flex h-full w-5/6 flex-col pt-10">
|
||||
<div className="flex w-full justify-between rounded-lg bg-white px-6 py-5 text-lg text-black shadow-lg dark:bg-gray-800 dark:text-white">
|
||||
<span className="basis-2/12">单词</span>
|
||||
<span className="basis-6/12">释义</span>
|
||||
<HeadWrongNumber className="basis-1/12" sortType={sortType} setSortType={setSort} />
|
||||
<span className="basis-2/12">词典</span>
|
||||
</div>
|
||||
<ScrollArea.Root className="flex-1 overflow-y-auto pt-5">
|
||||
<ScrollArea.Viewport className="h-full ">
|
||||
<div className="flex flex-col gap-3">
|
||||
{renderRecords.map((record) => (
|
||||
<ErrorRow key={`${record.dict}-${record.word}`} record={record} />
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea.Viewport>
|
||||
<ScrollArea.Scrollbar className="flex touch-none select-none bg-transparent" orientation="vertical"></ScrollArea.Scrollbar>
|
||||
</ScrollArea.Root>
|
||||
<>
|
||||
<div className={`relative flex h-screen w-full flex-col items-center pb-4 ease-in ${currentRowDetail && 'blur-sm'}`}>
|
||||
<div className="mr-8 mt-4 flex w-auto items-center justify-center self-end">
|
||||
<h1 className="font-lighter mr-4 w-auto self-end text-gray-500 opacity-70">Tip: 点击错误单词查看详细信息 </h1>
|
||||
<IconX className="h-7 w-7 cursor-pointer text-gray-400" onClick={onBack} />
|
||||
</div>
|
||||
|
||||
<div className="flex w-full flex-1 select-text items-start justify-center overflow-hidden">
|
||||
<div className="flex h-full w-5/6 flex-col pt-10">
|
||||
<div className="flex w-full justify-between rounded-lg bg-white px-6 py-5 text-lg text-black shadow-lg dark:bg-gray-800 dark:text-white">
|
||||
<span className="basis-2/12">单词</span>
|
||||
<span className="basis-6/12">释义</span>
|
||||
<HeadWrongNumber className="basis-1/12" sortType={sortType} setSortType={setSort} />
|
||||
<span className="basis-2/12">词典</span>
|
||||
</div>
|
||||
<ScrollArea.Root className="flex-1 overflow-y-auto pt-5">
|
||||
<ScrollArea.Viewport className="h-full ">
|
||||
<div className="flex flex-col gap-3">
|
||||
{renderRecords.map((record) => (
|
||||
<ErrorRow key={`${record.dict}-${record.word}`} record={record} />
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea.Viewport>
|
||||
<ScrollArea.Scrollbar className="flex touch-none select-none bg-transparent" orientation="vertical"></ScrollArea.Scrollbar>
|
||||
</ScrollArea.Root>
|
||||
</div>
|
||||
</div>
|
||||
<Pagination className="pt-3" page={currentPage} setPage={setPage} totalPages={totalPages} />
|
||||
</div>
|
||||
<Pagination className="pt-3" page={currentPage} setPage={setPage} />
|
||||
</div>
|
||||
{currentRowDetail && <RowDetail currentRowDetail={currentRowDetail} allRecords={sortedRecords} />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
4
src/pages/ErrorBook/store/index.ts
Normal file
4
src/pages/ErrorBook/store/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import type { groupedWordRecords } from '../type'
|
||||
import { atom } from 'jotai'
|
||||
|
||||
export const currentRowDetailAtom = atom<groupedWordRecords | null>(null)
|
@ -1,8 +1,8 @@
|
||||
import type { IWordRecord } from '@/utils/db/record'
|
||||
import type { WordRecord } from '@/utils/db/record'
|
||||
|
||||
export type groupedWordRecords = {
|
||||
word: string
|
||||
dict: string
|
||||
records: IWordRecord[]
|
||||
records: WordRecord[]
|
||||
wrongCount: number
|
||||
}
|
||||
|
@ -180,7 +180,7 @@ const ResultScreen = () => {
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
<div className="card fixed flex w-[90vw] max-w-6xl flex-col overflow-hidden rounded-3xl bg-white pb-14 pl-10 pr-5 pt-10 shadow-lg dark:bg-gray-800 md:w-4/5 lg:w-3/5">
|
||||
<div className="my-card fixed flex w-[90vw] max-w-6xl flex-col overflow-hidden rounded-3xl bg-white pb-14 pl-10 pr-5 pt-10 shadow-lg dark:bg-gray-800 md:w-4/5 lg:w-3/5">
|
||||
<div className="text-center font-sans text-xl font-normal text-gray-900 dark:text-gray-400 md:text-2xl">
|
||||
{`${currentDictInfo.name} 第 ${currentChapter + 1} 章`}
|
||||
</div>
|
||||
@ -268,7 +268,7 @@ const ResultScreen = () => {
|
||||
{!isLastChapter && (
|
||||
<Tooltip content="快捷键:enter">
|
||||
<button
|
||||
className={`btn-primary { isLastChapter ? 'cursor-not-allowed opacity-50' : ''} h-12 text-base font-bold `}
|
||||
className={`{ isLastChapter ? 'cursor-not-allowed opacity-50' : ''} btn-primary h-12 text-base font-bold `}
|
||||
type="button"
|
||||
onClick={nextButtonHandler}
|
||||
title="下一章节"
|
||||
|
@ -12,7 +12,7 @@ export default function Speed() {
|
||||
const inputNumber = state.chapterData.correctCount + state.chapterData.wrongCount
|
||||
|
||||
return (
|
||||
<div className="card flex w-3/5 rounded-xl bg-white p-4 py-10 opacity-50 transition-colors duration-300 dark:bg-gray-800">
|
||||
<div className="my-card flex w-3/5 rounded-xl bg-white p-4 py-10 opacity-50 transition-colors duration-300 dark:bg-gray-800">
|
||||
<InfoBox info={`${minutesString}:${secondsString}`} description="时间" />
|
||||
<InfoBox info={inputNumber + ''} description="输入数" />
|
||||
<InfoBox info={state.timerData.wpm + ''} description="WPM" />
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { isTextSelectableAtom, phoneticConfigAtom } from '@/store'
|
||||
import type { WordWithIndex } from '@/typings'
|
||||
import type { Word, WordWithIndex } from '@/typings'
|
||||
import { useAtomValue } from 'jotai'
|
||||
|
||||
export type PhoneticProps = {
|
||||
word: WordWithIndex
|
||||
word: WordWithIndex | Word
|
||||
}
|
||||
|
||||
function Phonetic({ word }: PhoneticProps) {
|
||||
|
@ -55,5 +55,23 @@ module.exports = {
|
||||
backgroundOpacity: ['dark'],
|
||||
},
|
||||
},
|
||||
plugins: [require('@headlessui/tailwindcss'), require('@tailwindcss/forms')],
|
||||
plugins: [require('@headlessui/tailwindcss'), require('@tailwindcss/forms'), require('daisyui')],
|
||||
daisyui: {
|
||||
themes: [
|
||||
{
|
||||
mytheme: {
|
||||
primary: '#6366f1',
|
||||
secondary: '#7dd3fc',
|
||||
accent: '#cc8316',
|
||||
neutral: '#272735',
|
||||
'base-100': '#f0eff1',
|
||||
info: '#f3f4f6',
|
||||
success: '#6fe7ab',
|
||||
warning: '#d6920a',
|
||||
error: '#f43f5e',
|
||||
},
|
||||
},
|
||||
'dark',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
68
yarn.lock
68
yarn.lock
@ -2539,6 +2539,11 @@ color@^4.0.1:
|
||||
color-convert "^2.0.1"
|
||||
color-string "^1.9.0"
|
||||
|
||||
colord@^2.9:
|
||||
version "2.9.3"
|
||||
resolved "https://registry.npmmirror.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43"
|
||||
integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==
|
||||
|
||||
colorette@^2.0.19:
|
||||
version "2.0.20"
|
||||
resolved "https://registry.npmmirror.com/colorette/-/colorette-2.0.20.tgz"
|
||||
@ -2640,6 +2645,14 @@ css-color-names@^0.0.4:
|
||||
resolved "https://registry.npmmirror.com/css-color-names/-/css-color-names-0.0.4.tgz"
|
||||
integrity sha512-zj5D7X1U2h2zsXOAM8EyUREBnnts6H+Jm+d1M2DbiQQcUtnqgQsMrdo8JW9R80YFUmIdBZeMu5wvYM7hcgWP/Q==
|
||||
|
||||
css-selector-tokenizer@^0.8:
|
||||
version "0.8.0"
|
||||
resolved "https://registry.npmmirror.com/css-selector-tokenizer/-/css-selector-tokenizer-0.8.0.tgz#88267ef6238e64f2215ea2764b3e2cf498b845dd"
|
||||
integrity sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==
|
||||
dependencies:
|
||||
cssesc "^3.0.0"
|
||||
fastparse "^1.1.2"
|
||||
|
||||
css-unit-converter@^1.1.1:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.npmmirror.com/css-unit-converter/-/css-unit-converter-1.1.2.tgz"
|
||||
@ -2655,6 +2668,17 @@ csstype@^3.0.2:
|
||||
resolved "https://registry.npmmirror.com/csstype/-/csstype-3.1.2.tgz"
|
||||
integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
|
||||
|
||||
daisyui@^3.5.1:
|
||||
version "3.5.1"
|
||||
resolved "https://registry.npmmirror.com/daisyui/-/daisyui-3.5.1.tgz#7b53668e2b68007b275fec88268f81b4aeaf3781"
|
||||
integrity sha512-7GG+9QXnr2qQMCqnyFU8TxpaOYJigXiEtmzoivmiiZZHvxqIwYdaMAkgivqTVxEgy3Hot3m1suzZjmt1zUrvmA==
|
||||
dependencies:
|
||||
colord "^2.9"
|
||||
css-selector-tokenizer "^0.8"
|
||||
postcss "^8"
|
||||
postcss-js "^4"
|
||||
tailwindcss "^3"
|
||||
|
||||
damerau-levenshtein@^1.0.8:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.npmmirror.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz"
|
||||
@ -3289,6 +3313,11 @@ fast-levenshtein@^2.0.6:
|
||||
resolved "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz"
|
||||
integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
|
||||
|
||||
fastparse@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.npmmirror.com/fastparse/-/fastparse-1.1.2.tgz#91728c5a5942eced8531283c79441ee4122c35a9"
|
||||
integrity sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==
|
||||
|
||||
fastq@^1.6.0:
|
||||
version "1.15.0"
|
||||
resolved "https://registry.npmmirror.com/fastq/-/fastq-1.15.0.tgz"
|
||||
@ -4624,7 +4653,7 @@ postcss-js@^2:
|
||||
camelcase-css "^2.0.1"
|
||||
postcss "^7.0.18"
|
||||
|
||||
postcss-js@^4.0.1:
|
||||
postcss-js@^4, postcss-js@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.npmmirror.com/postcss-js/-/postcss-js-4.0.1.tgz"
|
||||
integrity sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==
|
||||
@ -4718,6 +4747,15 @@ postcss@^7, postcss@^7.0.18, postcss@^7.0.32:
|
||||
picocolors "^0.2.1"
|
||||
source-map "^0.6.1"
|
||||
|
||||
postcss@^8:
|
||||
version "8.4.28"
|
||||
resolved "https://registry.npmmirror.com/postcss/-/postcss-8.4.28.tgz#c6cc681ed00109072816e1557f889ef51cf950a5"
|
||||
integrity sha512-Z7V5j0cq8oEKyejIKfpD8b4eBy9cwW2JWPk0+fB1HOAMsfHbnAXLLS+PfVWlzMSLQaWttKDt607I0XHmpE67Vw==
|
||||
dependencies:
|
||||
nanoid "^3.3.6"
|
||||
picocolors "^1.0.0"
|
||||
source-map-js "^1.0.2"
|
||||
|
||||
postcss@^8.0.0, postcss@^8.3.5, postcss@^8.4.21, postcss@^8.4.23:
|
||||
version "8.4.23"
|
||||
resolved "https://registry.npmmirror.com/postcss/-/postcss-8.4.23.tgz"
|
||||
@ -5397,6 +5435,34 @@ tabbable@^6.0.1:
|
||||
resolved "https://registry.npmmirror.com/tabbable/-/tabbable-6.1.2.tgz"
|
||||
integrity sha512-qCN98uP7i9z0fIS4amQ5zbGBOq+OSigYeGvPy7NDk8Y9yncqDZ9pRPgfsc2PJIVM9RrJj7GIfuRgmjoUU9zTHQ==
|
||||
|
||||
tailwindcss@^3:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.3.3.tgz#90da807393a2859189e48e9e7000e6880a736daf"
|
||||
integrity sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==
|
||||
dependencies:
|
||||
"@alloc/quick-lru" "^5.2.0"
|
||||
arg "^5.0.2"
|
||||
chokidar "^3.5.3"
|
||||
didyoumean "^1.2.2"
|
||||
dlv "^1.1.3"
|
||||
fast-glob "^3.2.12"
|
||||
glob-parent "^6.0.2"
|
||||
is-glob "^4.0.3"
|
||||
jiti "^1.18.2"
|
||||
lilconfig "^2.1.0"
|
||||
micromatch "^4.0.5"
|
||||
normalize-path "^3.0.0"
|
||||
object-hash "^3.0.0"
|
||||
picocolors "^1.0.0"
|
||||
postcss "^8.4.23"
|
||||
postcss-import "^15.1.0"
|
||||
postcss-js "^4.0.1"
|
||||
postcss-load-config "^4.0.1"
|
||||
postcss-nested "^6.0.1"
|
||||
postcss-selector-parser "^6.0.11"
|
||||
resolve "^1.22.2"
|
||||
sucrase "^3.32.0"
|
||||
|
||||
tailwindcss@^3.3.1:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.3.2.tgz"
|
||||
|
Loading…
Reference in New Issue
Block a user