Browse Source

Merge branch 'test/exam' of http://110.188.24.182:10023/Fengwo/hotline-web into test/exam

zjq 1 week ago
parent
commit
c2eb5240f4

+ 1 - 1
.env.development

@@ -5,7 +5,7 @@ VITE_STORAGE_NAME=dev
 # 业务系统基础请求地址
 VITE_API_URL=http://110.188.24.28:50300
 # 业务系统socket请求地址
-VITE_API_SOCKET_URL=http://110.188.24.28:50100/hubs/hotline
+VITE_API_SOCKET_URL=http://110.188.24.28:50300/hubs/hotline
 # 业务系统文件上传上传请求地址
 VITE_API_UPLOAD_URL=http://110.188.24.28:50120
 # 数据共享平台请求地址

+ 71 - 0
src/api/examTrain/marking.ts

@@ -0,0 +1,71 @@
+/*
+ * @Author: zjq
+ * @description 考试培训-在线培训-阅卷管理
+ */
+import request from '@/utils/request';
+/**
+ * @description 获取阅卷列表
+ * @param {object} data
+ */
+export const getMarkingList = (data?: object) => {
+	return request({
+		url: '/api/v1/UserExam/GetGradingPagedList',
+		method: 'post',
+		data,
+	});
+};
+/**
+ * @description 获取阅卷详情人员
+ * @param {object} params
+ */
+export const getMarkingDetailUser = (params?: object) => {
+	return request({
+		url: '/api/v1/UserExam/GetUserList',
+		method: 'get',
+		params,
+	});
+};
+/**
+ * @description 根据试卷人员ID获取阅卷详情
+ * @param {object} params
+ */
+export const getMarkingDetailByUser = (params?: object) => {
+	return request({
+		url: '/api/v1/UserExam/GetGradingQuestions',
+		method: 'get',
+		params,
+	});
+};
+/**
+ * @description 批量阅卷
+ * @param {object} data
+ */
+export const batchMarking = (data: object) => {
+	return request({
+		url: '/api/v1/UserExam/BatchGrading',
+		method: 'post',
+		data,
+	});
+}
+/**
+ * @description 完成阅卷
+ * @param {object} data
+ */
+export const finishMarking = (data: object) => {
+	return request({
+		url: '/api/v1/UserExam/CompleteGrading',
+		method: 'post',
+		data,
+	});
+};
+/**
+ * @description 获取阅卷试题
+ * @param {object} params
+ */
+export const getMarkingQuestion = (params?: object) => {
+	return request({
+		url: '/api/v1/UserExam/GetGradingQuestions',
+		method: 'post',
+		data: params,
+	});
+};

+ 36 - 13
src/router/route.ts

@@ -419,7 +419,8 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
 			title: '新建工单',
 			isKeepAlive: true,
 		},
-	},{
+	},
+	{
 		path: '/statistics/order/detailAcceptTypeList',
 		name: 'statisticsOrderDetailAcceptTypeList',
 		component: () => import('@/views/statistics/order/detailAcceptTypeList.vue'),
@@ -427,7 +428,8 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
 			title: '受理类型统计列表',
 			isKeepAlive: true,
 		},
-	},{
+	},
+	{
 		path: '/statistics/order/detailAcceptType',
 		name: 'statisticsDetailAcceptType',
 		component: () => import('@/views/statistics/order/detailAcceptType.vue'),
@@ -435,7 +437,8 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
 			title: '受理类型统计明细',
 			isKeepAlive: true,
 		},
-	},{
+	},
+	{
 		path: '/statistics/order/detailHotspotSatisfied',
 		name: 'statisticsOrderDetailHotspotSatisfied',
 		component: () => import('@/views/statistics/order/detailHotspotSatisfied.vue'),
@@ -443,7 +446,8 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
 			title: '热点满意度统计明细',
 			isKeepAlive: true,
 		},
-	},{
+	},
+	{
 		path: '/statistics/call/detailIndexTime',
 		name: 'statisticsCallDetailIndexTime',
 		component: () => import('@/views/statistics/call/detailIndexTime.vue'),
@@ -451,7 +455,8 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
 			title: '话务时段分析日期',
 			isKeepAlive: true,
 		},
