Compare commits

...

3 Commits

Author SHA1 Message Date
0de72a629c fixed a bug avoid crack answer 2026-05-22 14:50:30 +08:00
01bc52502f no soucemap generned 2026-05-22 14:23:59 +08:00
09aa034f52 fix some problems of games 2026-05-22 13:56:23 +08:00
11 changed files with 294 additions and 113 deletions

View File

@@ -84,7 +84,6 @@ _TD.a.push(function (TD) {
"laser_gun": { "laser_gun": {
damage: 25, damage: 25,
range: 6, range: 6,
max_range: 10,
speed: 20, speed: 20,
// bullet_speed: 10, // laser_gun 的 bullet_speed 属性没有用 // bullet_speed: 10, // laser_gun 的 bullet_speed 属性没有用
life: 100, life: 100,

View File

@@ -20,7 +20,20 @@
for (var i = 0; i < map.buildings.length; i++) { for (var i = 0; i < map.buildings.length; i++) {
var b = map.buildings[i]; var b = map.buildings[i];
if (!b || !b.grid) continue; if (!b || !b.grid) continue;
buildings.push({ type: b.type, mx: b.grid.mx, my: b.grid.my, level: b.level, money: b.money }); buildings.push({
type: b.type,
mx: b.grid.mx,
my: b.grid.my,
level: b.level,
money: b.money,
killed: b.killed,
damage: b.damage,
range: b.range,
speed: b.speed,
life: b.life,
shield: b.shield,
upgrade_records: b._upgrade_records || null
});
} }
} }
@@ -159,8 +172,36 @@
if (grid) grid.addBuilding(bi.type); if (grid) grid.addBuilding(bi.type);
var b = grid && grid.building; var b = grid && grid.building;
if (b) { if (b) {
if (typeof bi.level !== 'undefined') b.level = bi.level; var hasSavedBuildingAttrs = (
typeof bi.damage !== 'undefined' ||
typeof bi.range !== 'undefined' ||
typeof bi.speed !== 'undefined' ||
typeof bi.life !== 'undefined' ||
typeof bi.shield !== 'undefined'
);
if (!hasSavedBuildingAttrs && typeof bi.level !== 'undefined' && bi.level > 0 && typeof b.upgrade === 'function') {
for (var ul = 0; ul < bi.level; ul++) b.upgrade();
} else if (typeof bi.level !== 'undefined') {
b.level = bi.level;
}
if (typeof bi.damage !== 'undefined') b.damage = bi.damage;
if (typeof bi.range !== 'undefined') b.range = bi.range;
if (typeof bi.speed !== 'undefined') b.speed = bi.speed;
if (typeof bi.life !== 'undefined') b.life = bi.life;
if (typeof bi.shield !== 'undefined') b.shield = bi.shield;
if (typeof bi.money !== 'undefined') b.money = bi.money; if (typeof bi.money !== 'undefined') b.money = bi.money;
if (typeof bi.killed !== 'undefined') b.killed = bi.killed;
if (bi.upgrade_records) {
b._upgrade_records = {};
for (var rk in bi.upgrade_records) {
if (Object.prototype.hasOwnProperty.call(bi.upgrade_records, rk)) {
b._upgrade_records[rk] = bi.upgrade_records[rk];
}
}
}
b.range_px = b.range * TD.grid_size;
b.updateBtnDesc && b.updateBtnDesc(); b.updateBtnDesc && b.updateBtnDesc();
} }
} }

View File

