浏览代码

Merge branch 'refs/heads/test/exam' into dev

# Conflicts:
#	src/router/route.ts
#	src/views/examTrain/exam/marking/components/Exam-Marking.vue
#	src/views/examTrain/exam/marking/index.vue
zhangchong 3 天之前
父节点
当前提交
17c2294a2c
共有 28 个文件被更改,包括 1215 次插入208 次删除
  1. 1 1
      .env.development
  2. 2 2
      src/api/examTrain/questionBank.ts
  3. 66 0
      src/api/examTrain/userTrain.ts
  4. 45 14
      src/router/route.ts
  5. 1 1
      src/views/courseware/index/index.vue
  6. 1 1
      src/views/examTrain/exam/examManage/components/ExamManage-users.vue
  7. 3 3
      src/views/examTrain/exam/examManage/index.vue
  8. 3 3
      src/views/examTrain/exam/extractRule/index.vue
  9. 33 103
      src/views/examTrain/exam/marking/components/Exam-Marking.vue
  10. 4 36
      src/views/examTrain/exam/marking/index.vue
  11. 3 3
      src/views/examTrain/exam/practice/index.vue
  12. 5 6
      src/views/examTrain/exam/testPaper/components/TestPaper-optional.vue
  13. 3 3
      src/views/examTrain/exam/testPaper/index.vue
  14. 2 1
      src/views/examTrain/exam/userExam/components/UserExam-exam.vue
  15. 3 3
      src/views/examTrain/exam/userExam/index.vue
  16. 5 5
      src/views/examTrain/questionBank/components/Question-course.vue
  17. 4 4
      src/views/examTrain/questionBank/components/Question-knowledge.vue
  18. 3 0
      src/views/examTrain/questionBank/edit.vue
  19. 3 3
      src/views/examTrain/questionBank/index.vue
  20. 2 2
      src/views/examTrain/tag/index.vue
  21. 3 3
      src/views/examTrain/train/plan/index.vue
  22. 8 4
      src/views/examTrain/train/template/components/Template-add.vue
  23. 8 4
      src/views/examTrain/train/template/components/Template-edit.vue
  24. 196 0
      src/views/examTrain/train/template/components/Template-optional.vue
  25. 3 3
      src/views/examTrain/train/template/index.vue
  26. 223 0
      src/views/examTrain/train/userTrain/components/UserTrain-view.vue
  27. 170 0
      src/views/examTrain/train/userTrain/index.vue
  28. 412 0
      src/views/examTrain/train/userTrain/study.vue

+ 1 - 1
.env.development

@@ -5,7 +5,7 @@ VITE_STORAGE_NAME=dev
 # 业务系统基础请求地址
 VITE_API_URL=http://110.188.24.28:50100
 # 业务系统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
 # 数据共享平台请求地址

+ 2 - 2
src/api/examTrain/questionBank.ts

@@ -80,7 +80,7 @@ export const fileDownloadBatch = (params: object) => {
  */
 export const questionTemplate = (params?: object) => {
     return request({
-        url: ``,
+        url: `/api/v1/Question/Download`,
         method: 'get',
         responseType: 'blob'
     },{
@@ -94,7 +94,7 @@ export const questionTemplate = (params?: object) => {
  */
 export const questionImport = (data: object) => {
     return request({
-        url: ``,
+        url: `/api/v1/Question/ImportExcel`,
         method: 'post',
         data,
         headers: {

+ 66 - 0
src/api/examTrain/userTrain.ts

@@ -0,0 +1,66 @@
+/*
+ * @Author: zjq
+ * @description 我的培训
+ */
+import request from '@/utils/request';
+
+/**
+ * @description 获取培训列表
+ * @param {object} params
+ */
+export const getUserTrainData = (params?: object) => {
+    return request({
+        url: '/api/v1/TrainRecord/GetPagedList',
+        method: 'post',
+        data: params,
+    });
+};
+
+/**
+ * @description 获取培训详情
+ * @param {any} Id
+ */
+export const getTrainInfo = (Id: any) => {
+    return request({
+        url: `/api/v1/TrainRecord/Get`,
+        method: 'post',
+        data: {
+            id: Id
+        }
+    });
+};
+
+/**
+ * @description 提交培训习题
+ * @param {object} data
+ */
+export const answerUserTrain = (data: object) => {
+    return request({
+        url: '/api/v1/TrainRecord/Train',
+        method: 'post',
+        data,
+    });
+};
+
+/**
+ * @description 获取培训试题详情
+ * @param {any} Id
+ */
+export const getTrainQuestion = (Id: any, TrainPracticeId: any) => {
+    return request({
+        url: `/api/v1/TrainRecord/GetTrainPractice?Id=${Id}&TrainPracticeId=${TrainPracticeId}`,
+        method: 'get',
+    });
+};
+
+/**
+ * @description 结束培训
+ * @param {object} params
+ */
+export const submitUserTrain = (params?: object) => {
+    return request({
+        url: '/api/v1/TrainRecord/CompleteTrainRecord',
+        method: 'post',
+        data: params,
+    });
+};

+ 45 - 14
src/router/route.ts

@@ -437,7 +437,8 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
 			title: '新建工单',
 			isKeepAlive: true,
 		},
-	},{
+	},
+	{
 		path: '/statistics/order/detailAcceptTypeList',
 		name: 'statisticsOrderDetailAcceptTypeList',
 		component: () => import('@/views/statistics/order/detailAcceptTypeList.vue'),
@@ -445,7 +446,8 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
 			title: '受理类型统计列表',
 			isKeepAlive: true,
 		},
-	},{
+	},
+	{
 		path: '/statistics/order/detailAcceptType',
 		name: 'statisticsDetailAcceptType',
 		component: () => import('@/views/statistics/order/detailAcceptType.vue'),
@@ -453,7 +455,8 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
 			title: '受理类型统计明细',
 			isKeepAlive: true,
 		},
-	},{
+	},
+	{
 		path: '/statistics/order/detailHotspotSatisfied',
 		name: 'statisticsOrderDetailHotspotSatisfied',
 		component: () => import('@/views/statistics/order/detailHotspotSatisfied.vue'),
@@ -461,7 +464,8 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
 			title: '热点满意度统计明细',
 			isKeepAlive: true,
 		},
-	},{
+	},
+	{
 		path: '/statistics/call/detailIndexTime',
 		name: 'statisticsCallDetailIndexTime',
 		component: () => import('@/views/statistics/call/detailIndexTime.vue'),
@@ -469,7 +473,8 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
 			title: '话务时段分析日期',
 			isKeepAlive: true,
 		},
-	},{
+	},
+	{
 		path: '/statistics/call/detailIndexCall',
 		name: 'statisticsCallDetailIndexCall',
 		component: () => import('@/views/statistics/call/detailIndexCall.vue'),
@@ -486,8 +491,7 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
 			title: '坐席话务统计分析明细',
 			isKeepAlive: true,
 		},
-	}
-	,{
+	},{
 		path: '/statistics/center/detailReport/:id/:tagsViewName?',
 		name: 'statisticsCenterDetailReport',
 		component: () => import('@/views/statistics/center/detail-report.vue'),
@@ -496,7 +500,8 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
 			isKeepAlive: true,
 			isDynamic: true,
 		},
-	},{
+	},
+	{
 		path: '/statistics/center/detailAcceptCenter',
 		name: 'statisticsCenterDetailAcceptCenter',
 		component: () => import('@/views/statistics/center/detailAcceptCenter.vue'),
@@ -504,7 +509,8 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
 			title: '企业专席明细',
 			isKeepAlive: true,
 		},
-	},{
+	},
+	{
 		path: '/statistics/center/detailTwist',
 		name: 'statisticsCenterDetailTwist',
 		component: () => import('@/views/statistics/center/detailTwist.vue'),
@@ -512,7 +518,8 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
 			title: '扭转明细列表',
 			isKeepAlive: true,
 		},
-	},{
+	},
+	{
 		path: '/todo/edit/record',
 		name: 'todoEditRecord',
 		component: () => import('@/views/todo/edit/record.vue'),
@@ -520,7 +527,8 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
 			title: '修改记录',
 			isKeepAlive: true,
 		},
-	},{
+	},
+	{
 		path: '/statistics/call/detailSeatDate',
 		name: 'statisticsCallDetailSeatsDate',
 		component: () => import('@/views/statistics/call/detailSeatDate.vue'),
@@ -528,7 +536,8 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
 			title: '话务时段明细',
 			isKeepAlive: true,
 		},
-	},{
+	},
+	{
 		path: '/statistics/center/detailSeatSatisfactionList',
 		name: 'statisticsCenterDetailSeatSatisfactionList',
 		component: () => import('@/views/statistics/center/detailSeatSatisfactionList.vue'),
@@ -536,7 +545,8 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
 			title: '坐席满意度明细表',
 			isKeepAlive: true,
 		},
