优化题库管理系统:实现自动编号、动态表单和答案回显
**后端优化** - 实现题目编号自动生成机制,按题型连续编号 - 移除分页限制,返回所有题目 - 支持题型筛选和关键词搜索 - 题目按题型和编号排序 - 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:
parent
9e37cf8225
commit
dd2b197516
@ -6,6 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
@ -15,16 +16,7 @@ import (
|
||||
// GetPracticeQuestions 获取练习题目列表
|
||||
func GetPracticeQuestions(c *gin.Context) {
|
||||
typeParam := c.Query("type")
|
||||
category := c.Query("category")
|
||||
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
|
||||
}
|
||||
searchQuery := c.Query("search")
|
||||
|
||||
db := database.GetDB()
|
||||
var questions []models.PracticeQuestion
|
||||
@ -32,22 +24,22 @@ func GetPracticeQuestions(c *gin.Context) {
|
||||
|
||||
query := db.Model(&models.PracticeQuestion{})
|
||||
|
||||
// 根据题型过滤 - 直接使用前端传来的type
|
||||
// 根据题型过滤
|
||||
if typeParam != "" {
|
||||
query = query.Where("type = ?", typeParam)
|
||||
}
|
||||
|
||||
// 根据分类过滤
|
||||
if category != "" {
|
||||
query = query.Where("type_name = ?", category)
|
||||
// 根据搜索关键词过滤(搜索题目内容或题目编号)
|
||||
if searchQuery != "" {
|
||||
query = query.Where("question LIKE ? OR question_id LIKE ?", "%"+searchQuery+"%", "%"+searchQuery+"%")
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
query.Count(&total)
|
||||
|
||||
// 分页查询
|
||||
offset := (page - 1) * pageSize
|
||||
err := query.Offset(offset).Limit(pageSize).Find(&questions).Error
|
||||
// 查询所有题目 - 按题型和题目编号升序排序
|
||||
// 先将 question_id 转为文本,提取数字部分,再转为整数排序
|
||||
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 {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"success": false,
|
||||
@ -337,6 +329,12 @@ func convertToDTO(question models.PracticeQuestion) models.PracticeQuestionDTO {
|
||||
Options: []models.Option{},
|
||||
}
|
||||
|
||||
// 解析答案数据
|
||||
var answer interface{}
|
||||
if err := json.Unmarshal([]byte(question.AnswerData), &answer); err == nil {
|
||||
dto.Answer = answer
|
||||
}
|
||||
|
||||
// 判断题自动生成选项
|
||||
if question.Type == "true-false" {
|
||||
dto.Options = []models.Option{
|
||||
@ -350,11 +348,19 @@ func convertToDTO(question models.PracticeQuestion) models.PracticeQuestionDTO {
|
||||
if question.OptionsData != "" {
|
||||
var optionsMap map[string]string
|
||||
if err := json.Unmarshal([]byte(question.OptionsData), &optionsMap); err == nil {
|
||||
// 将map转换为Option数组
|
||||
for key, value := range optionsMap {
|
||||
// 将map转换为Option数组,并按key排序
|
||||
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{
|
||||
Key: key,
|
||||
Value: value,
|
||||
Value: optionsMap[key],
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -437,7 +443,6 @@ func GetStatistics(c *gin.Context) {
|
||||
// CreatePracticeQuestion 创建新的练习题目
|
||||
func CreatePracticeQuestion(c *gin.Context) {
|
||||
var req struct {
|
||||
QuestionID string `json:"question_id" binding:"required"`
|
||||
Type string `json:"type" binding:"required"`
|
||||
TypeName string `json:"type_name"`
|
||||
Question string `json:"question" binding:"required"`
|
||||
@ -453,6 +458,32 @@ func CreatePracticeQuestion(c *gin.Context) {
|
||||
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字符串
|
||||
answerData, err := json.Marshal(req.Answer)
|
||||
if err != nil {
|
||||
@ -478,7 +509,7 @@ func CreatePracticeQuestion(c *gin.Context) {
|
||||
}
|
||||
|
||||
question := models.PracticeQuestion{
|
||||
QuestionID: req.QuestionID,
|
||||
QuestionID: newQuestionID,
|
||||
Type: req.Type,
|
||||
TypeName: req.TypeName,
|
||||
Question: req.Question,
|
||||
@ -486,7 +517,6 @@ func CreatePracticeQuestion(c *gin.Context) {
|
||||
OptionsData: optionsData,
|
||||
}
|
||||
|
||||
db := database.GetDB()
|
||||
if err := db.Create(&question).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"success": false,
|
||||
@ -517,7 +547,6 @@ func UpdatePracticeQuestion(c *gin.Context) {
|
||||
}
|
||||
|
||||
var req struct {
|
||||
QuestionID string `json:"question_id"`
|
||||
Type string `json:"type"`
|
||||
TypeName string `json:"type_name"`
|
||||
Question string `json:"question"`
|
||||
@ -545,10 +574,7 @@ func UpdatePracticeQuestion(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 更新字段
|
||||
if req.QuestionID != "" {
|
||||
question.QuestionID = req.QuestionID
|
||||
}
|
||||
// 更新字段(注意:不允许修改 QuestionID,由系统自动生成)
|
||||
if req.Type != "" {
|
||||
question.Type = req.Type
|
||||
}
|
||||
|
||||
@ -20,12 +20,13 @@ func (PracticeQuestion) TableName() string {
|
||||
|
||||
// PracticeQuestionDTO 用于前端返回的数据传输对象
|
||||
type PracticeQuestionDTO struct {
|
||||
ID uint `json:"id"` // 数据库自增ID
|
||||
QuestionID string `json:"question_id"` // 题目编号(原JSON中的id)
|
||||
Type string `json:"type"` // 前端使用的简化类型: single, multiple, judge, fill
|
||||
Content string `json:"content"` // 题目内容
|
||||
Options []Option `json:"options"` // 选择题选项数组
|
||||
Category string `json:"category"` // 题目分类
|
||||
ID uint `json:"id"` // 数据库自增ID
|
||||
QuestionID string `json:"question_id"` // 题目编号(原JSON中的id)
|
||||
Type string `json:"type"` // 前端使用的简化类型: single, multiple, judge, fill
|
||||
Content string `json:"content"` // 题目内容
|
||||
Options []Option `json:"options"` // 选择题选项数组
|
||||
Category string `json:"category"` // 题目分类
|
||||
Answer interface{} `json:"answer"` // 正确答案(用于题目管理编辑)
|
||||
}
|
||||
|
||||
// PracticeAnswerSubmit 练习题答案提交
|
||||
|
||||
@ -2,7 +2,7 @@ import { request } from '../utils/request'
|
||||
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 })
|
||||
}
|
||||
|
||||
@ -71,7 +71,6 @@ export const clearWrongQuestions = () => {
|
||||
|
||||
// 创建题目
|
||||
export const createQuestion = (data: {
|
||||
question_id: string
|
||||
type: string
|
||||
type_name?: string
|
||||
question: string
|
||||
@ -83,7 +82,6 @@ export const createQuestion = (data: {
|
||||
|
||||
// 更新题目
|
||||
export const updateQuestion = (id: number, data: {
|
||||
question_id?: string
|
||||
type?: string
|
||||
type_name?: string
|
||||
question?: string
|
||||
|
||||
@ -144,7 +144,7 @@ const QuestionCard: React.FC<QuestionCardProps> = ({
|
||||
<Card className={styles.questionCard}>
|
||||
<Space size="small" style={{ marginBottom: 16, alignItems: 'center' }}>
|
||||
<Title level={5} style={{ margin: 0, display: 'inline' }}>
|
||||
第 {question.question_id || question.id} 题
|
||||
第 {question.question_id} 题
|
||||
</Title>
|
||||
<Tag color="blue">{question.category}</Tag>
|
||||
</Space>
|
||||
|
||||
@ -13,7 +13,6 @@ import {
|
||||
Tag,
|
||||
Card,
|
||||
Radio,
|
||||
InputNumber,
|
||||
Divider,
|
||||
} from 'antd'
|
||||
import {
|
||||
@ -21,6 +20,7 @@ import {
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
ArrowLeftOutlined,
|
||||
MinusCircleOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import * as questionApi from '../api/question'
|
||||
import type { Question } from '../types/question'
|
||||
@ -46,11 +46,22 @@ const QuestionManagement: React.FC = () => {
|
||||
const [editingQuestion, setEditingQuestion] = useState<Question | null>(null)
|
||||
const [form] = Form.useForm()
|
||||
|
||||
// 筛选和搜索状态
|
||||
const [selectedType, setSelectedType] = useState<string>('')
|
||||
const [searchText, setSearchText] = useState<string>('')
|
||||
|
||||
// 加载题目列表
|
||||
const loadQuestions = async () => {
|
||||
setLoading(true)
|
||||
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) {
|
||||
setQuestions(res.data)
|
||||
}
|
||||
@ -63,46 +74,29 @@ const QuestionManagement: React.FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
loadQuestions()
|
||||
}, [])
|
||||
}, [selectedType, searchText])
|
||||
|
||||
// 打开新建/编辑弹窗
|
||||
const handleOpenModal = (question?: Question) => {
|
||||
if (question) {
|
||||
setEditingQuestion(question)
|
||||
|
||||
// 解析答案
|
||||
let answerValue: any
|
||||
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 answerValue: any = question.answer
|
||||
|
||||
// 解析选项
|
||||
let optionsValue: string | undefined
|
||||
// 解析选项(单选题和多选题)
|
||||
let optionsValue: Array<{ key: string; value: string }> = []
|
||||
if (question.options && question.options.length > 0 &&
|
||||
(question.type === 'multiple-choice' || question.type === 'multiple-selection')) {
|
||||
const optionsObj = question.options.reduce((acc, opt) => {
|
||||
acc[opt.key] = opt.value
|
||||
return acc
|
||||
}, {} as Record<string, string>)
|
||||
optionsValue = JSON.stringify(optionsObj, null, 2)
|
||||
optionsValue = question.options.map(opt => ({
|
||||
key: opt.key,
|
||||
value: opt.value,
|
||||
}))
|
||||
}
|
||||
|
||||
// 设置表单值
|
||||
form.setFieldsValue({
|
||||
question_id: question.question_id,
|
||||
type: question.type,
|
||||
category: question.category,
|
||||
content: question.content,
|
||||
answer: answerValue,
|
||||
options: optionsValue,
|
||||
@ -110,6 +104,11 @@ const QuestionManagement: React.FC = () => {
|
||||
} else {
|
||||
setEditingQuestion(null)
|
||||
form.resetFields()
|
||||
// 新建时设置默认值
|
||||
form.setFieldsValue({
|
||||
type: 'multiple-choice',
|
||||
options: [{ key: 'A', value: '' }, { key: 'B', value: '' }],
|
||||
})
|
||||
}
|
||||
setModalVisible(true)
|
||||
}
|
||||
@ -134,10 +133,10 @@ const QuestionManagement: React.FC = () => {
|
||||
answer = values.answer
|
||||
} 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') {
|
||||
// 填空题答案是数组
|
||||
answer = values.answer.split(',').map((s: string) => s.trim())
|
||||
answer = values.answer
|
||||
} else if (values.type === 'short-answer') {
|
||||
answer = values.answer
|
||||
} else {
|
||||
@ -147,19 +146,19 @@ const QuestionManagement: React.FC = () => {
|
||||
// 解析选项(仅选择题和多选题需要)
|
||||
let options: Record<string, string> | undefined
|
||||
if (values.options && (values.type === 'multiple-choice' || values.type === 'multiple-selection')) {
|
||||
try {
|
||||
options = typeof values.options === 'string' ? JSON.parse(values.options) : values.options
|
||||
} catch (e) {
|
||||
message.error('选项格式错误,请使用正确的JSON格式')
|
||||
return
|
||||
}
|
||||
// 将数组格式转换为对象格式 { "A": "选项A", "B": "选项B" }
|
||||
options = values.options.reduce((acc: Record<string, string>, opt: any) => {
|
||||
if (opt && opt.key && opt.value) {
|
||||
acc[opt.key] = opt.value
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
// 构建请求数据
|
||||
const data = {
|
||||
question_id: values.question_id,
|
||||
type: values.type,
|
||||
type_name: values.category,
|
||||
type_name: '', // 不再使用分类字段
|
||||
question: values.content,
|
||||
answer: answer,
|
||||
options: options,
|
||||
@ -197,10 +196,10 @@ const QuestionManagement: React.FC = () => {
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{
|
||||
title: '题目ID',
|
||||
title: '题目编号',
|
||||
dataIndex: 'question_id',
|
||||
key: 'question_id',
|
||||
width: 120,
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '题型',
|
||||
@ -219,12 +218,6 @@ const QuestionManagement: React.FC = () => {
|
||||
return <Tag color={colorMap[type]}>{typeConfig?.label || type}</Tag>
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '分类',
|
||||
dataIndex: 'category',
|
||||
key: 'category',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '题目内容',
|
||||
dataIndex: 'content',
|
||||
@ -260,8 +253,8 @@ const QuestionManagement: React.FC = () => {
|
||||
},
|
||||
]
|
||||
|
||||
// 根据题型动态渲染答案输入
|
||||
const renderAnswerInput = () => {
|
||||
// 根据题型动态渲染表单项
|
||||
const renderFormByType = () => {
|
||||
const type = form.getFieldValue('type')
|
||||
|
||||
switch (type) {
|
||||
@ -282,20 +275,56 @@ const QuestionManagement: React.FC = () => {
|
||||
case 'multiple-choice':
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
label="选项"
|
||||
<Form.List
|
||||
name="options"
|
||||
rules={[{ required: true, message: '请输入选项' }]}
|
||||
rules={[
|
||||
{
|
||||
validator: async (_, options) => {
|
||||
if (!options || options.length < 2) {
|
||||
return Promise.reject(new Error('至少需要2个选项'))
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input.TextArea
|
||||
placeholder='JSON格式,例如: {"A":"选项A","B":"选项B","C":"选项C","D":"选项D"}'
|
||||
rows={4}
|
||||
/>
|
||||
</Form.Item>
|
||||
{(fields, { add, remove }, { errors }) => (
|
||||
<>
|
||||
<Form.Item label="选项" required>
|
||||
{fields.map((field, index) => (
|
||||
<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
|
||||
label="正确答案"
|
||||
name="answer"
|
||||
rules={[{ required: true, message: '请输入答案' }]}
|
||||
rules={[{ required: true, message: '请选择答案' }]}
|
||||
>
|
||||
<Input placeholder="输入选项键,如: A" />
|
||||
</Form.Item>
|
||||
@ -305,38 +334,103 @@ const QuestionManagement: React.FC = () => {
|
||||
case 'multiple-selection':
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
label="选项"
|
||||
<Form.List
|
||||
name="options"
|
||||
rules={[{ required: true, message: '请输入选项' }]}
|
||||
rules={[
|
||||
{
|
||||
validator: async (_, options) => {
|
||||
if (!options || options.length < 2) {
|
||||
return Promise.reject(new Error('至少需要2个选项'))
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input.TextArea
|
||||
placeholder='JSON格式,例如: {"A":"选项A","B":"选项B","C":"选项C","D":"选项D"}'
|
||||
rows={4}
|
||||
/>
|
||||
</Form.Item>
|
||||
{(fields, { add, remove }, { errors }) => (
|
||||
<>
|
||||
<Form.Item label="选项" required>
|
||||
{fields.map((field, index) => (
|
||||
<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
|
||||
label="正确答案"
|
||||
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>
|
||||
</>
|
||||
)
|
||||
|
||||
case 'fill-in-blank':
|
||||
return (
|
||||
<Form.Item
|
||||
label="正确答案"
|
||||
<Form.List
|
||||
name="answer"
|
||||
rules={[{ required: true, message: '请输入答案' }]}
|
||||
rules={[
|
||||
{
|
||||
validator: async (_, answers) => {
|
||||
if (!answers || answers.length < 1) {
|
||||
return Promise.reject(new Error('至少需要1个答案'))
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input.TextArea
|
||||
placeholder="多个空格用逗号分隔,如: 答案1,答案2,答案3"
|
||||
rows={3}
|
||||
/>
|
||||
</Form.Item>
|
||||
{(fields, { add, remove }, { errors }) => (
|
||||
<>
|
||||
<Form.Item label="正确答案" required>
|
||||
{fields.map((field, index) => (
|
||||
<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':
|
||||
@ -346,7 +440,7 @@ const QuestionManagement: React.FC = () => {
|
||||
name="answer"
|
||||
rules={[{ required: true, message: '请输入答案' }]}
|
||||
>
|
||||
<Input.TextArea placeholder="输入参考答案" rows={4} />
|
||||
<TextArea placeholder="输入参考答案" rows={4} />
|
||||
</Form.Item>
|
||||
)
|
||||
|
||||
@ -390,13 +484,43 @@ const QuestionManagement: React.FC = () => {
|
||||
|
||||
{/* 题目列表 */}
|
||||
<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
|
||||
columns={columns}
|
||||
dataSource={questions}
|
||||
loading={loading}
|
||||
rowKey="id"
|
||||
pagination={{
|
||||
defaultPageSize: 10,
|
||||
defaultPageSize: 20,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 道题目`,
|
||||
}}
|
||||
@ -416,22 +540,33 @@ const QuestionManagement: React.FC = () => {
|
||||
<Form
|
||||
form={form}
|
||||
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
|
||||
label="题型"
|
||||
name="type"
|
||||
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 => (
|
||||
<Option key={type.key} value={type.key}>
|
||||
{type.label}
|
||||
@ -440,10 +575,6 @@ const QuestionManagement: React.FC = () => {
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="分类" name="category">
|
||||
<Input placeholder="输入题目分类,如: 党史知识" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="题目内容"
|
||||
name="content"
|
||||
@ -452,7 +583,7 @@ const QuestionManagement: React.FC = () => {
|
||||
<TextArea rows={4} placeholder="输入题目内容" />
|
||||
</Form.Item>
|
||||
|
||||
{renderAnswerInput()}
|
||||
{renderFormByType()}
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
|
||||
@ -15,6 +15,7 @@ export interface Question {
|
||||
content: string
|
||||
options: Option[]
|
||||
category: string
|
||||
answer?: any // 正确答案(用于题目管理编辑)
|
||||
}
|
||||
|
||||
// 提交答案
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user