fixed some bugs

This commit is contained in:
2026-05-25 11:07:32 +08:00
parent 6cac2f0c9a
commit 4088fbc9e7
2 changed files with 204 additions and 111 deletions

View File

@@ -78,7 +78,11 @@ interface InteractionEventPayload {
ts: number;
key?: string;
inputType?: string;
value?: string;
valueLength?: number;
ctrlKey?: boolean;
metaKey?: boolean;
altKey?: boolean;
}
interface NativeInputLikeEvent extends Event {
@@ -110,7 +114,6 @@ interface SavedVocabularyTestRecordResponse {
duration: number;
};
results: TestResult[];
riskFlags?: string[];
invalidated?: boolean;
}
@@ -129,7 +132,6 @@ interface SubmitAnswerResponse {
total: number;
finished: boolean;
};
riskFlags?: string[];
}
interface OptionTokenResponse {
@@ -138,6 +140,9 @@ interface OptionTokenResponse {
}
const ANSWER_PROOF_SALT = 'd1ktsalt';
const INVALID_CREDENTIAL_MESSAGE = '凭证无效,请检查。';
const MIN_STUDY_WORD_COUNT = 10;
const MAX_STUDY_WORD_COUNT = 100;
const VocabularyStudy: React.FC = () => {
const navigate = useNavigate();
@@ -165,7 +170,6 @@ const VocabularyStudy: React.FC = () => {
const [pendingAnswers, setPendingAnswers] = useState<PendingAnswer[]>([]);
const [testAttemptLoading, setTestAttemptLoading] = useState(false);
const [options, setOptions] = useState<TestOption[]>([]);
const [testRiskFlags, setTestRiskFlags] = useState<string[]>([]);
const [isModalVisible, setIsModalVisible] = useState(false);
const timeOffsetRef = useRef<number>(0);
const [timer, setTimer] = useState<NodeJS.Timeout | null>(null);
@@ -332,7 +336,7 @@ const VocabularyStudy: React.FC = () => {
// 处理单词数量变更
const handleWordCountChange = (value: number | null) => {
if (value !== null) {
setWordCount(value);
setWordCount(Math.max(MIN_STUDY_WORD_COUNT, Math.min(value, MAX_STUDY_WORD_COUNT)));
}
};
@@ -495,7 +499,6 @@ const VocabularyStudy: React.FC = () => {
setTestResults([]);
setPendingAnswers([]);
setOptions(normalizeOptions(questions[0]));
setTestRiskFlags([]);
resetInteractionTrace();
setIsModalVisible(false);
setStudyStats({
@@ -506,11 +509,7 @@ const VocabularyStudy: React.FC = () => {
duration: 0
});
} catch (error) {
if (error instanceof ApiError) {
message.error(`创建测试会话失败: ${error.message}`);
} else {
message.error('创建测试会话失败');
}
message.error(INVALID_CREDENTIAL_MESSAGE);
} finally {
setTestAttemptLoading(false);
}
@@ -558,6 +557,40 @@ const VocabularyStudy: React.FC = () => {
].slice(-300);
};
const getTestInputElement = (): HTMLInputElement | null => {
return inputRef.current?.input || null;
};
const moveTestInputCaretToEnd = () => {
window.setTimeout(() => {
const input = getTestInputElement();
if (!input) return;
const end = input.value.length;
input.setSelectionRange(end, end);
}, 0);
};
const focusTestInputAtEnd = () => {
inputRef.current?.focus?.();
moveTestInputCaretToEnd();
};
const shouldBlockInputKey = (event: React.KeyboardEvent<HTMLInputElement>): boolean => {
const input = event.currentTarget;
const selectionStart = input.selectionStart ?? input.value.length;
const selectionEnd = input.selectionEnd ?? input.value.length;
const hasSelection = selectionStart !== selectionEnd;
const caretAtEnd = selectionStart === input.value.length && selectionEnd === input.value.length;
const navigationKeys = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'];
const shortcutKey = event.key.toLowerCase();
if (navigationKeys.includes(event.key) || event.key === 'Delete') return true;
if ((event.ctrlKey || event.metaKey) && ['a', 'x', 'v', 'z', 'y'].includes(shortcutKey)) return true;
if (event.key === 'Backspace') return hasSelection || !caretAtEnd;
return false;
};
const handleMultipleChoiceChange = async (option: TestOption) => {
const currentQuestion = testWords[testIndex];
if (!currentQuestion || !testAttemptId || showAnswer) return;
@@ -586,11 +619,7 @@ const VocabularyStudy: React.FC = () => {
));
setUserAnswer(response.optionToken);
} catch (error) {
if (error instanceof ApiError) {
message.error(`获取选项凭证失败: ${error.message}`);
} else {
message.error('获取选项凭证失败');
}
message.error(INVALID_CREDENTIAL_MESSAGE);
}
};
@@ -653,7 +682,6 @@ const VocabularyStudy: React.FC = () => {
setPendingAnswers(updatedPendingAnswers);
setTestResults(prev => [...prev, response.result]);
setTestRiskFlags(prev => Array.from(new Set([...prev, ...(response.riskFlags || [])])));
setShowAnswer(true);
setUserAnswer('');
@@ -679,11 +707,7 @@ const VocabularyStudy: React.FC = () => {
}, 1500);
} catch (error) {
answerSubmittingRef.current = false;
if (error instanceof ApiError) {
message.error(`提交答案失败: ${error.message}`);
} else {
message.error('提交答案失败');
}
message.error(INVALID_CREDENTIAL_MESSAGE);
}
};
@@ -712,24 +736,19 @@ const VocabularyStudy: React.FC = () => {
duration: savedRecord.stats.duration
});
setTestResults(savedRecord.results);
setTestRiskFlags(savedRecord.riskFlags || []);
setPendingAnswers([]);
setTestAttemptId('');
setShowAnswer(false);
setTestFinished(true);
if (savedRecord.invalidated) {
message.warning('测试行为异常,本次不计入掌握');
message.error(INVALID_CREDENTIAL_MESSAGE);
} else {
message.success('测试完成,记录已保存');
}
setIsModalVisible(true);
} catch (error) {
if (error instanceof ApiError) {
message.error(`保存测试记录失败: ${error.message}`);
} else {
message.error('保存测试记录失败');
}
message.error(INVALID_CREDENTIAL_MESSAGE);
}
};
@@ -751,7 +770,6 @@ const VocabularyStudy: React.FC = () => {
setTestResults([]);
setPendingAnswers([]);
setOptions([]);
setTestRiskFlags([]);
interactionsRef.current = [];
questionShownAtRef.current = 0;
answerSubmittingRef.current = false;
@@ -973,14 +991,14 @@ const VocabularyStudy: React.FC = () => {
<h4>:</h4>
<InputNumber
min={5}
max={100}
min={MIN_STUDY_WORD_COUNT}
max={MAX_STUDY_WORD_COUNT}
value={wordCount}
onChange={handleWordCountChange}
style={{ width: 120 }}
/>
<span style={{ marginLeft: 10, color: '#888' }}>
(范围: 5-100)
(: {MIN_STUDY_WORD_COUNT}-{MAX_STUDY_WORD_COUNT})
</span>
</div>
@@ -1267,19 +1285,6 @@ const VocabularyStudy: React.FC = () => {
/>
<div style={{ margin: '20px 0' }}>
{testRiskFlags.length > 0 && (
<div style={{
marginBottom: 12,
padding: '8px 10px',
border: '1px solid #faad14',
borderRadius: 4,
color: '#ad6800',
background: '#fffbe6'
}}>
</div>
)}
{testType === 'chinese-to-english' && (
<div style={{ fontSize: 24, marginBottom: 15, textAlign: 'center' }}>
<div style={{ color: '#666', fontSize: '0.8em', marginBottom: 5 }}></div>
@@ -1366,21 +1371,44 @@ const VocabularyStudy: React.FC = () => {
recordInteraction({
type: 'input',
inputType: (e.nativeEvent as NativeInputLikeEvent)?.inputType,
value: e.target.value,
valueLength: e.target.value.length
});
setUserAnswer(e.target.value);
moveTestInputCaretToEnd();
}}
onKeyDown={e => {
recordInteraction({
type: 'keydown',
key: e.key,
valueLength: userAnswer.length
value: userAnswer,
valueLength: userAnswer.length,
ctrlKey: e.ctrlKey,
metaKey: e.metaKey,
altKey: e.altKey
});
if (shouldBlockInputKey(e)) {
e.preventDefault();
moveTestInputCaretToEnd();
}
}}
onFocus={() => {
recordInteraction({ type: 'focus', valueLength: userAnswer.length });
moveTestInputCaretToEnd();
}}
onFocus={() => recordInteraction({ type: 'focus', valueLength: userAnswer.length })}
onBlur={() => recordInteraction({ type: 'blur', valueLength: userAnswer.length })}
onMouseDown={e => {
e.preventDefault();
focusTestInputAtEnd();
}}
onMouseUp={moveTestInputCaretToEnd}
onSelect={moveTestInputCaretToEnd}
onKeyUp={moveTestInputCaretToEnd}
onContextMenu={e => e.preventDefault()}
onCut={e => e.preventDefault()}
onDrop={e => e.preventDefault()}
disabled={showAnswer}
style={{ marginBottom: 15 }}
style={{ marginBottom: 15, userSelect: 'none', WebkitUserSelect: 'none' }}
onPressEnter={e => {
e.stopPropagation();
submitAnswer();
@@ -1388,7 +1416,7 @@ const VocabularyStudy: React.FC = () => {
autoFocus
size="large"
onPaste={e => {
recordInteraction({ type: 'paste', valueLength: userAnswer.length });
recordInteraction({ type: 'paste', value: userAnswer, valueLength: userAnswer.length });
e.preventDefault();
}}
/>