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 { PracticeMatrix } from '@/components/practice/PracticeMatrix'
|
||||||
import { InputArea } from '@/components/practice/InputArea'
|
import { InputArea } from '@/components/practice/InputArea'
|
||||||
import { HintInfo } from '@/components/practice/HintInfo'
|
import { HintInfo } from '@/components/practice/HintInfo'
|
||||||
|
import { useAppStore } from '@/stores/useAppStore'
|
||||||
|
import { clsx } from 'clsx'
|
||||||
|
|
||||||
type CharStatus = 'pending' | 'correct' | 'error'
|
type CharStatus = 'pending' | 'correct' | 'error'
|
||||||
|
|
||||||
export default function ProcessPracticePage() {
|
export default function ProcessPracticePage() {
|
||||||
|
const sidebarOpen = useAppStore((s) => s.sidebarOpen)
|
||||||
|
|
||||||
// 生成格子顺序
|
// 生成格子顺序
|
||||||
const [cellSequence] = useState<CellInfo[]>(() => generateCellSequence())
|
const [cellSequence] = useState<CellInfo[]>(() => generateCellSequence())
|
||||||
|
|
||||||
@@ -60,6 +64,8 @@ export default function ProcessPracticePage() {
|
|||||||
} | null>(null)
|
} | null>(null)
|
||||||
const [inputLocked, setInputLocked] = useState(false)
|
const [inputLocked, setInputLocked] = useState(false)
|
||||||
const latestInputRef = useRef<string[]>([])
|
const latestInputRef = useRef<string[]>([])
|
||||||
|
const bottomAreaRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [bottomHeight, setBottomHeight] = useState(0)
|
||||||
|
|
||||||
// 初始化输入框
|
// 初始化输入框
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -90,6 +96,19 @@ export default function ProcessPracticePage() {
|
|||||||
}
|
}
|
||||||
}, [answeredCells, currentCellId])
|
}, [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(
|
const switchToCell = useCallback(
|
||||||
(cell: CellInfo) => {
|
(cell: CellInfo) => {
|
||||||
@@ -251,6 +270,14 @@ export default function ProcessPracticePage() {
|
|||||||
setShowAnswerForCell(null)
|
setShowAnswerForCell(null)
|
||||||
setInputLocked(false)
|
setInputLocked(false)
|
||||||
announceToScreenReader('答案已隐藏')
|
announceToScreenReader('答案已隐藏')
|
||||||
|
|
||||||
|
// 恢复焦点到第一个输入框
|
||||||
|
setTimeout(() => {
|
||||||
|
const firstInput = document.querySelector(
|
||||||
|
'.practice-input-area input'
|
||||||
|
) as HTMLInputElement
|
||||||
|
firstInput?.focus()
|
||||||
|
}, 100)
|
||||||
}
|
}
|
||||||
}, [showAnswerForCell])
|
}, [showAnswerForCell])
|
||||||
|
|
||||||
@@ -262,6 +289,14 @@ export default function ProcessPracticePage() {
|
|||||||
if (remainingTime <= 0) {
|
if (remainingTime <= 0) {
|
||||||
setShowAnswerForCell(null)
|
setShowAnswerForCell(null)
|
||||||
setInputLocked(false)
|
setInputLocked(false)
|
||||||
|
|
||||||
|
// 恢复焦点
|
||||||
|
setTimeout(() => {
|
||||||
|
const firstInput = document.querySelector(
|
||||||
|
'.practice-input-area input'
|
||||||
|
) as HTMLInputElement
|
||||||
|
firstInput?.focus()
|
||||||
|
}, 100)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,6 +304,14 @@ export default function ProcessPracticePage() {
|
|||||||
setShowAnswerForCell(null)
|
setShowAnswerForCell(null)
|
||||||
setInputLocked(false)
|
setInputLocked(false)
|
||||||
announceToScreenReader('答案已自动隐藏')
|
announceToScreenReader('答案已自动隐藏')
|
||||||
|
|
||||||
|
// 恢复焦点
|
||||||
|
setTimeout(() => {
|
||||||
|
const firstInput = document.querySelector(
|
||||||
|
'.practice-input-area input'
|
||||||
|
) as HTMLInputElement
|
||||||
|
firstInput?.focus()
|
||||||
|
}, 100)
|
||||||
}, remainingTime)
|
}, remainingTime)
|
||||||
|
|
||||||
return () => clearTimeout(timer)
|
return () => clearTimeout(timer)
|
||||||
@@ -362,7 +405,7 @@ export default function ProcessPracticePage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 主内容区域 */}
|
{/* 主内容区域 */}
|
||||||
<div className="pb-80">
|
<div style={{ paddingBottom: `${bottomHeight}px` }}>
|
||||||
{/* 矩阵区域 */}
|
{/* 矩阵区域 */}
|
||||||
<div className="max-w-7xl mx-auto py-4">
|
<div className="max-w-7xl mx-auto py-4">
|
||||||
<PracticeMatrix
|
<PracticeMatrix
|
||||||
@@ -378,35 +421,44 @@ export default function ProcessPracticePage() {
|
|||||||
</div>
|
</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="max-w-7xl mx-auto">
|
||||||
{/* 输入区域 */}
|
{/* 输入区域 */}
|
||||||
<div className="py-4 border-b border-gray-200/50 dark:border-gray-700/50">
|
<div className="py-4 border-b border-gray-200/50 dark:border-gray-700/50">
|
||||||
<InputArea
|
<div className="flex items-center justify-center gap-3">
|
||||||
userInput={userInput}
|
<InputArea
|
||||||
charStatuses={charStatuses}
|
userInput={userInput}
|
||||||
isComposing={isComposing}
|
charStatuses={charStatuses}
|
||||||
inputLocked={inputLocked}
|
isComposing={isComposing}
|
||||||
lastErrorTimestamp={lastErrorTimestamp}
|
inputLocked={inputLocked}
|
||||||
onInputChange={handleInputChange}
|
lastErrorTimestamp={lastErrorTimestamp}
|
||||||
onCompositionStart={handleCompositionStart}
|
onInputChange={handleInputChange}
|
||||||
onCompositionEnd={handleCompositionEnd}
|
onCompositionStart={handleCompositionStart}
|
||||||
onPaste={handlePaste}
|
onCompositionEnd={handleCompositionEnd}
|
||||||
/>
|
onPaste={handlePaste}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* 提示按钮 */}
|
{/* 查看答案按钮 */}
|
||||||
<div className="flex justify-center mt-3">
|
|
||||||
<button
|
<button
|
||||||
onClick={() => currentCellId && handleLongPress(currentCellId)}
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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} />
|
<HintInfo currentCell={currentCell} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user