Просмотр исходного кода

Merge branch 'dev' of http://192.168.1.245:11111/shanming/onlineEducation-front into dev

zhangsq 7 месяцев назад
Родитель
Сommit
607d3e9932
32 измененных файлов с 3190 добавлено и 1002 удалено
  1. 18 0
      src/api/courseinfo/index.js
  2. 17 0
      src/api/exam/paper/examPaperApi.js
  3. 10 0
      src/api/exam/paper/subject.js
  4. 10 0
      src/api/exam/question/tQuestionApi.js
  5. 0 28
      src/api/paper/examPaperApi.js
  6. 0 40
      src/api/question/tQuestionApi.js
  7. 13 2
      src/components/Editor/index.vue
  8. 557 0
      src/components/UpLoadBreakPoint/index.vue
  9. 5 0
      src/router/portal.js
  10. 4 0
      src/router/whiteList.js
  11. 107 0
      src/store/exam.js
  12. 0 0
      src/views/courseDetails/components/tab/LearningStatistics.vue
  13. 2 0
      src/views/courseDetails/components/tab/LessonDetails.vue
  14. 2 0
      src/views/courseDetails/components/tab/StudentDetails.vue
  15. 3 3
      src/views/courseDetails/index.vue
  16. 22 0
      src/views/courseManagement/components/DialogView.vue
  17. 180 0
      src/views/courseManagement/components/ListView.vue
  18. 147 0
      src/views/courseManagement/components/QueryView.vue
  19. 82 0
      src/views/courseManagement/index.vue
  20. 275 130
      src/views/exm/exampaper/form.vue
  21. 240 176
      src/views/exm/exampaper/index.vue
  22. 73 58
      src/views/exm/question/components/Show.vue
  23. 255 0
      src/views/exm/question/edit/gap-filling.vue
  24. 251 0
      src/views/exm/question/edit/multiple-choice.vue
  25. 195 0
      src/views/exm/question/edit/short-answer.vue
  26. 209 239
      src/views/exm/question/edit/single-choice.vue
  27. 223 0
      src/views/exm/question/edit/true-false.vue
  28. 0 94
      src/views/exm/question/form.vue
  29. 275 230
      src/views/exm/question/index.vue
  30. 11 1
      src/views/myResources/resourceUpload.vue
  31. 3 0
      src/views/portal/components/Header.vue
  32. 1 1
      vite.config.js

+ 18 - 0
src/api/courseinfo/index.js

@@ -0,0 +1,18 @@
+// 文件模块相关接口
+import { moduleRequest } from '@/utils/reSourceRequest'
+
+const request = moduleRequest(`/api/webapp/`)
+
+/**
+ * 获取文件列表相关接口
+ */
+// 获取文件列表(区分文件路径)
+export const list = (p) => request('disk/courseinfo/page', p, 'get')
+// 获取文件列表(区分文件类型)
+// export const getFileListByType = (p) => request('file/selectfilebyfiletype', p, 'get')
+
+// 进入详情之后增加观看次数
+export const addViewCount = (p) => request('disk/courseauditrecord/addViewCount', p, 'post')
+//详情
+export const detail = (p) => request('disk/courseauditrecord/detail', p, 'get')
+//收藏增加

+ 17 - 0
src/api/exam/paper/examPaperApi.js

@@ -0,0 +1,17 @@
+import { baseRequest } from '@/utils/request'
+
+const request = (url, ...arg) => baseRequest(`/api/webapp/` + url, ...arg)
+
+/**
+ * t_exam_paperApi接口管理器
+ *
+ * @author zss
+ * @date  2025/07/07 10:10
+ **/
+export default {
+	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')
+}

+ 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 - 28
src/api/paper/examPaperApi.js

@@ -1,28 +0,0 @@
-import { baseRequest } from '@/utils/request'
-
-const request = (url, ...arg) => baseRequest(`/api/webapp/` + url, ...arg)
-
-/**
- * t_exam_paperApi接口管理器
- *
- * @author zss
- * @date  2025/07/07 10:10
- **/
-export default {
-	// 获取t_exam_paper分页
-	tExamPaperPage(data) {
-		return request('api/admin/exam/paper/page', data, 'post')
-	},
-	// 提交t_exam_paper表单 edit为true时为编辑,默认为新增
-	tExamPaperSubmitForm(data, edit = false) {
-		return request('api/admin/exam/paper/edit', data)
-	},
-	// 删除t_exam_paper
-	tExamPaperDelete(data) {
-		return request('delete', data)
-	},
-	// 获取t_exam_paper详情
-	tExamPaperDetail(data) {
-		return request('detail', data, 'get')
-	}
-}

+ 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:
 			default:
 				'undo redo |  forecolor backcolor bold italic underline strikethrough link | blocks fontfamily fontsize | \
 				'undo redo |  forecolor backcolor bold italic underline strikethrough link | blocks fontfamily fontsize | \
 				alignleft aligncenter alignright alignjustify outdent indent lineheight | bullist numlist | \
 				alignleft aligncenter alignright alignjustify outdent indent lineheight | bullist numlist | \
