Browse Source

考试管理,我的考试

zjq 1 month ago
parent
commit
b7eb0aef9d

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

@@ -0,0 +1,71 @@
+/*
+ * @Author: zjq
+ * @description 考试管理
+ */
+import request from '@/utils/request';
+
+/**
+ * @description 新增考试
+ * @param {object} data
+ */
+export const addExamManage = (data: object) => {
+    return request({
+        url: '/api/v1/ExamManage/Add',
+        method: 'post',
+        data,
+    });
+};
+/**
+ * @description 编辑考试
+ * @param {object} data
+ */
+export const editExamManage = (data: object) => {
+    return request({
+        url: '/api/v1/ExamManage/Update',
+        method: 'put',
+        data,
+    });
+};
+/**
+ * @description 删除考试
+ * @param {object} data
+ */
+export const deleteExamManage = (data: object) => {
+    return request({
+        url: '/api/v1/ExamManage/Delete',
+        method: 'delete',
+        data,
+    });
+};
+/**
+ * @description 获取考试列表
+ * @param {object} params
+ */
+export const getExamManageData = (params?: object) => {
+    return request({
+        url: '/api/v1/ExamManage/GetPagedList',
+        method: 'post',
+        data: params,
+    });
+};
+/**
+ * @description 查询考试详情
+ * @param {any} Id
+ */
+export const getExamManageDetail = (Id: any) => {
+    return request({
+        url: `/api/v1/ExamManage/Get?id=${Id}`,
+        method: 'get',
+    });
+};
+/**
+ * @description 根据选择的试卷id或者抽题规则id返回试题类型及数量
+ * @param {any} testPaperId
+ * @param {any} extractRuleId
+ */
+export const getTestPaperQuestionCount = (testPaperId: any, extractRuleId: any) => {
+    return request({
+        url: `/api/v1/TestPaper/GetTestPaperQuestionCount?testPaperId=${testPaperId}&extractRuleId=${extractRuleId}`,
+        method: 'get',
+    });
+};

+ 10 - 0
src/api/examTrain/extractRule.ts

@@ -68,4 +68,14 @@ export const getTagQuestionCount = (data: object) => {
         method: 'post',
         data,
     });
+};
+/**
+ * @description 获取列表全部数据
+ * @param {number} examType
+ */
+export const getExtractRuleAllList = () => {
+    return request({
+        url: `/api/v1/ExtractRule/GetList`,
+        method: 'get',
+    });
 };

+ 12 - 2
src/api/examTrain/testPaper.ts

@@ -50,9 +50,9 @@ export const getTestPaperData = (params?: object) => {
 };
 /**
  * @description 查询人工组卷详情
- * @param {string} Id
+ * @param {any} Id
  */
-export const getTestPaperDetail = (Id: string) => {
+export const getTestPaperDetail = (Id: any) => {
     return request({
         url: `/api/v1/TestPaper/Get?id=${Id}`,
         method: 'get',
@@ -79,4 +79,14 @@ export const getSecondsetQuestions = (params: object) => {
         method: 'post',
         data: params,
     });
+};
+/**
+ * @description 获取列表全部数据
+ * @param {number} examType
+ */
+export const getTestPaperAllList = () => {
+    return request({
+        url: `/api/v1/TestPaper/GetList`,
+        method: 'get',
+    });
 };

+ 37 - 0
src/api/examTrain/userExam.ts

@@ -0,0 +1,37 @@
+/*
+ * @Author: zjq
+ * @description 我的考试
+ */
+import request from '@/utils/request';
+
+/**
+ * @description 获取考试列表
+ * @param {object} params
+ */
+export const getUserExamData = (params?: object) => {
+    return request({
+        url: '/api/v1/UserExam/GetPagedList',
+        method: 'post',
+        data: params,
+    });
+};
+/**
+ * @description 获取试题类型分组数据
+ * @param {any} Id
+ */
+export const getExamQuestionGroup = (Id: any) => {
+    return request({
+        url: `/api/v1/UserExam/GetExamQuestionGroup?ExamId=${Id}`,
+        method: 'get',
+    });
+};
+/**
+ * @description 获取试题详情
+ * @param {any} Id
+ */
+export const getExamQuestion = (Id: any) => {
+    return request({
+        url: `/api/v1/UserExam/GetExamQuestion?QuestionId=${Id}`,
+        method: 'get',
+    });
+};

+ 10 - 0
src/router/route.ts

@@ -630,6 +630,16 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
 			isDynamic: true,
 		},
 	},
