fixed some bugs
This commit is contained in:
@@ -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();
|
||||
}}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user