-				image table  preview | code selectall'
+				image table  preview | code selectall | numberedline'
 		},
 		},
 		fileUploadFunction: {
 		fileUploadFunction: {
 			type: Function,
 			type: Function,
@@ -85,9 +85,20 @@
 			editor.on('init', () => {
 			editor.on('init', () => {
 				// getBody().style.fontSize = '14px'
 				// 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) => {
 	watch(props, (newValue) => {
 		contentValue.value = newValue.modelValue
 		contentValue.value = newValue.modelValue
 		emit('update:modelValue', newValue.modelValue)
 		emit('update:modelValue', newValue.modelValue)

+ 557 - 0
src/components/UpLoadBreakPoint/index.vue

@@ -0,0 +1,557 @@
+<template>
+	<div>
+		<a-upload-dragger
+			:file-list="fileList"
+			:before-upload="beforeUpload"
+			@change="handleChange"
+			:show-upload-list="false"
+			:customRequest="customRequest"
+			:multiple="false"
+			:drag="true"
+			:progress="progress"
+		>
+			<div style="padding: 40px; text-align: center">
+				<span class="text"> 点击上传或将文件拖拽至此区域上传 </span>
+				<p class="text">按住Ctrl可同时多选,支持上传PPT/excel/pdf/mp4/zip/rar,等单个文件不能超过2G</p>
+			</div>
+		</a-upload-dragger>
+
+		<div style="margin-bottom: 20px">
+			<a-button v-if="uploadFileList.length > 0" type="primary" @click="uploadFilesList">上传</a-button>
+		</div>
+
+		<div v-for="(item, index) in uploadFileList" :key="index">
+			<div style="padding: 10px">
+				<div style="display: flex; width: 100%; align-items: center; justify-content: space-between">
+					<div>
+						<span>{{ item.name }}</span>
+					</div>
+					<div>
+						<span v-if="item.time != ''" style="display: block; color: blue">{{ item.time }}</span>
+						<span
+							v-if="item.percents == 0 || item.percents == 100"
+							style="color: red; cursor: pointer; margin-left: 10px"
+							@click="handlerRemoveItem(index)"
+							>删除</span
+						>
+					</div>
+				</div>
+
+				<a-progress :percent="item.percents" />
+			</div>
+		</div>
+
+		<!-- <div> -->
+		<!-- <el-progress :text-inside="true" :stroke-width="20" :percentage="successfulChunkPercents" status="success" /> -->
+		<!-- <a-progress :percent="successfulChunkPercents" /> -->
+		<!-- </div> -->
+
+		<!--    已上传列表-->
+		<!-- <el-table :data="uploadList" border style="width: 100%">
+			<el-table-column fixed prop="id.date" label="日期" width="150"> </el-table-column>
+			<el-table-column prop="url" label="下载地址"> </el-table-column>
+
+			<el-table-column label="操作">
+				<template #default="scope">
+					<el-button link type="primary" size="small" @click.prevent="deleteFile(scope.row.url)"> 删除 </el-button>
+					<el-button link type="primary" size="small" @click.prevent="downloadFile(scope.row.url)"> 下载 </el-button>
+				</template>
+			</el-table-column>
+		</el-table> -->
+	</div>
+</template>
+<script setup>
+	import { ref, onMounted } from 'vue'
+	import axios from 'axios'
+	import SparkMD5 from 'spark-md5'
+	import tool from '@/utils/tool'
+	import { message } from 'ant-design-vue'
+	//当前选中的文件
+	const currentFile = ref(null)
+	const chunkSize = ref(5 * 1024 * 1024)
+
+	const uploadedSize = ref(0) // 已上传文件大小(字节)
+
+	const chunkCount = ref(0)
+	const chunksUploaded = ref(0)
+	const fileMd5 = ref('') //
+	const emit = defineEmits(['onUpLoading', 'onSuccess'])
+	const progress = {
+		strokeColor: {
+			'0%': '#108ee9',
+			'100%': '#87d068'
+		},
+		strokeWidth: 3,
+		format: (percent) => `${parseFloat(percent.toFixed(2))}%`,
+		class: 'test'
+	}
+	const allChunks = ref(0) // 文件的md5值
+	const successfulChunkPercents = ref(0) // 上传成功的分片百分比
+	const fileSuffix = ref('') // 文件后缀
+	const chunkList = ref([]) // 文件后缀
+	const uploadList = ref([]) // 文件后缀
+	const fileList = ref([]) // 文件后缀
+	const uploadFileList = ref([]) // 文件后缀
+	const uploadChunks = ref([]) // 文件后缀
+
+	const upLoadTag = ref(false) // 文件的md5值
+	const startTime = ref(0) // 开始时间戳(毫秒
+	const totalSize = ref(0) // 开始时间戳(毫秒
+	const handlerRemoveItem = (index) => {
+		uploadFileList.value.splice(index, 1)
+		emit('onSuccess', uploadFileList.value)
+	}
+
+	const calculateFileMD5 = (file) => {
+		return new Promise((resolve, reject) => {
+			const reader = new FileReader()
+			const spark = new SparkMD5.ArrayBuffer()
+
+			reader.onload = (e) => {
+				spark.append(e.target.result)
+				const md5 = spark.end()
+				resolve(md5)
+			}
+
+			reader.onerror = () => reject(new Error('文件读取失败'))
+
+			reader.readAsArrayBuffer(file)
+		})
+	}
+
+	const updateProgress = (chunkSize) => {
+		uploadedSize.value += chunkSize
+		const percent = Math.round((uploadedSize.value / totalSize.value) * 100)
+		console.log(`上传进度: ${percent}%`)
+	}
+	const calculateSpeed = () => {
+		const currentTime = new Date().getTime()
+		const timeElapsed = (currentTime - startTime.value) / 1000 // 单位:秒
+		if (timeElapsed > 0) {
+			const speed = uploadedSize.value / timeElapsed // 单位:字节/秒
+			return speed
+		}
+		return 0
+	}
+	const estimateRemainingTime = () => {
+		console.log('疑问', ' 总的 ', totalSize.value, ' 变化的 ', uploadedSize.value)
+		const remainingSize = totalSize.value - uploadedSize.value // 剩余文件大小
+		const speed = calculateSpeed() // 平均上传速度(字节/秒)
+
+		if (speed > 0) {
+			const remainingTimeSeconds = remainingSize / speed // 剩余时间(秒)
+			return remainingTimeSeconds
+		}
+		return Infinity // 如果上传速度为 0,则无法估算
+	}
+	const formatTime = (seconds) => {
+		const minutes = Math.floor(seconds / 60)
+		const secs = Math.floor(seconds % 60)
+		if (minutes == 0 && secs == 0) {
+			return ''
+		}
+		return `${minutes} 分 ${secs} 秒`
+	}
+	// 异步方法:选择文件时触发 ============
+	const handleFileChange = async (file) => {
+		fileMd5.value = ''
+		successfulChunkPercents.value = 0
+		// const reader = new FileReader()
+		//   const spark = new SparkMD5.ArrayBuffer()
+		// console.log("读取文件",file.raw)
+		// reader.readAsArrayBuffer(file.raw) // 异步读取文件内容
+
+		// const fileContent = await new Promise((resolve, reject) => {
+		//     reader.onload = (event) => {
+		//         console.log('开始读取文件了...', event, reader.result)
+		//          spark.append(event.target.result)
+		//         resolve(spark.end())
+		//     }
+		//     reader.onerror = (error) => {
+		//         console.log('读取文件时发生错误...', error)
+		//         reject(error)
+		//     }
+		//     reader.readAsArrayBuffer(file)
+		// })
+		// console.log('111文件的md5哈希值是 fileContent:', fileContent)
+		// 计算MD5哈希值
+		fileMd5.value = await calculateFileMD5(file.raw)
+		// fileMd5.value = md5(fileContent)
+		console.log('111文件的md5哈希值是:', fileMd5.value)
+
+		console.log('file对象的大小:', file.size)
+		console.log('file对象:', file)
+		chunkCount.value = Math.ceil(file.size / chunkSize.value) // 还是计算分块数量,向上取整
+		chunksUploaded.value = 0 // 已上传的分片数量
+		fileSuffix.value = '.' + file.raw.name.split('.').pop() // 得到.文件类型
+		successfulChunkPercents.value = 0 // 上传成功的分片百分比
+		currentFile.value = await new Promise((resolve, reject) => {
+			console.log('====开始获取File对象了...')
+			resolve(file.raw)
+			reject('获取File对象失败')
+		})
+
+		// for (let i = 0; i < chunkCount.value; i++) {
+		//     // 文件开始遍历切片存入数组
+		//     const start = i * chunkSize.value
+		//     console.log('循环中的currentFile:', currentFile.value)
+		//     const end = Math.min(
+		//         start + chunkSize.value - 1,
+		//         currentFile.value.size - 1
+		//     )
+		//     chunkList.value[i] = currentFile.value.slice(start, end)
+		// }
+		splitFileByChunkSize(currentFile.value, chunkSize.value)
+
+		console.log('md5:', fileMd5.value)
+		console.log('chunkList:', chunkList.value)
+		console.log('fileSuffix:', fileSuffix.value)
+		console.log('currentFile:', currentFile.value)
+		uploadFileList.value.push({
+			name: file.raw.name,
+			size: currentFile.value.size,
+			md5: fileMd5.value, // md5哈希值
+			chunks: chunkList.value, // 分块列表
+			fileSuffix: fileSuffix.value, // 后缀
+			percents: 0,
+			time: ''
+		}) // 使用对象数组进行处理 ============
+		chunkList.value = []
+		console.log('uploadFileList是:', uploadFileList.value)
+		console.log('结束handleFileChange了')
+	}
+
+	const splitFileByChunkSize = (file, chunkSizeValue) => {
+		const fileSize = file.size
+		const chunkCount = Math.ceil(fileSize / chunkSizeValue) // 计算总分块数
+		// console.log(
+		//     '统计:',
+		//     '块数',
+		//     chunkCount,
+		//     '文件大小',
+		//     fileSize,
+		//     ' 单块 ',
+		//     chunkSizeValue
+		// )
+		for (let i = 0; i < chunkCount; i++) {
+			const start = i * chunkSizeValue
+			const min = start + chunkSizeValue
+
+			console.log('奇怪:', ' start ', start, ' chunkSizeValue ', chunkSizeValue, ' min ', min)
+			const end = Math.min(min, fileSize)
+			// console.log(
+			//     '统计:',
+			//     ' start ',
+			//     start,
+			//     ' end ',
+			//     end,
+			//     ' 单块 ',
+			//     chunkSizeValue,
+			//     ' 比较谁大 ',
+			//     start + chunkSizeValue,
+			//     ' ssss ',
+			//     fileSize
+			// )
+			chunkList.value[i] = file.slice(start, end) // 注意:slice 是 [start, end) 前闭后开区间
+
+			console.log(
+				'准备开始:',
+				' 循环次数 ',
+				i,
+				' 块数 ',
+				chunkCount,
+				'开始的大小',
+				start,
+				'结束的大小',
+				end,
+				' file ',
+				file.slice(start, end)
+			)
+		}
+
+		console.log('分片完成:', chunkList.value)
+	}
+
+	// 点击上传按钮触发多文件上传 ===============
+	const uploadFilesList = async () => {
+		if (upLoadTag.value == true) {
+			message.loading('正在上传')
+			return
+		}
+		upLoadTag.value = true
+
+		emit('onUpLoading', upLoadTag.value)
+		if (currentFile.value == null) {
+			// alert('请选择文件后再上传!')
+			message.error('请选择文件后再上传!')
+			successfulChunkPercents.value = 0 // 重置百分比
+			fileList.value = [] // 文件列表
+			return
+		}
+		// 检查所有文件中是否存在未上传的,未上传则需要上传对应的文件 =========
+		let md5List = []
+		console.log('准备上传', uploadFileList.value)
+		for (let i = 0; i < uploadFileList.value.length; i++) {
+			let element = {
+				// md5: uploadFileList.value[i].md5,
+				md5: uploadFileList.value[i].md5,
+				chunkSize: uploadFileList.value[i].chunks.length,
+				fileName: uploadFileList.value[i].name,
+				fileSuffix: uploadFileList.value[i].fileSuffix
+			}
+			md5List.push(element)
+		}
+		console.log('上传的md5_suffix_List是:', md5List)
+
+		await axios
+			.post('/api/webapp/minio/checkMd5List', md5List, { headers: { Token: tool.data.get('TOKEN') } })
+			.then((res) => {
+				console.log('文件上传返回结果:', res.data)
+				// return
+				var list = res.data
+				if (list.length !== 0) {
+					let upList = []
+					for (let item1 of uploadFileList.value) {
+						for (let item2 of list) {
+							console.log('item回来的', JSON.stringify(item2))
+							if (item1.md5 === item2.md5) {
+								//重要的步骤
+								item1.userFileId = item2.userFileId
+								if (item1.userFileId) {
+									item1.percents = 100
+								}
+								upList.push(item1)
+							}
+						}
+					}
+					console.log('upList是:', upList)
+					uploadFileList.value = upList
+				} else {
+					clearFileList()
+				}
+				console.log('最后必须上传的文件:', uploadFileList.value)
+				// 文件均存在minio中了,无需上传
+				// if (uploadFileList.value.length === 0) {
+				// 	successfulChunkPercents.value = 100
+				// 	alert('文件上传成功')
+				// }
+			})
+			.catch((error) => {
+				console.log('检查返回错误', error)
+			})
+		// return
+		console.log('开始上传', uploadFileList.value)
+		// 检查上传的多个文件是否均存在,如果部分存在,进行剔除,剩余部分仍旧进行上传。
+		// 分块的Promises化
+		const chunkPromises = []
+		// 上传分块的数组,校验是否完成上传
+		const chunksUploadedList = []
+		// 直接计算一共多少个分块
+		uploadFileList.value.forEach((item) => {
+			allChunks.value += item.chunks.length
+		})
+
+		console.log('所有文件加起来一共有多少个分块?', allChunks.value, uploadFileList.value)
+		for (const item of uploadFileList.value) {
+			const md5 = item.md5
+			uploadChunks.value = 0
+
+			if (item.userFileId == undefined || item.userFileId == null) {
+				startTime.value = new Date().getTime() // 开始时间戳(毫秒
+				totalSize.value = item.size // 文件总大小
+				uploadedSize.value = 0
+				for (let i = 0; i < item.chunks.length; i++) {
+					console.log('上传第', i + 1, '个分片')
+					let chunk = item.chunks[i]
+					console.log('去上传...', i + 1, chunk)
+					chunkPromises.push(
+						await uploadFilesChunk(
+							{
+								md5,
+								chunk,
+								chunkIndex: i + 1,
+								fileSuffix: item.fileSuffix,
+								chunkSize: item.chunks.length,
+								fileName: item.name
+							},
+							() => {
+								chunksUploaded.value++
+								uploadChunks.value++
+
+								// successfulChunkPercents.value = 100 * (uploadChunks.value / allChunks.value).toFixed(2)
+								uploadedSize.value += chunk.size // 更新已上传大小
+								const remainingTime = estimateRemainingTime()
+								console.log(`预计剩余时间: ${formatTime(remainingTime)}`)
+								item.percents = (100 * (uploadChunks.value / item.chunks.length)).toFixed(2)
+								item.time = formatTime(remainingTime)
+								console.log(
+									'执行了自增...',
+									'分片长度',
+									item.chunks.length,
+									'执行到多少了',
+									uploadChunks.value,
+									'百分比',
+									100 * (uploadChunks.value / item.chunks.length).toFixed(2)
+								)
+								console.log('this.uploadChunks:', uploadChunks.value)
+								console.log('this.allChunks:', allChunks.value)
+								console.log('进度:', (uploadChunks.value / allChunks.value).toFixed(2))
+							}
+						)
+					)
+				}
+			}
+
+			console.log('this.chunkUploaded是:', chunksUploaded.value)
+			chunksUploadedList.push(chunksUploaded.value) // 存储不同文件的上传分块数量
+			chunksUploaded.value = 0
+		}
+		await Promise.all(chunkPromises)
+		console.log('上传完成!')
+		console.log('chunksUploadList是:', chunksUploadedList)
+		let mergeResults = []
+		for (let i = 0; i < chunksUploadedList.length; i++) {
+			console.log('this.uploadFileList' + i + '是:' + uploadFileList.value[i].chunks.length)
+			console.log('chunksUploadedList' + i + '是:' + chunksUploadedList[i])
+			if (uploadFileList.value[i].chunks.length === chunksUploadedList[i]) {
+				const mergeResult = await axios.post(
+					// `/api/webapp/disk/minio/merge?md5=${uploadFileList.value[i].md5}&fileSuffix=${uploadFileList.value[i].fileSuffix}&chunkTotal=${chunksUploadedList[i]}`
+					`/api/webapp/minio/merge?md5=${uploadFileList.value[i].md5}&fileSuffix=${uploadFileList.value[i].fileSuffix}&chunkTotal=${chunksUploadedList[i]}&fileName=${uploadFileList.value[i].name}&fileSize=${uploadFileList.value[i].size}`,
+					null,
+					{ headers: { Token: tool.data.get('TOKEN') } }
+				)
+				// if (mergeResult.data.startsWith('[miss_chunk]')) {
+				//     alert('文件缺失,请重新上传')
+				//     return
+				// }
+				console.log('合并结果1:', mergeResult)
+				uploadFileList.value[i].userFileId = mergeResult.data.userFileId
+				// mergeResults.push(mergeResult)
+			}
+		}
+		console.log('合并结果2:', uploadFileList.value)
+		upLoadTag.value = false
+		emit('onUpLoading', upLoadTag.value)
+		let finalRes = true
+		emit('onSuccess', uploadFileList.value)
+		// for (const result of mergeResults) {
+		// 	if (result.data === '失败') {
+		// 		finalRes = false
+		// 		alert('上传失败!请重新上传')
+		// 		return
+		// 	} else {
+		// 		alert('上传成功!')
+		// 		successfulChunkPercents.value = 100
+		// 		clearFileList()
+		// 		// getList()
+		// 		return
+		// 	}
+		// }
+	}
+
+	// 多文件上传分片 ============
+	const uploadFilesChunk = async (data, onSuccess) => {
+		console.log('进入了uploadFileChunk方法...')
+		let retryTime = 5 //重试次数
+		const formData = new FormData()
+		formData.append('md5', data.md5)
+		// formData.append('md5', md5)
+		formData.append('chunkIndex', data.chunkIndex)
+		formData.append('chunk', data.chunk)
+		formData.append('chunkSize', data.chunkSize)
+		formData.append('fileSuffix', data.fileSuffix)
+		formData.append('fileName', data.fileName)
+		return axios
+			.post('/api/webapp/minio/upload', formData, {
+				headers: { 'Content-Type': 'multipart/form-data', Token: tool.data.get('TOKEN') }
+			})
+			.then((res) => onSuccess())
+			.catch((error) => {
+				console.log('上传分片失败了...', error)
+				if (retryTime > 0) {
+					retryTime--
+					return uploadChunk(data, onSuccess)
+				}
+			})
+	}
+
+	// 上传分片 旧
+	const uploadChunk = (data, onSuccess) => {
+		let retryTime = 5 //重试次数
+		const formData = new FormData()
+		// formData.append('identifier', fileMd5.value)
+		formData.append('md5', data.md5)
+		// formData.append('md5', md5)
+		formData.append('chunkIndex', data.chunkIndex)
+		formData.append('chunk', data.chunk)
+		formData.append('chunkSize', data.chunkSize)
+		formData.append('fileSuffix', data.fileSuffix)
+		formData.append('fileName', data.fileName)
+		return axios
+			.post('/api/webapp/minio/upload', formData, {
+				headers: { 'Content-Type': 'multipart/form-data', Token: tool.data.get('TOKEN') }
+			})
+			.then((res) => onSuccess())
+			.catch((error) => {
+				if (retryTime > 0) {
+					retryTime--
+					return uploadChunk(data, onSuccess)
+				}
+			})
+	}
+
+	// 删除文件
+	const deleteFile = (url) => {
+		axios
+			.get(`/api/webapp/disk/delete?url=` + url)
+			.then((res) => {
+				console.log('删除文件:', res.data)
+				alert(res.data ? '删除成功!' : '删除失败!')
+				// getList()
+			})
+			.catch((error) => {
+				console.log('删除失败:', error)
+			})
+		console.log('url是:', url)
+	}
+
+	const downloadFile = (url) => {
+		window.location.href = url
+	}
+	const beforeUpload = (file) => {
+		handleFileChange({ raw: file })
+		return false // 阻止默认上传
+	}
+	const handleChange = (info) => {
+		const { file } = info
+		if (file.status === 'removed') {
+			fileList.value = []
+			clearFileList()
+		}
+	}
+
+	const customRequest = () => {}
+	const getList = () => {
+		axios
+			.get('http://127.0.0.1:9000/api/webapp/disk/minio/list')
+			.then((res) => {
+				this.uploadList = res.data
+				console.log('获取列表结果成功:', res.data)
+			})
+			.catch((error) => {
+				console.error('获取列表失败:', error)
+			})
+	}
+
+	const clearFileList = () => {
+		successfulChunkPercents.value = 0
+		uploadFileList.value = []
+		fileList.value = []
+	}
+
+	onMounted(() => {
+		// getList()
+	})
+</script>
+
+<style scoped></style>

+ 5 - 0
src/router/portal.js

@@ -67,6 +67,11 @@ const portal = [
 				name: 'portal.courseAdd',
 				name: 'portal.courseAdd',
 				path: '/portal/courseAdd',
 				path: '/portal/courseAdd',
 				component: () => import('@/views/courseAdd/index.vue')
 				component: () => import('@/views/courseAdd/index.vue')
+			},
+			{
+				name: 'portal.courseManagement',
+				path: '/portal/courseManagement',
+				component: () => import('@/views/courseManagement/index.vue')
 			}
 			}
 		]
 		]
 	},
 	},

+ 4 - 0
src/router/whiteList.js

@@ -47,6 +47,10 @@ const constRouters = [
 		path: '/portal/courseAdd',
 		path: '/portal/courseAdd',
 		component: () => import('@/views/courseAdd/index.vue')
 		component: () => import('@/views/courseAdd/index.vue')
 	},
 	},
+	{
+		path: '/portal/courseManagement',
+		component: () => import('@/views/courseManagement/index.vue')
+	},
 	{
 	{
 		path: '/other',
 		path: '/other',
 		name: 'other',
 		name: 'other',

+ 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()
+			}
+		}
+	}
+})

