forked from wangziqi/gongxue-base
feat: add safe rich content rendering for student flows
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Text, View } from '@tarojs/components';
|
||||
import RichContent from '@/components/RichContent';
|
||||
import { loadHandbookChapters, loadHandbookEntries, loadHandbookSubjects, type HandbookChapter, type HandbookEntry, type HandbookSubject } from '@/services/catalog';
|
||||
import '../student.css';
|
||||
|
||||
@@ -79,7 +80,10 @@ export default function StudentHandbookPage() {
|
||||
{entries.map(item => (
|
||||
<View className='list-row' key={item.id}>
|
||||
<Text className='row-main'>{item.title}</Text>
|
||||
<Text className='row-meta'>{item.summary || item.content || '暂无内容'}</Text>
|
||||
{item.summary ? <RichContent content={item.summary} compact muted /> : null}
|
||||
<View className='handbook-entry-body'>
|
||||
<RichContent content={item.content || '暂无内容'} compact />
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import Taro, { useRouter } from '@tarojs/taro';
|
||||
import { Button, Text, Textarea, View } from '@tarojs/components';
|
||||
import RichContent from '@/components/RichContent';
|
||||
import { loadCollectionQuestions, loadQuestions } from '@/services/catalog';
|
||||
import {
|
||||
createPracticeSession,
|
||||
@@ -527,8 +528,7 @@ export default function StudentPracticePage() {
|
||||
<Text className='row-meta'>{current.typeLabel || current.type || '题目'}</Text>
|
||||
<Text className='status-badge'>{answerLabel(answerState)}</Text>
|
||||
</View>
|
||||
<Text className='row-main'>{current.content || '未提供题干'}</Text>
|
||||
{current.mediaUrl ? <Text className='row-meta break-text'>{current.mediaUrl}</Text> : null}
|
||||
<RichContent content={current.content || '未提供题干'} mediaUrls={[current.mediaUrl]} />
|
||||
{hasCompositeSubQuestions ? <Text className='row-meta'>包含 {subQuestions.length} 个子题,请结合题干作答。</Text> : null}
|
||||
</View>
|
||||
{hasCompositeSubQuestions ? (
|
||||
@@ -545,7 +545,7 @@ export default function StudentPracticePage() {
|
||||
<Text className='row-meta'>第 {subQuestionIndex + 1} 小题 · {String(subQuestion.typeLabel || subQuestion.type || '子题')}</Text>
|
||||
{result ? <Text className='status-badge'>{result.isCorrect === true ? '正确' : result.isCorrect === false ? '错误' : '已答'}</Text> : null}
|
||||
</View>
|
||||
<Text className='row-main'>{String(subQuestion.content || '未提供子题题干')}</Text>
|
||||
<RichContent content={String(subQuestion.content || '未提供子题题干')} compact />
|
||||
{objective ? (
|
||||
<View className='sub-option-list'>
|
||||
{subOptions.map((option, optionIndex) => (
|
||||
@@ -554,7 +554,10 @@ export default function StudentPracticePage() {
|
||||
key={`${id}-${optionIndex}`}
|
||||
onClick={() => toggleSubOption(subQuestion, subQuestionIndex, optionIndex)}
|
||||
>
|
||||
<Text className='row-main'>{String.fromCharCode(65 + optionIndex)}. {plainOption(option, optionIndex)}</Text>
|
||||
<View className='option-line'>
|
||||
<Text className='option-prefix'>{String.fromCharCode(65 + optionIndex)}.</Text>
|
||||
<RichContent className='option-rich' content={plainOption(option, optionIndex)} compact />
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
@@ -579,8 +582,18 @@ export default function StudentPracticePage() {
|
||||
<View className='sub-explanation'>
|
||||
{result?.correctOptionIndices?.length ? <Text className='row-meta'>正确选项:{result.correctOptionIndices.map(item => String.fromCharCode(65 + item)).join('、')}</Text> : null}
|
||||
{result?.correctOptionIndex !== null && result?.correctOptionIndex !== undefined ? <Text className='row-meta'>正确选项:{String.fromCharCode(65 + result.correctOptionIndex)}</Text> : null}
|
||||
{result?.correctAnswerText ? <Text className='row-meta'>参考答案:{result.correctAnswerText}</Text> : null}
|
||||
{result?.explanation ? <Text className='row-meta'>{result.explanation}</Text> : null}
|
||||
{result?.correctAnswerText ? (
|
||||
<View className='answer-detail'>
|
||||
<Text className='answer-detail-label'>参考答案</Text>
|
||||
<RichContent content={result.correctAnswerText} compact muted />
|
||||
</View>
|
||||
) : null}
|
||||
{result?.explanation ? (
|
||||
<View className='answer-detail'>
|
||||
<Text className='answer-detail-label'>解析</Text>
|
||||
<RichContent content={result.explanation} compact muted />
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
@@ -593,7 +606,10 @@ export default function StudentPracticePage() {
|
||||
key={String(optionIndex)}
|
||||
onClick={() => toggleOption(optionIndex)}
|
||||
>
|
||||
<Text className='row-main'>{String.fromCharCode(65 + optionIndex)}. {plainOption(option, optionIndex)}</Text>
|
||||
<View className='option-line'>
|
||||
<Text className='option-prefix'>{String.fromCharCode(65 + optionIndex)}.</Text>
|
||||
<RichContent className='option-rich' content={plainOption(option, optionIndex)} compact />
|
||||
</View>
|
||||
</View>
|
||||
)) : (
|
||||
<View className='quiet-panel'>
|
||||
@@ -609,10 +625,20 @@ export default function StudentPracticePage() {
|
||||
<View className='quiet-panel'>
|
||||
{isSubmitted ? <Text className={answerState?.isCorrect ? 'success-text' : answerState?.isCorrect === false ? 'error-text' : 'row-meta'}>{answerLabel(answerState)}</Text> : null}
|
||||
{answerState?.subResults?.length ? <Text className='row-meta'>子题正确 {answerState.subResults.filter(item => item.isCorrect === true).length}/{answerState.subResults.length}</Text> : null}
|
||||
{current.answerText ? <Text className='row-main'>参考答案:{current.answerText}</Text> : null}
|
||||
{current.answerText ? (
|
||||
<View className='answer-detail'>
|
||||
<Text className='answer-detail-label'>参考答案</Text>
|
||||
<RichContent content={current.answerText} compact />
|
||||
</View>
|
||||
) : null}
|
||||
{current.correctOptionIndices?.length ? <Text className='row-meta'>正确选项:{current.correctOptionIndices.map(item => String.fromCharCode(65 + item)).join('、')}</Text> : null}
|
||||
{current.correctOptionIndex !== null && current.correctOptionIndex !== undefined ? <Text className='row-meta'>正确选项:{String.fromCharCode(65 + current.correctOptionIndex)}</Text> : null}
|
||||
{current.explanation ? <Text className='row-meta'>{current.explanation}</Text> : null}
|
||||
{current.explanation ? (
|
||||
<View className='answer-detail'>
|
||||
<Text className='answer-detail-label'>解析</Text>
|
||||
<RichContent content={current.explanation} compact muted />
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
) : null}
|
||||
<View className='toolbar wrap'>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import Taro, { useRouter } from '@tarojs/taro';
|
||||
import { Button, Text, View } from '@tarojs/components';
|
||||
import RichContent from '@/components/RichContent';
|
||||
import {
|
||||
loadPracticeHistory,
|
||||
loadPracticeReports,
|
||||
@@ -14,6 +15,28 @@ function percent(value?: number) {
|
||||
return `${Math.round((value || 0) * 100)}%`;
|
||||
}
|
||||
|
||||
function stringValue(source: Record<string, unknown>, key: string) {
|
||||
const value = source[key];
|
||||
return typeof value === 'string' ? value : '';
|
||||
}
|
||||
|
||||
function numberArray(source: Record<string, unknown>, key: string) {
|
||||
const value = source[key];
|
||||
return Array.isArray(value) ? value.map(item => Number(item)).filter(Number.isFinite) : [];
|
||||
}
|
||||
|
||||
function answerChoiceLabel(value: unknown) {
|
||||
const num = Number(value);
|
||||
return Number.isFinite(num) ? String.fromCharCode(65 + num) : String(value);
|
||||
}
|
||||
|
||||
function resultStatus(result: Record<string, unknown>) {
|
||||
if (!result.answered) return '未答';
|
||||
if (result.isCorrect === true) return '正确';
|
||||
if (result.isCorrect === false) return '错误';
|
||||
return '已答';
|
||||
}
|
||||
|
||||
export default function StudentReportsPage() {
|
||||
const router = useRouter();
|
||||
const practiceSessionId = router.params?.practiceSessionId || '';
|
||||
@@ -109,6 +132,68 @@ export default function StudentReportsPage() {
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{current?.questionResults?.length ? (
|
||||
<View className='section-block'>
|
||||
<Text className='section-heading'>逐题复盘</Text>
|
||||
<View className='list-stack'>
|
||||
{current.questionResults.map((raw, index) => {
|
||||
const item = raw || {};
|
||||
const result = item as Record<string, unknown>;
|
||||
const selectedOptions = numberArray(result, 'selectedOptions').map(answerChoiceLabel);
|
||||
const correctOptions = numberArray(result, 'correctOptionIndices').map(answerChoiceLabel);
|
||||
const correctOptionIndex = result.correctOptionIndex;
|
||||
if (!correctOptions.length && correctOptionIndex !== null && correctOptionIndex !== undefined) {
|
||||
correctOptions.push(answerChoiceLabel(correctOptionIndex));
|
||||
}
|
||||
const subResults = Array.isArray(result.subResults) ? result.subResults as Record<string, unknown>[] : [];
|
||||
return (
|
||||
<View className='list-row' key={stringValue(result, 'questionId') || String(index)}>
|
||||
<View className='amount-row'>
|
||||
<Text className='row-meta'>第 {index + 1} 题 · {stringValue(result, 'questionType') || '题目'}</Text>
|
||||
<Text className={`status-badge ${result.isCorrect === false ? 'danger' : ''}`}>{resultStatus(result)}</Text>
|
||||
</View>
|
||||
<RichContent content={stringValue(result, 'content') || '暂无题干'} compact />
|
||||
<Text className='row-meta'>得分 {String(result.score ?? 0)} / {String(result.totalScore ?? 0)}</Text>
|
||||
{selectedOptions.length ? <Text className='row-meta'>你的选择:{selectedOptions.join('、')}</Text> : null}
|
||||
{correctOptions.length ? <Text className='row-meta'>正确选项:{correctOptions.join('、')}</Text> : null}
|
||||
{stringValue(result, 'answerText') ? (
|
||||
<View className='answer-detail'>
|
||||
<Text className='answer-detail-label'>你的答案</Text>
|
||||
<RichContent content={stringValue(result, 'answerText')} compact muted />
|
||||
</View>
|
||||
) : null}
|
||||
{stringValue(result, 'correctAnswerText') ? (
|
||||
<View className='answer-detail'>
|
||||
<Text className='answer-detail-label'>参考答案</Text>
|
||||
<RichContent content={stringValue(result, 'correctAnswerText')} compact />
|
||||
</View>
|
||||
) : null}
|
||||
{stringValue(result, 'explanation') ? (
|
||||
<View className='answer-detail'>
|
||||
<Text className='answer-detail-label'>解析</Text>
|
||||
<RichContent content={stringValue(result, 'explanation')} compact muted />
|
||||
</View>
|
||||
) : null}
|
||||
{subResults.length ? (
|
||||
<View className='sub-explanation'>
|
||||
<Text className='answer-detail-label'>子题明细</Text>
|
||||
{subResults.map((subResult, subIndex) => (
|
||||
<View className='report-sub-result' key={stringValue(subResult, 'subQuestionId') || String(subIndex)}>
|
||||
<Text className='row-meta'>第 {subIndex + 1} 小题 · {resultStatus(subResult)} · 得分 {String(subResult.score ?? 0)} / {String(subResult.totalScore ?? 0)}</Text>
|
||||
<RichContent content={stringValue(subResult, 'content')} compact muted emptyText='暂无子题题干' />
|
||||
{stringValue(subResult, 'correctAnswerText') ? <RichContent content={`参考答案:${stringValue(subResult, 'correctAnswerText')}`} compact muted /> : null}
|
||||
{stringValue(subResult, 'explanation') ? <RichContent content={`解析:${stringValue(subResult, 'explanation')}`} compact muted /> : null}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{error ? <Text className='error-text'>{error}</Text> : null}
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -156,6 +156,40 @@
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.option-line {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.option-prefix {
|
||||
flex: 0 0 auto;
|
||||
color: #0f172a;
|
||||
font-size: 26px;
|
||||
font-weight: 850;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.option-rich {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.answer-detail {
|
||||
margin-top: 16px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.answer-detail-label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #334155;
|
||||
font-size: 22px;
|
||||
font-weight: 820;
|
||||
}
|
||||
|
||||
.row-main {
|
||||
display: block;
|
||||
color: #111827;
|
||||
@@ -303,6 +337,21 @@
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.status-badge.danger {
|
||||
background: #fff1f2;
|
||||
color: #be123c;
|
||||
}
|
||||
|
||||
.handbook-entry-body {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.report-sub-result {
|
||||
margin-top: 14px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px dashed #cbd5e1;
|
||||
}
|
||||
|
||||
.answer-sheet {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
|
||||
Reference in New Issue
Block a user