diff --git a/api/models/examTask.ts b/api/models/examTask.ts index 73d1429..58a2367 100644 --- a/api/models/examTask.ts +++ b/api/models/examTask.ts @@ -582,7 +582,6 @@ export class ExamTaskModel { } static async getUserTasks(userId: string): Promise { - const now = new Date().toISOString(); const rows = await all(` SELECT t.id, @@ -606,9 +605,9 @@ export class ExamTaskModel { WHERE user_id = ? GROUP BY task_id ) q ON q.task_id = t.id - WHERE tu.user_id = ? AND t.start_at <= ? AND t.end_at >= ? + WHERE tu.user_id = ? ORDER BY t.start_at ASC, t.end_at ASC - `, [userId, userId, now, now]); + `, [userId, userId]); return rows.map(row => ({ id: row.id, diff --git a/data/survey.db b/data/survey.db index 0a93c87..7b37a2b 100644 Binary files a/data/survey.db and b/data/survey.db differ diff --git a/package.json b/package.json index a3e3854..bd745a5 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "build": "tsc && vite build", "preview": "vite preview", "start": "node dist/api/server.js", - "test": "node --import tsx --test test/admin-task-stats.test.ts test/question-text-import.test.ts test/swipe-detect.test.ts", + "test": "node --import tsx --test test/admin-task-stats.test.ts test/question-text-import.test.ts test/swipe-detect.test.ts test/user-tasks.test.ts", "check": "tsc --noEmit" }, "dependencies": { diff --git a/src/pages/QuizPage.tsx b/src/pages/QuizPage.tsx index f353732..ebb5471 100644 --- a/src/pages/QuizPage.tsx +++ b/src/pages/QuizPage.tsx @@ -347,11 +347,17 @@ const QuizPage = () => { subjectId: subjectId || undefined, taskId: taskId || undefined, answers: answersData - }); + }) as any; + + const payload = response?.data ?? response; + const recordId = payload?.recordId; + if (!recordId) { + throw new Error('提交成功但未返回记录ID'); + } message.success('答题提交成功!'); clearActiveProgress(user!.id); - navigate(`/result/${response.data.recordId}`); + navigate(`/result/${recordId}`); } catch (error: any) { message.error(error.message || '提交失败'); } finally { diff --git a/src/pages/ResultPage.tsx b/src/pages/ResultPage.tsx index 6629232..d8aaeca 100644 --- a/src/pages/ResultPage.tsx +++ b/src/pages/ResultPage.tsx @@ -32,6 +32,55 @@ interface QuizAnswer { questionAnalysis?: string; } +const StatusIcon = ({ status }: { status: QuizRecord['status'] }) => { + const colorClass = + status === '优秀' ? 'text-green-500' : status === '合格' ? 'text-blue-500' : 'text-red-500'; + + const renderInner = () => { + if (status === '优秀') { + // 五角星(实心) + return ( + + ); + } + + if (status === '合格') { + // 对号(空心) + return ; + } + + // 感叹号(空心) + return ( + <> + + + + ); + }; + + return ( + + ); +}; + const ResultPage = () => { const { id: recordId } = useParams<{ id: string }>(); const navigate = useNavigate(); @@ -53,12 +102,14 @@ const ResultPage = () => { const fetchResultDetail = async () => { try { setLoading(true); - const response = await quizAPI.getRecordDetail(recordId!); - setRecord(response.record); - setAnswers(response.answers); + const response = await quizAPI.getRecordDetail(recordId!) as any; + const payload = response?.data ?? response; + setRecord(payload?.record ?? null); + setAnswers(Array.isArray(payload?.answers) ? payload.answers : []); } catch (error: any) { message.error(error.message || '获取答题结果失败'); - navigate('/'); + setRecord(null); + setAnswers([]); } finally { setLoading(false); } @@ -203,19 +254,7 @@ const ResultPage = () => {
- {record.status === '优秀' ? ( - - ) : record.status === '合格' ? ( - - ) : ( - - )} +
diff --git a/src/pages/SubjectSelectionPage.tsx b/src/pages/SubjectSelectionPage.tsx index 655cead..12c715d 100644 --- a/src/pages/SubjectSelectionPage.tsx +++ b/src/pages/SubjectSelectionPage.tsx @@ -66,19 +66,21 @@ export const SubjectSelectionPage: React.FC = () => { }; const getTaskStatus = (task: ExamTask) => { - const now = new Date(); - const startAt = new Date(task.startAt); - const endAt = new Date(task.endAt); + const nowMs = Date.now(); + const startMs = new Date(task.startAt).getTime(); + const endMs = new Date(task.endAt).getTime(); const usedAttempts = Number(task.usedAttempts) || 0; const maxAttempts = Number(task.maxAttempts) || 3; - if (now < startAt) { - return 'notStarted'; - } else if (now >= startAt && now <= endAt && usedAttempts < maxAttempts) { - return 'ongoing'; - } else { + // 日期解析失败时,按“已完成/不可开始”处理,避免误分组 + if (!Number.isFinite(startMs) || !Number.isFinite(endMs)) { return 'completed'; } + + if (nowMs < startMs) return 'notStarted'; + if (nowMs > endMs) return 'completed'; + if (usedAttempts >= maxAttempts) return 'completed'; + return 'ongoing'; }; const getTasksByStatus = (status: 'ongoing' | 'completed' | 'notStarted') => { @@ -163,7 +165,10 @@ export const SubjectSelectionPage: React.FC = () => {
- 我的考试任务 +
+ 我的考试任务 + {user && 欢迎,{user.name}} +
diff --git a/src/services/api.ts b/src/services/api.ts index 7a4cd1f..e3926b2 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -1,14 +1,22 @@ -import axios from 'axios'; +import axios, { AxiosInstance, AxiosResponse } from 'axios'; const API_BASE_URL = '/api'; -const api = axios.create({ +// 创建自定义Axios实例类型,考虑响应拦截器的行为 +type CustomAxiosInstance = AxiosInstance & { + post(url: string, data?: any, config?: any): Promise; + get(url: string, config?: any): Promise; + put(url: string, data?: any, config?: any): Promise; + delete(url: string, config?: any): Promise; +}; + +const api: CustomAxiosInstance = axios.create({ baseURL: API_BASE_URL, timeout: 30000, // 增加超时时间到30秒 headers: { 'Content-Type': 'application/json', }, -}); +}) as unknown as CustomAxiosInstance; // 请求拦截器 api.interceptors.request.use( @@ -95,14 +103,15 @@ export const questionAPI = { // 答题相关API export const quizAPI = { generateQuiz: (userId: string, subjectId?: string, taskId?: string) => - api.post('/quiz/generate', { userId, subjectId, taskId }), + api.post<{ questions: any[]; totalScore: number; timeLimit: number }>('/quiz/generate', { userId, subjectId, taskId }), submitQuiz: (data: { userId: string; subjectId?: string; taskId?: string; answers: any[] }) => - api.post('/quiz/submit', data), + api.post<{ recordId: string; totalScore: number; correctCount: number; totalCount: number }>('/quiz/submit', data), getUserRecords: (userId: string, params?: { page?: number; limit?: number }) => - api.get(`/quiz/records/${userId}`, { params }), - getRecordDetail: (recordId: string) => api.get(`/quiz/records/detail/${recordId}`), + api.get(`/quiz/records/${userId}`, { params }), + getRecordDetail: (recordId: string) => + api.get<{ record: any; answers: any[] }>(`/quiz/records/detail/${recordId}`), getAllRecords: (params?: { page?: number; limit?: number }) => - api.get('/quiz/records', { params }), + api.get(`/quiz/records`, { params }), }; export const examSubjectAPI = { diff --git a/test/admin-task-stats.test.ts b/test/admin-task-stats.test.ts index bf0602b..064a043 100644 --- a/test/admin-task-stats.test.ts +++ b/test/admin-task-stats.test.ts @@ -288,10 +288,11 @@ test('管理员任务分页统计接口返回结构正确', async () => { assert.equal(userTasks.status, 200); assert.equal(userTasks.json?.success, true); assert.ok(Array.isArray(userTasks.json?.data)); - assert.equal(userTasks.json?.data?.[0]?.id, activeTaskId); - assert.equal(userTasks.json?.data?.[0]?.usedAttempts, 3); - assert.equal(userTasks.json?.data?.[0]?.maxAttempts, 3); - assert.equal(userTasks.json?.data?.[0]?.bestScore, 30); + const activeTaskRow = (userTasks.json?.data as any[]).find((t) => t.id === activeTaskId); + assert.ok(activeTaskRow); + assert.equal(activeTaskRow.usedAttempts, 3); + assert.equal(activeTaskRow.maxAttempts, 3); + assert.equal(activeTaskRow.bestScore, 30); const fourthGenerate = await jsonFetch(baseUrl, '/api/quiz/generate', { method: 'POST', diff --git a/test/user-tasks.test.ts b/test/user-tasks.test.ts new file mode 100644 index 0000000..4716668 --- /dev/null +++ b/test/user-tasks.test.ts @@ -0,0 +1,119 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { randomUUID } from 'node:crypto'; + +process.env.NODE_ENV = 'test'; +process.env.DB_PATH = ':memory:'; + +const jsonFetch = async ( + baseUrl: string, + path: string, + options?: { method?: string; body?: unknown }, +) => { + const res = await fetch(`${baseUrl}${path}`, { + method: options?.method ?? 'GET', + headers: options?.body ? { 'Content-Type': 'application/json' } : undefined, + body: options?.body ? JSON.stringify(options.body) : undefined, + }); + + const text = await res.text(); + let json: any = null; + try { + json = text ? JSON.parse(text) : null; + } catch { + json = null; + } + return { status: res.status, json, text }; +}; + +test('用户任务接口返回未开始/进行中/已结束任务(由前端分组)', async () => { + const { initDatabase, run } = await import('../api/database'); + await initDatabase(); + + const { app } = await import('../api/server'); + const server = app.listen(0); + + try { + const addr = server.address(); + assert.ok(addr && typeof addr === 'object'); + const baseUrl = `http://127.0.0.1:${addr.port}`; + + const now = Date.now(); + const userId = randomUUID(); + const subjectId = randomUUID(); + + const historyTaskId = randomUUID(); + const upcomingTaskId = randomUUID(); + const activeTaskId = randomUUID(); + + await run(`INSERT INTO users (id, name, phone, password) VALUES (?, ?, ?, ?)`, [ + userId, + '测试用户', + '13800138099', + '', + ]); + + await run( + `INSERT INTO exam_subjects (id, name, type_ratios, category_ratios, total_score, duration_minutes) + VALUES (?, ?, ?, ?, ?, ?)`, + [ + subjectId, + '测试科目', + JSON.stringify({ single: 100 }), + JSON.stringify({ 通用: 100 }), + 100, + 60, + ], + ); + + const historyStartAt = new Date(now - 3 * 24 * 60 * 60 * 1000).toISOString(); + const historyEndAt = new Date(now - 2 * 24 * 60 * 60 * 1000).toISOString(); + const upcomingStartAt = new Date(now + 2 * 24 * 60 * 60 * 1000).toISOString(); + const upcomingEndAt = new Date(now + 3 * 24 * 60 * 60 * 1000).toISOString(); + const activeStartAt = new Date(now - 1 * 24 * 60 * 60 * 1000).toISOString(); + const activeEndAt = new Date(now + 1 * 24 * 60 * 60 * 1000).toISOString(); + + await run( + `INSERT INTO exam_tasks (id, name, subject_id, start_at, end_at, selection_config) + VALUES (?, ?, ?, ?, ?, ?)`, + [historyTaskId, '历史任务', subjectId, historyStartAt, historyEndAt, null], + ); + await run( + `INSERT INTO exam_tasks (id, name, subject_id, start_at, end_at, selection_config) + VALUES (?, ?, ?, ?, ?, ?)`, + [upcomingTaskId, '未开始任务', subjectId, upcomingStartAt, upcomingEndAt, null], + ); + await run( + `INSERT INTO exam_tasks (id, name, subject_id, start_at, end_at, selection_config) + VALUES (?, ?, ?, ?, ?, ?)`, + [activeTaskId, '进行中任务', subjectId, activeStartAt, activeEndAt, null], + ); + + const linkUserToTask = async (taskId: string) => { + await run(`INSERT INTO exam_task_users (id, task_id, user_id) VALUES (?, ?, ?)`, [ + randomUUID(), + taskId, + userId, + ]); + }; + + await linkUserToTask(historyTaskId); + await linkUserToTask(upcomingTaskId); + await linkUserToTask(activeTaskId); + + const res = await jsonFetch(baseUrl, `/api/exam-tasks/user/${userId}`); + assert.equal(res.status, 200); + assert.equal(res.json?.success, true); + assert.ok(Array.isArray(res.json?.data)); + + const names = (res.json?.data as any[]).map((t) => t.name).sort(); + assert.deepEqual(names, ['历史任务', '未开始任务', '进行中任务'].sort()); + + const upcoming = (res.json?.data as any[]).find((t) => t.name === '未开始任务'); + assert.ok(upcoming); + assert.equal(typeof upcoming.startAt, 'string'); + assert.equal(typeof upcoming.endAt, 'string'); + } finally { + server.close(); + } +});