+ 0 - 0
src/views/courseDetails/components/LearningStatistics.vue → src/views/courseDetails/components/tab/LearningStatistics.vue


+ 2 - 0
src/views/courseDetails/components/LessonDetails.vue → src/views/courseDetails/components/tab/LessonDetails.vue

@@ -71,6 +71,8 @@
 		border-radius: 0 0 12px 12px;
 		border-radius: 0 0 12px 12px;
 		box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.04);
 		box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.04);
 		padding: 1px 32px 32px 32px;
 		padding: 1px 32px 32px 32px;
+		height: calc(100vh - 400px);
+		overflow: auto;
 	}
 	}
 	.section-block {
 	.section-block {
 		margin-top: 32px;
 		margin-top: 32px;

+ 2 - 0
src/views/courseDetails/components/StudentDetails.vue → src/views/courseDetails/components/tab/StudentDetails.vue

@@ -408,6 +408,8 @@
 		margin: 0 auto;
 		margin: 0 auto;
 		padding: 32px 32px 24px 32px;
 		padding: 32px 32px 24px 32px;
 		box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.04);
 		box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.04);
+		height: calc(100vh - 400px);
+		overflow: auto;
 		.search-bar {
 		.search-bar {
 			display: flex;
 			display: flex;
 			align-items: center;
 			align-items: center;

+ 3 - 3
src/views/courseDetails/index.vue

@@ -72,9 +72,9 @@
 	import { EyeOutlined, ClockCircleOutlined, EditOutlined, DeleteOutlined, DownOutlined } from '@ant-design/icons-vue'
 	import { EyeOutlined, ClockCircleOutlined, EditOutlined, DeleteOutlined, DownOutlined } from '@ant-design/icons-vue'
 	import EditCourse from './components/EditCourse.vue'
 	import EditCourse from './components/EditCourse.vue'
 	import AddClassHours from './components/AddClassHours.vue'
 	import AddClassHours from './components/AddClassHours.vue'
-	import LessonDetails from './components/LessonDetails.vue'
-	import StudentDetails from './components/StudentDetails.vue'
-	import LearningStatistics from './components/LearningStatistics.vue'
+	import LessonDetails from './components/tab/LessonDetails.vue'
+	import StudentDetails from './components/tab/StudentDetails.vue'
+	import LearningStatistics from './components/tab/LearningStatistics.vue'
 	import { getCourseDetail } from '@/api/course/courseDetail.js'
 	import { getCourseDetail } from '@/api/course/courseDetail.js'
 
 
 	const course = ref({})
 	const course = ref({})

+ 22 - 0
src/views/courseManagement/components/DialogView.vue

@@ -0,0 +1,22 @@
+<template>
+	<div>
+		<a-modal v-model:visible="visible" title="上传" @ok="handleOk">
+			<UpLoadBreakPoint></UpLoadBreakPoint>
+		</a-modal>
+	</div>
+</template>
+<script setup>
+	import { ref } from 'vue'
+	import UpLoadBreakPoint from '@/components/UpLoadBreakPoint/index.vue'
+
+	const visible = ref(false)
+	const open = () => {
+		visible.value = true
+		console.log('对话框打开', visible.value)
+	}
+	const handleOk = (e) => {
+		// console.logckPoint.value = false
+	}
+
+	defineExpose({ open })
+</script>

+ 180 - 0
src/views/courseManagement/components/ListView.vue

@@ -0,0 +1,180 @@
+<template>
+	<a-table
+		ref="table"
+		:columns="columns"
+		:data-source="dataSource"
+		:row-key="(record) => record.collegeId"
+		bordered
+		:expand-row-by-click="true"
+		:pagination="false"
+		size="small"
+	>
+		<template #bodyCell="{ column, text }">
+			<template v-if="column.dataIndex === 'publishTime'">{{ formatTimestamp(text) }}</template>
+			<template v-if="column.dataIndex === 'action'">
+				<a-button size="small" @click="handleDetail(record)" style="margin-right: 5px">详情</a-button>
+				<a-button size="small" @click="handleEdit(record)" style="margin-right: 5px">编辑</a-button>
+				<a-button size="small" @click="handleShelf(record)" style="margin-right: 5px">上架</a-button>
+				<a-button size="small" @click="handleDelete(record)" style="margin-right: 5px">删除</a-button>
+			</template>
+		</template>
+	</a-table>
+	<div style="display: flex; width: 100%; justify-content: flex-end; margin-top: 10px">
+		<a-pagination
+			v-model:current="pagination.current"
+			v-model:pageSize="pagination.size"
+			:total="pagination.total"
+			show-less-items
+			@change="handlerChange"
+		/>
+	</div>
+</template>
+
+<script setup>
+	import tool from '@/utils/tool'
+	import { ref, onMounted } from 'vue'
+	import { EyeOutlined, EditOutlined, SnippetsOutlined, DeleteOutlined } from '@ant-design/icons-vue'
+	import { list } from '@/api/courseinfo'
+	import { useRouter } from 'vue-router'
+	import collegeApi from '@/api/college'
+
+	const router = useRouter()
+	//发布按钮状态
+	const releaseVisible = ref(false)
+	const loading = ref(false) // 列表loading
+	const dataSource = ref([])
+	const formState = ref({
+		name: '',
+		loacl: ''
+	}) // 列表loading
+	const columns = [
+		{
+			title: '课程名称',
+			dataIndex: 'courseName',
+			sorter: true,
+			width: '20%'
+		},
+		{
+			title: '状态',
+			dataIndex: 'courseType',
+			sorter: true,
+			width: '10%'
+		},
+		{
+			title: '课程类型',
+			dataIndex: 'isCreaterName',
+			sorter: true,
+			width: '20%'
+		},
+		{
+			title: '课时数量',
+			dataIndex: 'majorId',
+			sorter: true,
+			width: '10%'
+		},
+		{
+			title: '更新时间',
+			dataIndex: 'publishTime',
+			sorter: true,
+			width: '20%'
+		},
+		{
+			title: '操作',
+			dataIndex: 'action',
+			sorter: true,
+			width: '20%'
+		}
+	]
+	// tool.formatTimestamp()
+
+	const formatTimestamp = (time) => {
+		return tool.formatTimestamp(time)
+	}
+	const pagination = reactive({
+		size: 10,
+		current: 1,
+		total: 0
+	})
+	const onChangeCurrent = (current) => {
+		router.push({
+			path: '/' + current
+		})
+	}
+	const handlerChange = (page, pageSize) => {
+		pagination.size = pageSize
+		pagination.current = page
+
+		getList()
+	}
+	const publishedData = ref()
+	//发布确定
+
+	// 上传资源模态框
+	const uploadModalVisible = ref(false)
+	// 详情按钮点击事件
+	const handleDetail = (record) => {
+		console.log('查看详情', record)
+		// 在这里添加查看详情的逻辑
+	}
+
+	// 编辑按钮点击事件
+	const handleEdit = (record) => {
+		console.log('编辑记录', record)
+		// 在这里添加编辑记录的逻辑
+	}
+
+	// 上架按钮点击事件
+	const handleShelf = (record) => {
+		console.log('上架记录', record)
+		// 在这里添加上架记录的逻辑
+	}
+
+	// 删除按钮点击事件
+	const handleDelete = (record) => {
+		console.log('删除记录', record)
+		// 在这里添加删除记录的逻辑
+	}
+	const getList = () => {
+		console.log('获取列表', list)
+
+		list({ ...pagination }).then((data) => {
+			if (data.code == 200) {
+				dataSource.value = data.data.records
+				pagination.current = data.data.current
+				pagination.size = data.data.size
+				pagination.total = data.data.total
+			}
+			// data.records
+		})
+	}
+	const setList = (search) => {
+		console.log('获取列表', list)
+		formState.value = search
+		pagination.current = 1
+		list({ ...pagination, ...search }).then((data) => {
+			if (data.code == 200) {
+				dataSource.value = data.data.records
+				pagination.current = data.data.current
+				pagination.size = data.data.size
+				pagination.total = data.data.total
+			}
+			// data.records
+		})
+	}
+
+	// 重置按钮点击事件
+	onMounted(() => {
+		// getListData()
+		getList()
+	})
+
+	defineExpose({
+		setList
+	})
+</script>
+
+<style scoped>
+	.desc p {
+		margin-bottom: 1em;
+	}
+</style>

+ 147 - 0
src/views/courseManagement/components/QueryView.vue

@@ -0,0 +1,147 @@
+<template>
+	<div style="display: flex; justify-content: space-between; align-items: center">
+		<div>
+			<a-form layout="inline" :model="formState">
+				<a-form-item label="" style="width: 200px">
+					<a-input v-model:value="formState.courseName" placeholder="请输入课程名称" allowClear />
+				</a-form-item>
+				<a-form-item label="" style="width: 200px">
+					<a-cascader
+						v-model:value="formState.loacl"
+						:options="options"
+						placeholder="选择院校/专业"
+						change-on-select
+						allowClear
+						:field-names="{ label: 'name', value: 'id', children: 'children' }"
+					/>
+				</a-form-item>
+				<a-form-item label="" style="width: 200px">
+					<a-input v-model:value="formState.type" placeholder="选择课程类型" allowClear />
+				</a-form-item>
+				<a-form-item label="" style="width: 300px">
+					<a-range-picker allowClear />
+				</a-form-item>
+			</a-form>
+		</div>
+		<div>
+			<a-button type="primary" @click="handleSearch">
+				<template #icon><SearchOutlined /></template>
+				查询
+			</a-button>
+			<a-button style="margin-left: 10px" @click="handleReset">
+				<template #icon><ReloadOutlined /></template>
+				重置
+			</a-button>
+		</div>
+	</div>
+</template>
+
+<script setup>
+	import { ref, onMounted } from 'vue'
+	import { SearchOutlined, ReloadOutlined } from '@ant-design/icons-vue'
+	import tool from '@/utils/tool'
+	import { useRouter } from 'vue-router'
+	import collegeApi from '@/api/college'
+
+	const emit = defineEmits([])
+	const router = useRouter()
+	//发布按钮状态
+	const releaseVisible = ref(false)
+	const loading = ref(false) // 列表loading
+
+	const formState = ref({
+		courseName: '',
+		collegeId: '',
+		majorId: '',
+		courseType: '',
+		loacl: []
+	}) // 列表loading
+
+	const options = ref([
+		// {
+		// 	value: 'zhejiang',
+		// 	label: 'Zhejiang',
+		// 	isLeaf: false
+		// },
+		// {
+		// 	value: 'jiangsu',
+		// 	label: 'Jiangsu',
+		// 	isLeaf: false
+		// }
+	])
+
+	// 搜索值
+	const searchValue = ref('')
+
+	const pagination = reactive({
+		pageSize: 10,
+		pageNum: 1,
+		total: 0
+	})
+	const onChangeCurrent = (current) => {
+		router.push({
+			path: '/' + current
+		})
+	}
+	const publishedData = ref()
+	//发布确定
+
+	// 上传资源模态框
+	const uploadModalVisible = ref(false)
+
+	const loadData = (selectedOptions) => {
+		const targetOption = selectedOptions[selectedOptions.length - 1]
+		targetOption.loading = true
+
+		// load options lazily
+		setTimeout(() => {
+			targetOption.loading = false
+			targetOption.children = [
+				{
+					label: `${targetOption.label} Dynamic 1`,
+					value: 'dynamic1'
+				},
+				{
+					label: `${targetOption.label} Dynamic 2`,
+					value: 'dynamic2'
+				}
+			]
+			options.value = [...options.value]
+		}, 1000)
+	}
+	const getList = () => {
+		collegeApi.treeAll().then((data) => {
+			options.value = data
+		})
+	}
+	const handleSearch = () => {
+		console.log('执行查询操作', formState.value)
+		// 在这里添加查询逻辑
+
+		emit('handlerSearch', formState.value)
+	}
+
+	// 重置按钮点击事件
+	const handleReset = () => {
+		formState.value = {
+			courseName: '',
+			collegeId: '',
+			majorId: '',
+			courseType: '',
+			loacl: []
+			// 其他需要重置的字段
+		}
+		emit('handlerSearch', formState.value)
+	}
+	onMounted(() => {
+		// getListData()
+
+		getList()
+	})
+</script>
+
+<style scoped>
+	.desc p {
+		margin-bottom: 1em;
+	}
+</style>

+ 82 - 0
src/views/courseManagement/index.vue

@@ -0,0 +1,82 @@
+<template>
+	<div style="overflow-y: auto">
+		<a-layout>
+			<Header @onChangeCurrent="onChangeCurrent" />
+			<div style="width: 71%; margin-left: 10%">
+				<QueryView style="margin-top: 10px" @handlerSearch="handlerSearch"></QueryView>
+				<!-- 新建课程按钮 -->
+				<a-button style="margin-top: 10px" type="primary" @click="handleNewCourse">
+					<template #icon>
+						<PlusOutlined />
+					</template>
+					新建课程
+				</a-button>
+				<div style="height: 10px"></div>
+				<ListView ref="listViewRef" style="margin-top: 10px"></ListView>
+			</div>
+		</a-layout>
+		<Footer />
+		<DialogView ref="dialogViewRef"></DialogView>
+	</div>
+</template>
+
+<script setup>
+	import { ref, onMounted } from 'vue'
+	import { PlusOutlined } from '@ant-design/icons-vue'
+	import tool from '@/utils/tool'
+	import Header from '@/views/portal/components/Header.vue'
+	import Footer from '@/views/portal/components/Footer.vue'
+	import QueryView from './components/QueryView.vue'
+	import ListView from './components/ListView.vue'
+	import DialogView from './components/DialogView.vue'
+	import { useRouter } from 'vue-router'
+	const router = useRouter()
+	//发布按钮状态
+	const releaseVisible = ref(false)
+	const loading = ref(false) // 列表loading
+
+	const isState = ref(0) // 列表loading
+	const listViewRef = ref(null)
+	const dialogViewRef = ref(null)
+
+	// 搜索值
+	const searchValue = ref('')
+	const open = ref(false)
+
+	const handleNewCourse = () => {
+		console.log('新建课程111')
+		// 在这里添加新建课程的逻辑
+		dialogViewRef.value.open()
+	}
+	const handlerSearch = (data) => {
+		console.log('新建课程')
+		// 在这里添加新建课程的逻辑
+		listViewRef.value.setList(data)
+	}
+
+	const pagination = reactive({
+		pageSize: 10,
+		pageNum: 1,
+		total: 0
+	})
+	const onChangeCurrent = (current) => {
+		router.push({
+			path: '/' + current
+		})
+	}
+	const publishedData = ref()
+	//发布确定
+
+	// 上传资源模态框
+	const uploadModalVisible = ref(false)
+
+	onMounted(() => {
+		// getListData()
+	})
+</script>
+
+<style scoped>
+	.desc p {
+		margin-bottom: 1em;
+	}
+</style>

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

@@ -1,149 +1,294 @@
 <template>
 <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-select>
 			</a-form-item>
 			</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-select>
 			</a-form-item>
 			</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-select>
 			</a-form-item>
 			</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>
-
-
-
-			<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-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>
 </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 '@/views/exm/question/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 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
 		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>
 </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;
+		padding-right: 20px;
+		box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
+		.q-title {
+			margin: 0 5px;
+		}
 	}
 	}
