2 Commits e59057c48d ... 47131e3baa

Author SHA1 Message Date
  zjq 47131e3baa Merge remote-tracking branch 'origin/test/exam' into dev 1 month ago
  zjq 4df2b50584 培训模板、培训计划 1 month ago

+ 60 - 0
src/api/examTrain/trainPlan.ts

@@ -0,0 +1,60 @@
+/*
+ * @Author: zjq
+ * @description 培训计划
+ */
+import request from '@/utils/request';
+
+/**
+ * @description 新增培训计划
+ * @param {object} data
+ */
+export const addTrainPlan = (data: object) => {
+    return request({
+        url: '/api/v1/TrainPlan/Add',
+        method: 'post',
+        data,
+    });
+};
+/**
+ * @description 编辑培训计划
+ * @param {object} data
+ */
+export const editTrainPlan = (data: object) => {
+    return request({
+        url: '/api/v1/TrainPlan/Update',
+        method: 'put',
+        data,
+    });
+};
+/**
+ * @description 删除培训计划
+ * @param {object} data
+ */
+export const deleteTrainPlan = (data: object) => {
+    return request({
+        url: '/api/v1/TrainPlan/Delete',
+        method: 'delete',
+        data,
+    });
+};
+/**
+ * @description 获取培训计划列表
+ * @param {object} params
+ */
+export const getTrainPlanData = (params?: object) => {
+    return request({
+        url: '/api/v1/TrainPlan/GetPagedList',
+        method: 'post',
+        data: params,
+    });
+};
+/**
+ * @description 查询培训计划详情
+ * @param {object} Id
+ */
+export const getTrainPlanDetail = (Id: string) => {
+    return request({
+        url: `/api/v1/TrainPlan/Get?id=${Id}`,
+        method: 'get',
+    });
+};

+ 70 - 0
src/api/examTrain/trainTemplate.ts

@@ -0,0 +1,70 @@
+/*
+ * @Author: zjq
+ * @description 培训模板
+ */
+import request from '@/utils/request';
+
+/**
+ * @description 新增培训模板
+ * @param {object} data
+ */
+export const addTrainTemplate = (data: object) => {
+    return request({
+        url: '/api/v1/TrainTemplate/Add',
+        method: 'post',
+        data,
+    });
+};
+/**
+ * @description 编辑培训模板
+ * @param {object} data
+ */
+export const editTrainTemplate = (data: object) => {
+    return request({
+        url: '/api/v1/TrainTemplate/Update',
+        method: 'put',
+        data,
+    });
+};
+/**
+ * @description 删除培训模板
+ * @param {object} data
+ */
+export const deleteTrainTemplate = (data: object) => {
+    return request({
+        url: '/api/v1/TrainTemplate/Delete',
+        method: 'delete',
+        data,
+    });
+};
+/**
+ * @description 获取培训模板列表
+ * @param {object} params
+ */
+export const getTrainTemplateData = (params?: object) => {
+    return request({
+        url: '/api/v1/TrainTemplate/GetPagedList',
+        method: 'post',
+        data: params,
+    });
+};
+/**
+ * @description 查询培训模板详情
+ * @param {object} Id
+ */
+export const getTrainTemplateDetail = (Id: string) => {
+    return request({
+        url: `/api/v1/TrainTemplate/Get?id=${Id}`,
+        method: 'get',
+    });
+};
+/**
+ * @description 获取列表全部数据
+ * @param {number} 
+ */
+export const getTrainTemplateAllList = () => {
+    return request({
+        url: `/api/v1/TrainTemplate/GetList`,
+        method: 'get',
+    });
+};

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

@@ -1,5 +1,5 @@
 <template>
 <template>
-	<el-dialog title="选择参考人员" v-model="dialogVisible" draggable append-to-body destroy-on-close width="800px">
+	<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 :model="state.queryParams" ref="ruleDialogFormRef" inline @submit.native.prevent :disabled="state.loading" class="ruleDialogForm">
 			<el-form-item label="部门" prop="OrgCode">
 			<el-form-item label="部门" prop="OrgCode">
                 <el-tree-select
                 <el-tree-select

+ 15 - 5
src/views/examTrain/exam/examManage/index.vue

@@ -55,22 +55,32 @@
                 >
                 >
                     <vxe-column field="code" title="考试编号" width="150"></vxe-column>
                     <vxe-column field="code" title="考试编号" width="150"></vxe-column>
                     <vxe-column field="examTypeDes" title="考试类型" width="100"></vxe-column>
                     <vxe-column field="examTypeDes" title="考试类型" width="100"></vxe-column>
-                    <vxe-column field="mehtodDes" title="考核方式" width="100"></vxe-column>
+                    <vxe-column field="modeDes" title="考核方式" width="100"></vxe-column>
                     <vxe-column field="name" title="考试标题" min-width="150"></vxe-column>
                     <vxe-column field="name" title="考试标题" min-width="150"></vxe-column>
                     <vxe-column field="totalScore" title="考试总分" width="100"></vxe-column>
                     <vxe-column field="totalScore" title="考试总分" width="100"></vxe-column>
                     <vxe-column field="cutoffScore" 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="count" title="可考次数" width="100">
+                        <template #default="{ row }">
+                            {{ row.examType == 0 ? row.count : '' }}
+						</template>
+                    </vxe-column>
                     <vxe-column field="startTime" title="可考开始时间" width="160">
                     <vxe-column field="startTime" title="可考开始时间" width="160">
 						<template #default="{ row }">
 						<template #default="{ row }">
