feat: add student learning flow pages

This commit is contained in:
Codex
2026-06-29 13:03:02 +08:00
parent 1bd608f134
commit 2776ce12a5
23 changed files with 681 additions and 18 deletions

View File

@@ -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<string[]>([]);
const [answerText, setAnswerText] = useState('');
const [feedbackText, setFeedbackText] = useState('');
const [resultByQuestion, setResultByQuestion] = useState<Record<string, boolean | null>>({});
const [report, setReport] = useState<PracticeReport | null>(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 (
<View className='student-page'>
<View className='student-topbar'>
<View className='student-title-block'>
<Text className='student-kicker'>{session ? `${index + 1}/${questions.length || session.questionCount}` : 'Practice'}</Text>
<Text className='student-title'>{session?.mode || '练习'}</Text>
<Text className='student-subtitle'> session 访</Text>
<Text className='student-subtitle'> session </Text>
</View>
</View>
{report ? (
<View className='report-panel section-block'>
<Text className='metric-value'>{String(report.score)} / {String(report.totalScore)}</Text>
<Text className='metric-label'> {Math.round((report.accuracy || 0) * 100)}% · {report.answeredCount}/{report.totalQuestions}</Text>
<View className='toolbar'>
<Button className='primary-button' onClick={openReport}></Button>
</View>
</View>
) : null}
{current ? (
<View className='list-stack'>
<View className='quiet-panel'>
@@ -116,7 +191,16 @@ export default function StudentPracticePage() {
<View className='toolbar'>
<Button className='primary-button' onClick={handleSubmit}></Button>
<Button className='secondary-button' onClick={handleFavorite}></Button>
<Button className='secondary-button' onClick={openVideo}></Button>
<Button className='secondary-button' onClick={nextQuestion}></Button>
<Button className='secondary-button' onClick={handleSubmitSession}></Button>
</View>
<View className='quiet-panel'>
<Text className='row-main'></Text>
<Textarea className='textarea' placeholder='题干、答案、解析或视频存在问题,可以在这里提交给后台处理。' value={feedbackText} onInput={event => setFeedbackText(String(event.detail.value || ''))} />
<View className='toolbar'>
<Button className='secondary-button' onClick={handleFeedback}></Button>
</View>
</View>
</View>
) : <View className='empty-state'></View>}