优化题库管理系统:实现自动编号、动态表单和答案回显

**后端优化**
- 实现题目编号自动生成机制,按题型连续编号
- 移除分页限制,返回所有题目
- 支持题型筛选和关键词搜索
- 题目按题型和编号排序
- DTO 中包含答案字段,支持编辑时回显
- 选项按字母顺序排序

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
燕陇琪 2025-11-04 22:00:29 +08:00
parent 9e37cf8225
commit dd2b197516
6 changed files with 287 additions and 130 deletions

View File

@ -6,6 +6,7 @@ import (
"encoding/json" "encoding/json"
"log" "log"
"net/http" "net/http"
"sort"
"strconv" "strconv"
"time" "time"
@ -15,16 +16,7 @@ import (
// GetPracticeQuestions 获取练习题目列表 // GetPracticeQuestions 获取练习题目列表
func GetPracticeQuestions(c *gin.Context) { func GetPracticeQuestions(c *gin.Context) {
typeParam := c.Query("type") typeParam := c.Query("type")
category := c.Query("category") searchQuery := c.Query("search")
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "100"))
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 100
}
db := database.GetDB() db := database.GetDB()
var questions []models.PracticeQuestion var questions []models.PracticeQuestion
@ -32,22 +24,22 @@ func GetPracticeQuestions(c *gin.Context) {
query := db.Model(&models.PracticeQuestion{}) query := db.Model(&models.PracticeQuestion{})
// 根据题型过滤 - 直接使用前端传来的type // 根据题型过滤
if typeParam != "" { if typeParam != "" {
query = query.Where("type = ?", typeParam) query = query.Where("type = ?", typeParam)
} }
// 根据分类过滤 // 根据搜索关键词过滤(搜索题目内容或题目编号)
if category != "" { if searchQuery != "" {
query = query.Where("type_name = ?", category) query = query.Where("question LIKE ? OR question_id LIKE ?", "%"+searchQuery+"%", "%"+searchQuery+"%")
} }
// 获取总数 // 获取总数
query.Count(&total) query.Count(&total)
// 分页查询 // 查询所有题目 - 按题型和题目编号升序排序
offset := (page - 1) * pageSize // 先将 question_id 转为文本,提取数字部分,再转为整数排序
err := query.Offset(offset).Limit(pageSize).Find(&questions).Error err := query.Order("type ASC, CAST(COALESCE(NULLIF(REGEXP_REPLACE(question_id::text, '[^0-9]', '', 'g'), ''), '0') AS INTEGER) ASC").Find(&questions).Error
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusInternalServerError, gin.H{
"success": false, "success": false,
@ -337,6 +329,12 @@ func convertToDTO(question models.PracticeQuestion) models.PracticeQuestionDTO {
Options: []models.Option{}, Options: []models.Option{},
} }
// 解析答案数据
var answer interface{}
if err := json.Unmarshal([]byte(question.AnswerData), &answer); err == nil {
dto.Answer = answer
}
// 判断题自动生成选项 // 判断题自动生成选项
if question.Type == "true-false" { if question.Type == "true-false" {
dto.Options = []models.Option{ dto.Options = []models.Option{
@ -350,11 +348,19 @@ func convertToDTO(question models.PracticeQuestion) models.PracticeQuestionDTO {
if question.OptionsData != "" { if question.OptionsData != "" {
var optionsMap map[string]string var optionsMap map[string]string
if err := json.Unmarshal([]byte(question.OptionsData), &optionsMap); err == nil { if err := json.Unmarshal([]byte(question.OptionsData), &optionsMap); err == nil {
// 将map转换为Option数组 // 将map转换为Option数组并按key排序
for key, value := range optionsMap { keys := make([]string, 0, len(optionsMap))
for key := range optionsMap {
keys = append(keys, key)
}
// 对keys进行排序
sort.Strings(keys)
// 按排序后的key顺序添加选项
for _, key := range keys {
dto.Options = append(dto.Options, models.Option{ dto.Options = append(dto.Options, models.Option{
Key: key, Key: key,
Value: value, Value: optionsMap[key],
}) })
} }
} }
@ -437,7 +443,6 @@ func GetStatistics(c *gin.Context) {
// CreatePracticeQuestion 创建新的练习题目 // CreatePracticeQuestion 创建新的练习题目
func CreatePracticeQuestion(c *gin.Context) { func CreatePracticeQuestion(c *gin.Context) {
var req struct { var req struct {
QuestionID string `json:"question_id" binding:"required"`
Type string `json:"type" binding:"required"` Type string `json:"type" binding:"required"`
TypeName string `json:"type_name"` TypeName string `json:"type_name"`
Question string `json:"question" binding:"required"` Question string `json:"question" binding:"required"`
@ -453,6 +458,32 @@ func CreatePracticeQuestion(c *gin.Context) {
return return
} }
db := database.GetDB()
// 自动生成题目编号:找到该题型的最大编号并+1
var maxQuestionID string
err := db.Model(&models.PracticeQuestion{}).
Where("type = ?", req.Type).
Select("question_id").
Order("CAST(COALESCE(NULLIF(REGEXP_REPLACE(question_id::text, '[^0-9]', '', 'g'), ''), '0') AS INTEGER) DESC").
Limit(1).
Pluck("question_id", &maxQuestionID).Error
// 生成新的题目编号
var newQuestionID string
if err != nil || maxQuestionID == "" {
// 没有找到该题型的题目从1开始
newQuestionID = "1"
} else {
// 从最大编号中提取数字并+1
var maxNum int
_, scanErr := strconv.Atoi(maxQuestionID)
if scanErr == nil {
maxNum, _ = strconv.Atoi(maxQuestionID)
}
newQuestionID = strconv.Itoa(maxNum + 1)
}
// 将答案序列化为JSON字符串 // 将答案序列化为JSON字符串
answerData, err := json.Marshal(req.Answer) answerData, err := json.Marshal(req.Answer)
if err != nil { if err != nil {
@ -478,7 +509,7 @@ func CreatePracticeQuestion(c *gin.Context) {
} }
question := models.PracticeQuestion{ question := models.PracticeQuestion{
QuestionID: req.QuestionID, QuestionID: newQuestionID,
Type: req.Type, Type: req.Type,
TypeName: req.TypeName, TypeName: req.TypeName,
Question: req.Question, Question: req.Question,
@ -486,7 +517,6 @@ func CreatePracticeQuestion(c *gin.Context) {
OptionsData: optionsData, OptionsData: optionsData,
} }
db := database.GetDB()
if err := db.Create(&question).Error; err != nil { if err := db.Create(&question).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusInternalServerError, gin.H{
"success": false, "success": false,
@ -517,7 +547,6 @@ func UpdatePracticeQuestion(c *gin.Context) {
} }
var req struct { var req struct {
QuestionID string `json:"question_id"`
Type string `json:"type"` Type string `json:"type"`
TypeName string `json:"type_name"` TypeName string `json:"type_name"`
Question string `json:"question"` Question string `json:"question"`
@ -545,10 +574,7 @@ func UpdatePracticeQuestion(c *gin.Context) {
return return
} }
// 更新字段 // 更新字段(注意:不允许修改 QuestionID由系统自动生成
if req.QuestionID != "" {
question.QuestionID = req.QuestionID
}
if req.Type != "" { if req.Type != "" {
question.Type = req.Type question.Type = req.Type
} }

View File

@ -20,12 +20,13 @@ func (PracticeQuestion) TableName() string {
// PracticeQuestionDTO 用于前端返回的数据传输对象 // PracticeQuestionDTO 用于前端返回的数据传输对象
type PracticeQuestionDTO struct { type PracticeQuestionDTO struct {
ID uint `json:"id"` // 数据库自增ID ID uint `json:"id"` // 数据库自增ID
QuestionID string `json:"question_id"` // 题目编号(原JSON中的id) QuestionID string `json:"question_id"` // 题目编号(原JSON中的id)
Type string `json:"type"` // 前端使用的简化类型: single, multiple, judge, fill Type string `json:"type"` // 前端使用的简化类型: single, multiple, judge, fill
Content string `json:"content"` // 题目内容 Content string `json:"content"` // 题目内容
Options []Option `json:"options"` // 选择题选项数组 Options []Option `json:"options"` // 选择题选项数组
Category string `json:"category"` // 题目分类 Category string `json:"category"` // 题目分类
Answer interface{} `json:"answer"` // 正确答案(用于题目管理编辑)
} }
// PracticeAnswerSubmit 练习题答案提交 // PracticeAnswerSubmit 练习题答案提交

View File

@ -2,7 +2,7 @@ import { request } from '../utils/request'
import type { Question, SubmitAnswer, AnswerResult, Statistics, ApiResponse, WrongQuestion, WrongQuestionStats } from '../types/question' import type { Question, SubmitAnswer, AnswerResult, Statistics, ApiResponse, WrongQuestion, WrongQuestionStats } from '../types/question'
// 获取题目列表 // 获取题目列表
export const getQuestions = (params?: { type?: string; category?: string }) => { export const getQuestions = (params?: { type?: string; search?: string }) => {
return request.get<ApiResponse<Question[]>>('/practice/questions', { params }) return request.get<ApiResponse<Question[]>>('/practice/questions', { params })
} }
@ -71,7 +71,6 @@ export const clearWrongQuestions = () => {
// 创建题目 // 创建题目
export const createQuestion = (data: { export const createQuestion = (data: {
question_id: string
type: string type: string
type_name?: string type_name?: string
question: string question: string
@ -83,7 +82,6 @@ export const createQuestion = (data: {
// 更新题目 // 更新题目
export const updateQuestion = (id: number, data: { export const updateQuestion = (id: number, data: {
question_id?: string
type?: string type?: string
type_name?: string type_name?: string
question?: string question?: string

View File

@ -144,7 +144,7 @@ const QuestionCard: React.FC<QuestionCardProps> = ({
<Card className={styles.questionCard}> <Card className={styles.questionCard}>
<Space size="small" style={{ marginBottom: 16, alignItems: 'center' }}> <Space size="small" style={{ marginBottom: 16, alignItems: 'center' }}>
<Title level={5} style={{ margin: 0, display: 'inline' }}> <Title level={5} style={{ margin: 0, display: 'inline' }}>
{question.question_id || question.id} {question.question_id}
</Title> </Title>
<Tag color="blue">{question.category}</Tag> <Tag color="blue">{question.category}</Tag>
</Space> </Space>

View File

@ -13,7 +13,6 @@ import {
Tag, Tag,
Card, Card,
Radio, Radio,
InputNumber,
Divider, Divider,
} from 'antd' } from 'antd'
import { import {
@ -21,6 +20,7 @@ import {
EditOutlined, EditOutlined,
DeleteOutlined, DeleteOutlined,
ArrowLeftOutlined, ArrowLeftOutlined,
MinusCircleOutlined,
} from '@ant-design/icons' } from '@ant-design/icons'
import * as questionApi from '../api/question' import * as questionApi from '../api/question'
import type { Question } from '../types/question' import type { Question } from '../types/question'
@ -46,11 +46,22 @@ const QuestionManagement: React.FC = () => {
const [editingQuestion, setEditingQuestion] = useState<Question | null>(null) const [editingQuestion, setEditingQuestion] = useState<Question | null>(null)
const [form] = Form.useForm() const [form] = Form.useForm()
// 筛选和搜索状态
const [selectedType, setSelectedType] = useState<string>('')
const [searchText, setSearchText] = useState<string>('')
// 加载题目列表 // 加载题目列表
const loadQuestions = async () => { const loadQuestions = async () => {
setLoading(true) setLoading(true)
try { try {
const res = await questionApi.getQuestions() const params: any = {}
if (selectedType) {
params.type = selectedType
}
if (searchText) {
params.search = searchText
}
const res = await questionApi.getQuestions(params)
if (res.success && res.data) { if (res.success && res.data) {
setQuestions(res.data) setQuestions(res.data)
} }
@ -63,46 +74,29 @@ const QuestionManagement: React.FC = () => {
useEffect(() => { useEffect(() => {
loadQuestions() loadQuestions()
}, []) }, [selectedType, searchText])
// 打开新建/编辑弹窗 // 打开新建/编辑弹窗
const handleOpenModal = (question?: Question) => { const handleOpenModal = (question?: Question) => {
if (question) { if (question) {
setEditingQuestion(question) setEditingQuestion(question)
// 解析答案 // 直接使用后端返回的答案数据
let answerValue: any let answerValue: any = question.answer
if (question.type === 'multiple-selection') {
// 多选题答案是数组
answerValue = Array.isArray(question.options?.[0]?.key)
? question.options.map(opt => opt.key).join(',')
: ''
} else if (question.type === 'fill-in-blank') {
// 填空题答案是数组
answerValue = Array.isArray(question.options?.[0]?.key)
? question.options.map(opt => opt.key).join(',')
: ''
} else {
// 其他题型直接取第一个选项的key
answerValue = question.options?.[0]?.key
}
// 解析选项 // 解析选项(单选题和多选题)
let optionsValue: string | undefined let optionsValue: Array<{ key: string; value: string }> = []
if (question.options && question.options.length > 0 && if (question.options && question.options.length > 0 &&
(question.type === 'multiple-choice' || question.type === 'multiple-selection')) { (question.type === 'multiple-choice' || question.type === 'multiple-selection')) {
const optionsObj = question.options.reduce((acc, opt) => { optionsValue = question.options.map(opt => ({
acc[opt.key] = opt.value key: opt.key,
return acc value: opt.value,
}, {} as Record<string, string>) }))
optionsValue = JSON.stringify(optionsObj, null, 2)
} }
// 设置表单值 // 设置表单值
form.setFieldsValue({ form.setFieldsValue({
question_id: question.question_id,
type: question.type, type: question.type,
category: question.category,
content: question.content, content: question.content,
answer: answerValue, answer: answerValue,
options: optionsValue, options: optionsValue,
@ -110,6 +104,11 @@ const QuestionManagement: React.FC = () => {
} else { } else {
setEditingQuestion(null) setEditingQuestion(null)
form.resetFields() form.resetFields()
// 新建时设置默认值
form.setFieldsValue({
type: 'multiple-choice',
options: [{ key: 'A', value: '' }, { key: 'B', value: '' }],
})
} }
setModalVisible(true) setModalVisible(true)
} }
@ -134,10 +133,10 @@ const QuestionManagement: React.FC = () => {
answer = values.answer answer = values.answer
} else if (values.type === 'multiple-selection') { } else if (values.type === 'multiple-selection') {
// 多选题答案是数组 // 多选题答案是数组
answer = values.answer.split(',').map((s: string) => s.trim()) answer = values.answer
} else if (values.type === 'fill-in-blank') { } else if (values.type === 'fill-in-blank') {
// 填空题答案是数组 // 填空题答案是数组
answer = values.answer.split(',').map((s: string) => s.trim()) answer = values.answer
} else if (values.type === 'short-answer') { } else if (values.type === 'short-answer') {
answer = values.answer answer = values.answer
} else { } else {
@ -147,19 +146,19 @@ const QuestionManagement: React.FC = () => {
// 解析选项(仅选择题和多选题需要) // 解析选项(仅选择题和多选题需要)
let options: Record<string, string> | undefined let options: Record<string, string> | undefined
if (values.options && (values.type === 'multiple-choice' || values.type === 'multiple-selection')) { if (values.options && (values.type === 'multiple-choice' || values.type === 'multiple-selection')) {
try { // 将数组格式转换为对象格式 { "A": "选项A", "B": "选项B" }
options = typeof values.options === 'string' ? JSON.parse(values.options) : values.options options = values.options.reduce((acc: Record<string, string>, opt: any) => {
} catch (e) { if (opt && opt.key && opt.value) {
message.error('选项格式错误请使用正确的JSON格式') acc[opt.key] = opt.value
return }
} return acc
}, {})
} }
// 构建请求数据 // 构建请求数据
const data = { const data = {
question_id: values.question_id,
type: values.type, type: values.type,
type_name: values.category, type_name: '', // 不再使用分类字段
question: values.content, question: values.content,
answer: answer, answer: answer,
options: options, options: options,
@ -197,10 +196,10 @@ const QuestionManagement: React.FC = () => {
// 表格列定义 // 表格列定义
const columns = [ const columns = [
{ {
title: '题目ID', title: '题目编号',
dataIndex: 'question_id', dataIndex: 'question_id',
key: 'question_id', key: 'question_id',
width: 120, width: 100,
}, },
{ {
title: '题型', title: '题型',
@ -219,12 +218,6 @@ const QuestionManagement: React.FC = () => {
return <Tag color={colorMap[type]}>{typeConfig?.label || type}</Tag> return <Tag color={colorMap[type]}>{typeConfig?.label || type}</Tag>
}, },
}, },
{
title: '分类',
dataIndex: 'category',
key: 'category',
width: 150,
},
{ {
title: '题目内容', title: '题目内容',
dataIndex: 'content', dataIndex: 'content',
@ -260,8 +253,8 @@ const QuestionManagement: React.FC = () => {
}, },
] ]
// 根据题型动态渲染答案输入 // 根据题型动态渲染表单项
const renderAnswerInput = () => { const renderFormByType = () => {
const type = form.getFieldValue('type') const type = form.getFieldValue('type')
switch (type) { switch (type) {
@ -282,20 +275,56 @@ const QuestionManagement: React.FC = () => {
case 'multiple-choice': case 'multiple-choice':
return ( return (
<> <>
<Form.Item <Form.List
label="选项"
name="options" name="options"
rules={[{ required: true, message: '请输入选项' }]} rules={[
{
validator: async (_, options) => {
if (!options || options.length < 2) {
return Promise.reject(new Error('至少需要2个选项'))
}
},
},
]}
> >
<Input.TextArea {(fields, { add, remove }, { errors }) => (
placeholder='JSON格式例如: {"A":"选项A","B":"选项B","C":"选项C","D":"选项D"}' <>
rows={4} <Form.Item label="选项" required>
/> {fields.map((field, index) => (
</Form.Item> <Space key={field.key} style={{ display: 'flex', marginBottom: 8 }} align="baseline">
<Form.Item
{...field}
name={[field.name, 'key']}
rules={[{ required: true, message: '请输入选项键' }]}
noStyle
>
<Input placeholder="A" style={{ width: 60 }} />
</Form.Item>
<Form.Item
{...field}
name={[field.name, 'value']}
rules={[{ required: true, message: '请输入选项内容' }]}
noStyle
>
<Input placeholder="选项内容" style={{ width: 400 }} />
</Form.Item>
{fields.length > 2 && (
<MinusCircleOutlined onClick={() => remove(field.name)} />
)}
</Space>
))}
<Form.ErrorList errors={errors} />
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
</Button>
</Form.Item>
</>
)}
</Form.List>
<Form.Item <Form.Item
label="正确答案" label="正确答案"
name="answer" name="answer"
rules={[{ required: true, message: '请输入答案' }]} rules={[{ required: true, message: '请选择答案' }]}
> >
<Input placeholder="输入选项键,如: A" /> <Input placeholder="输入选项键,如: A" />
</Form.Item> </Form.Item>
@ -305,38 +334,103 @@ const QuestionManagement: React.FC = () => {
case 'multiple-selection': case 'multiple-selection':
return ( return (
<> <>
<Form.Item <Form.List
label="选项"
name="options" name="options"
rules={[{ required: true, message: '请输入选项' }]} rules={[
{
validator: async (_, options) => {
if (!options || options.length < 2) {
return Promise.reject(new Error('至少需要2个选项'))
}
},
},
]}
> >
<Input.TextArea {(fields, { add, remove }, { errors }) => (
placeholder='JSON格式例如: {"A":"选项A","B":"选项B","C":"选项C","D":"选项D"}' <>
rows={4} <Form.Item label="选项" required>
/> {fields.map((field, index) => (
</Form.Item> <Space key={field.key} style={{ display: 'flex', marginBottom: 8 }} align="baseline">
<Form.Item
{...field}
name={[field.name, 'key']}
rules={[{ required: true, message: '请输入选项键' }]}
noStyle
>
<Input placeholder="A" style={{ width: 60 }} />
</Form.Item>
<Form.Item
{...field}
name={[field.name, 'value']}
rules={[{ required: true, message: '请输入选项内容' }]}
noStyle
>
<Input placeholder="选项内容" style={{ width: 400 }} />
</Form.Item>
{fields.length > 2 && (
<MinusCircleOutlined onClick={() => remove(field.name)} />
)}
</Space>
))}
<Form.ErrorList errors={errors} />
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
</Button>
</Form.Item>
</>
)}
</Form.List>
<Form.Item <Form.Item
label="正确答案" label="正确答案"
name="answer" name="answer"
rules={[{ required: true, message: '请输入答案' }]} rules={[{ required: true, message: '请选择答案' }]}
tooltip="多个答案请用逗号分隔,如: A,B,C"
> >
<Input placeholder="输入选项键数组,如: A,B,C" /> <Select mode="tags" placeholder="选择或输入答案(可多选)" />
</Form.Item> </Form.Item>
</> </>
) )
case 'fill-in-blank': case 'fill-in-blank':
return ( return (
<Form.Item <Form.List
label="正确答案"
name="answer" name="answer"
rules={[{ required: true, message: '请输入答案' }]} rules={[
{
validator: async (_, answers) => {
if (!answers || answers.length < 1) {
return Promise.reject(new Error('至少需要1个答案'))
}
},
},
]}
> >
<Input.TextArea {(fields, { add, remove }, { errors }) => (
placeholder="多个空格用逗号分隔,如: 答案1,答案2,答案3" <>
rows={3} <Form.Item label="正确答案" required>
/> {fields.map((field, index) => (
</Form.Item> <Space key={field.key} style={{ display: 'flex', marginBottom: 8 }} align="baseline">
<span> {index + 1}:</span>
<Form.Item
{...field}
rules={[{ required: true, message: '请输入答案' }]}
noStyle
>
<Input placeholder="填空答案" style={{ width: 400 }} />
</Form.Item>
{fields.length > 1 && (
<MinusCircleOutlined onClick={() => remove(field.name)} />
)}
</Space>
))}
<Form.ErrorList errors={errors} />
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
</Button>
</Form.Item>
</>
)}
</Form.List>
) )
case 'short-answer': case 'short-answer':
@ -346,7 +440,7 @@ const QuestionManagement: React.FC = () => {
name="answer" name="answer"
rules={[{ required: true, message: '请输入答案' }]} rules={[{ required: true, message: '请输入答案' }]}
> >
<Input.TextArea placeholder="输入参考答案" rows={4} /> <TextArea placeholder="输入参考答案" rows={4} />
</Form.Item> </Form.Item>
) )
@ -390,13 +484,43 @@ const QuestionManagement: React.FC = () => {
{/* 题目列表 */} {/* 题目列表 */}
<Card className={styles.content}> <Card className={styles.content}>
{/* 筛选和搜索 */}
<Space style={{ marginBottom: 16 }} size="middle">
<span></span>
<Select
style={{ width: 150 }}
placeholder="全部题型"
allowClear
value={selectedType || undefined}
onChange={(value) => setSelectedType(value || '')}
>
{questionTypes.map(type => (
<Option key={type.key} value={type.key}>
{type.label}
</Option>
))}
</Select>
<Input.Search
placeholder="搜索题目内容或编号"
style={{ width: 300 }}
allowClear
onSearch={(value) => setSearchText(value)}
onChange={(e) => {
if (!e.target.value) {
setSearchText('')
}
}}
/>
</Space>
<Table <Table
columns={columns} columns={columns}
dataSource={questions} dataSource={questions}
loading={loading} loading={loading}
rowKey="id" rowKey="id"
pagination={{ pagination={{
defaultPageSize: 10, defaultPageSize: 20,
showSizeChanger: true, showSizeChanger: true,
showTotal: (total) => `${total} 道题目`, showTotal: (total) => `${total} 道题目`,
}} }}
@ -416,22 +540,33 @@ const QuestionManagement: React.FC = () => {
<Form <Form
form={form} form={form}
layout="vertical" layout="vertical"
initialValues={{ type: 'multiple-choice' }} initialValues={{
type: 'multiple-choice',
options: [{ key: 'A', value: '' }, { key: 'B', value: '' }]
}}
> >
<Form.Item
label="题目ID"
name="question_id"
rules={[{ required: true, message: '请输入题目ID' }]}
>
<Input placeholder="输入题目ID如: Q001" />
</Form.Item>
<Form.Item <Form.Item
label="题型" label="题型"
name="type" name="type"
rules={[{ required: true, message: '请选择题型' }]} rules={[{ required: true, message: '请选择题型' }]}
> >
<Select placeholder="选择题型" onChange={() => form.setFieldsValue({ answer: undefined })}> <Select
placeholder="选择题型"
onChange={(value) => {
// 切换题型时重置答案和选项
form.setFieldsValue({ answer: undefined, options: undefined })
// 为单选和多选题设置默认选项
if (value === 'multiple-choice' || value === 'multiple-selection') {
form.setFieldsValue({
options: [{ key: 'A', value: '' }, { key: 'B', value: '' }]
})
}
// 为填空题设置默认答案数组
if (value === 'fill-in-blank') {
form.setFieldsValue({ answer: [''] })
}
}}
>
{questionTypes.map(type => ( {questionTypes.map(type => (
<Option key={type.key} value={type.key}> <Option key={type.key} value={type.key}>
{type.label} {type.label}
@ -440,10 +575,6 @@ const QuestionManagement: React.FC = () => {
</Select> </Select>
</Form.Item> </Form.Item>
<Form.Item label="分类" name="category">
<Input placeholder="输入题目分类,如: 党史知识" />
</Form.Item>
<Form.Item <Form.Item
label="题目内容" label="题目内容"
name="content" name="content"
@ -452,7 +583,7 @@ const QuestionManagement: React.FC = () => {
<TextArea rows={4} placeholder="输入题目内容" /> <TextArea rows={4} placeholder="输入题目内容" />
</Form.Item> </Form.Item>
{renderAnswerInput()} {renderFormByType()}
</Form> </Form>
</Modal> </Modal>
</div> </div>

View File

@ -15,6 +15,7 @@ export interface Question {
content: string content: string
options: Option[] options: Option[]
category: string category: string
answer?: any // 正确答案(用于题目管理编辑)
} }
// 提交答案 // 提交答案