Ver Fonte

feat(考试模块): 新增考试相关API、组件和页面

refactor(问题展示组件): 重构QuestionShow组件为组合式API
feat(编辑器组件): 添加横杠数字按钮功能
feat(考试模块): 新增考试相关API接口
feat(考试模块): 新增考试状态管理store
feat(考试模块): 新增问题编辑页面
feat(考试模块): 重构试卷管理页面
feat(考试模块): 新增试卷表单组件
style(考试模块): 优化样式和布局
tanshanming há 7 meses atrás
pai
commit
1f44f92c18

+ 5 - 4
src/api/paper/examPaperApi.js → src/api/exam/paper/examPaperApi.js

@@ -9,10 +9,11 @@ const request = (url, ...arg) => baseRequest(`/api/webapp/` + url, ...arg)
  * @date  2025/07/07 10:10
  **/
 export default {
-	// 获取t_exam_paper分页
-	tExamPaperPage(data) {
-		return request('api/admin/exam/paper/page', data, 'post')
-	},
+	pageList: (data) => request('api/admin/exam/paper/page', data, 'post'),
+	taskExamPage: (query) => request('api/admin/exam/paper/taskExamPage', query, 'post'),
+	edit: (query) => request('api/admin/exam/paper/edit', query, 'post'),
+	select: (id) => request('api/admin/exam/paper/select/' + id, '', 'post'),
+	deletePaper: (id) => request('api/admin/exam/paper/delete/' + id, '', 'post'),
 	// 提交t_exam_paper表单 edit为true时为编辑,默认为新增
 	tExamPaperSubmitForm(data, edit = false) {
 		return request('api/admin/exam/paper/edit', data)

+ 10 - 0
src/api/exam/paper/subject.js

@@ -0,0 +1,10 @@
+import { baseRequest } from '@/utils/request'
+
+const request = (url, ...arg) => baseRequest(`/api/webapp/` + url, ...arg)
+export default {
+	list: () => request('api/admin/education/subject/list', {}, 'post'),
+	pageList: (query) => request('api/admin/education/subject/page', query, 'post'),
+	edit: (query) => request('api/admin/education/subject/edit', query, 'post'),
+	select: (id) => request('api/admin/education/subject/select/' + id, '', 'get'),
+	deleteSubject: (id) => request('api/admin/education/subject/delete/' + id, '', 'get')
+}

+ 10 - 0
src/api/exam/question/tQuestionApi.js

@@ -0,0 +1,10 @@
+import { baseRequest } from '@/utils/request'
+
+const request = (url, ...arg) => baseRequest(`/api/webapp/` + url, ...arg)
+
+export default {
+	pageList: (query) => request('api/admin/question/page', query, 'post'),
+	edit: (query) => request('api/admin/question/edit', query, 'post'),
+	select: (id) => request('api/admin/question/select/' + id, '', 'post'),
+	deleteQuestion: (id) => request('api/admin/question/delete/' + id, '', 'post')
+}

+ 0 - 40
src/api/question/tQuestionApi.js

@@ -1,40 +0,0 @@
-import { baseRequest } from '@/utils/request'
-
-const request = (url, ...arg) => baseRequest(`/api/webapp/` + url, ...arg)
-
-/**
- * t_questionApi接口管理器
- *
- * @author zss
- * @date  2025/07/08 16:24
- **/
-export default {
-	// 获取t_question分页
-	tQuestionPage(data) {
-		return request('api/admin/question/page', data, 'post')
-	},
-	// 提交t_question表单 edit为true时为编辑,默认为新增
-	tQuestionSubmitForm(data, edit = false) {
-		return request('api/admin/question/edit', data)
-	},
-	// 删除t_question
-	tQuestionDelete(data) {
-		return request('delete', data)
-	},
-	// 获取t_question详情
-	tQuestionDetail(data) {
-		return request('detail', data, 'get')
-	},
-	pageList(data){
-		return request('api/admin/question/page', data, 'post')
-	},
-	edit(data){
-		return request('api/admin/question/edit', data)
-	},
-	select(id){
-		return request('api/admin/question/select/'+ id)
-	},
-	deleteQuestion(id){
-		return request('api/admin/question/delete/' + id)
-	}
-}

+ 13 - 2
src/components/Editor/index.vue

@@ -44,7 +44,7 @@
 			default:
 				'undo redo |  forecolor backcolor bold italic underline strikethrough link | blocks fontfamily fontsize | \
 				alignleft aligncenter alignright alignjustify outdent indent lineheight | bullist numlist | \
-				image table  preview | code selectall'
+				image table  preview | code selectall | numberedline'
 		},
 		fileUploadFunction: {
 			type: Function,
@@ -85,9 +85,20 @@
 			editor.on('init', () => {
 				// getBody().style.fontSize = '14px'
 			})
+			editor.ui.registry.addButton('numberedline', {
+				text: '横杠数字',
+				onAction: function () {
+					const count = editor.getBody().querySelectorAll('.gapfilling-span').length
+					const lineNumber = count + 1
+					const uuid = 'gapfilling-' + Date.now() + '-' + Math.floor(Math.random() * 10000)
+					editor.insertContent(
+						`<span class="gapfilling-span ${uuid}" style="color:red;padding:0 30px;margin:0 5px;border-bottom:3px double red;">${lineNumber}</span>&ZeroWidthSpace;`
+					)
+				}
+			})
 		}
 	})