@@ -19,7 +19,20 @@ window.__TD_getState = function () {
for (var i = 0; i < map.buildings.length; i++) { for (var i = 0; i < map.buildings.length; i++) {
var b = map.buildings[i]; var b = map.buildings[i];
if (!b || !b.grid) continue; if (!b || !b.grid) continue;
buildings.push({ type: b.type, mx: b.grid.mx, my: b.grid.my, level: b.level, money: b.money }); buildings.push({
type: b.type,
mx: b.grid.mx,
my: b.grid.my,
level: b.level,
money: b.money,
killed: b.killed,
damage: b.damage,
range: b.range,
speed: b.speed,
life: b.life,
shield: b.shield,
upgrade_records: b._upgrade_records || null
});
} }
} }
@@ -158,8 +171,36 @@ window.__TD_loadState = function (s) {
if (grid) grid.addBuilding(bi.type); if (grid) grid.addBuilding(bi.type);
var b = grid && grid.building; var b = grid && grid.building;
if (b) { if (b) {
if (typeof bi.level !== 'undefined') b.level = bi.level; var hasSavedBuildingAttrs = (
typeof bi.damage !== 'undefined' ||
typeof bi.range !== 'undefined' ||
typeof bi.speed !== 'undefined' ||
typeof bi.life !== 'undefined' ||
typeof bi.shield !== 'undefined'
);
if (!hasSavedBuildingAttrs && typeof bi.level !== 'undefined' && bi.level > 0 && typeof b.upgrade === 'function') {
for (var ul = 0; ul < bi.level; ul++) b.upgrade();
} else if (typeof bi.level !== 'undefined') {
b.level = bi.level;
}
if (typeof bi.damage !== 'undefined') b.damage = bi.damage;
if (typeof bi.range !== 'undefined') b.range = bi.range;
if (typeof bi.speed !== 'undefined') b.speed = bi.speed;
if (typeof bi.life !== 'undefined') b.life = bi.life;
if (typeof bi.shield !== 'undefined') b.shield = bi.shield;
if (typeof bi.money !== 'undefined') b.money = bi.money; if (typeof bi.money !== 'undefined') b.money = bi.money;
if (typeof bi.killed !== 'undefined') b.killed = bi.killed;
if (bi.upgrade_records) {
b._upgrade_records = {};
for (var rk in bi.upgrade_records) {
if (Object.prototype.hasOwnProperty.call(bi.upgrade_records, rk)) {
b._upgrade_records[rk] = bi.upgrade_records[rk];
}
}
}
b.range_px = b.range * TD.grid_size;
b.updateBtnDesc && b.updateBtnDesc(); b.updateBtnDesc && b.updateBtnDesc();
} }
} }

View File

@@ -268,7 +268,20 @@ var _TD = {
for (var i = 0; i < map.buildings.length; i++) { for (var i = 0; i < map.buildings.length; i++) {
var b = map.buildings[i]; var b = map.buildings[i];
if (!b || !b.grid) continue; if (!b || !b.grid) continue;
buildings.push({ type: b.type, mx: b.grid.mx, my: b.grid.my, level: b.level, money: b.money }); buildings.push({
type: b.type,
mx: b.grid.mx,
my: b.grid.my,
level: b.level,
money: b.money,
killed: b.killed,
damage: b.damage,
range: b.range,
speed: b.speed,
life: b.life,
shield: b.shield,
upgrade_records: b._upgrade_records || null
});
} }
} }
@@ -359,8 +372,36 @@ var _TD = {
if (grid) grid.addBuilding(bi.type); if (grid) grid.addBuilding(bi.type);
var b = grid && grid.building; var b = grid && grid.building;
if (b) { if (b) {
if (typeof bi.level !== 'undefined') b.level = bi.level; var hasSavedBuildingAttrs = (
typeof bi.damage !== 'undefined' ||
typeof bi.range !== 'undefined' ||
typeof bi.speed !== 'undefined' ||
typeof bi.life !== 'undefined' ||
typeof bi.shield !== 'undefined'
);
if (!hasSavedBuildingAttrs && typeof bi.level !== 'undefined' && bi.level > 0 && typeof b.upgrade === 'function') {
for (var ul = 0; ul < bi.level; ul++) b.upgrade();
} else if (typeof bi.level !== 'undefined') {
b.level = bi.level;
}
if (typeof bi.damage !== 'undefined') b.damage = bi.damage;
if (typeof bi.range !== 'undefined') b.range = bi.range;
if (typeof bi.speed !== 'undefined') b.speed = bi.speed;
if (typeof bi.life !== 'undefined') b.life = bi.life;
if (typeof bi.shield !== 'undefined') b.shield = bi.shield;
if (typeof bi.money !== 'undefined') b.money = bi.money; if (typeof bi.money !== 'undefined') b.money = bi.money;
if (typeof bi.killed !== 'undefined') b.killed = bi.killed;
if (bi.upgrade_records) {
b._upgrade_records = {};
for (var rk in bi.upgrade_records) {
if (Object.prototype.hasOwnProperty.call(bi.upgrade_records, rk)) {
b._upgrade_records[rk] = bi.upgrade_records[rk];
}
}
}
b.range_px = b.range * TD.grid_size;
b.updateBtnDesc && b.updateBtnDesc(); b.updateBtnDesc && b.updateBtnDesc();
} }
} }