-}
 </style>
 </style>

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

@@ -1,193 +1,257 @@
 <template>
 <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"
 			: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 #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>
 			</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>
 </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,
 		id: null,
-		name: null,
+		level: null,
 		subjectId: 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: 200,
+			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
+
+			await examPaperApi.deletePaper(row.id)
+			search()
+		} 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>
 </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>
 <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>
 </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>
 </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>
 <template>
 	<div class="app-container">
 	<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
 		<a-modal
 			v-model:visible="richEditor.dialogVisible"
 			v-model:visible="richEditor.dialogVisible"
+			width="800px"
 			:footer="null"
 			:footer="null"
-			:body-style="{ padding: '0' }"
-			:width="800"
+			:closable="false"
 			centered
 			centered
 			destroy-on-close
 			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>
-
-		<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" />
 			<QuestionShow :qType="questionShow.qType" :question="questionShow.question" :qLoading="questionShow.loading" />
 		</a-modal>
 		</a-modal>
 	</div>
 	</div>
 </template>
 </template>
 
 
 <script setup>
 <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,
 		id: null,
 		questionType: 1,
 		questionType: 1,
 		gradeLevel: null,
 		gradeLevel: null,
@@ -233,47 +106,144 @@ function resetForm() {
 		score: '',
 		score: '',
 		difficult: 0
 		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>
 </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>

+ 275 - 230
src/views/exm/question/index.vue

@@ -1,241 +1,286 @@
 <template>
 <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"
+					style="width: 100px"
+				>
+					<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 style="width: 100px">
+					<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 style="width: 100px">
+					<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"
+			tableLayout="fixed"
+		>
+			<a-table-column title="Id" data-index="id" key="id" width="90px" />
+			<a-table-column
+				title="学科"
+				data-index="subjectId"
+				key="subjectId"
+				width="150px"
+				:customRender="subjectFormatter"
+			/>
+			<a-table-column
+				title="题型"
+				data-index="questionType"
+				key="questionType"
+				width="90px"
+				:customRender="questionTypeFormatter"
+			/>
+			<a-table-column title="题干" data-index="shortTitle" key="shortTitle" ellipsis />
+			<a-table-column title="分数" data-index="score" key="score" width="90px" />
+			<a-table-column title="难度" data-index="difficult" key="difficult" width="110px">
+				<template #default="{ text }">
+					<StarFilled v-for="n in Number(text)" :key="n" style="color: #faad14" />
+					<StarOutlined v-for="n in 5 - Number(text)" :key="'empty' + n" style="color: #e8e8e8" />
 				</template>
 				</template>
-				<template v-if="column.dataIndex === 'subjectId'">
-					{{ $TOOL.dictTypeData('SUBJECT', String(record.subjectId)) }}
+			</a-table-column>
+			<a-table-column title="创建时间" data-index="createTime" key="createTime" width="120px" />
+			<a-table-column title="操作" key="action" width="200px">
+				<template #default="{ record }">
+					<a-button size="small" style="margin-right: 8px" @click="showQuestion(record)">预览</a-button>
+					<a-button size="small" style="margin-right: 8px" @click="editQuestion(record)">编辑</a-button>
+					<a-button size="small" danger @click="deleteQuestion(record)">删除</a-button>
 				</template>
 				</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>
 </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 { StarFilled, StarOutlined } from '@ant-design/icons-vue'
+	import { Modal } from 'ant-design-vue'
+
+	const examStore = useExamStore()
+
+	const queryParam = reactive({
+		id: null,
+		questionType: null,
+		level: null,
+		subjectId: null,
+		current: 1,
+		size: 10
+	})
+
+	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 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()
+	}
 
 
-	let aMenuSelect = questionTypeOptions.map(item => {
-		return {
-			label: item.label,
-			key: item.value
-		}
+	onMounted(async () => {
+		await examStore.initSubject()
+		subjectFilter.value = subjects.value
+		search()
 	})
 	})