-	},{
+	},
+	{
 		path: '/statistics/center/detailSeatSatisfaction',
 		name: 'statisticsCenterDetailSeatSatisfaction',
 		component: () => import('@/views/statistics/center/detailSeatSatisfaction.vue'),
@@ -544,7 +554,8 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
 			title: '坐席满意度列表明细',
 			isKeepAlive: true,
 		},
-	},{
+	},
+	{
 		path: '/statistics/call/detailSeatsMoth',
 		name: 'statisticsCallSeatsMothDetail',
 		component: () => import('@/views/statistics/call/detailSeatsMoth.vue'),
@@ -795,6 +806,26 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
 			isDynamic: true,
 		},
 	},
+	{
+		path: '/examTrain/train/userTrain/study/:tagsViewName/:id/:trainName?',
+		name: 'trainStudy',
+		component: () => import('@/views/examTrain/train/userTrain/study.vue'),
+		meta: {
+			title: '培训学习',
+			isKeepAlive: true,
+			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界面

+ 1 - 1
src/views/courseware/index/index.vue

@@ -199,7 +199,7 @@ const queryList = () => {
 		.then((response: any) => {
             console.log(response)
 			state.tableData = response?.result.items ?? [];
-			state.total = response?.result.pagination.totalCount;
+			state.total = response?.result.pagination.totalCount ?? 0;
 			state.tableLoading = false;
 		})
 		.catch(() => {

+ 1 - 1
src/views/examTrain/exam/examManage/components/ExamManage-users.vue

@@ -121,7 +121,7 @@ const queryList = () => {
 	getUserListPaged(state.queryParams)
 		.then((res: any) => {
 			state.tableData = res.result?.items ?? [];
-			state.total = res.result?.total ?? 0;
+			state.total = res.result?.pagination.totalCount ?? 0;
             const rows = [] as any[];
             state.tableCheckbox.forEach(item => {
                 rows.push(state.tableData.find(it => it.id === item.userId));

+ 3 - 3
src/views/examTrain/exam/examManage/index.vue

@@ -49,7 +49,7 @@
                     show-overflow
                     :print-config="{}"
                     :scrollY="{ enabled: true, gt: 100 }"
-                    id="examManage"
+                    id="examTrainExamExamManage"
                     :custom-config="{ storage: true }"
                     showHeaderOverflow
                 >
@@ -128,7 +128,7 @@
 	</div>
 </template>
 
-<script lang="tsx" setup name="examManage">
+<script lang="tsx" setup name="examTrainExamExamManage">
 import { ref, reactive, onMounted, defineAsyncComponent, computed } from 'vue';
 import { ElMessageBox, ElMessage } from 'element-plus';
 import { useRouter } from 'vue-router';
@@ -171,7 +171,7 @@ const queryList = () => {
 	getExamManageData(requestParams.value)
 		.then((response: any) => {
 			state.tableData = response?.result.items ?? [];
-			state.total = response?.result.pagination.totalCount;
+			state.total = response?.result.pagination.totalCount ?? 0;
 			state.tableLoading = false;
 		})
 		.catch(() => {

+ 3 - 3
src/views/examTrain/exam/extractRule/index.vue

@@ -46,7 +46,7 @@
                     show-overflow
                     :print-config="{}"
                     :scrollY="{ enabled: true, gt: 100 }"
-                    id="extractRule"
+                    id="examTrainExamExtractRule"
                     :custom-config="{ storage: true }"
                     showHeaderOverflow
                 >
@@ -84,7 +84,7 @@
 	</div>
 </template>
 
-<script lang="tsx" setup name="extractRule">
+<script lang="tsx" setup name="examTrainExamExtractRule">
 import { ref, reactive, onMounted, defineAsyncComponent, computed } from 'vue';
 import { ElMessageBox, ElMessage } from 'element-plus';
 import { useRouter } from 'vue-router';
@@ -129,7 +129,7 @@ const queryList = () => {
 		.then((response: any) => {
             console.log(response)
 			state.tableData = response?.result.items ?? [];
-			state.total = response?.result.pagination.totalCount;
+			state.total = response?.result.pagination.totalCount ?? 0;
 			state.tableLoading = false;
 		})
 		.catch(() => {

+ 33 - 103
src/views/examTrain/exam/marking/components/Exam-Marking.vue

@@ -13,7 +13,7 @@
 											:data="state.userList"
 											highlight-current
 											:expand-on-click-node="false"
-											node-key="id"
+											node-key="userId"
 											:props="{ children: 'children', label: 'userName' }"
 											@node-click="handleNodeClick"
 											:current-node-key="state.userList[0]?.id"
@@ -22,6 +22,9 @@
 											empty-text="暂无数据"
 											:height="height"
 										>
+											<template #default="{ node }">
+												<span>{{ node.label }}</span>
+											</template>
 										</el-tree-v2>
 									</template>
 								</el-skeleton>
@@ -35,7 +38,7 @@
 						<el-scrollbar style="height: calc(100% - 32px)" always v-loading="state.loading">
 							<div class="h100 w100 exam-container">
 								<div v-for="item in state.examList" :key="item.id" class="exam-item">
-									<p class="exam-item-questionName">题目:{{ item.questionName }}</p>
+									<p class="exam-item-questionName">题目:{{ item.title }}</p>
 									<p class="exam-item-answer">回答答案:{{ item.answer }}</p>
 									<el-divider></el-divider>
 									<p class="exam-item-referenceAnswer">参考答案:{{ item.referenceAnswer }}</p>
@@ -85,7 +88,7 @@ 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';
+import { batchMarking, getMarkingDetailByUser, getMarkingDetailUser } from '@/api/examTrain/marking';
 
 // 定义变量内容
 const state = reactive<any>({
@@ -102,35 +105,19 @@ const router = useRouter(); // 路由跳转
 const ruleFormRef = ref<any>(); // 表单ref
 // 点击人员查询试卷
 const handleNodeClick = (data: any) => {
-	state.userId = data.id;
+	state.UserId = data.userId;
 	getExamManageDetailData();
 };
 // 获取用户列表
 const routeParams = route.params;
 const getUserList = async () => {
-	const mockUserList = [
-		{
-			id: '999',
-			userName: '张伟',
-		},
-		{
-			id: '888',
-			userName: '吴迪',
-		},
-	];
 	state.userLoading = true;
 	try {
-		/*	const { result } = await getMarkingDetailUser({ ExamId: route.params.examId });
+		const { result } = await getMarkingDetailUser({ ExamId: route.params.examId });
 		state.userList = result ?? [];
 		state.userLoading = false;
 		state.UserId = result[0]?.userId;
-		await getExamManageDetailData();*/
-		state.userList = other.deepClone(mockUserList);
-		state.UserId = mockUserList[0]?.id;
-		setTimeout(() => {
-			state.userLoading = false;
-			getExamManageDetailData();
-		}, 500);
+		await getExamManageDetailData();
 	} catch (error) {
 		console.error(error);
 		state.userLoading = false;
@@ -140,106 +127,49 @@ const getUserList = async () => {
 const getExamManageDetailData = async () => {
 	state.loading = true;
 	try {
-		/*		const { result } = await getMarkingDetailByUser({ ExamId: route.params.examId, UserId: state.UserId });
-		console.log(result);*/
-		setTimeout(() => {
-			// 模拟数据
-			if (isView) {
-				state.examList = [
-					{
-						id: '1',
-						questionName: '经销商员工管理,如新增员工信息需()在创建角色。',
-						answer: '电脑上',
-						referenceAnswer: '(电脑端)',
-						score: 100,
-					},
-					{
-						id: '2',
-						questionName: '上级经销商一键入库后,()无法一键入库只能扫码入库。',
-						answer: '',
-						referenceAnswer: '(分销商)',
-						score: 0,
-					},
-					{
-						id: '3',
-						questionName: '客户需妥善管理经销商登录密码,密码重置需(),暂不支持自行重置。',
-						answer: '管理员',
-						referenceAnswer: '(超级管理员)',
-						score: 100,
-					},
-				];
-			} else {
-				state.examList = [
-					{
-						id: '1',
-						questionName: '经销商员工管理,如新增员工信息需()在创建角色。',
-						answer: '电脑上',
-						referenceAnswer: '(电脑端)',
-						score: null,
-					},
-					{
-						id: '2',
-						questionName: '上级经销商一键入库后,()无法一键入库只能扫码入库。',
-						answer: '分销商',
-						referenceAnswer: '(分销商)',
-						score: null,
-					},
-					{
-						id: '3',
-						questionName: '客户需妥善管理经销商登录密码,密码重置需(),暂不支持自行重置。',
-						answer: '管理员',
-						referenceAnswer: '(超级管理员)',
-						score: null,
-					},
-				];
-			}
-			state.loading = false;
-		}, 500);
+		const { result } = await getMarkingDetailByUser({ ExamId: route.params.examId, UserId: state.UserId });
+		state.examList = result ?? [];
+		state.loading = false;
 	} catch (error) {
 		console.error(error);
 		state.loading = false;
 	}
 };
 // 保存提交
-const onSave = throttle((formEl: FormInstance | undefined) => {
-	state.loading = true;
-	handleSuccess();
-	/*	if (!formEl) return;
-	 formEl.validate((valid: boolean) => {
-		if (!valid) return;
-		state.loading = true;
-		const submitObj = other.deepClone(state.ruleForm);
-		 handleSuccess();
-	});*/
+const onSave = throttle(() => {
+	const request = state.examList.map((item: any) => {
+		return {
+			userExamItemId: item.id,
+			Score: item.score,
+			isCheck: true,
+		};
+	});
+	batchMarking({items:request})
+		.then(() => {
+			state.loading = false;
+			handleSuccess();
+		})
+		.catch((error) => {
+			console.error(error);
+			state.loading = false;
+		});
 }, 300);
-// 提交
-const onSubmit = () => {};
 // 取消
 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 = () => {
-	setTimeout(() => {
-		state.loading = false;
-		ElMessage.success('保存成功');
-	}, 500);
-	// 关闭当前 tagsView
-	/*	mittBus.emit('onCurrentContextmenuClick', Object.assign({}, { contextMenuClickId: 1, ...route }));
-	mittBus.emit('clearCache', 'examTrainExamMarking');
-	router.push({
-		path: '/examTrain/exam/marking',
-	});*/
 };
 // 是否查看
 const isView = computed(() => {
 	return route.params.isView === 'true';
 });
+const handleSuccess = () => {
+	state.loading = false;
+	ElMessage.success('保存成功');
+};
 onMounted(() => {
 	state.queryParams.ExamId = routeParams.examId;
 	getUserList();

+ 4 - 36
src/views/examTrain/exam/marking/index.vue

@@ -42,7 +42,7 @@
 					showHeaderOverflow
 				>
 					<vxe-column field="examCode" title="考试编号" width="200"></vxe-column>
-					<vxe-column field="examName" title="考试标题" min-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">
@@ -79,7 +79,6 @@ import Other from '@/utils/other';
 import { FormInstance } from 'element-plus';
 import { getMarkingList } from '@/api/examTrain/marking';
 import { useRouter } from 'vue-router';
-import other from '@/utils/other';
 
 // 引入组件
 const pagination = defineAsyncComponent(() => import('@/components/ProTable/components/Pagination.vue')); // 分页
@@ -106,39 +105,7 @@ const requestParams = ref<EmptyObjectType>({});
 const queryList = () => {
 	state.tableLoading = true;
 	requestParams.value = Other.deepClone(state.queryParams);
-	const data = [
-		{
-			examCode: '20251001',
-			examName: '2025年秋季学期数学期末考试',
-			totalScore: 100,
-			cutoffScore: 60,
-			isCheck: true,
-			remark: '2025年秋季学期数学期末考试',
-			id: '0',
-		},
-		{
-			examCode: '20251002',
-			examName: '2025年秋季学期英语期末考试',
-			totalScore: 100,
-			cutoffScore: 60,
-			isCheck: false,
-			remark: '2025年秋季学期英语期末考试',
-			id: '1',
-		},
-	];
-	// 模拟查询
-	setTimeout(() => {
-		if (state.queryParams.isCheck === null) {
-			state.tableLoading = false;
-			state.tableData = other.deepClone(data);
-			return;
-		}
-		console.log('请求参数', requestParams.value);
-		// 模拟数据
-		state.tableData = data.filter((item) => item.isCheck === state.queryParams.isCheck);
-		state.tableLoading = false;
-	}, 500);
-	/*	getMarkingList(requestParams.value)
+	getMarkingList(requestParams.value)
 		.then((response: any) => {
 			state.tableData = response?.result.items ?? [];
 			state.total = response?.result.pagination.totalCount ?? 0;
@@ -146,7 +113,7 @@ const queryList = () => {
 		})
 		.catch(() => {
 			state.tableLoading = false;
-		});*/
+		});
 };
 /** 重置按钮操作 */
 const ruleFormRef = ref<RefType>(); // 表单ref
@@ -167,6 +134,7 @@ const onMarking = (row: any) => {
 	});
 };
 // 阅卷查看
+const examMarkingViewRef = ref<RefType>();
 const onView = (row: any) => {
 	router.push({
 		name: 'examTrainExamMarkingEdit',

+ 3 - 3
src/views/examTrain/exam/practice/index.vue

@@ -28,7 +28,7 @@
                     show-overflow
                     :print-config="{}"
                     :scrollY="{ enabled: true, gt: 100 }"
-                    id="practice"
+                    id="examTrainExamPractice"
                     :custom-config="{ storage: true }"
                     showHeaderOverflow
                 >
@@ -62,7 +62,7 @@
 	</div>
 </template>
 
-<script lang="tsx" setup name="practice">
+<script lang="tsx" setup name="examTrainExamPractice">
 import { ref, reactive, onMounted, defineAsyncComponent, computed } from 'vue';
 import { ElMessageBox, ElMessage } from 'element-plus';
 import { useRouter } from 'vue-router';
@@ -104,7 +104,7 @@ const queryList = () => {
 	getPracticeData(requestParams.value)
 		.then((response: any) => {
 			state.tableData = response?.result.items ?? [];
-			state.total = response?.result.pagination.totalCount;
+			state.total = response?.result.pagination.totalCount ?? 0;
 			state.tableLoading = false;
 		})
 		.catch(() => {

+ 5 - 6
src/views/examTrain/exam/testPaper/components/TestPaper-optional.vue

@@ -16,7 +16,7 @@
                 />
             </el-form-item>
             <el-form-item label="难易程度" prop="difficultyLevel">
-                <el-select v-model="state.queryParams.difficultyLevel" clearable placeholder="请选择" @change="handleQuery" :disabled="state.tableLoading" style="width: 150px">
+                <el-select v-model="state.queryParams.difficultyLevel" clearable placeholder="请选择" @change="handleQuery" :disabled="state.loading" style="width: 150px">
                     <el-option key='0' label="容易" :value="0" />
                     <el-option key='1' label="适中" :value="1" />
                     <el-option key='2' label="困难" :value="2" />
@@ -87,7 +87,7 @@ const emit = defineEmits(['choose']);
 
 // 定义变量内容
 const state = reactive({
-	tableData: [], // 表格数据
+	tableData: [] as any[], // 表格数据
 	total: 0, // 总条数
 	loading: false, // 加载
 	queryParams: {
@@ -96,7 +96,7 @@ const state = reactive({
 		tagIds: null, // 标签id集合
         difficultyLevel: null, // 难易程度
 	},
-	tableCheckbox: [], // 已选择的试题
+	tableCheckbox: [] as any [], // 已选择的试题
 	tagData: [], // 标签数据
 });
 const ruleDialogFormRef = ref<FormInstance>(); // 表单
@@ -137,9 +137,9 @@ const queryList = () => {
 	getQuestionData(state.queryParams)
 		.then((res: any) => {
 			state.tableData = res.result?.items ?? [];
-			state.total = res.result?.total ?? 0;
+			state.total = res.result?.pagination.totalCount ?? 0;
 			state.loading = false;
-            const rows = [];
+            const rows = [] as any[];
             state.tableCheckbox.forEach(item => {
                 rows.push(state.tableData.find(it => it.id === item.questionId));
             })
@@ -152,7 +152,6 @@ const queryList = () => {
 const resetQuery = (formEl: FormInstance | undefined) => {
 	if (!formEl) return;
 	formEl.resetFields();
-	state.queryParams.name = null;
 	handleQuery();
 };
 // 单个复选框点击

+ 3 - 3
src/views/examTrain/exam/testPaper/index.vue

@@ -46,7 +46,7 @@
                     show-overflow
                     :print-config="{}"
                     :scrollY="{ enabled: true, gt: 100 }"
-                    id="testPaper"
+                    id="examTrainExamTestPaper"
                     :custom-config="{ storage: true }"
                     showHeaderOverflow
                 >
@@ -104,7 +104,7 @@
 	</div>
 </template>
 
-<script lang="tsx" setup name="testPaper">
+<script lang="tsx" setup name="examTrainExamTestPaper">
 import { ref, reactive, onMounted, defineAsyncComponent, computed } from 'vue';
 import { ElMessageBox, ElMessage } from 'element-plus';
 import { useRouter } from 'vue-router';
@@ -148,7 +148,7 @@ const queryList = () => {
 	getTestPaperData(requestParams.value)
 		.then((response: any) => {
 			state.tableData = response?.result.items ?? [];
-			state.total = response?.result.pagination.totalCount;
+			state.total = response?.result.pagination.totalCount ?? 0;
 			state.tableLoading = false;
 		})
 		.catch(() => {

+ 2 - 1
src/views/examTrain/exam/userExam/components/UserExam-exam.vue

@@ -268,7 +268,8 @@ const onSubmit = async () => {
     state.loading = true;
     await onSelQuestion('');
 	submitUserExam({
-        isSubmit: 1
+        isSubmit: 1,
+        id: state.userExamId
     }).then(() => {
         emit('updateList');
         state.loading = false;

+ 3 - 3
src/views/examTrain/exam/userExam/index.vue

@@ -40,7 +40,7 @@
                     show-overflow
                     :print-config="{}"
                     :scrollY="{ enabled: true, gt: 100 }"
-                    id="userExam"
+                    id="examTrainExamUserExam"
                     :custom-config="{ storage: true }"
                     showHeaderOverflow
                 >
@@ -129,7 +129,7 @@
 	</div>
 </template>
 
-<script lang="tsx" setup name="userExam">
+<script lang="tsx" setup name="examTrainExamUserExam">
 import { ref, reactive, onMounted, defineAsyncComponent, computed } from 'vue';
 import { ElMessageBox, ElMessage } from 'element-plus';
 import { useRouter } from 'vue-router';
@@ -180,7 +180,7 @@ const queryList = () => {
 	getUserExamData(requestParams.value)
 		.then((response: any) => {
 			state.tableData = response?.result.items ?? [];
-			state.total = response?.result.pagination.totalCount;
+			state.total = response?.result.pagination.totalCount ?? 0;
 			state.tableLoading = false;
 		})
 		.catch(() => {

+ 5 - 5
src/views/examTrain/questionBank/components/Question-course.vue

@@ -56,7 +56,7 @@ const emit = defineEmits(['choose']);
 
 // 定义变量内容
 const state = reactive({
-	tableData: [], // 表格数据
+	tableData: [] as any [], // 表格数据
 	total: 0, // 总条数
 	loading: false, // 加载
 	queryParams: {
@@ -64,7 +64,7 @@ const state = reactive({
 		PageSize: 10, // 每页条数
 		name: null, // 关键字
 	},
-	tableCheckbox: '', // 选择的ID
+	tableCheckbox: [] as any[], // 选择的ID
 });
 const ruleDialogFormRef = ref<FormInstance>(); // 表单
 const dialogVisible = ref(false); // 弹窗
@@ -77,7 +77,7 @@ const openDialog = async (row: any[]) => {
         if (row) {
 			state.tableCheckbox = row;
 		} else {
-			state.tableCheckbox = '';
+			state.tableCheckbox = [];
 		}
 		queryList();
 	} catch (error) {
@@ -96,9 +96,9 @@ const queryList = () => {
 	getCoursewareData(state.queryParams)
 		.then((res: any) => {
 			state.tableData = res.result?.items ?? [];
-			state.total = res.result?.total ?? 0;
+			state.total = res.result?.pagination.totalCount ?? 0;
 			state.loading = false;
-            const rows = [];
+            const rows = [] as any[];
             state.tableCheckbox.forEach(item => {
                 rows.push(state.tableData.find((it) => it.id === item.sourcewareId));
             })

+ 4 - 4
src/views/examTrain/questionBank/components/Question-knowledge.vue

@@ -61,7 +61,7 @@ const emit = defineEmits(['choose']);
 
 // 定义变量内容
 const state = reactive({
-	tableData: [], // 表格数据
+	tableData: [] as any [], // 表格数据
 	total: 0, // 总条数
 	loading: false, // 加载
 	queryParams: {
@@ -70,7 +70,7 @@ const state = reactive({
 		Keyword: null, // 关键字
 		RetrievalType: '1', // 标题
 	},
-	tableCheckbox: [], // 选择的知识
+	tableCheckbox: [] as any[], // 选择的知识
 });
 const ruleDialogFormRef = ref<FormInstance>(); // 表单
 const dialogVisible = ref(false); // 弹窗
@@ -101,9 +101,9 @@ const queryList = () => {
 		.then((res: any) => {
 			state.tableData = res.result?.items ?? [];
             console.log(state.tableData)
-			state.total = res.result?.total ?? 0;
+			state.total = res.result?.pagination.totalCount ?? 0;
 			state.loading = false;
-            const rows = [];
+            const rows = [] as any [];
             state.tableCheckbox.forEach(item => {
                 rows.push(state.tableData.find((it) => it.id === item.knowladgeId));
             })

+ 3 - 0
src/views/examTrain/questionBank/edit.vue

@@ -510,6 +510,9 @@ const getDetail = async () => {
 				}
 			});
 		}
+		if (!state.ruleForm.questionAnswerDto){
+			state.ruleForm.questionAnswerDto = {answer: ''};
+		}
 	}else {
 		for(let num = ref(0); num.value < 3; num.value++){
 			state.ruleForm.questionOptionsDtos.push({temp: guid(), content: '', isAnswer: false});

+ 3 - 3
src/views/examTrain/questionBank/index.vue

@@ -74,7 +74,7 @@
                     show-overflow
                     :print-config="{}"
                     :scrollY="{ enabled: true, gt: 100 }"
-                    id="questionBank"
+                    id="examTrainQuestionBank"
                     :custom-config="{ storage: true }"
                     showHeaderOverflow
                 >
@@ -112,7 +112,7 @@
 	</div>
 </template>
 
-<script lang="tsx" setup name="questionBank">
+<script lang="tsx" setup name="examTrainQuestionBank">
 import { ref, reactive, onMounted, defineAsyncComponent, computed } from 'vue';
 import { ElMessageBox, ElMessage } from 'element-plus';
 import { useRouter } from 'vue-router';
@@ -169,7 +169,7 @@ const queryList = () => {
 	getQuestionData(requestParams.value)
 		.then((response: any) => {
 			state.tableData = response?.result.items ?? [];
-			state.total = response?.result.pagination.totalCount;
+			state.total = response?.result.pagination.totalCount ?? 0;
 			state.tableLoading = false;
 		})
 		.catch(() => {

+ 2 - 2
src/views/examTrain/tag/index.vue

@@ -35,7 +35,7 @@
 					auto-resize
 					show-overflow
 					:scrollY="{ enabled: true, gt: 0 }"
-					id="examTag"
+					id="examTrainTag"
 					:custom-config="{ storage: true }"
 					showHeaderOverflow
 					:tree-config="{
@@ -65,7 +65,7 @@
 		<tag-edit ref="tagEditRef" @updateList="queryList" />
 	</div>
 </template>
-<script lang="tsx" setup name="examTag">
+<script lang="tsx" setup name="examTrainTag">
 import { defineAsyncComponent, nextTick, onMounted, reactive, ref } from 'vue';
 import type { FormInstance } from 'element-plus';
 import { ElMessage, ElMessageBox } from 'element-plus';

+ 3 - 3
src/views/examTrain/train/plan/index.vue

@@ -43,7 +43,7 @@
                     show-overflow
                     :print-config="{}"
                     :scrollY="{ enabled: true, gt: 100 }"
-                    id="plan"
+                    id="examTrainTrainPlan"
                     :custom-config="{ storage: true }"
                     showHeaderOverflow
                 >
@@ -130,7 +130,7 @@
 	</div>
 </template>
 
-<script lang="tsx" setup name="plan">
+<script lang="tsx" setup name="examTrainTrainPlan">
 import { ref, reactive, onMounted, defineAsyncComponent, computed } from 'vue';
 import { ElMessageBox, ElMessage } from 'element-plus';
 import { useRouter } from 'vue-router';
@@ -182,7 +182,7 @@ const queryList = () => {
 		.then((response: any) => {
             console.log(response)
 			state.tableData = response?.result.items ?? [];
-			state.total = response?.result.pagination.totalCount;
+			state.total = response?.result.pagination.totalCount ?? 0;
 			state.tableLoading = false;
 		})
 		.catch(() => {

+ 8 - 4
src/views/examTrain/train/template/components/Template-add.vue

@@ -108,7 +108,7 @@
         <!-- 关联知识库 -->
 		<question-knowledge ref="QuestionKnowledgeRef" @choose="chooseKnowledge" />
         <!-- 自选试题页面 -->
-		<testPaper-optional ref="TestPaperOptionalRef" @choose="chooseQuestion" />
+		<template-optional ref="TemplateOptionalRef" @choose="chooseQuestion" />
 	</el-dialog>
 </template>
 
@@ -120,7 +120,7 @@ import {addTrainTemplate} from '@/api/examTrain/trainTemplate';
 
 // 引入组件
 const QuestionKnowledge = defineAsyncComponent(() => import('@/views/examTrain/questionBank/components/Question-knowledge.vue')); // 关联知识
-const TestPaperOptional = defineAsyncComponent(() => import('@/views/examTrain/exam/testPaper/components/TestPaper-optional.vue')); // 自选试题页面
+const TemplateOptional = defineAsyncComponent(() => import('@/views/examTrain/train/template/components/Template-optional.vue')); // 自选试题页面
 
 // 定义子组件向父组件传值/事件
 const emit = defineEmits(['updateList']);
@@ -170,9 +170,13 @@ const onRowDelKnowladgesTable = (row: any) => {
     ElMessage.success('删除成功');
 };
 // 选择试题新增
-const TestPaperOptionalRef = ref<RefType>();
+const TemplateOptionalRef = ref<RefType>();
 const onAddQuestionsTable = () => {
-    TestPaperOptionalRef.value.openDialog(state.ruleForm.trainPracticeDtos);
+	if (state.ruleForm.trainKnowladges.length > 0){
+		TemplateOptionalRef.value.openDialog(state.ruleForm.trainKnowladges, state.ruleForm.trainPracticeDtos);
+	}else {
+		ElMessage.warning('请选择培训资料');
+	}
 };
 // 确定选择试题
 const chooseQuestion = (data: any) => {

+ 8 - 4
src/views/examTrain/train/template/components/Template-edit.vue

@@ -108,7 +108,7 @@
         <!-- 关联知识库 -->
 		<question-knowledge ref="QuestionKnowledgeRef" @choose="chooseKnowledge" />
         <!-- 自选试题页面 -->
-		<testPaper-optional ref="TestPaperOptionalRef" @choose="chooseQuestion" />
+		<template-optional ref="TemplateOptionalRef" @choose="chooseQuestion" />
 	</el-dialog>
 </template>
 
@@ -120,7 +120,7 @@ import {getTrainTemplateDetail, editTrainTemplate} from '@/api/examTrain/trainTe
 
 // 引入组件
 const QuestionKnowledge = defineAsyncComponent(() => import('@/views/examTrain/questionBank/components/Question-knowledge.vue')); // 关联知识
-const TestPaperOptional = defineAsyncComponent(() => import('@/views/examTrain/exam/testPaper/components/TestPaper-optional.vue')); // 自选试题页面
+const TemplateOptional = defineAsyncComponent(() => import('@/views/examTrain/train/template/components/Template-optional.vue')); // 自选试题页面
 
 // 定义子组件向父组件传值/事件
 const emit = defineEmits(['updateList']);
@@ -177,9 +177,13 @@ const onRowDelKnowladgesTable = (row: any) => {
     ElMessage.success('删除成功');
 };
 // 选择试题新增
-const TestPaperOptionalRef = ref<RefType>();
+const TemplateOptionalRef = ref<RefType>();
 const onAddQuestionsTable = () => {
-    TestPaperOptionalRef.value.openDialog(state.ruleForm.trainPracticeDtos);
+	if (state.ruleForm.trainKnowladges.length > 0){
+		TemplateOptionalRef.value.openDialog(state.ruleForm.trainKnowladges, state.ruleForm.trainPracticeDtos);
+	}else {
+		ElMessage.warning('请选择培训资料');
+	}
 };
 // 确定选择试题
 const chooseQuestion = (data: any) => {

+ 196 - 0
src/views/examTrain/train/template/components/Template-optional.vue

@@ -0,0 +1,196 @@
+<template>
+	<el-dialog title="选择试题" v-model="dialogVisible" draggable append-to-body destroy-on-close width="800px">
+		<el-form :model="state.queryParams" ref="ruleDialogFormRef" inline @submit.native.prevent :disabled="state.loading" class="ruleDialogForm">
+			<el-form-item label="标签" prop="tagIds">
+                <el-tree-select
+                    v-model="state.queryParams.tagIds"
+                    :data="state.tagData"
+                    node-key="id"
+                    :props="{ label: 'name' }"
+                    multiple
+                    collapse-tags
+                    collapse-tags-tooltip
+                    :render-after-expand="false"
+                    style="width: 200px;"
+                    @change="handleQuery"
+                />
+            </el-form-item>
+            <el-form-item label="难易程度" prop="difficultyLevel">
+                <el-select v-model="state.queryParams.difficultyLevel" clearable placeholder="请选择" @change="handleQuery" :disabled="state.loading" style="width: 150px">
+                    <el-option key='0' label="容易" :value="0" />
+                    <el-option key='1' label="适中" :value="1" />
+                    <el-option key='2' label="困难" :value="2" />
+                </el-select>
+            </el-form-item>
+            <!-- <el-form-item label="难易程度" prop="questionType">
+                <el-select v-model="state.queryParams.questionType" clearable placeholder="请选择" @change="handleQuery" :disabled="state.tableLoading">
+                    <el-option key='0' label="单选题" :value="0" />
+                    <el-option key='1' label="多选题" :value="1" />
+                    <el-option key='2' label="判断题" :value="2" />
+                    <el-option key='3' label="填空题" :value="3" />
+                    <el-option key='4' label="问答题" :value="4" />
+                </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(ruleDialogFormRef)" class="default-button"> <SvgIcon name="ele-Refresh" class="mr5" />重置 </el-button>
+			</el-form-item>
+		</el-form>
+		<vxe-table
+			border
+			:loading="state.loading"
+			:data="state.tableData"
+			:sort-config="{ remote: true }"
+			:column-config="{ resizable: true }"
+			:row-config="{ isHover: true, height: 30, keyField: 'id' }"
+			ref="tableRef"
+			height="300px"
+			show-overflow
+			:scrollY="{ enabled: true, gt: 100 }"
+			@checkbox-change="checkboxChangeEvent"
+			:checkbox-config="{ highlight: true, showHeader: false }"
+		>
+			>
+			<vxe-column type="checkbox" fixed="left" width="70" align="center"></vxe-column>
+			<vxe-column field="questionTypeDesc" title="题型" width="100"></vxe-column>
+            <vxe-column field="difficultyLevelDesc" title="难度" width="100"></vxe-column>
+            <vxe-column field="title" title="标题" min-width="150"></vxe-column>
+		</vxe-table>
+		<pagination
+			@pagination="queryList"
+			:total="state.total"
+			v-model:current-page="state.queryParams.PageIndex"
+			v-model:page-size="state.queryParams.PageSize"
+			:disabled="state.loading"
+		/>
+		<template #footer>
+			<span class="dialog-footer">
+				<el-button @click="dialogVisible = false" class="default-button">取 消</el-button>
+				<el-button type="primary" @click="onSubmit" :disabled="!state.tableCheckbox">保 存</el-button>
+			</span>
+		</template>
+	</el-dialog>
+</template>
+
+<script lang="ts" setup>
+import { defineAsyncComponent, reactive, ref } from 'vue';
+import { FormInstance } from 'element-plus';
+import { formatDate } from '@/utils/formatTime';
+import other from '@/utils/other';
+import {getQuestionData} from '@/api/examTrain/questionBank';
+import { examTagTreeList} from '@/api/examTrain/tag';
+
+const pagination = defineAsyncComponent(() => import('@/components/ProTable/components/Pagination.vue')); // 分页
+
+// 定义子组件向父组件传值/事件
+const emit = defineEmits(['choose']);
+
+// 定义变量内容
+const state = reactive({
+	tableData: [] as any[], // 表格数据
+	total: 0, // 总条数
+	loading: false, // 加载
+	queryParams: {
+		PageIndex: 1, // 当前页
+		PageSize: 20, // 每页条数
+		tagIds: null, // 标签id集合
+        difficultyLevel: null, // 难易程度
+        knowladgeIds: [] as any [] // 关联的知识库id
+	},
+	tableCheckbox: [] as any [], // 已选择的试题
+	tagData: [], // 标签数据
+});
+const ruleDialogFormRef = ref<FormInstance>(); // 表单
+const dialogVisible = ref(false); // 弹窗
+
+// 获取标签数据
+const getTagData = async () => {
+	try {
+		const { result } = await examTagTreeList();
+		state.tagData = result ?? [];
+	} catch (error) {
+	}
+};
+// 打开弹窗
+const openDialog = async (knowladgesData: any[], selData: any[]) => {
+	dialogVisible.value = true;
+	try {
+        if (knowladgesData) {
+			state.queryParams.knowladgeIds = knowladgesData.map(it => it.knowladgeId);
+		} else {
+			state.queryParams.knowladgeIds = [];
+		}
+        if (selData) {
+			state.tableCheckbox = other.deepClone(selData);
+		} else {
+			state.tableCheckbox = [];
+		}
+	    getTagData();
+		queryList();
+	} catch (error) {
+		console.log(error);
+		state.loading = false;
+	}
+};
+/** 搜索按钮操作 */
+const handleQuery = () => {
+	state.queryParams.PageIndex = 1;
+	queryList();
+};
+const tableRef = ref<RefType>();
+const queryList = () => {
+	state.loading = true;
+	getQuestionData(state.queryParams)
+		.then((res: any) => {
+			state.tableData = res.result?.items ?? [];
+			state.total = res.result?.pagination.totalCount ?? 0;
+			state.loading = false;
+            const rows = [] as any[];
+            state.tableCheckbox.forEach(item => {
+                rows.push(state.tableData.find(it => it.id === item.questionId));
+            })
+            tableRef.value.setCheckboxRow(rows, true);
+		})
+		.catch(() => {
+			state.loading = false;
+		});
+};
+const resetQuery = (formEl: FormInstance | undefined) => {
+	if (!formEl) return;
+	formEl.resetFields();
+	handleQuery();
+};
+// 单个复选框点击
+const checkboxChangeEvent = ({row}) => {
+    let blog = 'add';
+    state.tableCheckbox.forEach(item => {
+        if(item.questionId == row.id){
+            blog = 'delete'
+        }
+    });
+    if (blog == 'add'){
+        const rowObj = other.deepClone(row);
+        rowObj.questionId = rowObj.id;
+        Reflect.deleteProperty(rowObj, 'id');
+        state.tableCheckbox.push(rowObj)
+    }else {
+        state.tableCheckbox = state.tableCheckbox.filter(it => it.questionId !== row.id )
+    }
+}
+// 关闭弹窗
+const closeDialog = () => {
+	dialogVisible.value = false;
+};
+// 选择试题
+const onSubmit = () => {
+	emit('choose', state.tableCheckbox);
+	closeDialog();
+};
+//暴漏变量和方法
+defineExpose({ closeDialog, openDialog });
+</script>
+<style>
+.el-form--inline.ruleDialogForm .el-form-item .el-select {
+    min-width: 100px;
+}
+</style>

+ 3 - 3
src/views/examTrain/train/template/index.vue

@@ -46,7 +46,7 @@
                     show-overflow
                     :print-config="{}"
                     :scrollY="{ enabled: true, gt: 100 }"
-                    id="template"
+                    id="examTrainTrainTemplate"
                     :custom-config="{ storage: true }"
                     showHeaderOverflow
                 >
@@ -115,7 +115,7 @@
 	</div>
 </template>
 
-<script lang="tsx" setup name="template">
+<script lang="tsx" setup name="examTrainTrainTemplate">
 import { ref, reactive, onMounted, defineAsyncComponent, computed } from 'vue';
 import { ElMessageBox, ElMessage } from 'element-plus';
 import { useRouter } from 'vue-router';
@@ -164,7 +164,7 @@ const queryList = () => {
 		.then((response: any) => {
             console.log(response)
 			state.tableData = response?.result.items ?? [];
-			state.total = response?.result.pagination.totalCount;
+			state.total = response?.result.pagination.totalCount ?? 0;
 			state.tableLoading = false;
 		})
 		.catch(() => {

+ 223 - 0
src/views/examTrain/train/userTrain/components/UserTrain-view.vue

@@ -0,0 +1,223 @@
+<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">
+                <el-skeleton :loading="state.loading" animated :rows="10">
+                    <p class="knowladgeDtosItem" v-for="(item) in state.trainPracticeQuestionsDtos" @click="onSelQuestion(item.id)">
+                        <el-icon color="#409efc" class="el-input__icon">
+                            <ele-Tickets />
+                        </el-icon>
+                        {{ item.title }}
+                    </p>
+                </el-skeleton>
+            </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.trainPracticeOptionsDtos" 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.trainPracticeOptionsDtos" style="margin-bottom: 10px;">
+                                    <el-checkbox :label="item.content" :value="item.questionOptionId" disabled />
+                                </el-form-item>
+                            </el-checkbox-group>
+                        </div>
+                        <div v-else-if="state.questionDetail.questionType == 3">
+                            <el-form-item label="答:">
+                                <el-input type="textarea" v-model="state.answerValue" :rows="3"disabled></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" disabled></el-input>
+                            </el-form-item>
+                        </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.trainPracticeKnowladgeDtos.length > 0">
+                    <span>关联知识:</span>
+                    <p v-for="item in state.questionDetail.trainPracticeKnowladgeDtos" @click="onKnowladgeTo(item)">{{item.title}}</p>
+                </div>
+                <div class="referenceAnswer" v-if="state.questionDetail && state.questionDetail.trainPracticeSourcewareDtos.length > 0">
+                    <span>关联课件:</span>
+                    <p v-for="item in state.questionDetail.trainPracticeSourcewareDtos" @click="onSourcewareTo(item)">{{item.name}}</p>
+                </div>
+            </el-col>
+        </el-row>
+	</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 { getTrainInfo, answerUserTrain, getTrainQuestion, submitUserTrain } from '@/api/examTrain/userTrain';
+
+const router = useRouter(); // 路由
+// 定义子组件向父组件传值/事件
+const emit = defineEmits(['updateList']);
+
+// 定义变量内容
+const state = reactive<any>({
+	dialogVisible: false, // 弹窗
+	loading: false, // 加载
+    userTrainId: '', // 用户培训id
+	trainPracticeQuestionsDtos: [], // 习题数据
+    questionsData: [] as any[], // 试题集合
+    selQuestionID: '', // 选择的问题id
+    questionDetail: null, // 选择的问题详情
+    selRadioValue: '', // 单选题选择的值
+    selCheckboxValue: [] as any[], // 多选题选择的值
+    answerValue: '', // 填空、问答填写的值
+});
+// 打开弹窗
+const openDialog = async (rows: any) => {
+    state.dialogVisible = true;
+    state.userTrainId = rows.id;
+	state.loading = true;
+    getQuestionsData();
+};
+const getQuestionsData = async () => {
+    try {
+		const res: any = await getTrainInfo(state.userTrainId);
+        state.trainPracticeQuestionsDtos = res.result.questionDtos;
+        state.loading = false;
+	} catch (error) {
+		// 打印错误信息
+		console.error(error);
+	}
+}
+// 关闭弹窗
+const closeDialog = () => {
+	state.dialogVisible = false;
+    state.trainPracticeQuestionsDtos = []; 
+    state.selQuestionID = ''; 
+    state.questionDetail = null; 
+    state.selRadioValue = ''; 
+    state.selCheckboxValue = []; 
+    state.answerValue = ''; 
+};
+// 选择的问题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 getTrainQuestion(state.userTrainId, state.selQuestionID);
+		state.questionDetail = result;
+        let answerDesc = '';
+        state.questionDetail.trainPracticeOptionsDtos.forEach(it => {it.isAnswer && (answerDesc += it.label)});
+        state.questionDetail.answerDesc = answerDesc;
+        if ([0,2].includes(state.questionDetail.questionType)){
+            let obj = state.questionDetail.trainPracticeOptionsDtos.find((x: any) => x.isSelected === true);
+            state.selRadioValue = obj ? obj.questionOptionId : '';
+        }else if (state.questionDetail.questionType == 1){
+            let arr = [] as any[];
+            state.questionDetail.trainPracticeOptionsDtos.forEach(it => {if (it.isSelected) arr.push(it);})
+            state.selCheckboxValue = arr ? arr.map((x: any) => x.questionOptionId) : [];
+            console.log(state.selCheckboxValue)
+        }else {
+            state.answerValue = state.questionDetail.answer || '';
+        }
+        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;}
+    .knowladgeDtosItem{color: var(--el-color-primary);margin-bottom: 10px;cursor: pointer;}
+</style>

+ 170 - 0
src/views/examTrain/train/userTrain/index.vue

@@ -0,0 +1,170 @@
+<template>
+	<div class="plan-index-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="code">
+                    <el-input v-model="state.queryParams.code" placeholder="培训编号" clearable @keyup.enter="handleQuery" class="keyword-input" />
+                </el-form-item>
+                <el-form-item label="培训名称" prop="name">
+                    <el-input v-model="state.queryParams.name" placeholder="培训名称" clearable @keyup.enter="handleQuery" class="keyword-input" />
+                </el-form-item>
+                <el-form-item>
+                    <el-button type="primary" @click="handleQuery" :loading="state.tableLoading"> <SvgIcon name="ele-Search" class="mr5" />查询 </el-button>
+				    <el-button @click="resetQuery(ruleFormRef)" class="default-button"> <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="examTrainTrainUserTrain"
+                    :custom-config="{ storage: true }"
+                    showHeaderOverflow
+                >
+                    <vxe-column field="trainCode" title="培训编号" width="150"></vxe-column>
+                    <vxe-column field="trainName" title="培训标题" min-width="150"></vxe-column>
+                    <vxe-column field="trainStartTime" title="培训开始时间" width="160">
+						<template #default="{ row }">
+							{{ formatDate(row.trainStartTime, 'YYYY-mm-dd HH:MM:SS') }}
+						</template>
+					</vxe-column>
+                    <vxe-column field="trainEndTime" title="培训结束时间" width="160">
+						<template #default="{ row }">
+							{{ formatDate(row.trainEndTime, 'YYYY-mm-dd HH:MM:SS') }}
+						</template>
+					</vxe-column>
+                    <vxe-column field="isCompleteDes" title="是否完成培训" align="center" width="120"></vxe-column>
+                    <!-- <vxe-column field="" title="创建人" width="100"></vxe-column>
+                    <vxe-column field="endTime" title="创建时间" width="160">
+						<template #default="{ row }">
+                            <span v-if="row.examType == 0">{{formatDate(row.endTime, 'YYYY-mm-dd HH:MM:SS')}}</span>
+                            <span v-else></span>
+						</template>
+					</vxe-column> -->
+                    <vxe-column title="操作" fixed="right" width="160" align="center" :show-overflow="false">
+                        <template #default="{ row }">
+                            <el-button type="primary" v-if="!row.isComplete" @click="onStudy(row)" size="small" title="学习" v-auth="'userTrain:index:study'">
+                                学习
+                            </el-button>
+                            <el-button link type="primary" v-else @click="onView(row)" title="查看小测" v-auth="'userTrain:index:view'">
+                                查看小测
+                            </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>
+
+        <!-- 查看小测 -->
+		<userTrain-view ref="userTrainViewRef" @updateList="queryList" />
+	</div>
+</template>
+
+<script lang="tsx" setup name="examTrainTrainUserTrain">
+import { ref, reactive, onMounted, defineAsyncComponent, computed } from 'vue';
+import { ElMessageBox, ElMessage } from 'element-plus';
+import { useRouter } from 'vue-router';
+import type { FormInstance } from 'element-plus';
+import { formatDate } from '@/utils/formatTime';
+import 'splitpanes/dist/splitpanes.css';
+import Other from '@/utils/other';
+import { downloadFileByStream } from '@/utils/tools';
+import {getUserTrainData} from '@/api/examTrain/userTrain';
+
+// 引入组件
+const pagination = defineAsyncComponent(() => import('@/components/ProTable/components/Pagination.vue')); // 分页
+const UserTrainView = defineAsyncComponent(() => import('@/views/examTrain/train/userTrain/components/UserTrain-view.vue')); // 查看组件
+
+const router = useRouter(); //路由
+
+// 定义变量内容
+const state = reactive<any>({
+	queryParams: {
+		PageIndex: 1, //页码
+		PageSize: 20, //每页条数
+        code: '', // 编号
+        name: '', // 标题
+	},
+	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);
+	getUserTrainData(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();
+	ruleFormRef.value?.resetFields();
+	handleQuery();
+};
+// 学习
+const onStudy  = (row: any) => {
+    console.log(row)
+	router.push({
+		name: 'trainStudy',
+		params: {
+			id: row.id,
+            trainName: row.trainName,
+			tagsViewName: '培训学习',
+		},
+	});
+};
+// 打开查看弹窗
+const userTrainViewRef = ref<RefType>(); // 修改ref
+const onView = (row: any) => {
+	userTrainViewRef.value.openDialog(row, state.tableData.value);
+};
+// 表格选中状态
+const tableRef = ref<RefType>();
+const toolbarRef = ref<RefType>();
+onMounted(() => {
+	queryList();
+	if (tableRef.value && toolbarRef.value) {
+		tableRef.value.connect(toolbarRef.value);
+	}
+});
+</script>

+ 412 - 0
src/views/examTrain/train/userTrain/study.vue

@@ -0,0 +1,412 @@
+<template>
+	<div class="layout-pd h100">
+		<div class="layout-padding-auto layout-padding-view pd20">
+            <splitpanes class="h100" :horizontal="horizontal">
+				<pane size="30">
+                    <el-tabs v-model="state.activeName" stretch @tab-change="tabChange">
+                        <el-tab-pane label="培训知识" name="0" :disabled="state.loading"></el-tab-pane>
+                        <el-tab-pane label="习题" name="1" :disabled="state.loading" v-if="state.trainPracticeQuestionsDtos.length > 0" ></el-tab-pane>
+                    </el-tabs>
+                    <el-scrollbar style="height: calc(100% - 100px);" ref="scrollBarRef">
+                        <el-skeleton :loading="state.loading" animated :rows="10" v-if="state.activeName === '0'">
+                            <p class="knowladgeDtosItem" v-for="(item) in state.trainPracticeKnowladgeDtos" @click="onClickKnowladge(item.id)">
+                                <el-icon color="#409efc" class="el-input__icon">
+                                    <ele-Notebook />
+                                </el-icon>
+                                {{ item.title }}
+                            </p>
+                        </el-skeleton>
+                        <el-skeleton :loading="state.loading" animated :rows="10" v-if="state.activeName === '1'">
+                            <p class="knowladgeDtosItem" v-for="(item) in state.trainPracticeQuestionsDtos" @click="onSelQuestion(item.id)">
+                                <el-icon color="#409efc" class="el-input__icon">
+                                    <ele-Tickets />
+                                </el-icon>
+                                {{ item.title }}
+                            </p>
+                        </el-skeleton>
+                    </el-scrollbar>
+                </pane>
+				<pane class="h100" style="display: flex;flex-direction: column;flex: 1;">
+                    <el-card shadow="never" v-if="state.activeName === '0' && state.KnowladgeInfo">
+                        <template #header>
+                            <div class="card-header">
+                                <span class="headerText">{{state.trainTitle}}</span>
+                                <el-button class="headerBtn" type="primary" @click="onSubmit" :loading="state.loading">结束培训</el-button>
+                            </div>
+                        </template>
+                        <el-skeleton :loading="state.knowladgeLoading" animated>
+                            <template #template>
+                                <el-skeleton-item />
+                            </template>
+                            <template #default>
+                                <h1 class="font18 mb10" style="text-align: center">{{ state.KnowladgeInfo.title }}</h1>
+                            </template>
+                        </el-skeleton>
+                        <el-row class="color-info flex-center-between" :gutter="20">
+                            <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+                                <el-skeleton :loading="state.knowladgeLoading" animated>
+                                    <template #template>
+                                        <el-skeleton-item />
+                                    </template>
+                                    <template #default>
+                                        <span class="mr30">创建时间:{{ formatDate(state.KnowladgeInfo.creationTime, 'YYYY-mm-dd HH:MM:SS') }}</span>
+                                        <span class="mr30">创建人:{{ state.KnowladgeInfo.creatorName }}</span>
+                                        <span>创建部门:{{ state.KnowladgeInfo.creatorOrgName }}</span>
+                                    </template>
+                                </el-skeleton>
+                            </el-col>
+                            <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" style="display: flex; align-items: center; justify-content: center">
+                                <el-skeleton :loading="state.knowladgeLoading" animated>
+                                    <template #template>
+                                        <el-skeleton-item />
+                                    </template>
+                                    <template #default>
+                                        <span class="mr20 flex-center-align" v-if="state.KnowladgeInfo.knowledgeTypeText"
+                                            >知识分类:
+                                            <el-tooltip effect="dark" content="Top Left prompts info" :disabled="state.KnowladgeInfo.knowledgeTypeText.length < 8">
+                                                <span style="display: block; width: 100px" class="text-no-wrap">{{ state.KnowladgeInfo.knowledgeTypeText }}</span>
+                                                <template #content>
+                                                    {{ state.KnowladgeInfo.knowledgeTypeText }}
+                                                </template>
+                                            </el-tooltip>
+                                        </span>
+                                        <span class="mr30">是否公开:{{ state.KnowladgeInfo.isPublic ? '是' : '否' }}</span>
+                                        <span>已阅:{{ state.KnowladgeInfo.pageView }}</span>
+                                    </template>
+                                </el-skeleton>
+                            </el-col>
+                        </el-row>
+                        <el-row class="color-info flex-center-between">
+                            <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+                                <el-skeleton :loading="state.knowladgeLoading" animated>
+                                    <template #template>
+                                        <el-skeleton-item />
+                                    </template>
+                                    <template #default>
+                                        <span class="mr30" v-if="state.KnowladgeInfo.documentNo">文档号:{{ state.KnowladgeInfo.documentNo }}</span>
+                                        <span class="mr30" v-if="state.KnowladgeInfo.versionDescription">版本说明:{{ state.KnowladgeInfo.versionDescription }}</span>
+                                        <span class="mr30" v-if="state.KnowladgeInfo.indexNo">索引号:{{ state.KnowladgeInfo.indexNo }}</span>
+                                        <span v-if="state.KnowladgeInfo.fileNo">文号:{{ state.KnowladgeInfo.fileNo }}</span>
+                                    </template>
+                                </el-skeleton>
+                            </el-col>
+                        </el-row>
+                        <el-divider />
+                        <el-skeleton :loading="state.knowladgeLoading" animated>
+                            <template #template>
+                                <el-skeleton :rows="5" />
+                            </template>
+                            <template #default>
+                                <div class="mt5 editor-content-view">
+                                    <div v-html="state.KnowladgeInfo.content" class="lineHeight24"></div>
+                                </div>
+                            </template>
+                        </el-skeleton>
+                        <template v-if="state.KnowladgeInfo.knowledgeDtos && state.KnowladgeInfo.knowledgeDtos.length > 0">
+                            <el-divider />
+                            <el-skeleton :loading="state.knowladgeLoading" animated>
+                                <template #template>
+                                    <el-skeleton-item />
+                                </template>
+                                <template #default>
+                                    关联知识
+                                    <div class="mt5">
+                                        <div v-for="(item, index) in state.KnowladgeInfo.knowledgeDtos" :key="index" @click="onPreview(item)" class="relevance">
+                                            <el-link type="primary">{{ item.title }}</el-link>
+                                        </div>
+                                    </div>
+                                </template>
+                            </el-skeleton>
+                        </template>
+                        <template>
+                            <el-divider />
+                            <el-skeleton :loading="state.knowladgeLoading" animated>
+                                <template #template>
+                                    <el-form-item label="附件">
+                                        <el-skeleton-item />
+                                    </el-form-item>
+                                </template>
+                                <template #default>
+                                    <div class="mt10">
+                                        <el-form-item label="附件">
+                                            <annex-list name="知识附件" v-model="state.KnowladgeInfo.files" readonly classify="知识附件" />
+                                        </el-form-item>
+                                    </div>
+                                </template>
+                            </el-skeleton>
+                        </template>
+                    </el-card>
+                    <el-card shadow="never" v-if="state.activeName === '1'">
+                        <template #header>
+                            <div class="card-header">
+                                <span class="headerText">{{state.trainTitle}}</span>
+                                <el-button class="headerBtn" type="primary" @click="onSubmit" :loading="state.loading">结束培训</el-button>
+                            </div>
+                        </template>
+                        <el-skeleton :loading="state.questionLoading" animated :rows="5">
+                            <template #default>
+                                <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.trainPracticeOptionsDtos" 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.trainPracticeOptionsDtos" 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>
+                                <div class="referenceAnswer" v-if="state.questionDetail && state.questionDetail.trainPracticeKnowladgeDtos.length > 0">
+                                    <span>关联知识:</span>
+                                    <p v-for="item in state.questionDetail.trainPracticeKnowladgeDtos" @click="onKnowladgeTo(item)">{{item.title}}</p>
+                                </div>
+                                <div class="referenceAnswer" v-if="state.questionDetail && state.questionDetail.trainPracticeSourcewareDtos.length > 0">
+                                    <span>关联课件:</span>
+                                    <p v-for="item in state.questionDetail.trainPracticeSourcewareDtos" @click="onSourcewareTo(item)">{{item.name}}</p>
+                                </div>
+                            </template>
+                        </el-skeleton>
+                    </el-card>
+                </pane>
+            </splitpanes>
+        </div>
+		
+    </div>
+</template>
+
+<script setup lang="ts" name="trainStudy">
+import { defineAsyncComponent, nextTick, onMounted, reactive, ref, computed } from 'vue';
+import type { FormInstance } from 'element-plus';
+import { ElMessage, ElMessageBox } 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 { formatDate } from '@/utils/formatTime';
+import { Local } from '@/utils/storage';
+import other from '@/utils/other';
+import { throttle, transformFile, guid } from '@/utils/tools';
+import { fileDownloadByUrl } from '@/api/public/file';
+import { getTrainInfo, answerUserTrain, getTrainQuestion, submitUserTrain } from '@/api/examTrain/userTrain';
+import { KnowledgeInfo} from '@/api/knowledge';
+
+const router = useRouter(); //路由
+const route = useRoute(); //  获取路由参数
+const horizontal = ref(false);
+
+// 定义变量内容
+const state = reactive<any>({
+    loading: false, // 加载状态
+    knowladgeLoading: false, // 问题加载状态
+    questionLoading: false, // 问题加载状态
+    trainTitle: '我的培训', // 培训标题
+	activeName: '0', //tab切换 默认关联知识
+    isContainsPractice: false, // 是否包含习题
+    trainPracticeKnowladgeDtos: [], // 知识库数据
+    KnowladgeInfo: null,
+    // trainPracticeSourcewareDtos: [], // 课件数据
+    trainPracticeQuestionsDtos: [], // 习题数据
+    questionsData: [] as any[], // 试题集合
+    selQuestionID: '', // 选择的问题id
+    questionDetail: null, // 选择的问题详情
+    selRadioValue: '', // 单选题选择的值
+    selCheckboxValue: [] as any[], // 多选题选择的值
+    answerValue: '', // 填空、问答填写的值
+});
+
+// 初始化数据
+const getInfo = async () => {
+	state.loading = true;
+    state.trainTitle = route.params.trainName;
+    console.log(state.trainName);
+    const res: any = await getTrainInfo(route.params.id);
+    state.trainPracticeKnowladgeDtos = res.result.knowladgeDtos;
+    // state.trainPracticeSourcewareDtos = res.result.trainPracticeSourcewareDtos;
+    state.trainPracticeQuestionsDtos = res.result.questionDtos;
+    state.loading = false;
+};
+// tab切换
+const tabChange = () => {
+
+};
+//  知识点击
+const onClickKnowladge = async(id: any) => {
+    state.knowladgeLoading = true;
+    const res: any = await KnowledgeInfo(id, { isAddPv: 'true' });
+    state.KnowladgeInfo = res.result ?? {};
+    state.KnowladgeInfo.files = transformFile(state.KnowladgeInfo.files);
+    state.knowladgeLoading = false;
+};
+// 知识预览
+const onPreview = (row: any) => {
+	router.push({
+		name: 'knowledgePreview',
+		params: {
+			id: row.id,
+			tagsViewName: '知识查看',
+		},
+	});
+};
+ 
+// 问题点击
+// 选择的问题id 提交当前试题答案,再请求下一道题详情 value: 请求下一题详情id
+const onSelQuestion = async (value: any) => {
+	state.questionLoading = true;
+    if (state.questionDetail){
+        // 保存当前题目选项
+        const submitObj = {
+            trainRecordId: route.params.id,
+            trainPracticeId: state.questionDetail.id,
+            addTrainRecordOptionDtos: [] as any[],
+            addTrainRecordAnswerDto: null as any
+        };
+        if ([0,2].includes(state.questionDetail.questionType)){
+            if (state.selRadioValue){
+                submitObj.addTrainRecordOptionDtos.push({questionOptionId: state.selRadioValue})
+            }
+        }else if (state.questionDetail.questionType == 1){
+            if (state.selCheckboxValue.length > 0){
+                state.selCheckboxValue.forEach(it => {
+                    submitObj.addTrainRecordOptionDtos.push({questionOptionId: it})
+                })
+            }
+        }else if ([3,4].includes(state.questionDetail.questionType)){
+            submitObj.addTrainRecordAnswerDto = {answer: state.answerValue};
+        }
+        answerUserTrain(submitObj)
+            .then((res) => {
+                onGetQuestionDetail(value);
+            })
+            .catch(() => {
+                state.questionLoading = false;
+            });
+    }else {
+        onGetQuestionDetail(value);
+    }
+}
+const onGetQuestionDetail = async (value) => {
+    if (!value){
+        state.questionLoading = false;
+        return;
+    }
+    // 初始化选择选择/填写的答案
+    state.selRadioValue = '';  
+    state.selCheckboxValue = []; 
+    // 请求下一道题
+    state.selQuestionID = value;
+    try {
+		const { result } = await getTrainQuestion(route.params.id, state.selQuestionID);
+		state.questionDetail = result;
+        if ([0,2].includes(state.questionDetail.questionType)){
+            let obj = state.questionDetail.trainPracticeOptionsDtos.find((x: any) => x.isSelected === true);
+            state.selRadioValue = obj ? obj.questionOptionId : '';
+        }else if (state.questionDetail.questionType == 1){
+            let arr = [] as any[];
+            state.questionDetail.trainPracticeOptionsDtos.forEach(it => {if (it.isSelected) arr.push(it);})
+            state.selCheckboxValue = arr ? arr.map((x: any) => x.questionOptionId) : [];
+            console.log(state.selCheckboxValue)
+        }else {
+            state.answerValue = state.questionDetail.answer || '';
+        }
+        state.questionLoading = 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(() => {});
+}
+// 结束培训
+const onSubmit = async () => {
+    state.loading = true;
+    await onSelQuestion('');
+	submitUserTrain({
+        id: route.params.id,
+        isComplete: true
+    }).then(() => {
+        mittBus.emit('onCurrentContextmenuClick', Object.assign({}, { contextMenuClickId: 1, ...route }));
+        mittBus.emit('clearCache', 'userTrain');
+        router.push({
+            path: '/examTrain/train/userTrain',
+        });
+        state.loading = false;
+    }).catch(() => {
+        state.loading = false;
+    });
+};
+onMounted(() => {
+	getInfo();
+});
+</script>
+<style lang="scss">
+    .knowladgeDtosItem{color: var(--el-color-primary);margin-bottom: 10px;cursor: pointer;}
+    .questionBox{padding: 20px 10px 0;}
+    .questionBox .questionTitle{margin-bottom: 10px;font-size: 16px;}
+    .questionBox .questionAnswer{padding: 0 20px;}
+    .questionBox .referenceAnswer{background-color: #ebf9ff;margin: 15px 40px 15px 30px;padding: 10px;}
+    .questionBox .referenceAnswer span{vertical-align: top;display: inline-block;}
+    .questionBox .referenceAnswer p{width: calc(100% - 80px);display: inline-block;}
+    .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;}
+    .el-card__header {padding: 7px 20px 8px;}
+    .card-header{text-align: center;position: relative;}
+    .headerText{font-size: 20px;color: #000;font-weight: bolder;}
+    .headerBtn{position: absolute;top: -7px;right: 0px;}
+</style>