mirror of
https://github.com/RealKai42/qwerty-learner.git
synced 2026-04-05 14:29:04 +08:00
257 lines
12 KiB
TypeScript
257 lines
12 KiB
TypeScript
import { TypingContext, TypingStateActionType } from '../../store'
|
||
import ShareButton from '../ShareButton'
|
||
import ConclusionBar from './ConclusionBar'
|
||
import RemarkRing from './RemarkRing'
|
||
import WordChip from './WordChip'
|
||
import styles from './index.module.css'
|
||
import redBookLogo from '@/assets/redBook-color-logo.svg'
|
||
import Tooltip from '@/components/Tooltip'
|
||
import { currentChapterAtom, currentDictInfoAtom, infoPanelStateAtom, randomConfigAtom } from '@/store'
|
||
import { InfoPanelType } from '@/typings'
|
||
import { WordWithIndex } from '@/typings'
|
||
import { recordOpenInfoPanelAction } from '@/utils'
|
||
import { Transition } from '@headlessui/react'
|
||
import { IconX } from '@tabler/icons-react'
|
||
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||
import { useCallback, useContext, useMemo } from 'react'
|
||
import { useHotkeys } from 'react-hotkeys-hook'
|
||
|
||
const ResultScreen = () => {
|
||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||
const { state, dispatch } = useContext(TypingContext)!
|
||
|
||
const currentDictInfo = useAtomValue(currentDictInfoAtom)
|
||
const [currentChapter, setCurrentChapter] = useAtom(currentChapterAtom)
|
||
const setInfoPanelState = useSetAtom(infoPanelStateAtom)
|
||
const randomConfig = useAtomValue(randomConfigAtom)
|
||
|
||
const wrongWords = useMemo(() => {
|
||
const wordList = state.chapterData.wrongWordIndexes.map((index) => state.chapterData.words.find((word) => word.index === index))
|
||
|
||
return wordList.filter((word) => word !== undefined) as WordWithIndex[]
|
||
}, [state.chapterData.wrongWordIndexes, state.chapterData.words])
|
||
|
||
const isLastChapter = useMemo(() => {
|
||
return currentChapter >= currentDictInfo.chapterCount - 1
|
||
}, [currentChapter, currentDictInfo])
|
||
|
||
const correctRate = useMemo(() => {
|
||
const chapterLength = state.chapterData.words.length
|
||
const correctCount = chapterLength - state.chapterData.wrongWordIndexes.length
|
||
return Math.floor((correctCount / chapterLength) * 100)
|
||
}, [state.chapterData.words.length, state.chapterData.wrongWordIndexes])
|
||
|
||
const mistakeLevel = useMemo(() => {
|
||
if (correctRate >= 85) {
|
||
return 0
|
||
} else if (correctRate >= 70) {
|
||
return 1
|
||
} else {
|
||
return 2
|
||
}
|
||
}, [correctRate])
|
||
|
||
const timeString = useMemo(() => {
|
||
const seconds = state.timerData.time
|
||
const minutes = Math.floor(seconds / 60)
|
||
const minuteString = minutes < 10 ? '0' + minutes : minutes + ''
|
||
const restSeconds = seconds % 60
|
||
const secondString = restSeconds < 10 ? '0' + restSeconds : restSeconds + ''
|
||
return `${minuteString}:${secondString}`
|
||
}, [state.timerData.time])
|
||
|
||
const repeatButtonHandler = useCallback(() => {
|
||
dispatch({ type: TypingStateActionType.REPEAT_CHAPTER, shouldShuffle: randomConfig.isOpen })
|
||
}, [dispatch, randomConfig.isOpen])
|
||
|
||
const dictationButtonHandler = useCallback(() => {
|
||
dispatch({ type: TypingStateActionType.DICTATION_CHAPTER, shouldShuffle: randomConfig.isOpen })
|
||
}, [dispatch, randomConfig.isOpen])
|
||
|
||
const nextButtonHandler = useCallback(() => {
|
||
if (!isLastChapter) {
|
||
setCurrentChapter((old) => old + 1)
|
||
dispatch({ type: TypingStateActionType.NEXT_CHAPTER })
|
||
}
|
||
}, [dispatch, isLastChapter, setCurrentChapter])
|
||
|
||
const exitButtonHandler = useCallback(() => {
|
||
dispatch({ type: TypingStateActionType.REPEAT_CHAPTER, shouldShuffle: false })
|
||
}, [dispatch])
|
||
useHotkeys(
|
||
'enter',
|
||
() => {
|
||
nextButtonHandler()
|
||
},
|
||
{ preventDefault: true },
|
||
)
|
||
|
||
useHotkeys(
|
||
'space',
|
||
() => {
|
||
repeatButtonHandler()
|
||
},
|
||
{ preventDefault: true },
|
||
)
|
||
|
||
useHotkeys(
|
||
'shift+enter',
|
||
() => {
|
||
dictationButtonHandler()
|
||
},
|
||
{ preventDefault: true },
|
||
)
|
||
|
||
const handleOpenInfoPanel = useCallback(
|
||
(modalType: InfoPanelType) => {
|
||
recordOpenInfoPanelAction(modalType, 'resultScreen')
|
||
setInfoPanelState((state) => {
|
||
return {
|
||
...state,
|
||
[modalType]: true,
|
||
}
|
||
})
|
||
},
|
||
[setInfoPanelState],
|
||
)
|
||
|
||
return (
|
||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||
<div className="absolute inset-0 bg-gray-300 opacity-80 dark:bg-gray-600"></div>
|
||
<Transition
|
||
show={true}
|
||
enter="ease-in duration-300"
|
||
enterFrom="opacity-0"
|
||
enterTo="opacity-100"
|
||
leave="ease-out duration-100"
|
||
leaveFrom="opacity-100"
|
||
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="text-center font-sans text-xl font-normal text-gray-900 dark:text-gray-400 md:text-2xl">
|
||
{`${currentDictInfo.name} 第 ${currentChapter + 1} 章`}
|
||
</div>
|
||
<button className="absolute right-7 top-5" onClick={exitButtonHandler}>
|
||
<IconX className="text-gray-400" />
|
||
</button>
|
||
<div className="mt-10 flex flex-row gap-2 overflow-hidden">
|
||
<div className="flex flex-shrink-0 flex-grow-0 flex-col gap-3 px-4 sm:px-1 md:px-2 lg:px-4">
|
||
<RemarkRing remark={`${state.timerData.accuracy}%`} caption="正确率" percentage={state.timerData.accuracy} />
|
||
<RemarkRing remark={timeString} caption="章节耗时" />
|
||
<RemarkRing remark={state.timerData.wpm + ''} caption="WPM" />
|
||
</div>
|
||
<div className="z-10 ml-6 flex-1 overflow-visible rounded-xl bg-indigo-50 dark:bg-gray-700">
|
||
<div className="customized-scrollbar z-20 ml-8 mr-1 flex h-80 flex-row flex-wrap content-start gap-4 overflow-y-auto overflow-x-hidden pr-7 pt-9">
|
||
{wrongWords.map((word, index) => (
|
||
<WordChip key={`${index}-${word.name}`} word={word} />
|
||
))}
|
||
</div>
|
||
<div className="align-center flex w-full flex-row justify-start rounded-b-xl bg-indigo-200 px-4 dark:bg-indigo-400">
|
||
<ConclusionBar mistakeLevel={mistakeLevel} mistakeCount={state.chapterData.wrongWordIndexes.length} />
|
||
</div>
|
||
</div>
|
||
<div className="ml-2 flex flex-col items-center justify-end gap-3.5 text-xl">
|
||
<ShareButton />
|
||
|
||
<img
|
||
src={redBookLogo}
|
||
onClick={(e) => {
|
||
handleOpenInfoPanel('redBook')
|
||
e.currentTarget.blur()
|
||
}}
|
||
className="h-5 cursor-pointer fill-current text-gray-50"
|
||
alt="red book"
|
||
style={{ fill: '#6B7280' }}
|
||
/>
|
||
|
||
<button
|
||
onClick={(e) => {
|
||
handleOpenInfoPanel('donate')
|
||
e.currentTarget.blur()
|
||
}}
|
||
className="cursor-pointer text-gray-500 dark:text-gray-400"
|
||
type="button"
|
||
title="捐赠我们的项目"
|
||
>
|
||
<svg
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
viewBox="0 0 640 512"
|
||
className={`h-5.5 w-5.5 fill-current text-gray-500 focus:outline-none dark:text-gray-400 ${styles.imgShake}`}
|
||
>
|
||
<path d="M96 64c0-17.7 14.3-32 32-32H448h64c70.7 0 128 57.3 128 128s-57.3 128-128 128H480c0 53-43 96-96 96H192c-53 0-96-43-96-96V64zM480 224h32c35.3 0 64-28.7 64-64s-28.7-64-64-64H480V224zM32 416H544c17.7 0 32 14.3 32 32s-14.3 32-32 32H32c-17.7 0-32-14.3-32-32s14.3-32 32-32z" />
|
||
</svg>
|
||
</button>
|
||
|
||
<button
|
||
onClick={(e) => {
|
||
handleOpenInfoPanel('community')
|
||
e.currentTarget.blur()
|
||
}}
|
||
className="cursor-pointer text-gray-500 dark:text-gray-400"
|
||
type="button"
|
||
title="加入我们的社区"
|
||
>
|
||
<svg
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
viewBox="0 0 576 512"
|
||
className="h-5.5 w-5.5 fill-current text-gray-500 focus:outline-none dark:text-gray-400"
|
||
>
|
||
<path d="M385.2 167.6c6.4 0 12.6.3 18.8 1.1C387.4 90.3 303.3 32 207.7 32 100.5 32 13 104.8 13 197.4c0 53.4 29.3 97.5 77.9 131.6l-19.3 58.6 68-34.1c24.4 4.8 43.8 9.7 68.2 9.7 6.2 0 12.1-.3 18.3-.8-4-12.9-6.2-26.6-6.2-40.8-.1-84.9 72.9-154 165.3-154zm-104.5-52.9c14.5 0 24.2 9.7 24.2 24.4 0 14.5-9.7 24.2-24.2 24.2-14.8 0-29.3-9.7-29.3-24.2.1-14.7 14.6-24.4 29.3-24.4zm-136.4 48.6c-14.5 0-29.3-9.7-29.3-24.2 0-14.8 14.8-24.4 29.3-24.4 14.8 0 24.4 9.7 24.4 24.4 0 14.6-9.6 24.2-24.4 24.2zM563 319.4c0-77.9-77.9-141.3-165.4-141.3-92.7 0-165.4 63.4-165.4 141.3S305 460.7 397.6 460.7c19.3 0 38.9-5.1 58.6-9.9l53.4 29.3-14.8-48.6C534 402.1 563 363.2 563 319.4zm-219.1-24.5c-9.7 0-19.3-9.7-19.3-19.6 0-9.7 9.7-19.3 19.3-19.3 14.8 0 24.4 9.7 24.4 19.3 0 10-9.7 19.6-24.4 19.6zm107.1 0c-9.7 0-19.3-9.7-19.3-19.6 0-9.7 9.7-19.3 19.3-19.3 14.5 0 24.4 9.7 24.4 19.3.1 10-9.9 19.6-24.4 19.6z" />
|
||
</svg>
|
||
</button>
|
||
|
||
<a href="https://github.com/Kaiyiwing/qwerty-learner" target="_blank" rel="noreferrer" className="leading-[0px]">
|
||
<svg
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
viewBox="0 0 496 512"
|
||
className="h-5.5 w-5.5 fill-current text-gray-500 focus:outline-none dark:text-gray-400"
|
||
>
|
||
<path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z" />
|
||
</svg>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
<div className="mt-10 flex w-full justify-center gap-5 px-5 text-xl">
|
||
<Tooltip content="快捷键:shift + enter">
|
||
<button
|
||
className="btn-primary h-12 border-2 border-solid border-gray-300 bg-white text-base text-gray-700 dark:border-gray-700 dark:bg-gray-600 dark:text-white dark:hover:bg-gray-700"
|
||
type="button"
|
||
onClick={dictationButtonHandler}
|
||
title="默写本章节"
|
||
>
|
||
默写本章节
|
||
</button>
|
||
</Tooltip>
|
||
<Tooltip content="快捷键:space">
|
||
<button
|
||
className="btn-primary h-12 border-2 border-solid border-gray-300 bg-white text-base text-gray-700 dark:border-gray-700 dark:bg-gray-600 dark:text-white dark:hover:bg-gray-700"
|
||
type="button"
|
||
onClick={repeatButtonHandler}
|
||
title="重复本章节"
|
||
>
|
||
重复本章节
|
||
</button>
|
||
</Tooltip>
|
||
{!isLastChapter && (
|
||
<Tooltip content="快捷键:enter">
|
||
<button
|
||
className={`btn-primary { isLastChapter ? 'cursor-not-allowed opacity-50' : ''} h-12 text-base font-bold `}
|
||
type="button"
|
||
onClick={nextButtonHandler}
|
||
title="下一章节"
|
||
>
|
||
下一章节
|
||
</button>
|
||
</Tooltip>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Transition>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default ResultScreen
|