fix(练习): 修复底部区域布局和焦点问题
- 动态计算底部固定区域高度,避免固定值导致的空白或遮挡 - 底部区域适配侧边栏宽度,不再被左侧菜单遮挡 - 答案隐藏后自动恢复输入框焦点 - 增加辅助信息显示高度(max-h-48) via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run>
This commit is contained in:
@@ -9,10 +9,14 @@ import {
|
||||
import { PracticeMatrix } from '@/components/practice/PracticeMatrix'
|
||||
import { InputArea } from '@/components/practice/InputArea'
|
||||
import { HintInfo } from '@/components/practice/HintInfo'
|
||||
import { useAppStore } from '@/stores/useAppStore'
|
||||
import { clsx } from 'clsx'
|
||||
|
||||
type CharStatus = 'pending' | 'correct' | 'error'
|
||||
|
||||
export default function ProcessPracticePage() {
|
||||
const sidebarOpen = useAppStore((s) => s.sidebarOpen)
|
||||
|
||||
// 生成格子顺序
|
||||
const [cellSequence] = useState<CellInfo[]>(() => generateCellSequence())
|
||||
|
||||
@@ -60,6 +64,8 @@ export default function ProcessPracticePage() {
|
||||
} | null>(null)
|
||||
const [inputLocked, setInputLocked] = useState(false)
|
||||
const latestInputRef = useRef<string[]>([])
|
||||
const bottomAreaRef = useRef<HTMLDivElement>(null)
|
||||
const [bottomHeight, setBottomHeight] = useState(0)
|
||||
|
||||
// 初始化输入框
|
||||
useEffect(() => {
|
||||
@@ -90,6 +96,19 @@ export default function ProcessPracticePage() {
|
||||
}
|
||||
}, [answeredCells, currentCellId])
|
||||
|
||||
// 动态计算底部固定区域高度
|
||||
useEffect(() => {
|
||||
const updateBottomHeight = () => {
|
||||
if (bottomAreaRef.current) {
|
||||
setBottomHeight(bottomAreaRef.current.offsetHeight)
|
||||
}
|
||||
}
|
||||
|
||||
updateBottomHeight()
|
||||
window.addEventListener('resize', updateBottomHeight)
|
||||
return () => window.removeEventListener('resize', updateBottomHeight)
|
||||
}, [userInput.length]) // 当输入框数量变化时重新计算
|
||||
|
||||
// 切换到指定格子
|
||||
const switchToCell = useCallback(
|
||||
(cell: CellInfo) => {
|
||||
@@ -251,6 +270,14 @@ export default function ProcessPracticePage() {
|
||||
setShowAnswerForCell(null)
|
||||
setInputLocked(false)
|
||||
announceToScreenReader('答案已隐藏')
|
||||
|
||||
// 恢复焦点到第一个输入框
|
||||
setTimeout(() => {
|
||||
const firstInput = document.querySelector(
|
||||
'.practice-input-area input'
|
||||
) as HTMLInputElement
|
||||
firstInput?.focus()
|
||||
}, 100)
|
||||
}
|
||||
}, [showAnswerForCell])
|
||||
|
||||
@@ -262,6 +289,14 @@ export default function ProcessPracticePage() {
|
||||
if (remainingTime <= 0) {
|
||||
setShowAnswerForCell(null)
|
||||
setInputLocked(false)
|
||||
|
||||
// 恢复焦点
|
||||
setTimeout(() => {
|
||||
const firstInput = document.querySelector(
|
||||
'.practice-input-area input'
|
||||
) as HTMLInputElement
|
||||
firstInput?.focus()
|
||||
}, 100)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -269,6 +304,14 @@ export default function ProcessPracticePage() {
|
||||
setShowAnswerForCell(null)
|
||||
setInputLocked(false)
|
||||
announceToScreenReader('答案已自动隐藏')
|
||||
|
||||
// 恢复焦点
|
||||
setTimeout(() => {
|
||||
const firstInput = document.querySelector(
|
||||
'.practice-input-area input'
|
||||
) as HTMLInputElement
|
||||
firstInput?.focus()
|
||||
}, 100)
|
||||
}, remainingTime)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
@@ -362,7 +405,7 @@ export default function ProcessPracticePage() {
|
||||
</div>
|
||||
|
||||
{/* 主内容区域 */}
|
||||
<div className="pb-80">
|
||||
<div style={{ paddingBottom: `${bottomHeight}px` }}>
|
||||
{/* 矩阵区域 */}
|
||||
<div className="max-w-7xl mx-auto py-4">
|
||||
<PracticeMatrix
|
||||
@@ -378,35 +421,44 @@ export default function ProcessPracticePage() {
|
||||
</div>
|
||||
|
||||
{/* 底部固定区域 */}
|
||||
<div className="fixed bottom-0 left-0 right-0 bg-white/60 dark:bg-gray-800/60 backdrop-blur-md border-t border-gray-200 dark:border-gray-700 z-10">
|
||||
<div
|
||||
ref={bottomAreaRef}
|
||||
className={clsx(
|
||||
'fixed bottom-0 left-0 right-0 bg-white/60 dark:bg-gray-800/60 backdrop-blur-md border-t border-gray-200 dark:border-gray-700 z-10 transition-all duration-300',
|
||||
sidebarOpen ? 'lg:ml-64' : 'lg:ml-20'
|
||||
)}
|
||||
>
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* 输入区域 */}
|
||||
<div className="py-4 border-b border-gray-200/50 dark:border-gray-700/50">
|
||||
<InputArea
|
||||
userInput={userInput}
|
||||
charStatuses={charStatuses}
|
||||
isComposing={isComposing}
|
||||
inputLocked={inputLocked}
|
||||
lastErrorTimestamp={lastErrorTimestamp}
|
||||
onInputChange={handleInputChange}
|
||||
onCompositionStart={handleCompositionStart}
|
||||
onCompositionEnd={handleCompositionEnd}
|
||||
onPaste={handlePaste}
|
||||
/>
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<InputArea
|
||||
userInput={userInput}
|
||||
charStatuses={charStatuses}
|
||||
isComposing={isComposing}
|
||||
inputLocked={inputLocked}
|
||||
lastErrorTimestamp={lastErrorTimestamp}
|
||||
onInputChange={handleInputChange}
|
||||
onCompositionStart={handleCompositionStart}
|
||||
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"
|
||||
className="p-2 text-gray-500 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 transition-colors"
|
||||
title="查看答案(长按格子也可以)"
|
||||
>
|
||||
想不起来?长按格子或点击此处查看答案
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 辅助信息区域 */}
|
||||
<div className="py-3 px-4 max-h-40 overflow-y-auto">
|
||||
<div className="py-3 px-4 max-h-48 overflow-y-auto">
|
||||
<HintInfo currentCell={currentCell} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user