fix(练习): 修复底部区域布局和焦点问题

- 动态计算底部固定区域高度,避免固定值导致的空白或遮挡
- 底部区域适配侧边栏宽度,不再被左侧菜单遮挡
- 答案隐藏后自动恢复输入框焦点
- 增加辅助信息显示高度(max-h-48)

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

Co-Authored-By: HAPI <noreply@hapi.run>
This commit is contained in:
ittoview
2026-03-01 16:12:50 +00:00
parent 4b347be9f5
commit 08bd8dd4dc

View File

@@ -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>