View File

@@ -71,7 +71,20 @@
for (var i = 0; i < map.buildings.length; i++) { for (var i = 0; i < map.buildings.length; i++) {
var b = map.buildings[i]; var b = map.buildings[i];
if (!b || !b.grid) continue; if (!b || !b.grid) continue;
buildings.push({ type: b.type, mx: b.grid.mx, my: b.grid.my, level: b.level, money: b.money }); buildings.push({
type: b.type,
mx: b.grid.mx,
my: b.grid.my,
level: b.level,
money: b.money,
killed: b.killed,
damage: b.damage,
range: b.range,
speed: b.speed,
life: b.life,
shield: b.shield,
upgrade_records: b._upgrade_records || null
});
} }
} }
@@ -210,8 +223,36 @@
if (grid) grid.addBuilding(bi.type); if (grid) grid.addBuilding(bi.type);
var b = grid && grid.building; var b = grid && grid.building;
if (b) { if (b) {
if (typeof bi.level !== 'undefined') b.level = bi.level; var hasSavedBuildingAttrs = (
typeof bi.damage !== 'undefined' ||
typeof bi.range !== 'undefined' ||
typeof bi.speed !== 'undefined' ||
typeof bi.life !== 'undefined' ||
typeof bi.shield !== 'undefined'
);
if (!hasSavedBuildingAttrs && typeof bi.level !== 'undefined' && bi.level > 0 && typeof b.upgrade === 'function') {
for (var ul = 0; ul < bi.level; ul++) b.upgrade();
} else if (typeof bi.level !== 'undefined') {
b.level = bi.level;
}
if (typeof bi.damage !== 'undefined') b.damage = bi.damage;
if (typeof bi.range !== 'undefined') b.range = bi.range;
if (typeof bi.speed !== 'undefined') b.speed = bi.speed;
if (typeof bi.life !== 'undefined') b.life = bi.life;
if (typeof bi.shield !== 'undefined') b.shield = bi.shield;
if (typeof bi.money !== 'undefined') b.money = bi.money; if (typeof bi.money !== 'undefined') b.money = bi.money;
if (typeof bi.killed !== 'undefined') b.killed = bi.killed;
if (bi.upgrade_records) {
b._upgrade_records = {};
for (var rk in bi.upgrade_records) {
if (Object.prototype.hasOwnProperty.call(bi.upgrade_records, rk)) {
b._upgrade_records[rk] = bi.upgrade_records[rk];
}
}
}
b.range_px = b.range * TD.grid_size;
b.updateBtnDesc && b.updateBtnDesc(); b.updateBtnDesc && b.updateBtnDesc();
} }
} }

View File

