Browse Source

feat: 添加任务中心功能并优化课程详情页

refactor(课程详情): 替换mock数据为真实API接口
feat(任务中心): 新增任务列表展示及操作功能
fix(学科管理): 修复学科表单提交问题
style: 统一时间格式化显示
docs: 移除无用注释代码
tanshanming 7 months ago
parent
commit
29a692bda7

+ 18 - 83
src/api/course/courseDetail.js

@@ -1,75 +1,10 @@
-// 课程详情 mock 数据
-export const mockCourseDetail = {
-	id: 1,
-	name: '课程名称',
-	status: '正常',
-	teacher: '赵小刚',
-	category: '起步小班',
-	type: '航空理论类-初级飞行训练',
-	duration: 16,
-	views: 10000,
-	updateTime: '05-22 10:49',
-	sections: [
-		{
-			id: 1,
-			title: '第一章 课程导学',
-			lessons: [
-				{
-					id: 11,
-					title: '1-1 课程简介',
-					duration: '02:30',
-					size: '300MB',
-					publishTime: '2025-07-01 10:23:59',
-					author: '张三'
-				},
-				{
-					id: 12,
-					title: '1-2 课程前瞻',
-					duration: '02:30',
-					size: '300MB',
-					publishTime: '2025-07-01 10:23:59',
-					author: '张三'
-				}
-			]
-		},
-		{
-			id: 2,
-			title: '第二章 课程XX',
-			lessons: [
-				{
-					id: 21,
-					title: '2-1 课程对题',
-					duration: '02:30',
-					size: '300MB',
-					publishTime: '2025-07-01 10:23:59',
-					author: '张三'
-				},
-				{
-					id: 22,
-					title: '2-2 课程对题',
-					duration: '02:30',
-					size: '300MB',
-					publishTime: '2025-07-01 10:23:59',
-					author: '张三'
-				},
-				{
-					id: 23,
-					title: '2-3 课程对题',
-					duration: '02:30',
-					size: '300MB',
-					publishTime: '2025-07-01 10:23:59',
-					author: '张三'
-				}
-			]
-		}
-	]
-}
+// 文件模块相关接口
+import { moduleRequest } from '@/utils/reSourceRequest'
 
-// 获取课程详情的 mock 方法
-export function getCourseDetail() {
-	return Promise.resolve(mockCourseDetail)
-}
+const request = moduleRequest(`/api/webapp/`)
 
