fix(练习): 修复知识领域显示和输入焦点问题
- 隐藏未答对的知识领域名称,只在答对后显示 - 增加底部输入区域透明度(80% -> 60%) - 修复切换格子后输入框未自动聚焦的问题 - 优化连续输入处理,支持多字符自动分配到后续输入框 via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run>
This commit is contained in:
@@ -41,8 +41,21 @@ export function InputArea({
|
|||||||
if (inputLocked) return
|
if (inputLocked) return
|
||||||
|
|
||||||
const newInput = [...userInput]
|
const newInput = [...userInput]
|
||||||
// 只取第一个字符
|
|
||||||
newInput[index] = value.slice(0, 1)
|
// 处理多字符输入(连续输入或粘贴)
|
||||||
|
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)
|
onInputChange(newInput)
|
||||||
|
|
||||||
// 自动跳转到下一个输入框
|
// 自动跳转到下一个输入框
|
||||||
@@ -50,6 +63,7 @@ export function InputArea({
|
|||||||
inputRefs.current[index + 1]?.focus()
|
inputRefs.current[index + 1]?.focus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleKeyDown = (index: number, e: React.KeyboardEvent) => {
|
const handleKeyDown = (index: number, e: React.KeyboardEvent) => {
|
||||||
if (inputLocked) return
|
if (inputLocked) return
|
||||||
@@ -80,7 +94,7 @@ export function InputArea({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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">
|
<div className="flex gap-3">
|
||||||
{userInput.map((char, index) => {
|
{userInput.map((char, index) => {
|
||||||
const status = charStatuses[index] || 'pending'
|
const status = charStatuses[index] || 'pending'
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { knowledgeAreas, processGroups } from '@/data'
|
import { knowledgeAreas, processGroups } from '@/data'
|
||||||
import { getProcessesByKaAndPg } from '@/utils/practice'
|
import { getProcessesByKaAndPg } from '@/utils/practice'
|
||||||
import { ProcessCell } from './ProcessCell'
|
import { ProcessCell } from './ProcessCell'
|
||||||
|
import { useLongPress } from '@/hooks/useLongPress'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
interface PracticeMatrixProps {
|
interface PracticeMatrixProps {
|
||||||
answeredCells: Map<string, boolean>
|
answeredCells: Map<string, boolean>
|
||||||
@@ -12,6 +14,77 @@ interface PracticeMatrixProps {
|
|||||||
getCellTabIndex: (cellId: string) => number
|
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({
|
export function PracticeMatrix({
|
||||||
answeredCells,
|
answeredCells,
|
||||||
currentCellId,
|
currentCellId,
|
||||||
@@ -48,26 +121,24 @@ export function PracticeMatrix({
|
|||||||
{/* 表体:知识领域 × 过程 */}
|
{/* 表体:知识领域 × 过程 */}
|
||||||
<tbody>
|
<tbody>
|
||||||
{sortedKAs.map((ka) => {
|
{sortedKAs.map((ka) => {
|
||||||
|
const kaCellId = `ka-${ka.id}`
|
||||||
return (
|
return (
|
||||||
<tr key={ka.id}>
|
<tr key={ka.id}>
|
||||||
{/* 知识领域名称 */}
|
{/* 知识领域名称 */}
|
||||||
<td
|
<KnowledgeAreaCell
|
||||||
className="sticky left-0 z-10 p-2 border border-gray-200 dark:border-gray-700 font-medium"
|
ka={ka}
|
||||||
style={{
|
isAnswered={answeredCells.get(kaCellId) || false}
|
||||||
backgroundColor: `${ka.color}15`,
|
isFocused={currentCellId === kaCellId}
|
||||||
borderLeftWidth: 4,
|
showAnswer={
|
||||||
borderLeftColor: ka.color,
|
showAnswerForCell?.cellId === kaCellId
|
||||||
}}
|
? showAnswerForCell.answer
|
||||||
>
|
: null
|
||||||
<div className="flex items-center gap-2">
|
}
|
||||||
<span className="font-bold text-xs" style={{ color: ka.color }}>
|
onLongPress={onLongPress}
|
||||||
{ka.order}
|
onLongPressEnd={onLongPressEnd}
|
||||||
</span>
|
onClick={onCellClick}
|
||||||
<span className="text-xs text-gray-900 dark:text-white">
|
tabIndex={getCellTabIndex(kaCellId)}
|
||||||
{ka.name}
|
/>
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
{/* 每个过程组的单元格 */}
|
{/* 每个过程组的单元格 */}
|
||||||
{sortedPGs.map((pg) => {
|
{sortedPGs.map((pg) => {
|
||||||
|
|||||||
@@ -57,17 +57,21 @@ export default function ProcessPracticePage() {
|
|||||||
setCharStatuses(new Array(cell.answer.length).fill('pending'))
|
setCharStatuses(new Array(cell.answer.length).fill('pending'))
|
||||||
setLastErrorTimestamp(null)
|
setLastErrorTimestamp(null)
|
||||||
|
|
||||||
// 滚动到可见区域并聚焦
|
// 滚动到可见区域
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const element = document.querySelector(
|
const element = document.querySelector(
|
||||||
`[data-cell-id="${cell.id}"]`
|
`[data-cell-id="${cell.id}"]`
|
||||||
) as HTMLElement
|
) as HTMLElement
|
||||||
element?.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
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>
|
||||||
|
|
||||||
{/* 底部固定区域 */}
|
{/* 底部固定区域 */}
|
||||||
<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="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">
|
||||||
|
|||||||
Reference in New Issue
Block a user