forked from wangziqi/gongxue-base
feat: add student learning flow pages
This commit is contained in:
@@ -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>}
|
||||
|
||||
Reference in New Issue
Block a user