+// 获取课程详情
+export const getCourseDetail = (p) => request('disk/courseinfo/detail', p, 'get')
 // 部门与成员 mock 数据
 export const mockDepartments = [
 	{
@@ -131,21 +66,21 @@ export function getDepartmentMembers() {
 
 // 学生详情 mock 数据
 export const mockStudentDetail = {
-  id: '20208447466',
-  name: '张三',
-  phone: '18088889999',
-  gender: '男',
-  college: '院系名称',
-  className: '班级名称',
-  birthday: '1984年6月22日',
-  city: '黑龙江省哈尔滨市',
-  educationStatus: '正常',
-  onlineStatus: '离线',
-  registerTime: '2017-07-24 17:25:38',
-  lastLogin: '2020-11-24 10:00:00'
+	id: '20208447466',
+	name: '张三',
+	phone: '18088889999',
+	gender: '男',
+	college: '院系名称',
+	className: '班级名称',
+	birthday: '1984年6月22日',
+	city: '黑龙江省哈尔滨市',
+	educationStatus: '正常',
+	onlineStatus: '离线',
+	registerTime: '2017-07-24 17:25:38',
+	lastLogin: '2020-11-24 10:00:00'
 }
 
 // 获取学生详情的 mock 方法
 export function getStudentDetail() {
-  return Promise.resolve(mockStudentDetail)
+	return Promise.resolve(mockStudentDetail)
 }

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

@@ -15,4 +15,3 @@ export const list = (p) => request('disk/courseinfo/page', p, 'get')
 export const addViewCount = (p) => request('disk/courseauditrecord/addViewCount', p, 'post')
 //详情
 export const detail = (p) => request('disk/courseauditrecord/detail', p, 'get')
-//收藏增加

+ 2 - 2
src/api/exam/paper/subject.js

@@ -5,6 +5,6 @@ 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')
+	select: (id) => request('api/admin/education/subject/select/' + id, '', 'post'),
+	deleteSubject: (id) => request('api/admin/education/subject/delete/' + id, '', 'post')
 }

+ 10 - 0
src/api/exam/paper/task.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/task/page', query, 'post'),
+	edit: (query) => request('/api/admin/task/edit', query, 'post'),
+	select: (id) => request('/api/admin/task/select/' + id, '', 'post'),
+	deleteTask: (id) => v('/api/admin/task/delete/' + id, '', 'post')
+}

+ 2 - 1
src/api/student/examPaper.js

@@ -4,5 +4,6 @@ const request = (url, ...arg) => baseRequest(`/api/webapp/` + url, ...arg)
 
 export default {
 	select: (id) => request('api/student/exam/paper/select/' + id, '', 'post'),
-	pageList: (query) => request('api/student/exam/paper/pageList', query, 'post')
+	pageList: (query) => request('api/student/exam/paper/pageList', query, 'post'),
+	task: () => request('api/student/dashboard/task')
 }

+ 59 - 11
src/views/courseDetails/index.vue

@@ -4,13 +4,13 @@
 		<a-card class="course-info-card" bordered>
 			<div class="course-info-main">
 				<div class="cover-box">
-					<a-avatar shape="square" :size="120" icon="play-circle" />
+					<a-avatar shape="square" :size="120" :src="course.coverImagePath" />
 				</div>
 				<div class="info-box">
-					<div class="title">{{ course.name }}</div>
+					<div class="title">{{ course.courseName }}</div>
 					<div class="meta">
 						<p><EyeOutlined class="mr-0" /> {{ course.views }}</p>
-						<p><ClockCircleOutlined class="mr-0" /> {{ course.updateTime }}</p>
+						<p><ClockCircleOutlined class="mr-0" /> {{ course.publishTime }}</p>
 					</div>
 				</div>
 				<div class="action-box">
@@ -20,15 +20,20 @@
 						<a-button> <DeleteOutlined /> 删除课程</a-button>
 					</div>
 					<div class="extra-box">
-						<div class="row"><span>当前状态</span><span class="status-normal">● 正常</span></div>
 						<div class="row">
-							<span>授课教师</span><span>{{ course.teacher }}</span>
+							<span>当前状态</span><span class="status-normal">● {{ course.putawayStatusName }}</span>
 						</div>
 						<div class="row">
-							<span>课程分类</span><span>{{ course.category }}</span>
+							<span>授课教师</span><span>{{ course.teacherIdName }}</span>
 						</div>
 						<div class="row">
-							<span>课时数量</span><span>{{ course.duration }}</span>
+							<span>课程分类</span><span>{{ course.courseTypeName }}</span>
+						</div>
+						<div class="row">
+							<span>课时数量</span
+							><span>{{
+								course.timeLimitType === '0' ? '无限课时' : getTimeLimit(course.startTime, course.endTime)
+							}}</span>
 						</div>
 					</div>
 				</div>
@@ -76,8 +81,36 @@
 	import StudentDetails from './components/tab/StudentDetails.vue'
 	import LearningStatistics from './components/tab/LearningStatistics.vue'
 	import { getCourseDetail } from '@/api/course/courseDetail.js'
-
-	const course = ref({})
+	import { useRoute } from 'vue-router'
+	const route = useRoute()
+	const course = ref({
+		courseId: '',
+		courseName: '',
+		courseType: '',
+		courseTypeName: '',
+		courseDesc: '',
+		teacherId: '',
+		teacherIdName: '',
+		collegeId: '',
+		collegeIdName: '',
+		majorId: '',
+		majorIdName: '',
+		publishTime: '',
+		timeLimitType: 0,
+		startTime: '',
+		endTime: '',
+		coverImageId: '',
+		coverImagePath: '',
+		collegeTwoId: '',
+		collegeTwoIdName: '',
+		collegeThreeId: '',
+		collegeThreeIdName: '',
+		collegeAllId: '',
+		collegeAllIdName: '',
+		putawayStatus: '',
+		putawayStatusName: '',
+		sections: []
+	})
 	const activeTab = ref('detail')
 	const currentPage = ref(1)
 	const pageSize = ref(10)
@@ -92,8 +125,14 @@
 	})
 
 	onMounted(() => {
-		getCourseDetail().then((data) => {
-			course.value = data
+		const params = {
+			courseId: route.query.id
+		}
+		getCourseDetail(params).then((res) => {
+			course.value = {
+				...course.value,
+				...res.data
+			}
 		})
 	})
 
@@ -121,6 +160,15 @@
 		}
 		addClassHoursVisible.value = false
 	}