-	},{
+	},
+	{
 		path: '/statistics/call/detailIndexCall',
 		name: 'statisticsCallDetailIndexCall',
 		component: () => import('@/views/statistics/call/detailIndexCall.vue'),
@@ -459,7 +464,8 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
 			title: '话务时段分析明细',
 			isKeepAlive: true,
 		},
-	},{
+	},
+	{
 		path: '/statistics/center/detailReport/:id/:tagsViewName?',
 		name: 'statisticsCenterDetailReport',
 		component: () => import('@/views/statistics/center/detail-report.vue'),
@@ -468,7 +474,8 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
 			isKeepAlive: true,
 			isDynamic: true,
 		},
-	},{
+	},
+	{
 		path: '/statistics/center/detailAcceptCenter',
 		name: 'statisticsCenterDetailAcceptCenter',
 		component: () => import('@/views/statistics/center/detailAcceptCenter.vue'),
@@ -476,7 +483,8 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
 			title: '企业专席明细',
 			isKeepAlive: true,
 		},
-	},{
+	},
+	{
 		path: '/statistics/center/detailTwist',
 		name: 'statisticsCenterDetailTwist',
 		component: () => import('@/views/statistics/center/detailTwist.vue'),
@@ -484,7 +492,8 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
 			title: '扭转明细列表',
 			isKeepAlive: true,
 		},
-	},{
+	},
+	{
 		path: '/todo/edit/record',
 		name: 'todoEditRecord',
 		component: () => import('@/views/todo/edit/record.vue'),
@@ -492,7 +501,8 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
 			title: '修改记录',
 			isKeepAlive: true,
 		},
-	},{
+	},
+	{
 		path: '/statistics/call/detailSeatDate',
 		name: 'statisticsCallDetailSeatsDate',
 		component: () => import('@/views/statistics/call/detailSeatDate.vue'),
@@ -500,7 +510,8 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
 			title: '话务时段明细',
 			isKeepAlive: true,
 		},
-	},{
+	},
+	{
 		path: '/statistics/center/detailSeatSatisfactionList',
 		name: 'statisticsCenterDetailSeatSatisfactionList',
 		component: () => import('@/views/statistics/center/detailSeatSatisfactionList.vue'),
@@ -508,7 +519,8 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
 			title: '坐席满意度明细表',
 			isKeepAlive: true,
 		},
-	},{
+	},
+	{
 		path: '/statistics/center/detailSeatSatisfaction',
 		name: 'statisticsCenterDetailSeatSatisfaction',
 		component: () => import('@/views/statistics/center/detailSeatSatisfaction.vue'),
@@ -516,7 +528,8 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
 			title: '坐席满意度列表明细',
 			isKeepAlive: true,
 		},
-	},{
+	},
+	{
 		path: '/statistics/call/detailSeatsMoth',
 		name: 'statisticsCallSeatsMothDetail',
 		component: () => import('@/views/statistics/call/detailSeatsMoth.vue'),
@@ -650,6 +663,16 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
 			isDynamic: true,
 		},
 	},
