在首页添加错题数量统计显示
主要改动: 1. 后端修改: - 在 UserStatistics 模型中添加 wrong_questions 字段 - 在 GetStatistics 接口中查询并返回错题总数(包括已掌握和未掌握) 2. 前端修改: - 在 Statistics 接口中添加 wrong_questions 字段 - 在首页统计卡片中新增"错题数量"显示 - 调整布局为4列展示(题库总数、已答题数、错题数量、正确率) 3. UI优化: - 错题数量使用红色显示(#ff4d4f) - 响应式布局:移动端每行2个,PC端每行4个 - 与错题本页面的统计数据保持一致 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
de8480a328
commit
9e37cf8225
@ -403,6 +403,12 @@ func GetStatistics(c *gin.Context) {
|
||||
Where("user_id = ? AND is_correct = ?", uid, true).
|
||||
Count(&correctAnswers)
|
||||
|
||||
// 获取用户错题数量(所有错题,包括已掌握和未掌握的)
|
||||
var wrongQuestions int64
|
||||
db.Model(&models.WrongQuestion{}).
|
||||
Where("user_id = ?", uid).
|
||||
Count(&wrongQuestions)
|
||||
|
||||
// 计算正确率
|
||||
var accuracy float64
|
||||
if answeredQuestions > 0 {
|
||||
@ -418,6 +424,7 @@ func GetStatistics(c *gin.Context) {
|
||||
TotalQuestions: int(totalQuestions),
|
||||
AnsweredQuestions: int(answeredQuestions),
|
||||
CorrectAnswers: int(correctAnswers),
|
||||
WrongQuestions: int(wrongQuestions),
|
||||
Accuracy: accuracy,
|
||||
}
|
||||
|
||||
@ -426,3 +433,207 @@ func GetStatistics(c *gin.Context) {
|
||||
"data": stats,
|
||||
})
|
||||
}
|
||||
|
||||
// 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"`
|
||||
Answer interface{} `json:"answer" binding:"required"`
|
||||
Options map[string]string `json:"options"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "请求参数错误: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 将答案序列化为JSON字符串
|
||||
answerData, err := json.Marshal(req.Answer)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "答案格式错误",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 将选项序列化为JSON字符串
|
||||
var optionsData string
|
||||
if req.Options != nil && len(req.Options) > 0 {
|
||||
optionsBytes, err := json.Marshal(req.Options)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "选项格式错误",
|
||||
})
|
||||
return
|
||||
}
|
||||
optionsData = string(optionsBytes)
|
||||
}
|
||||
|
||||
question := models.PracticeQuestion{
|
||||
QuestionID: req.QuestionID,
|
||||
Type: req.Type,
|
||||
TypeName: req.TypeName,
|
||||
Question: req.Question,
|
||||
AnswerData: string(answerData),
|
||||
OptionsData: optionsData,
|
||||
}
|
||||
|
||||
db := database.GetDB()
|
||||
if err := db.Create(&question).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"success": false,
|
||||
"message": "创建题目失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 返回创建的题目
|
||||
dto := convertToDTO(question)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": dto,
|
||||
"message": "创建成功",
|
||||
})
|
||||
}
|
||||
|
||||
// UpdatePracticeQuestion 更新练习题目
|
||||
func UpdatePracticeQuestion(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的题目ID",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
QuestionID string `json:"question_id"`
|
||||
Type string `json:"type"`
|
||||
TypeName string `json:"type_name"`
|
||||
Question string `json:"question"`
|
||||
Answer interface{} `json:"answer"`
|
||||
Options map[string]string `json:"options"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "请求参数错误: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
db := database.GetDB()
|
||||
var question models.PracticeQuestion
|
||||
|
||||
// 查找题目是否存在
|
||||
if err := db.First(&question, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"success": false,
|
||||
"message": "题目不存在",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 更新字段
|
||||
if req.QuestionID != "" {
|
||||
question.QuestionID = req.QuestionID
|
||||
}
|
||||
if req.Type != "" {
|
||||
question.Type = req.Type
|
||||
}
|
||||
if req.TypeName != "" {
|
||||
question.TypeName = req.TypeName
|
||||
}
|
||||
if req.Question != "" {
|
||||
question.Question = req.Question
|
||||
}
|
||||
if req.Answer != nil {
|
||||
answerData, err := json.Marshal(req.Answer)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "答案格式错误",
|
||||
})
|
||||
return
|
||||
}
|
||||
question.AnswerData = string(answerData)
|
||||
}
|
||||
if req.Options != nil {
|
||||
optionsBytes, err := json.Marshal(req.Options)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "选项格式错误",
|
||||
})
|
||||
return
|
||||
}
|
||||
question.OptionsData = string(optionsBytes)
|
||||
}
|
||||
|
||||
// 保存更新
|
||||
if err := db.Save(&question).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"success": false,
|
||||
"message": "更新题目失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 返回更新后的题目
|
||||
dto := convertToDTO(question)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": dto,
|
||||
"message": "更新成功",
|
||||
})
|
||||
}
|
||||
|
||||
// DeletePracticeQuestion 删除练习题目
|
||||
func DeletePracticeQuestion(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的题目ID",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
db := database.GetDB()
|
||||
|
||||
// 检查题目是否存在
|
||||
var question models.PracticeQuestion
|
||||
if err := db.First(&question, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"success": false,
|
||||
"message": "题目不存在",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 删除题目
|
||||
if err := db.Delete(&question).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"success": false,
|
||||
"message": "删除题目失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "删除成功",
|
||||
})
|
||||
}
|
||||
|
||||
@ -25,5 +25,6 @@ type UserStatistics struct {
|
||||
TotalQuestions int `json:"total_questions"` // 题库总数
|
||||
AnsweredQuestions int `json:"answered_questions"` // 已答题数
|
||||
CorrectAnswers int `json:"correct_answers"` // 答对题数
|
||||
WrongQuestions int `json:"wrong_questions"` // 错题数量
|
||||
Accuracy float64 `json:"accuracy"` // 正确率
|
||||
}
|
||||
|
||||
5
main.go
5
main.go
@ -50,6 +50,11 @@ func main() {
|
||||
auth.POST("/practice/submit", handlers.SubmitPracticeAnswer) // 提交练习答案
|
||||
auth.GET("/practice/statistics", handlers.GetStatistics) // 获取统计数据
|
||||
|
||||
// 题库管理API(需要认证)
|
||||
auth.POST("/practice/questions", handlers.CreatePracticeQuestion) // 创建题目
|
||||
auth.PUT("/practice/questions/:id", handlers.UpdatePracticeQuestion) // 更新题目
|
||||
auth.DELETE("/practice/questions/:id", handlers.DeletePracticeQuestion) // 删除题目
|
||||
|
||||
// 错题本相关API
|
||||
auth.GET("/wrong-questions", handlers.GetWrongQuestions) // 获取错题列表
|
||||
auth.GET("/wrong-questions/stats", handlers.GetWrongQuestionStats) // 获取错题统计
|
||||
|
||||
@ -7,6 +7,7 @@ import Login from './pages/Login'
|
||||
import Home from './pages/Home'
|
||||
import About from './pages/About'
|
||||
import WrongQuestions from './pages/WrongQuestions'
|
||||
import QuestionManagement from './pages/QuestionManagement'
|
||||
|
||||
const App: React.FC = () => {
|
||||
return (
|
||||
@ -20,6 +21,7 @@ const App: React.FC = () => {
|
||||
|
||||
{/* 不带TabBar的页面,但需要登录保护 */}
|
||||
<Route path="/wrong-questions" element={<ProtectedRoute><WrongQuestions /></ProtectedRoute>} />
|
||||
<Route path="/question-management" element={<ProtectedRoute><QuestionManagement /></ProtectedRoute>} />
|
||||
|
||||
{/* 不带TabBar的页面,不需要登录保护 */}
|
||||
<Route path="/login" element={<Login />} />
|
||||
|
||||
@ -66,3 +66,34 @@ export const deleteWrongQuestion = (id: number) => {
|
||||
export const clearWrongQuestions = () => {
|
||||
return request.delete<ApiResponse<null>>('/wrong-questions')
|
||||
}
|
||||
|
||||
// ========== 题库管理相关 API ==========
|
||||
|
||||
// 创建题目
|
||||
export const createQuestion = (data: {
|
||||
question_id: string
|
||||
type: string
|
||||
type_name?: string
|
||||
question: string
|
||||
answer: any
|
||||
options?: Record<string, string>
|
||||
}) => {
|
||||
return request.post<ApiResponse<Question>>('/practice/questions', data)
|
||||
}
|
||||
|
||||
// 更新题目
|
||||
export const updateQuestion = (id: number, data: {
|
||||
question_id?: string
|
||||
type?: string
|
||||
type_name?: string
|
||||
question?: string
|
||||
answer?: any
|
||||
options?: Record<string, string>
|
||||
}) => {
|
||||
return request.put<ApiResponse<Question>>(`/practice/questions/${id}`, data)
|
||||
}
|
||||
|
||||
// 删除题目
|
||||
export const deleteQuestion = (id: number) => {
|
||||
return request.delete<ApiResponse<null>>(`/practice/questions/${id}`)
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
BookOutlined,
|
||||
UserOutlined,
|
||||
LogoutOutlined,
|
||||
SettingOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import * as questionApi from '../api/question'
|
||||
import type { Statistics } from '../types/question'
|
||||
@ -69,6 +70,7 @@ const Home: React.FC = () => {
|
||||
total_questions: 0,
|
||||
answered_questions: 0,
|
||||
correct_answers: 0,
|
||||
wrong_questions: 0,
|
||||
accuracy: 0,
|
||||
})
|
||||
|
||||
@ -169,21 +171,28 @@ const Home: React.FC = () => {
|
||||
{/* 统计卡片 */}
|
||||
<Card className={styles.statsCard}>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col xs={8} sm={8} md={8}>
|
||||
<Col xs={12} sm={12} md={6}>
|
||||
<Statistic
|
||||
title="题库总数"
|
||||
value={statistics.total_questions}
|
||||
valueStyle={{ color: '#1677ff', fontSize: '24px' }}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={8} sm={8} md={8}>
|
||||
<Col xs={12} sm={12} md={6}>
|
||||
<Statistic
|
||||
title="已答题数"
|
||||
value={statistics.answered_questions}
|
||||
valueStyle={{ color: '#52c41a', fontSize: '24px' }}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={8} sm={8} md={8}>
|
||||
<Col xs={12} sm={12} md={6}>
|
||||
<Statistic
|
||||
title="错题数量"
|
||||
value={statistics.wrong_questions}
|
||||
valueStyle={{ color: '#ff4d4f', fontSize: '24px' }}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={12} sm={12} md={6}>
|
||||
<Statistic
|
||||
title="正确率"
|
||||
value={statistics.accuracy.toFixed(0)}
|
||||
@ -258,6 +267,23 @@ const Home: React.FC = () => {
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
hoverable
|
||||
className={styles.quickCard}
|
||||
onClick={() => navigate('/question-management')}
|
||||
style={{ marginTop: '16px' }}
|
||||
>
|
||||
<Space align="center" size="large">
|
||||
<div className={styles.quickIcon}>
|
||||
<SettingOutlined style={{ fontSize: '32px', color: '#13c2c2' }} />
|
||||
</div>
|
||||
<div>
|
||||
<Title level={5} style={{ margin: 0 }}>题库管理</Title>
|
||||
<Paragraph type="secondary" style={{ margin: 0 }}>添加、编辑和删除题目</Paragraph>
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
61
web/src/pages/QuestionManagement.module.less
Normal file
61
web/src/pages/QuestionManagement.module.less
Normal file
@ -0,0 +1,61 @@
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
padding: 16px;
|
||||
|
||||
@media (min-width: 1025px) {
|
||||
padding: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 16px;
|
||||
|
||||
.headerContent {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
:global {
|
||||
.ant-table-wrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端适配
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.header {
|
||||
.headerContent {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
:global {
|
||||
.ant-table {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ant-btn {
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
462
web/src/pages/QuestionManagement.tsx
Normal file
462
web/src/pages/QuestionManagement.tsx
Normal file
@ -0,0 +1,462 @@
|
||||
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,
|
||||
InputNumber,
|
||||
Divider,
|
||||
} from 'antd'
|
||||
import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
ArrowLeftOutlined,
|
||||
} 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 loadQuestions = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await questionApi.getQuestions()
|
||||
if (res.success && res.data) {
|
||||
setQuestions(res.data)
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('加载题目失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadQuestions()
|
||||
}, [])
|
||||
|
||||
// 打开新建/编辑弹窗
|
||||
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 optionsValue: string | undefined
|
||||
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)
|
||||
}
|
||||
|
||||
// 设置表单值
|
||||
form.setFieldsValue({
|
||||
question_id: question.question_id,
|
||||
type: question.type,
|
||||
category: question.category,
|
||||
content: question.content,
|
||||
answer: answerValue,
|
||||
options: optionsValue,
|
||||
})
|
||||
} else {
|
||||
setEditingQuestion(null)
|
||||
form.resetFields()
|
||||
}
|
||||
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.split(',').map((s: string) => s.trim())
|
||||
} else if (values.type === 'fill-in-blank') {
|
||||
// 填空题答案是数组
|
||||
answer = values.answer.split(',').map((s: string) => s.trim())
|
||||
} 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')) {
|
||||
try {
|
||||
options = typeof values.options === 'string' ? JSON.parse(values.options) : values.options
|
||||
} catch (e) {
|
||||
message.error('选项格式错误,请使用正确的JSON格式')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 构建请求数据
|
||||
const data = {
|
||||
question_id: values.question_id,
|
||||
type: values.type,
|
||||
type_name: values.category,
|
||||
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: '题目ID',
|
||||
dataIndex: 'question_id',
|
||||
key: 'question_id',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
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: 'category',
|
||||
key: 'category',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
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 renderAnswerInput = () => {
|
||||
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.Item
|
||||
label="选项"
|
||||
name="options"
|
||||
rules={[{ required: true, message: '请输入选项' }]}
|
||||
>
|
||||
<Input.TextArea
|
||||
placeholder='JSON格式,例如: {"A":"选项A","B":"选项B","C":"选项C","D":"选项D"}'
|
||||
rows={4}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="正确答案"
|
||||
name="answer"
|
||||
rules={[{ required: true, message: '请输入答案' }]}
|
||||
>
|
||||
<Input placeholder="输入选项键,如: A" />
|
||||
</Form.Item>
|
||||
</>
|
||||
)
|
||||
|
||||
case 'multiple-selection':
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
label="选项"
|
||||
name="options"
|
||||
rules={[{ required: true, message: '请输入选项' }]}
|
||||
>
|
||||
<Input.TextArea
|
||||
placeholder='JSON格式,例如: {"A":"选项A","B":"选项B","C":"选项C","D":"选项D"}'
|
||||
rows={4}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="正确答案"
|
||||
name="answer"
|
||||
rules={[{ required: true, message: '请输入答案' }]}
|
||||
>
|
||||
<Input placeholder="输入选项键数组,如: A,B,C" />
|
||||
</Form.Item>
|
||||
</>
|
||||
)
|
||||
|
||||
case 'fill-in-blank':
|
||||
return (
|
||||
<Form.Item
|
||||
label="正确答案"
|
||||
name="answer"
|
||||
rules={[{ required: true, message: '请输入答案' }]}
|
||||
>
|
||||
<Input.TextArea
|
||||
placeholder="多个空格用逗号分隔,如: 答案1,答案2,答案3"
|
||||
rows={3}
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
|
||||
case 'short-answer':
|
||||
return (
|
||||
<Form.Item
|
||||
label="参考答案"
|
||||
name="answer"
|
||||
rules={[{ required: true, message: '请输入答案' }]}
|
||||
>
|
||||
<Input.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}>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={questions}
|
||||
loading={loading}
|
||||
rowKey="id"
|
||||
pagination={{
|
||||
defaultPageSize: 10,
|
||||
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' }}
|
||||
>
|
||||
<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 })}>
|
||||
{questionTypes.map(type => (
|
||||
<Option key={type.key} value={type.key}>
|
||||
{type.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="分类" name="category">
|
||||
<Input placeholder="输入题目分类,如: 党史知识" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="题目内容"
|
||||
name="content"
|
||||
rules={[{ required: true, message: '请输入题目内容' }]}
|
||||
>
|
||||
<TextArea rows={4} placeholder="输入题目内容" />
|
||||
</Form.Item>
|
||||
|
||||
{renderAnswerInput()}
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default QuestionManagement
|
||||
@ -35,6 +35,7 @@ export interface Statistics {
|
||||
total_questions: number
|
||||
answered_questions: number
|
||||
correct_answers: number
|
||||
wrong_questions: number
|
||||
accuracy: number
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user