-	const contentValue = ref()
+	const contentValue = ref(props.modelValue)
 	watch(props, (newValue) => {
 		contentValue.value = newValue.modelValue
 		emit('update:modelValue', newValue.modelValue)

+ 107 - 0
src/store/exam.js

@@ -0,0 +1,107 @@
+import { defineStore } from 'pinia'
+import subjectApi from '@/api/exam/paper/subject.js'
+
+const format = function (array, key) {
+	for (let item of array) {
+		if (item.key === key) {
+			return item.value
+		}
+	}
+	return null
+}
+
+export const useExamStore = defineStore('exam', {
+	state: () => ({
+		// 学科相关
+		subjects: [],
+		// 枚举相关
+		user: {
+			sexEnum: [
+				{ key: 1, value: '男' },
+				{ key: 2, value: '女' }
+			],
+			statusEnum: [
+				{ key: 1, value: '启用' },
+				{ key: 2, value: '禁用' }
+			],
+			levelEnum: [
+				{ key: 1, value: '一年级' },
+				{ key: 2, value: '二年级' },
+				{ key: 3, value: '三年级' },
+				{ key: 4, value: '四年级' },
+				{ key: 5, value: '五年级' },
+				{ key: 6, value: '六年级' },
+				{ key: 7, value: '初一' },
+				{ key: 8, value: '初二' },
+				{ key: 9, value: '初三' },
+				{ key: 10, value: '高一' },
+				{ key: 11, value: '高二' },
+				{ key: 12, value: '高三' }
+			],
+			roleEnum: [
+				{ key: 1, value: '学生' },
+				{ key: 2, value: '教师' },
+				{ key: 3, value: '管理员' }
+			],
+			statusTag: [
+				{ key: 1, value: 'success' },
+				{ key: 2, value: 'danger' }
+			],
+			statusBtn: [
+				{ key: 1, value: '禁用' },
+				{ key: 2, value: '启用' }
+			]
+		},
+		exam: {
+			examPaper: {
+				paperTypeEnum: [
+					{ key: 1, value: '固定试卷' },
+					{ key: 4, value: '时段试卷' },
+					{ key: 6, value: '任务试卷' }
+				]
+			},
+			question: {
+				typeEnum: [
+					{ key: 1, value: '单选题' },
+					{ key: 2, value: '多选题' },
+					{ key: 3, value: '判断题' },
+					{ key: 4, value: '填空题' },
+					{ key: 5, value: '简答题' }
+				],
+				editUrlEnum: [
+					{ key: 1, value: './edit/single-choice.vue', name: '单选题' },
+					{ key: 2, value: './edit/multiple-choice.vue', name: '多选题' },
+					{ key: 3, value: './edit/true-false.vue', name: '判断题' },
+					{ key: 4, value: './edit/gap-filling.vue', name: '填空题' },
+					{ key: 5, value: './edit/short-answer.vue', name: '简答题' }
+				]
+			}
+		}
+	}),
+	getters: {
+		// 枚举格式化
+		enumFormat: (state) => (array, key) => format(array, key),
+		levelEnum: (state) => state.user.levelEnum,
+		paperTypeEnum: (state) => state.exam.examPaper.paperTypeEnum,
+		questionTypeEnum: (state) => state.exam.question.typeEnum,
+		// 学科格式化
+		subjectEnumFormat: (state) => (key) => {
+			for (let item of state.subjects) {
+				if (item.id === key) {
+					return item.name + ' ( ' + item.levelName + ' )'
+				}
+			}
+			return null
+		},
+		getLevelEnum: (state) => state.user.levelEnum
+	},
+	actions: {
+		async initSubject(action) {
+			const re = await subjectApi.list()
+			this.subjects = re
+			if (action !== undefined) {
+				action()
+			}
+		}
+	}
+})

+ 275 - 130
src/views/exm/exampaper/form.vue

@@ -1,149 +1,294 @@
 <template>
-    <xn-form-container
-        :title="formData.id ? '编辑t_exam_paper' : '增加t_exam_paper'"
-        :width="700"
-        :visible="visible"
-        :destroy-on-close="true"
-        @close="onClose"
-    >
-        <a-form ref="formRef" :model="formData" :rules="formRules" layout="vertical">
-			<a-form-item label="年级:" name="level">
-				<a-select
-					v-model:value="formData.level"
-					:options="gradeLevelOptions"
-					style="width: 100%"
-					placeholder="请选择年级"
-				>
+	<div class="app-container">
+		<a-form
+			:model="form"
+			ref="formRef"
+			:label-col="{ span: 4 }"
+			:wrapper-col="{ span: 16 }"
+			:rules="rules"
+			:loading="formLoading"
+		>
+			<a-form-item label="年级" name="level" :rules="rules.level">
+				<a-select v-model:value="form.level" placeholder="请选择年级" @change="levelChange">
+					<a-select-option v-for="item in levelEnum" :key="item.key" :value="item.key">
+						{{ item.value }}
+					</a-select-option>
 				</a-select>
 			</a-form-item>
-			<a-form-item label="学科:" name="subjectId">
-				<a-select
-					v-model:value="formData.subjectId"
-					:options="susbjectOptions"
-					style="width: 100%"
-					placeholder="请选择学科"
-				>
+			<a-form-item label="学科" name="subjectId" :rules="rules.subjectId">
+				<a-select v-model:value="form.subjectId" placeholder="请选择学科">
+					<a-select-option v-for="item in subjectFilter" :key="item.id" :value="item.id">
+						{{ item.name + ' ( ' + item.levelName + ' )' }}
+					</a-select-option>
 				</a-select>
 			</a-form-item>
-			<a-form-item label="试卷类型:" name="paperType">
-				<a-select
-					v-model:value="formData.paperType"
-					:options="paperTypeOptions"
-					style="width: 100%"
-					placeholder="请选择试卷类型"
-				>
+			<a-form-item label="试卷类型" name="paperType" :rules="rules.paperType">
+				<a-select v-model:value="form.paperType" placeholder="请选择试卷类型">
+					<a-select-option v-for="item in paperTypeEnum" :key="item.key" :value="item.key">
+						{{ item.value }}
+					</a-select-option>
 				</a-select>
 			</a-form-item>
-            <a-form-item label="试卷名称:" name="name">
-                <a-input v-model:value="formData.name" placeholder="请输入试卷名称" allow-clear />
-            </a-form-item>
-
-
-			<a-form-item :key="index" :label="'标题'+(index+1)+':'" required v-for="(titleItem,index) in titleItems">
-				<a-input v-model="titleItem.name" style="width: 65%"/>
-				<a-button  style="margin-left: 20px" @click="addQuestion(titleItem)">添加题目</a-button>
-				<a-button  style="margin-left: 20px;background-color: red;"  @click="titleItems.splice(index,1)" type="primary">删除</a-button>
-				<a-card class="exampaper-item-box" v-if="titleItem.questionItems.length!==0">
-					<a-form-item :key="questionIndex" :label="'题目'+(questionIndex+1)+':'"
-								  v-for="(questionItem,questionIndex) in titleItem.questionItems" style="margin-bottom: 15px">
-						<a-row>
-							<a-col :span="23">
-								<QuestionShow :qType="questionItem.questionType" :question="questionItem"/>
-							</a-col>
-							<a-col :span="1">
-								<a-button @click="titleItem.value.questionItems.splice(questionIndex,1)">删除</a-button>
-							</a-col>
-						</a-row>
-					</a-form-item>
-				</a-card>
+			<a-form-item label="时间限制" v-if="form.paperType === 4">
+				<a-range-picker
+					v-model:value="form.limitDateTime"
+					show-time
+					format="YYYY-MM-DD HH:mm:ss"
+					:placeholder="['开始日期', '结束日期']"
+				/>
 			</a-form-item>
-
-
-
-			<a-form-item label="建议时长:" name="suggestTime">
-				<a-input-number id="inputNumber" width="100%" v-model:value="formData.suggestTime" :min="1" :max="180" placeholder="分钟" allow-clear/>
+			<a-form-item label="试卷名称" name="name" :rules="rules.name">
+				<a-input v-model:value="form.name" placeholder="请输入试卷名称" />
+			</a-form-item>
+			<template v-for="(titleItem, index) in form.titleItems" :key="index">
+				<a-form-item :label="'标题' + (index + 1)" required>
+					<a-input v-model:value="titleItem.name" style="width: 80%" />
+					<a-button type="link" @click="addQuestion(titleItem)" style="margin-left: 20px">添加题目</a-button>
+					<a-button type="link" danger @click="removeTitleItem(index)">删除</a-button>
+					<a-card v-if="titleItem.questionItems.length !== 0" class="exampaper-item-box">
+						<template v-for="(questionItem, questionIndex) in titleItem.questionItems" :key="questionIndex">
+							<a-form-item :label="'题目' + (questionIndex + 1)" style="margin-bottom: 15px">
+								<a-row>
+									<a-col :span="23">
+										<!-- <QuestionShow :qType="questionItem.questionType" :question="questionItem" /> -->
+									</a-col>
+									<a-col :span="1">
+										<a-button type="link" danger @click="removeQuestion(titleItem, questionIndex)">删除</a-button>
+									</a-col>
+								</a-row>
+							</a-form-item>
+						</template>
+					</a-card>
+				</a-form-item>
+			</template>
+			<a-form-item label="建议时长" name="suggestTime" :rules="rules.suggestTime">
+				<a-input v-model:value="form.suggestTime" placeholder="分钟" />
 			</a-form-item>
-        </a-form>
-        <template #footer>
-			<a-button style="margin-right: 8px" @click="addTitle">添加标题</a-button>
-            <a-button style="margin-right: 8px" @click="onClose">关闭</a-button>
-            <a-button type="primary" @click="onSubmit" :loading="submitLoading">保存</a-button>
-        </template>
-    </xn-form-container>
+			<a-form-item>
+				<a-space>
+					<a-button type="primary" @click="submitForm">提交</a-button>
+					<a-button @click="resetForm">重置</a-button>
+					<a-button type="dashed" @click="addTitle">添加标题</a-button>
+				</a-space>
+			</a-form-item>
+		</a-form>
+		<a-modal
+			v-model:visible="questionPage.showDialog"
+			title="选择题目"
+			width="70%"
+			@ok="confirmQuestionSelect"
+			@cancel="() => (questionPage.showDialog = false)"
+		>
+			<a-form layout="inline">
+				<a-form-item label="ID">
+					<a-input v-model:value="questionPage.queryParam.id" allow-clear />
+				</a-form-item>
+				<a-form-item label="题型">
+					<a-select v-model:value="questionPage.queryParam.questionType" allow-clear style="width: 120px">
+						<a-select-option v-for="item in questionTypeEnum" :key="item.key" :value="item.key">
+							{{ item.value }}
+						</a-select-option>
+					</a-select>
+				</a-form-item>
+				<a-form-item>
+					<a-button type="primary" @click="queryForm">查询</a-button>
+				</a-form-item>
+			</a-form>
+			<a-table
+				:dataSource="questionPage.tableData"
+				:loading="questionPage.listLoading"
+				rowKey="id"
+				:rowSelection="rowSelection"
+				:pagination="false"
+				style="margin-top: 16px"
+			>
+				<a-table-column type="selection" width="35" />
+				<a-table-column title="Id" dataIndex="id" width="60" />
+				<a-table-column title="题型" dataIndex="questionType" :customRender="questionTypeFormatter" width="70" />
+				<a-table-column title="题干" dataIndex="shortTitle" ellipsis />
+			</a-table>
+			<a-pagination
+				v-if="questionPage.total > 0"
+				:total="questionPage.total"
+				:current="questionPage.queryParam.pageIndex"
+				:pageSize="questionPage.queryParam.pageSize"
+				@change="onPageChange"
+				style="margin-top: 16px; text-align: right"
+			/>
+		</a-modal>
+	</div>
 </template>
 
-<script setup name="tExamPaperForm">
-    import { cloneDeep } from 'lodash-es'
-    import { required } from '@/utils/formRules'
-    import tExamPaperApi from '@/api/paper/examPaperApi'
-	import tool from "@/utils/tool";
-    // 抽屉状态
-    const visible = ref(false)
-    const emit = defineEmits({ successful: null })
-    const formRef = ref()
-    // 表单数据
-    const formData = ref({})
-    const submitLoading = ref(false)
-	// 打开抽屉
-    const onOpen = (record) => {
-        visible.value = true
-        if (record) {
-            let recordData = cloneDeep(record)
-            formData.value = Object.assign({}, recordData)
-        }
-    }
-    // 关闭抽屉
-    const onClose = () => {
-        formRef.value.resetFields()
-        formData.value = {}
-        visible.value = false
-    }
-    // 默认要校验的
-    const formRules = {
-    }
-    // 验证并提交数据
-    const onSubmit = () => {
-        formRef.value.validate().then(() => {
-            submitLoading.value = true
-            const formDataParam = cloneDeep(formData.value)
-            tExamPaperApi
-                .tExamPaperSubmitForm(formDataParam, formDataParam.id)
-                .then(() => {
-                    onClose()
-                    emit('successful')
-                })
-                .finally(() => {
-                    submitLoading.value = false
-                })
-        })
-    }
-	const gradeLevelOptions = tool.dictList('SEMESTER')
-	const susbjectOptions = tool.dictList('SUBJECT')
-	const paperTypeOptions = tool.dictList('PAPER_TYPE')
-	const titleItems =  ref([])
+<script setup>
+	import { ref, reactive, computed, onMounted } from 'vue'
+	import { useExamStore } from '@/store/exam'
+	import examPaperApi from '@/api/exam/paper/examPaperApi'
+	import questionApi from '@/api/exam/question/tQuestionApi'
+	// import QuestionShow from '@/components/Show.vue'
+	import { message } from 'ant-design-vue'
+	import dayjs from 'dayjs'
+	const props = defineProps({
+		id: {
+			type: Number,
+			default: 0
+		}
+	})
+	const emit = defineEmits(['success'])
+	const formRef = ref()
+	const examStore = useExamStore()
+
+	const form = reactive({
+		id: null,
+		level: null,
+		subjectId: null,
+		paperType: 1,
+		limitDateTime: [],
+		name: '',
+		suggestTime: null,
+		titleItems: []
+	})
+	const subjectFilter = ref([])
+	const formLoading = ref(false)
+	const rules = {
+		level: [{ required: true, message: '请选择年级', trigger: 'change' }],
+		subjectId: [{ required: true, message: '请选择学科', trigger: 'change' }],
+		paperType: [{ required: true, message: '请选择试卷类型', trigger: 'change' }],
+		name: [{ required: true, message: '请输入试卷名称', trigger: 'blur' }],
+		suggestTime: [{ required: true, message: '请输入建议时长', trigger: 'blur' }]
+	}
+	const questionPage = reactive({
+		multipleSelection: [],
+		showDialog: false,
+		queryParam: {
+			id: null,
+			questionType: null,
+			subjectId: 1,
+			pageIndex: 1,
+			pageSize: 5
+		},
+		listLoading: false,
+		tableData: [],
+		total: 0
+	})
 	const currentTitleItem = ref(null)
-	const addTitle = () => {
-		titleItems.value.push({
-			name: '',
-			questionItems: []
-		})
+
+	const levelEnum = computed(() => examStore.levelEnum)
+	const paperTypeEnum = computed(() => examStore.paperTypeEnum)
+	const questionTypeEnum = computed(() => examStore.questionTypeEnum)
+	const subjects = computed(() => examStore.subjects)
+
+	onMounted(async () => {
+		await examStore.initSubject()
+		subjectFilter.value = subjects.value
+		const id = props.id
+		if (id && parseInt(id) !== 0) {
+			formLoading.value = true
+			const re = await examPaperApi.select(id)
+			Object.assign(form, re)
+			formLoading.value = false
+		}
+	})
+
+	function submitForm() {
+		formRef.value
+			.validate()
+			.then(async () => {
+				formLoading.value = true
+				const re = await examPaperApi.edit(form)
+				if (re) {
+					emit('success')
+				} else {
+					message.error(re.message)
+					formLoading.value = false
+				}
+			})
+			.catch(() => {})
+	}
+	function addTitle() {
+		form.titleItems.push({ name: '', questionItems: [] })
 	}
-	const addQuestion = (titleItem) =>{
+	function addQuestion(titleItem) {
 		currentTitleItem.value = titleItem
-		this.questionPage.showDialog = true
-		this.search()
+		questionPage.showDialog = true
+		search()
+	}
+	function removeTitleItem(index) {
+		form.titleItems.splice(index, 1)
+	}
+	function removeQuestion(titleItem, questionIndex) {
+		titleItem.questionItems.splice(questionIndex, 1)
+	}
+	function queryForm() {
+		questionPage.queryParam.pageIndex = 1
+		search()
+	}
+	function confirmQuestionSelect() {
+		Promise.all(questionPage.multipleSelection.map((q) => questionApi.select(q.id))).then((resArr) => {
+			resArr.forEach((re) => {
+				currentTitleItem.value.questionItems.push(re)
+			})
+			questionPage.showDialog = false
+		})
+	}
+	function levelChange() {
+		form.subjectId = null
+		subjectFilter.value = subjects.value.filter((data) => data.level === form.level)
+	}
+	function search() {
+		questionPage.queryParam.subjectId = form.subjectId
+		questionPage.listLoading = true
+		const params = {
+			...questionPage.queryParam,
+			size: questionPage.queryParam.pageSize,
+			current: questionPage.queryParam.pageIndex
+		}
+		questionApi.pageList(params).then((data) => {
+			const re = data
+			questionPage.tableData = re.records
+			questionPage.total = re.total
+			questionPage.queryParam.pageIndex = re.current
+			questionPage.listLoading = false
+		})
+	}
+	function onPageChange(page) {
+		questionPage.queryParam.pageIndex = page
+		search()
+	}
+	const rowSelection = {
+		selectedRowKeys: computed(() => questionPage.multipleSelection.map((q) => q.id)),
+		onChange: (selectedRowKeys, selectedRows) => {
+			questionPage.multipleSelection = selectedRows
+		}
+	}
+	function questionTypeFormatter({ text }) {
+		return examStore.enumFormat(questionTypeEnum.value, text)
+	}
+	function resetForm() {
+		const lastId = form.id
+		Object.assign(form, {
+			id: null,
+			level: null,
+			subjectId: null,
+			paperType: 1,
+			limitDateTime: [],
+			name: '',
+			suggestTime: null,
+			titleItems: []
+		})
+		form.id = lastId
 	}
-	// 抛出函数
-    defineExpose({
-        onOpen
-    })
 </script>
-<style>
-.exampaper-item-box {
-	.q-title {
-		margin: 0px 5px 0px 5px;
+
+<style lang="less" scoped>
+	.app-container {
+		padding: 24px;
 	}
-	.q-item-content {
+	.exampaper-item-box {
+		margin-top: 12px;
+		.q-title {
+			margin: 0 5px;
+		}
+		.q-item-content {
+		}
 	}
-}
 </style>

+ 245 - 176
src/views/exm/exampaper/index.vue

@@ -1,193 +1,262 @@
 <template>
-	<a-card :bordered="false">
-		<!-- 搜索和操作区域 -->
-
-		<a-card :bordered="false" style="margin-bottom: 10px">
-			<a-form ref="searchFormRef" name="advanced_search" class="ant-advanced-search-form" :model="formState">
-				<a-row :gutter="24">
-					<a-col :span="4">
-						<a-form-item name="name" label="ID:">
-							<a-input v-model:value="formState.id" placeholder="请输入试卷ID" style="width: 70%;"/>
-						</a-form-item>
-					</a-col>
-					<a-col :span="4">
-						<a-form-item label="学科名称:" name="subjectId">
-							<a-select
-								v-model:value="formState.subjectId"
-								:options="susbjectOptions"
-								style="width: 70%"
-								placeholder="请选择学科"
-							>
-							</a-select>
-						</a-form-item>
-					</a-col>
-					<a-col :span="4">
-						<a-form-item name="name" label="试卷名称:">
-							<a-input v-model:value="formState.name" placeholder="请输入试卷名称" style="width: 70%;"/>
-						</a-form-item>
-					</a-col>
-					<a-col :span="4">
-						<a-button type="primary" @click="table.refresh(true)" style="margin-right: 10px">
-							<template #icon><SearchOutlined /></template>
-							查询
-						</a-button>
-						<a-button class="snowy-buttom-left" @click="reset">
-							<template #icon><redo-outlined /></template>
-							重置
-						</a-button>
-					</a-col>
-				</a-row>
-			</a-form>
-		</a-card>
-
-
-
-
-
-		<s-table
-			ref="table"
+	<div class="app-container">
+		<!-- 查询表单 -->
+		<a-form :model="queryParam" layout="inline" class="search-form">
+			<a-form-item label="题目ID:">
+				<a-input v-model:value="queryParam.id" placeholder="请输入题目ID" allow-clear />
+			</a-form-item>
+			<a-form-item label="年级:">
+				<a-select
+					v-model:value="queryParam.level"
+					placeholder="请选择年级"
+					@change="levelChange"
+					allow-clear
+					style="width: 120px"
+				>
+					<a-select-option v-for="item in levelEnum" :key="item.key" :value="item.key">
+						{{ item.value }}
+					</a-select-option>
+				</a-select>
+			</a-form-item>
+			<a-form-item label="学科:">
+				<a-select v-model:value="queryParam.subjectId" placeholder="请选择学科" allow-clear style="width: 200px">
+					<a-select-option v-for="item in subjectFilter" :key="item.id" :value="item.id">
+						{{ item.name }} ( {{ item.levelName }} )
+					</a-select-option>
+				</a-select>
+			</a-form-item>
+			<a-form-item>
+				<a-button type="primary" @click="submitForm">查询</a-button>
+				<a-button type="primary" @click="openDrawer('add')" style="margin-left: 8px">添加</a-button>
+			</a-form-item>
+		</a-form>
+
+		<!-- 数据表格 -->
+		<a-table
+			:loading="listLoading"
+			:data-source="tableData"
 			:columns="columns"
-			:data="loadData"
-			:alert="options.alert.show"
-			bordered
-			:row-key="(record) => record.id"
-			:tool-config="toolConfig"
-			:row-selection="options.rowSelection"
+			:pagination="false"
+			row-key="id"
+			class="data-table"
+			:locale="{ emptyText: '暂无数据' }"
 		>
-			<template #operator class="table-operator">
-				<a-space>
-					<a-button type="primary" @click="formRef.onOpen()" v-if="hasPerm('tExamPaperAdd')">
-						<template #icon><plus-outlined /></template>
-						新增
-					</a-button>
-					<xn-batch-delete
-						v-if="hasPerm('tExamPaperBatchDelete')"
-						:selectedRowKeys="selectedRowKeys"
-						@batchDelete="deleteBatchTExamPaper"
-					/>
-				</a-space>
-			</template>
 			<template #bodyCell="{ column, record }">
-				<template v-if="column.dataIndex === 'action'">
-					<a-space>
-						<a @click="formRef.onOpen(record)" v-if="hasPerm('tExamPaperEdit')">编辑</a>
-						<a-divider type="vertical" v-if="hasPerm(['tExamPaperEdit', 'tExamPaperDelete'], 'and')" />
-						<a-popconfirm title="确定要删除吗?" @confirm="deleteTExamPaper(record)">
-							<a-button type="link" danger size="small" v-if="hasPerm('tExamPaperDelete')">删除</a-button>
-						</a-popconfirm>
-					</a-space>
+				<template v-if="column.key === 'action'">
+					<a-button size="small" @click="openDrawer('edit', record.id)">编辑</a-button>
+					<a-button size="small" type="primary" danger @click="deletePaper(record)" style="margin-left: 8px">
+						删除
+					</a-button>
 				</template>
 			</template>
-		</s-table>
-	</a-card>
-	<Form ref="formRef" @successful="table.refresh(true)" />
+		</a-table>
+
+		<!-- 分页 -->
+		<a-pagination
+			v-if="total > 0"
+			:current="queryParam.pageIndex"
+			:page-size="queryParam.pageSize"
+			:total="total"
+			:show-size-changer="true"
+			:show-quick-jumper="true"
+			:show-total="(total, range) => `第 ${range[0]}-${range[1]} 条/共 ${total} 条`"
+			@change="handlePageChange"
+			@show-size-change="handlePageSizeChange"
+			class="pagination"
+		/>
+
+		<!-- 编辑/添加 抽屉 -->
+		<a-drawer
+			:visible="drawerVisible"
+			:title="drawerTitle"
+			placement="right"
+			width="900"
+			@close="closeDrawer"
+			destroyOnClose
+		>
+			<FormEdit v-if="drawerVisible" :id="editId" @success="onEditSuccess" />
+		</a-drawer>
+	</div>
 </template>
 
-<script setup name="exampaper">
-	import Form from './form.vue'
-	import tExamPaperApi from '@/api/paper/examPaperApi'
-	import tool from "@/utils/tool";
-	const formState = ref({
+<script setup>
+	import { ref, reactive, computed, onMounted } from 'vue'
+	import { message, Modal } from 'ant-design-vue'
+	import { useExamStore } from '@/store/exam'
+	import examPaperApi from '@/api/exam/paper/examPaperApi'
+	import FormEdit from './form.vue'
+
+	const examStore = useExamStore()
+
+	// 响应式数据
+	const queryParam = reactive({
 		id: null,
-		name: null,
+		level: null,
 		subjectId: null,
+		current: 1,
+		size: 10
 	})
-	const gradeLevelOptions = tool.dictList('SEMESTER')
-	const susbjectOptions = tool.dictList('SUBJECT')
-	const paperTypeOptions = tool.dictList('PAPER_TYPE')
-
-	const searchFormRef = ref()
-    const table = ref()
-    const formRef = ref()
-    const toolConfig = { refresh: true, height: true, columnSetting: true, striped: false }
-    const columns = [
-        {
-            title: 'ID',
-            dataIndex: 'ID'
-        },
-        {
-            title: '学科',
-            dataIndex: 'subjectId'
-        },
-        {
-            title: '名称',
-            dataIndex: 'name'
-        },
-        {
-            title: '创建时间',
-            dataIndex: 'createTime'
-        }
-    ]
-    // 操作栏通过权限判断是否显示
-    if (hasPerm(['tExamPaperEdit', 'tExamPaperDelete'])) {
-        columns.push({
-            title: '操作',
-            dataIndex: 'action',
-            align: 'center',
-            width: '150px'
-        })
-    }
-    const selectedRowKeys = ref([])
-    // 列表选择配置
-    const options = {
-        // columns数字类型字段加入 needTotal: true 可以勾选自动算账
-        alert: {
-            show: true,
-            clear: () => {
-                selectedRowKeys.value = ref([])
-            }
-        },
-        rowSelection: {
-            onChange: (selectedRowKey, selectedRows) => {
-                selectedRowKeys.value = selectedRowKey
-            }
-        }
-    }
-    const loadData = (parameter) => {
-        return tExamPaperApi.tExamPaperPage(Object.assign(parameter, formState.value)).then((data) => {
-            return data
-        })
-    }
-    // 重置
-    const reset = () => {
-        searchFormRef.value.resetFields()
-        table.value.refresh(true)
-    }
-    // 删除
-    const deleteTExamPaper = (record) => {
-        let params = [
-            {
-                id: record.id
-            }
-        ]
-        tExamPaperApi.tExamPaperDelete(params).then(() => {
-            table.value.refresh(true)
-        })
-    }
-    // 批量删除
-    const deleteBatchTExamPaper = (params) => {
-		tExamPaperApi.tExamPaperDelete(params).then(() => {
-            table.value.clearRefreshSelected()
-        })
-    }
+
+	const subjectFilter = ref([])
+	const listLoading = ref(true)
+	const tableData = ref([])
+	const total = ref(0)
+
+	// Drawer 控制
+	const drawerVisible = ref(false)
+	const drawerTitle = ref('')
+	const editId = ref(null)
+
+	function openDrawer(type, id = null) {
+		if (type === 'add') {
+			drawerTitle.value = '添加试卷'
+			editId.value = null
+		} else {
+			drawerTitle.value = '编辑试卷'
+			editId.value = id
+		}
+		drawerVisible.value = true
+	}
+	function closeDrawer() {
+		drawerVisible.value = false
+	}
+	function onEditSuccess() {
+		closeDrawer()
+		search()
+	}
+
+	// 表格列配置
+	const columns = [
+		{
+			title: 'Id',
+			dataIndex: 'id',
+			key: 'id',
+			width: 90
+		},
+		{
+			title: '学科',
+			dataIndex: 'subjectId',
+			key: 'subjectId',
+			width: 120,
+			customRender: ({ text }) => examStore.subjectEnumFormat(text)
+		},
+		{
+			title: '名称',
+			dataIndex: 'name',
+			key: 'name'
+		},
+		{
+			title: '创建时间',
+			dataIndex: 'createTime',
+			key: 'createTime',
+			width: 160
+		},
+		{
+			title: '操作',
+			key: 'action',
+			width: 160,
+			align: 'center'
+		}
+	]
+	// 计算属性
+	const levelEnum = computed(() => examStore.getLevelEnum)
 
 	// 方法
-	const handleSearch = () => {
-		loadData()
+	const submitForm = () => {
+		queryParam.pageIndex = 1
+		search()
 	}
 
-	const handleReset = () => {
-		searchValue.value = null
-		majorIdName.value = null
-		resourceName.value = null
-		formState.fileName = null
-		formState.resourceType = null
-		formState.resourceTwoType = null
-		formState.suffix = null
-		formState.collegeTwoId = null
-		// formState.majorId = null
-		formState.collegeId = null
-		formState.collegeThreeId = null
-		loadData()
+	const search = async () => {
+		listLoading.value = true
+		try {
+			const response = await examPaperApi.pageList(queryParam)
+			if (response) {
+				const data = response
+				tableData.value = data.records || []
+				total.value = data.total || 0
+				queryParam.pageIndex = data.current || 1
+			} else {
+				message.error(response.message || '获取数据失败')
+			}
+		} catch (error) {
+			console.error('获取试卷列表失败:', error)
+			message.error('获取数据失败')
+		} finally {
+			listLoading.value = false
+		}
 	}
+
+	const deletePaper = async (row) => {
+		try {
+			// 显示确认对话框
+			const confirmed = await new Promise((resolve) => {
+				Modal.confirm({
+					title: '确认删除',
+					content: `确定要删除试卷"${row.name}"吗?`,
+					okText: '确定',
+					cancelText: '取消',
+					onOk: () => resolve(true),
+					onCancel: () => resolve(false)
+				})
+			})
+
+			if (!confirmed) return
+
+			const response = await examPaperApi.tExamPaperDelete({ id: row.id })
+			if (response.code === 1) {
+				message.success(response.message || '删除成功')
+				search()
+			} else {
+				message.error(response.message || '删除失败')
+			}
+		} catch (error) {
+			console.error('删除试卷失败:', error)
+			message.error('删除失败')
+		}
+	}
+
+	const levelChange = () => {
+		queryParam.subjectId = null
+		subjectFilter.value = examStore.subjects.filter((data) => data.level === queryParam.level)
+	}
+
+	const handlePageChange = (page, pageSize) => {
+		queryParam.pageIndex = page
+		queryParam.pageSize = pageSize
+		search()
+	}
+
+	const handlePageSizeChange = (current, size) => {
+		queryParam.pageIndex = 1
+		queryParam.pageSize = size
+		search()
+	}
+
+	// 生命周期
+	onMounted(async () => {
+		examStore.initSubject(search)
+	})
 </script>
+
+<style lang="less" scoped>
+	.app-container {
+		padding: 24px;
+		background: #fff;
+
+		.search-form {
+			margin-bottom: 24px;
+			padding: 24px;
+			background: #fafafa;
+			border-radius: 6px;
+		}
+
+		.data-table {
+			margin-bottom: 24px;
+		}
+
+		.pagination {
+			text-align: right;
+		}
+	}
+</style>

+ 73 - 58
src/views/exm/question/components/Show.vue

@@ -1,62 +1,77 @@
 <template>
-  <div style="line-height:1.8">
-    <div v-if="qType==1" v-loading="qLoading">
-      <div class="q-title" v-html="question.title"/>
-      <div class="q-content">
-          <span :key="item.id" v-for="item in question.items" class="q-item-contain">
-            <span class="q-item-prefix">{{item.prefix}}</span>
-            <span v-html="item.content" class="q-item-content"></span>
-          </span>
-      </div>
-    </div>
-    <div v-else-if="qType==2" v-loading="qLoading">
-      <div class="q-title" v-html="question.title"/>
-      <div class="q-content">
-          <span :key="item.id" v-for="item in question.items" class="q-item-contain">
-            <span class="q-item-prefix">{{item.prefix}}</span>
-            <span v-html="item.content" class="q-item-content"></span>
-          </span>
-      </div>
-    </div>
-    <div v-else-if="qType==3" v-loading="qLoading">
-      <div class="q-title" v-html="question.title" style="display: inline;margin-right: 10px"/>
-      <span>(</span>
-      <span :key="item.id" v-for="item in question.items">
-        <span v-html="item.content" class="q-item-content"></span>
-       </span>
-      <span>)</span>
-    </div>
-    <div v-else-if="qType==4" v-loading="qLoading">
-      <div class="q-title" v-html="question.title"/>
-    </div>
-    <div v-else-if="qType==5" v-loading="qLoading">
-      <div class="q-title" v-html="question.title"/>
-    </div>
-    <div v-else>
-    </div>
-  </div>
-
+	<div style="line-height: 1.8">
+		<template v-if="qType === 1">
+			<div class="q-title" v-html="question.title"></div>
+			<div class="q-content">
+				<span v-for="item in question.items" :key="item.id" class="q-item-contain">
+					<span class="q-item-prefix">{{ item.prefix }}</span>
+					<span v-html="item.content" class="q-item-content"></span>
+				</span>
+			</div>
+		</template>
+		<template v-else-if="qType === 2">
+			<div class="q-title" v-html="question.title"></div>
+			<div class="q-content">
+				<span v-for="item in question.items" :key="item.id" class="q-item-contain">
+					<span class="q-item-prefix">{{ item.prefix }}</span>
+					<span v-html="item.content" class="q-item-content"></span>
+				</span>
+			</div>
+		</template>
+		<template v-else-if="qType === 3">
+			<div class="q-title" v-html="question.title" style="display: inline; margin-right: 10px"></div>
+			<span>(</span>
+			<span v-for="item in question.items" :key="item.id">
+				<span v-html="item.content" class="q-item-content"></span>
+			</span>
+			<span>)</span>
+		</template>
+		<template v-else-if="qType === 4">
+			<div class="q-title" v-html="question.title"></div>
+		</template>
+		<template v-else-if="qType === 5">
+			<div class="q-title" v-html="question.title"></div>
+		</template>
+		<template v-else> </template>
+	</div>
 </template>
 
-<script>
-export default {
-  name: 'QuestionShow',
-  props: {
-    question: {
-      type: Object,
-      default: function () {
-        return {}
-      }
-    },
-    qLoading: {
-      type: Boolean,
-      default: false
-    },
-    qType: {
-      type: Number,
-      default: 0
-    }
-  },
-  methods: {}
-}
+<script setup>
+	import { computed } from 'vue'
+	const props = defineProps({
+		question: {
+			type: Object,
+			default: () => ({})
+		},
+		qLoading: {
+			type: Boolean,
+			default: false
+		},
+		qType: {
+			type: Number,
+			default: 0
+		}
+	})
 </script>
+
+<style scoped>
+	.q-title {
+		font-weight: bold;
+		margin-bottom: 8px;
+	}
+	.q-content {
+		margin-left: 16px;
+	}
+	.q-item-contain {
+		display: inline-block;
+		margin-right: 16px;
+		margin-bottom: 4px;
+	}
+	.q-item-prefix {
+		font-weight: bold;
+		margin-right: 4px;
+	}
+	.q-item-content {
+		display: inline;
+	}
+</style>

+ 255 - 0
src/views/exm/question/edit/gap-filling.vue

@@ -0,0 +1,255 @@
+<template>
+	<div class="app-container">
+		<a-form :model="form" ref="formRef" :rules="rules" layout="vertical">
+			<a-form-item label="年级:" name="gradeLevel" required>
+				<a-select v-model:value="form.gradeLevel" placeholder="年级" @change="levelChange">
+					<a-select-option v-for="item in levelEnum" :key="item.key" :value="item.key">
+						{{ item.value }}
+					</a-select-option>
+				</a-select>
+			</a-form-item>
+			<a-form-item label="学科:" name="subjectId" required>
+				<a-select v-model:value="form.subjectId" placeholder="学科">
+					<a-select-option v-for="item in subjectFilter" :key="item.id" :value="item.id">
+						{{ item.name + ' ( ' + item.levelName + ' )' }}
+					</a-select-option>
+				</a-select>
+			</a-form-item>
+			<a-form-item label="题干:" name="title" required>
+				<a-input v-model:value="form.title" readonly @click="inputClick(form, 'title')" />
+			</a-form-item>
+			<a-form-item label="填空答案:" required>
+				<div v-for="item in form.items" :key="item.prefix" class="question-item-label">
+					<a-input
+						v-model:value="item.content"
+						readonly
+						@click="inputClick(item, 'content')"
+						class="question-item-content-input"
+						style="width: 80%"
+					/>
+					<span class="question-item-span">分数:</span>
+					<a-input-number v-model:value="item.score" :precision="1" :step="1" :max="100" />
+				</div>
+			</a-form-item>
+			<a-form-item label="解析:" name="analyze" required>
+				<a-input v-model:value="form.analyze" readonly @click="inputClick(form, 'analyze')" />
+			</a-form-item>
+			<a-form-item label="分数:" name="score" required>
+				<a-input-number v-model:value="form.score" :precision="1" :step="1" :max="100" />
+			</a-form-item>
+			<a-form-item label="难度:" required>
+				<a-rate v-model:value="form.difficult" class="question-item-rate" />
+			</a-form-item>
+			<a-form-item>
+				<a-button type="primary" @click="submitForm" :loading="formLoading">提交</a-button>
+				<a-button @click="resetForm">重置</a-button>
+				<a-button type="success" @click="showQuestion">预览</a-button>
+			</a-form-item>
+		</a-form>
+		<a-modal
+			v-model:visible="richEditor.dialogVisible"
+			width="800px"
+			:footer="null"
+			:closable="false"
+			centered
+			destroy-on-close
+		>
+			<Editor v-model="richEditorContent" :height="300" />
+			<div style="text-align: right; margin-top: 16px">
+				<a-button type="primary" @click="editorConfirm">确定</a-button>
+				<a-button @click="richEditor.dialogVisible = false">取消</a-button>
+			</div>
+		</a-modal>
+		<a-modal v-model:visible="questionShow.dialog" width="800px" :footer="null" :bodyStyle="{ padding: '24px' }">
+			<QuestionShow :qType="questionShow.qType" :question="questionShow.question" :qLoading="questionShow.loading" />
+		</a-modal>
+	</div>
+</template>
+
+<script setup>
+	import { ref, reactive, computed, onMounted } from 'vue'
+	import { message } from 'ant-design-vue'
+	import { useExamStore } from '@/store/exam'
+	import tQuestionApi from '@/api/exam/question/tQuestionApi'
+	import QuestionShow from '../components/Show.vue'
+	import Editor from '@/components/Editor/index.vue'
+
+	const examStore = useExamStore()
+	const props = defineProps({
+		id: {
+			type: Number,
+			default: 0
+		}
+	})
+	const emit = defineEmits(['successful'])
+	const formRef = ref()
+	const form = reactive({
+		id: null,
+		questionType: 4,
+		gradeLevel: null,
+		subjectId: null,
+		title: '',
+		items: [],
+		analyze: '',
+		correct: '',
+		score: '',
+		difficult: 0
+	})
+	const subjectFilter = ref([])
+	const formLoading = ref(false)
+	const rules = {
+		gradeLevel: [{ required: true, message: '请选择年级', trigger: 'change' }],
+		subjectId: [{ required: true, message: '请选择学科', trigger: 'change' }],
+		title: [{ required: true, message: '请输入题干', trigger: 'blur' }],
+		analyze: [{ required: true, message: '请输入解析', trigger: 'blur' }],
+		score: [{ required: true, message: '请输入分数', trigger: 'blur' }]
+	}
+	const richEditor = reactive({
+		dialogVisible: false,
+		object: null,
+		parameterName: '',
+		instance: null
+	})
+	const richEditorContent = ref('')
+	const questionShow = reactive({
+		qType: 0,
+		dialog: false,
+		question: null,
+		loading: false
+	})
+	const levelEnum = computed(() => examStore.levelEnum)
+	const subjects = computed(() => examStore.subjects)
+
+	onMounted(async () => {
+		await examStore.initSubject()
+		subjectFilter.value = subjects.value
+		const id = props.id
+		if (id && parseInt(id) !== 0) {
+			formLoading.value = true
+			tQuestionApi.select(id).then((re) => {
+				Object.assign(form, re)
+				formLoading.value = false
+			})
+		}
+	})
+
+	function inputClick(object, parameterName) {
+		richEditor.object = object
+		richEditor.parameterName = parameterName
+		richEditorContent.value = object[parameterName] || ''
+		richEditor.dialogVisible = true
+	}
+
+	function editorConfirm() {
+		if (richEditor.parameterName === 'title') {
+			if (questionItemReset(richEditorContent.value)) {
+				richEditor.object[richEditor.parameterName] = richEditorContent.value
+				richEditor.dialogVisible = false
+			}
+		} else {
+			richEditor.object[richEditor.parameterName] = richEditorContent.value
+			richEditor.dialogVisible = false
+		}
+	}
+
+	function questionItemReset(content) {
+		const spanRegex = /<span class="gapfilling-span (.*?)">(.*?)<\/span>/g
+		const newFormItem = []
+		const gapfillingItems = content.match(spanRegex)
+		if (gapfillingItems === null) {
+			message.error('请插入填空')
+			return false
+		}
+		gapfillingItems.forEach((span) => {
+			const pairRegex = /<span class="gapfilling-span (.*?)">(.*?)<\/span>/
+			pairRegex.test(span)
+			newFormItem.push({ id: null, itemUuid: RegExp.$1, prefix: RegExp.$2, content: '', score: '0' })
+		})
+		newFormItem.forEach((item) => {
+			form.items.some((oldItem) => {
+				if (oldItem.itemUuid === item.itemUuid) {
+					item.content = oldItem.content
+					item.id = oldItem.id
+					item.score = oldItem.score
+					return true
+				}
+			})
+		})
+		form.items = newFormItem
+		return true
+	}
+
+	function submitForm() {
+		formRef.value.validate().then((valid) => {
+			if (valid) {
+				formLoading.value = true
+				tQuestionApi
+					.edit(form)
+					.then(() => {
+						emit('successful')
+						formLoading.value = false
+					})
+					.catch(() => {
+						formLoading.value = false
+					})
+			}
+		})
+	}
+
+	function levelChange() {
+		form.subjectId = null
+		subjectFilter.value = subjects.value.filter((data) => data.level === form.gradeLevel)
+	}
+
+	function showQuestion() {
+		questionShow.dialog = true
+		questionShow.qType = form.questionType
+		questionShow.question = { ...form }
+	}
+
+	function resetForm() {
+		const lastId = form.id
+		formRef.value.resetFields()
+		Object.assign(form, {
+			id: null,
+			questionType: 4,
+			gradeLevel: null,
+			subjectId: null,
+			title: '',
+			items: [],
+			analyze: '',
+			correct: '',
+			score: '',
+			difficult: 0
+		})
+		form.id = lastId
+	}
+</script>
+
+<style lang="less" scoped>
+	.app-container {
+		background: #fff;
+		padding: 24px;
+		border-radius: 8px;
+	}
+	.question-item-label {
+		margin-top: 10px;
+		margin-bottom: 10px !important;
+	}
+	.question-item-content-input {
+		margin-left: 8px;
+		width: 60%;
+		height: 20px;
+	}
+	.question-item-span {
+		vertical-align: middle;
+		font-size: 14px;
+		color: #606266;
+		font-weight: 700;
+		box-sizing: border-box;
+		margin-left: 10px;
+	}
+	.question-item-rate {
+		line-height: 2.5;
+	}
+</style>

+ 251 - 0
src/views/exm/question/edit/multiple-choice.vue

@@ -0,0 +1,251 @@
+<template>
+	<div class="app-container">
+		<a-form :model="form" ref="formRef" :rules="rules" layout="vertical">
+			<a-form-item label="年级:" name="gradeLevel" required>
+				<a-select v-model:value="form.gradeLevel" placeholder="年级" @change="levelChange">
+					<a-select-option v-for="item in levelEnum" :key="item.key" :value="item.key">
+						{{ item.value }}
+					</a-select-option>
+				</a-select>
+			</a-form-item>
+			<a-form-item label="学科:" name="subjectId" required>
+				<a-select v-model:value="form.subjectId" placeholder="学科">
+					<a-select-option v-for="item in subjectFilter" :key="item.id" :value="item.id">
+						{{ item.name + ' ( ' + item.levelName + ' )' }}
+					</a-select-option>
+				</a-select>
+			</a-form-item>
+			<a-form-item label="题干:" name="title" required>
+				<a-input v-model:value="form.title" readonly @click="inputClick(form, 'title')" />
+			</a-form-item>
+			<a-form-item label="选项:" required>
+				<div v-for="(item, index) in form.items" :key="item.prefix" class="question-item-label">
+					<a-input v-model:value="item.prefix" style="width: 50px; margin-right: 8px" />
+					<a-input
+						v-model:value="item.content"
+						readonly
+						@click="inputClick(item, 'content')"
+						class="question-item-content-input"
+						style="width: 60%"
+					/>
+					<a-button danger size="small" class="question-item-remove" @click="questionItemRemove(index)">删除</a-button>
+				</div>
+			</a-form-item>
+			<a-form-item label="解析:" name="analyze" required>
+				<a-input v-model:value="form.analyze" readonly @click="inputClick(form, 'analyze')" />
+			</a-form-item>
+			<a-form-item label="分数:" name="score" required>
+				<a-input-number v-model:value="form.score" :precision="1" :step="1" :max="100" />
+			</a-form-item>
+			<a-form-item label="难度:" required>
+				<a-rate v-model:value="form.difficult" class="question-item-rate" />
+			</a-form-item>
+			<a-form-item label="正确答案:" name="correctArray" required>
+				<a-checkbox-group v-model:value="form.correctArray">
+					<a-checkbox v-for="item in form.items" :value="item.prefix" :key="item.prefix">{{ item.prefix }}</a-checkbox>
+				</a-checkbox-group>
+			</a-form-item>
+			<a-form-item>
+				<a-button type="primary" @click="submitForm" :loading="formLoading">提交</a-button>
+				<a-button @click="resetForm">重置</a-button>
+				<a-button type="success" @click="questionItemAdd">添加选项</a-button>
+				<a-button type="success" @click="showQuestion">预览</a-button>
+			</a-form-item>
+		</a-form>
+		<a-modal
+			v-model:visible="richEditor.dialogVisible"
+			width="800px"
+			:footer="null"
+			:closable="false"
+			centered
+			destroy-on-close
+		>
+			<Editor v-model="richEditorContent" :height="300" />
+			<div style="text-align: right; margin-top: 16px">
+				<a-button type="primary" @click="editorConfirm">确定</a-button>
+				<a-button @click="richEditor.dialogVisible = false">取消</a-button>
+			</div>
+		</a-modal>
+		<a-modal v-model:visible="questionShow.dialog" width="800px" :footer="null" :bodyStyle="{ padding: '24px' }">
+			<QuestionShow :qType="questionShow.qType" :question="questionShow.question" :qLoading="questionShow.loading" />
+		</a-modal>
+	</div>
+</template>
+
+<script setup>
+	import { ref, reactive, computed, onMounted } from 'vue'
+	import { message } from 'ant-design-vue'
+	import { useExamStore } from '@/store/exam'
+	import tQuestionApi from '@/api/exam/question/tQuestionApi'
+	import QuestionShow from '../components/Show.vue'
+	import Editor from '@/components/Editor/index.vue'
+
+	const examStore = useExamStore()
+	const props = defineProps({
+		id: {
+			type: Number,
+			default: 0
+		}
+	})
+	const emit = defineEmits(['successful'])
+	const formRef = ref()
+	const form = reactive({
+		id: null,
+		questionType: 2,
+		gradeLevel: null,
+		subjectId: null,
+		title: '',
+		items: [
+			{ id: null, prefix: 'A', content: '' },
+			{ id: null, prefix: 'B', content: '' },
+			{ id: null, prefix: 'C', content: '' },
+			{ id: null, prefix: 'D', content: '' }
+		],
+		analyze: '',
+		correct: '',
+		correctArray: [],
+		score: '',
+		difficult: 0
+	})
+	const subjectFilter = ref([])
+	const formLoading = ref(false)
+	const rules = {
+		gradeLevel: [{ required: true, message: '请选择年级', trigger: 'change' }],
+		subjectId: [{ required: true, message: '请选择学科', trigger: 'change' }],
+		title: [{ required: true, message: '请输入题干', trigger: 'blur' }],
+		analyze: [{ required: true, message: '请输入解析', trigger: 'blur' }],
+		score: [{ required: true, message: '请输入分数', trigger: 'blur' }],
+		correctArray: [{ required: true, message: '请选择正确答案', trigger: 'change' }]
+	}
+	const richEditor = reactive({
+		dialogVisible: false,
+		object: null,
+		parameterName: '',
+		instance: null
+	})
+	const richEditorContent = ref('')
+	const questionShow = reactive({
+		qType: 0,
+		dialog: false,
+		question: null,
+		loading: false
+	})
+	const levelEnum = computed(() => examStore.levelEnum)
+	const subjects = computed(() => examStore.subjects)
+
+	onMounted(async () => {
+		await examStore.initSubject()
+		subjectFilter.value = subjects.value
+		const id = props.id
+		if (id && parseInt(id) !== 0) {
+			formLoading.value = true
+			tQuestionApi.select(id).then((re) => {
+				Object.assign(form, re)
+				formLoading.value = false
+			})
+		}
+	})
+
+	function inputClick(object, parameterName) {
+		richEditor.object = object
+		richEditor.parameterName = parameterName
+		richEditorContent.value = object[parameterName] || ''
+		richEditor.dialogVisible = true
+	}
+
+	function editorConfirm() {
+		richEditor.object[richEditor.parameterName] = richEditorContent.value
+		richEditor.dialogVisible = false
+	}
+
+	function questionItemRemove(index) {
+		form.items.splice(index, 1)
+	}
+
+	function questionItemAdd() {
+		const items = form.items
+		let newLastPrefix
+		if (items.length > 0) {
+			let last = items[items.length - 1]
+			newLastPrefix = String.fromCharCode(last.prefix.charCodeAt() + 1)
+		} else {
+			newLastPrefix = 'A'
+		}
+		items.push({ id: null, prefix: newLastPrefix, content: '' })
+	}
+
+	function submitForm() {
+		formRef.value.validate().then((valid) => {
+			if (valid) {
+				formLoading.value = true
+				tQuestionApi
+					.edit(form)
+					.then((re) => {
+						emit('successful')
+						formLoading.value = false
+					})
+					.catch(() => {
+						formLoading.value = false
+					})
+			}
+		})
+	}
+
+	function levelChange() {
+		form.subjectId = null
+		subjectFilter.value = subjects.value.filter((data) => data.level === form.gradeLevel)
+	}
+
+	function showQuestion() {
+		questionShow.dialog = true
+		questionShow.qType = form.questionType
+		questionShow.question = { ...form }
+	}
+
+	function resetForm() {
+		const lastId = form.id
+		formRef.value.resetFields()
+		Object.assign(form, {
+			id: null,
+			questionType: 2,
+			gradeLevel: null,
+			subjectId: null,
+			title: '',
+			items: [
+				{ id: null, prefix: 'A', content: '' },
+				{ id: null, prefix: 'B', content: '' },
+				{ id: null, prefix: 'C', content: '' },
+				{ id: null, prefix: 'D', content: '' }
+			],
+			analyze: '',
+			correct: '',
+			correctArray: [],
+			score: '',
+			difficult: 0
+		})
+		form.id = lastId
+	}
+</script>
+
+<style lang="less" scoped>
+	.app-container {
+		background: #fff;
+		padding: 24px;
+		border-radius: 8px;
+	}
+	.question-item-label {
+		margin-top: 10px;
+		margin-bottom: 10px !important;
+	}
+	.question-item-remove {
+		margin-left: 20px;
+	}
+	.question-item-content-input {
+		margin-left: 8px;
+		width: 60%;
+		height: 20px;
+	}
+	.question-item-rate {
+		line-height: 2.5;
+	}
+</style>

+ 195 - 0
src/views/exm/question/edit/short-answer.vue

@@ -0,0 +1,195 @@
+<template>
+	<div class="app-container">
+		<a-form :model="form" ref="formRef" :rules="rules" layout="vertical">
+			<a-form-item label="年级:" name="gradeLevel" required>
+				<a-select v-model:value="form.gradeLevel" placeholder="年级" @change="levelChange">
+					<a-select-option v-for="item in levelEnum" :key="item.key" :value="item.key">
+						{{ item.value }}
+					</a-select-option>
+				</a-select>
+			</a-form-item>
+			<a-form-item label="学科:" name="subjectId" required>
+				<a-select v-model:value="form.subjectId" placeholder="学科">
+					<a-select-option v-for="item in subjectFilter" :key="item.id" :value="item.id">
+						{{ item.name + ' ( ' + item.levelName + ' )' }}
+					</a-select-option>
+				</a-select>
+			</a-form-item>
+			<a-form-item label="题干:" name="title" required>
+				<a-input v-model:value="form.title" readonly @click="inputClick(form, 'title')" />
+			</a-form-item>
+			<a-form-item label="答案:" name="correct" required>
+				<a-input v-model:value="form.correct" readonly @click="inputClick(form, 'correct')" />
+			</a-form-item>
+			<a-form-item label="解析:" name="analyze" required>
+				<a-input v-model:value="form.analyze" readonly @click="inputClick(form, 'analyze')" />
+			</a-form-item>
+			<a-form-item label="分数:" name="score" required>
+				<a-input-number v-model:value="form.score" :precision="1" :step="1" :max="100" />
+			</a-form-item>
+			<a-form-item label="难度:" required>
+				<a-rate v-model:value="form.difficult" class="question-item-rate" />
+			</a-form-item>
+			<a-form-item>
+				<a-button type="primary" @click="submitForm" :loading="formLoading">提交</a-button>
+				<a-button @click="resetForm">重置</a-button>
+				<a-button type="success" @click="showQuestion">预览</a-button>
+			</a-form-item>
+		</a-form>
+		<a-modal
+			v-model:visible="richEditor.dialogVisible"
+			width="800px"
+			:footer="null"
+			:closable="false"
+			centered
+			destroy-on-close
+		>
+			<Editor v-model="richEditorContent" :height="300" />
+			<div style="text-align: right; margin-top: 16px">
+				<a-button type="primary" @click="editorConfirm">确定</a-button>
+				<a-button @click="richEditor.dialogVisible = false">取消</a-button>
+			</div>
+		</a-modal>
+		<a-modal v-model:visible="questionShow.dialog" width="800px" :footer="null" :bodyStyle="{ padding: '24px' }">
+			<QuestionShow :qType="questionShow.qType" :question="questionShow.question" :qLoading="questionShow.loading" />
+		</a-modal>
+	</div>
+</template>
+
+<script setup>
+	import { ref, reactive, computed, onMounted } from 'vue'
+	import { message } from 'ant-design-vue'
+	import { useExamStore } from '@/store/exam'
+	import tQuestionApi from '@/api/exam/question/tQuestionApi'
+	import QuestionShow from '../components/Show.vue'
+	import Editor from '@/components/Editor/index.vue'
+
+	const examStore = useExamStore()
+	const props = defineProps({
+		id: {
+			type: Number,
+			default: 0
+		}
+	})
+	const emit = defineEmits(['successful'])
+	const formRef = ref()
+	const form = reactive({
+		id: null,
+		questionType: 5,
+		gradeLevel: null,
+		subjectId: null,
+		title: '',
+		items: [],
+		analyze: '',
+		correct: '',
+		score: '',
+		difficult: 0
+	})
+	const subjectFilter = ref([])
+	const formLoading = ref(false)
+	const rules = {
+		gradeLevel: [{ required: true, message: '请选择年级', trigger: 'change' }],
+		subjectId: [{ required: true, message: '请选择学科', trigger: 'change' }],
+		title: [{ required: true, message: '请输入题干', trigger: 'blur' }],
+		correct: [{ required: true, message: '请输入答案', trigger: 'blur' }],
+		analyze: [{ required: true, message: '请输入解析', trigger: 'blur' }],
+		score: [{ required: true, message: '请输入分数', trigger: 'blur' }]
+	}
+	const richEditor = reactive({
+		dialogVisible: false,
+		object: null,
+		parameterName: '',
+		instance: null
+	})
+	const richEditorContent = ref('')
+	const questionShow = reactive({
+		qType: 0,
+		dialog: false,
+		question: null,
+		loading: false
+	})
+	const levelEnum = computed(() => examStore.levelEnum)
+	const subjects = computed(() => examStore.subjects)
+
+	onMounted(async () => {
+		await examStore.initSubject()
+		subjectFilter.value = subjects.value
+		const id = props.id
+		if (id && parseInt(id) !== 0) {
+			formLoading.value = true
+			tQuestionApi.select(id).then((re) => {
+				Object.assign(form, re)
+				formLoading.value = false
+			})
+		}
+	})
+
+	function inputClick(object, parameterName) {
+		richEditor.object = object
+		richEditor.parameterName = parameterName
+		richEditorContent.value = object[parameterName] || ''
+		richEditor.dialogVisible = true
+	}
+
+	function editorConfirm() {
+		richEditor.object[richEditor.parameterName] = richEditorContent.value
+		richEditor.dialogVisible = false
+	}
+
+	function submitForm() {
+		formRef.value.validate().then((valid) => {
+			if (valid) {
+				formLoading.value = true
+				tQuestionApi
+					.edit(form)
+					.then(() => {
+						emit('successful')
+						formLoading.value = false
+					})
+					.catch(() => {
+						formLoading.value = false
+					})
+			}
+		})
+	}
+
+	function resetForm() {
+		const lastId = form.id
+		formRef.value.resetFields()
+		Object.assign(form, {
+			id: null,
+			questionType: 5,
+			gradeLevel: null,
+			subjectId: null,
+			title: '',
+			items: [],
+			analyze: '',
+			correct: '',
+			score: '',
+			difficult: 0
+		})
+		form.id = lastId
+	}
+
+	function levelChange() {
+		form.subjectId = null
+		subjectFilter.value = subjects.value.filter((data) => data.level === form.gradeLevel)
+	}
+
+	function showQuestion() {
+		questionShow.dialog = true
+		questionShow.qType = form.questionType
+		questionShow.question = { ...form }
+	}
+</script>
+
+<style lang="less" scoped>
+	.app-container {
+		background: #fff;
+		padding: 24px;
+		border-radius: 8px;
+	}
+	.question-item-rate {
+		line-height: 2.5;
+	}
+</style>

+ 209 - 239
src/views/exm/question/edit/single-choice.vue

@@ -1,222 +1,95 @@
 <template>
 	<div class="app-container">
-		<a-spin :spinning="formLoading" tip="加载中...">
-			<a-form :model="form" ref="formRef" label-width="100px" :rules="rules">
-				<a-form-item label="年级:" prop="gradeLevel" required>
-					<a-select v-model:value="form.gradeLevel" placeholder="年级" :options="gradeLevelOptions"  clearable />
-				</a-form-item>
-				<a-form-item label="学科:" prop="subjectId" required>
-					<a-select v-model:value="form.subjectId" placeholder="学科" :options="susbjectOptions" />
-				</a-form-item>
-				<a-form-item label="题干:" prop="title" required>
-					<a-input v-model="form.title"   @focus="inputClick(form,'title')" />
-					<xn-editor v-model:value="form.title"  :height="150"/>
-				</a-form-item>
-				<a-form-item label="选项:" required>
-					<a-form-item :key="item.prefix" v-for="(item, index) in form.items" label-width="50px" class="question-item-label">
-						<template #label>
-							{{ item.prefix }}
-						</template>
-						<a-input v-model:value="item.prefix" style="width:50px;" />
-						<a-button type="primary" danger class="question-item-remove" @click="questionItemRemove(index)">删除</a-button>
-						<xn-editor v-model:value="item.content"  :height="100"/>
-					</a-form-item>
-				</a-form-item>
-				<a-form-item label="解析:" prop="analyze" required>
-					<xn-editor v-model:value="form.analyze" :height="150"/>
-				</a-form-item>
-				<a-form-item label="分数:" prop="score" required>
-					<a-input-number v-model:value="form.score" :precision="1" :step="1" :max="100" />
-				</a-form-item>
-				<a-form-item label="难度:" required>
-					<a-rate v-model:value="form.difficult" class="question-item-rate" />
-				</a-form-item>
-				<a-form-item label="正确答案:" prop="correct" required>
-					<a-radio-group v-model:value="form.correct">
-						<a-radio v-for="item in form.items" :key="item.prefix" :value="item.prefix">{{ item.prefix }}</a-radio>
-					</a-radio-group>
-				</a-form-item>
-				<a-form-item>
-					<a-button type="primary" @click="submitForm" style="margin-right: 10px;">提交</a-button>
-					<a-button @click="resetForm" style="margin-right: 10px;">重置</a-button>
-					<a-button type="success" @click="questionItemAdd" style="margin-right: 10px;">添加选项</a-button>
-					<a-button type="success" @click="showQuestion">预览</a-button>
-				</a-form-item>
-			</a-form>
-		</a-spin>
-
+		<a-form :model="form" ref="formRef" :rules="rules" layout="vertical">
+			<a-form-item label="年级:" name="gradeLevel" required>
+				<a-select v-model:value="form.gradeLevel" placeholder="年级" @change="levelChange">
+					<a-select-option v-for="item in levelEnum" :key="item.key" :value="item.key">
+						{{ item.value }}
+					</a-select-option>
+				</a-select>
+			</a-form-item>
+			<a-form-item label="学科:" name="subjectId" required>
+				<a-select v-model:value="form.subjectId" placeholder="学科">
+					<a-select-option v-for="item in subjectFilter" :key="item.id" :value="item.id">
+						{{ item.name + ' ( ' + item.levelName + ' )' }}
+					</a-select-option>
+				</a-select>
+			</a-form-item>
+			<a-form-item label="题干:" name="title" required>
+				<a-input v-model:value="form.title" readonly @click="inputClick(form, 'title')" />
+			</a-form-item>
+			<a-form-item label="选项:" required>
+				<div v-for="(item, index) in form.items" :key="item.prefix" class="question-item-label">
+					<a-input v-model:value="item.prefix" style="width: 50px; margin-right: 8px" />
+					<a-input
+						v-model:value="item.content"
+						readonly
+						@click="inputClick(item, 'content')"
+						class="question-item-content-input"
+						style="width: 60%"
+					/>
+					<a-button danger size="small" class="question-item-remove" @click="questionItemRemove(index)">删除</a-button>
+				</div>
+			</a-form-item>
+			<a-form-item label="解析:" name="analyze" required>
+				<a-input v-model:value="form.analyze" readonly @click="inputClick(form, 'analyze')" />
+			</a-form-item>
+			<a-form-item label="分数:" name="score" required>
+				<a-input-number v-model:value="form.score" :precision="1" :step="1" :max="100" />
+			</a-form-item>
+			<a-form-item label="难度:" required>
+				<a-rate v-model:value="form.difficult" class="question-item-rate" />
+			</a-form-item>
+			<a-form-item label="正确答案:" name="correct" required>
+				<a-radio-group v-model:value="form.correct">
+					<a-radio v-for="item in form.items" :value="item.prefix" :key="item.prefix">{{ item.prefix }}</a-radio>
+				</a-radio-group>
+			</a-form-item>
+			<a-form-item>
+				<a-button type="primary" @click="submitForm" :loading="formLoading">提交</a-button>
+				<a-button @click="resetForm">重置</a-button>
+				<a-button type="success" @click="questionItemAdd">添加选项</a-button>
+				<a-button type="success" @click="showQuestion">预览</a-button>
+			</a-form-item>
+		</a-form>
 		<a-modal
 			v-model:visible="richEditor.dialogVisible"
+			width="800px"
 			:footer="null"
-			:body-style="{ padding: '0' }"
-			:width="800"
+			:closable="false"
 			centered
 			destroy-on-close
 		>
-			<Ueditor @ready="editorReady"/>
-			<a-button type="primary" @click="editorConfirm">确 定</a-button>
-			<a-button @click="richEditor.dialogVisible = false">取 消</a-button>
+			<Editor v-model="richEditorContent" :height="300" />
+			<div style="text-align: right; margin-top: 16px">
+				<a-button type="primary" @click="editorConfirm">确定</a-button>
+				<a-button @click="richEditor.dialogVisible = false">取消</a-button>
+			</div>
 		</a-modal>
-
-		<a-modal
-			v-model:visible="questionShow.dialog"
-			:footer="null"
-			:body-style="{ padding: '0' }"
-			:width="800"
-			centered
-			destroy-on-close
-		>
+		<a-modal v-model:visible="questionShow.dialog" width="800px" :footer="null" :bodyStyle="{ padding: '24px' }">
 			<QuestionShow :qType="questionShow.qType" :question="questionShow.question" :qLoading="questionShow.loading" />
 		</a-modal>
 	</div>
 </template>
 
 <script setup>
-import { ref, reactive, onMounted } from 'vue'
-import { useRoute, useRouter } from 'vue-router'
-import questionApi from '@/api/question/tQuestionApi'
-import QuestionShow from '@/views/exm/question/components/Show.vue'
-import tool from "@/utils/tool"
-import XnEditor from '@/components/Editor/index.vue'
-import Ueditor from '@/components/Ueditor/index.vue'
-
-
-const route = useRoute()
-const router = useRouter()
-
-const formLoading = ref(false)
-// 表单引用
-const formRef = ref()
-
-// 数据初始化
-const form = ref({
-	id: null,
-	questionType: 1,
-	gradeLevel: null,
-	subjectId: null,
-	title: '',
-	items: [
-		{ prefix: 'A', content: '' },
-		{ prefix: 'B', content: '' },
-		{ prefix: 'C', content: '' },
-		{ prefix: 'D', content: '' }
-	],
-	analyze: '',
-	correct: '',
-	score: '',
-	difficult: 0
-})
-
-const richEditor = reactive({
-	dialogVisible: false,
-	object: null,
-	parameterName: '',
-	instance: null
-})
-
-const questionShow = reactive({
-	qType: 0,
-	dialog: false,
-	question: null,
-	loading: false
-})
-
-const gradeLevelOptions = tool.dictList('SEMESTER')
-const susbjectOptions = tool.dictList('SUBJECT')
-const subjectFilter = ref(null)
-
-// 校验规则
-const rules = {
-	gradeLevel: [{ required: true, message: '请选择年级', trigger: 'change' }],
-	subjectId: [{ required: true, message: '请选择学科', trigger: 'change' }],
-	title: [{ required: true, message: '请输入题干', trigger: 'blur' }],
-	analyze: [{ required: true, message: '请输入解析', trigger: 'blur' }],
-	score: [{ required: true, message: '请输入分数', trigger: 'blur' }],
-	correct: [{ required: true, message: '请选择正确答案', trigger: 'change' }]
-}
-
-// 生命周期:组件挂载时处理路由参数
-onMounted(() => {
-	const id = route.query.id
-	if (id && parseInt(id) !== 0) {
-		formRef.value?.resetFields()
-		questionApi.select(id).then(re => {
-			Object.assign(form, re.response)
-		})
-	}
-})
-
-// 方法定义
-
-function editorReady(instance) {
-	richEditor.instance = instance
-	let currentContent = richEditor.object[richEditor.parameterName]
-	richEditor.instance.setContent(currentContent)
-	richEditor.instance.focus(true)
-}
-
-function inputClick(object, parameterName) {
-	// 获取当前聚焦的元素并移除焦点
-	const activeElement = document.activeElement
-	if (activeElement && typeof activeElement.blur === 'function') {
-		activeElement.blur()
-	}
-
-	richEditor.object = object
-	richEditor.parameterName = parameterName
-	richEditor.dialogVisible = true
-}
-
-function editorConfirm() {
-	console.log(richEditor)
-	let content = richEditor.instance.getContent()
-	richEditor.object[richEditor.parameterName] = content
-	richEditor.dialogVisible = false
-}
-
-function questionItemRemove(index) {
-	form.value.items.splice(index, 1)
-}
-
-function questionItemAdd() {
-	const items = form.value.items
-	let newLastPrefix
-	console.log('last', items)
-	if (items.length > 0) {
-		let last = items[items.length - 1]
-		newLastPrefix = String.fromCharCode(last.prefix.charCodeAt() + 1)
-	} else {
-		newLastPrefix = 'A'
-	}
-	items.push({ id: null, prefix: newLastPrefix, content: '' })
-}
-
-function submitForm() {
-	formRef.value.validate().then(valid => {
-		if (valid) {
-			formLoading.value = true
-			questionApi.edit(form).then(re => {
-				if (re.code === 1) {
-					store.dispatch('tagsView/delCurrentView').then(() => {
-						router.push('/exam/question/list')
-					})
-				}
-			}).catch(e => {
-				console.error(e)
-			}).finally(() => {
-				formLoading.value = false
-			})
-		} else {
-			return false
+	import { ref, reactive, computed, onMounted } from 'vue'
+	import { message } from 'ant-design-vue'
+	import { useExamStore } from '@/store/exam'
+	import tQuestionApi from '@/api/exam/question/tQuestionApi'
+	import QuestionShow from '../components/Show.vue'
+	import Editor from '@/components/Editor/index.vue'
+
+	const examStore = useExamStore()
+	const props = defineProps({
+		id: {
+			type: Number,
+			default: 0
 		}
 	})
-}
-
-
-function resetForm() {
-	const lastId = form.id
-	formRef.value.resetFields()
-	Object.assign(form, {
+	const emit = defineEmits(['successful'])
+	const formRef = ref()
+	const form = reactive({
 		id: null,
 		questionType: 1,
 		gradeLevel: null,
@@ -233,47 +106,144 @@ function resetForm() {
 		score: '',
 		difficult: 0
 	})
-	form.id = lastId
-}
-
-function showQuestion() {
-	questionShow.dialog = true
-	questionShow.qType = form.value.questionType
-	console.log(form)
-	console.log(questionShow)
-	questionShow.question = form.value
-	form.value.items.forEach((item, index) => {
-		console.log(`选项 ${item.prefix} 的内容:`, item.content)
+	const subjectFilter = ref([])
+	const formLoading = ref(false)
+	const rules = {
+		gradeLevel: [{ required: true, message: '请选择年级', trigger: 'change' }],
+		subjectId: [{ required: true, message: '请选择学科', trigger: 'change' }],
+		title: [{ required: true, message: '请输入题干', trigger: 'blur' }],
+		analyze: [{ required: true, message: '请输入解析', trigger: 'blur' }],
+		score: [{ required: true, message: '请输入分数', trigger: 'blur' }],
+		correct: [{ required: true, message: '请选择正确答案', trigger: 'change' }]
+	}
+	const richEditor = reactive({
+		dialogVisible: false,
+		object: null,
+		parameterName: '',
+		instance: null
 	})
-}
-</script>
+	const richEditorContent = ref('')
+	const questionShow = reactive({
+		qType: 0,
+		dialog: false,
+		question: null,
+		loading: false
+	})
+	const levelEnum = computed(() => examStore.levelEnum)
+	const subjects = computed(() => examStore.subjects)
+
+	onMounted(async () => {
+		await examStore.initSubject()
+		subjectFilter.value = subjects.value
+		const id = props.id
+		if (id && parseInt(id) !== 0) {
+			formLoading.value = true
+			tQuestionApi.select(id).then((re) => {
+				Object.assign(form, re)
+				formLoading.value = false
+			})
+		}
+	})
+
+	function inputClick(object, parameterName) {
+		richEditor.object = object
+		richEditor.parameterName = parameterName
+		richEditorContent.value = object[parameterName] || ''
+		richEditor.dialogVisible = true
+	}
+
+	function editorConfirm() {
+		richEditor.object[richEditor.parameterName] = richEditorContent.value
+		richEditor.dialogVisible = false
+	}
+
+	function questionItemRemove(index) {
+		form.items.splice(index, 1)
+	}
+
+	function questionItemAdd() {
+		const items = form.items
+		let newLastPrefix
+		if (items.length > 0) {
+			let last = items[items.length - 1]
+			newLastPrefix = String.fromCharCode(last.prefix.charCodeAt() + 1)
+		} else {
+			newLastPrefix = 'A'
+		}
+		items.push({ prefix: newLastPrefix, content: '' })
+	}
 
-<style scoped>
-.question-item-label {
-	margin-top: 10px;
-	margin-bottom: 10px !important;
-}
+	function submitForm() {
+		formRef.value.validate().then((valid) => {
+			if (valid) {
+				formLoading.value = true
+				tQuestionApi
+					.edit(form)
+					.then((re) => {
+						emit('successful')
+						formLoading.value = false
+					})
+					.catch(() => {
+						formLoading.value = false
+					})
+			}
+		})
+	}
 
-.question-item-remove {
-	margin-left: 20px;
-}
+	function resetForm() {
+		const lastId = form.id
+		formRef.value.resetFields()
+		Object.assign(form, {
+			id: null,
+			questionType: 1,
+			gradeLevel: null,
+			subjectId: null,
+			title: '',
+			items: [
+				{ prefix: 'A', content: '' },
+				{ prefix: 'B', content: '' },
+				{ prefix: 'C', content: '' },
+				{ prefix: 'D', content: '' }
+			],
+			analyze: '',
+			correct: '',
+			score: '',
+			difficult: 0
+		})
+		form.id = lastId
+	}
 
-.question-item-content-input {
-	margin-left: 8px;
-	width: 60%;
-	height: 20px;
-}
+	function levelChange() {
+		form.subjectId = null
+		subjectFilter.value = subjects.value.filter((data) => data.level === form.gradeLevel)
+	}
 
-.question-item-span {
-	vertical-align: middle;
-	font-size: 14px;
-	color: #606266;
-	font-weight: 700;
-	box-sizing: border-box;
-	margin-left: 10px;
-}
+	function showQuestion() {
+		questionShow.dialog = true
+		questionShow.qType = form.questionType
+		questionShow.question = { ...form }
+	}
+</script>
 
-.question-item-rate {
-	line-height: 2.5;
-}
+<style lang="less" scoped>
+	.app-container {
+		background: #fff;
+		padding: 24px;
+		border-radius: 8px;
+	}
+	.question-item-label {
+		margin-top: 10px;
+		margin-bottom: 10px !important;
+	}
+	.question-item-remove {
+		margin-left: 20px;
+	}
+	.question-item-content-input {
+		margin-left: 8px;
+		width: 60%;
+		height: 20px;
+	}
+	.question-item-rate {
+		line-height: 2.5;
+	}
 </style>

+ 223 - 0
src/views/exm/question/edit/true-false.vue

@@ -0,0 +1,223 @@
+<template>
+	<div class="app-container">
+		<a-form :model="form" ref="formRef" :rules="rules" layout="vertical">
+			<a-form-item label="年级:" name="gradeLevel" required>
+				<a-select v-model:value="form.gradeLevel" placeholder="年级" @change="levelChange">
+					<a-select-option v-for="item in levelEnum" :key="item.key" :value="item.key">
+						{{ item.value }}
+					</a-select-option>
+				</a-select>
+			</a-form-item>
+			<a-form-item label="学科:" name="subjectId" required>
+				<a-select v-model:value="form.subjectId" placeholder="学科">
+					<a-select-option v-for="item in subjectFilter" :key="item.id" :value="item.id">
+						{{ item.name + ' ( ' + item.levelName + ' )' }}
+					</a-select-option>
+				</a-select>
+			</a-form-item>
+			<a-form-item label="题干:" name="title" required>
+				<a-input v-model:value="form.title" readonly @click="inputClick(form, 'title')" />
+			</a-form-item>
+			<a-form-item label="选项:" required>
+				<div v-for="item in form.items" :key="item.prefix" class="question-item-label">
+					<a-input v-model:value="item.prefix" style="width: 50px; margin-right: 8px" />
+					<a-input
+						v-model:value="item.content"
+						readonly
+						@click="inputClick(item, 'content')"
+						class="question-item-content-input"
+						style="width: 60%"
+					/>
+				</div>
+			</a-form-item>
+			<a-form-item label="解析:" name="analyze" required>
+				<a-input v-model:value="form.analyze" readonly @click="inputClick(form, 'analyze')" />
+			</a-form-item>
+			<a-form-item label="分数:" name="score" required>
+				<a-input-number v-model:value="form.score" :precision="1" :step="1" :max="100" />
+			</a-form-item>
+			<a-form-item label="难度:" required>
+				<a-rate v-model:value="form.difficult" class="question-item-rate" />
+			</a-form-item>
+			<a-form-item label="正确答案:" name="correct" required>
+				<a-radio-group v-model:value="form.correct">
+					<a-radio v-for="item in form.items" :value="item.prefix" :key="item.prefix">{{ item.prefix }}</a-radio>
+				</a-radio-group>
+			</a-form-item>
+			<a-form-item>
+				<a-button type="primary" @click="submitForm" :loading="formLoading">提交</a-button>
+				<a-button @click="resetForm">重置</a-button>
+				<a-button type="success" @click="showQuestion">预览</a-button>
+			</a-form-item>
+		</a-form>
+		<a-modal
+			v-model:visible="richEditor.dialogVisible"
+			width="800px"
+			:footer="null"
+			:closable="false"
+			centered
+			destroy-on-close
+		>
+			<Editor v-model="richEditorContent" :height="300" />
+			<div style="text-align: right; margin-top: 16px">
+				<a-button type="primary" @click="editorConfirm">确定</a-button>
+				<a-button @click="richEditor.dialogVisible = false">取消</a-button>
+			</div>
+		</a-modal>
+		<a-modal v-model:visible="questionShow.dialog" width="800px" :footer="null" :bodyStyle="{ padding: '24px' }">
+			<QuestionShow :qType="questionShow.qType" :question="questionShow.question" :qLoading="questionShow.loading" />
+		</a-modal>
+	</div>
+</template>
+
+<script setup>
+	import { ref, reactive, computed, onMounted } from 'vue'
+	import { message } from 'ant-design-vue'
+	import { useExamStore } from '@/store/exam'
+	import tQuestionApi from '@/api/exam/question/tQuestionApi'
+	import QuestionShow from '../components/Show.vue'
+	import Editor from '@/components/Editor/index.vue'
+	const examStore = useExamStore()
+	const props = defineProps({
+		id: {
+			type: Number,
+			default: 0
+		}
+	})
+	const emit = defineEmits(['successful'])
+	const formRef = ref()
+	const form = reactive({
+		id: null,
+		questionType: 3,
+		gradeLevel: null,
+		subjectId: null,
+		title: '',
+		items: [
+			{ id: null, prefix: 'A', content: '是' },
+			{ id: null, prefix: 'B', content: '否' }
+		],
+		analyze: '',
+		correct: '',
+		score: '',
+		difficult: 0
+	})
+	const subjectFilter = ref([])
+	const formLoading = ref(false)
+	const rules = {
+		gradeLevel: [{ required: true, message: '请选择年级', trigger: 'change' }],
+		subjectId: [{ required: true, message: '请选择学科', trigger: 'change' }],
+		title: [{ required: true, message: '请输入题干', trigger: 'blur' }],
+		analyze: [{ required: true, message: '请输入解析', trigger: 'blur' }],
+		score: [{ required: true, message: '请输入分数', trigger: 'blur' }],
+		correct: [{ required: true, message: '请选择正确答案', trigger: 'change' }]
+	}
+	const richEditor = reactive({
+		dialogVisible: false,
+		object: null,
+		parameterName: '',
+		instance: null
+	})
+	const richEditorContent = ref('')
+	const questionShow = reactive({
+		qType: 0,
+		dialog: false,
+		question: null,
+		loading: false
+	})
+	const levelEnum = computed(() => examStore.levelEnum)
+	const subjects = computed(() => examStore.subjects)
+
+	onMounted(async () => {
+		await examStore.initSubject()
+		subjectFilter.value = subjects.value
+		const id = props.id
+		if (id && parseInt(id) !== 0) {
+			formLoading.value = true
+			tQuestionApi.select(id).then((re) => {
+				Object.assign(form, re)
+				formLoading.value = false
+			})
+		}
+	})
+
+	function inputClick(object, parameterName) {
+		richEditor.object = object
+		richEditor.parameterName = parameterName
+		richEditorContent.value = object[parameterName] || ''
+		richEditor.dialogVisible = true
+	}
+
+	function editorConfirm() {
+		richEditor.object[richEditor.parameterName] = richEditorContent.value
+		richEditor.dialogVisible = false
+	}
+
+	function submitForm() {
+		formRef.value.validate().then((valid) => {
+			if (valid) {
+				formLoading.value = true
+				tQuestionApi
+					.edit(form)
+					.then(() => {
+						emit('successful')
+						formLoading.value = false
+					})
+					.catch(() => {
+						formLoading.value = false
+					})
+			}
+		})
+	}
+
+	function resetForm() {
+		const lastId = form.id
+		formRef.value.resetFields()
+		Object.assign(form, {
+			id: null,
+			questionType: 3,
+			gradeLevel: null,
+			subjectId: null,
+			title: '',
+			items: [
+				{ id: null, prefix: 'A', content: '是' },
+				{ id: null, prefix: 'B', content: '否' }
+			],
+			analyze: '',
+			correct: '',
+			score: '',
+			difficult: 0
+		})
+		form.id = lastId
+	}
+
+	function levelChange() {
+		form.subjectId = null
+		subjectFilter.value = subjects.value.filter((data) => data.level === form.gradeLevel)
+	}
+
+	function showQuestion() {
+		questionShow.dialog = true
+		questionShow.qType = form.questionType
+		questionShow.question = { ...form }
+	}
+</script>
+
+<style lang="less" scoped>
+	.app-container {
+		background: #fff;
+		padding: 24px;
+		border-radius: 8px;
+	}
+	.question-item-label {
+		margin-top: 10px;
+		margin-bottom: 10px !important;
+	}
+	.question-item-content-input {
+		margin-left: 8px;
+		width: 60%;
+		height: 20px;
+	}
+	.question-item-rate {
+		line-height: 2.5;
+	}
+</style>

+ 0 - 94
src/views/exm/question/form.vue

@@ -1,94 +0,0 @@
-<template>
-    <xn-form-container
-        :title="formData.id ? '编辑t_question' : '增加t_question'"
-        :width="700"
-        :visible="visible"
-        :destroy-on-close="true"
-        @close="onClose"
-    >
-        <a-form ref="formRef" :model="formData" :rules="formRules" layout="vertical">
-            <a-form-item label="QUESTION_TYPE:" name="questionType">
-                <a-input v-model:value="formData.questionType" placeholder="请输入QUESTION_TYPE" allow-clear />
-            </a-form-item>
-            <a-form-item label="SUBJECT_ID:" name="subjectId">
-                <a-input v-model:value="formData.subjectId" placeholder="请输入SUBJECT_ID" allow-clear />
-            </a-form-item>
-            <a-form-item label="SCORE:" name="score">
-                <a-input v-model:value="formData.score" placeholder="请输入SCORE" allow-clear />
-            </a-form-item>
-            <a-form-item label="GRADE_LEVEL:" name="gradeLevel">
-                <a-input v-model:value="formData.gradeLevel" placeholder="请输入GRADE_LEVEL" allow-clear />
-            </a-form-item>
-            <a-form-item label="DIFFICULT:" name="difficult">
-                <a-input v-model:value="formData.difficult" placeholder="请输入DIFFICULT" allow-clear />
-            </a-form-item>
-            <a-form-item label="CORRECT:" name="correct">
-                <a-input v-model:value="formData.correct" placeholder="请输入CORRECT" allow-clear />
-            </a-form-item>
-            <a-form-item label="INFO_TEXT_CONTENT_ID:" name="infoTextContentId">
-                <a-input v-model:value="formData.infoTextContentId" placeholder="请输入INFO_TEXT_CONTENT_ID" allow-clear />
-            </a-form-item>
-            <a-form-item label="STATUS:" name="status">
-                <a-input v-model:value="formData.status" placeholder="请输入STATUS" allow-clear />
-            </a-form-item>
-            <a-form-item label="DELETED:" name="deleted">
-                <a-input v-model:value="formData.deleted" placeholder="请输入DELETED" allow-clear />
-            </a-form-item>
-        </a-form>
-        <template #footer>
-            <a-button style="margin-right: 8px" @click="onClose">关闭</a-button>
-            <a-button type="primary" @click="onSubmit" :loading="submitLoading">保存</a-button>
-        </template>
-    </xn-form-container>
-</template>
-
-<script setup name="tQuestionForm">
-    import { cloneDeep } from 'lodash-es'
-    import { required } from '@/utils/formRules'
-    import tQuestionApi from '@/api/question/tQuestionApi'
-    // 抽屉状态
-    const visible = ref(false)
-    const emit = defineEmits({ successful: null })
-    const formRef = ref()
-    // 表单数据
-    const formData = ref({})
-    const submitLoading = ref(false)
-
-    // 打开抽屉
-    const onOpen = (record) => {
-        visible.value = true
-        if (record) {
-            let recordData = cloneDeep(record)
-            formData.value = Object.assign({}, recordData)
-        }
-    }
-    // 关闭抽屉
-    const onClose = () => {
-        formRef.value.resetFields()
-        formData.value = {}
-        visible.value = false
-    }
-    // 默认要校验的
-    const formRules = {
-    }
-    // 验证并提交数据
-    const onSubmit = () => {
-        formRef.value.validate().then(() => {
-            submitLoading.value = true
-            const formDataParam = cloneDeep(formData.value)
-            tQuestionApi
-                .tQuestionSubmitForm(formDataParam, formDataParam.id)
-                .then(() => {
-                    onClose()
-                    emit('successful')
-                })
-                .finally(() => {
-                    submitLoading.value = false
-                })
-        })
-    }
-    // 抛出函数
-    defineExpose({
-        onOpen
-    })
-</script>

+ 268 - 231
src/views/exm/question/index.vue

@@ -1,241 +1,278 @@
 <template>
-    <a-card :bordered="false">
-        <a-form ref="searchFormRef" name="advanced_search" :model="searchFormState" class="ant-advanced-search-form">
-            <a-row :gutter="24">
-                <a-col :span="6">
-					<a-form-item label="题型名称:" name="questionType">
-						<a-select
-							v-model:value="searchFormState.questionType"
-							:options="questionTypeOptions"
-							style="width: 70%"
-							placeholder="请选择题型"
-						>
-						</a-select>
-					</a-form-item>
-                </a-col>
-                <a-col :span="6">
-					<a-form-item label="学科名称:" name="subjectId">
-						<a-select
-							v-model:value="searchFormState.subjectId"
-							:options="susbjectOptions"
-							style="width: 70%"
-							placeholder="请选择学科"
-						>
-						</a-select>
-					</a-form-item>
-                </a-col>
-                <a-col :span="6">
-					<a-form-item label="学期名称:" name="gradeLevel">
-						<a-select
-							v-model:value="searchFormState.gradeLevel"
-							:options="gradeLevelOptions"
-							style="width: 70%"
-							placeholder="请选择学期"
-						>
-						</a-select>
-					</a-form-item>
-                </a-col>
-                <a-col :span="6">
-                    <a-button type="primary" @click="table.refresh(true)">查询</a-button>
-                    <a-button style="margin: 0 8px" @click="reset">重置</a-button>
-                </a-col>
-            </a-row>
-        </a-form>
-        <s-table
-            ref="table"
-            :columns="columns"
-            :data="loadData"
-            :alert="options.alert.show"
-            bordered
-            :row-key="(record) => record.id"
-            :tool-config="toolConfig"
-            :row-selection="options.rowSelection"
-        >
-            <template #operator class="table-operator">
-                <a-space>
-					<a-dropdown v-model:visible="isDropdownVisible" :trigger="['click']">
-						<template #overlay>
-							<a-menu slot="overlay" >
-								<a-menu @click="handleMenuClick">
-									<a-menu-item v-for="item in aMenuSelect" :key="item.key">
-										{{ item.label }}
-									</a-menu-item>
-								</a-menu>
-								<a-menu-divider />
-							</a-menu>
-						</template>
-						<a-button type="primary"  v-if="hasPerm('tQuestionAdd')">
-							创建
-							<DownOutlined />
-						</a-button>
-					</a-dropdown>
-<!--                    <a-button type="primary" @click="formRef.onOpen()" v-if="hasPerm('tQuestionAdd')">-->
-<!--                        <template #icon><plus-outlined /></template>-->
-<!--                        新增-->
-<!--                    </a-button>-->
-                    <xn-batch-delete
-                        v-if="hasPerm('tQuestionBatchDelete')"
-                        :selectedRowKeys="selectedRowKeys"
-                        @batchDelete="deleteBatchTQuestion"
-                    />
-                </a-space>
-            </template>
-            <template #bodyCell="{ column, record }">
-				<template v-if="column.dataIndex === 'questionType'">
-					{{ $TOOL.dictTypeData('QUESTION_TYPE', String(record.questionType)) }}
+	<div class="app-container">
+		<a-form :model="queryParam" layout="inline" @submit.prevent>
+			<a-form-item label="题目ID:">
+				<a-input v-model:value="queryParam.id" allow-clear />
+			</a-form-item>
+			<a-form-item label="题目内容:">
+				<a-input v-model:value="queryParam.content" allow-clear />
+			</a-form-item>
+			<a-form-item label="年级:">
+				<a-select v-model:value="queryParam.level" placeholder="年级" allow-clear @change="levelChange">
+					<a-select-option v-for="item in levelEnum" :key="item.key" :value="item.key">{{
+						item.value
+					}}</a-select-option>
+				</a-select>
+			</a-form-item>
+			<a-form-item label="学科:">
+				<a-select v-model:value="queryParam.subjectId" allow-clear>
+					<a-select-option v-for="item in subjectFilter" :key="item.id" :value="item.id">
+						{{ item.name + ' ( ' + item.levelName + ' )' }}
+					</a-select-option>
+				</a-select>
+			</a-form-item>
+			<a-form-item label="题型:">
+				<a-select v-model:value="queryParam.questionType" allow-clear>
+					<a-select-option v-for="item in questionTypeEnum" :key="item.key" :value="item.key">{{
+						item.value
+					}}</a-select-option>
+				</a-select>
+			</a-form-item>
+			<a-form-item>
+				<a-button type="primary" @click="submitForm">查询</a-button>
+				<a-dropdown>
+					<a-button type="primary" class="link-left">添加</a-button>
+					<template #overlay>
+						<a-menu>
+							<a-menu-item v-for="item in editUrlEnum" :key="item.key" @click="openDrawer(item.value)">
+								{{ item.name }}
+							</a-menu-item>
+						</a-menu>
+					</template>
+				</a-dropdown>
+			</a-form-item>
+		</a-form>
+		<a-table
+			:loading="listLoading"
+			:data-source="tableData"
+			:pagination="false"
+			row-key="id"
+			bordered
+			style="margin-top: 16px"
+		>
+			<a-table-column title="Id" data-index="id" key="id" width="90" />
+			<a-table-column
+				title="学科"
+				data-index="subjectId"
+				key="subjectId"
+				width="120"
+				:customRender="subjectFormatter"
+			/>
+			<a-table-column
+				title="题型"
+				data-index="questionType"
+				key="questionType"
+				width="70"
+				:customRender="questionTypeFormatter"
+			/>
+			<a-table-column title="题干" data-index="shortTitle" key="shortTitle" ellipsis />
+			<a-table-column title="分数" data-index="score" key="score" width="60" />
+			<a-table-column title="难度" data-index="difficult" key="difficult" width="60" />
+			<a-table-column title="创建时间" data-index="createTime" key="createTime" width="160" />
+			<a-table-column title="操作" key="action" width="220">
+				<template #default="{ record }">
+					<a-button size="small" @click="showQuestion(record)">预览</a-button>
+					<a-button size="small" @click="editQuestion(record)">编辑</a-button>
+					<a-button size="small" danger @click="deleteQuestion(record)">删除</a-button>
 				</template>
-				<template v-if="column.dataIndex === 'subjectId'">
-					{{ $TOOL.dictTypeData('SUBJECT', String(record.subjectId)) }}
-				</template>
-				<template v-if="column.dataIndex === 'gradeLevel'">
-					{{ $TOOL.dictTypeData('SEMESTER', String(record.gradeLevel)) }}
-				</template>
-                <template v-if="column.dataIndex === 'action'">
-                    <a-space>
-                        <a @click="formRef.onOpen(record)" v-if="hasPerm('tQuestionEdit')">编辑</a>
-                        <a-divider type="vertical" v-if="hasPerm(['tQuestionEdit', 'tQuestionDelete'], 'and')" />
-                        <a-popconfirm title="确定要删除吗?" @confirm="deleteTQuestion(record)">
-                            <a-button type="link" danger size="small" v-if="hasPerm('tQuestionDelete')">删除</a-button>
-                        </a-popconfirm>
-                    </a-space>
-                </template>
-            </template>
-        </s-table>
-    </a-card>
-    <Form ref="formRef" @successful="table.refresh(true)" />
-	<!-- 模态框部分 -->
-	<a-modal v-model:visible="showModal" title="创建题目" :footer="null" width="80%">
-		<component :is="getComponentByType(selectedQuestionType)" />
-	</a-modal>
+			</a-table-column>
+		</a-table>
+		<custom-pagination
+			v-show="total > 0"
+			:total="total"
+			:page-size="queryParam.pageSize"
+			:current="queryParam.pageIndex"
+			@change="onPageChange"
+			@showSizeChange="onPageSizeChange"
+			style="margin-top: 16px; text-align: right"
+		/>
+		<a-modal v-model:visible="questionShow.dialog" width="800px" :footer="null" :bodyStyle="{ padding: '24px' }">
+			<div v-if="questionShow.loading" style="text-align: center; padding: 40px 0">
+				<a-spin />
+			</div>
+			<div v-else>
+				<!-- 题目预览内容,Show.vue 为空可先简单展示 -->
+				<div v-if="questionShow.question">
+					<h3>题型:{{ enumFormat(questionTypeEnum, questionShow.qType) }}</h3>
+					<div v-html="questionShow.question.title || questionShow.question.shortTitle"></div>
+					<div v-if="questionShow.question.items">
+						<div v-for="item in questionShow.question.items" :key="item.prefix">
+							<b>{{ item.prefix }}:</b><span v-html="item.content"></span>
+						</div>
+					</div>
+					<div>解析:<span v-html="questionShow.question.analyze"></span></div>
+					<div>分数:{{ questionShow.question.score }}</div>
+					<div>难度:{{ questionShow.question.difficult }}</div>
+				</div>
+			</div>
+		</a-modal>
+		<a-drawer
+			:visible="drawerOpen"
+			title="添加题目"
+			placement="right"
+			width="900"
+			:destroyOnClose="true"
+			@close="closeDrawer"
+		>
+			<component
+				:is="currentComponent"
+				v-if="currentComponent"
+				:id="currentComponentId"
+				@successful="onDrawerSuccess"
+			/>
+		</a-drawer>
+	</div>
 </template>
 
-<script setup name="question">
-    import Form from './form.vue'
-	import { DownOutlined } from '@ant-design/icons-vue';
-    import tQuestionApi from '@/api/question/tQuestionApi'
-	import SingleChoice from '@/views/exm/question/edit/single-choice.vue'
-	import tool from "@/utils/tool";
-    let searchFormState = reactive({})
-    const searchFormRef = ref()
-    const table = ref()
-    const formRef = ref()
-    const toolConfig = { refresh: true, height: true, columnSetting: true, striped: false }
-	const gradeLevelOptions = tool.dictList('SEMESTER')
-	const susbjectOptions = tool.dictList('SUBJECT')
-	const questionTypeOptions = tool.dictList('QUESTION_TYPE')
+<script setup>
+	import { ref, reactive, computed, onMounted, defineAsyncComponent, shallowRef } from 'vue'
+	import { useExamStore } from '@/store/exam'
+	import tQuestionApi from '@/api/exam/question/tQuestionApi'
+	import customPagination from '@/components/customPagination.vue'
+	import { message, Modal } from 'ant-design-vue'
 
-	let aMenuSelect = questionTypeOptions.map(item => {
-		return {
-			label: item.label,
-			key: item.value
-		}
+	const examStore = useExamStore()
+
+	const queryParam = reactive({
+		id: null,
+		questionType: null,
+		level: null,
+		subjectId: null,
+		current: 1,
+		size: 10
 	})
-	console.log(aMenuSelect)
 
-    const columns = [
-		{
-			title: 'ID',
-			dataIndex: 'id'
-		},
-        {
-            title: '题型',
-            dataIndex: 'questionType'
-        },
-        {
-            title: '学科',
-            dataIndex: 'subjectId'
-        },
-        {
-            title: '分数',
-            dataIndex: 'score'
-        },
-        {
-            title: '学期',
-            dataIndex: 'gradeLevel'
-        },
-        {
-            title: '难度',
-            dataIndex: 'difficult'
-        },
-        {
-            title: '创建时间',
-            dataIndex: 'createTime'
-        },
-    ]
-    // 操作栏通过权限判断是否显示
-    if (hasPerm(['tQuestionEdit', 'tQuestionDelete'])) {
-        columns.push({
-            title: '操作',
-            dataIndex: 'action',
-            align: 'center',
-            width: '150px'
-        })
-    }
-    const selectedRowKeys = ref([])
-    // 列表选择配置
-    const options = {
-        // columns数字类型字段加入 needTotal: true 可以勾选自动算账
-        alert: {
-            show: true,
-            clear: () => {
-                selectedRowKeys.value = ref([])
-            }
-        },
-        rowSelection: {
-            onChange: (selectedRowKey, selectedRows) => {
-                selectedRowKeys.value = selectedRowKey
-            }
-        }
-    }
-    const loadData = (parameter) => {
-        const searchFormParam = JSON.parse(JSON.stringify(searchFormState))
-        return tQuestionApi.tQuestionPage(Object.assign(parameter, searchFormParam)).then((data) => {
-            return data
-        })
-    }
-    // 重置
-    const reset = () => {
-        searchFormRef.value.resetFields()
-        table.value.refresh(true)
-    }
-    // 删除
-    const deleteTQuestion = (record) => {
-        let params = [
-            {
-                id: record.id
-            }
-        ]
-        tQuestionApi.tQuestionDelete(params).then(() => {
-            table.value.refresh(true)
-        })
-    }
-    // 批量删除
-    const deleteBatchTQuestion = (params) => {
-        tQuestionApi.tQuestionDelete(params).then(() => {
-            table.value.clearRefreshSelected()
-        })
-    }
-	const isDropdownVisible = ref(false)
-	const showModal = ref(false)
-	const selectedQuestionType = ref(null)
+	const subjectFilter = ref([])
+	const listLoading = ref(false)
+	const tableData = ref([])
+	const total = ref(0)
+	const questionShow = reactive({
+		qType: 0,
+		dialog: false,
+		question: null,
+		loading: false
+	})
 
-	const handleMenuClick = (e) => {
-		const selectedKey = e.key
-		selectedQuestionType.value = selectedKey
-		showModal.value = true
-		isDropdownVisible.value = false // 点击后隐藏下拉菜单
-	}
-	// 根据题型返回对应组件
-	const getComponentByType = (type) => {
-		switch (type) {
-			case '1': // 单选题
-				return SingleChoice
-			case '2': // 多选题
-				return MultipleChoice
-			case '3': // 填空题
-				return FillInBlank
-			default:
-				return null
-		}
+	const levelEnum = computed(() => examStore.levelEnum)
+	const questionTypeEnum = computed(() => examStore.questionTypeEnum)
+	const editUrlEnum = computed(() => examStore.exam.question.editUrlEnum)
+	const subjects = computed(() => examStore.subjects)
+	const enumFormat = examStore.enumFormat
+	const subjectEnumFormat = examStore.subjectEnumFormat
+	const currentComponentId = ref(null)
+	// 动态抽屉相关
+	const drawerOpen = ref(false)
+	const currentComponent = shallowRef(null)
+	const openDrawer = (componentPath) => {
+		currentComponentId.value = 0
+		currentComponent.value = defineAsyncComponent(() => import(/* @vite-ignore */ componentPath))
+		drawerOpen.value = true
+	}
+	const closeDrawer = () => {
+		drawerOpen.value = false
+		currentComponent.value = null
+	}
+	const onDrawerSuccess = () => {
+		closeDrawer()
+		search()
+	}
+
+	onMounted(async () => {
+		await examStore.initSubject()
+		subjectFilter.value = subjects.value
+		search()
+	})
+
+	const submitForm = () => {
+		queryParam.pageIndex = 1
+		search()
+	}
+
+	const search = () => {
+		listLoading.value = true
+		tQuestionApi
+			.pageList({
+				...queryParam
+			})
+			.then((data) => {
+				const re = data
+				tableData.value = re.records
+				total.value = re.total
+				queryParam.pageIndex = re.current
+				listLoading.value = false
+			})
+			.catch(() => {
+				listLoading.value = false
+			})
+	}
+
+	const levelChange = () => {
+		queryParam.subjectId = null
+		subjectFilter.value = subjects.value.filter((data) => data.level === queryParam.level)
+	}
+
+	const showQuestion = (row) => {
+		questionShow.dialog = true
+		questionShow.loading = true
+		tQuestionApi.select(row.id).then((re) => {
+			questionShow.qType = re.questionType
+			questionShow.question = re
+			questionShow.loading = false
+		})
+	}
+
+	const editQuestion = (row) => {
+		const url = enumFormat(editUrlEnum.value, row.questionType)
+		currentComponent.value = defineAsyncComponent(() => import(/* @vite-ignore */ url))
+		drawerOpen.value = true
+		currentComponentId.value = row.id
+	}
+
+	const deleteQuestion = (row) => {
+		Modal.confirm({
+			title: '确认删除该题目吗?',
+			onOk: () => {
+				tQuestionApi.deleteQuestion(row.id).then((re) => {
+					if (re.code === 1) {
+						search()
+						message.success(re.message)
+					} else {
+						message.error(re.message)
+					}
+				})
+			}
+		})
+	}
+
+	const questionTypeFormatter = ({ text }) => {
+		return enumFormat(questionTypeEnum.value, text)
+	}
+	const subjectFormatter = ({ text }) => {
+		return subjectEnumFormat(text)
+	}
+	const onPageChange = (page) => {
+		queryParam.pageIndex = page
+		search()
+	}
+	const onPageSizeChange = (page, size) => {
+		queryParam.pageIndex = page
+		queryParam.pageSize = size
+		search()
 	}
 </script>
+
+<style lang="less" scoped>
+	.app-container {
+		background: #fff;
+		padding: 24px;
+		border-radius: 8px;
+	}
+	.link-left {
+		margin-left: 8px;
+	}
+</style>
+<style>
+	.ant-modal div[aria-hidden='true'] {
+		display: none !important;
+	}
+</style>