+	{
+		path: '/examTrain/exam/examManage/edit/:tagsViewName/:id?',
+		name: 'examManageEdit',
+		component: () => import('@/views/examTrain/exam/examManage/edit.vue'),
+		meta: {
+			title: '考试管理新增/编辑',
+			isKeepAlive: true,
+			isDynamic: true,
+		},
+	},
 ];
 /**
  * 定义404、401界面

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

@@ -0,0 +1,174 @@
+<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="OrgCode">
+                <el-tree-select
+                    v-model="state.queryParams.OrgCode"
+                    :data="state.orgData"
+                    node-key="id"
+                    :props="{ label: 'name' }"
+                    collapse-tags
+                    collapse-tags-tooltip
+                    :render-after-expand="false"
+                    style="width: 200px;"
+                    @change="handleQuery"
+                />
+            </el-form-item>
+            <el-form-item label="关键词" prop="Keyword">
+                <el-input v-model="state.queryParams.Keyword" placeholder="姓名/电话/账号" clearable style="width: 150px"></el-input>
+            </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="name" title="姓名" min-width="100"></vxe-column>
+            <vxe-column field="fullOrgName" title="部门" min-width="100"></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 { getCanUseOrgByUser, getUserListPaged } from '@/api/system/user';
+
+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, // 每页条数
+		OrgCode: null, // 部门id
+        Keyword: null, // 关键词
+	},
+	tableCheckbox: [] as any[], // 已选择的人员
+	orgData: [], // 部门数据
+});
+const ruleDialogFormRef = ref<FormInstance>(); // 表单
+const dialogVisible = ref(false); // 弹窗
+
+// 获取部门数据
+const getOrgData = async () => {
+	try {
+		const { result } = await getCanUseOrgByUser();
+		state.orgData = result ?? [];
+	} catch (error) {
+	}
+};
+// 打开弹窗
+const openDialog = async (selData: any[]) => {
+	dialogVisible.value = true;
+	try {
+        if (selData) {
+			state.tableCheckbox = other.deepClone(selData);
+		} else {
+			state.tableCheckbox = [];
+		}
+	    getOrgData();
+		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;
+	getUserListPaged(state.queryParams)
+		.then((res: any) => {
+			state.tableData = res.result?.items ?? [];
+			state.total = res.result?.total ?? 0;
+            const rows = [] as any[];
+            state.tableCheckbox.forEach(item => {
+                rows.push(state.tableData.find(it => it.id === item.userId));
+            })
+            tableRef.value.setCheckboxRow(rows, true);
+			state.loading = false;
+		})
+		.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.userId == row.id){
+            blog = 'delete'
+        }
+    });
+    if (blog == 'add'){
+        const rowObj = other.deepClone(row);
+        rowObj.userId = rowObj.id;
+        Reflect.deleteProperty(rowObj, 'id');
+        state.tableCheckbox.push(rowObj)
+    }else {
+        state.tableCheckbox = state.tableCheckbox.filter(it => it.userId !== 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>

+ 481 - 0
src/views/examTrain/exam/examManage/edit.vue

@@ -0,0 +1,481 @@
+<template>
+	<div class="knowledge-edit-container layout-pd">
+		<el-card shadow="never" style="padding: 0 50px">
+			<el-form :model="state.ruleForm" ref="ruleFormRef" label-width="120px" scroll-to-error>
+				<el-row :gutter="35">
+					<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
+                        <el-skeleton :loading="state.loading" animated>
+							<template #template>
+								<el-form-item label="考试编号">
+									<el-skeleton-item variant="h1" />
+								</el-form-item>
+							</template>
+							<template #default>
+								<el-form-item label="考试编号" prop="code" :rules="[{ required: true, message: '请输入考试编号', trigger: 'blur' }]">
+                                    <el-input v-model="state.ruleForm.code" placeholder="请输入考试编号" clearable></el-input>
+								</el-form-item>
+							</template>
+						</el-skeleton>
+                        <el-skeleton :loading="state.loading" animated>
+							<template #template>
+								<el-form-item label="考试标题">
+									<el-skeleton-item variant="h1" />
+								</el-form-item>
+							</template>
+							<template #default>
+								<el-form-item label="考试标题" prop="name" :rules="[{ required: true, message: '请输入考试标题', trigger: 'blur' }]">
+                                    <el-input v-model="state.ruleForm.name" placeholder="请输入考试标题" clearable></el-input>
+								</el-form-item>
+							</template>
+						</el-skeleton>
+						<el-skeleton :loading="state.loading" animated>
+							<template #template>
+								<el-form-item label="考试类型">
+									<el-skeleton-item variant="h1" />
+								</el-form-item>
+							</template>
+							<template #default>
+								<el-form-item label="考试类型" prop="examType" :rules="[{ required: true, message: '请选择考试类型', trigger: 'change' }]" >
+                                    <el-radio-group v-model="state.ruleForm.examType" @change="selMode">
+                                        <el-radio :value="0">正式考试</el-radio>
+                                        <el-radio :value="1">模拟考试</el-radio>
+                                    </el-radio-group>
+								</el-form-item>
+							</template>
+						</el-skeleton>
+                        <div v-if="[0].includes(state.ruleForm.examType)">
+                            <el-skeleton :loading="state.loading" animated>
+                                <template #template>
+                                    <el-form-item label="可考次数">
+                                        <el-skeleton-item variant="h1" />
+                                    </el-form-item>
+                                </template>
+                                <template #default>
+                                    <el-form-item label="可考次数" prop="count" :rules="[{ validator: vaildateInt, trigger: 'blur'},{ required: true, message: '请输入可考次数', trigger: 'blur' }]">
+                                        <el-input-number v-model="state.ruleForm.count" :min="0" class="w100" ></el-input-number>
+                                    </el-form-item>
+                                </template>
+                            </el-skeleton>
+                            <el-skeleton :loading="state.loading" animated>
+                                <template #template>
+                                    <el-form-item label="考试时长">
+                                        <el-skeleton-item variant="h1" />
+                                    </el-form-item>
+                                </template>
+                                <template #default>
+                                    <el-form-item label="考试时长" prop="timeSpan" :rules="[{ validator: vaildateInt, trigger: 'blur'}, { required: true, message: '请输入可考次数', trigger: 'blur' }]">
+                                        <el-input-number v-model="state.ruleForm.timeSpan" :min="0" class="w100" >
+                                            <template #suffix>
+                                                <span>分钟</span>
+                                            </template>
+                                        </el-input-number>
+                                    </el-form-item>
+                                </template>
+                            </el-skeleton>
+                            <el-skeleton :loading="state.loading" animated>
+                                <template #template>
+                                    <el-form-item label="考试时间">
+                                        <el-skeleton-item variant="h1" />
+                                    </el-form-item>
+                                </template>
+                                <template #default>
+                                    <el-form-item label="考试时间" prop="rangeTime" :rules="[{ required: true, message: '请选择考试时间', trigger: 'change' }]">
+                                        <el-date-picker v-model="state.ruleForm.rangeTime" type="datetimerange" class="w100" value-format="YYYY-MM-DD[T]HH:mm:ss" range-separator="-" start-placeholder="请选择考试开始时间" end-placeholder="请选择考试结束时间" :disabled-date="disabledDate" popper-class="no-atTheMoment" />
+                                    </el-form-item>
+                                </template>
+                            </el-skeleton>
+                        </div>
+                        <el-skeleton :loading="state.loading" animated>
+							<template #template>
+								<el-form-item label="考核方式">
+									<el-skeleton-item variant="h1" />
+								</el-form-item>
+							</template>
+							<template #default>
+								<el-form-item label="考核方式" prop="mode" :rules="[{ required: true, message: '请选择考核方式', trigger: 'change' }]" >
+                                    <el-radio-group v-model="state.ruleForm.mode" @change="selMode">
+                                        <el-radio :value="0">统一试卷</el-radio>
+                                        <el-radio :value="1">随机抽题</el-radio>
+                                    </el-radio-group>
+								</el-form-item>
+							</template>
+						</el-skeleton>
+                        <el-skeleton :loading="state.loading" animated>
+							<template #template>
+								<el-form-item :label="state.ruleForm.mode == 0 ? '选择试卷' : '抽题规则'">
+									<el-skeleton-item variant="h1" />
+								</el-form-item>
+							</template>
+							<template #default>
+								<el-form-item :label="state.ruleForm.mode == 0 ? '选择试卷' : '抽题规则'" prop="testPaperId" :rules="[{ required: true, message: state.ruleForm.mode == 0 ? '请选择试卷' : '请选择抽题规则', trigger: 'change' }]">
+                                    <el-select v-model="state.ruleForm.testPaperId" clearable filterable :placeholder="state.ruleForm.mode == 0 ? '请选择试卷' : '请选择抽题规则'" @change="selTestPaper">
+                                        <el-option v-for="item in state.testPaperData" :key='item.id' :label="item.name" :value="item.id"  />
+                                    </el-select>
+                                </el-form-item>
+							</template>
+						</el-skeleton>
+                        <el-skeleton :loading="state.loading" animated v-if="state.ruleForm.mode == 0">
+							<template #template>
+                                <el-form-item label="试题顺序">
+                                    <el-skeleton-item variant="h1" />
+                                </el-form-item>
+							</template>
+							<template #default>
+                                <el-form-item label="试题排序" prop="questionSort" :rules="[{ required: true, message: '请选择试题排序', trigger: 'change' }]" >
+                                    <el-radio-group v-model="state.ruleForm.questionSort">
+                                        <el-radio :value="0">顺序</el-radio>
+                                        <el-radio :value="1">乱序</el-radio>
+                                    </el-radio-group>
+                                </el-form-item>
+							</template>
+						</el-skeleton>
+                        <el-skeleton :loading="state.loading" animated>
+							<template #template>
+                                <el-form-item label="选项顺序">
+                                    <el-skeleton-item variant="h1" />
+                                </el-form-item>
+							</template>
+							<template #default>
+                                <el-form-item label="选项排序" prop="optionSort" :rules="[{ required: true, message: '请选择选项排序', trigger: 'change' }]" >
+                                    <el-radio-group v-model="state.ruleForm.optionSort">
+                                        <el-radio :value="0">顺序</el-radio>
+                                        <el-radio :value="1">乱序</el-radio>
+                                    </el-radio-group>
+                                </el-form-item>
+							</template>
+						</el-skeleton>
+                        <el-skeleton :loading="state.loading" animated>
+							<template #template>
+								<el-form-item label="备注">
+									<el-skeleton-item variant="h1" />
+								</el-form-item>
+							</template>
+							<template #default>
+								<el-form-item label="备注" prop="remark" :rules="[{ required: false, message: '请输入备注', trigger: 'blur' }]">
+                                    <el-input type="textarea" :rows="3" v-model="state.ruleForm.remark" placeholder="请输入备注" clearable></el-input>
+								</el-form-item>
+							</template>
+						</el-skeleton>
+					</el-col>
+					<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
+                        <el-skeleton :loading="state.loading" animated>
+							<template #template>
+								<el-form-item label="题型分数">
+									<el-skeleton-item variant="h1" />
+								</el-form-item>
+							</template>
+							<template #default>
+								<el-form-item label="题型分数" prop="examQuestionScoreDtos" class="w100 tableFormItem" :rules="[{ required: true, message: '请输入题型分数', trigger: 'change' }]">
+									<vxe-table
+										border
+										:data="state.ruleForm.examQuestionScoreDtos"
+										:column-config="{ resizable: true }"
+										:row-config="{ height: 50, useKey: true }"
+										ref="tableRef"
+										height="300"
+										auto-resize
+										show-overflow
+										:scrollY="{ enabled: true, gt: 100 }"
+										id="countTable"
+										:custom-config="{ storage: true }"
+										showHeaderOverflow
+									>
+										<vxe-column field="questionTypeDesc" title="题型" width="100"></vxe-column>
+                                        <vxe-column title="题型分数" min-width="250">
+                                            <template #default="scope">
+                                                <el-form-item :prop="`examQuestionScoreDtos.` + scope.$rowIndex+ `.score`" :rules="[{ validator: vaildateInt, trigger: 'blur'}]">
+                                                    <el-input-number v-model="scope.row.score" :min="0" class="w100" @change="onScoreChange" >
+                                                        <template #suffix>
+                                                            <span>分/题  共{{scope.row.count}}题</span>
+                                                        </template>
+                                                    </el-input-number>
+                                                </el-form-item>
+                                            </template>
+                                        </vxe-column>
+									</vxe-table>
+								</el-form-item>
+							</template>
+						</el-skeleton>
+						<el-skeleton :loading="state.loading" animated>
+							<template #template>
+								<el-form-item label="合格分数">
+									<el-skeleton-item variant="h1" />
+								</el-form-item>
+							</template>
+							<template #default>
+								<el-form-item label="合格分数" prop="cutoffScore" :rules="[{ required: true, message: '请输入合格分数', trigger: 'blur' }]">
+                                    <el-input-number v-model="state.ruleForm.cutoffScore" :min="0" :max="state.ruleForm.totalScore" class="w100" >
+                                        <template #suffix>
+                                            <span>总分:{{state.ruleForm.totalScore}}</span>
+                                        </template>
+                                    </el-input-number>
+								</el-form-item>
+							</template>
+						</el-skeleton>
+                        <el-skeleton :loading="state.loading" animated>
+							<template #template>
+								<el-form-item label="参考人员">
+									<el-skeleton-item variant="h1" />
+								</el-form-item>
+							</template>
+							<template #default>
+								<el-form-item label="参考人员" prop="userExamDtos" class="w100 tableFormItem" :rules="[{ required: true, message: '请选择参考人员', trigger: 'change' }]">
+                                    <vxe-toolbar
+                                        ref="toolbarRef"
+                                        custom
+                                    >
+                                        <template #buttons>
+                                            <el-button type="primary" @click="onAddUserTable">
+                                                <SvgIcon name="ele-Plus" class="mr10" />新增
+                                            </el-button>
+                                        </template>
+                                    </vxe-toolbar>
+									<vxe-table
+										border
+										:data="state.ruleForm.userExamDtos"
+										:column-config="{ resizable: true }"
+										:row-config="{ height: 50, useKey: true }"
+										ref="tableRef"
+										height="300"
+										auto-resize
+										show-overflow
+										:scrollY="{ enabled: true, gt: 100 }"
+										id="userTable"
+										:custom-config="{ storage: true }"
+										showHeaderOverflow
+									>
+										<vxe-column field="name" title="姓名" min-width="100"></vxe-column>
+										<vxe-column field="fullOrgName" title="部门" min-width="100"></vxe-column>
+                                        <vxe-column title="操作" fixed="right" width="80" align="center" :show-overflow="false">
+                                            <template #default="{ row }">
+                                                <el-button link type="danger" @click="onRowDelUserTable(row)" title="删除">
+                                                    删除
+                                                </el-button>
+                                            </template>
+                                        </vxe-column>
+									</vxe-table>
+								</el-form-item>
+							</template>
+						</el-skeleton>
+					</el-col>
+					<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" 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>
+					</el-col>
+				</el-row>
+			</el-form>
+		</el-card>
+
+		<!-- 选择参考人员 -->
+		<examManage-users ref="ExamManageUsersRef" @choose="chooseUsers" />
+	</div>
+</template>
+
+<script setup lang="ts" name="questionEdit">
+import { defineAsyncComponent, nextTick, 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 { Local } from '@/utils/storage';
+import other from '@/utils/other';
+import { throttle, transformFile, guid } from '@/utils/tools';
+import { removeDuplicate } from '@/utils/arrayOperation';
+import { VTreeDrop } from '@wsfe/vue-tree';
+import {getExtractRuleAllList} from '@/api/examTrain/extractRule';
+import {getTestPaperAllList} from '@/api/examTrain/testPaper';
+import { addExamManage, editExamManage, getExamManageDetail, getTestPaperQuestionCount } from '@/api/examTrain/examManage';
+// 引入组件
+const ExamManageUsers = defineAsyncComponent(() => import('@/views/examTrain/exam/examManage/components/ExamManage-users.vue')); // 选择参考人员
+
+// 定义变量内容
+const state = reactive<any>({
+	dialogVisible: false,
+	ruleForm: {
+		code: '', // 考试编号
+		name: '', // 考试标题
+		examType: 0, // 考试类型
+		mode: 0, // 考核方式
+        testPaperId: '', // 试卷||抽题规则id
+		count: 0, // 可考次数
+		timeSpan: 0, // 考试时长
+        startTime: '', // 考试开始时间
+        endTime: '', // 考试结束时间
+        rangeTime: [], // 考试时间范围
+		questionSort: 0, // 试题顺序
+		optionSort: 0, // 选项顺序
+		remark: '', // 备注
+        cutoffScore: 0, // 合格分数
+        totalScore: 0, // 总分
+		examQuestionScoreDtos: [], // 题型分数
+		userExamDtos: [], // 参考人员数据
+	},
+	loading: false,
+    testPaperData: [], // 试题或者抽题规则数据
+    paperData: [], // 试题数据
+    ruleData: [], // 抽题规则数据
+});
+
+const route = useRoute(); // 获取路由
+const router = useRouter(); // 路由跳转
+const ruleFormRef = ref<any>(); // 表单ref
+
+// 切换考核方式
+const selMode = () => {
+    state.ruleForm.testPaperId = '';
+    if (state.ruleForm.mode == 0){
+        state.testPaperData = other.deepClone(state.paperData.filter(it => it.examType == state.ruleForm.examType));
+    }else if (state.ruleForm.mode == 1) {
+        state.testPaperData = other.deepClone(state.ruleData.filter(it => it.examType == state.ruleForm.examType));
+    }
+}
+// 选择试卷||抽题规则
+const selTestPaper = async (value: any) => {
+	try {
+        const testPaperId = state.ruleForm.mode == 0 ? state.ruleForm.testPaperId : '';
+        const extractRuleId = state.ruleForm.mode == 1 ? state.ruleForm.testPaperId : '';
+		const { result } = await getTestPaperQuestionCount(testPaperId, extractRuleId);
+		state.ruleForm.examQuestionScoreDtos = result ?? [];
+        state.ruleForm.examQuestionScoreDtos.forEach(it => {it.score = 0})
+        state.ruleForm.cutoffScore = 0;
+        state.ruleForm.totalScore = 0;
+	} catch (error) {
+        console.log(error);
+	}
+};
+// 设置考试开始时间选择范围
+const disabledDate = (time: Date) => {
+	return time.getTime() < Date.now() - 8.64e7; // - 8.64e7是今天可以选
+};
+// 分数变化
+const onScoreChange = (value: any) => {
+    state.ruleForm.totalScore = 0;
+    state.ruleForm.examQuestionScoreDtos.forEach(it => state.ruleForm.totalScore += (it.score * it.count));
+}
+// 选择参考人员
+const ExamManageUsersRef = ref<RefType>();
+const onAddUserTable = () => {
+	ExamManageUsersRef.value.openDialog(state.ruleForm.userExamDtos);
+};
+// 删除参考人员
+const onRowDelUserTable = (row: any) => {
+	state.ruleForm.userExamDtos = state.ruleForm.userExamDtos.filter(it => it.userId !== row.userId );
+    ElMessage.success('删除成功');
+};
+// 确定选择了参考人员
+const chooseUsers = (data: any) => {
+    state.ruleForm.userExamDtos = [];
+	if (data) {
+		data.forEach(it => state.ruleForm.userExamDtos.push({userId: it.userId, name: it.name, fullOrgName: it.fullOrgName}));
+	}
+};
+// 验证input值>0
+const vaildateInt = (rule: any, value: any, callback: any) => {
+	if (value <= 0) {
+    callback(new Error("请输入大于0的整数"))
+  } else {
+    callback()
+  }
+};
+// 保存提交
+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', 'examManage');
+	router.push({
+		path: '/examTrain/exam/examManage',
+	});
+	state.loading = false;
+};
+const handleSuccess = () => {
+	state.loading = false;
+	ElMessage.success('操作成功');
+	// 关闭当前 tagsView
+	mittBus.emit('onCurrentContextmenuClick', Object.assign({}, { contextMenuClickId: 1, ...route }));
+	mittBus.emit('clearCache', 'examManage');
+	router.push({
+		path: '/examTrain/exam/examManage',
+	});
+};
+// 获取统一试卷数据
+const getPaperData = async () => {
+	state.loading = true;
+	try {
+		const { result } = await getTestPaperAllList();
+		state.paperData = result ?? [];
+		state.loading = false;
+	} catch (error) {
+		state.loading = false;
+	}
+};
+// 获取抽题规则数据
+const getRuleData = async () => {
+	state.loading = true;
+	try {
+		const { result } = await getExtractRuleAllList();
+		state.ruleData = result ?? [];
+		state.loading = false;
+	} catch (error) {
+		state.loading = false;
+	}
+};
+const getDetail = async () => {
+    selMode();
+	if (route.params.id) {
+		const res: any = await getExamManageDetail(route.params.id);
+		state.ruleForm = res.result ?? {};
+		state.ruleForm.rangeTime = [state.ruleForm.startTime, state.ruleForm.endTime];
+	}
+};
+onMounted(async () => {
+	await getPaperData();
+	await getRuleData();
+	await getDetail();
+});
+</script>
+<style lang="scss">
+.vtree-tree-drop__wrapper {
+	width: 100%;
+}
+.tableFormItem .el-form-item__content{
+	display: block;
+}
+</style>

+ 224 - 0
src/views/examTrain/exam/examManage/index.vue

@@ -0,0 +1,224 @@
+<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="examType">
+                    <el-select v-model="state.queryParams.examType" clearable placeholder="考试类型" @change="handleQuery" :disabled="state.tableLoading">
+                        <el-option key='0' label="正式考试" :value="0" />
+                        <el-option key='1' label="模拟考试" :value="1" />
+                    </el-select>
+                </el-form-item>
+                <el-form-item label="考核方式" prop="mode">
+                    <el-select v-model="state.queryParams.mode" clearable placeholder="考核方式" @change="handleQuery" :disabled="state.tableLoading">
+                        <el-option key='0' label="统一试卷" :value="0" />
+                        <el-option key='1' label="随机抽题" :value="1" />
+                    </el-select>
+                </el-form-item>
+                <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>
+                    <el-button type="primary" @click="handleQuery" :loading="state.tableLoading"> <SvgIcon name="ele-Search" class="mr5" />查询 </el-button>
+                    <el-button @click="drawer = true" class="default-button"> <SvgIcon name="ele-Search" class="mr5" />更多查询</el-button>
+                </el-form-item>
+            </el-form>
+            <vxe-toolbar
+                ref="toolbarRef"
+                :loading="state.tableLoading"
+                custom
+                :refresh="{
+                    queryMethod: handleQuery,
+                }"
+            >
+                <template #buttons>
+                    <el-button type="primary" @click="onAdd" v-auth="'examManage:index:add'" :loading="state.tableLoading">
+                        <SvgIcon name="ele-Plus" class="mr10" />新增
+                    </el-button>
+                </template>
+            </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="examManage"
+                    :custom-config="{ storage: true }"
+                    showHeaderOverflow
+                >
+                    <vxe-column field="code" title="考试编号" width="150"></vxe-column>
+                    <vxe-column field="examTypeDes" title="考试类型" width="100"></vxe-column>
+                    <vxe-column field="mehtodDes" title="考核方式" width="100"></vxe-column>
+                    <vxe-column field="name" title="考试标题" min-width="150"></vxe-column>
+                    <vxe-column field="totalScore" title="考试总分" width="100"></vxe-column>
+                    <vxe-column field="cutoffScore" title="合格分数" width="100"></vxe-column>
+                    <vxe-column field="count" title="可考次数" width="100"></vxe-column>
+                    <vxe-column field="startTime" title="可考开始时间" width="160">
+						<template #default="{ row }">
+							{{ formatDate(row.startTime, 'YYYY-mm-dd HH:MM:SS') }}
+						</template>
+					</vxe-column>
+                    <vxe-column field="endTime" title="可考结束时间" width="160">
+						<template #default="{ row }">
+							{{ formatDate(row.endTime, 'YYYY-mm-dd HH:MM:SS') }}
+						</template>
+					</vxe-column>
+                    <vxe-column field="timeSpan" title="考试时长" width="100"></vxe-column>
+                    <vxe-column field="examStatus" title="考试状态" width="100">
+                        <template #default="{ row }">
+                            <span v-if="row.examStatus == 0">未开始</span>
+                            <span v-else-if="row.examStatus == 1">考试中</span>
+                            <span v-else-if="row.examStatus == 2">考试结束</span>
+                        </template>
+                    </vxe-column>
+                    <vxe-column field="status" title="状态" width="160">
+                        <template #default="{ row }">
+                            <el-switch v-model="row.status" />
+                        </template>
+                    </vxe-column>
+                    <vxe-column title="操作" fixed="right" width="120" align="center" :show-overflow="false">
+                        <template #default="{ row }">
+                            <el-button link type="primary" @click="onEdit(row)" title="编辑" v-auth="'examManage:index:edit'">
+                                编辑
+                            </el-button>
+                            <el-button link type="danger" @click="onRowDel(row)" title="删除" v-auth="'examManage:index:delete'">
+                                删除
+                            </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>
+        <!--	更多查询	-->
+		<el-drawer v-model="drawer" title="更多查询" size="500px">
+			<el-form :model="state.queryParams" ref="drawerRuleFormRef" @submit.native.prevent label-width="100px">
+				<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>
+			<template #footer>
+				<el-button type="primary" @click="handleQuery" :loading="state.tableLoading"> <SvgIcon name="ele-Search" class="mr5" />查询 </el-button>
+				<el-button @click="resetQuery(drawerRuleFormRef)" class="default-button"> <SvgIcon name="ele-Refresh" class="mr5" />重置 </el-button>
+			</template>
+		</el-drawer>
+	</div>
+</template>
+
+<script lang="tsx" setup name="examManage">
+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 {deleteExamManage, getExamManageData} from '@/api/examTrain/examManage';
+
+// 引入组件
+const pagination = defineAsyncComponent(() => import('@/components/ProTable/components/Pagination.vue')); // 分页
+
+const router = useRouter(); //路由
+
+// 定义变量内容
+const state = reactive<any>({
+	queryParams: {
+		PageIndex: 1, //页码
+		PageSize: 20, //每页条数
+        code: '', // 编号
+        name: '', // 标题
+		examType: null, // 考试类型
+		mode: 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);
+	getExamManageData(requestParams.value)
+		.then((response: any) => {
+			state.tableData = response?.result.items ?? [];
+			state.total = response?.result.pagination.totalCount;
+			state.tableLoading = false;
+		})
+		.catch(() => {
+			state.tableLoading = false;
+		});
+};
+/** 重置按钮操作 */
+const drawerRuleFormRef = ref();
+const ruleFormRef = ref<RefType>(); // 表单ref
+const drawer = ref(false);
+const resetQuery = (formEl: FormInstance | undefined) => {
+	if (!formEl) return;
+	formEl.resetFields();
+	ruleFormRef.value?.resetFields();
+	handleQuery();
+};
+// 新增
+const onAdd = () => {
+	router.push({
+		name: 'examManageEdit',
+		params: {
+			tagsViewName: '新增考试',
+		},
+	});
+};
+// 编辑
+const onEdit  = (row: any) => {
+	router.push({
+		name: 'examManageEdit',
+		params: {
+			id: row.id,
+			tagsViewName: '编辑考试',
+		},
+	});
+};
+// 删除当前行
+const onRowDel = (row: any) => {
+	ElMessageBox.confirm(`是否确认删除当前试卷?删除后不可恢复`, '提示', {
+		confirmButtonText: '确定',
+		cancelButtonText: '取消',
+		type: 'warning',
+	})
+        .then(() => {
+			deleteExamManage({id: row.id}).then(() => {
+				ElMessage.success('删除成功');
+				queryList();
+			});
+		})
+		.catch(() => {});
+};
+// 表格选中状态
+const tableRef = ref<RefType>();
+const toolbarRef = ref<RefType>();
+onMounted(() => {
+	queryList();
+	if (tableRef.value && toolbarRef.value) {
+		tableRef.value.connect(toolbarRef.value);
+	}
+});
+</script>

