AnCao/web/src/components/QuestionCard.tsx
yanlongqi dd2b197516 优化题库管理系统:实现自动编号、动态表单和答案回显
**后端优化**
- 实现题目编号自动生成机制,按题型连续编号
- 移除分页限制,返回所有题目
- 支持题型筛选和关键词搜索
- 题目按题型和编号排序
- DTO 中包含答案字段,支持编辑时回显
- 选项按字母顺序排序

**前端优化**
- 移除手动输入题目ID,系统自动生成
- 实现动态表单,支持添加/删除选项和答案
- 添加题型筛选下拉框
- 添加搜索框,支持搜索题目内容和编号
- 优化答案回显逻辑,直接使用后端返回的答案数据
- 表格显示题目编号列

**修复问题**
- 修复 PostgreSQL SQL 语法错误
- 修复编辑题目时答案无法正确回显的问题
- 修复题目列表不完整的问题

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 22:00:29 +08:00

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