**后端优化** - 实现题目编号自动生成机制,按题型连续编号 - 移除分页限制,返回所有题目 - 支持题型筛选和关键词搜索 - 题目按题型和编号排序 - DTO 中包含答案字段,支持编辑时回显 - 选项按字母顺序排序 **前端优化** - 移除手动输入题目ID,系统自动生成 - 实现动态表单,支持添加/删除选项和答案 - 添加题型筛选下拉框 - 添加搜索框,支持搜索题目内容和编号 - 优化答案回显逻辑,直接使用后端返回的答案数据 - 表格显示题目编号列 **修复问题** - 修复 PostgreSQL SQL 语法错误 - 修复编辑题目时答案无法正确回显的问题 - 修复题目列表不完整的问题 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
186 lines
5.3 KiB
TypeScript
186 lines
5.3 KiB
TypeScript
import React, { useState } from 'react'
|
|
import { Card, Space, Tag, Typography, Radio, Checkbox, Input, Button } from 'antd'
|
|
import type { Question, AnswerResult as AnswerResultType } from '../types/question'
|
|
import AnswerResult from './AnswerResult'
|
|
import styles from '../pages/Question.module.less'
|
|
|
|
const { TextArea } = Input
|
|
const { Title } = Typography
|
|
|
|
interface QuestionCardProps {
|
|
question: Question
|
|
selectedAnswer: string | string[]
|
|
showResult: boolean
|
|
answerResult: AnswerResultType | null
|
|
loading: boolean
|
|
autoNextLoading: boolean
|
|
onAnswerChange: (answer: string | string[]) => void
|
|
onSubmit: () => void
|
|
onNext: () => void
|
|
}
|
|
|
|
const QuestionCard: React.FC<QuestionCardProps> = ({
|
|
question,
|
|
selectedAnswer,
|
|
showResult,
|
|
answerResult,
|
|
loading,
|
|
autoNextLoading,
|
|
onAnswerChange,
|
|
onSubmit,
|
|
onNext,
|
|
}) => {
|
|
const [fillAnswers, setFillAnswers] = useState<string[]>([])
|
|
|
|
// 渲染填空题内容
|
|
const renderFillContent = () => {
|
|
const content = question.content
|
|
const parts = content.split('****')
|
|
|
|
if (parts.length === 1) {
|
|
return <div className={styles.questionContent}>{content}</div>
|
|
}
|
|
|
|
if (fillAnswers.length === 0) {
|
|
setFillAnswers(new Array(parts.length - 1).fill(''))
|
|
}
|
|
|
|
return (
|
|
<div className={styles.questionContent}>
|
|
{parts.map((part, index) => (
|
|
<React.Fragment key={index}>
|
|
<span>{part}</span>
|
|
{index < parts.length - 1 && (
|
|
<Input
|
|
className={styles.fillInput}
|
|
placeholder={`填空${index + 1}`}
|
|
value={fillAnswers[index] || ''}
|
|
onChange={(e) => {
|
|
const newAnswers = [...fillAnswers]
|
|
newAnswers[index] = e.target.value
|
|
setFillAnswers(newAnswers)
|
|
onAnswerChange(newAnswers)
|
|
}}
|
|
disabled={showResult}
|
|
style={{
|
|
display: 'inline-block',
|
|
width: '120px',
|
|
margin: '0 8px',
|
|
}}
|
|
/>
|
|
)}
|
|
</React.Fragment>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// 渲染题目选项
|
|
const renderOptions = () => {
|
|
if (question.type === 'fill-in-blank') {
|
|
return null
|
|
}
|
|
|
|
if (question.type === 'short-answer') {
|
|
return (
|
|
<TextArea
|
|
placeholder="请输入答案"
|
|
value={selectedAnswer as string}
|
|
onChange={(e) => onAnswerChange(e.target.value)}
|
|
disabled={showResult}
|
|
rows={4}
|
|
style={{ marginTop: 20 }}
|
|
/>
|
|
)
|
|
}
|
|
|
|
if (question.type === 'multiple-selection') {
|
|
const sortedOptions = [...question.options].sort((a, b) => a.key.localeCompare(b.key))
|
|
|
|
return (
|
|
<Checkbox.Group
|
|
value={selectedAnswer as string[]}
|
|
onChange={(val) => onAnswerChange(val as string[])}
|
|
disabled={showResult}
|
|
style={{ width: '100%', marginTop: 20 }}
|
|
>
|
|
<Space direction="vertical" style={{ width: '100%' }}>
|
|
{sortedOptions.map((option) => (
|
|
<Checkbox key={option.key} value={option.key}>
|
|
<span style={{ fontSize: 16 }}>
|
|
{option.key}. {option.value}
|
|
</span>
|
|
</Checkbox>
|
|
))}
|
|
</Space>
|
|
</Checkbox.Group>
|
|
)
|
|
}
|
|
|
|
// 单选题和判断题
|
|
const sortedOptions = [...question.options].sort((a, b) => a.key.localeCompare(b.key))
|
|
|
|
return (
|
|
<Radio.Group
|
|
value={selectedAnswer as string}
|
|
onChange={(e) => onAnswerChange(e.target.value)}
|
|
disabled={showResult}
|
|
style={{ width: '100%', marginTop: 20 }}
|
|
>
|
|
<Space direction="vertical" style={{ width: '100%' }}>
|
|
{sortedOptions.map((option) => (
|
|
<Radio key={option.key} value={option.key}>
|
|
<span style={{ fontSize: 16 }}>
|
|
{question.type === 'true-false' ? option.value : `${option.key}. ${option.value}`}
|
|
</span>
|
|
</Radio>
|
|
))}
|
|
</Space>
|
|
</Radio.Group>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<Card className={styles.questionCard}>
|
|
<Space size="small" style={{ marginBottom: 16, alignItems: 'center' }}>
|
|
<Title level={5} style={{ margin: 0, display: 'inline' }}>
|
|
第 {question.question_id} 题
|
|
</Title>
|
|
<Tag color="blue">{question.category}</Tag>
|
|
</Space>
|
|
|
|
{question.type === 'fill-in-blank' ? (
|
|
renderFillContent()
|
|
) : (
|
|
<div className={styles.questionContent}>{question.content}</div>
|
|
)}
|
|
|
|
{renderOptions()}
|
|
|
|
{/* 答案结果 */}
|
|
{showResult && answerResult && (
|
|
<AnswerResult
|
|
answerResult={answerResult}
|
|
selectedAnswer={selectedAnswer}
|
|
questionType={question.type}
|
|
/>
|
|
)}
|
|
|
|
{/* 按钮 */}
|
|
<div className={styles.buttonGroup}>
|
|
{!showResult ? (
|
|
<Button type="primary" size="large" block onClick={onSubmit} loading={loading}>
|
|
提交答案
|
|
</Button>
|
|
) : (
|
|
<Button type="primary" size="large" block onClick={onNext} loading={autoNextLoading}>
|
|
{autoNextLoading ? '正在跳转到下一题...' : '下一题'}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
export default QuestionCard
|