AnCao/web/src/pages/QuestionManagement.tsx
2025-11-05 09:37:29 +08:00

594 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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