@@ -10,7 +10,7 @@
"debug:server": "cross-env NODE_OPTIONS=\"--loader ts-node/esm --inspect=5858\" ts-node-dev server/server.ts", "debug:server": "cross-env NODE_OPTIONS=\"--loader ts-node/esm --inspect=5858\" ts-node-dev server/server.ts",
"debug:client": "cross-env PORT=3001 react-scripts start", "debug:client": "cross-env PORT=3001 react-scripts start",
"debug": "concurrently \"npm run debug:server\" \"npm run debug:client\"", "debug": "concurrently \"npm run debug:server\" \"npm run debug:client\"",
"build": "react-scripts build", "build": "cross-env GENERATE_SOURCEMAP=false react-scripts build",
"test": "react-scripts test", "test": "react-scripts test",
"eject": "react-scripts eject", "eject": "react-scripts eject",
"migrate-passwords": "ts-node -P tsconfig.json scripts/migratePasswords.ts" "migrate-passwords": "ts-node -P tsconfig.json scripts/migratePasswords.ts"

View File

@@ -265,7 +265,20 @@ var _TD = {
for (var i = 0; i < map.buildings.length; i++) { for (var i = 0; i < map.buildings.length; i++) {
var b = map.buildings[i]; var b = map.buildings[i];
if (!b || !b.grid) continue; if (!b || !b.grid) continue;
buildings.push({ type: b.type, mx: b.grid.mx, my: b.grid.my, level: b.level, money: b.money }); buildings.push({
type: b.type,
mx: b.grid.mx,
my: b.grid.my,
level: b.level,
money: b.money,
killed: b.killed,
damage: b.damage,
range: b.range,
speed: b.speed,
life: b.life,
shield: b.shield,
upgrade_records: b._upgrade_records || null
});
} }
} }
return { money: TD.money, life: TD.life, score: TD.score, wave: (scene && scene.wave) || 0, buildings: buildings }; return { money: TD.money, life: TD.life, score: TD.score, wave: (scene && scene.wave) || 0, buildings: buildings };
@@ -294,8 +307,36 @@ var _TD = {
if (grid) grid.addBuilding(bi.type); if (grid) grid.addBuilding(bi.type);
var b = grid && grid.building; var b = grid && grid.building;
if (b) { if (b) {
if (typeof bi.level !== 'undefined') b.level = bi.level; var hasSavedBuildingAttrs = (
typeof bi.damage !== 'undefined' ||
typeof bi.range !== 'undefined' ||
typeof bi.speed !== 'undefined' ||
typeof bi.life !== 'undefined' ||
typeof bi.shield !== 'undefined'
);
if (!hasSavedBuildingAttrs && typeof bi.level !== 'undefined' && bi.level > 0 && typeof b.upgrade === 'function') {
for (var ul = 0; ul < bi.level; ul++) b.upgrade();
} else if (typeof bi.level !== 'undefined') {
b.level = bi.level;
}
if (typeof bi.damage !== 'undefined') b.damage = bi.damage;
if (typeof bi.range !== 'undefined') b.range = bi.range;
if (typeof bi.speed !== 'undefined') b.speed = bi.speed;
if (typeof bi.life !== 'undefined') b.life = bi.life;
if (typeof bi.shield !== 'undefined') b.shield = bi.shield;
if (typeof bi.money !== 'undefined') b.money = bi.money; if (typeof bi.money !== 'undefined') b.money = bi.money;
if (typeof bi.killed !== 'undefined') b.killed = bi.killed;
if (bi.upgrade_records) {
b._upgrade_records = {};
for (var rk in bi.upgrade_records) {
if (Object.prototype.hasOwnProperty.call(bi.upgrade_records, rk)) {
b._upgrade_records[rk] = bi.upgrade_records[rk];
}
}
}
b.range_px = b.range * TD.grid_size;
b.updateBtnDesc && b.updateBtnDesc(); b.updateBtnDesc && b.updateBtnDesc();
} }
} }
@@ -3845,7 +3886,6 @@ _TD.a.push(function (TD) {
"laser_gun": { "laser_gun": {
damage: 25, damage: 25,
range: 6, range: 6,
max_range: 10,
speed: 20, speed: 20,
// bullet_speed: 10, // laser_gun 的 bullet_speed 属性没有用 // bullet_speed: 10, // laser_gun 的 bullet_speed 属性没有用
life: 100, life: 100,
@@ -4584,5 +4624,3 @@ _TD.a.push(function (TD) {
}; };
}); // _TD.a.push end }); // _TD.a.push end

View File

@@ -3824,7 +3824,6 @@ _TD.a.push(function (TD) {
"laser_gun": { "laser_gun": {
damage: 25, damage: 25,
range: 6, range: 6,
max_range: 10,
speed: 20, speed: 20,
// bullet_speed: 10, // laser_gun 的 bullet_speed 属性没有用 // bullet_speed: 10, // laser_gun 的 bullet_speed 属性没有用
life: 100, life: 100,

View File

@@ -95,7 +95,20 @@
for (var i = 0; i < map.buildings.length; i++) { for (var i = 0; i < map.buildings.length; i++) {
var b = map.buildings[i]; var b = map.buildings[i];
if (!b || !b.grid) continue; if (!b || !b.grid) continue;
buildings.push({ type: b.type, mx: b.grid.mx, my: b.grid.my, level: b.level, money: b.money }); buildings.push({
type: b.type,
mx: b.grid.mx,
my: b.grid.my,
level: b.level,
money: b.money,
killed: b.killed,
damage: b.damage,
range: b.range,
speed: b.speed,
life: b.life,
shield: b.shield,
upgrade_records: b._upgrade_records || null
});
} }
} }
@@ -190,8 +203,36 @@
grid.addBuilding(bi.type); grid.addBuilding(bi.type);
var b = grid.building; var b = grid.building;
if (b) { if (b) {
if (typeof bi.level !== 'undefined') b.level = bi.level; var hasSavedBuildingAttrs = (
typeof bi.damage !== 'undefined' ||
typeof bi.range !== 'undefined' ||
typeof bi.speed !== 'undefined' ||
typeof bi.life !== 'undefined' ||
typeof bi.shield !== 'undefined'
);
if (!hasSavedBuildingAttrs && typeof bi.level !== 'undefined' && bi.level > 0 && typeof b.upgrade === 'function') {
for (var ul = 0; ul < bi.level; ul++) b.upgrade();
} else if (typeof bi.level !== 'undefined') {
b.level = bi.level;
}
if (typeof bi.damage !== 'undefined') b.damage = bi.damage;
if (typeof bi.range !== 'undefined') b.range = bi.range;
if (typeof bi.speed !== 'undefined') b.speed = bi.speed;
if (typeof bi.life !== 'undefined') b.life = bi.life;
if (typeof bi.shield !== 'undefined') b.shield = bi.shield;
if (typeof bi.money !== 'undefined') b.money = bi.money; if (typeof bi.money !== 'undefined') b.money = bi.money;
if (typeof bi.killed !== 'undefined') b.killed = bi.killed;
if (bi.upgrade_records) {
b._upgrade_records = {};
for (var rk in bi.upgrade_records) {
if (Object.prototype.hasOwnProperty.call(bi.upgrade_records, rk)) {
b._upgrade_records[rk] = bi.upgrade_records[rk];
}
}
}
b.range_px = b.range * TD.grid_size;
b.updateBtnDesc && b.updateBtnDesc(); b.updateBtnDesc && b.updateBtnDesc();
} }
} }

View File

@@ -48,6 +48,7 @@ const WORD_RECORD_MODES: WordRecordModeKey[] = [
const ATTEMPT_TTL_MS = 30 * 60 * 1000; const ATTEMPT_TTL_MS = 30 * 60 * 1000;
const MIN_SECONDS_PER_QUESTION = Number(process.env.VOCABULARY_MIN_SECONDS_PER_QUESTION || 0.4); const MIN_SECONDS_PER_QUESTION = Number(process.env.VOCABULARY_MIN_SECONDS_PER_QUESTION || 0.4);
const ATTEMPT_SECRET = process.env.VOCABULARY_ATTEMPT_SECRET || config.JWT_SECRET; const ATTEMPT_SECRET = process.env.VOCABULARY_ATTEMPT_SECRET || config.JWT_SECRET;
const ANSWER_PROOF_SALT = 'd1ktsalt';
const isVocabularyTestType = (value: unknown): value is VocabularyTestType => { const isVocabularyTestType = (value: unknown): value is VocabularyTestType => {
return value === 'chinese-to-english' || return value === 'chinese-to-english' ||
@@ -104,8 +105,8 @@ const buildAnswerProof = (
userAnswer: string, userAnswer: string,
submittedAt: number submittedAt: number
): string => crypto ): string => crypto
.createHash('sha256') .createHash('md5')
.update(`${attemptId}:${questionToken}:${submittedAt}:${String(userAnswer ?? '')}`) .update(`${ANSWER_PROOF_SALT}:${attemptId}:${questionToken}:${submittedAt}:${String(userAnswer ?? '')}:${ANSWER_PROOF_SALT}`)
.digest('hex'); .digest('hex');
const buildMultipleChoiceOptions = (word: any, pool: any[]): string[] => { const buildMultipleChoiceOptions = (word: any, pool: any[]): string[] => {
@@ -879,6 +880,7 @@ router.post('/test-record', authMiddleware, async (req, res) => {
message: '测试记录已保存', message: '测试记录已保存',
attemptId: attempt._id.toString(), attemptId: attempt._id.toString(),
stats: verifiedStats, stats: verifiedStats,
results: evaluatedResults,
submittedAt: new Date(lastSubmittedAtMs) submittedAt: new Date(lastSubmittedAtMs)
}); });
} catch (error) { } catch (error) {

View File

@@ -93,6 +93,7 @@ interface SavedVocabularyTestRecordResponse {
endTime: string | Date; endTime: string | Date;
duration: number; duration: number;
}; };
results: TestResult[];
} }
interface TestAttemptResponse { interface TestAttemptResponse {
@@ -102,6 +103,8 @@ interface TestAttemptResponse {
questions: TestQuestion[]; questions: TestQuestion[];
} }
const ANSWER_PROOF_SALT = 'd1ktsalt';
const VocabularyStudy: React.FC = () => { const VocabularyStudy: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -124,7 +127,6 @@ const VocabularyStudy: React.FC = () => {
const [testStarted, setTestStarted] = useState(false); const [testStarted, setTestStarted] = useState(false);
const [testFinished, setTestFinished] = useState(false); const [testFinished, setTestFinished] = useState(false);
const [testResults, setTestResults] = useState<TestResult[]>([]); const [testResults, setTestResults] = useState<TestResult[]>([]);
const [answerFeedback, setAnswerFeedback] = useState<TestResult | null>(null);
const [testAttemptId, setTestAttemptId] = useState<string>(''); const [testAttemptId, setTestAttemptId] = useState<string>('');
const [pendingAnswers, setPendingAnswers] = useState<PendingAnswer[]>([]); const [pendingAnswers, setPendingAnswers] = useState<PendingAnswer[]>([]);
const [testAttemptLoading, setTestAttemptLoading] = useState(false); const [testAttemptLoading, setTestAttemptLoading] = useState(false);
@@ -429,7 +431,6 @@ const VocabularyStudy: React.FC = () => {
setTestAttemptId(''); setTestAttemptId('');
setPendingAnswers([]); setPendingAnswers([]);
setTestResults([]); setTestResults([]);
setAnswerFeedback(null);
const wordIds = studyWords.map(getWordId).filter(Boolean); const wordIds = studyWords.map(getWordId).filter(Boolean);
if (wordIds.length === 0) { if (wordIds.length === 0) {
message.error('单词数据不完整,请重新加载后再试'); message.error('单词数据不完整,请重新加载后再试');
@@ -457,6 +458,7 @@ const VocabularyStudy: React.FC = () => {
setTestResults([]); setTestResults([]);
setPendingAnswers([]); setPendingAnswers([]);
setOptions(questions[0]?.options || []); setOptions(questions[0]?.options || []);
setIsModalVisible(false);
setStudyStats({ setStudyStats({
totalWords: 0, totalWords: 0,
correctWords: 0, correctWords: 0,
@@ -493,42 +495,7 @@ const VocabularyStudy: React.FC = () => {
userAnswer: string, userAnswer: string,
submittedAt: number submittedAt: number
): string => { ): string => {
return CryptoJS.SHA256(`${attemptId}:${questionToken}:${submittedAt}:${userAnswer}`).toString(); return CryptoJS.MD5(`${ANSWER_PROOF_SALT}:${attemptId}:${questionToken}:${submittedAt}:${userAnswer}:${ANSWER_PROOF_SALT}`).toString();
};
function normalizeAnswer(str: string): string {
return str
.replace(//g, '(')
.replace(//g, ')')
.replace(//g, ',') // 全角逗号转半角
.replace(/\s+/g, '') // 去除所有空格
.replace(/,/g, '') // 去除所有逗号
.replace(/[^\w()]/g, '') // 只保留字母、数字、括号
.toLowerCase();
}
const getQuestionWord = (question: TestQuestion): Word | undefined => {
return studyWords.find(word => getWordId(word) === question.wordId);
};
const buildLocalTestResult = (question: TestQuestion, rawUserAnswer: string): TestResult => {
const matchedWord = getQuestionWord(question);
const userAnswer = rawUserAnswer.trim();
const correctAnswer = testType === 'multiple-choice'
? String(matchedWord?.translation ?? question.translation ?? '').trim()
: String(matchedWord?.word ?? question.word ?? '').trim();
const isCorrect = testType === 'multiple-choice'
? userAnswer === correctAnswer
: normalizeAnswer(userAnswer) === normalizeAnswer(correctAnswer);
return {
questionToken: question.questionToken,
wordId: question.wordId,
word: String(matchedWord?.word ?? question.word ?? question.translation ?? '').trim(),
userAnswer,
correctAnswer,
isCorrect
};
}; };
// 提交答案 // 提交答案
@@ -543,7 +510,6 @@ const VocabularyStudy: React.FC = () => {
} }
const submittedAt = Date.now(); const submittedAt = Date.now();
const normalizedUserAnswer = userAnswer.trim(); const normalizedUserAnswer = userAnswer.trim();
const localResult = buildLocalTestResult(currentQuestion, normalizedUserAnswer);
const answerProof = buildAnswerProof( const answerProof = buildAnswerProof(
testAttemptId, testAttemptId,
currentQuestion.questionToken, currentQuestion.questionToken,
@@ -560,14 +526,8 @@ const VocabularyStudy: React.FC = () => {
answerProof answerProof
} }
]; ];
const updatedTestResults = [
...testResults,
localResult
];
setPendingAnswers(updatedPendingAnswers); setPendingAnswers(updatedPendingAnswers);
setTestResults(updatedTestResults);
setAnswerFeedback(localResult);
setShowAnswer(true); setShowAnswer(true);
setUserAnswer(''); setUserAnswer('');
@@ -576,7 +536,6 @@ const VocabularyStudy: React.FC = () => {
const nextIndex = testIndex + 1; const nextIndex = testIndex + 1;
setTestIndex(nextIndex); setTestIndex(nextIndex);
setShowAnswer(false); setShowAnswer(false);
setAnswerFeedback(null);
answerSubmittingRef.current = false; answerSubmittingRef.current = false;
if (testType === 'audio-to-english') { if (testType === 'audio-to-english') {
setTimeout(() => { setTimeout(() => {
@@ -588,31 +547,14 @@ const VocabularyStudy: React.FC = () => {
} }
} else { } else {
answerSubmittingRef.current = false; answerSubmittingRef.current = false;
finishTestWithResults(updatedPendingAnswers, updatedTestResults); finishTestWithResults(updatedPendingAnswers);
} }
}, 1500); }, 1500);
}; };
// 使用指定结果完成测试 // 使用指定结果完成测试
const finishTestWithResults = async (finalAnswers: PendingAnswer[], finalResults: TestResult[]) => { const finishTestWithResults = async (finalAnswers: PendingAnswer[]) => {
try { try {
const lastSubmittedAtMs = finalAnswers.length > 0
? Math.max(...finalAnswers.map(item => item.submittedAt))
: Date.now();
const localCorrectWords = finalResults.filter(result => result.isCorrect).length;
const localStats = {
totalWords: finalResults.length,
correctWords: localCorrectWords,
accuracy: finalResults.length > 0 ? (localCorrectWords / finalResults.length) * 100 : 0,
startTime: studyStats.startTime,
endTime: new Date(lastSubmittedAtMs),
duration: Math.max(0, (lastSubmittedAtMs - studyStats.startTime.getTime()) / 1000)
};
setTestResults(finalResults);
setStudyStats(localStats);
setTestFinished(true);
// 添加调试信息 // 添加调试信息
console.log('测试完成状态(带结果):', { console.log('测试完成状态(带结果):', {
finalResultsLength: finalAnswers.length, finalResultsLength: finalAnswers.length,
@@ -640,8 +582,11 @@ const VocabularyStudy: React.FC = () => {
endTime: new Date(savedRecord.stats.endTime), endTime: new Date(savedRecord.stats.endTime),
duration: savedRecord.stats.duration duration: savedRecord.stats.duration
}); });
setTestResults(savedRecord.results);
setPendingAnswers([]); setPendingAnswers([]);
setTestAttemptId(''); setTestAttemptId('');
setShowAnswer(false);
setTestFinished(true);
message.success('测试完成,记录已保存'); message.success('测试完成,记录已保存');
setIsModalVisible(true); setIsModalVisible(true);
@@ -657,7 +602,7 @@ const VocabularyStudy: React.FC = () => {
// 完成测试 - 兼容原来的调用方式 // 完成测试 - 兼容原来的调用方式
const finishTest = async () => { const finishTest = async () => {
// 使用当前的测试结果完成测试 // 使用当前的测试结果完成测试
await finishTestWithResults([...pendingAnswers], [...testResults]); await finishTestWithResults([...pendingAnswers]);
}; };
// 重新开始测试 // 重新开始测试
@@ -670,7 +615,6 @@ const VocabularyStudy: React.FC = () => {
setUserAnswer(''); setUserAnswer('');
setShowAnswer(false); setShowAnswer(false);
setTestResults([]); setTestResults([]);
setAnswerFeedback(null);
setPendingAnswers([]); setPendingAnswers([]);
setOptions([]); setOptions([]);
answerSubmittingRef.current = false; answerSubmittingRef.current = false;
@@ -1271,24 +1215,18 @@ const VocabularyStudy: React.FC = () => {
{showAnswer && ( {showAnswer && (
<div style={{ <div style={{
padding: 15, padding: 15,
backgroundColor: answerFeedback?.isCorrect ? '#f6ffed' : '#fff2f0', backgroundColor: '#f6f8fa',
borderRadius: 4, borderRadius: 4,
marginBottom: 15, marginBottom: 15,
border: `1px solid ${answerFeedback?.isCorrect ? '#b7eb8f' : '#ffccc7'}` border: '1px solid #d9d9d9'
}}> }}>
<div style={{ <div style={{
fontWeight: 'bold', fontWeight: 'bold',
color: answerFeedback?.isCorrect ? '#389e0d' : '#cf1322', color: '#1677ff',
marginBottom: 5 marginBottom: 5
}}> }}>
{answerFeedback?.isCorrect ? '回答正确' : '回答错误'}
</div> </div>
{answerFeedback && (
<>
<div>: {answerFeedback.correctAnswer}</div>
<div>: {answerFeedback.userAnswer || '(空)'}</div>
</>
)}
<div>...</div> <div>...</div>
</div> </div>
)} )}