-	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 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) => {
+					search()
+				})
+			}
+		})
+	}
 
 
-	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 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>
 </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>

+ 11 - 1
src/views/myResources/resourceUpload.vue

@@ -124,7 +124,8 @@
 		</a-form>
 		</a-form>
 		<template v-if="isState == 0">
 		<template v-if="isState == 0">
 			<!-- 资源上传 -->
 			<!-- 资源上传 -->
-			<UploadModal @success="uploadSuccess"></UploadModal>
+			<!-- <UploadModal @success="uploadSuccess"></UploadModal> -->
+			<UpLoadBreakPoint @onSuccess="onSuccess"></UpLoadBreakPoint>
 		</template>
 		</template>
 	</a-modal>
 	</a-modal>
 </template>
 </template>
@@ -136,6 +137,7 @@
 	import userSelection from './userSelection.vue'
 	import userSelection from './userSelection.vue'
 	import UploadModal from './UploadModal.vue'
 	import UploadModal from './UploadModal.vue'
 	import coverUpload from './coverUpload/index.vue'
 	import coverUpload from './coverUpload/index.vue'
+	import UpLoadBreakPoint from '@/components/UpLoadBreakPoint/index.vue'
 	import { useMyResourceStore } from '@/store/myResource'
 	import { useMyResourceStore } from '@/store/myResource'
 	const myResourceStore = useMyResourceStore()
 	const myResourceStore = useMyResourceStore()
 	const { proxy } = getCurrentInstance()
 	const { proxy } = getCurrentInstance()
