fix(练习): 修复知识领域显示和输入焦点问题
- 隐藏未答对的知识领域名称,只在答对后显示 - 增加底部输入区域透明度(80% -> 60%) - 修复切换格子后输入框未自动聚焦的问题 - 优化连续输入处理,支持多字符自动分配到后续输入框 via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run>
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user