fix(练习): 修复知识领域显示和输入焦点问题

- 隐藏未答对的知识领域名称,只在答对后显示
- 增加底部输入区域透明度(80% -> 60%)
- 修复切换格子后输入框未自动聚焦的问题
- 优化连续输入处理,支持多字符自动分配到后续输入框

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

Co-Authored-By: HAPI <noreply@hapi.run>
This commit is contained in:
ittoview
2026-03-01 14:57:25 +00:00
parent 7edaebf0ab
commit 977187b2d5
3 changed files with 119 additions and 30 deletions

View File

@@ -41,13 +41,27 @@ export function InputArea({
if (inputLocked) return
const newInput = [...userInput]
// 只取第一个字符
newInput[index] = value.slice(0, 1)
onInputChange(newInput)
// 自动跳转到下一个输入框
if (value && index < userInput.length - 1) {
inputRefs.current[index + 1]?.focus()
// 处理多字符输入(连续输入或粘贴)
if (value.length > 1) {
const chars = value.split('')
for (let i = 0; i < chars.length && index + i < userInput.length; i++) {
newInput[index + i] = chars[i]
}
onInputChange(newInput)
// 聚焦到最后填充的位置的下一个
const nextIndex = Math.min(index + chars.length, userInput.length - 1)
inputRefs.current[nextIndex]?.focus()
} else {
// 单字符输入
newInput[index] = value
onInputChange(newInput)
// 自动跳转到下一个输入框
if (value && index < userInput.length - 1) {
inputRefs.current[index + 1]?.focus()
}
}
}
@@ -80,7 +94,7 @@ export function InputArea({
}
return (
<div className="flex flex-col items-center gap-4">
<div className="flex flex-col items-center gap-4 practice-input-area">
<div className="flex gap-3">
{userInput.map((char, index) => {
const status = charStatuses[index] || 'pending'

View File

@@ -1,6 +1,8 @@
import { knowledgeAreas, processGroups } from '@/data'
import { getProcessesByKaAndPg } from '@/utils/practice'
import { ProcessCell } from './ProcessCell'
import { useLongPress } from '@/hooks/useLongPress'
import clsx from 'clsx'
interface PracticeMatrixProps {
answeredCells: Map<string, boolean>
@@ -12,6 +14,77 @@ interface PracticeMatrixProps {
getCellTabIndex: (cellId: string) => number
}
interface KnowledgeAreaCellProps {
ka: any
isAnswered: boolean
isFocused: boolean
showAnswer?: string | null
onLongPress: (cellId: string) => void
onLongPressEnd: () => void
onClick: (cellId: string) => void
tabIndex: number
}
function KnowledgeAreaCell({
ka,
isAnswered,
isFocused,
showAnswer,
onLongPress,
onLongPressEnd,
onClick,
tabIndex,
}: KnowledgeAreaCellProps) {
const cellId = `ka-${ka.id}`
const longPressHandlers = useLongPress(cellId, {
onLongPress,
onLongPressEnd,
})
return (
<td
data-cell-id={cellId}
className={clsx(
'sticky left-0 z-10 p-2 border border-gray-200 dark:border-gray-700 font-medium cursor-pointer transition-all duration-200',
isFocused && 'ring-2 ring-blue-500 ring-inset',
!isAnswered && 'border-dashed'
)}
style={{
backgroundColor: isAnswered ? `${ka.color}15` : 'transparent',
borderLeftWidth: 4,
borderLeftColor: isFocused ? '#3b82f6' : ka.color,
}}
onClick={() => onClick(cellId)}
tabIndex={tabIndex}
role="button"
aria-label={`知识领域:${ka.name}`}
{...longPressHandlers}
>
{isAnswered ? (
<div className="flex items-center gap-2">
<span className="font-bold text-xs" style={{ color: ka.color }}>
{ka.order}
</span>
<span className="text-xs text-gray-900 dark:text-white">
{ka.name}
</span>
</div>
) : (
<div className="min-h-[24px]" />
)}
{/* 长按显示答案 */}
{showAnswer && (
<div className="absolute inset-0 flex items-center justify-center bg-black/80 rounded z-20">
<span className="text-white text-sm font-medium px-2 text-center">
{showAnswer}
</span>
</div>
)}
</td>
)
}
export function PracticeMatrix({
answeredCells,
currentCellId,
@@ -48,26 +121,24 @@ export function PracticeMatrix({
{/* 表体:知识领域 × 过程 */}
<tbody>
{sortedKAs.map((ka) => {
const kaCellId = `ka-${ka.id}`
return (
<tr key={ka.id}>
{/* 知识领域名称 */}
<td
className="sticky left-0 z-10 p-2 border border-gray-200 dark:border-gray-700 font-medium"
style={{
backgroundColor: `${ka.color}15`,
borderLeftWidth: 4,
borderLeftColor: ka.color,
}}
>
<div className="flex items-center gap-2">
<span className="font-bold text-xs" style={{ color: ka.color }}>
{ka.order}
</span>
<span className="text-xs text-gray-900 dark:text-white">
{ka.name}
</span>
</div>
</td>
<KnowledgeAreaCell
ka={ka}
isAnswered={answeredCells.get(kaCellId) || false}
isFocused={currentCellId === kaCellId}
showAnswer={
showAnswerForCell?.cellId === kaCellId
? showAnswerForCell.answer
: null
}
onLongPress={onLongPress}
onLongPressEnd={onLongPressEnd}
onClick={onCellClick}
tabIndex={getCellTabIndex(kaCellId)}
/>
{/* 每个过程组的单元格 */}
{sortedPGs.map((pg) => {

View File

@@ -57,17 +57,21 @@ export default function ProcessPracticePage() {
setCharStatuses(new Array(cell.answer.length).fill('pending'))
setLastErrorTimestamp(null)
// 滚动到可见区域并聚焦
// 滚动到可见区域
requestAnimationFrame(() => {
const element = document.querySelector(
`[data-cell-id="${cell.id}"]`
) as HTMLElement
element?.scrollIntoView({ behavior: 'smooth', block: 'center' })
// 聚焦格子,使键盘长按生效
setTimeout(() => {
element?.focus()
}, 100)
})
// 延迟聚焦到第一个输入框,确保 DOM 已更新
setTimeout(() => {
const firstInput = document.querySelector(
'.practice-input-area input'
) as HTMLInputElement
firstInput?.focus()
}, 150)
},
[]
)
@@ -314,7 +318,7 @@ export default function ProcessPracticePage() {
</div>
{/* 底部固定区域 */}
<div className="fixed bottom-0 left-0 right-0 bg-white/80 dark:bg-gray-800/80 backdrop-blur-md border-t border-gray-200 dark:border-gray-700 z-10">
<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 className="max-w-7xl mx-auto">
{/* 输入区域 */}
<div className="py-4 border-b border-gray-200/50 dark:border-gray-700/50">