-							{{ formatDate(row.startTime, 'YYYY-mm-dd HH:MM:SS') }}
+                            <span v-if="row.examType == 0">{{formatDate(row.startTime, 'YYYY-mm-dd HH:MM:SS')}}</span>
+                            <span v-else></span>
 						</template>
 						</template>
 					</vxe-column>
 					</vxe-column>
                     <vxe-column field="endTime" title="可考结束时间" width="160">
                     <vxe-column field="endTime" title="可考结束时间" width="160">
 						<template #default="{ row }">
 						<template #default="{ row }">
-							{{ formatDate(row.endTime, 'YYYY-mm-dd HH:MM:SS') }}
+                            <span v-if="row.examType == 0">{{formatDate(row.endTime, 'YYYY-mm-dd HH:MM:SS')}}</span>
+                            <span v-else></span>
 						</template>
 						</template>
 					</vxe-column>
 					</vxe-column>
-                    <vxe-column field="timeSpan" title="考试时长" width="100"></vxe-column>
+                    <vxe-column field="timeSpan" title="考试时长" width="100">
+                        <template #default="{ row }">
+                            {{ row.examType == 0 ? row.timeSpan : '' }}
+						</template>
+                    </vxe-column>
                     <vxe-column field="examStatus" title="考试状态" width="100">
                     <vxe-column field="examStatus" title="考试状态" width="100">
                         <template #default="{ row }">
                         <template #default="{ row }">
                             <span v-if="row.examStatus == 0">未开始</span>
                             <span v-if="row.examStatus == 0">未开始</span>

+ 202 - 0
src/views/examTrain/train/plan/components/Plan-add.vue

@@ -0,0 +1,202 @@
+<template>
+	<el-dialog title="新增培训计划" v-model="state.dialogVisible" draggable append-to-body destroy-on-close @close="close" width="800px">
+		<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="name" :rules="[{ required: true, message: '请填写计划名称', trigger: 'blur' }]">
+						<el-input v-model="state.ruleForm.name" placeholder="请填写计划名称" clearable></el-input>
+					</el-form-item>
+				</el-col>
+				<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+                    <el-form-item label="培训模板" prop="trainTemplateIds" :rules="[{ required: true, message: '请选择培训模板', trigger: 'change' }]">
+                        <el-select v-model="state.ruleForm.trainTemplateIds" clearable filterable multiple placeholder="请选择培训模板" @change="selTrainTemplate">
+                            <el-option v-for="item in state.trainTemplateData" :key='item.id' :label="item.name" :value="item.id"  />
+                        </el-select>
+                    </el-form-item>
+				</el-col>
+                <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+					<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>
+				</el-col>
+				<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+					<el-form-item label="培训人员" prop="trainRecordDtos" 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.trainRecordDtos"
+                            :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>
+				</el-col>
+				<!-- <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+					<el-form-item label="排序" prop="weight" :rules="[{ required: true, message: '请填写排序', trigger: 'blur' }]">
+                        <el-input v-model="state.ruleForm.weight" 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>
+
+        <!-- 选择培训人员 -->
+		<examManage-users ref="ExamManageUsersRef" @choose="chooseUsers" />
+	</el-dialog>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref, defineAsyncComponent } from 'vue';
+import { ElMessage, FormInstance } from 'element-plus';
+import Other from '@/utils/other';
+import {getTrainTemplateAllList} from '@/api/examTrain/trainTemplate';
+import {addTrainPlan} from '@/api/examTrain/trainPlan';
+
+// 引入组件
+const ExamManageUsers = defineAsyncComponent(() => import('@/views/examTrain/exam/examManage/components/ExamManage-users.vue')); // 选择培训人员
+
+// 定义子组件向父组件传值/事件
+const emit = defineEmits(['updateList']);
+
+// 定义变量内容
+const state = reactive<any>({
+	dialogVisible: false, // 弹窗
+	ruleForm: {
+		name: '', // 模板名称
+        trainStartTime: '', // 培训开始时间
+        trainEndTime: '', // 培训结束时间
+        rangeTime: [], // 培训时间范围
+        trainTemplateIds: [], // 培训模板选择人员ids
+		trainPlanTemplateDtos : [], // 培训模板
+		trainRecordDtos: [], // 培训人员
+	},
+	loading: false, // 加载
+	tableLoading: false, // 表格加载
+    trainTemplateData: [], // 培训模板下拉数据
+});
+// 打开弹窗
+const ruleFormRef = ref<any>(); // 表单ref
+// 获取培训模板数据
+const getTrainTemplateData = async () => {
+	state.loading = true;
+	try {
+		const { result } = await getTrainTemplateAllList();
+		state.trainTemplateData = result ?? [];
+		state.loading = false;
+	} catch (error) {
+		state.loading = false;
+	}
+};
+// 打开弹窗
+const openDialog = async () => {
+	state.dialogVisible = true;
+    await getTrainTemplateData();
+};
+const close = () => {
+	ruleFormRef.value?.clearValidate();
+	ruleFormRef.value?.resetFields();
+	state.ruleForm.trainTemplateIds = [];
+	state.ruleForm.trainPlanTemplateDtos = [];
+	state.ruleForm.trainRecordDtos = [];
+};
+// 关闭弹窗
+const closeDialog = () => {
+	state.dialogVisible = false;
+	state.ruleForm.trainTemplateIds = [];
+	state.ruleForm.trainPlanTemplateDtos = [];
+	state.ruleForm.trainRecordDtos = [];
+};
+// 选择培训模板
+const selTrainTemplate = async (value: any) => {
+	state.ruleForm.trainPlanTemplateDtos = [];
+    state.ruleForm.trainTemplateIds.forEach(item => {
+        state.ruleForm.trainPlanTemplateDtos.push({trainTemplateId: item});
+    })
+};
+// 设置培训开始时间选择范围
+const disabledDate = (time: Date) => {
+	return time.getTime() < Date.now() - 8.64e7; // - 8.64e7是今天可以选
+};
+// 选择培训人员
+const ExamManageUsersRef = ref<RefType>();
+const onAddUserTable = () => {
+	ExamManageUsersRef.value.openDialog(state.ruleForm.trainRecordDtos);
+};
+// 删除培训人员
+const onRowDelUserTable = (row: any) => {
+	state.ruleForm.trainRecordDtos = state.ruleForm.trainRecordDtos.filter(it => it.userId !== row.userId );
+    ElMessage.success('删除成功');
+};
+// 确定选择了培训人员
+const chooseUsers = (data: any) => {
+    state.ruleForm.trainRecordDtos = [];
+	if (data) {
+		data.forEach(it => state.ruleForm.trainRecordDtos.push({userId: it.userId, name: it.name, fullOrgName: it.fullOrgName}));
+	}
+};
+// 新增
+const onSubmit = async (formEl: FormInstance | undefined) => {
+	if (!formEl) return;
+	await formEl.validate((valid: boolean) => {
+		if (!valid) return;
+		state.loading = true;
+        const submitObj = Other.deepClone(state.ruleForm);
+        submitObj.trainStartTime = submitObj.rangeTime[0];
+        submitObj.trainEndTime = submitObj.rangeTime[1];
+        Reflect.deleteProperty(submitObj, 'rangeTime');
+        Reflect.deleteProperty(submitObj, 'trainTemplateIds');
+		console.log(submitObj);
+		addTrainPlan(submitObj)
+			.then(() => {
+				emit('updateList');
+				closeDialog(); // 关闭弹窗
+				ElMessage.success('操作成功');
+				state.loading = false;
+			})
+			.catch(() => {
+				state.loading = false;
+			});
+	});
+};
+// 暴露变量
+defineExpose({
+	openDialog,
+	closeDialog,
+});
+</script>
+<style lang="scss">
+	.tableFormItem .el-form-item__content{
+		display: block;
+	}
+</style>