+	{
+		path: '/examTrain/exam/marking/mark/:examId/:tagsViewName/:isView?',
+		name: 'examTrainExamMarkingEdit',
+		component: () => import('@/views/examTrain/exam/marking/components/Exam-Marking.vue'),
+		meta: {
+			title: '阅卷',
+			isKeepAlive: true,
+			isDynamic: true,
+		},
+	},
 ];
 /**
  * 定义404、401界面

+ 331 - 0
src/views/examTrain/exam/marking/components/Exam-Marking-View.vue

@@ -0,0 +1,331 @@
+<template>
+	<el-dialog v-model="state.dialogVisible" draggable destroy-on-close :show-close="false" width="80%" :before-close="closeDialog">
+		<template #header="{ close, titleId, titleClass }">
+			<div class="topContent">
+				<div class="titleBox">
+					<span class="title">自我练习</span>
+				</div>
+				<div class="submitBox">
+					<el-button type="info" @click="closeDialog">关 闭</el-button>
+				</div>
+			</div>
+		</template>
+		<el-row :gutter="10" v-loading="state.loading">
+			<el-col :xs="6" :sm="6" :md="6" :lg="6" :xl="6">
+				<div class="examListContent" v-for="item in state.questionTypeData">
+					<p class="examType">{{ item.questionTypeDesc }}</p>
+					<ul class="examList">
+						<li
+							class="examListItem"
+							:class="{ select: questionsItem.id == state.selQuestionID, right: questionsItem.isRight, error: !questionsItem.isRight }"
+							v-for="questionsItem in item.questions"
+							@click="onSelQuestion(questionsItem.id)"
+						>
+							{{ questionsItem.sortIndex }}
+						</li>
+					</ul>
+				</div>
+			</el-col>
+			<el-col :xs="18" :sm="18" :md="18" :lg="18" :xl="18">
+				<div class="questionBox" v-if="state.questionDetail">
+					<p class="questionTitle">题干:{{ state.questionDetail.title }}</p>
+					<div class="questionAnswer">
+						<div v-if="[0, 2].includes(state.questionDetail.questionType)">
+							<el-radio-group style="display: block" v-model="state.selRadioValue">
+								<el-form-item :label="item.label + '. '" v-for="item in state.questionDetail.practiceQuestionOptionsDtos" style="margin-bottom: 10px">
+									<el-radio :label="item.content" :value="item.questionOptionId" disabled />
+								</el-form-item>
+							</el-radio-group>
+						</div>
+						<div v-else-if="state.questionDetail.questionType == 1">
+							<el-checkbox-group v-model="state.selCheckboxValue">
+								<el-form-item :label="item.label + '. '" v-for="item in state.questionDetail.practiceQuestionOptionsDtos" style="margin-bottom: 10px">
+									<el-checkbox :label="item.content" :value="item.questionOptionId" disabled />
+								</el-form-item>
+							</el-checkbox-group>
+						</div>
+					</div>
+				</div>
+				<div class="referenceAnswer" v-if="state.questionDetail">
+					<span>参考答案:</span>
+					<p>{{ state.questionDetail.answerDesc || '略' }}</p>
+				</div>
+				<div class="referenceAnswer" v-if="state.questionDetail && state.questionDetail.practiceQuestionKnowladgeDtos.length > 0">
+					<span>关联知识:</span>
+					<p v-for="item in state.questionDetail.practiceQuestionKnowladgeDtos" @click="onKnowladgeTo(item)">{{ item.title }}</p>
+				</div>
+				<div class="referenceAnswer" v-if="state.questionDetail && state.questionDetail.practiceQuestionSourcewareDtos.length > 0">
+					<span>关联课件:</span>
+					<p v-for="item in state.questionDetail.practiceQuestionSourcewareDtos" @click="onSourcewareTo(item)">{{ item.name }}</p>
+				</div>
+			</el-col>
+		</el-row>
+		<template #footer>
+			<span class="dialog-footer" v-if="state.selQuestionID">
+				<el-button type="primary" @click="onLastQuestion" :loading="state.loading">上一题</el-button>
+				<el-button type="primary" @click="onNextQuestion" :loading="state.loading">下一题</el-button>
+			</span>
+		</template>
+	</el-dialog>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref } from 'vue';
+import { useRouter } from 'vue-router';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import { excludeSelfById } from '@/utils/tools';
+import other from '@/utils/other';
+import { fileDownloadByUrl } from '@/api/public/file';
+import { getViewPracticeQuestions, getPracticeView } from '@/api/examTrain/practice';
+
+const router = useRouter(); // 路由
+// 定义子组件向父组件传值/事件
+const emit = defineEmits(['updateList']);
+
+// 定义变量内容
+const state = reactive<any>({
+	dialogVisible: false, // 弹窗
+	loading: false, // 加载
+	practiceId: '', // 用户练习id
+	questionTypeData: [] as any[], // 试卷题型数据
+	questionsData: [] as any[], // 试题集合
+	selQuestionID: '', // 选择的问题id
+	questionDetail: null, // 选择的问题明细
+	selRadioValue: '', // 单选题选择的值
+	selCheckboxValue: [] as any[], // 多选题选择的值
+});
+// 打开弹窗
+const openDialog = async (rows: any) => {
+	state.dialogVisible = true;
+	state.practiceId = rows.id;
+	state.loading = true;
+	getQuestionsData();
+};
+const getQuestionsData = async () => {
+	try {
+		const { result } = await getViewPracticeQuestions(state.practiceId);
+		let sortIndexNum = 1;
+		result.forEach((item) => {
+			switch (item.questionType) {
+				case 0:
+					item.questionTypeDesc = '单选题';
+					break;
+				case 1:
+					item.questionTypeDesc = '多选题';
+					break;
+				case 2:
+					item.questionTypeDesc = '判断题';
+					break;
+				default:
+					break;
+			}
+			item.questions.forEach((it) => {
+				it.sortIndex = sortIndexNum;
+				it.questionType = item.questionType;
+				state.questionsData.push(it);
+				sortIndexNum++;
+			});
+		});
+		state.questionTypeData = result;
+		state.loading = false;
+	} catch (error) {
+		// 打印错误信息
+		console.error(error);
+	}
+};
+// 关闭弹窗
+const closeDialog = () => {
+	state.dialogVisible = false;
+	state.questionTypeData = [];
+	state.questionsData = [];
+	state.selQuestionID = '';
+	state.questionDetail = null;
+	state.selRadioValue = '';
+	state.selCheckboxValue = [];
+};
+// 上一题下一题
+const onLastQuestion = () => {
+	let index: number = state.questionsData.findIndex((it) => it.id == state.selQuestionID);
+	index = index - 1 == -1 ? state.questionsData.length : index - 1;
+	onSelQuestion(state.questionsData[index].id);
+};
+const onNextQuestion = () => {
+	let index: number = state.questionsData.findIndex((it) => it.id == state.selQuestionID);
+	index = index + 1 == state.questionsData.length ? 0 : index + 1;
+	onSelQuestion(state.questionsData[index].id);
+};
+// 选择的问题id 提交当前试题答案,再请求下一道题详情 value: 请求下一题详情id
+const onSelQuestion = async (value: any) => {
+	state.loading = true;
+	onGetQuestionDetail(value);
+};
+const onGetQuestionDetail = async (value) => {
+	// 初始化选择选择/填写的答案
+	state.selRadioValue = '';
+	state.selCheckboxValue = [];
+	// 请求下一道题
+	state.selQuestionID = value;
+	try {
+		const { result } = await getPracticeView(state.selQuestionID);
+		state.questionDetail = result;
+		let answerDesc = '';
+		state.questionDetail.practiceQuestionOptionsDtos.forEach((it) => {
+			it.isAnswer && (answerDesc += it.label);
+		});
+		state.questionDetail.answerDesc = answerDesc;
+		if ([0, 2].includes(state.questionDetail.questionType)) {
+			let obj = state.questionDetail.practiceQuestionOptionsDtos.find((x: any) => x.isSelected === true);
+			state.selRadioValue = obj ? obj.questionOptionId : '';
+		} else if (state.questionDetail.questionType == 1) {
+			let arr = [] as any[];
+			state.questionDetail.practiceQuestionOptionsDtos.forEach((it) => {
+				if (it.isSelected) arr.push(it);
+			});
+			state.selCheckboxValue = arr ? arr.map((x: any) => x.questionOptionId) : [];
+		}
+		state.loading = false;
+	} catch (error) {
+		// 打印错误信息
+		console.error(error);
+	}
+};
+// 跳转知识详情页面
+const onKnowladgeTo = (row: any) => {
+	router.push({
+		name: 'knowledgePreview',
+		params: {
+			id: row.knowladgeId,
+			isAddPv: 'isAddPv',
+			tagsViewName: row.title,
+		},
+	});
+};
+// 课件预览下载
+const onSourcewareTo = (row: any) => {
+	ElMessageBox.confirm(`您确定要下载课件,是否继续?`, '提示', {
+		confirmButtonText: '确认',
+		cancelButtonText: '取消',
+		type: 'warning',
+		draggable: true,
+		cancelButtonClass: 'default-button',
+		autofocus: false,
+	})
+		.then(() => {
+			fileDownloadByUrl({
+				Source: 'hotline',
+				Id: row.attachmentId,
+			}).then((res: any) => {
+				let blob: Blob = new Blob([res.data], { type: res.data.type }); // 创建blob 设置blob文件类型 data 设置为后端返回的文件(例如mp3,jpeg) type:这里设置后端返回的类型 为 mp3
+				let down: HTMLAnchorElement = document.createElement('a'); // 创建A标签
+				let href: string = window.URL.createObjectURL(blob); // 创建下载的链接
+				down.href = href; // 下载地址
+				down.download = row.name; // 下载文件名
+				document.body.appendChild(down);
+				down.click(); // 模拟点击A标签
+				document.body.removeChild(down); // 下载完成移除元素
+				window.URL.revokeObjectURL(href); // 释放blob对象
+			});
+		})
+		.catch(() => {});
+};
+// 暴露变量
+defineExpose({
+	openDialog,
+	closeDialog,
+});
+</script>
+<style lang="scss">
+.topContent {
+	padding: 10px 30px;
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+}
+.topContent .surplusBox {
+	text-align: center;
+}
+.topContent .surplusBox .surplus {
+	font-size: 18px;
+	color: #000000;
+}
+.topContent .titleBox {
+	text-align: center;
+}
+.topContent .titleBox .title {
+	font-size: 20px;
+	font-weight: bold;
+	letter-spacing: 1px;
+	color: #000000;
+}
+.topContent .submitBox {
+	text-align: right;
+	vertical-align: top;
+}
+.examListContent .examType {
+	font-weight: bold;
+	color: #000000;
+}
+.examListContent .examList {
+	padding: 5px 0 10px;
+}
+.examListContent .examList .examListItem {
+	display: inline-block;
+	margin: 5px 10px 5px 0;
+	text-align: center;
+	width: 50px;
+	height: 35px;
+	line-height: 35px;
+	cursor: pointer;
+	position: relative;
+	border: #f2f2f2 1px solid;
+}
+.examListContent .examList .right::after {
+	content: ' ';
+	width: 5px;
+	height: 5px;
+	position: absolute;
+	top: 2px;
+	right: 3px;
+	background-color: #009688;
+	border-radius: 50%;
+}
+.examListContent .examList .error::after {
+	content: ' ';
+	width: 5px;
+	height: 5px;
+	position: absolute;
+	top: 2px;
+	right: 3px;
+	background-color: #ff5722;
+	border-radius: 50%;
+}
+.examListContent .examList .select {
+	background-color: #1890ff;
+	color: #fff;
+}
+.questionBox {
+	padding: 20px 10px 0;
+}
+.questionBox .questionTitle {
+	margin-bottom: 10px;
+	font-size: 16px;
+}
+.questionBox .questionAnswer {
+	padding: 0 20px;
+}
+.referenceAnswer {
+	background-color: #ebf9ff;
+	margin: 15px 40px 15px 30px;
+	padding: 10px;
+}
+.referenceAnswer span {
+	display: block;
+}
+.referenceAnswer p {
+	width: calc(100% - 80px);
+	display: block;
+	margin-top: 10px;
+	color: #1890ff;
+	cursor: pointer;
+}
+</style>

+ 227 - 0
src/views/examTrain/exam/marking/components/Exam-Marking.vue

@@ -0,0 +1,227 @@
+<template>
+	<div class="exam-train-exam-marking-edit-container layout-padding">
+		<div class="exam-train-exam-marking-edit-container-box pd20 h100">
+			<splitpanes :horizontal="false">
+				<pane min-size="16" max-size="25" size="16" class="left-container">
+					<p class="border-title mb20">考试人员</p>
+					<el-scrollbar ref="scrollBarRef" style="height: calc(100% - 55px)" always>
+						<el-auto-resizer class="table">
+							<template #default="{ height }">
+								<el-skeleton :loading="state.userLoading" animated :rows="10">
+									<template #default>
+										<el-tree-v2
+											:data="state.userList"
+											highlight-current
+											:expand-on-click-node="false"
+											node-key="userId"
+											:props="{ children: 'children', label: 'userName' }"
+											@node-click="handleNodeClick"
+											:current-node-key="state.userList[0]?.id"
+											ref="treeRef"
+											:item-size="36"
+											empty-text="暂无数据"
+											:height="height"
+										>
+											<template #default="{ node }">
+												<span>{{ node.label }}</span>
+											</template>
+										</el-tree-v2>
+									</template>
+								</el-skeleton>
+							</template>
+						</el-auto-resizer>
+					</el-scrollbar>
+				</pane>
+				<pane class="right-container">
+					<p class="border-title mb20">阅卷试卷</p>
+					<div style="overflow: hidden; width: 100%; height: 100%; flex: 1">
+						<el-row v-loading="state.loading" class="h100 w100">
+							<el-col>
+								<div class="questionBox" v-if="state.questionDetail">
+									<p class="questionTitle">题干:{{ state.questionDetail.title }}</p>
+									<div class="questionAnswer">
+										<div v-if="[0, 2].includes(state.questionDetail.questionType)">
+											<el-radio-group style="display: block" v-model="state.selRadioValue">
+												<el-form-item
+													:label="item.label + '. '"
+													v-for="item in state.questionDetail.practiceQuestionOptionsDtos"
+													style="margin-bottom: 10px"
+												>
+													<el-radio :label="item.content" :value="item.questionOptionId" />
+												</el-form-item>
+											</el-radio-group>
+										</div>
+										<div v-else-if="state.questionDetail.questionType == 1">
+											<el-checkbox-group v-model="state.selCheckboxValue">
+												<el-form-item
+													:label="item.label + '. '"
+													v-for="item in state.questionDetail.practiceQuestionOptionsDtos"
+													style="margin-bottom: 10px"
+												>
+													<el-checkbox :label="item.content" :value="item.questionOptionId" />
+												</el-form-item>
+											</el-checkbox-group>
+										</div>
+										<!-- <div v-else-if="state.questionDetail.questionType == 2"></div> -->
+										<!-- <div v-else-if="state.questionDetail.questionType == 3">
+												<el-form-item label="答:">
+														<el-input type="textarea" v-model="state.answerValue" :rows="3" placeholder="请输入答案,如果有多个请依照顺序用英文 , 隔开" clearable></el-input>
+												</el-form-item>
+										</div>
+										<div v-else-if="state.questionDetail.questionType == 4">
+												<el-form-item label="答:">
+														<el-input type="textarea" v-model="state.answerValue" :rows="3" placeholder="请输入答案" clearable></el-input>
+												</el-form-item>
+										</div> -->
+									</div>
+								</div>
+							</el-col>
+						</el-row>
+					</div>
+					<div style="text-align: center">
+						<el-button type="primary" @click="onSave(ruleFormRef)" :loading="state.loading">保存</el-button>
+						<el-button class="default-button" @click="onCancel" :loading="state.loading">关闭 </el-button>
+					</div>
+				</pane>
+			</splitpanes>
+		</div>
+	</div>
+</template>
+
+<script setup lang="ts" name="examTrainExamMarkingEdit">
+import { onMounted, reactive, ref } from 'vue';
+import type { FormInstance } from 'element-plus';
+import { ElMessage } from 'element-plus';
+import mittBus from '@/utils/mitt';
+import { useRoute, useRouter } from 'vue-router';
+import { Splitpanes, Pane } from 'splitpanes';
+import 'splitpanes/dist/splitpanes.css';
+import { Local } from '@/utils/storage';
+import other from '@/utils/other';
+import { throttle, transformFile, guid } from '@/utils/tools';
+import { addExamManage, editExamManage, getExamManageDetail, getTestPaperQuestionCount } from '@/api/examTrain/examManage';
+import { getMarkingDetailByUser, getMarkingDetailUser } from '@/api/examTrain/marking';
+
+// 定义变量内容
+const state = reactive<any>({
+	loading: false,
+	userList: [], // 用户列表
+	queryParams: {},
+	userLoading: false,
+	UserId: null, // 用户ID
+});
+
+const route = useRoute(); // 获取路由
+const router = useRouter(); // 路由跳转
+const ruleFormRef = ref<any>(); // 表单ref
+// 点击人员查询试卷
+const handleNodeClick = (data: any) => {
+	state.UserId = data.userId;
+	getExamManageDetailData();
+};
+// 获取用户列表
+const routeParams = route.params;
+const getUserList = async () => {
+	state.userLoading = true;
+	try {
+		const { result } = await getMarkingDetailUser({ ExamId: route.params.examId });
+		state.userList = result ?? [];
+		state.userLoading = false;
+		state.UserId = result[0]?.userId;
+		await getExamManageDetailData();
+	} catch (error) {
+		console.error(error);
+		state.userLoading = false;
+	}
+};
+// 获取试卷内容
+const getExamManageDetailData = async () => {
+	state.loading = true;
+	try {
+		const { result } = await getMarkingDetailByUser({ ExamId: route.params.examId, UserId: state.UserId });
+		console.log(result);
+		state.loading = false;
+	} catch (error) {
+		console.error(error);
+		state.loading = false;
+	}
+};
+// 保存提交
+const onSave = throttle(async (formEl: FormInstance | undefined) => {
+	if (!formEl) return;
+	await formEl.validate((valid: boolean) => {
+		if (!valid) return;
+		state.loading = true;
+		const submitObj = other.deepClone(state.ruleForm);
+		if (submitObj.examType == 0) {
+			submitObj.startTime = submitObj.rangeTime[0];
+			submitObj.endTime = submitObj.rangeTime[1];
+			Reflect.deleteProperty(submitObj, 'rangeTime');
+		} else if (submitObj.examType == 1) {
+			Reflect.deleteProperty(submitObj, 'rangeTime');
+			Reflect.deleteProperty(submitObj, 'startTime');
+			Reflect.deleteProperty(submitObj, 'endTime');
+			Reflect.deleteProperty(submitObj, 'count');
+			Reflect.deleteProperty(submitObj, 'timeSpan');
+		}
+		if (submitObj.mode == 1) {
+			Reflect.deleteProperty(submitObj, 'questionSort');
+		}
+		console.log(submitObj);
+		if (route.params.id) {
+			editExamManage(submitObj)
+				.then(handleSuccess)
+				.catch(() => {
+					state.loading = false;
+				});
+		} else {
+			// 新增
+			addExamManage(submitObj)
+				.then(handleSuccess)
+				.catch(() => {
+					state.loading = false;
+				});
+		}
+	});
+}, 300);
+// 取消
+const onCancel = () => {
+	state.loading = true;
+	mittBus.emit('onCurrentContextmenuClick', Object.assign({}, { contextMenuClickId: 1, ...route }));
+	mittBus.emit('clearCache', 'examTrainExamMarking');
+	router.push({
+		path: '/examTrain/exam/marking',
+	});
+	state.loading = false;
+};
+const handleSuccess = () => {
+	state.loading = false;
+	ElMessage.success('操作成功');
+	// 关闭当前 tagsView
+	mittBus.emit('onCurrentContextmenuClick', Object.assign({}, { contextMenuClickId: 1, ...route }));
+	mittBus.emit('clearCache', 'examTrainExamMarking');
+	router.push({
+		path: '/examTrain/exam/marking',
+	});
+};
+onMounted(() => {
+	state.queryParams.ExamId = routeParams.examId;
+	getUserList();
+});
+</script>
+<style lang="scss" scoped>
+.exam-train-exam-marking-edit-container {
+	.exam-train-exam-marking-edit-container-box {
+		background-color: var(--el-color-white);
+		overflow: hidden;
+	}
+	.right-container {
+		height: 100%;
+		display: flex;
+		flex-direction: column;
+	}
+}
+:deep(.el-tree-node__content) {
+	height: 40px;
+}
+</style>

+ 157 - 0
src/views/examTrain/exam/marking/index.vue

@@ -0,0 +1,157 @@
+<template>
+	<div class="exam-train-exam-marking-container layout-padding">
+		<div class="layout-padding-auto layout-padding-view pd20">
+			<el-form :model="state.queryParams" ref="ruleFormRef" inline @submit.native.prevent :disabled="state.tableLoading">
+				<el-form-item label="是否批改" prop="isCheck">
+					<el-select v-model="state.queryParams.isCheck" clearable placeholder="请选择是否批改" @change="handleQuery">
+						<el-option label="已批改完成" :value="true" />
+						<el-option label="未批改完成" :value="false" />
+					</el-select>
+				</el-form-item>
+				<el-form-item>
+					<el-button type="primary" @click="handleQuery" :loading="state.loading"> <SvgIcon name="ele-Search" class="mr5" />查询 </el-button>
+					<el-button @click="resetQuery(ruleFormRef)" class="default-button" :loading="state.loading">
+						<SvgIcon name="ele-Refresh" class="mr5" />重置
+					</el-button>
+				</el-form-item>
+			</el-form>
+			<vxe-toolbar
+				ref="toolbarRef"
+				:loading="state.tableLoading"
+				custom
+				:refresh="{
+					queryMethod: handleQuery,
+				}"
+			>
+			</vxe-toolbar>
+			<div style="overflow: hidden; width: 100%; height: 100%; flex: 1">
+				<vxe-table
+					border
+					:loading="state.tableLoading"
+					:data="state.tableData"
+					:column-config="{ resizable: true }"
+					:row-config="{ isCurrent: true, isHover: true, height: 30, useKey: true }"
+					ref="tableRef"
+					height="auto"
+					auto-resize
+					show-overflow
+					:print-config="{}"
+					:scrollY="{ enabled: true, gt: 100 }"
+					id="examTrainExamMarking"
+					:custom-config="{ storage: true }"
+					showHeaderOverflow
+				>
+					<vxe-column field="examCode" title="考试编号" width="200"></vxe-column>
+					<vxe-column field="examName" title="考试标题" width="150"></vxe-column>
+					<vxe-column field="totalScore" title="考试总分" width="150"></vxe-column>
+					<vxe-column field="cutoffScore" title="合格分数" width="150"></vxe-column>
+					<vxe-column field="isCheck" title="是否批改" width="150">
+						<template #default="{ row }">
+							{{ row.isCheck ? '已批改完成' : '未批改完成' }}
+						</template>
+					</vxe-column>
+					<vxe-column field="remark" title="备注" min-width="200"></vxe-column>
+					<vxe-column title="操作" fixed="right" width="80" align="center" :show-overflow="false">
+						<template #default="{ row }">
+							<el-button link type="primary" @click="onView(row)" title="查看" v-if="row.isCheck"> 查看 </el-button>
+							<el-button link type="primary" @click="onMarking(row)" title="阅卷" v-else> 阅卷 </el-button>
+						</template>
+					</vxe-column>
+				</vxe-table>
+			</div>
+			<pagination
+				@pagination="queryList"
+				:total="state.total"
+				v-model:current-page="state.queryParams.PageIndex"
+				v-model:page-size="state.queryParams.PageSize"
+				:disabled="state.tableLoading"
+			/>
+		</div>
+		<!-- 阅卷查看	-->
+		<exam-marking-view ref="examMarkingViewRef" />
+	</div>
+</template>
+
+<script lang="tsx" setup name="examTrainExamMarking">
+import { ref, reactive, onMounted, defineAsyncComponent } from 'vue';
+import 'splitpanes/dist/splitpanes.css';
+import Other from '@/utils/other';
+import { FormInstance } from 'element-plus';
+import { getMarkingList } from '@/api/examTrain/marking';
+import { useRouter } from 'vue-router';
+
+// 引入组件
+const pagination = defineAsyncComponent(() => import('@/components/ProTable/components/Pagination.vue')); // 分页
+const ExamMarkingView = defineAsyncComponent(() => import('@/views/examTrain/exam/marking/components/Exam-Marking-View.vue')); // 阅卷查看
+
+// 定义变量内容
+const state = reactive<any>({
+	queryParams: {
+		PageIndex: 1, //页码
+		PageSize: 20, //每页条数
+		isCheck: null, // 是否批改
+	},
+	tableLoading: false, //表格loading
+	tableData: [], //表格数据
+	total: 0, //总条数
+});
+/** 搜索按钮操作 节流操作 */
+const handleQuery = () => {
+	state.queryParams.PageIndex = 1;
+	queryList();
+};
+/** 获取试题列表 */
+const requestParams = ref<EmptyObjectType>({});
+const queryList = () => {
+	state.tableLoading = true;
+	requestParams.value = Other.deepClone(state.queryParams);
+	getMarkingList(requestParams.value)
+		.then((response: any) => {
+			state.tableData = response?.result.items ?? [];
+			state.total = response?.result.pagination.totalCount ?? 0;
+			state.tableLoading = false;
+		})
+		.catch(() => {
+			state.tableLoading = false;
+		});
+};
+/** 重置按钮操作 */
+const ruleFormRef = ref<RefType>(); // 表单ref
+const resetQuery = (formEl: FormInstance | undefined) => {
+	if (!formEl) return;
+	formEl.resetFields();
+	queryList();
+};
+// 阅卷
+const router = useRouter(); //路由
+const onMarking = (row: any) => {
+	router.push({
+		name: 'examTrainExamMarkingEdit',
+		params: {
+			tagsViewName: `阅卷 ${row.examName}`,
+			examId: row.id,
+		},
+	});
+};
+// 阅卷查看
+const examMarkingViewRef = ref<RefType>();
+const onView = (row: any) => {
+	router.push({
+		name: 'examTrainExamMarkingEdit',
+		params: {
+			tagsViewName: `阅卷 ${row.examName}`,
+			examId: row.id,
+			isView: 'true',
+		},
+	});
+	examMarkingViewRef.value.openDialog(row);
+};
+const tableRef = ref<RefType>();
+const toolbarRef = ref<RefType>();
+onMounted(() => {
+	queryList();
+	if (tableRef.value && toolbarRef.value) {
+		tableRef.value.connect(toolbarRef.value);
+	}
+});
+</script>