From 2776ce12a55748601b04bca46c1320a481ec4070 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 29 Jun 2026 13:03:02 +0800 Subject: [PATCH] feat: add student learning flow pages --- apps/api/src/features/catalog/routes.ts | 14 +++ apps/taro/README.md | 8 +- apps/taro/src/app.config.ts | 4 + apps/taro/src/pages/student/home/index.tsx | 3 + .../taro/src/pages/student/practice/index.tsx | 90 +++++++++++++++- apps/taro/src/pages/student/profile/index.tsx | 6 ++ .../src/pages/student/region/index.config.ts | 3 + apps/taro/src/pages/student/region/index.tsx | 69 ++++++++++++ .../src/pages/student/reports/index.config.ts | 3 + apps/taro/src/pages/student/reports/index.tsx | 85 +++++++++++++++ .../src/pages/student/review/index.config.ts | 3 + apps/taro/src/pages/student/review/index.tsx | 102 ++++++++++++++++++ apps/taro/src/pages/student/student.css | 31 ++++++ .../src/pages/student/video/index.config.ts | 3 + apps/taro/src/pages/student/video/index.tsx | 74 +++++++++++++ apps/taro/src/services/catalog.ts | 4 +- apps/taro/src/services/learning.ts | 95 ++++++++++++++++ apps/taro/src/services/profile.ts | 33 ++++++ apps/taro/src/services/video.ts | 38 +++++++ docs/refactor/backend-handoff-roadmap.md | 4 +- docs/refactor/frontend-handoff-index.md | 12 ++- docs/refactor/next-development-todo.md | 4 +- docs/refactor/taro-frontend-integration.md | 11 +- 23 files changed, 681 insertions(+), 18 deletions(-) create mode 100644 apps/taro/src/pages/student/region/index.config.ts create mode 100644 apps/taro/src/pages/student/region/index.tsx create mode 100644 apps/taro/src/pages/student/reports/index.config.ts create mode 100644 apps/taro/src/pages/student/reports/index.tsx create mode 100644 apps/taro/src/pages/student/review/index.config.ts create mode 100644 apps/taro/src/pages/student/review/index.tsx create mode 100644 apps/taro/src/pages/student/video/index.config.ts create mode 100644 apps/taro/src/pages/student/video/index.tsx create mode 100644 apps/taro/src/services/video.ts diff --git a/apps/api/src/features/catalog/routes.ts b/apps/api/src/features/catalog/routes.ts index 7070c2e7..64b1af8f 100644 --- a/apps/api/src/features/catalog/routes.ts +++ b/apps/api/src/features/catalog/routes.ts @@ -2,6 +2,15 @@ import type { RequestContext } from '../../core/http.js'; import { query } from '../../core/db.js'; import { intParam, tenantIdFrom } from '../../core/request.js'; +function stringArrayParam(ctx: RequestContext, name: string, max = 200) { + const repeated = ctx.url.searchParams.getAll(name); + const csv = ctx.url.searchParams.get(name); + const values = [...repeated, ...(csv ? csv.split(',') : [])] + .map(value => value.trim()) + .filter(Boolean); + return [...new Set(values)].slice(0, max); +} + function dateOnly(value: unknown) { if (!value) return null; if (value instanceof Date && !Number.isNaN(value.getTime())) return value.toISOString().slice(0, 10); @@ -245,10 +254,15 @@ export async function questionsRoute(ctx: RequestContext) { const entryId = ctx.url.searchParams.get('entryId'); const contentNodeId = ctx.url.searchParams.get('contentNodeId'); const collectionId = ctx.url.searchParams.get('collectionId'); + const questionIds = stringArrayParam(ctx, 'questionIds', 300); const limit = intParam(ctx, 'limit', 200, 500); const params: unknown[] = [tenantId]; const filters = ['q.tenant_id = $1', `q.status = 'published'`]; + if (questionIds.length) { + params.push(questionIds); + filters.push(`q.id = any($${params.length}::uuid[])`); + } if (subjectId) { params.push(subjectId); filters.push(`q.subject_id = $${params.length}`); diff --git a/apps/taro/README.md b/apps/taro/README.md index d35aea23..e3d0b668 100644 --- a/apps/taro/README.md +++ b/apps/taro/README.md @@ -28,8 +28,12 @@ apps/taro/dist/h5-platform-admin ```text pages/student/login/index 短信登录 pages/student/home/index 首页与功能入口 +pages/student/region/index 地区选择、目标地区保存 pages/student/catalog/index 题库入口、分类、集合、练习蓝图 -pages/student/practice/index 创建练习 session、答题、收藏 +pages/student/practice/index 创建练习 session、答题、收藏、反馈、视频入口、交卷报告 +pages/student/review/index 错题本、收藏夹、错题/收藏复习 +pages/student/reports/index 练习报告、模考报告、历史报告 +pages/student/video/index 题目视频解析、播放签名 pages/student/vocabulary/index 单词单元、复习计划、认识/再记、收藏 pages/student/handbook/index 手册科目、章节、知识点阅读 pages/student/scoreline/index 分数线列表 @@ -37,7 +41,7 @@ pages/student/assets/index 资料列表、预览签名、下载签名 pages/student/profile/index 个人中心、会员、订单、签到、激活码、勋章 ``` -这些页面是联调骨架,不是最终视觉稿。后续应继续参照旧题库样式完善刷题交互、视频解析、题目反馈、模考报告、错题复习、订单收银台和小程序兼容。 +这些页面是联调骨架,不是最终视觉稿。当前学生端已覆盖地区选择、刷题、题目反馈、视频解析、交卷报告、错题本和收藏夹第一版;后续应继续参照旧题库样式完善刷题细节、收银台、订单详情、售后入口和小程序兼容。 ## 当前租户后台页面 diff --git a/apps/taro/src/app.config.ts b/apps/taro/src/app.config.ts index 13e14e8f..3a76a42e 100644 --- a/apps/taro/src/app.config.ts +++ b/apps/taro/src/app.config.ts @@ -3,8 +3,12 @@ export default defineAppConfig({ 'pages/bootstrap/index', 'pages/student/login/index', 'pages/student/home/index', + 'pages/student/region/index', 'pages/student/catalog/index', 'pages/student/practice/index', + 'pages/student/review/index', + 'pages/student/reports/index', + 'pages/student/video/index', 'pages/student/vocabulary/index', 'pages/student/handbook/index', 'pages/student/scoreline/index', diff --git a/apps/taro/src/pages/student/home/index.tsx b/apps/taro/src/pages/student/home/index.tsx index 56b57f73..763e7bdc 100644 --- a/apps/taro/src/pages/student/home/index.tsx +++ b/apps/taro/src/pages/student/home/index.tsx @@ -19,7 +19,10 @@ export default function StudentHomePage() { const entryNames = useMemo(() => (snapshot?.entries || []).slice(0, 6), [snapshot]); const brandName = tenant?.branding.brandName || tenant?.branding.shortName || '工学题库'; const quickActions = [ + { name: '选地区', path: '/pages/student/region/index', meta: '目标地区 / 权益' }, { name: '刷题', path: '/pages/student/catalog/index', meta: '顺序 / 随机 / 模考' }, + { name: '错题收藏', path: '/pages/student/review/index', meta: '错题 / 收藏复习' }, + { name: '练习报告', path: '/pages/student/reports/index', meta: '模考 / 历史' }, { name: '背单词', path: '/pages/student/vocabulary/index', meta: '复习计划' }, { name: '知识手册', path: '/pages/student/handbook/index', meta: '章节阅读' }, { name: '分数线', path: '/pages/student/scoreline/index', meta: '院校趋势' }, diff --git a/apps/taro/src/pages/student/practice/index.tsx b/apps/taro/src/pages/student/practice/index.tsx index 46988113..4fd41154 100644 --- a/apps/taro/src/pages/student/practice/index.tsx +++ b/apps/taro/src/pages/student/practice/index.tsx @@ -2,7 +2,16 @@ import { useEffect, useMemo, useState } from 'react'; import Taro, { useRouter } from '@tarojs/taro'; import { Button, Text, Textarea, View } from '@tarojs/components'; import { loadCollectionQuestions, loadQuestions } from '@/services/catalog'; -import { createPracticeSession, submitAnswer, toggleQuestionFavorite, type PracticeSession, type QuestionItem } from '@/services/learning'; +import { + createPracticeSession, + submitAnswer, + submitPracticeSession, + toggleQuestionFavorite, + type PracticeReport, + type PracticeSession, + type QuestionItem, +} from '@/services/learning'; +import { submitFeedback } from '@/services/profile'; import '../student.css'; function plainOption(option: unknown, index: number) { @@ -22,7 +31,9 @@ export default function StudentPracticePage() { const [index, setIndex] = useState(0); const [selected, setSelected] = useState([]); const [answerText, setAnswerText] = useState(''); + const [feedbackText, setFeedbackText] = useState(''); const [resultByQuestion, setResultByQuestion] = useState>({}); + const [report, setReport] = useState(null); const [error, setError] = useState(''); useEffect(() => { @@ -41,7 +52,12 @@ export default function StudentPracticePage() { setSession(nextSession); const questionPayload = collectionId ? await loadCollectionQuestions(collectionId, 300) - : await loadQuestions({ entryId: body.entryId, contentNodeId: body.contentNodeId, limit: 300 }); + : await loadQuestions({ + entryId: body.entryId, + contentNodeId: body.contentNodeId, + questionIds: nextSession.questionIds, + limit: Math.max(nextSession.questionIds?.length || 0, 50), + }); const byId = new Map((questionPayload.items || []).map(item => [item.id, item])); const ordered = (nextSession.questionIds || []).map(id => byId.get(id)).filter(Boolean) as QuestionItem[]; setQuestions(ordered.length ? ordered : questionPayload.items || []); @@ -76,6 +92,7 @@ export default function StudentPracticePage() { function nextQuestion() { setSelected([]); setAnswerText(''); + setFeedbackText(''); setIndex(prev => Math.min(questions.length - 1, prev + 1)); } @@ -84,16 +101,74 @@ export default function StudentPracticePage() { await toggleQuestionFavorite(current.id, true).catch(nextError => setError(nextError instanceof Error ? nextError.message : '收藏失败')); } + async function handleFeedback() { + if (!current || !feedbackText.trim()) { + setError('请先填写反馈内容。'); + return; + } + try { + await submitFeedback({ + questionId: current.id, + type: 'question_error', + title: '题目反馈', + description: feedbackText.trim(), + priority: 'normal', + }); + setFeedbackText(''); + Taro.showToast({ title: '已提交反馈', icon: 'success' }); + } catch (nextError) { + setError(nextError instanceof Error ? nextError.message : '反馈提交失败'); + } + } + + async function handleSubmitSession() { + if (!session) return; + const ok = await Taro.showModal({ + title: '交卷', + content: '确认交卷并生成练习报告?报告会按后端 session 快照评分。', + confirmText: '交卷', + cancelText: '取消', + }); + if (!ok.confirm) return; + try { + const payload = await submitPracticeSession(session.id); + setReport(payload.item || null); + Taro.showToast({ title: '报告已生成', icon: 'success' }); + } catch (nextError) { + setError(nextError instanceof Error ? nextError.message : '交卷失败'); + } + } + + function openReport() { + if (!session) return; + Taro.navigateTo({ url: `/pages/student/reports/index?practiceSessionId=${session.id}` }); + } + + function openVideo() { + if (!current) return; + Taro.navigateTo({ url: `/pages/student/video/index?questionId=${current.id}` }); + } + return ( {session ? `${index + 1}/${questions.length || session.questionCount}` : 'Practice'} {session?.mode || '练习'} - 题目来自后端 session 快照,答题提交会校验当前用户是否拥有该题访问权限。 + 题目来自后端 session 快照,答题、交卷、错题和报告都以后端校验为准。 + {report ? ( + + {String(report.score)} / {String(report.totalScore)} + 正确率 {Math.round((report.accuracy || 0) * 100)}% · 已答 {report.answeredCount}/{report.totalQuestions} + + + + + ) : null} + {current ? ( @@ -116,7 +191,16 @@ export default function StudentPracticePage() { + + + + + 题目反馈 +