594 lines
18 KiB
TypeScript
594 lines
18 KiB
TypeScript
import React, { useEffect, useState } from 'react'
|
||
import { useNavigate } from 'react-router-dom'
|
||
import {
|
||
Table,
|
||
Button,
|
||
Space,
|
||
Modal,
|
||
Form,
|
||
Input,
|
||
Select,
|
||
message,
|
||
Popconfirm,
|
||
Tag,
|
||
Card,
|
||
Radio,
|
||
Divider,
|
||
} from 'antd'
|
||
import {
|
||
PlusOutlined,
|
||
EditOutlined,
|
||
DeleteOutlined,
|
||
ArrowLeftOutlined,
|
||
MinusCircleOutlined,
|
||
} from '@ant-design/icons'
|
||
import * as questionApi from '../api/question'
|
||
import type { Question } from '../types/question'
|
||
import styles from './QuestionManagement.module.less'
|
||
|
||
const { Option } = Select
|
||
const { TextArea } = Input
|
||
|
||
// 题型配置
|
||
const questionTypes = [
|
||
{ key: 'multiple-choice', label: '单选题' },
|
||
{ key: 'multiple-selection', label: '多选题' },
|
||
{ key: 'true-false', label: '判断题' },
|
||
{ key: 'fill-in-blank', label: '填空题' },
|
||
{ key: 'short-answer', label: '简答题' },
|
||
]
|
||
|
||
const QuestionManagement: React.FC = () => {
|
||
const navigate = useNavigate()
|
||
const [questions, setQuestions] = useState<Question[]>([])
|
||
const [loading, setLoading] = useState(false)
|
||
const [modalVisible, setModalVisible] = useState(false)
|
||
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 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)
|
||
}
|
||
} catch (error) {
|
||
message.error('加载题目失败')
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
useEffect(() => {
|
||
loadQuestions()
|
||
}, [selectedType, searchText])
|
||
|
||
// 打开新建/编辑弹窗
|
||
const handleOpenModal = (question?: Question) => {
|
||
if (question) {
|
||
setEditingQuestion(question)
|
||
|
||
// 直接使用后端返回的答案数据
|
||
let answerValue: any = question.answer
|
||
|
||
// 解析选项(单选题和多选题)
|
||
let optionsValue: Array<{ key: string; value: string }> = []
|
||
if (question.options && question.options.length > 0 &&
|
||
(question.type === 'multiple-choice' || question.type === 'multiple-selection')) {
|
||
optionsValue = question.options.map(opt => ({
|
||
key: opt.key,
|
||
value: opt.value,
|
||
}))
|
||
}
|
||
|
||
// 设置表单值
|
||
form.setFieldsValue({
|
||
type: question.type,
|
||
content: question.content,
|
||
answer: answerValue,
|
||
options: optionsValue,
|
||
})
|
||
} else {
|
||
setEditingQuestion(null)
|
||
form.resetFields()
|
||
// 新建时设置默认值
|
||
form.setFieldsValue({
|
||
type: 'multiple-choice',
|
||
options: [{ key: 'A', value: '' }, { key: 'B', value: '' }],
|
||
})
|
||
}
|
||
setModalVisible(true)
|
||
}
|
||
|
||
// 关闭弹窗
|
||
const handleCloseModal = () => {
|
||
setModalVisible(false)
|
||
setEditingQuestion(null)
|
||
form.resetFields()
|
||
}
|
||
|
||
// 保存题目
|
||
const handleSave = async () => {
|
||
try {
|
||
const values = await form.validateFields()
|
||
|
||
// 解析答案
|
||
let answer: any
|
||
if (values.type === 'true-false') {
|
||
answer = values.answer
|
||
} else if (values.type === 'multiple-choice') {
|
||
answer = values.answer
|
||
} else if (values.type === 'multiple-selection') {
|
||
// 多选题答案是数组
|
||
answer = values.answer
|
||
} else if (values.type === 'fill-in-blank') {
|
||
// 填空题答案是数组
|
||
answer = values.answer
|
||
} else if (values.type === 'short-answer') {
|
||
answer = values.answer
|
||
} else {
|
||
answer = values.answer
|
||
}
|
||
|
||
// 解析选项(仅选择题和多选题需要)
|
||
let options: Record<string, string> | undefined
|
||
if (values.options && (values.type === 'multiple-choice' || values.type === 'multiple-selection')) {
|
||
// 将数组格式转换为对象格式 { "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 = {
|
||
type: values.type,
|
||
type_name: '', // 不再使用分类字段
|
||
question: values.content,
|
||
answer: answer,
|
||
options: options,
|
||
}
|
||
|
||
if (editingQuestion) {
|
||
// 更新
|
||
await questionApi.updateQuestion(editingQuestion.id, data)
|
||
message.success('更新成功')
|
||
} else {
|
||
// 创建
|
||
await questionApi.createQuestion(data)
|
||
message.success('创建成功')
|
||
}
|
||
|
||
handleCloseModal()
|
||
loadQuestions()
|
||
} catch (error) {
|
||
console.error('保存失败:', error)
|
||
message.error('保存失败')
|
||
}
|
||
}
|
||
|
||
// 删除题目
|
||
const handleDelete = async (id: number) => {
|
||
try {
|
||
await questionApi.deleteQuestion(id)
|
||
message.success('删除成功')
|
||
loadQuestions()
|
||
} catch (error) {
|
||
message.error('删除失败')
|
||
}
|
||
}
|
||
|
||
// 表格列定义
|
||
const columns = [
|
||
{
|
||
title: '题目编号',
|
||
dataIndex: 'question_id',
|
||
key: 'question_id',
|
||
width: 100,
|
||
},
|
||
{
|
||
title: '题型',
|
||
dataIndex: 'type',
|
||
key: 'type',
|
||
width: 120,
|
||
render: (type: string) => {
|
||
const typeConfig = questionTypes.find(t => t.key === type)
|
||
const colorMap: Record<string, string> = {
|
||
'multiple-choice': 'blue',
|
||
'multiple-selection': 'green',
|
||
'true-false': 'orange',
|
||
'fill-in-blank': 'purple',
|
||
'short-answer': 'magenta',
|
||
}
|
||
return <Tag color={colorMap[type]}>{typeConfig?.label || type}</Tag>
|
||
},
|
||
},
|
||
{
|
||
title: '题目内容',
|
||
dataIndex: 'content',
|
||
key: 'content',
|
||
ellipsis: true,
|
||
},
|
||
{
|
||
title: '操作',
|
||
key: 'action',
|
||
width: 150,
|
||
render: (_: any, record: Question) => (
|
||
<Space size="small">
|
||
<Button
|
||
type="link"
|
||
size="small"
|
||
icon={<EditOutlined />}
|
||
onClick={() => handleOpenModal(record)}
|
||
>
|
||
编辑
|
||
</Button>
|
||
<Popconfirm
|
||
title="确定删除这道题目吗?"
|
||
onConfirm={() => handleDelete(record.id)}
|
||
okText="确定"
|
||
cancelText="取消"
|
||
>
|
||
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
|
||
删除
|
||
</Button>
|
||
</Popconfirm>
|
||
</Space>
|
||
),
|
||
},
|
||
]
|
||
|
||
// 根据题型动态渲染表单项
|
||
const renderFormByType = () => {
|
||
const type = form.getFieldValue('type')
|
||
|
||
switch (type) {
|
||
case 'true-false':
|
||
return (
|
||
<Form.Item
|
||
label="正确答案"
|
||
name="answer"
|
||
rules={[{ required: true, message: '请选择答案' }]}
|
||
>
|
||
<Radio.Group>
|
||
<Radio value={true}>正确</Radio>
|
||
<Radio value={false}>错误</Radio>
|
||
</Radio.Group>
|
||
</Form.Item>
|
||
)
|
||
|
||
case 'multiple-choice':
|
||
return (
|
||
<>
|
||
<Form.List
|
||
name="options"
|
||
rules={[
|
||
{
|
||
validator: async (_, options) => {
|
||
if (!options || options.length < 2) {
|
||
return Promise.reject(new Error('至少需要2个选项'))
|
||
}
|
||
},
|
||
},
|
||
]}
|
||
>
|
||
{(fields, { add, remove }, { errors }) => (
|
||
<>
|
||
<Form.Item label="选项" required>
|
||
{fields.map((field) => (
|
||
<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: '请选择答案' }]}
|
||
>
|
||
<Input placeholder="输入选项键,如: A" />
|
||
</Form.Item>
|
||
</>
|
||
)
|
||
|
||
case 'multiple-selection':
|
||
return (
|
||
<>
|
||
<Form.List
|
||
name="options"
|
||
rules={[
|
||
{
|
||
validator: async (_, options) => {
|
||
if (!options || options.length < 2) {
|
||
return Promise.reject(new Error('至少需要2个选项'))
|
||
}
|
||
},
|
||
},
|
||
]}
|
||
>
|
||
{(fields, { add, remove }, { errors }) => (
|
||
<>
|
||
<Form.Item label="选项" required>
|
||
{fields.map((field) => (
|
||
<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: '请选择答案' }]}
|
||
tooltip="多个答案请用逗号分隔,如: A,B,C"
|
||
>
|
||
<Select mode="tags" placeholder="选择或输入答案(可多选)" />
|
||
</Form.Item>
|
||
</>
|
||
)
|
||
|
||
case 'fill-in-blank':
|
||
return (
|
||
<Form.List
|
||
name="answer"
|
||
rules={[
|
||
{
|
||
validator: async (_, answers) => {
|
||
if (!answers || answers.length < 1) {
|
||
return Promise.reject(new Error('至少需要1个答案'))
|
||
}
|
||
},
|
||
},
|
||
]}
|
||
>
|
||
{(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':
|
||
return (
|
||
<Form.Item
|
||
label="参考答案"
|
||
name="answer"
|
||
rules={[{ required: true, message: '请输入答案' }]}
|
||
>
|
||
<TextArea placeholder="输入参考答案" rows={4} />
|
||
</Form.Item>
|
||
)
|
||
|
||
default:
|
||
return (
|
||
<Form.Item
|
||
label="答案"
|
||
name="answer"
|
||
rules={[{ required: true, message: '请输入答案' }]}
|
||
>
|
||
<Input placeholder="输入答案" />
|
||
</Form.Item>
|
||
)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className={styles.container}>
|
||
{/* 头部 */}
|
||
<Card className={styles.header}>
|
||
<div className={styles.headerContent}>
|
||
<Space>
|
||
<Button
|
||
icon={<ArrowLeftOutlined />}
|
||
onClick={() => navigate('/')}
|
||
>
|
||
返回首页
|
||
</Button>
|
||
<Divider type="vertical" />
|
||
<h2 className={styles.title}>题库管理</h2>
|
||
</Space>
|
||
<Button
|
||
type="primary"
|
||
icon={<PlusOutlined />}
|
||
onClick={() => handleOpenModal()}
|
||
>
|
||
新建题目
|
||
</Button>
|
||
</div>
|
||
</Card>
|
||
|
||
{/* 题目列表 */}
|
||
<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: 20,
|
||
showSizeChanger: true,
|
||
showTotal: (total) => `共 ${total} 道题目`,
|
||
}}
|
||
/>
|
||
</Card>
|
||
|
||
{/* 新建/编辑弹窗 */}
|
||
<Modal
|
||
title={editingQuestion ? '编辑题目' : '新建题目'}
|
||
open={modalVisible}
|
||
onOk={handleSave}
|
||
onCancel={handleCloseModal}
|
||
width={700}
|
||
okText="保存"
|
||
cancelText="取消"
|
||
>
|
||
<Form
|
||
form={form}
|
||
layout="vertical"
|
||
initialValues={{
|
||
type: 'multiple-choice',
|
||
options: [{ key: 'A', value: '' }, { key: 'B', value: '' }]
|
||
}}
|
||
>
|
||
<Form.Item
|
||
label="题型"
|
||
name="type"
|
||
rules={[{ required: true, message: '请选择题型' }]}
|
||
>
|
||
<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}
|
||
</Option>
|
||
))}
|
||
</Select>
|
||
</Form.Item>
|
||
|
||
<Form.Item
|
||
label="题目内容"
|
||
name="content"
|
||
rules={[{ required: true, message: '请输入题目内容' }]}
|
||
>
|
||
<TextArea rows={4} placeholder="输入题目内容" />
|
||
</Form.Item>
|
||
|
||
{renderFormByType()}
|
||
</Form>
|
||
</Modal>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default QuestionManagement
|