+	function getTimeLimit(startTime, endTime) {
+		const start = new Date(startTime)
+		const end = new Date(endTime)
+		const diff = end - start
+		const days = Math.floor(diff / (1000 * 60 * 60 * 24))
+		const hours = Math.floor((diff / (1000 * 60 * 60)) % 24)
+		const minutes = Math.floor((diff / (1000 * 60)) % 60)
+		return `${days * 24 + hours}小时${minutes}分钟`
+	}
 </script>
 
 <style lang="less" scoped>

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

@@ -116,6 +116,12 @@
 	// 详情按钮点击事件
 	const handleDetail = (record) => {
 		console.log('查看详情', record)
+		router.push({
+			path: '/portal/courseDetails',
+			query: {
+				id: record.courseId
+			}
+		})
 		// 在这里添加查看详情的逻辑
 	}
 

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

@@ -127,6 +127,8 @@
 		if (id && parseInt(id) !== 0) {
 			formLoading.value = true
 			tQuestionApi.select(id).then((re) => {
+				Object.assign(form, re)
+				levelChange()
 				Object.assign(form, re)
 				formLoading.value = false
 			})

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

@@ -140,6 +140,8 @@
 		if (id && parseInt(id) !== 0) {
 			formLoading.value = true
 			tQuestionApi.select(id).then((re) => {
+				Object.assign(form, re)
+				levelChange()
 				Object.assign(form, re)
 				formLoading.value = false
 			})

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

@@ -118,6 +118,8 @@
 		if (id && parseInt(id) !== 0) {
 			formLoading.value = true
 			tQuestionApi.select(id).then((re) => {
+				Object.assign(form, re)
+				levelChange()
 				Object.assign(form, re)
 				formLoading.value = false
 			})

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

@@ -139,6 +139,8 @@
 		if (id && parseInt(id) !== 0) {
 			formLoading.value = true
 			tQuestionApi.select(id).then((re) => {
+				Object.assign(form, re)
+				levelChange()
 				Object.assign(form, re)
 				formLoading.value = false
 			})

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

@@ -134,6 +134,8 @@
 		if (id && parseInt(id) !== 0) {
 			formLoading.value = true
 			tQuestionApi.select(id).then((re) => {
+				Object.assign(form, re)
+				levelChange()
 				Object.assign(form, re)
 				formLoading.value = false
 			})

+ 98 - 0
src/views/exm/subject/form.vue

@@ -0,0 +1,98 @@
+<template>
+	<div class="subject-form-container">
+		<a-form ref="formRef" :model="form" :rules="rules" layout="vertical" v-loading="formLoading">
+			<a-form-item label="学科" name="name" :rules="rules.name" required>
+				<a-input v-model:value="form.name" placeholder="请输入学科名称" allow-clear />
+			</a-form-item>
+			<a-form-item label="年级" name="level" :rules="rules.level" required>
+				<a-select v-model:value="form.level" placeholder="请选择年级">
+					<a-select-option v-for="item in levelEnum" :key="item.key" :value="item.key">
+						{{ item.value }}
+					</a-select-option>
+				</a-select>
+			</a-form-item>
+			<a-form-item>
+				<a-button type="primary" @click="submitForm" :loading="formLoading">提交</a-button>
+				<a-button style="margin-left: 8px" @click="resetForm">重置</a-button>
+			</a-form-item>
+		</a-form>
+	</div>
+</template>
+
+<script setup>
+	import { ref, reactive, onMounted } from 'vue'
+	import { message } from 'ant-design-vue'
+	import subjectApi from '@/api/exam/paper/subject.js'
+	import { useExamStore } from '@/store/exam.js'
+
+	const examStore = useExamStore()
+	const levelEnum = examStore.levelEnum
+	const enumFormat = examStore.enumFormat
+	const emit = defineEmits(['success'])
+	const props = defineProps({
+		id: {
+			type: Number,
+			default: null
+		}
+	})
+	const formRef = ref()
+	const form = reactive({
+		id: null,
+		name: '',
+		level: 1,
+		levelName: ''
+	})
+	const formLoading = ref(false)
+	const rules = {
+		name: [{ required: true, message: '请输入学科名称', trigger: 'blur' }],
+		level: [{ required: true, message: '请选择年级', trigger: 'change' }]
+	}
+
+	// 初始化:编辑时获取详情
+	onMounted(async () => {
+		const id = props.id
+		if (id && parseInt(id) !== 0) {
+			formLoading.value = true
+			try {
+				const res = await subjectApi.select(id)
+				if (res) {
+					Object.assign(form, res)
+				}
+			} finally {
+				formLoading.value = false
+			}
+		}
+	})
+
+	const submitForm = () => {
+		formRef.value.validate().then(async () => {
+			formLoading.value = true
+			form.levelName = enumFormat(levelEnum, form.level)
+			try {
+				const data = await subjectApi.edit(form)
+				emit('success')
+			} catch (e) {
+				// 网络异常等
+				message.error('提交失败')
+			} finally {
+				formLoading.value = false
+			}
+		})
+	}
+
+	const resetForm = () => {
+		const lastId = form.id
+		formRef.value.resetFields()
+		form.id = lastId
+		form.name = ''
+		form.level = 1
+		form.levelName = ''
+	}
+</script>
+
+<style lang="less" scoped>
+	.subject-form-container {
+		max-width: 480px;
+		margin: 40px auto;
+	}
+</style>

+ 157 - 0
src/views/exm/subject/index.vue

@@ -0,0 +1,157 @@
+<template>
+	<div class="subject-container">
+		<a-form layout="inline">
+			<a-form-item label="年级:">
+				<a-select v-model:value="queryParam.level" placeholder="年级" allowClear style="width: 160px">
+					<a-select-option v-for="item in levelEnum" :key="item.key" :value="item.key">
+						{{ item.value }}
+					</a-select-option>
+				</a-select>
+			</a-form-item>
+			<a-form-item>
+				<a-button type="primary" @click="submitForm">查询</a-button>
+				<a-button type="primary" class="ml-8" @click="goAdd">添加</a-button>
+			</a-form-item>
+		</a-form>
+
+		<a-table
+			:loading="listLoading"
+			:data-source="tableData"
+			:pagination="false"
+			row-key="id"
+			bordered
+			style="margin-top: 16px"
+		>
+			<a-table-column title="Id" dataIndex="id" key="id" />
+			<a-table-column title="学科" dataIndex="name" key="name" />
+			<a-table-column title="年级" dataIndex="levelName" key="levelName" />
+			<a-table-column title="操作" key="action" width="180px">
+				<template #default="{ record }">
+					<a-button size="small" @click="goEdit(record)" class="mr-8">编辑</a-button>
+					<a-popconfirm title="确定要删除该学科吗?" @confirm="delSubject(record)" ok-text="确定" cancel-text="取消">
+						<a-button size="small" danger>删除</a-button>
+					</a-popconfirm>
+				</template>
+			</a-table-column>
+		</a-table>
+		<a-pagination
+			v-show="total > 0"
+			:total="total"
+			:current="queryParam.pageIndex"
+			:pageSize="queryParam.pageSize"
+			:show-size-changer="true"
+			:show-quick-jumper="true"
+			@change="onPageChange"
+			@showSizeChange="onPageSizeChange"
+			style="margin-top: 16px; text-align: right"
+		/>
+		<!-- 编辑/添加 抽屉 -->
+		<a-drawer
+			:visible="drawerVisible"
+			:title="drawerTitle"
+			placement="right"
+			width="900"
+			@close="closeDrawer"
+			destroyOnClose
+		>
+			<FormEdit v-if="drawerVisible" :id="editId" @success="onEditSuccess" />
+		</a-drawer>
+	</div>
+</template>
+
+<script setup>
+	import { ref, onMounted } from 'vue'
+	import { message } from 'ant-design-vue'
+	import subjectApi from '@/api/exam/paper/subject.js'
+	import { useExamStore } from '@/store/exam.js'
+	import FormEdit from './form.vue'
+	const examStore = useExamStore()
+	const levelEnum = examStore.levelEnum
+	const drawerTitle = ref('添加学科')
+	const drawerVisible = ref(false)
+	const queryParam = ref({
+		level: null,
+		pageIndex: 1,
+		pageSize: 10
+	})
+	const listLoading = ref(false)
+	const tableData = ref([])
+	const total = ref(0)
+	const editId = ref(null)
+	const fetchData = async () => {
+		listLoading.value = true
+		try {
+			const params = {
+				...queryParam.value,
+				current: queryParam.value.pageIndex,
+				size: queryParam.value.pageSize
+			}
+			delete params.pageIndex
+			delete params.pageSize
+			const data = await subjectApi.pageList(params)
+			const re = data
+			tableData.value = re.records
+			total.value = re.total
+			queryParam.value.pageIndex = re.current
+		} finally {
+			listLoading.value = false
+		}
+	}
+
+	onMounted(() => {
+		fetchData()
+	})
+
+	const submitForm = () => {
+		queryParam.value.pageIndex = 1
+		fetchData()
+	}
+
+	const onPageChange = (page) => {
+		queryParam.value.pageIndex = page
+		fetchData()
+	}
+	const onPageSizeChange = (current, size) => {
+		queryParam.value.pageSize = size
+		queryParam.value.pageIndex = 1
+		fetchData()
+	}
+
+	const delSubject = async (row) => {
+		await subjectApi.deleteSubject(row.id)
+		fetchData()
+	}
+
+	const closeDrawer = () => {
+		drawerVisible.value = false
+		editId.value = null
+	}
+
+	const goAdd = () => {
+		drawerTitle.value = '添加学科'
+		drawerVisible.value = true
+	}
+	const goEdit = (row) => {
+		drawerTitle.value = '编辑学科'
+		drawerVisible.value = true
+		editId.value = row.id
+	}
+	const onEditSuccess = () => {
+		closeDrawer()
+		fetchData()
+	}
+</script>
+
+<style lang="less" scoped>
+	.subject-container {
+		background: #fff;
+		padding: 24px;
+		border-radius: 8px;
+	}
+	.ml-8 {
+		margin-left: 8px;
+	}
+	.mr-8 {
+		margin-right: 8px;
+	}
+</style>

+ 286 - 0
src/views/exm/task/form.vue

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

+ 170 - 0
src/views/exm/task/index.vue

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

+ 90 - 1
src/views/student/paper/index.vue

@@ -1,5 +1,58 @@
 <template>
 	<div class="paper-list">
+		<!-- 任务中心开始 -->
+		<div class="task-center" style="margin-bottom: 24px">
+			<h3 style="border-left: solid 4px #3651d4; padding-left: 8px; margin-bottom: 12px; font-size: 18px">任务中心</h3>
+			<a-spin :spinning="taskLoading">
+				<a-collapse v-if="taskList.length !== 0" accordion>
+					<a-collapse-panel v-for="taskItem in taskList" :key="taskItem.id" :header="taskItem.title">
+						<a-table
+							:dataSource="taskItem.paperItems"
+							:columns="taskColumns"
+							:pagination="false"
+							rowKey="examPaperId"
+							size="small"
+						>
+							<template #bodyCell="{ column, record }">
+								<template v-if="column.key === 'examPaperName'">
+									{{ record.examPaperName }}
+								</template>
+								<template v-else-if="column.key === 'status'">
+									<a-tag v-if="record.status !== null" :color="statusTagFormatter(record.status)" size="small">
+										{{ statusTextFormatter(record.status) }}
+									</a-tag>
+								</template>
+								<template v-else-if="column.key === 'action'">
+									<router-link
+										v-if="record.status === null"
+										:to="{ path: '/student/do', query: { id: record.examPaperId } }"
+										target="_blank"
+									>
+										<a-button type="link" size="small">开始答题</a-button>
+									</router-link>
+									<router-link
+										v-else-if="record.status === 1"
+										:to="{ path: '/student/edit', query: { id: record.examPaperAnswerId } }"
+										target="_blank"
+									>
+										<a-button type="link" size="small">批改试卷</a-button>
+									</router-link>
+									<router-link
+										v-else-if="record.status === 2"
+										:to="{ path: '/student/read', query: { id: record.examPaperAnswerId } }"
+										target="_blank"
+									>
+										<a-button type="link" size="small">查看试卷</a-button>
+									</router-link>
+								</template>
+							</template>
+						</a-table>
+					</a-collapse-panel>
+				</a-collapse>
+				<div v-else style="color: #999; padding: 16px 0">暂无任务</div>
+			</a-spin>
+		</div>
+		<!-- 任务中心结束 -->
 		<a-spin :spinning="listLoading">
 			<a-tabs tab-position="left" v-model:activeKey="tabId" @change="subjectChange" class="subject-tabs">
 				<a-tab-pane v-for="item in subjectList" :key="item.id" :tab="item.name">
@@ -40,10 +93,42 @@
 	import { ref, reactive, onMounted, computed } from 'vue'
 	import { useExamStore } from '@/store/exam'
 	import examPaperApi from '@/api/student/examPaper'
+	import taskApi from '@/api/student/examPaper'
 
 	// store
 	const examStore = useExamStore()
-	const paperTypeEnum = computed(() => examStore.paperTypeEnum)
+	const paperTypeEnum = computed(() => examStore.paperTypeEnum.filter((item) => item.key !== 6))
+
+	// 任务中心相关
+	const taskList = ref([])
+	const taskLoading = ref(false)
+	const taskColumns = [
+		{ title: '试卷名称', dataIndex: 'examPaperName', key: 'examPaperName' },
+		{ title: '状态', dataIndex: 'status', key: 'status', width: 90 },
+		{ title: '操作', key: 'action', align: 'right', width: 120 }
+	]
+
+	const statusTextFormatter = (status) => {
+		if (status === null) return '未作答'
+		if (status === 1) return '待批改'
+		if (status === 2) return '已批改'
+		return ''
+	}
+	const statusTagFormatter = (status) => {
+		if (status === 1) return 'orange'
+		if (status === 2) return 'green'
+		return 'default'
+	}
+	const getTaskList = async () => {
+		taskLoading.value = true
+		try {
+			const res = await taskApi.task()
+			taskList.value = res || []
+		} catch (e) {
+			taskList.value = []
+		}
+		taskLoading.value = false
+	}
 
 	// data
 	const queryParam = reactive({
@@ -119,6 +204,7 @@
 
 	// lifecycle
 	onMounted(() => {
+		getTaskList()
 		initSubject()
 	})
 </script>
@@ -126,6 +212,9 @@
 <style lang="less" scoped>
 	.paper-list {
 		margin-top: 10px;
+		.task-center {
+			margin-bottom: 24px;
+		}
 		.subject-tabs {
 			.ant-tabs-nav {
 				margin-right: 20px;

+ 20 - 18
src/views/student/question-error/index.vue

@@ -10,20 +10,6 @@
 					:custom-row="customRow"
 					:pagination="false"
 				>
-					<template #bodyCell="{ column, record }">
-						<template v-if="column.key === 'questionType'">
-							{{ questionTypeFormatter(record.questionType) }}
-						</template>
-						<template v-else-if="column.key === 'subjectName'">
-							{{ record.subjectName }}
-						</template>
-						<template v-else-if="column.key === 'createTime'">
-							{{ record.createTime }}
-						</template>
-						<template v-else-if="column.key === 'shortTitle'">
-							<span :title="record.shortTitle">{{ record.shortTitle }}</span>
-						</template>
-					</template>
 				</a-table>
 				<a-pagination
 					v-if="total > 0"
@@ -60,7 +46,7 @@
 	import { useExamStore } from '@/store/exam'
 	import examPaperAnswerApi from '@/api/student/questionAnswer'
 	import QuestionAnswerShow from '../exam/components/QuestionAnswerShow.vue'
-
+	import { parseTime } from '@/utils/exam'
 	const examStore = useExamStore()
 
 	const queryParam = reactive({
@@ -79,9 +65,21 @@
 
 	const columns = [
 		{ title: '题干', dataIndex: 'shortTitle', key: 'shortTitle', ellipsis: true },
-		{ title: '题型', dataIndex: 'questionType', key: 'questionType', width: 70 },
-		{ title: '学科', dataIndex: 'subjectName', key: 'subjectName', width: 50 },
-		{ title: '做题时间', dataIndex: 'createTime', key: 'createTime', width: 170 }
+		{
+			title: '题型',
+			dataIndex: 'questionType',
+			key: 'questionType',
+			width: 90,
+			customRender: ({ text }) => questionTypeFormatter(text)
+		},
+		{ title: '学科', dataIndex: 'subjectName', key: 'subjectName', width: 90 },
+		{
+			title: '做题时间',
+			dataIndex: 'createTime',
+			key: 'createTime',
+			width: 200,
+			customRender: ({ text }) => formatDateTime(text)
+		}
 	]
 
 	function search() {
@@ -105,6 +103,10 @@
 				listLoading.value = false
 			})
 	}
+	function formatDateTime(val) {
+		if (!val) return ''
+		return parseTime(val, '{y}-{m}-{d} {h}:{i}:{s}')
+	}
 
 	function customRow(record) {
 		return {