fixed fake post attack
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Card, Button, Input, Progress, Modal, message, Tabs, Radio, Spin, Select, Table, Tag, InputNumber, Space } from 'antd';
|
||||
import { SoundOutlined, CaretLeftOutlined, CaretRightOutlined, TrophyOutlined, HistoryOutlined, ReloadOutlined, SettingOutlined, LinkOutlined } from '@ant-design/icons';
|
||||
import CryptoJS from 'crypto-js';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { api, ApiError, authEvents } from '../api/apiClient';
|
||||
import { API_PATHS } from '../config';
|
||||
@@ -8,6 +9,7 @@ import type { TabsProps } from 'antd';
|
||||
import type { RadioChangeEvent } from 'antd/lib/radio';
|
||||
|
||||
interface Word {
|
||||
_id?: string;
|
||||
id: string;
|
||||
word: string;
|
||||
translation: string;
|
||||
@@ -56,11 +58,57 @@ interface LeaderboardItem {
|
||||
rank: number;
|
||||
}
|
||||
|
||||
interface TestResult {
|
||||
questionToken?: string;
|
||||
wordId: string;
|
||||
word: string;
|
||||
userAnswer: string;
|
||||
correctAnswer: string;
|
||||
isCorrect: boolean;
|
||||
}
|
||||
|
||||
interface PendingAnswer {
|
||||
questionToken: string;
|
||||
userAnswer: string;
|
||||
submittedAt: number;
|
||||
answerProof: string;
|
||||
}
|
||||
|
||||
interface TestQuestion {
|
||||
questionToken: string;
|
||||
wordId: string;
|
||||
word?: string;
|
||||
translation?: string;
|
||||
pronunciation?: string;
|
||||
options?: string[];
|
||||
}
|
||||
|
||||
interface SavedVocabularyTestRecordResponse {
|
||||
message: string;
|
||||
stats: {
|
||||
totalWords: number;
|
||||
correctWords: number;
|
||||
accuracy: number;
|
||||
startTime: string | Date;
|
||||
endTime: string | Date;
|
||||
duration: number;
|
||||
};
|
||||
results: TestResult[];
|
||||
}
|
||||
|
||||
interface TestAttemptResponse {
|
||||
attemptId: string;
|
||||
testType: string;
|
||||
expiresAt: string | Date;
|
||||
questions: TestQuestion[];
|
||||
}
|
||||
|
||||
const VocabularyStudy: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [wordSets, setWordSets] = useState<WordSet[]>([]);
|
||||
const [selectedWordSet, setSelectedWordSet] = useState<string>('');
|
||||
const [loadedWordSetId, setLoadedWordSetId] = useState<string>('');
|
||||
const [currentWords, setCurrentWords] = useState<Word[]>([]);
|
||||
const [studyStats, setStudyStats] = useState<StudyStats>({
|
||||
totalWords: 0,
|
||||
@@ -76,12 +124,10 @@ const VocabularyStudy: React.FC = () => {
|
||||
const [showAnswer, setShowAnswer] = useState(false);
|
||||
const [testStarted, setTestStarted] = useState(false);
|
||||
const [testFinished, setTestFinished] = useState(false);
|
||||
const [testResults, setTestResults] = useState<{
|
||||
word: string;
|
||||
userAnswer: string;
|
||||
correctAnswer: string;
|
||||
isCorrect: boolean;
|
||||
}[]>([]);
|
||||
const [testResults, setTestResults] = useState<TestResult[]>([]);
|
||||
const [testAttemptId, setTestAttemptId] = useState<string>('');
|
||||
const [pendingAnswers, setPendingAnswers] = useState<PendingAnswer[]>([]);
|
||||
const [testAttemptLoading, setTestAttemptLoading] = useState(false);
|
||||
const [options, setOptions] = useState<string[]>([]);
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const timeOffsetRef = useRef<number>(0);
|
||||
@@ -96,6 +142,7 @@ const VocabularyStudy: React.FC = () => {
|
||||
|
||||
// 使用ref存储当前选中ID,这样可以立即访问
|
||||
const currentWordSetIdRef = useRef<string>('');
|
||||
const answerSubmittingRef = useRef(false);
|
||||
|
||||
// 测试输入框ref
|
||||
const inputRef = useRef<any>(null);
|
||||
@@ -103,7 +150,7 @@ const VocabularyStudy: React.FC = () => {
|
||||
// 新增 state
|
||||
const [studyWords, setStudyWords] = useState<Word[]>([]);
|
||||
const [studyIndex, setStudyIndex] = useState(0);
|
||||
const [testWords, setTestWords] = useState<Word[]>([]);
|
||||
const [testWords, setTestWords] = useState<TestQuestion[]>([]);
|
||||
const [testIndex, setTestIndex] = useState(0);
|
||||
|
||||
// 添加声音相关的状态
|
||||
@@ -192,9 +239,14 @@ const VocabularyStudy: React.FC = () => {
|
||||
console.log('获取到的学习单词:', response);
|
||||
|
||||
if (response && response.length > 0) {
|
||||
const shuffledWords = shuffleArray(response);
|
||||
const normalizedWords = response.map(item => ({
|
||||
...item,
|
||||
id: item.id || item._id || ''
|
||||
}));
|
||||
const shuffledWords = shuffleArray(normalizedWords);
|
||||
setStudyWords(shuffledWords);
|
||||
setStudyIndex(0);
|
||||
setLoadedWordSetId(effectiveId);
|
||||
setActiveTab('study');
|
||||
message.success(`成功加载 ${shuffledWords.length} 个单词`);
|
||||
} else {
|
||||
@@ -365,54 +417,61 @@ const VocabularyStudy: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 开始测试
|
||||
const startTest = () => {
|
||||
// 开始测试会话
|
||||
const startTest = async () => {
|
||||
if (studyWords.length === 0) {
|
||||
message.error('请先加载单词');
|
||||
return;
|
||||
}
|
||||
const shuffled = shuffleArray([...studyWords]);
|
||||
setTestWords(shuffled);
|
||||
setTestIndex(0);
|
||||
setTestStarted(true);
|
||||
setTestFinished(false);
|
||||
setUserAnswer('');
|
||||
setShowAnswer(false);
|
||||
setTestResults([]);
|
||||
// 重置统计信息
|
||||
setStudyStats({
|
||||
totalWords: 0,
|
||||
correctWords: 0,
|
||||
accuracy: 0,
|
||||
startTime: new Date(),
|
||||
duration: 0
|
||||
});
|
||||
setActiveTab('test');
|
||||
};
|
||||
|
||||
// 生成多选题选项
|
||||
const generateMultipleChoiceOptions = (index: number, wordsArr?: Word[]) => {
|
||||
const arr = wordsArr || testWords;
|
||||
const correctTranslation = arr[index].translation;
|
||||
let availableOptions = arr
|
||||
.filter(w => w.translation !== correctTranslation)
|
||||
.map(w => w.translation);
|
||||
|
||||
// 如果可用选项太少,添加一些假选项
|
||||
if (availableOptions.length < 3) {
|
||||
const fakeOptions = [
|
||||
'假选项1', '假选项2', '假选项3', '假选项4', '假选项5'
|
||||
].filter(opt => opt !== correctTranslation);
|
||||
availableOptions = [...availableOptions, ...fakeOptions];
|
||||
try {
|
||||
setTestAttemptLoading(true);
|
||||
setTestAttemptId('');
|
||||
setPendingAnswers([]);
|
||||
setTestResults([]);
|
||||
const wordIds = studyWords.map(getWordId).filter(Boolean);
|
||||
if (wordIds.length === 0) {
|
||||
message.error('单词数据不完整,请重新加载后再试');
|
||||
return;
|
||||
}
|
||||
const response = await api.post<TestAttemptResponse>(API_PATHS.VOCABULARY.TEST_ATTEMPT, {
|
||||
wordSetId: loadedWordSetId || selectedWordSet || currentWordSetIdRef.current,
|
||||
testType,
|
||||
wordIds
|
||||
});
|
||||
|
||||
const questions = response.questions.map(question => ({
|
||||
...question,
|
||||
options: question.options || []
|
||||
}));
|
||||
|
||||
setTestAttemptId(response.attemptId);
|
||||
setTestWords(questions);
|
||||
setTestIndex(0);
|
||||
answerSubmittingRef.current = false;
|
||||
setTestStarted(true);
|
||||
setTestFinished(false);
|
||||
setUserAnswer('');
|
||||
setShowAnswer(false);
|
||||
setTestResults([]);
|
||||
setPendingAnswers([]);
|
||||
setOptions(questions[0]?.options || []);
|
||||
setStudyStats({
|
||||
totalWords: 0,
|
||||
correctWords: 0,
|
||||
accuracy: 0,
|
||||
startTime: new Date(),
|
||||
duration: 0
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError) {
|
||||
message.error(`创建测试会话失败: ${error.message}`);
|
||||
} else {
|
||||
message.error('创建测试会话失败');
|
||||
}
|
||||
} finally {
|
||||
setTestAttemptLoading(false);
|
||||
}
|
||||
|
||||
// 打乱并选择3个错误选项
|
||||
availableOptions = shuffleArray(availableOptions);
|
||||
const incorrectOptions = availableOptions.slice(0, 3);
|
||||
|
||||
// 合并正确选项和错误选项,然后打乱
|
||||
const allOptions = shuffleArray([correctTranslation, ...incorrectOptions]);
|
||||
setOptions(allOptions);
|
||||
};
|
||||
|
||||
// 数组随机排序
|
||||
@@ -425,6 +484,17 @@ const VocabularyStudy: React.FC = () => {
|
||||
return newArray;
|
||||
};
|
||||
|
||||
const getWordId = (word: Word): string => word._id || word.id;
|
||||
|
||||
const buildAnswerProof = (
|
||||
attemptId: string,
|
||||
questionToken: string,
|
||||
userAnswer: string,
|
||||
submittedAt: number
|
||||
): string => {
|
||||
return CryptoJS.SHA256(`${attemptId}:${questionToken}:${submittedAt}:${userAnswer}`).toString();
|
||||
};
|
||||
|
||||
function normalizeAnswer(str: string): string {
|
||||
return str
|
||||
.replace(/(/g, '(')
|
||||
@@ -438,130 +508,92 @@ const VocabularyStudy: React.FC = () => {
|
||||
|
||||
// 提交答案
|
||||
const submitAnswer = async () => {
|
||||
if (!testStarted || testWords.length === 0) return;
|
||||
if (!testStarted || testWords.length === 0 || !testAttemptId || answerSubmittingRef.current) return;
|
||||
answerSubmittingRef.current = true;
|
||||
|
||||
const currentWord = testWords[testIndex];
|
||||
let isCorrect = false;
|
||||
let correctAnswer = '';
|
||||
|
||||
switch (testType) {
|
||||
case 'chinese-to-english':
|
||||
case 'audio-to-english':
|
||||
correctAnswer = currentWord.word;
|
||||
const normUser = normalizeAnswer(userAnswer);
|
||||
const normCorrect = normalizeAnswer(correctAnswer);
|
||||
isCorrect = normUser === normCorrect;
|
||||
break;
|
||||
case 'multiple-choice':
|
||||
correctAnswer = currentWord.translation;
|
||||
isCorrect = userAnswer === correctAnswer;
|
||||
break;
|
||||
const currentQuestion = testWords[testIndex];
|
||||
if (!currentQuestion) {
|
||||
answerSubmittingRef.current = false;
|
||||
return;
|
||||
}
|
||||
const submittedAt = Date.now();
|
||||
const normalizedUserAnswer = userAnswer.trim();
|
||||
const answerProof = buildAnswerProof(
|
||||
testAttemptId,
|
||||
currentQuestion.questionToken,
|
||||
normalizedUserAnswer,
|
||||
submittedAt
|
||||
);
|
||||
|
||||
// 更新测试结果数组
|
||||
const updatedResults = [
|
||||
...testResults,
|
||||
const updatedPendingAnswers = [
|
||||
...pendingAnswers,
|
||||
{
|
||||
word: currentWord.word,
|
||||
userAnswer,
|
||||
correctAnswer,
|
||||
isCorrect
|
||||
questionToken: currentQuestion.questionToken,
|
||||
userAnswer: normalizedUserAnswer,
|
||||
submittedAt,
|
||||
answerProof
|
||||
}
|
||||
];
|
||||
setTestResults(updatedResults);
|
||||
|
||||
// 更新学习统计信息
|
||||
const correctCount = updatedResults.filter(r => r.isCorrect).length;
|
||||
const totalCount = updatedResults.length;
|
||||
|
||||
setStudyStats(prev => ({
|
||||
...prev,
|
||||
totalWords: totalCount,
|
||||
correctWords: correctCount,
|
||||
accuracy: (correctCount / totalCount) * 100
|
||||
}));
|
||||
|
||||
// 记录学习记录
|
||||
try {
|
||||
await api.post(API_PATHS.VOCABULARY.WORD_RECORD, {
|
||||
wordId: currentWord._id || currentWord.id,
|
||||
isCorrect,
|
||||
testType
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('记录单词学习结果失败', error);
|
||||
}
|
||||
|
||||
if (isCorrect) {
|
||||
message.success('正确!');
|
||||
} else {
|
||||
message.error(`错误! 正确答案是: ${correctAnswer}`);
|
||||
}
|
||||
|
||||
setPendingAnswers(updatedPendingAnswers);
|
||||
setShowAnswer(true);
|
||||
setUserAnswer('');
|
||||
|
||||
setTimeout(() => {
|
||||
if (testIndex < testWords.length - 1) {
|
||||
setTestIndex(testIndex + 1);
|
||||
setUserAnswer('');
|
||||
setShowAnswer(false);
|
||||
// 多选题生成选项
|
||||
if (testType === 'multiple-choice') {
|
||||
generateMultipleChoiceOptions(testIndex + 1);
|
||||
}
|
||||
// 听力自动播放
|
||||
answerSubmittingRef.current = false;
|
||||
if (testType === 'audio-to-english') {
|
||||
setTimeout(() => {
|
||||
playWordSound(testWords[testIndex + 1].word);
|
||||
playWordSound(testWords[testIndex + 1]?.word || '');
|
||||
}, 500);
|
||||
}
|
||||
if (testType === 'multiple-choice') {
|
||||
setOptions(testWords[testIndex + 1]?.options || []);
|
||||
}
|
||||
} else {
|
||||
// 最后一题,使用本地更新的结果,避免异步状态更新问题
|
||||
finishTestWithResults(updatedResults);
|
||||
answerSubmittingRef.current = false;
|
||||
finishTestWithResults(updatedPendingAnswers);
|
||||
}
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
// 使用指定结果完成测试
|
||||
const finishTestWithResults = async (finalResults: any[]) => {
|
||||
const finishTestWithResults = async (finalAnswers: PendingAnswer[]) => {
|
||||
try {
|
||||
setTestFinished(true);
|
||||
|
||||
// 获取服务器时间
|
||||
const { serverTime } = await api.get<{ serverTime: number }>(API_PATHS.SYSTEM.SERVER_TIME);
|
||||
|
||||
const correctCount = finalResults.filter(r => r.isCorrect).length;
|
||||
const totalCount = finalResults.length;
|
||||
|
||||
// 添加调试信息
|
||||
console.log('测试完成状态(带结果):', {
|
||||
finalResultsLength: finalResults.length,
|
||||
finalResultsLength: finalAnswers.length,
|
||||
testWordsLength: testWords.length,
|
||||
correctCount,
|
||||
totalCount,
|
||||
attemptId: testAttemptId,
|
||||
currentStudyStats: studyStats
|
||||
});
|
||||
|
||||
// 直接使用传入的测试结果计算
|
||||
const updatedStats = {
|
||||
totalWords: totalCount,
|
||||
correctWords: correctCount,
|
||||
accuracy: totalCount > 0 ? (correctCount / totalCount) * 100 : 0,
|
||||
startTime: studyStats.startTime,
|
||||
endTime: new Date(serverTime),
|
||||
duration: (serverTime - studyStats.startTime.getTime()) / 1000
|
||||
};
|
||||
|
||||
// 更新统计信息
|
||||
setStudyStats(updatedStats);
|
||||
|
||||
// 提交测试记录 - 使用计算好的最新数据
|
||||
await api.post(API_PATHS.VOCABULARY.TEST_RECORD, {
|
||||
wordSetId: selectedWordSet,
|
||||
testType,
|
||||
stats: updatedStats,
|
||||
results: finalResults // 发送详细的测试结果,包括每道题的回答情况
|
||||
// 提交测试记录,后端会用数据库答案重新判分
|
||||
const savedRecord = await api.post<SavedVocabularyTestRecordResponse>(API_PATHS.VOCABULARY.TEST_RECORD, {
|
||||
attemptId: testAttemptId,
|
||||
answers: finalAnswers.map(answer => ({
|
||||
questionToken: answer.questionToken,
|
||||
userAnswer: answer.userAnswer,
|
||||
submittedAt: answer.submittedAt,
|
||||
answerProof: answer.answerProof
|
||||
}))
|
||||
});
|
||||
|
||||
setStudyStats({
|
||||
totalWords: savedRecord.stats.totalWords,
|
||||
correctWords: savedRecord.stats.correctWords,
|
||||
accuracy: savedRecord.stats.accuracy,
|
||||
startTime: new Date(savedRecord.stats.startTime),
|
||||
endTime: new Date(savedRecord.stats.endTime),
|
||||
duration: savedRecord.stats.duration
|
||||
});
|
||||
setTestResults(savedRecord.results);
|
||||
setPendingAnswers([]);
|
||||
setTestAttemptId('');
|
||||
|
||||
message.success('测试完成,记录已保存');
|
||||
setIsModalVisible(true);
|
||||
@@ -577,19 +609,22 @@ const VocabularyStudy: React.FC = () => {
|
||||
// 完成测试 - 兼容原来的调用方式
|
||||
const finishTest = async () => {
|
||||
// 使用当前的测试结果完成测试
|
||||
await finishTestWithResults([...testResults]);
|
||||
await finishTestWithResults([...pendingAnswers]);
|
||||
};
|
||||
|
||||
// 重新开始测试
|
||||
const restartTest = () => {
|
||||
const shuffled = shuffleArray([...testWords]);
|
||||
setTestWords(shuffled);
|
||||
setTestWords([]);
|
||||
setTestIndex(0);
|
||||
setTestAttemptId('');
|
||||
setTestStarted(false); // 回到模式选择界面
|
||||
setTestFinished(false);
|
||||
setUserAnswer('');
|
||||
setShowAnswer(false);
|
||||
setTestResults([]);
|
||||
setPendingAnswers([]);
|
||||
setOptions([]);
|
||||
answerSubmittingRef.current = false;
|
||||
// 重置统计信息
|
||||
setStudyStats({
|
||||
totalWords: 0,
|
||||
@@ -704,7 +739,7 @@ const VocabularyStudy: React.FC = () => {
|
||||
}
|
||||
}, [testIndex, testStarted, testType, testFinished]);
|
||||
|
||||
// 切换题目时自动生成选项
|
||||
// 切换题目时同步多选题选项
|
||||
useEffect(() => {
|
||||
if (
|
||||
testStarted &&
|
||||
@@ -712,7 +747,7 @@ const VocabularyStudy: React.FC = () => {
|
||||
testType === 'multiple-choice' &&
|
||||
testWords.length > 0
|
||||
) {
|
||||
generateMultipleChoiceOptions(testIndex);
|
||||
setOptions(testWords[testIndex]?.options || []);
|
||||
}
|
||||
}, [testIndex, testType, testStarted, testFinished, testWords]);
|
||||
|
||||
@@ -729,6 +764,14 @@ const VocabularyStudy: React.FC = () => {
|
||||
}
|
||||
}, [testStarted, testType, testWords, testFinished, testIndex]);
|
||||
|
||||
const handleTabChange = (nextTab: string) => {
|
||||
if (testStarted && !testFinished && activeTab === 'test' && nextTab !== 'test') {
|
||||
message.warning('测试进行中,请完成本次测试后再切换页面');
|
||||
return;
|
||||
}
|
||||
setActiveTab(nextTab);
|
||||
};
|
||||
|
||||
// Tab 切换
|
||||
const items: TabsProps['items'] = [
|
||||
{
|
||||
@@ -976,30 +1019,9 @@ const VocabularyStudy: React.FC = () => {
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
// 只有这里才初始化测试
|
||||
const shuffled = shuffleArray([...studyWords]);
|
||||
setTestWords(shuffled);
|
||||
setTestIndex(0);
|
||||
setTestStarted(true);
|
||||
setTestFinished(false);
|
||||
setUserAnswer('');
|
||||
setShowAnswer(false);
|
||||
setTestResults([]);
|
||||
// 重置统计信息
|
||||
setStudyStats({
|
||||
totalWords: 0,
|
||||
correctWords: 0,
|
||||
accuracy: 0,
|
||||
startTime: new Date(),
|
||||
duration: 0
|
||||
});
|
||||
// 如果是选择翻译,生成选项
|
||||
if (testType === 'multiple-choice') {
|
||||
generateMultipleChoiceOptions(0, shuffled);
|
||||
}
|
||||
}}
|
||||
onClick={startTest}
|
||||
disabled={studyWords.length === 0}
|
||||
loading={testAttemptLoading}
|
||||
style={{
|
||||
width: '100%',
|
||||
background: studyWords.length === 0 ? '#f5f5f5' : undefined,
|
||||
@@ -1169,6 +1191,7 @@ const VocabularyStudy: React.FC = () => {
|
||||
{testType === 'multiple-choice' ? (
|
||||
<Radio.Group
|
||||
value={userAnswer}
|
||||
disabled={showAnswer}
|
||||
onChange={e => setUserAnswer(e.target.value)}
|
||||
style={{ width: '100%', display: 'flex', flexDirection: 'column' }}
|
||||
>
|
||||
@@ -1199,23 +1222,19 @@ const VocabularyStudy: React.FC = () => {
|
||||
{showAnswer && (
|
||||
<div style={{
|
||||
padding: 15,
|
||||
backgroundColor: testResults[testResults.length - 1]?.isCorrect ? '#f6ffed' : '#fff2f0',
|
||||
backgroundColor: '#f6f8fa',
|
||||
borderRadius: 4,
|
||||
marginBottom: 15,
|
||||
border: `1px solid ${testResults[testResults.length - 1]?.isCorrect ? '#b7eb8f' : '#ffccc7'}`
|
||||
border: '1px solid #d9d9d9'
|
||||
}}>
|
||||
<div style={{
|
||||
fontWeight: 'bold',
|
||||
color: testResults[testResults.length - 1]?.isCorrect ? '#52c41a' : '#f5222d',
|
||||
color: '#1677ff',
|
||||
marginBottom: 5
|
||||
}}>
|
||||
{testResults[testResults.length - 1]?.isCorrect ? '✓ 回答正确' : '✗ 回答错误'}
|
||||
本题已提交
|
||||
</div>
|
||||
<div>正确答案: {testType === 'multiple-choice'
|
||||
? testWords[testIndex].translation
|
||||
: testWords[testIndex].word}
|
||||
</div>
|
||||
<div>你的答案: {userAnswer}</div>
|
||||
<div>正在进入下一题...</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1516,7 +1535,7 @@ const VocabularyStudy: React.FC = () => {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Tabs activeKey={activeTab} items={items} onChange={setActiveTab} />
|
||||
<Tabs activeKey={activeTab} items={items} onChange={handleTabChange} />
|
||||
|
||||
{/* 添加声音设置弹窗 */}
|
||||
{renderVoiceSettings()}
|
||||
@@ -1570,4 +1589,4 @@ const VocabularyStudy: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default VocabularyStudy;
|
||||
export default VocabularyStudy;
|
||||
|
||||
@@ -48,6 +48,7 @@ export const API_PATHS = {
|
||||
WORD_SETS: '/vocabulary/word-sets',
|
||||
STUDY_WORDS: '/vocabulary/study-words',
|
||||
UPLOAD: '/vocabulary/upload',
|
||||
TEST_ATTEMPT: '/vocabulary/test-attempt',
|
||||
WORD_RECORD: '/vocabulary/word-record',
|
||||
TEST_RECORD: '/vocabulary/test-record',
|
||||
STUDY_RECORDS: '/vocabulary/test-records',
|
||||
@@ -117,4 +118,4 @@ export const config = {
|
||||
} as const;
|
||||
|
||||
// 默认导出
|
||||
export default config;
|
||||
export default config;
|
||||
|
||||
Reference in New Issue
Block a user