+ 160 - 0
src/views/examTrain/exam/userExam/components/UserExam-exam.vue

@@ -0,0 +1,160 @@
+<template>
+	<el-dialog v-model="state.dialogVisible" draggable append-to-body destroy-on-close :show-close="false" width="80%">
+        <template #header="{ close, titleId, titleClass }">
+            <div class="topContent">
+                <div class="surplusBox">
+                    <span>剩余时间:</span>
+                    <span class="surplus">{{ state.surplusTime }}</span>
+                </div>
+                <div class="titleBox">
+                    <span class="title">统一考试</span>
+                </div>
+                <div class="submitBox">
+				    <el-button type="info" @click="close">关 闭</el-button>
+                    <el-button type="primary" @click="onSubmit()" :loading="state.loading">交 卷</el-button>
+                </div>
+            </div>
+        </template>
+        <el-row :gutter="10">
+            <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 {{questionsItem.IsAnswer == '1' ? 'done' : ''}}" v-for="(questionsItem, questionsIndex) in item.questions" @click="onSelQuestion(questionsItem.id)">{{questionsIndex + 1}}</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 }}({{ state.questionDetail.score }}分)</p>
+                    <div class="questionAnswer">
+                        <div v-if="[0,2].includes(state.questionDetail.questionType)">
+							<el-radio-group>
+                                <el-radio :label="item.content" :value="item.id" v-for="item in state.questionDetail.questionOptions" />
+                            </el-radio-group>
+                        </div>
+                        <div v-else-if="state.questionDetail.questionType == 1">
+                            <el-checkbox-group>
+                                <el-checkbox :label="item.content" :value="item.id" v-for="item in state.questionDetail.questionOptions" />
+                            </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" :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" :rows="3" placeholder="请输入答案" clearable></el-input>
+                            </el-form-item>
+                        </div>
+                    </div>
+                </div>
+            </el-col>
+        </el-row>
+		<template #footer>
+			<span class="dialog-footer">
+				<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 { ElMessage, FormInstance } from 'element-plus';
+import { excludeSelfById } from '@/utils/tools';
+import other from '@/utils/other';
+import { getExamQuestionGroup, getExamQuestion } from '@/api/examTrain/userExam';
+
+// 定义子组件向父组件传值/事件
+const emit = defineEmits(['updateList']);
+
+// 定义变量内容
+const state = reactive<any>({
+	dialogVisible: false, // 弹窗
+	loading: false, // 加载
+    surplusTime: '00:00:00', // 剩余时间
+	questionTypeData: [] as any[], // 试卷题型数据
+    selQuestionID: '', // 选择的问题id
+    questionDetail: null, // 选择的问题明细
+});
+// 打开弹窗
+const ruleFormRef = ref<any>(); // 表单ref
+const openDialog = async (row: any) => {
+	try {
+		state.dialogVisible = true;
+		const { result } = await getExamQuestionGroup(row.id);
+        result.forEach(item => {
+            switch(item.questionType){
+				case 0: item.questionTypeDesc = '单选题'; break;
+				case 1: item.questionTypeDesc = '多选题'; break;
+				case 2: item.questionTypeDesc = '判断题'; break;
+				case 3: item.questionTypeDesc = '填空题'; break;
+				case 4: item.questionTypeDesc = '问答题'; break;
+				default: break;
+			}
+        })
+		state.questionTypeData = result;
+	} catch (error) {
+		// 打印错误信息
+		console.error(error);
+	}
+};
+const close = () => {
+	ruleFormRef.value?.clearValidate();
+	ruleFormRef.value?.resetFields();
+};
+// 关闭弹窗
+const closeDialog = () => {
+	state.dialogVisible = false;
+};
+// 上一题下一题
+const onLastQuestion = () => {
+
+}
+const onNextQuestion = () => {
+
+}
+// 选择的问题id
+const onSelQuestion = async (value: any) => {
+    console.log(value);
+    try {
+		const { result } = await getExamQuestion(value);
+		state.questionDetail = result;
+        console.log(state.questionDetail);
+	} catch (error) {
+		// 打印错误信息
+		console.error(error);
+	}
+}
+// 保存
+const onSubmit = async () => {
+	
+};
+// 暴露变量
+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 .marked::after{content: "*";width: 5px;height: 5px;position: absolute;top: -3px;right: 4px;color: #FF5722;font-size: 25px;}
+    .examListContent .examList .done{background-color: #5FB878;color: #fff;}
+    .questionBox{padding: 20px 10px 0;}
+    .questionBox .questionTitle{margin-bottom: 10px;font-size: 16px;}
+    .questionBox .questionAnswer{padding: 0 20px;}
+    .questionBox .questionAnswer .answerItem{width:calc(100%);}
+    .questionBox .questionAnswer .answerItem .optionIndex{vertical-align: sub;font-size: 16px;display: inline-block;}
+</style>

+ 108 - 0
src/views/examTrain/exam/userExam/components/UserExam-view.vue

@@ -0,0 +1,108 @@
+<template>
+	<el-dialog title="修改标签" v-model="state.dialogVisible" draggable append-to-body destroy-on-close @close="close" width="500px">
+		<el-form :model="state.ruleForm" ref="ruleFormRef" label-width="80px" v-loading="state.loading">
+			<el-row :gutter="10">
+				<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+					<el-form-item label="上级标签" prop="parentId" :rules="[{ required: false, message: '请选择上级标签', trigger: 'change' }]">
+						<el-cascader
+							:options="state.treeData"
+							filterable
+							:props="{ checkStrictly: true, value: 'id', label: 'name', emitPath: false, children: 'children' }"
+							placeholder="请选择上级标签"
+							clearable
+							class="w100"
+							v-model="state.ruleForm.parentId"
+							ref="cascadeRef"
+							@change="getExamTagList"
+						>
+						</el-cascader>
+					</el-form-item>
+				</el-col>
+				<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+					<el-form-item label="标签名称" prop="name" :rules="[{ required: true, message: '请填写标签名称', trigger: 'blur' }]">
+						<el-input v-model="state.ruleForm.name" placeholder="请填写标签名称" clearable></el-input>
+					</el-form-item>
+				</el-col>
+			</el-row>
+		</el-form>
+		<template #footer>
+			<span class="dialog-footer">
+				<el-button @click="closeDialog" class="default-button">取 消</el-button>
+				<el-button type="primary" @click="onSubmit(ruleFormRef)" :loading="state.loading">确 定 </el-button>
+			</span>
+		</template>
+	</el-dialog>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref } from 'vue';
+import { ElMessage, FormInstance } from 'element-plus';
+import { excludeSelfById } from '@/utils/tools';
+import other from '@/utils/other';
+import { examTagDetail, updateExamTag } from '@/api/examTrain/tag';
+
+// 定义子组件向父组件传值/事件
+const emit = defineEmits(['updateList']);
+
+// 定义变量内容
+const state = reactive<any>({
+	dialogVisible: false, // 弹窗
+	ruleForm: {
+		name: '', // 类型名称
+		parentId: '', // 上级类型
+	},
+	treeData: [], // 上级
+	loading: false, // 加载
+});
+// 打开弹窗
+const ruleFormRef = ref<any>(); // 表单ref
+const openDialog = async (row: any, treeData: any) => {
+	try {
+		state.dialogVisible = true;
+		const { result } = await examTagDetail(row.id);
+		state.ruleForm = result;
+		state.treeData = other.deepClone(treeData);
+		state.treeData = excludeSelfById(state.treeData, row.id);
+	} catch (error) {
+		// 打印错误信息
+		console.error(error);
+	}
+};
+const close = () => {
+	ruleFormRef.value?.clearValidate();
+	ruleFormRef.value?.resetFields();
+};
+// 关闭弹窗
+const closeDialog = () => {
+	state.dialogVisible = false;
+};
+const cascadeRef = ref<RefType>();
+// 获取选择name值
+const getExamTagList = () => {
+	let currentNode = cascadeRef.value.getCheckedNodes();
+	state.ruleForm.parentName = currentNode[0]?.label ?? '';
+};
+// 保存
+const onSubmit = async (formEl: FormInstance | undefined) => {
+	if (!formEl) return;
+	await formEl.validate((valid: boolean) => {
+		if (!valid) return;
+		state.loading = true;
+		updateExamTag(state.ruleForm)
+			.then(() => {
+				closeDialog(); // 关闭弹窗
+				ElMessage.success('操作成功');
+				state.loading = false;
+				emit('updateList');
+			})
+			.catch(() => {
+				state.loading = false;
+			});
+	});
+};
+// 暴露变量
+defineExpose({
+	openDialog,
+	closeDialog,
+});
+</script>

+ 225 - 0
src/views/examTrain/exam/userExam/index.vue

@@ -0,0 +1,225 @@
+<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="name">
+                    <el-input v-model="state.queryParams.name" placeholder="考试标题" clearable class="keyword-input" />
+                </el-form-item>
+                <el-form-item label="考试类型" prop="examType">
+                    <el-select v-model="state.queryParams.examType" clearable placeholder="考试类型" @change="handleQuery" :disabled="state.tableLoading">
+                        <el-option key='0' label="正式考试" :value="0" />
+                        <el-option key='1' label="模拟考试" :value="1" />
+                    </el-select>
+                </el-form-item>
+                <el-form-item label="开始时间>=" prop="minStartTime">
+                    <el-date-picker v-model="state.queryParams.minStartTime" type="datetime" class="w100" value-format="YYYY-MM-DD[T]HH:mm:ss"  placeholder="请选择" popper-class="no-atTheMoment" />
+                </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="drawer = true" class="default-button"> <SvgIcon name="ele-Search" class="mr5" />更多查询</el-button>
+                </el-form-item>
+            </el-form>
+            <vxe-toolbar
+                ref="toolbarRef"
+                :loading="state.tableLoading"
+                custom
+                :refresh="{
+                    queryMethod: handleQuery,
+                }"
+            >
+                <template #buttons>
+                    <el-button type="primary" @click="onExam({id: '08dd66b8-22b4-4dc3-83e5-85ca716c0957'})" :loading="state.tableLoading">
+                        <SvgIcon name="ele-Plus" class="mr10" />新增
+                    </el-button>
+                </template>
+            </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="userExam"
+                    :custom-config="{ storage: true }"
+                    showHeaderOverflow
+                >
+                    <vxe-column field="examName" title="考试标题" width="150"></vxe-column>
+                    <vxe-column field="examTypeDesc" title="考试类型" width="100"></vxe-column>
+                    <vxe-column field="startTime" title="可考开始时间" width="160">
+						<template #default="{ row }">
+							{{ formatDate(row.startTime, 'YYYY-mm-dd HH:MM:SS') }}
+						</template>
+					</vxe-column>
+                    <vxe-column field="endTime" title="可考结束时间" width="160">
+						<template #default="{ row }">
+							{{ formatDate(row.endTime, 'YYYY-mm-dd HH:MM:SS') }}
+						</template>
+					</vxe-column>
+                    <vxe-column field="timeSpan" title="考试时长" width="100"></vxe-column>
+                    <vxe-column field="examStatus" title="考试状态" width="100">
+                        <template #default="{ row }">
+                            <span v-if="row.examStatus == 0">未开始</span>
+                            <span v-else-if="row.examStatus == 1">考试中</span>
+                            <span v-else-if="row.examStatus == 2">考试结束</span>
+                        </template>
+                    </vxe-column>
+                    <vxe-column field="totalScore" title="考试总分" width="100"></vxe-column>
+                    <vxe-column field="cutoffScore" title="合格分数" width="100"></vxe-column>
+                    <vxe-column field="score" title="考试得分" width="100"></vxe-column>
+                    <vxe-column field="isSuccess" title="是否合格" width="100">
+                        <template #default="{ row }">
+                            <span v-if="row.isSuccess">合格</span>
+                            <span v-else-if="!row.examStatus">不合格</span>
+                        </template>
+                    </vxe-column>
+                    <vxe-column title="操作" fixed="right" width="120" align="center" :show-overflow="false">
+                        <template #default="{ row }">
+                            <el-button type="primary" @click="onExam(row)" title="考试" v-auth="'userExam:index:exam'">
+                                考试
+                            </el-button>
+                            <el-button link type="primary" @click="onView(row)" title="查看" v-auth="'userExam: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>
+        <!--	更多查询	-->
+		<el-drawer v-model="drawer" title="更多查询" size="500px">
+			<el-form :model="state.queryParams" ref="drawerRuleFormRef" @submit.native.prevent label-width="100px">
+                <el-form-item label="开始时间<=" prop="maxStartTime">
+                    <el-date-picker v-model="state.queryParams.maxStartTime" type="datetime" class="w100" value-format="YYYY-MM-DD[T]HH:mm:ss"  placeholder="请选择" popper-class="no-atTheMoment" />
+                </el-form-item>
+                <el-form-item label="结束时间>=" prop="minEndTime">
+                    <el-date-picker v-model="state.queryParams.minEndTime" type="datetime" class="w100" value-format="YYYY-MM-DD[T]HH:mm:ss"  placeholder="请选择" popper-class="no-atTheMoment" />
+                </el-form-item>
+                <el-form-item label="结束时间<=" prop="maxEndTime">
+                    <el-date-picker v-model="state.queryParams.maxEndTime" type="datetime" class="w100" value-format="YYYY-MM-DD[T]HH:mm:ss"  placeholder="请选择" popper-class="no-atTheMoment" />
+                </el-form-item>
+                <el-form-item label="考试总分>=" prop="minTotalScore">
+                    <el-input type="number" v-model="state.queryParams.minTotalScore" placeholder="请填写" clearable></el-input>
+                </el-form-item>
+                <el-form-item label="考试总分<=" prop="maxTotalScore">
+                    <el-input type="number" v-model="state.queryParams.maxTotalScore" placeholder="请填写" clearable></el-input>
+                </el-form-item>
+                <el-form-item label="考试得分>=" prop="minScore">
+                    <el-input type="number" v-model="state.queryParams.minScore" placeholder="请填写" clearable></el-input>
+                </el-form-item>
+                <el-form-item label="考试得分<=" prop="maxScore">
+                    <el-input type="number" v-model="state.queryParams.maxScore" placeholder="请填写" clearable></el-input>
+                </el-form-item>
+			</el-form>
+			<template #footer>
+				<el-button type="primary" @click="handleQuery" :loading="state.tableLoading"> <SvgIcon name="ele-Search" class="mr5" />查询 </el-button>
+				<el-button @click="resetQuery(drawerRuleFormRef)" class="default-button"> <SvgIcon name="ele-Refresh" class="mr5" />重置 </el-button>
+			</template>
+		</el-drawer>
+
+        <userExam-exam ref="userExamExamRef" @updateList="queryList" />
+		<userExam-view ref="userExamViewRef" @updateList="queryList" />
+	</div>
+</template>
+
+<script lang="tsx" setup name="userExam">
+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 {getUserExamData} from '@/api/examTrain/userExam';
+
+// 引入组件
+const pagination = defineAsyncComponent(() => import('@/components/ProTable/components/Pagination.vue')); // 分页
+const UserExamExam = defineAsyncComponent(() => import('@/views/examTrain/exam/userExam/components/UserExam-exam.vue')); // 考试组件
+const UserExamView = defineAsyncComponent(() => import('@/views/examTrain/exam/userExam/components/UserExam-view.vue')); // 查看组件
+
+const router = useRouter(); //路由
+
+// 定义变量内容
+const state = reactive<any>({
+	queryParams: {
+		PageIndex: 1, //页码
+		PageSize: 20, //每页条数
+        name: '', // 标题
+		examType: null, // 考试类型
+        minStartTime: null, // 最小开始时间
+        maxStartTime: null, // 最大开始时间
+        minEndTime: null, // 最小结束时间
+        maxEndTime: null, // 最大结束时间
+        minTotalScore: null, // 最小总分
+        maxTotalScore: null, // 最大总分
+        minScore: null, // 最小分数
+        maxScore: 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);
+	getUserExamData(requestParams.value)
+		.then((response: any) => {
+			state.tableData = response?.result.items ?? [];
+			state.total = response?.result.pagination.totalCount;
+			state.tableLoading = false;
+		})
+		.catch(() => {
+			state.tableLoading = false;
+		});
+};
+/** 重置按钮操作 */
+const drawerRuleFormRef = ref();
+const ruleFormRef = ref<RefType>(); // 表单ref
+const drawer = ref(false);
+const resetQuery = (formEl: FormInstance | undefined) => {
+	if (!formEl) return;
+	formEl.resetFields();
+	ruleFormRef.value?.resetFields();
+	handleQuery();
+};
+// 打开考试弹窗
+const userExamExamRef = ref<RefType>(); // 新增ref
+const onExam = (row: any) => {
+	userExamExamRef.value.openDialog(row, state.tableData.value);
+};
+// 打开查看弹窗
+const userExamViewRef = ref<RefType>(); // 修改ref
+const onView = (row: any) => {
+	userExamViewRef.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>