+ 211 - 0
src/views/examTrain/train/plan/components/Plan-edit.vue

@@ -0,0 +1,211 @@
+<template>
+	<el-dialog title="编辑培训计划" v-model="state.dialogVisible" draggable append-to-body destroy-on-close @close="close" width="800px">
+		<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="name" :rules="[{ required: true, message: '请填写计划名称', trigger: 'blur' }]">
+						<el-input v-model="state.ruleForm.name" placeholder="请填写计划名称" clearable></el-input>
+					</el-form-item>
+				</el-col>
+				<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+                    <el-form-item label="培训模板" prop="trainTemplateIds" :rules="[{ required: true, message: '请选择培训模板', trigger: 'change' }]">
+                        <el-select v-model="state.ruleForm.trainTemplateIds" clearable filterable multiple placeholder="请选择培训模板" @change="selTrainTemplate">
+                            <el-option v-for="item in state.trainTemplateData" :key='item.id' :label="item.name" :value="item.id"  />
+                        </el-select>
+                    </el-form-item>
+				</el-col>
+                <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+					<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>
+				</el-col>
+				<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+					<el-form-item label="培训人员" prop="trainRecordDtos" 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.trainRecordDtos"
+                            :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>
+				</el-col>
+				<!-- <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+					<el-form-item label="排序" prop="weight" :rules="[{ required: true, message: '请填写排序', trigger: 'blur' }]">
+                        <el-input v-model="state.ruleForm.weight" 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>
+
+        <!-- 选择培训人员 -->
+		<examManage-users ref="ExamManageUsersRef" @choose="chooseUsers" />
+	</el-dialog>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref, defineAsyncComponent } from 'vue';
+import { ElMessage, FormInstance } from 'element-plus';
+import Other from '@/utils/other';
+import {getTrainTemplateAllList} from '@/api/examTrain/trainTemplate';
+import {editTrainPlan, getTrainPlanDetail} from '@/api/examTrain/trainPlan';
+
+// 引入组件
+const ExamManageUsers = defineAsyncComponent(() => import('@/views/examTrain/exam/examManage/components/ExamManage-users.vue')); // 选择培训人员
+
+// 定义子组件向父组件传值/事件
+const emit = defineEmits(['updateList']);
+
+// 定义变量内容
+const state = reactive<any>({
+	dialogVisible: false, // 弹窗
+	ruleForm: {
+		name: '', // 模板名称
+        trainStartTime: '', // 培训开始时间
+        trainEndTime: '', // 培训结束时间
+        rangeTime: [], // 培训时间范围
+        trainTemplateIds: [], // 培训模板选择人员ids
+		trainPlanTemplateDtos : [], // 培训模板
+		trainRecordDtos: [], // 培训人员
+	},
+	loading: false, // 加载
+	tableLoading: false, // 表格加载
+    trainTemplateData: [], // 培训模板下拉数据
+});
+// 打开弹窗
+const ruleFormRef = ref<any>(); // 表单ref
+// 获取培训模板数据
+const getTrainTemplateData = async () => {
+	state.loading = true;
+	try {
+		const { result } = await getTrainTemplateAllList();
+		state.trainTemplateData = result ?? [];
+		state.loading = false;
+	} catch (error) {
+		state.loading = false;
+	}
+};
+// 打开弹窗
+const openDialog = async (row: any) => {
+	state.dialogVisible = true;
+    await getTrainTemplateData();
+    try {
+		const { result } = await getTrainPlanDetail(row.id);
+		state.ruleForm = result;
+		state.ruleForm.trainTemplateIds = state.ruleForm.trainPlanTemplateDtos.map(it => it.trainTemplateId);
+		state.ruleForm.rangeTime = [state.ruleForm.trainStartTime, state.ruleForm.trainEndTime];
+	} catch (error) {
+		console.log(error);
+		state.loading = false;
+	}
+};
+const close = () => {
+	ruleFormRef.value?.clearValidate();
+	ruleFormRef.value?.resetFields();
+	state.ruleForm.trainTemplateIds = [];
+	state.ruleForm.trainPlanTemplateDtos = [];
+	state.ruleForm.trainRecordDtos = [];
+};
+// 关闭弹窗
+const closeDialog = () => {
+	state.dialogVisible = false;
+	state.ruleForm.trainTemplateIds = [];
+	state.ruleForm.trainPlanTemplateDtos = [];
+	state.ruleForm.trainRecordDtos = [];
+};
+// 选择培训模板
+const selTrainTemplate = async (value: any) => {
+	state.ruleForm.trainPlanTemplateDtos = [];
+    state.ruleForm.trainTemplateIds.forEach(item => {
+        state.ruleForm.trainPlanTemplateDtos.push({trainTemplateId: item});
+    })
+};
+// 设置培训开始时间选择范围
+const disabledDate = (time: Date) => {
+	return time.getTime() < Date.now() - 8.64e7; // - 8.64e7是今天可以选
+};
+// 选择培训人员
+const ExamManageUsersRef = ref<RefType>();
+const onAddUserTable = () => {
+	ExamManageUsersRef.value.openDialog(state.ruleForm.trainRecordDtos);
+};
+// 删除参考人员
+const onRowDelUserTable = (row: any) => {
+	state.ruleForm.trainRecordDtos = state.ruleForm.trainRecordDtos.filter(it => it.userId !== row.userId );
+    ElMessage.success('删除成功');
+};
+// 确定选择了参考人员
+const chooseUsers = (data: any) => {
+    state.ruleForm.trainRecordDtos = [];
+	if (data) {
+		data.forEach(it => state.ruleForm.trainRecordDtos.push({userId: it.userId, name: it.name, fullOrgName: it.fullOrgName}));
+	}
+};
+// 新增
+const onSubmit = async (formEl: FormInstance | undefined) => {
+	if (!formEl) return;
+	await formEl.validate((valid: boolean) => {
+		if (!valid) return;
+		state.loading = true;
+        const submitObj = Other.deepClone(state.ruleForm);
+        submitObj.trainStartTime = submitObj.rangeTime[0];
+        submitObj.trainEndTime = submitObj.rangeTime[1];
+        Reflect.deleteProperty(submitObj, 'rangeTime');
+        Reflect.deleteProperty(submitObj, 'trainTemplateIds');
+		console.log(submitObj);
+		editTrainPlan(submitObj)
+			.then(() => {
+				emit('updateList');
+				closeDialog(); // 关闭弹窗
+				ElMessage.success('操作成功');
+				state.loading = false;
+			})
+			.catch(() => {
+				state.loading = false;
+			});
+	});
+};
+// 暴露变量
+defineExpose({
+	openDialog,
+	closeDialog,
+});
+</script>
+<style lang="scss">
+	.tableFormItem .el-form-item__content{
+		display: block;
+	}
+</style>

