Browse Source

feat(考试管理): 新增考试和问卷管理功能模块

refactor(题目编辑): 优化题目编辑表单逻辑,根据bankType动态显示分数字段
feat(登录): 添加eduIdentity字段支持教育身份标识
refactor(试卷表单): 调整paperType初始值为null并优化类型处理
feat(API): 新增考试管理相关API接口
tanshanming 7 tháng trước cách đây
mục cha
commit
996577a636

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

@@ -0,0 +1,10 @@
+import { baseRequest } from '@/utils/request'
+
+const request = (url, ...arg) => baseRequest(`/api/webapp/api/admin/t_exam/` + url, ...arg)
+export default {
+	pageList: (data) => request('page', data, 'get'),
+	edit: (query) => request('edit', query, 'post'),
+	select: (id) => request(`detail?id=${id}`, '', 'get'),
+	deleteExam: (data) => request('delete', data, 'post'),
+	createExam: (query) => request('add', query, 'post')
+}

+ 4 - 2
src/views/auth/login/login.vue

@@ -127,7 +127,8 @@
 					password: '123456',
 					validCode: '',
 					validCodeReqNo: '',
-					autologin: false
+					autologin: false,
+					eduIdentity: '0'
 				},
 				rules: {
 					account: [required(this.$t('login.accountError'), 'blur')],
@@ -212,7 +213,8 @@
 						// 密码进行SM2加密,传输过程中看到的只有密文,后端存储使用hash
 						password: smCrypto.doSm2Encrypt(this.ruleForm.password),
 						validCode: this.ruleForm.validCode,
-						validCodeReqNo: this.ruleForm.validCodeReqNo
+						validCodeReqNo: this.ruleForm.validCodeReqNo,
+						eduIdentity: this.ruleForm.eduIdentity
 					}
 					// 获取token
 					try {

+ 339 - 0
src/views/exm/examinationManagement/form.vue

@@ -0,0 +1,339 @@
+<template>
+	<div class="app-container">
+		<a-form
+			:model="form"
+			ref="formRef"
+			:label-col="{ span: 4 }"
+			:wrapper-col="{ span: 16 }"
+			:rules="rules"
+			:loading="formLoading"
+			layout="horizontal"
+		>
+			<a-form-item label="考试标题" name="examName" :rules="rules.examName">
+				<a-input v-model:value="form.examName" placeholder="请输入考试标题" />
+			</a-form-item>
+			<a-form-item label="试卷类型" name="paperType">
+				<a-select v-model:value="paperPage.queryParam.paperType" placeholder="请选择试卷类型" @change="paperTypeChange">
+					<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="paperId" :rules="rules.paperId">
+				<a-input-group compact>
+					<a-input
+						v-model:value="selectedPaperName"
+						placeholder="请选择试卷"
+						readonly
+						style="width: calc(100% - 100px)"
+					/>
+					<a-button type="primary" @click="addPaper" style="width: 100px">选择试卷</a-button>
+				</a-input-group>
+			</a-form-item>
+			<a-form-item label="章节" name="chapterId">
+				<a-select v-model:value="form.chapterId" placeholder="请选择章节(可选)" allowClear>
+					<!-- 预留章节选择位置 -->
+				</a-select>
+			</a-form-item>
+			<a-form-item label="开始时间" name="startTime" :rules="rules.startTime">
+				<a-date-picker
+					v-model:value="form.startTime"
+					show-time
+					format="YYYY-MM-DD HH:mm:ss"
+					placeholder="请选择开始时间"
+					style="width: 100%"
+				/>
+			</a-form-item>
+			<a-form-item label="结束时间" name="endTime" :rules="rules.endTime">
+				<a-date-picker
+					v-model:value="form.endTime"
+					show-time
+					format="YYYY-MM-DD HH:mm:ss"
+					placeholder="请选择结束时间"
+					style="width: 100%"
+				/>
+			</a-form-item>
+			<a-form-item label="考试状态" name="examStatus">
+				<a-radio-group v-model:value="form.examStatus">
+					<a-radio :value="0">未开始</a-radio>
+					<a-radio :value="1">已开始</a-radio>
+					<a-radio :value="2">已结束</a-radio>
+				</a-radio-group>
+			</a-form-item>
+			<a-form-item>
+				<a-space>
+					<a-button type="primary" @click="submitForm">提交</a-button>
+					<a-button @click="resetForm">重置</a-button>
+				</a-space>
+			</a-form-item>
+		</a-form>
+
+		<a-modal
+			v-model:visible="paperPage.showDialog"
+			width="70%"
+			title="选择试卷"
+			@ok="confirmPaperSelect"
+			@cancel="() => (paperPage.showDialog = false)"
+		>
+			<a-form layout="inline">
+				<a-form-item label="学科">
+					<a-select v-model:value="paperPage.queryParam.subjectId" allowClear style="width: 200px">
+						<a-select-option v-for="item in paperPage.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="examPaperSubmitForm">查询</a-button>
+				</a-form-item>
+			</a-form>
+			<a-table
+				:dataSource="paperPage.tableData"
+				:columns="modalColumns"
+				rowKey="id"
+				:loading="paperPage.listLoading"
+				:rowSelection="rowSelection"
+				bordered
+				:pagination="false"
+				style="margin-top: 16px"
+			>
+				<template #bodyCell="{ column, record }">
+					<template v-if="column.key === 'subjectId'">
+						{{ subjectEnumFormat(record.subjectId) }}
+					</template>
+				</template>
+			</a-table>
+			<a-pagination
+				v-show="paperPage.total > 0"
+				:total="paperPage.total"
+				:current="paperPage.queryParam.pageIndex"
+				:pageSize="paperPage.queryParam.pageSize"
+				@change="onPageChange"
+				@showSizeChange="onPageSizeChange"
+				show-size-changer
+				style="margin-top: 16px; text-align: right"
+			/>
+		</a-modal>
+	</div>
+</template>
+
+<script setup>
+	import { ref, reactive, onMounted, computed } from 'vue'
+	import { message } from 'ant-design-vue'
+	import { useExamStore } from '@/store/exam.js'
+	import examManagerApi from '@/api/exam/paper/examManager.js'
+	import examPaperApi from '@/api/exam/paper/examPaperApi.js'
+	import dayjs from 'dayjs'
+	const emit = defineEmits(['success'])
+	const props = defineProps({
+		id: {
+			type: Number,
+			default: null
+		}
+	})
+	const formRef = ref()
+	const examStore = useExamStore()
+	const { subjectEnumFormat } = examStore
+	const paperTypeEnum = computed(() => examStore.paperTypeEnum)
+	const formLoading = ref(false)
+	const form = reactive({
+		id: null,
+		examName: '',
+		paperId: null,
+		chapterId: null,
+		startTime: null,
+		endTime: null,
+		examStatus: 0
+	})
+
+	const rules = {
+		examName: [{ required: true, message: '请输入考试标题', trigger: 'blur' }],
+		paperId: [{ required: true, message: '请选择试卷', trigger: 'change' }],
+		startTime: [{ required: true, message: '请选择开始时间', trigger: 'change' }],
+		endTime: [{ required: true, message: '请选择结束时间', trigger: 'change' }]
+	}
+
+	const selectedPaperName = ref('')
+
+	const modalColumns = [
+		{ title: 'Id', dataIndex: 'id', key: 'id', width: 90 },
+		{ title: '学科', dataIndex: 'subjectId', key: 'subjectId', width: 120 },
+		{ title: '名称', dataIndex: 'name', key: 'name' },
+		{
+			title: '创建时间',
+			dataIndex: 'createTimeStr',
+			key: 'createTimeStr',
+			width: 200
+		}
+	]
+
+	const paperPage = reactive({
+		subjectFilter: [],
+		selectedPaper: null,
+		showDialog: false,
+		queryParam: {
+			subjectId: null,
+			paperType: null,
+			pageIndex: 1,
+			pageSize: 5
+		},
+		listLoading: false,
+		tableData: [],
+		total: 0
+	})
+
+	// 试卷选择表格单选
+	const selectedRowKeys = ref([])
+	const rowSelection = reactive({
+		type: 'radio',
+		selectedRowKeys: selectedRowKeys,
+		onChange: (selectedRowKeysVal, selectedRows) => {
+			selectedRowKeys.value = selectedRowKeysVal
+			paperPage.selectedPaper = selectedRows[0] || null
+		}
+	})
+
+	// 初始化学科
+	const initSubject = async (cb) => {
+		await examStore.initSubject()
+		paperPage.subjectFilter = examStore.subjects
+		if (cb) cb()
+	}
+
+	// 试卷类型变更
+	const paperTypeChange = () => {
+		paperPage.queryParam.subjectId = null
+		form.paperId = null
+		selectedPaperName.value = ''
+	}
+
+	// 选择试卷
+	const addPaper = () => {
+		if (!paperPage.queryParam.paperType) {
+			message.warning('请先选择试卷类型')
+			return
+		}
+		paperPage.showDialog = true
+		search()
+	}
+
+	// 查询试卷
+	const search = async () => {
+		paperPage.listLoading = true
+		paperPage.showDialog = true
+		const params = {
+			...paperPage.queryParam,
+			current: paperPage.queryParam.pageIndex,
+			size: paperPage.queryParam.pageSize
+		}
+		delete params.pageIndex
+		delete params.pageSize
+		const data = await examPaperApi.taskExamPage(params)
+		const re = data
+		paperPage.tableData = re.records
+		paperPage.total = re.total
+		paperPage.queryParam.pageIndex = re.current
+		paperPage.listLoading = false
+	}
+
+	// 确认选择试卷
+	const confirmPaperSelect = () => {
+		if (!paperPage.selectedPaper) {
+			message.warning('请选择一个试卷')
+			return
+		}
+		form.paperId = paperPage.selectedPaper.id
+		selectedPaperName.value = paperPage.selectedPaper.name
+		paperPage.showDialog = false
+		selectedRowKeys.value = []
+	}
+
+	// 分页
+	const onPageChange = (page) => {
+		paperPage.queryParam.pageIndex = page
+		search()
+	}
+	const onPageSizeChange = (current, size) => {
+		paperPage.queryParam.pageSize = size
+		paperPage.queryParam.pageIndex = 1
+		search()
+	}
+
+	// 查询按钮
+	const examPaperSubmitForm = () => {
+		paperPage.queryParam.pageIndex = 1
+		search()
+	}
+
+	// 提交表单
+	const submitForm = () => {
+		formRef.value.validate().then(async () => {
+			formLoading.value = true
+			try {
+				const submitData = {
+					...form,
+					startTime: form.startTime ? form.startTime.format('YYYY-MM-DD HH:mm:ss') : null,
+					endTime: form.endTime ? form.endTime.format('YYYY-MM-DD HH:mm:ss') : null
+				}
+
+				if (form.id) {
+					await examManagerApi.edit(submitData)
+				} else {
+					await examManagerApi.createExam(submitData)
+				}
+				message.success('操作成功')
+				emit('success')
+			} catch (e) {
+				message.error(e.msg || '操作失败')
+			} finally {
+				formLoading.value = false
+			}
+		})
+	}
+
+	// 重置表单
+	const resetForm = () => {
+		const lastId = form.id
+		formRef.value.resetFields()
+		form.id = lastId
+		form.examName = ''
+		form.paperId = null
+		form.chapterId = null
+		form.startTime = null
+		form.endTime = null
+		form.examStatus = 0
+		selectedPaperName.value = ''
+		paperPage.queryParam.paperType = null
+	}
+
+	// 初始化
+	onMounted(() => {
+		initSubject(() => {
+			paperPage.subjectFilter = examStore.subjects
+		})
+		const id = props.id
+		if (id && parseInt(id) !== 0) {
+			formLoading.value = true
+			examManagerApi.select(id).then((re) => {
+				Object.assign(form, {
+					...re,
+					startTime: re.startTime ? dayjs(re.startTime) : null,
+					endTime: re.endTime ? dayjs(re.endTime) : null
+				})
+				// 如果有试卷ID,需要获取试卷名称显示
+				if (re.paperId) {
+					// 这里可以根据需要调用接口获取试卷名称
+					selectedPaperName.value = '已选择试卷'
+				}
+				formLoading.value = false
+			})
+		}
+	})
+</script>
+
+<style lang="less" scoped>
+	.app-container {
+		padding: 24px;
+		background: #fff;
+	}
+</style>

+ 195 - 0
src/views/exm/examinationManagement/index.vue

@@ -0,0 +1,195 @@
+<template>
+	<div class="task-container">
+		<a-form layout="inline" :model="queryParam">
+			<a-form-item label="考试标题:">
+				<a-input v-model:value="queryParam.examName" placeholder="请输入考试标题" style="min-width: 200px" />
+			</a-form-item>
+			<a-form-item label="考试状态:">
+				<a-select style="min-width: 150px" v-model:value="queryParam.examStatus" allowClear placeholder="考试状态">
+					<a-select-option :value="0">未开始</a-select-option>
+					<a-select-option :value="1">已开始</a-select-option>
+					<a-select-option :value="2">已结束</a-select-option>
+				</a-select>
+			</a-form-item>
+			<a-form-item>
+				<a-button type="primary" @click="submitForm">查询</a-button>
+				<a-button style="margin-left: 20px" type="primary" @click="createTask">创建考试</a-button>
+			</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" dataIndex="id" key="id" width="100" />
+			<a-table-column title="考试标题" dataIndex="examName" key="examName" />
+			<a-table-column title="考试状态" dataIndex="examStatus" key="examStatus" width="120">
+				<template #default="{ record }">
+					<a-tag :color="record.examStatus === 0 ? 'default' : record.examStatus === 1 ? 'processing' : 'success'">
+						{{ record.examStatus === 0 ? '未开始' : record.examStatus === 1 ? '已开始' : '已结束' }}
+					</a-tag>
+				</template>
+			</a-table-column>
+			<a-table-column title="开始时间" dataIndex="startTime" key="startTime" width="160">
+				<template #default="{ record }">
+					{{ formatDateTime(record.startTime) }}
+				</template>
+			</a-table-column>
+			<a-table-column title="结束时间" dataIndex="endTime" key="endTime" width="160">
+				<template #default="{ record }">
+					{{ formatDateTime(record.endTime) }}
+				</template>
+			</a-table-column>
+			<a-table-column title="创建时间" dataIndex="createTime" key="createTime" width="160">
+				<template #default="{ record }">
+					{{ formatDateTime(record.createTime) }}
+				</template>
+			</a-table-column>
+			<a-table-column title="操作" key="action" align="center" width="160">
+				<template #default="{ record }">
+					<a-button size="small" @click="editTask(record)">编辑</a-button>
+					<a-button size="small" danger style="margin-left: 8px" @click="deleteTask(record)">删除</a-button>
+				</template>
+			</a-table-column>
+		</a-table>
+		<a-pagination
+			v-show="total > 0"
+			:total="total"
+			:current="queryParam.pageIndex"
+			:pageSize="queryParam.pageSize"
+			@change="onPageChange"
+			@showSizeChange="onPageSizeChange"
+			:showSizeChanger="true"
+			:pageSizeOptions="['10', '20', '50', '100']"
+			style="margin-top: 16px; text-align: right"
+		/>
+		<a-drawer
+			:visible="drawerVisible"
+			:title="drawerTitle"
+			placement="right"
+			width="900"
+			@close="closeDrawer"
+			destroyOnClose
+		>
+			<TaskEdit v-if="drawerVisible" :id="editId" @success="onEditSuccess" />
+		</a-drawer>
+	</div>
+</template>
+
+<script setup>
+	import { ref, reactive, onMounted } from 'vue'
+	import { message, Modal } from 'ant-design-vue'
+	import examManagerApi from '@/api/exam/paper/examManager.js'
+	import TaskEdit from './form.vue'
+	import { useExamStore } from '@/store/exam.js'
+	import { storeToRefs } from 'pinia'
+	import { parseTime } from '@/utils/exam'
+	const examStore = useExamStore()
+	const { levelEnum, enumFormat } = storeToRefs(examStore)
+	const drawerVisible = ref(false)
+	const drawerTitle = ref('')
+	const editId = ref(null)
+
+	const queryParam = reactive({
+		examName: null,
+		examStatus: null,
+		pageIndex: 1,
+		pageSize: 10
+	})
+	const listLoading = ref(false)
+	const tableData = ref([])
+	const total = ref(0)
+
+	const fetchList = async () => {
+		listLoading.value = true
+		try {
+			const params = {
+				...queryParam,
+				current: queryParam.pageIndex,
+				size: queryParam.pageSize
+			}
+			delete params.pageIndex
+			delete params.pageSize
+			const data = await examManagerApi.pageList(params)
+			tableData.value = data.records || []
+			total.value = data.total || 0
+			queryParam.pageIndex = data.current || 1
+		} finally {
+			listLoading.value = false
+		}
+	}
+
+	onMounted(() => {
+		fetchList()
+	})
+
+	const submitForm = () => {
+		queryParam.pageIndex = 1
+		fetchList()
+	}
+
+	const onPageChange = (page, pageSize) => {
+		queryParam.pageIndex = page
+		queryParam.pageSize = pageSize
+		fetchList()
+	}
+	const onPageSizeChange = (current, size) => {
+		queryParam.pageIndex = 1
+		queryParam.pageSize = size
+		fetchList()
+	}
+
+	const editTask = (record) => {
+		drawerVisible.value = true
+		drawerTitle.value = '编辑考试'
+		editId.value = record.id
+	}
+
+	const deleteTask = (record) => {
+		Modal.confirm({
+			title: '确认删除该考试吗?',
+			onOk: async () => {
+				try {
+					await examManagerApi.deleteExam([{ id: record.id }])
+					message.success('删除成功')
+					fetchList()
+				} catch (e) {
+					message.error(e.msg || '删除失败')
+				}
+			}
+		})
+	}
+
+	const levelFormatter = ({ text }) => {
+		return enumFormat.value(levelEnum.value, text)
+	}
+	const createTask = () => {
+		drawerVisible.value = true
+		drawerTitle.value = '创建考试'
+		editId.value = null
+	}
+	const onEditSuccess = () => {
+		editId.value = null
+		drawerVisible.value = false
+		fetchList()
+	}
+	const closeDrawer = () => {
+		drawerVisible.value = false
+	}
+	const formatDateTime = (val) => {
+		if (!val) return ''
+		return parseTime(val, '{y}-{m}-{d} {h}:{i}:{s}')
+	}
+</script>
+
+<style lang="less" scoped>
+	.task-container {
+		background: #fff;
+		padding: 24px;
+		border-radius: 8px;
+	}
+</style>

+ 5 - 4
src/views/exm/exampaper/form.vue

@@ -148,7 +148,7 @@
 		id: null,
 		// level: null,
 		// subjectId: null,
-		paperType: 1,
+		paperType: null,
 		limitDateTime: [],
 		name: '',
 		suggestTime: null,
@@ -185,12 +185,13 @@
 	const subjects = computed(() => examStore.subjects)
 
 	onMounted(async () => {
-		await examStore.initSubject()
-		subjectFilter.value = subjects.value
+		// 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)
+			re.paperType = re.paperType.toString()
 			Object.assign(form, re)
 			formLoading.value = false
 		}
@@ -276,7 +277,7 @@
 			id: null,
 			// level: null,
 			// subjectId: null,
-			paperType: 1,
+			paperType: null,
 			limitDateTime: [],
 			name: '',
 			suggestTime: null,

+ 17 - 1
src/views/exm/exampaper/index.vue

@@ -5,6 +5,19 @@
 			<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.paperType"
+					placeholder="请选择试卷类型"
+					@change="paperTypeChange"
+					allow-clear
+					style="width: 120px"
+				>
+					<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="年级:">
 				<a-select
 					v-model:value="queryParam.level"
@@ -88,10 +101,12 @@
 	import { parseTime } from '@/utils/exam'
 
 	const examStore = useExamStore()
+	const paperTypeEnum = computed(() => examStore.paperTypeEnum)
 
 	// 响应式数据
 	const queryParam = reactive({
 		id: null,
+		paperType: null,
 		// level: null,
 		// subjectId: null,
 		current: 1,
@@ -232,7 +247,8 @@
 
 	// 生命周期
 	onMounted(async () => {
-		examStore.initSubject(search)
+		// examStore.initSubject(search)
+		search()
 	})
 </script>
 

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

@@ -71,7 +71,7 @@
 					@click="inputClick(form, 'analyze')"
 				/>
 			</a-form-item>
-			<a-form-item label="分数:" name="score" required>
+			<a-form-item v-if="form.bankType !== '2'" 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>

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

@@ -69,7 +69,7 @@
 					@click="inputClick(form, 'analyze')"
 				/>
 			</a-form-item>
-			<a-form-item label="分数:" name="score" required>
+			<a-form-item v-if="form.bankType !== '2'" 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>
@@ -143,7 +143,7 @@
 		correctArray: [],
 		score: '',
 		difficult: 0,
-		bankType: 1
+		bankType: null
 	})
 	const subjectFilter = ref([])
 	const formLoading = ref(false)
@@ -261,7 +261,7 @@
 			correctArray: [],
 			score: '',
 			difficult: 0,
-			bankType: 1
+			bankType: null
 		})
 		form.id = lastId
 	}

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