@@ -224,6 +226,14 @@
 		// 	console.error('部分请求失败:', err)
 		// 	console.error('部分请求失败:', err)
 		// }
 		// }
 	}
 	}
+	const onSuccess = (uploadFileList) => {
+		let list = []
+		for (let i = 0; i < uploadFileList.length; i++) {
+			list.push(uploadFileList[i].userFileId)
+		}
+		formState.userfileIds = list.join(',')
+	}
+
 	// 自定义校验函数示例
 	// 自定义校验函数示例
 	const validateKeywords = (rule, value, callback) => {
 	const validateKeywords = (rule, value, callback) => {
 		if (value.length < 2) {
 		if (value.length < 2) {

+ 3 - 0
src/views/portal/components/Header.vue

@@ -13,6 +13,9 @@
 						>个人资源</a-menu-item
 						>个人资源</a-menu-item
 					>
 					>
 					<a-menu-item style="margin-left: 10px; margin-right: 10px" key="portal/courseCenter">课程中心</a-menu-item>
 					<a-menu-item style="margin-left: 10px; margin-right: 10px" key="portal/courseCenter">课程中心</a-menu-item>
+					<a-menu-item style="margin-left: 10px; margin-right: 10px" key="portal/courseManagement"
+						>课程管理</a-menu-item
+					>
 				</a-menu>
 				</a-menu>
 			</div>
 			</div>
 
 

+ 1 - 1
vite.config.js

@@ -52,7 +52,7 @@ export default defineConfig(({ command, mode }) => {
 			port: envConfig.VITE_PORT,
 			port: envConfig.VITE_PORT,
 			proxy: {
 			proxy: {
 				'/api': {
 				'/api': {
-					target: 'http://192.168.31.81:19003',
+					target: 'http://192.168.31.14:9003',
 					ws: false,
 					ws: false,
 					changeOrigin: true
 					changeOrigin: true
 					// rewrite: (path) => path.replace(/^\/api/, '')
 					// rewrite: (path) => path.replace(/^\/api/, '')