+ 236 - 0
src/views/examTrain/train/plan/index.vue

@@ -0,0 +1,236 @@
+<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 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="onOpenAdd" v-auth="'plan: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="plan"
+                    :custom-config="{ storage: true }"
+                    showHeaderOverflow
+                >
+                    <vxe-column field="code" title="计划编号" width="200"></vxe-column>
+                    <vxe-column field="name" title="计划标题" min-width="200"></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="status" title="状态" width="160">
+                        <template #default="{ row }">
+                            <el-switch v-model="row.status" />
+                        </template>
+                    </vxe-column>
+                    <vxe-column field="creatorName" title="创建人" width="150"></vxe-column>
+                    <vxe-column field="creatorOrgName" title="创建部门" width="150"></vxe-column>
+                    <vxe-column field="creationTime" title="创建时间" width="160">
+						<template #default="{ row }">
+							{{ formatDate(row.creationTime, 'YYYY-mm-dd HH:MM:SS') }}
+						</template>
+					</vxe-column>
+                    <vxe-column title="操作" fixed="right" width="120" align="center" :show-overflow="false">
+                        <template #default="{ row }">
+                            <el-button link type="primary" @click="onOpenEdit(row)" title="编辑" v-auth="'plan:index:edit'">
+                                编辑
+                            </el-button>
+                            <el-button link type="danger" @click="onRowDel(row)" title="删除" v-auth="'plan: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="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="status">
+                    <el-select v-model="state.queryParams.status" 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="creatorName">
+                    <el-input v-model="state.queryParams.creatorName" placeholder="请输入" clearable class="keyword-input" />
+                </el-form-item>
+                <el-form-item label="创建时间>=" prop="minCreationTime">
+                    <el-date-picker v-model="state.queryParams.minCreationTime" 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="maxCreationTime">
+                    <el-date-picker v-model="state.queryParams.maxCreationTime" type="datetime" class="w100" value-format="YYYY-MM-DD[T]HH:mm:ss"  placeholder="请选择" popper-class="no-atTheMoment" />
+                </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>
+
+        <plan-add ref="planAddRef" @updateList="queryList" />
+		<plan-edit ref="planEditRef" @updateList="queryList" />
+	</div>
+</template>
+
+<script lang="tsx" setup name="plan">
+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 {deleteTrainPlan, getTrainPlanData} from '@/api/examTrain/trainPlan';
+
+// 引入组件
+const pagination = defineAsyncComponent(() => import('@/components/ProTable/components/Pagination.vue')); // 分页
+const PlanAdd = defineAsyncComponent(() => import('@/views/examTrain/train/plan/components/Plan-add.vue')); // 新增组件
+const PlanEdit = defineAsyncComponent(() => import('@/views/examTrain/train/plan/components/Plan-edit.vue')); // 修改组件
+
+const router = useRouter(); //路由
+
+// 定义变量内容
+const state = reactive<any>({
+	queryParams: {
+		PageIndex: 1, //页码
+		PageSize: 20, //每页条数
+        code: null, // 编号
+        name: null, // 名称
+		status: null, // 状态
+        minStartTime: null , // 开始时间>=
+        maxStartTime: null , // 开始时间<=
+        minEndTime: null , // 结束时间>=
+        maxEndTTime: null , // 结束时间<=
+        creatorName: null , // 创建人
+        minCreationTime: null , // 创建时间>=
+        maxCreationTime: null , // 创建时间<=
+	},
+	tableData: [], //表格数据
+	total: 0, //总条数
+	tableLoading: false, //表格loading
+});
+/** 搜索按钮操作 节流操作 */
+const handleQuery = () => {
+	state.queryParams.PageIndex = 1;
+	queryList();
+};
+/** 获取试题列表 */
+const requestParams = ref<EmptyObjectType>({});
+const queryList = () => {
+	state.tableLoading = true;
+    requestParams.value = Other.deepClone(state.queryParams);
+	getTrainPlanData(requestParams.value)
+		.then((response: any) => {
+            console.log(response)
+			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 planAddRef = ref<RefType>(); // 新增ref
+const onOpenAdd = () => {
+	planAddRef.value.openDialog();
+};
+// 打开编辑弹窗
+const planEditRef = ref<RefType>(); // 修改ref
+const onOpenEdit = (row: any) => {
+	planEditRef.value.openDialog(row, state.tableData);
+};
+// 删除当前行
+const onRowDel = (row: any) => {
+	ElMessageBox.confirm(`是否确认删除当前培训计划?删除后不可恢复`, '提示', {
+		confirmButtonText: '确定',
+		cancelButtonText: '取消',
+		type: 'warning',
+	})
+        .then(() => {
+			deleteTrainPlan({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>

+ 219 - 0
src/views/examTrain/train/template/components/Template-add.vue

@@ -0,0 +1,219 @@
+<template>
+	<el-dialog title="新增培训模板" v-model="state.dialogVisible" draggable append-to-body destroy-on-close @close="close" width="800px">
+		<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="name" :rules="[{ required: true, message: '请填写模板名称', trigger: 'blur' }]">
+						<el-input v-model="state.ruleForm.name" placeholder="请填写模板名称" clearable></el-input>
+					</el-form-item>
+				</el-col>
+				<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+					<el-form-item label="培训资料" prop="trainKnowladges" class="w100 tableFormItem" :rules="[{ required: true, message: '请选择培训资料', trigger: 'change' }]">
+                        <vxe-toolbar
+                            ref="toolbarRef"
+                            custom
+                        >
+                            <template #buttons>
+                                <el-button type="primary" @click="onAddKnowladgesTable">
+                                    <SvgIcon name="ele-Plus" class="mr10" />新增
+                                </el-button>
+                            </template>
+                        </vxe-toolbar>
+                        <vxe-table
+                            border
+                            :data="state.ruleForm.trainKnowladges"
+                            :column-config="{ resizable: true }"
+                            :row-config="{ height: 50, useKey: true }"
+                            ref="tableRef"
+                            height="300"
+                            auto-resize
+                            show-overflow
+                            :scrollY="{ enabled: true, gt: 100 }"
+                            id="knowladgesTable"
+                            :custom-config="{ storage: true }"
+                            showHeaderOverflow
+                        >
+                            <vxe-column field="title" 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="onRowDelKnowladgesTable(row)" title="删除">
+                                        删除
+                                    </el-button>
+                                </template>
+                            </vxe-column>
+                        </vxe-table>
+                    </el-form-item>
+				</el-col>
+				<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+					<el-form-item label="包含习题" prop="isContainsPractice" :rules="[{ required: true, message: '请选择是否包含习题', trigger: 'change' }]">
+						<el-radio-group v-model="state.ruleForm.isContainsPractice">
+							<el-radio :value="true">是</el-radio>
+							<el-radio :value="false">否</el-radio>
+						</el-radio-group>
+					</el-form-item>
+				</el-col>
+				<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" v-if="state.ruleForm.isContainsPractice == true">
+					<el-form-item label="选择习题" prop="trainPracticeDtos" class="w100 tableFormItem" :rules="[{ required: true, message: '请选择习题', trigger: 'change' }]">
+                        <vxe-toolbar
+                            ref="toolbarRef"
+                            custom
+                        >
+                            <template #buttons>
+                                <el-button type="primary" @click="onAddQuestionsTable">
+                                    <SvgIcon name="ele-Plus" class="mr10" />新增
+                                </el-button>
+                            </template>
+                        </vxe-toolbar>
+                        <vxe-table
+                            border
+                            :data="state.ruleForm.trainPracticeDtos"
+                            :column-config="{ resizable: true }"
+                            :row-config="{ height: 50, useKey: true }"
+                            ref="tableRef"
+                            height="300"
+                            auto-resize
+                            show-overflow
+                            :scrollY="{ enabled: true, gt: 100 }"
+                            id="questionsTable"
+                            :custom-config="{ storage: true }"
+                            showHeaderOverflow
+                        >
+                            <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="100"></vxe-column>
+                            <vxe-column title="操作" fixed="right" width="80" align="center" :show-overflow="false">
+                                <template #default="{ row }">
+                                    <el-button link type="danger" @click="onRowDelQuestionsTable(row)" title="删除">
+                                        删除
+                                    </el-button>
+                                </template>
+                            </vxe-column>
+                        </vxe-table>
+                    </el-form-item>
+				</el-col>
+				<!-- <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+					<el-form-item label="排序" prop="weight" :rules="[{ required: true, message: '请填写排序', trigger: 'blur' }]">
+                        <el-input v-model="state.ruleForm.weight" 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>
+
+        <!-- 关联知识库 -->
+		<question-knowledge ref="QuestionKnowledgeRef" @choose="chooseKnowledge" />
+        <!-- 自选试题页面 -->
+		<testPaper-optional ref="TestPaperOptionalRef" @choose="chooseQuestion" />
+	</el-dialog>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref, defineAsyncComponent } from 'vue';
+import { ElMessage, FormInstance } from 'element-plus';
+import Other from '@/utils/other';
+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 emit = defineEmits(['updateList']);
+
+// 定义变量内容
+const state = reactive<any>({
+	dialogVisible: false, // 弹窗
+	ruleForm: {
+		name: '', // 模板名称
+		trainKnowladges : [], // 培训资料
+		isContainsPractice: null, // 是否包含习题
+		trainPracticeDtos: [], // 习题
+	},
+	loading: false, // 加载
+	tableLoading: false, // 表格加载
+});
+// 打开弹窗
+const ruleFormRef = ref<any>(); // 表单ref
+// 打开弹窗
+const openDialog = async () => {
+	state.dialogVisible = true;
+};
+const close = () => {
+	ruleFormRef.value?.clearValidate();
+	ruleFormRef.value?.resetFields();
+	state.ruleForm.trainKnowladges = [];
+	state.ruleForm.trainPracticeDtos = [];
+};
+// 关闭弹窗
+const closeDialog = () => {
+	state.dialogVisible = false;
+	state.ruleForm.trainKnowladges = [];
+	state.ruleForm.trainPracticeDtos = [];
+};
+// 选择知识
+const QuestionKnowledgeRef = ref<RefType>();
+const onAddKnowladgesTable = () => {
+	QuestionKnowledgeRef.value.openDialog(state.ruleForm.trainKnowladges);
+};
+// 确定选择了知识库
+const chooseKnowledge = (data: any) => {
+    state.ruleForm.trainKnowladges = Other.deepClone(data);
+};
+// 删除知识
+const onRowDelKnowladgesTable = (row: any) => {
+	state.ruleForm.trainKnowladges = state.ruleForm.trainKnowladges.filter(it => it.knowladgeId !== row.knowladgeId );
+    ElMessage.success('删除成功');
+};
+// 选择试题新增
+const TestPaperOptionalRef = ref<RefType>();
+const onAddQuestionsTable = () => {
+    TestPaperOptionalRef.value.openDialog(state.ruleForm.trainPracticeDtos);
+};
+// 确定选择试题
+const chooseQuestion = (data: any) => {
+    state.ruleForm.trainPracticeDtos = Other.deepClone(data);
+};
+// 删除试题
+const onRowDelQuestionsTable = (row: any) => {
+	state.ruleForm.trainPracticeDtos = state.ruleForm.trainPracticeDtos.filter(it => it.questionId !== row.questionId );
+    ElMessage.success('删除成功');
+};
+// 新增
+const onSubmit = async (formEl: FormInstance | undefined) => {
+	if (!formEl) return;
+	await formEl.validate((valid: boolean) => {
+		if (!valid) return;
+		state.loading = true;
+		console.log(state.ruleForm);
+		const submitObj = Other.deepClone(state.ruleForm);
+		if (!submitObj.isContainsPractice){
+			submitObj.trainPracticeDtos = [];
+		}
+		addTrainTemplate(submitObj)
+			.then(() => {
+				emit('updateList');
+				closeDialog(); // 关闭弹窗
+				ElMessage.success('操作成功');
+				state.loading = false;
+			})
+			.catch(() => {
+				state.loading = false;
+			});
+	});
+};
+// 暴露变量
+defineExpose({
+	openDialog,
+	closeDialog,
+});
+</script>
+<style lang="scss">
+	.tableFormItem .el-form-item__content{
+		display: block;
+	}
+</style>

+ 226 - 0
src/views/examTrain/train/template/components/Template-edit.vue

@@ -0,0 +1,226 @@
+<template>
+	<el-dialog title="编辑培训模板" v-model="state.dialogVisible" draggable append-to-body destroy-on-close @close="close" width="800px">
+		<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="name" :rules="[{ required: true, message: '请填写模板名称', trigger: 'blur' }]">
+						<el-input v-model="state.ruleForm.name" placeholder="请填写模板名称" clearable></el-input>
+					</el-form-item>
+				</el-col>
+				<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+					<el-form-item label="培训资料" prop="trainKnowladges" class="w100 tableFormItem" :rules="[{ required: true, message: '请选择培训资料', trigger: 'change' }]">
+                        <vxe-toolbar
+                            ref="toolbarRef"
+                            custom
+                        >
+                            <template #buttons>
+                                <el-button type="primary" @click="onAddKnowladgesTable">
+                                    <SvgIcon name="ele-Plus" class="mr10" />新增
+                                </el-button>
+                            </template>
+                        </vxe-toolbar>
+                        <vxe-table
+                            border
+                            :data="state.ruleForm.trainKnowladges"
+                            :column-config="{ resizable: true }"
+                            :row-config="{ height: 50, useKey: true }"
+                            ref="tableRef"
+                            height="300"
+                            auto-resize
+                            show-overflow
+                            :scrollY="{ enabled: true, gt: 100 }"
+                            id="knowladgesTable"
+                            :custom-config="{ storage: true }"
+                            showHeaderOverflow
+                        >
+                            <vxe-column field="title" 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="onRowDelKnowladgesTable(row)" title="删除">
+                                        删除
+                                    </el-button>
+                                </template>
+                            </vxe-column>
+                        </vxe-table>
+                    </el-form-item>
+				</el-col>
+				<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+					<el-form-item label="包含习题" prop="isContainsPractice" :rules="[{ required: true, message: '请选择是否包含习题', trigger: 'change' }]">
+						<el-radio-group v-model="state.ruleForm.isContainsPractice">
+							<el-radio :value="true">是</el-radio>
+							<el-radio :value="false">否</el-radio>
+						</el-radio-group>
+					</el-form-item>
+				</el-col>
+				<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" v-if="state.ruleForm.isContainsPractice == true">
+					<el-form-item label="选择习题" prop="trainPracticeDtos" class="w100 tableFormItem" :rules="[{ required: true, message: '请选择习题', trigger: 'change' }]">
+                        <vxe-toolbar
+                            ref="toolbarRef"
+                            custom
+                        >
+                            <template #buttons>
+                                <el-button type="primary" @click="onAddQuestionsTable">
+                                    <SvgIcon name="ele-Plus" class="mr10" />新增
+                                </el-button>
+                            </template>
+                        </vxe-toolbar>
+                        <vxe-table
+                            border
+                            :data="state.ruleForm.trainPracticeDtos"
+                            :column-config="{ resizable: true }"
+                            :row-config="{ height: 50, useKey: true }"
+                            ref="tableRef"
+                            height="300"
+                            auto-resize
+                            show-overflow
+                            :scrollY="{ enabled: true, gt: 100 }"
+                            id="questionsTable"
+                            :custom-config="{ storage: true }"
+                            showHeaderOverflow
+                        >
+                            <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="100"></vxe-column>
+                            <vxe-column title="操作" fixed="right" width="80" align="center" :show-overflow="false">
+                                <template #default="{ row }">
+                                    <el-button link type="danger" @click="onRowDelQuestionsTable(row)" title="删除">
+                                        删除
+                                    </el-button>
+                                </template>
+                            </vxe-column>
+                        </vxe-table>
+                    </el-form-item>
+				</el-col>
+				<!-- <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+					<el-form-item label="排序" prop="weight" :rules="[{ required: true, message: '请填写排序', trigger: 'blur' }]">
+                        <el-input v-model="state.ruleForm.weight" 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>
+
+        <!-- 关联知识库 -->
+		<question-knowledge ref="QuestionKnowledgeRef" @choose="chooseKnowledge" />
+        <!-- 自选试题页面 -->
+		<testPaper-optional ref="TestPaperOptionalRef" @choose="chooseQuestion" />
+	</el-dialog>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref, defineAsyncComponent } from 'vue';
+import { ElMessage, FormInstance } from 'element-plus';
+import Other from '@/utils/other';
+import {getTrainTemplateDetail, editTrainTemplate} 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 emit = defineEmits(['updateList']);
+
+// 定义变量内容
+const state = reactive<any>({
+	dialogVisible: false, // 弹窗
+	ruleForm: {
+		name: '', // 模板名称
+		trainKnowladges : [], // 培训资料
+		isContainsPractice: null, // 是否包含习题
+		trainPracticeDtos: [], // 习题
+	},
+	loading: false, // 加载
+	tableLoading: false, // 表格加载
+});
+// 打开弹窗
+const ruleFormRef = ref<any>(); // 表单ref
+// 打开弹窗
+const openDialog = async (row: any) => {
+	state.dialogVisible = true;
+	try {
+		const { result } = await getTrainTemplateDetail(row.id);
+		state.ruleForm = result;
+	} catch (error) {
+		console.log(error);
+		state.loading = false;
+	}
+};
+const close = () => {
+	ruleFormRef.value?.clearValidate();
+	ruleFormRef.value?.resetFields();
+	state.ruleForm.trainKnowladges = [];
+	state.ruleForm.trainPracticeDtos = [];
+};
+// 关闭弹窗
+const closeDialog = () => {
+	state.dialogVisible = false;
+	state.ruleForm.trainKnowladges = [];
+	state.ruleForm.trainPracticeDtos = [];
+};
+// 选择知识
+const QuestionKnowledgeRef = ref<RefType>();
+const onAddKnowladgesTable = () => {
+	QuestionKnowledgeRef.value.openDialog(state.ruleForm.trainKnowladges);
+};
+// 确定选择了知识库
+const chooseKnowledge = (data: any) => {
+    state.ruleForm.trainKnowladges = Other.deepClone(data);
+};
+// 删除知识
+const onRowDelKnowladgesTable = (row: any) => {
+	state.ruleForm.trainKnowladges = state.ruleForm.trainKnowladges.filter(it => it.knowladgeId !== row.knowladgeId );
+    ElMessage.success('删除成功');
+};
+// 选择试题新增
+const TestPaperOptionalRef = ref<RefType>();
+const onAddQuestionsTable = () => {
+    TestPaperOptionalRef.value.openDialog(state.ruleForm.trainPracticeDtos);
+};
+// 确定选择试题
+const chooseQuestion = (data: any) => {
+    state.ruleForm.trainPracticeDtos = Other.deepClone(data);
+};
+// 删除试题
+const onRowDelQuestionsTable = (row: any) => {
+	state.ruleForm.trainPracticeDtos = state.ruleForm.trainPracticeDtos.filter(it => it.questionId !== row.questionId );
+    ElMessage.success('删除成功');
+};
+// 新增
+const onSubmit = async (formEl: FormInstance | undefined) => {
+	if (!formEl) return;
+	await formEl.validate((valid: boolean) => {
+		if (!valid) return;
+		state.loading = true;
+	    console.log(state.ruleForm);
+        const submitObj = Other.deepClone(state.ruleForm);
+		if (!submitObj.isContainsPractice){
+			submitObj.trainPracticeDtos = [];
+		}
+		editTrainTemplate(submitObj)
+			.then(() => {
+				emit('updateList');
+				closeDialog(); // 关闭弹窗
+				ElMessage.success('操作成功');
+				state.loading = false;
+			})
+			.catch(() => {
+				state.loading = false;
+			});
+	});
+};
+// 暴露变量
+defineExpose({
+	openDialog,
+	closeDialog,
+});
+</script>
+<style lang="scss">
+	.tableFormItem .el-form-item__content{
+		display: block;
+	}
+</style>

+ 218 - 0
src/views/examTrain/train/template/index.vue

@@ -0,0 +1,218 @@
+<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 label="包含习题" prop="isContainsPractice">
+                    <el-select v-model="state.queryParams.isContainsPractice" 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>
+                    <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="onOpenAdd" v-auth="'template: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="template"
+                    :custom-config="{ storage: true }"
+                    showHeaderOverflow
+                >
+                    <vxe-column field="code" title="模板编号" width="200"></vxe-column>
+                    <vxe-column field="name" title="模板标题" min-width="200"></vxe-column>
+                    <vxe-column field="isContainsPracticeDes" title="是否包含习题" width="150"></vxe-column>
+                    <vxe-column field="status" title="状态" width="160">
+                        <template #default="{ row }">
+                            <el-switch v-model="row.status" />
+                        </template>
+                    </vxe-column>
+                    <vxe-column field="creatorName" title="创建人" width="150"></vxe-column>
+                    <vxe-column field="creatorOrgName" title="创建部门" width="150"></vxe-column>
+                    <vxe-column field="creationTime" title="创建时间" width="160">
+						<template #default="{ row }">
+							{{ formatDate(row.creationTime, 'YYYY-mm-dd HH:MM:SS') }}
+						</template>
+					</vxe-column>
+                    <vxe-column title="操作" fixed="right" width="120" align="center" :show-overflow="false">
+                        <template #default="{ row }">
+                            <el-button link type="primary" @click="onOpenEdit(row)" title="编辑" v-auth="'template:index:edit'">
+                                编辑
+                            </el-button>
+                            <el-button link type="danger" @click="onRowDel(row)" title="删除" v-auth="'template: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="status">
+                    <el-select v-model="state.queryParams.status" 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="creatorName">
+                    <el-input v-model="state.queryParams.creatorName" placeholder="请输入" clearable class="keyword-input" />
+                </el-form-item>
+                <el-form-item label="创建时间>=" prop="minCreationTime">
+                    <el-date-picker v-model="state.queryParams.minCreationTime" 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="maxCreationTime">
+                    <el-date-picker v-model="state.queryParams.maxCreationTime" type="datetime" class="w100" value-format="YYYY-MM-DD[T]HH:mm:ss"  placeholder="请选择" popper-class="no-atTheMoment" />
+                </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>
+
+        <template-add ref="templateAddRef" @updateList="queryList" />
+		<template-edit ref="templateEditRef" @updateList="queryList" />
+	</div>
+</template>
+
+<script lang="tsx" setup name="template">
+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 {deleteTrainTemplate, getTrainTemplateData} from '@/api/examTrain/trainTemplate';
+
+// 引入组件
+const pagination = defineAsyncComponent(() => import('@/components/ProTable/components/Pagination.vue')); // 分页
+const TemplateAdd = defineAsyncComponent(() => import('@/views/examTrain/train/template/components/Template-add.vue')); // 新增组件
+const TemplateEdit = defineAsyncComponent(() => import('@/views/examTrain/train/template/components/Template-edit.vue')); // 修改组件
+
+const router = useRouter(); //路由
+
+// 定义变量内容
+const state = reactive<any>({
+	queryParams: {
+		PageIndex: 1, //页码
+		PageSize: 20, //每页条数
+        code: null, // 编号
+        name: null, // 名称
+		status: null, // 状态
+        isContainsPractice: null , //  是否包含习题
+        creatorName: null , // 创建人
+        minCreationTime: null , // 创建时间>=
+        maxCreationTime: null , // 创建时间<=
+	},
+	tableData: [], //表格数据
+	total: 0, //总条数
+	tableLoading: false, //表格loading
+});
+/** 搜索按钮操作 节流操作 */
+const handleQuery = () => {
+	state.queryParams.PageIndex = 1;
+	queryList();
+};
+/** 获取试题列表 */
+const requestParams = ref<EmptyObjectType>({});
+const queryList = () => {
+	state.tableLoading = true;
+    requestParams.value = Other.deepClone(state.queryParams);
+	getTrainTemplateData(requestParams.value)
+		.then((response: any) => {
+            console.log(response)
+			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 templateAddRef = ref<RefType>(); // 新增ref
+const onOpenAdd = () => {
+	templateAddRef.value.openDialog();
+};
+// 打开编辑弹窗
+const templateEditRef = ref<RefType>(); // 修改ref
+const onOpenEdit = (row: any) => {
+	templateEditRef.value.openDialog(row, state.tableData);
+};
+// 删除当前行
+const onRowDel = (row: any) => {
+	ElMessageBox.confirm(`是否确认删除当前培训模板?删除后不可恢复`, '提示', {
+		confirmButtonText: '确定',
+		cancelButtonText: '取消',
+		type: 'warning',
+	})
+        .then(() => {
+			deleteTrainTemplate({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>