@@ -62,7 +62,7 @@
 					@click="inputClick(form, 'analyze')"
 				/>
 			</a-form-item>
-			<a-form-item label="分数:" name="score" required>
+			<a-form-item v-if="form.bankType !== '2'" 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>
@@ -123,7 +123,7 @@
 		correct: '',
 		score: '',
 		difficult: 0,
-		bankType: 1
+		bankType: null
 	})
 	const subjectFilter = ref([])
 	const formLoading = ref(false)
@@ -207,7 +207,7 @@
 			analyze: '',
 			correct: '',
 			score: '',
-			bankType: 1,
+			bankType: null,
 			difficult: 0
 		})
 		form.id = lastId

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

@@ -69,7 +69,7 @@
 					@click="inputClick(form, 'analyze')"
 				/>
 			</a-form-item>
-			<a-form-item label="分数:" name="score" required>
+			<a-form-item v-if="form.bankType !== '2'" 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>
@@ -142,7 +142,7 @@
 		correct: '',
 		score: '',
 		difficult: 0,
-		bankType: 1
+		bankType: null
 	})
 	const subjectFilter = ref([])
 	const formLoading = ref(false)
@@ -248,7 +248,7 @@
 			correct: '',
 			score: '',
 			difficult: 0,
-			bankType: 1
+			bankType: null
 		})
 		form.id = lastId
 	}

