feat(练习): 添加进度缓存和用户体验优化

- 使用 localStorage 缓存答题进度,支持切换页面后继续
- 修复暗色主题下输入框文字不可见问题
- 添加"想不起来"提示按钮,引导用户查看答案
- 添加清除进度按钮,方便重新开始练习

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
This commit is contained in:
ittoview
2026-03-01 15:27:19 +00:00
parent a38e275642
commit 4b347be9f5
2 changed files with 67 additions and 4 deletions

View File

@@ -128,6 +128,7 @@ export function InputArea({
'w-10 h-12 text-center text-2xl font-medium',
'bg-transparent border-b-2 transition-all duration-200',
'focus:outline-none',
'text-gray-900 dark:text-gray-100',
isComposing && 'border-gray-300 dark:border-gray-600 opacity-70',
!isComposing && !char && 'border-gray-400 dark:border-gray-500',
!isComposing && isCorrect && 'border-green-500',

View File

@@ -16,12 +16,32 @@ export default function ProcessPracticePage() {
// 生成格子顺序
const [cellSequence] = useState<CellInfo[]>(() => generateCellSequence())
// 从 localStorage 加载答题进度
const loadProgress = useCallback(() => {
try {
const saved = localStorage.getItem('practice-progress')
if (saved) {
const data = JSON.parse(saved)
return {
answeredCells: new Map<string, boolean>(data.answeredCells || []),
currentCellId: data.currentCellId || cellSequence[0]?.id || null,
}
}
} catch (e) {
console.error('加载进度失败:', e)
}
return {
answeredCells: new Map<string, boolean>(),
currentCellId: cellSequence[0]?.id || null,
}
}, [cellSequence])
// 答题状态
const [answeredCells, setAnsweredCells] = useState<Map<string, boolean>>(
new Map()
() => loadProgress().answeredCells
)
const [currentCellId, setCurrentCellId] = useState<string | null>(
cellSequence[0]?.id || null
() => loadProgress().currentCellId
)
// 输入状态
@@ -55,6 +75,21 @@ export default function ProcessPracticePage() {
latestInputRef.current = userInput
}, [userInput])
// 保存答题进度到 localStorage
useEffect(() => {
try {
localStorage.setItem(
'practice-progress',
JSON.stringify({
answeredCells: Array.from(answeredCells.entries()),
currentCellId,
})
)
} catch (e) {
console.error('保存进度失败:', e)
}
}, [answeredCells, currentCellId])
// 切换到指定格子
const switchToCell = useCallback(
(cell: CellInfo) => {
@@ -283,6 +318,15 @@ export default function ProcessPracticePage() {
const answeredCount = answeredCells.size
const totalCount = cellSequence.length
// 清除进度
const handleClearProgress = useCallback(() => {
if (confirm('确定要清除所有答题进度吗?')) {
setAnsweredCells(new Map())
setCurrentCellId(cellSequence[0]?.id || null)
localStorage.removeItem('practice-progress')
}
}, [cellSequence])
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
{/* 顶部进度条 */}
@@ -292,9 +336,17 @@ export default function ProcessPracticePage() {
<h1 className="text-xl font-bold text-gray-900 dark:text-gray-100">
</h1>
<div className="flex items-center gap-3">
<div className="text-sm text-gray-600 dark:text-gray-400">
{answeredCount} / {totalCount}
</div>
<button
onClick={handleClearProgress}
className="text-xs px-2 py-1 text-gray-500 dark:text-gray-400 hover:text-red-600 dark:hover:text-red-400 transition-colors"
>
</button>
</div>
</div>
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<motion.div
@@ -341,6 +393,16 @@ export default function ProcessPracticePage() {
onCompositionEnd={handleCompositionEnd}
onPaste={handlePaste}
/>
{/* 提示按钮 */}
<div className="flex justify-center mt-3">
<button
onClick={() => currentCellId && handleLongPress(currentCellId)}
className="px-4 py-1.5 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 transition-colors"
>
</button>
</div>
</div>
{/* 辅助信息区域 */}