+ 3 - 4
src/views/exm/question/edit/true-false.vue

@@ -68,7 +68,7 @@
 					@click="inputClick(form, 'analyze')"
 				/>
 			</a-form-item>
-			<a-form-item label="分数:" name="score" required>
+			<a-form-item v-if="form.bankType !== '2'" 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>
@@ -107,7 +107,6 @@
 
 <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'
@@ -137,7 +136,7 @@
 		correct: '',
 		score: '',
 		difficult: 0,
-		bankType: 1
+		bankType: null
 	})
 	const subjectFilter = ref([])
 	const formLoading = ref(false)
@@ -225,7 +224,7 @@
 			correct: '',
 			score: '',
 			difficult: 0,
-			bankType: 1
+			bankType: null
 		})
 		form.id = lastId
 	}

+ 285 - 0
src/views/exm/questionnaireManagement/form.vue

@@ -0,0 +1,285 @@
+<template>
+	<div class="app-container">
+		<a-form
+			:model="form"
+			ref="formRef"
+			:label-col="{ span: 4 }"
+			:wrapper-col="{ span: 16 }"
+			:rules="rules"
+			:loading="formLoading"
+			layout="horizontal"
+		>
+			<a-form-item label="年级" name="gradeLevel" :rules="rules.gradeLevel">
+				<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="title" :rules="rules.title">
+				<a-input v-model:value="form.title" placeholder="请输入任务标题" />
+			</a-form-item>
+			<a-form-item label="试卷">
+				<a-table :dataSource="form.paperItems" :columns="paperColumns" rowKey="id" bordered :pagination="false">
+					<template #bodyCell="{ column, record }">
+						<template v-if="column.key === 'subjectId'">
+							{{ subjectEnumFormat(record.subjectId) }}
+						</template>
+						<template v-else-if="column.key === 'action'">
+							<a-button type="link" danger @click="removePaper(record)">删除</a-button>
+						</template>
+					</template>
+				</a-table>
+			</a-form-item>
+			<a-form-item>
+				<a-space>
+					<a-button type="primary" @click="submitForm">提交</a-button>
+					<a-button @click="resetForm">重置</a-button>
+					<a-button type="dashed" @click="addPaper">添加试卷</a-button>
+				</a-space>
+			</a-form-item>
+		</a-form>
+
+		<a-modal
+			v-model:visible="paperPage.showDialog"
+			width="70%"
+			title="选择试卷"
+			@ok="confirmPaperSelect"
+			@cancel="() => (paperPage.showDialog = false)"
+		>
+			<a-form layout="inline">
+				<a-form-item label="学科">
+					<a-select v-model:value="paperPage.queryParam.subjectId" allowClear style="width: 200px">
+						<a-select-option v-for="item in paperPage.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="examPaperSubmitForm">查询</a-button>
+				</a-form-item>
+			</a-form>
+			<a-table
+				:dataSource="paperPage.tableData"
+				:columns="modalColumns"
+				rowKey="id"
+				:loading="paperPage.listLoading"
+				:rowSelection="rowSelection"
+				bordered
+				:pagination="false"
+				style="margin-top: 16px"
+			>
+				<template #bodyCell="{ column, record }">
+					<template v-if="column.key === 'subjectId'">
+						{{ subjectEnumFormat(record.subjectId) }}
+					</template>
+				</template>
+			</a-table>
+			<a-pagination
+				v-show="paperPage.total > 0"
+				:total="paperPage.total"
+				:current="paperPage.queryParam.pageIndex"
+				:pageSize="paperPage.queryParam.pageSize"
+				@change="onPageChange"
+				@showSizeChange="onPageSizeChange"
+				show-size-changer
+				style="margin-top: 16px; text-align: right"
+			/>
+		</a-modal>
+	</div>
+</template>
+
+<script setup>
+	import { ref, reactive, onMounted } from 'vue'
+	import { useExamStore } from '@/store/exam.js'
+	import taskApi from '@/api/exam/paper/task.js'
+	import examPaperApi from '@/api/exam/paper/examPaperApi.js'
+	const emit = defineEmits(['success'])
+	const props = defineProps({
+		id: {
+			type: Number,
+			default: null
+		}
+	})
+	const formRef = ref()
+	const examStore = useExamStore()
+	const { subjectEnumFormat } = examStore
+	const levelEnum = computed(() => examStore.getLevelEnum)
+	const formLoading = ref(false)
+	const form = reactive({
+		id: null,
+		gradeLevel: null,
+		title: '',
+		paperItems: []
+	})
+
+	const rules = {
+		gradeLevel: [{ required: true, message: '请选择年级', trigger: 'change' }],
+		title: [{ required: true, message: '请输入任务标题', trigger: 'blur' }]
+	}
+
+	const paperColumns = [
+		{ title: '学科', dataIndex: 'subjectId', key: 'subjectId', width: 120 },
+		{ title: '名称', dataIndex: 'name', key: 'name' },
+		{ title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 160 },
+		{ title: '操作', key: 'action', width: 100 }
+	]
+
+	const modalColumns = [
+		{ title: 'Id', dataIndex: 'id', key: 'id', width: 90 },
+		{ title: '学科', dataIndex: 'subjectId', key: 'subjectId', width: 120 },
+		{ title: '名称', dataIndex: 'name', key: 'name' },
+		{
+			title: '创建时间',
+			dataIndex: 'createTimeStr',
+			key: 'createTimeStr',
+			width: 200
+		}
+	]
+
+	const paperPage = reactive({
+		subjectFilter: [],
+		multipleSelection: [],
+		showDialog: false,
+		queryParam: {
+			subjectId: null,
+			level: null,
+			paperType: 6,
+			pageIndex: 1,
+			pageSize: 5
+		},
+		listLoading: false,
+		tableData: [],
+		total: 0
+	})
+
+	// 试卷选择表格多选
+	const selectedRowKeys = ref([])
+	const rowSelection = reactive({
+		selectedRowKeys: selectedRowKeys,
+		onChange: (selectedRowKeysVal, selectedRows) => {
+			selectedRowKeys.value = selectedRowKeysVal
+			paperPage.multipleSelection = selectedRows
+		}
+	})
+
+	// 初始化学科
+	const initSubject = async (cb) => {
+		await examStore.initSubject()
+		paperPage.subjectFilter = examStore.subjects
+		if (cb) cb()
+	}
+
+	// 年级变更
+	const levelChange = () => {
+		paperPage.queryParam.subjectId = null
+		paperPage.subjectFilter = examStore.subjects.filter((data) => data.level === form.gradeLevel)
+	}
+
+	// 添加试卷
+	const addPaper = () => {
+		paperPage.queryParam.level = form.gradeLevel
+		paperPage.showDialog = true
+		search()
+	}
+
+	// 查询试卷
+	const search = async () => {
+		paperPage.listLoading = true
+		paperPage.showDialog = true
+		const params = {
+			...paperPage.queryParam,
+			current: paperPage.queryParam.pageIndex,
+			size: paperPage.queryParam.pageSize
+		}
+		delete params.pageIndex
+		delete params.pageSize
+		const data = await examPaperApi.taskExamPage(params)
+		const re = data
+		paperPage.tableData = re.records
+		paperPage.total = re.total
+		paperPage.queryParam.pageIndex = re.current
+		paperPage.listLoading = false
+	}
+
+	// 确认选择试卷
+	const confirmPaperSelect = () => {
+		paperPage.multipleSelection.forEach((ep) => {
+			if (!form.paperItems.some((item) => item.id === ep.id)) {
+				form.paperItems.push(ep)
+			}
+		})
+		paperPage.showDialog = false
+		selectedRowKeys.value = []
+	}
+
+	// 分页
+	const onPageChange = (page) => {
+		paperPage.queryParam.pageIndex = page
+		search()
+	}
+	const onPageSizeChange = (current, size) => {
+		paperPage.queryParam.pageSize = size
+		paperPage.queryParam.pageIndex = 1
+		search()
+	}
+
+	// 查询按钮
+	const examPaperSubmitForm = () => {
+		paperPage.queryParam.pageIndex = 1
+		search()
+	}
+
+	// 删除试卷
+	const removePaper = (row) => {
+		const idx = form.paperItems.findIndex((item) => item.id === row.id)
+		if (idx !== -1) form.paperItems.splice(idx, 1)
+	}
+
+	// 提交表单
+	const submitForm = () => {
+		formRef.value.validate().then(async () => {
+			formLoading.value = true
+			try {
+				await taskApi.edit(form)
+				emit('success')
+			} catch (e) {
+				//
+			} finally {
+				formLoading.value = false
+			}
+		})
+	}
+
+	// 重置表单
+	const resetForm = () => {
+		const lastId = form.id
+		formRef.value.resetFields()
+		form.id = lastId
+		form.gradeLevel = null
+		form.title = ''
+		form.paperItems = []
+	}
+
+	// 初始化
+	onMounted(() => {
+		initSubject(() => {
+			paperPage.subjectFilter = examStore.subjects
+		})
+		const id = props.id
+		if (id && parseInt(id) !== 0) {
+			formLoading.value = true
+			taskApi.select(id).then((re) => {
+				Object.assign(form, re)
+				formLoading.value = false
+			})
+		}
+	})
+</script>
+
+<style lang="less" scoped>
+	.app-container {
+		padding: 24px;
+		background: #fff;
+	}
+</style>

+ 176 - 0
src/views/exm/questionnaireManagement/index.vue

@@ -0,0 +1,176 @@
+<template>
+	<div class="task-container">
+		<a-form layout="inline" :model="queryParam">
+			<a-form-item label="年级:">
+				<a-select style="min-width: 150px" v-model:value="queryParam.gradeLevel" allowClear placeholder="年级">
+					<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>
+				<a-button type="primary" @click="submitForm">查询</a-button>
+				<a-button style="margin-left: 20px" type="primary" @click="createTask">创建任务</a-button>
+			</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" dataIndex="id" key="id" width="100" />
+			<a-table-column title="标题" dataIndex="title" key="title" />
+			<!-- <a-table-column title="学级" dataIndex="gradeLevel" key="gradeLevel" :customRender="levelFormatter" /> -->
+			<a-table-column title="发送人" dataIndex="createUserName" key="createUserName" width="100" />
+			<a-table-column title="创建时间" dataIndex="createTime" key="createTime" width="160">
+				<template #default="{ record }">
+					{{ formatDateTime(record.createTime) }}
+				</template>
+			</a-table-column>
+			<a-table-column title="操作" key="action" align="center" width="160">
+				<template #default="{ record }">
+					<a-button size="small" @click="editTask(record)">编辑</a-button>
+					<a-button size="small" danger style="margin-left: 8px" @click="deleteTask(record)">删除</a-button>
+				</template>
+			</a-table-column>
+		</a-table>
+		<a-pagination
+			v-show="total > 0"
+			:total="total"
+			:current="queryParam.pageIndex"
+			:pageSize="queryParam.pageSize"
+			@change="onPageChange"
+			@showSizeChange="onPageSizeChange"
+			:showSizeChanger="true"
+			:pageSizeOptions="['10', '20', '50', '100']"
+			style="margin-top: 16px; text-align: right"
+		/>
+		<a-drawer
+			:visible="drawerVisible"
+			:title="drawerTitle"
+			placement="right"
+			width="900"
+			@close="closeDrawer"
+			destroyOnClose
+		>
+			<TaskEdit v-if="drawerVisible" :id="editId" @success="onEditSuccess" />
+		</a-drawer>
+	</div>
+</template>
+
+<script setup>
+	import { ref, reactive, onMounted } from 'vue'
+	import { message, Modal } from 'ant-design-vue'
+	import taskApi from '@/api/exam/paper/task.js'
+	import TaskEdit from './form.vue'
+	import { useExamStore } from '@/store/exam.js'
+	import { storeToRefs } from 'pinia'
+	import { parseTime } from '@/utils/exam'
+	const examStore = useExamStore()
+	const { levelEnum, enumFormat } = storeToRefs(examStore)
+	const drawerVisible = ref(false)
+	const drawerTitle = ref('')
+	const editId = ref(null)
+
+	const queryParam = reactive({
+		gradeLevel: null,
+		pageIndex: 1,
+		pageSize: 10
+	})
+	const listLoading = ref(false)
+	const tableData = ref([])
+	const total = ref(0)
+
+	const fetchList = async () => {
+		listLoading.value = true
+		try {
+			const params = {
+				...queryParam,
+				current: queryParam.pageIndex,
+				size: queryParam.pageSize
+			}
+			delete params.pageIndex
+			delete params.pageSize
+			const data = await taskApi.pageList(params)
+			tableData.value = data.records || []
+			total.value = data.total || 0
+			queryParam.pageIndex = data.current || 1
+		} finally {
+			listLoading.value = false
+		}
+	}
+
+	onMounted(() => {
+		fetchList()
+	})
+
+	const submitForm = () => {
+		queryParam.pageIndex = 1
+		fetchList()
+	}
+
+	const onPageChange = (page, pageSize) => {
+		queryParam.pageIndex = page
+		queryParam.pageSize = pageSize
+		fetchList()
+	}
+	const onPageSizeChange = (current, size) => {
+		queryParam.pageIndex = 1
+		queryParam.pageSize = size
+		fetchList()
+	}
+
+	const editTask = (record) => {
+		drawerVisible.value = true
+		drawerTitle.value = '编辑任务'
+		editId.value = record.id
+	}
+
+	const deleteTask = (record) => {
+		Modal.confirm({
+			title: '确认删除该任务吗?',
+			onOk: async () => {
+				try {
+					await taskApi.deleteTask(record.id)
+					message.success('删除成功')
+					fetchList()
+				} catch (e) {
+					message.error(e.msg || '删除失败')
+				}
+			}
+		})
+	}
+
+	const levelFormatter = ({ text }) => {
+		return enumFormat.value(levelEnum.value, text)
+	}
+	const createTask = () => {
+		drawerVisible.value = true
+		drawerTitle.value = '创建任务'
+		editId.value = null
+	}
+	const onEditSuccess = () => {
+		editId.value = null
+		drawerVisible.value = false
+		fetchList()
+	}
+	const closeDrawer = () => {
+		drawerVisible.value = false
+	}
+	const formatDateTime = (val) => {
+		if (!val) return ''
+		return parseTime(val, '{y}-{m}-{d} {h}:{i}:{s}')
+	}
+</script>
+
+<style lang="less" scoped>
+	.task-container {
+		background: #fff;
+		padding: 24px;
+		border-radius: 8px;
+	}
+</style>