소스 검색

课件,标签,题库

zjq 1 개월 전
부모
커밋
5d22ea78b7

+ 15 - 0
src/api/courseware/index.ts

@@ -57,4 +57,19 @@ export const getCoursewareDetail = (Id: string) => {
         url: `/api/v1/Sourcewares/Get?id=${Id}`,
         method: 'get',
     });
+};
+/**
+ * @description 批量下载文件 (文件服务)
+ * @param {object}  params
+ */
+export const fileDownloadBatch = (params: object) => {
+    return request({
+        url: `/file/downloadfile_batch`,
+        method: 'get',
+        responseType: 'blob',
+        params,
+        baseURL: import.meta.env.VITE_API_UPLOAD_URL,
+    },{
+        reduce_data_format:false
+    });
 };

+ 104 - 0
src/api/examTrain/questionBank.ts

@@ -0,0 +1,104 @@
+/*
+ * @Author: zjq
+ * @description 题库管理
+ */
+import request from '@/utils/request';
+
+/**
+ * @description 新增试题
+ * @param {object} data
+ */
+export const addQuestion = (data: object) => {
+    return request({
+        url: '/api/v1/Question/Add',
+        method: 'post',
+        data,
+    });
+};
+/**
+ * @description 编辑试题
+ * @param {object} data
+ */
+export const editQuestion = (data: object) => {
+    return request({
+        url: '/api/v1/Question/Update',
+        method: 'put',
+        data,
+    });
+};
+/**
+ * @description 删除试题
+ * @param {object} data
+ */
+export const deleteQuestion = (data: object) => {
+    return request({
+        url: '/api/v1/Question/Delete',
+        method: 'delete',
+        data,
+    });
+};
+/**
+ * @description 获取试题列表
+ * @param {object} params
+ */
+export const getQuestionData = (params?: object) => {
+    return request({
+        url: '/api/v1/Question/GetPagedList',
+        method: 'post',
+        data: params,
+    });
+};
+/**
+ * @description 查询试题详情
+ * @param {object} Id
+ */
+export const getQuestionDetail = (Id: string) => {
+    return request({
+        url: `/api/v1/Question/Get?id=${Id}`,
+        method: 'get',
+    });
+};
+/**
+ * @description 批量下载文件 (文件服务)
+ * @param {object}  params
+ */
+export const fileDownloadBatch = (params: object) => {
+    return request({
+        url: `/file/downloadfile_batch`,
+        method: 'get',
+        responseType: 'blob',
+        params,
+        baseURL: import.meta.env.VITE_API_UPLOAD_URL,
+    },{
+        reduce_data_format:false
+    });
+};
+/**
+ * @description  获取试题模板下载链接
+ * @param {object} params
+ * @return {*}
+ */
+export const questionTemplate = (params?: object) => {
+    return request({
+        url: ``,
+        method: 'get',
+        responseType: 'blob'
+    },{
+        reduce_data_format:false
+    });
+};
+/**
+ * @description  试题导入
+ * @param {object} data
+ * @return {*}
+ */
+export const questionImport = (data: object) => {
+    return request({
+        url: ``,
+        method: 'post',
+        data,
+        headers: {
+            'Content-Type':'multipart/form-data'
+        },
+    });
+};

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

@@ -0,0 +1,60 @@
+/*
+ * @Author: zjq
+ * @description 考试培训 - 标签管理
+ */
+import request from '@/utils/request';
+/**
+ * @description 标签管理树形
+ */
+export const examTagTreeList = () => {
+    return request({
+        url: '/api/v1/ExamTag/GetTreeList',
+        method: 'post',
+        data: {},
+    });
+};
+/**
+ * @description 新增标签
+ * @param {object} data
+ */
+export const addExamTag = (data: object) => {
+    return request({
+        url: '/api/v1/ExamTag/Add',
+        method: 'post',
+        data,
+    });
+};
+/**
+ * @description 编辑标签
+ * @param {object} data
+ */
+export const updateExamTag = (data: object) => {
+    return request({
+        url: '/api/v1/ExamTag/Update',
+        method: 'put',
+        data,
+    });
+};
+/**
+ * @description 查询标签详情
+ * @param {object} Id
+ */
+export const examTagDetail = (Id: string) => {
+    return request({
+        url: `/api/v1/ExamTag/Get?id=${Id}`,
+        method: 'get',
+    });
+};
+/**
+ * @description 删除标签
+ * @param {object} Id
+ */
+export const delExamTag = (Id: string) => {
+    return request({
+        url: `/api/v1/ExamTag/Delete`,
+        method: 'delete',
+        data: {
+            id:Id 
+        }
+    });
+};

+ 30 - 3
src/views/courseware/index/index.vue

@@ -77,7 +77,7 @@
 							show-overflow
 							:print-config="{}"
 							:scrollY="{ enabled: true, gt: 100 }"
-							id="courseManage"
+							id="coursewareManage"
 							:custom-config="{ storage: true }"
 							showHeaderOverflow
 							@checkbox-all="selectAllChangeEvent"
@@ -137,9 +137,10 @@ import Other from '@/utils/other';
 import { downloadFileByStream } from '@/utils/tools';
 import { VxeUI } from 'vxe-pc-ui';
 import { VTreeSearch } from '@wsfe/vue-tree';
-import {deleteCourseware, getCoursewareData} from '@/api/courseware/index';
+import {deleteCourseware, getCoursewareData, fileDownloadBatch} from '@/api/courseware/index';
 import { coursewareTreeList } from '@/api/courseware/type';
 import { fileDownloadByUrl } from '@/api/public/file';
+import { util } from 'echarts';
 
 // 引入组件
 const pagination = defineAsyncComponent(() => import('@/components/ProTable/components/Pagination.vue')); // 分页
@@ -243,7 +244,33 @@ const onAdd = () => {
 };
 // 批量下载
 const onBatchDownload = () => {
-
+	const ids = checkTable.value.map((item: any) => item.attachmentId);
+	console.log(ids);
+    ElMessageBox.confirm(`您确定要下载选中的课件?`, '提示', {
+		confirmButtonText: '确定',
+		cancelButtonText: '取消',
+		type: 'warning',
+		draggable: true,
+		autofocus: false,
+	})
+		.then(() => {
+			fileDownloadBatch({
+				Source: 'hotline',
+				Ids: ids,
+			}).then((res: any) => {
+				console.log(res);
+				let blob: Blob = new Blob([res.data], { type: res.data.type }); // 创建blob 设置blob文件类型 data 设置为后端返回的文件(例如mp3,jpeg) type:这里设置后端返回的类型 为 mp3
+				let down: HTMLAnchorElement = document.createElement('a'); // 创建A标签
+				let href: string = window.URL.createObjectURL(blob); // 创建下载的链接
+				down.href = href; // 下载地址
+				down.download = formatDate(new Date(), 'YYYYmmddHHMMSS')// 下载文件名
+				document.body.appendChild(down);
+				down.click(); // 模拟点击A标签
+				document.body.removeChild(down); // 下载完成移除元素
+				window.URL.revokeObjectURL(href); // 释放blob对象
+			});
+		})
+		.catch(() => {});
 }
 // 批量删除
 const onBatchDelete = () => {

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

@@ -133,7 +133,7 @@ const onDelete = (row: any) => {
 		})
 		.catch(() => {});
 };
-// 获取知识库类型
+// 获取课件分类数据
 const tableRef = ref<RefType>();
 const tableData = ref<EmptyArrayType>([]);
 const list = ref<any[]>([]);

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

@@ -0,0 +1,143 @@
+<template>
+	<el-dialog title="选择课件" v-model="dialogVisible" draggable append-to-body destroy-on-close>
+		<el-form :model="state.queryParams" ref="ruleDialogFormRef" inline @submit.native.prevent :disabled="state.loading">
+			<el-form-item label="课件标题" prop="name">
+				<el-input v-model="state.queryParams.name" placeholder="课件标题" clearable @keyup.enter="handleQuery" class="keyword-input" />
+			</el-form-item>
+			<el-form-item>
+				<el-button type="primary" @click="handleQuery" :loading="state.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"
+			max-height="500px"
+			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="200"></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="tsx" setup>
+import { defineAsyncComponent, reactive, ref } from 'vue';
+import { FormInstance } from 'element-plus';
+import { formatDate } from '@/utils/formatTime';
+import {getCoursewareData} from '@/api/courseware/index';
+
+const pagination = defineAsyncComponent(() => import('@/components/ProTable/components/Pagination.vue')); // 分页
+
+// 定义子组件向父组件传值/事件
+const emit = defineEmits(['choose']);
+
+// 定义变量内容
+const state = reactive({
+	tableData: [], // 表格数据
+	total: 0, // 总条数
+	loading: false, // 加载
+	queryParams: {
+		PageIndex: 1, // 当前页
+		PageSize: 10, // 每页条数
+		name: null, // 关键字
+	},
+	tableCheckbox: '', // 选择的ID
+});
+const ruleDialogFormRef = ref<FormInstance>(); // 表单
+const dialogVisible = ref(false); // 弹窗
+
+// 打开弹窗
+const openDialog = async (row: any[]) => {
+	dialogVisible.value = true;
+    console.log(row)
+	try {
+        if (row) {
+			state.tableCheckbox = row;
+		} else {
+			state.tableCheckbox = '';
+		}
+		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;
+	getCoursewareData(state.queryParams)
+		.then((res: any) => {
+			state.tableData = res.result?.items ?? [];
+			state.total = res.result?.total ?? 0;
+			state.loading = false;
+            const rows = [];
+            state.tableCheckbox.forEach(item => {
+                rows.push(state.tableData.find((it) => it.id === item.sourcewareId));
+            })
+            tableRef.value.setCheckboxRow(rows, true);
+		})
+		.catch(() => {
+			state.loading = false;
+		});
+};
+const resetQuery = (formEl: FormInstance | undefined) => {
+	if (!formEl) return;
+	formEl.resetFields();
+	state.queryParams.name = null;
+	handleQuery();
+};
+// 单个复选框点击
+const checkboxChangeEvent = ({row}) => {
+    let blog = 'add';
+    state.tableCheckbox.forEach(item => {
+        if(item.sourcewareId == row.id){
+            blog = 'delete'
+        }
+    });
+    if (blog == 'add'){
+        state.tableCheckbox.push({sourcewareId: row.id, name: row.name})
+    }else {
+        state.tableCheckbox = state.tableCheckbox.filter(it => it.sourcewareId !== row.id )
+    }
+    console.log(state.tableCheckbox);
+}
+// 关闭弹窗
+const closeDialog = () => {
+	dialogVisible.value = false;
+};
+// 选择课件
+const onSubmit = () => {
+	emit('choose', state.tableCheckbox);
+	closeDialog();
+};
+//暴漏变量和方法
+defineExpose({ closeDialog, openDialog });
+</script>

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

@@ -0,0 +1,148 @@
+<template>
+	<el-dialog title="选择知识" v-model="dialogVisible" draggable append-to-body destroy-on-close>
+		<el-form :model="state.queryParams" ref="ruleDialogFormRef" inline @submit.native.prevent :disabled="state.loading">
+			<el-form-item label="知识标题" prop="Keyword">
+				<el-input v-model="state.queryParams.Keyword" placeholder="知识标题" clearable @keyup.enter="handleQuery" class="keyword-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"
+			max-height="500px"
+			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="title" title="知识标题" min-width="200"></vxe-column>
+			<vxe-column field="creationTime" title="创建时间" width="160">
+				<template #default="{ row }">
+					<span>{{ formatDate(row.creationTime, 'YYYY-mm-dd HH:MM:SS') }}</span>
+				</template>
+			</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="tsx" setup>
+import { defineAsyncComponent, reactive, ref } from 'vue';
+import { FormInstance } from 'element-plus';
+import { formatDate } from '@/utils/formatTime';
+import { knowledgeRetrieval } from '@/api/knowledge/retrieval';
+
+const pagination = defineAsyncComponent(() => import('@/components/ProTable/components/Pagination.vue')); // 分页
+
+// 定义子组件向父组件传值/事件
+const emit = defineEmits(['choose']);
+
+// 定义变量内容
+const state = reactive({
+	tableData: [], // 表格数据
+	total: 0, // 总条数
+	loading: false, // 加载
+	queryParams: {
+		PageIndex: 1, // 当前页
+		PageSize: 10, // 每页条数
+		Keyword: null, // 关键字
+		RetrievalType: '1', // 标题
+	},
+	tableCheckbox: [], // 选择的知识
+});
+const ruleDialogFormRef = ref<FormInstance>(); // 表单
+const dialogVisible = ref(false); // 弹窗
+
+// 打开弹窗
+const openDialog = async (row: any[]) => {
+	dialogVisible.value = true;
+	try {
+        if (row) {
+			state.tableCheckbox = row;
+		} else {
+			state.tableCheckbox = [];
+		}
+		queryList();
+	} catch (error) {
+		state.loading = false;
+	}
+};
+/** 搜索按钮操作 */
+const handleQuery = () => {
+	state.queryParams.PageIndex = 1;
+	queryList();
+};
+const tableRef = ref<RefType>();
+const queryList = () => {
+	state.loading = true;
+	knowledgeRetrieval(state.queryParams)
+		.then((res: any) => {
+			state.tableData = res.result?.items ?? [];
+            console.log(state.tableData)
+			state.total = res.result?.total ?? 0;
+			state.loading = false;
+            const rows = [];
+            state.tableCheckbox.forEach(item => {
+                rows.push(state.tableData.find((it) => it.id === item.knowladgeId));
+            })
+            tableRef.value.setCheckboxRow(rows, true);
+		})
+		.catch(() => {
+			state.loading = false;
+		});
+};
+const resetQuery = (formEl: FormInstance | undefined) => {
+	if (!formEl) return;
+	formEl.resetFields();
+	state.queryParams.Keyword = null;
+	handleQuery();
+};
+// 单个复选框点击
+const checkboxChangeEvent = ({row}) => {
+    let blog = 'add';
+    state.tableCheckbox.forEach(item => {
+        if(item.knowladgeId == row.id){
+            blog = 'delete'
+        }
+    });
+    if (blog == 'add'){
+        state.tableCheckbox.push({knowladgeId: row.id, title: row.title})
+    }else {
+        state.tableCheckbox = state.tableCheckbox.filter(it => it.knowladgeId !== row.id )
+    }
+    console.log(state.tableCheckbox);
+}
+// 关闭弹窗
+const closeDialog = () => {
+	dialogVisible.value = false;
+};
+// 选择知识
+const onSubmit = () => {
+	emit('choose', state.tableCheckbox);
+	closeDialog();
+};
+//暴漏变量和方法
+defineExpose({ closeDialog, openDialog });
+</script>

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

@@ -0,0 +1,528 @@
+<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="questionType" :rules="[{ required: true, message: '请选择试题类型', trigger: 'change' }]" >
+                                    <el-radio-group v-model="state.ruleForm.questionType">
+                                        <el-radio :value="0">单选题</el-radio>
+                                        <el-radio :value="1">多选题</el-radio>
+                                        <el-radio :value="2">判断题</el-radio>
+                                        <el-radio :value="3">填空题</el-radio>
+                                        <el-radio :value="4">问答题</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="questionTagIds" :rules="[{ required: true, message: '请选择试题标签', trigger: 'change' }]">
+									<el-tree-select
+										v-model="state.ruleForm.questionTagIds"
+										:data="state.tagData"
+										node-key="id"
+										:props="{ label: 'name' }"
+										filterable
+										multiple
+										collapse-tags
+										collapse-tags-tooltip
+										:max-collapse-tags="2"
+										:render-after-expand="false"
+										style="width: 100%"
+										ref="treeSelectRef"
+										@change="selQuestionTag"
+									/>
+								</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="difficultyLevel" :rules="[{ required: true, message: '请选择难易程度', trigger: 'change' }]">
+                                    <el-select v-model="state.ruleForm.difficultyLevel" clearable placeholder="请选择难易程度">
+                                        <el-option key='0' label="容易" :value="0" />
+                                        <el-option key='1' label="适中" :value="1" />
+                                        <el-option key='2' label="困难" :value="2" />
+                                    </el-select>
+								</el-form-item>
+							</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="formalEnable" :rules="[{ required: true, message: '请选择是否正式可用', trigger: 'change' }]">
+                                    <el-radio-group v-model="state.ruleForm.formalEnable">
+                                        <el-radio :value="true">是</el-radio>
+                                        <el-radio :value="false">否</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="simulateEnable" :rules="[{ required: true, message: '请选择是否模拟可用', trigger: 'change' }]">
+                                    <el-radio-group v-model="state.ruleForm.simulateEnable">
+                                        <el-radio :value="true">是</el-radio>
+                                        <el-radio :value="false">否</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="knowledgeTitle" :rules="[{ required: false, message: '请选择关联知识', trigger: 'change' }]">
+									<div class="flex-center-align w100">
+										<el-input v-model="state.ruleForm.knowledgeTitle" placeholder="请选择关联知识" style="flex: 1" clearable readonly>
+											<template #append>
+												<el-button @click="onKnowledgeTo">选择知识</el-button>
+											</template>
+										</el-input>
+										<el-button class="ml10" @click="clearKnowledge" type="danger">清空</el-button>
+									</div>
+								</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="courseTitle" :rules="[{ required: false, message: '请选择关联课件', trigger: 'change' }]">
+									<div class="flex-center-align w100">
+										<el-input v-model="state.ruleForm.courseTitle" placeholder="请选择关联课件" style="flex: 1" clearable readonly>
+											<template #append>
+												<el-button @click="onCourseTo">选择课件</el-button>
+											</template>
+										</el-input>
+										<el-button class="ml10" @click="clearCourse" type="danger">清空</el-button>
+									</div>
+								</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="title" :rules="[{ required: true, message: '请输入题干', trigger: 'blur' }]">
+                                    <el-input type="textarea" :rows="3" v-model="state.ruleForm.title" placeholder="请输入题干" clearable></el-input>
+								</el-form-item>
+							</template>
+						</el-skeleton>
+                        <div v-if="[0].includes(state.ruleForm.questionType)">
+							<el-radio-group v-model="state.ruleForm.radioKey" style="display: block;">
+								<el-skeleton :loading="state.loading" animated v-for="(item, index) in state.ruleForm.questionOptionsDtos" :key="item.temp">
+									<template #template>
+										<el-form-item label="选项">
+											<el-skeleton-item variant="h1" />
+										</el-form-item>
+									</template>
+									<template #default>
+										<el-form-item label="选项" :prop="`questionOptionsDtos.${index}.content`" :rules="[{required: true, message: '请输入选项', trigger: 'blur' }]">
+											<el-row :gutter="0" justify="space-between" style="width: 100%;">
+												<el-col :span="17">
+													<el-input v-model="item.content" placeholder="请输入选项" clearable>
+													<template #append>
+														<el-radio label="答案" :value="item.temp" />
+													</template>
+												</el-input>
+												</el-col>
+												<el-col :span="7" style="text-align: right">
+													<el-button type="primary" size="small" v-if="index == 0" @click="addOption"> <SvgIcon name="ele-Plus" class="mr5" /> </el-button>
+													<el-button size="small" class="default-button" @click="deleteOption(item.temp)" > <SvgIcon name="ele-Delete" class="mr5" /></el-button>
+												</el-col>
+											</el-row>
+										</el-form-item>
+									</template>
+								</el-skeleton>
+							</el-radio-group>
+                        </div>
+						<div v-else-if="[1].includes(state.ruleForm.questionType)">
+							<el-checkbox-group v-model="state.ruleForm.checkBoxKey">
+								<el-skeleton :loading="state.loading" animated v-for="(item, index) in state.ruleForm.questionOptionsDtos" :key="item.temp">
+									<template #template>
+										<el-form-item label="选项">
+											<el-skeleton-item variant="h1" />
+										</el-form-item>
+									</template>
+									<template #default>
+										<el-form-item label="选项" :prop="`questionOptionsDtos.${index}.content`" :rules="[{required: true, message: '请输入选项', trigger: 'blur' }]">
+											<el-row :gutter="0" justify="space-between" style="width: 100%;">
+												<el-col :span="17">
+													<el-input v-model="item.content" placeholder="请输入选项" clearable>
+														<template #append>
+															<el-checkbox label="答案" :value="item.temp" />
+														</template>
+													</el-input>
+												</el-col>
+												<el-col :span="7" style="text-align: right">
+													<el-button type="primary" size="small" v-if="index == 0" @click="addOption"> <SvgIcon name="ele-Plus" class="mr5" /> </el-button>
+													<el-button size="small" class="default-button" @click="deleteOption(item.temp)"> <SvgIcon name="ele-Delete" class="mr5" /></el-button>
+												</el-col>
+											</el-row>
+										</el-form-item>
+									</template>
+								</el-skeleton>
+							</el-checkbox-group>
+                        </div>
+                        <div v-else-if="[2].includes(state.ruleForm.questionType)">
+                            <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="judgeKey" :rules="[{ required: true, message: '请选择答案', trigger: 'change' }]">
+										<el-radio-group v-model="state.ruleForm.judgeKey">
+											<el-radio label="错误" :value="false"></el-radio>
+											<el-radio label="正确" :value="true"></el-radio>
+										</el-radio-group>
+									</el-form-item>
+								</template>
+							</el-skeleton>
+                        </div>
+                        <div v-else-if="[3,4].includes(state.ruleForm.questionType)">
+                            <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="`questionAnswerDto.answer`" :rules="[{ required: true, message: '请输入答案', trigger: 'blur' }]">
+										<el-input type="textarea" :rows="3" v-model="state.ruleForm.questionAnswerDto.answer" placeholder="请输入答案" clearable></el-input>
+									</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="remark">
+                                    <el-input type="textarea" :rows="3" v-model="state.ruleForm.remark" 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="weight" :rules="[{ required: true, message: '请填写排序', trigger: 'blur' }]">
+                                    <el-input v-model="state.ruleForm.weight" placeholder="请填写排序" clearable></el-input>
+                                </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>
+
+		<!-- 关联课件 -->
+		<question-course ref="QuestionCourseRef" @choose="chooseCourse" />
+		<!-- 关联知识库 -->
+		<question-knowledge ref="QuestionKnowledgeRef" @choose="chooseKnowledge" />
+	</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 { examTagTreeList } from '@/api/examTrain/tag';
+import {getCoursewareData} from '@/api/courseware/index';
+import { addQuestion, editQuestion, getQuestionDetail } from '@/api/examTrain/questionBank';
+// 引入组件
+const QuestionCourse = defineAsyncComponent(() => import('@/views/examTrain/questionBank/components/Question-course.vue')); // 关联课件
+const QuestionKnowledge = defineAsyncComponent(() => import('@/views/examTrain/questionBank/components/Question-knowledge.vue')); // 关联知识
+
+// 定义变量内容
+const state = reactive<any>({
+	dialogVisible: false,
+	ruleForm: {
+		title: '', // 题干
+		questionType: 0, // 题型
+		difficultyLevel: null, // 难易程度
+		formalEnable: false, // 正式可用
+		simulateEnable: false, // 模拟可用
+		remark: '', // 备注
+		weight: '', // 排序
+		questionTagIds: [], // 选中的标签id
+		questionTagDtos: [], // 试题标签
+		questionAnswerDto: {
+			answer: ''
+		}, // 填空、问答答案
+		questionOptionsDtos: [], // 单选、多选、判断 选项
+		courseTitle: '', // 选择的课件标题
+		questionSourcewareDtos: [], // 关联课件
+		knowledgeTitle: '', // 选择的知识库标题
+		questionKnowladgeDtos: [], // 关联知识库
+		radioKey: '', // 单选答案id
+		checkBoxKey: [], // 多选答案id
+		judgeKey: false, // 判断答案value
+	},
+	loading: false,
+    tagData: [], // 试题标签初始化数据
+});
+
+const route = useRoute(); // 获取路由
+const router = useRouter(); // 路由跳转
+const ruleFormRef = ref<any>(); // 表单ref
+
+// 选择试题标签
+const treeSelectRef = ref<RefType>();
+const selQuestionTag = (value: any) => {
+	state.ruleForm.questionTagDtos = [];
+	state.ruleForm.questionTagIds.forEach(item => {
+		let tagObj = other.deepClone(treeSelectRef.value.getNode(item).data);
+		state.ruleForm.questionTagDtos.push({tagId: tagObj.id});
+	})
+}
+// 增加单选多选选项
+const addOption = () => {
+	if ([0,1].includes(state.ruleForm.questionType)){
+		state.ruleForm.questionOptionsDtos.push({temp: guid(), content: '', isAnswer: false})
+	}
+};
+// 删除单选多选选项
+const deleteOption = (id: any) => {
+	if ([0,1].includes(state.ruleForm.questionType)){
+		state.ruleForm.questionOptionsDtos = state.ruleForm.questionOptionsDtos.filter(it => it.temp !== id );
+		if ([0].includes(state.ruleForm.questionType)){
+			state.ruleForm.radioKey = state.ruleForm.radioKey.filter(it => it.temp !== id )
+		}else {
+			state.ruleForm.checkBoxKey = state.ruleForm.checkBoxKey.filter(it => it.temp !== id )
+		}
+	}
+}
+// 选择知识
+const QuestionKnowledgeRef = ref<RefType>();
+const onKnowledgeTo = () => {
+	QuestionKnowledgeRef.value.openDialog(state.ruleForm.questionKnowladgeDtos);
+};
+// 清除选择的知识
+const clearKnowledge = () => {
+	state.ruleForm.questionKnowladgeDtos = [];
+	state.ruleForm.knowledgeTitle = '';
+};
+// 确定选择了知识库
+const chooseKnowledge = (data: any) => {
+	clearKnowledge();
+	if (data) {
+		data.forEach(it => state.ruleForm.questionKnowladgeDtos.push({knowladgeId: it.knowladgeId, title: it.title}));
+		state.ruleForm.knowledgeTitle = data.map(it => it.title).join(',');
+	}
+};
+// 选择课件
+const QuestionCourseRef = ref<RefType>();
+const onCourseTo = () => {
+	QuestionCourseRef.value.openDialog(state.ruleForm.questionSourcewareDtos);
+};
+// 清除选择的课件
+const clearCourse = () => {
+	state.ruleForm.questionSourcewareDtos = [];
+	state.ruleForm.courseTitle = '';
+};
+// 确定选择了课件
+const chooseCourse = (data: any) => {
+	clearCourse();
+	if (data) {
+		data.forEach(it => state.ruleForm.questionSourcewareDtos.push({sourcewareId: it.sourcewareId, name: it.name}));
+		state.ruleForm.courseTitle = data.map(it => it.name).join(',');
+	}
+};
+// 保存提交
+const onSave = throttle(async (formEl: FormInstance | undefined) => {
+	// 表单额外验证
+	if (state.ruleForm.questionType == 0){
+		if (!state.ruleForm.radioKey){
+			ElMessage.warning('请指定正确选项!')
+			return;
+		};
+	}else if (state.ruleForm.questionType == 1){
+		if (state.ruleForm.checkBoxKey.length == 0){
+			ElMessage.warning('请指定正确选项!')
+			return;
+		};
+	}
+	if (!formEl) return;
+	await formEl.validate((valid: boolean) => {
+		if (!valid) return;
+		state.loading = true;
+		const submitObj = other.deepClone(state.ruleForm);
+		if ([0,1,2].includes(state.ruleForm.questionType)){
+			submitObj.questionAnswerDto = null;
+			if (submitObj.questionType == 0){
+				submitObj.questionOptionsDtos.forEach(item => {
+					if (item.temp == submitObj.radioKey){
+						item.isAnswer = true;
+					}else {
+						item.isAnswer = false;
+					}
+				})
+			}else if (submitObj.questionType == 1){
+				submitObj.questionOptionsDtos.forEach(item => {
+					if (submitObj.checkBoxKey.indexOf(item.temp) != -1){
+						item.isAnswer = true;
+					}else {
+						item.isAnswer = false;
+					}
+				})
+			}else {
+				submitObj.questionOptionsDtos = [
+					{temp: guid(), content: '错误', isAnswer: !submitObj.judgeKey},
+					{temp: guid(), content: '正确', isAnswer: submitObj.judgeKey}
+				];
+			}
+		}else{
+			submitObj.questionOptionsDtos = [];
+		}
+		Reflect.deleteProperty(submitObj, 'radioKey');
+		Reflect.deleteProperty(submitObj, 'checkBoxKey');
+		Reflect.deleteProperty(submitObj, 'judgeKey');
+		Reflect.deleteProperty(submitObj, 'questionTagIds');
+		Reflect.deleteProperty(submitObj, 'knowledgeTitle');
+		Reflect.deleteProperty(submitObj, 'courseTitle');
+		console.log(submitObj)
+		if (route.params.id) {
+			editQuestion(submitObj)
+				.then(handleSuccess)
+				.catch(() => {
+					state.loading = false;
+				});
+		} else {
+			// 新增
+			addQuestion(submitObj)
+				.then(handleSuccess)
+				.catch(() => {
+					state.loading = false;
+				});
+		}
+	});
+}, 300);
+// 取消
+const onCancel = () => {
+	state.loading = true;
+	mittBus.emit('onCurrentContextmenuClick', Object.assign({}, { contextMenuClickId: 1, ...route }));
+	mittBus.emit('clearCache', 'questionBank');
+	router.push({
+		path: '/examTrain/questionBank',
+	});
+	state.loading = false;
+};
+const handleSuccess = () => {
+	state.loading = false;
+	ElMessage.success('操作成功');
+	// 关闭当前 tagsView
+	mittBus.emit('onCurrentContextmenuClick', Object.assign({}, { contextMenuClickId: 1, ...route }));
+	mittBus.emit('clearCache', 'questionBank');
+	router.push({
+		path: '/examTrain/questionBank',
+	});
+};
+// 获取标签数据
+const getTagData = async () => {
+	state.loading = true;
+	try {
+		const { result } = await examTagTreeList();
+		state.tagData = result ?? [];
+		state.loading = false;
+	} catch (error) {
+		state.loading = false;
+	}
+};
+const getDetail = async () => {
+	if (route.params.id) {
+		const res: any = await getQuestionDetail(route.params.id); // 试题详情
+		state.ruleForm = res.result ?? {};
+		state.ruleForm.questionTagIds = state.ruleForm.questionTagDtos.map(it => it.tagId);
+		state.ruleForm.knowledgeTitle = state.ruleForm.questionKnowladgeDtos.map(it => it.title).join(',');
+		state.ruleForm.courseTitle = state.ruleForm.questionSourcewareDtos.map(it => it.name).join(',');
+		state.ruleForm.radioKey = '';
+		state.ruleForm.checkBoxKey = [];
+		state.ruleForm.judgeKey = false;
+		if ([0,1,2].includes(state.ruleForm.questionType)){
+			state.ruleForm.questionOptionsDtos.forEach(item => {
+				item.temp = item.id;
+				if (item.isAnswer){
+					if (state.ruleForm.questionType == 0){
+						state.ruleForm.radioKey = item.id;
+					}else if (state.ruleForm.questionType == 1){
+						state.ruleForm.checkBoxKey.push(item.id);
+					}else if (state.ruleForm.questionType == 2){
+						item.content == '正确' ? state.ruleForm.judgeKey = true : state.ruleForm.judgeKey = false
+					}
+				}
+			});
+		}
+	}else {
+		for(let num:number = ref(0); num.value < 3; num.value++){
+			state.ruleForm.questionOptionsDtos.push({temp: guid(), content: '', isAnswer: false});
+		}
+	}
+};
+onMounted(async () => {
+	await getTagData();
+	await getDetail();
+});
+</script>
+<style lang="scss">
+.vtree-tree-drop__wrapper {
+	width: 100%;
+}
+</style>

+ 265 - 0
src/views/examTrain/questionBank/index.vue

@@ -0,0 +1,265 @@
+<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="tagIds">
+                    <el-tree-select
+                        v-model="state.queryParams.tagIds"
+                        :data="state.tagData"
+                        node-key="id"
+                        :props="{ label: 'name' }"
+                        multiple
+                        collapse-tags
+                        collapse-tags-tooltip
+                        :render-after-expand="false"
+                        style="width: 240px"
+                        @change="handleQuery"
+                    />
+                </el-form-item>
+                <el-form-item label="难易程度" prop="difficultyLevel">
+                    <el-select v-model="state.queryParams.difficultyLevel" clearable placeholder="请选择" @change="handleQuery" :disabled="state.tableLoading">
+                        <el-option key='0' label="容易" :value="0" />
+                        <el-option key='1' label="适中" :value="1" />
+                        <el-option key='2' label="困难" :value="2" />
+                    </el-select>
+                </el-form-item>
+                <el-form-item label="题干" prop="title">
+                    <el-input v-model="state.queryParams.title" placeholder="题干" clearable @keyup.enter="handleQuery" class="keyword-input" />
+                </el-form-item>
+                <el-form-item>
+                    <el-button type="primary" @click="handleQuery" :loading="state.tableLoading"> <SvgIcon name="ele-Search" class="mr5" />查询 </el-button>
+                    <el-button @click="resetQuery(ruleFormRef)" class="default-button"> <SvgIcon name="ele-Refresh" class="mr5" />重置</el-button>
+                </el-form-item>
+            </el-form>
+            <vxe-toolbar
+                ref="toolbarRef"
+                :loading="state.tableLoading"
+                custom
+                :refresh="{
+                    queryMethod: handleQuery,
+                }"
+            >
+                <template #buttons>
+                    <el-button type="primary" @click="onAdd" v-auth="'question:index:add'" :loading="state.tableLoading">
+                        <SvgIcon name="ele-Plus" class="mr10" />新增
+                    </el-button>
+                    <el-upload
+                        v-model:file-list="fileList"
+                        action="#"
+                        :multiple="false"
+                        ref="uploadListRef"
+                        name="file"
+                        :http-request="onUpload"
+                        :show-file-list="false"
+                        class="ml10 mr10"
+                        :disabled="state.loading"
+                    >
+                        <el-button type="primary" plain :loading="state.loading" v-auth="'question:index:upload'"> <SvgIcon name="ele-Upload" class="mr5" /> 试题导入 </el-button>
+                    </el-upload>
+                    <el-button type="primary" plain @click="onDownload" v-auth="'question:index:download'" :loading="state.loading">
+                        <SvgIcon name="ele-Download" class="mr5" />模板下载
+                    </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="questionBank"
+                    :custom-config="{ storage: true }"
+                    showHeaderOverflow
+                >
+                    <vxe-column field="questionTypeDesc" title="题型" width="100"></vxe-column>
+                    <vxe-column field="tag" title="标签" width="150"></vxe-column>
+                    <vxe-column field="difficultyLevelDesc" title="难度" width="100"></vxe-column>
+                    <vxe-column field="title" title="题干" min-width="200"></vxe-column>
+                    <vxe-column field="status" title="状态" width="160">
+                        <template #default="{ row }">
+                            <el-switch v-model="row.status" />
+                        </template>
+                    </vxe-column>
+                    <vxe-column field="formalEnableDes" title="正式可用" width="150"></vxe-column>
+                    <vxe-column field="simulateEnableDes" title="模拟可用" width="150"></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="'question:index:edit'">
+                                编辑
+                            </el-button>
+                            <el-button link type="danger" @click="onRowDel(row)" title="删除" v-auth="'question: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>
+	</div>
+</template>
+
+<script lang="tsx" setup name="questionBank">
+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 {deleteQuestion, getQuestionData, questionImport, questionTemplate} from '@/api/examTrain/questionBank';
+import { examTagTreeList} from '@/api/examTrain/tag';
+
+// 引入组件
+const pagination = defineAsyncComponent(() => import('@/components/ProTable/components/Pagination.vue')); // 分页
+
+const router = useRouter(); //路由
+
+// 定义变量内容
+const state = reactive<any>({
+	queryParams: {
+		PageIndex: 1, //页码
+		PageSize: 20, //每页条数
+        tagIds: null, // 标签id集合
+        difficultyLevel: null, // 难易程度
+		title: null, //题干
+	},
+	tagLoading: false, // 标签loading
+	tagData: [], // 标签数据
+	tableData: [], //表格数据
+	total: 0, //总条数
+    loading: false, // 下载上传loading
+	tableLoading: false, //表格loading
+});
+/** 搜索按钮操作 节流操作 */
+const handleQuery = () => {
+	state.queryParams.PageIndex = 1;
+	queryList();
+};
+// 获取标签数据
+const getTagData = async () => {
+	state.tagLoading = true;
+	try {
+		const { result } = await examTagTreeList();
+		state.tagData = result ?? [];
+		state.tagLoading = false;
+	} catch (error) {
+		state.tagLoading = false;
+	}
+};
+/** 获取试题列表 */
+const requestParams = ref<EmptyObjectType>({});
+const queryList = () => {
+	state.tableLoading = true;
+    requestParams.value = Other.deepClone(state.queryParams);
+	requestParams.value.tagId = state.queryParams.tagIds || '';
+	Reflect.deleteProperty(requestParams.value, 'tagIds'); 
+    console.log(requestParams.value)
+	getQuestionData(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 ruleFormRef = ref<RefType>(); // 表单ref
+const resetQuery = (formEl: FormInstance | undefined) => {
+	if (!formEl) return;
+	formEl.resetFields();
+	ruleFormRef.value?.resetFields();
+	handleQuery();
+};
+// 新增
+const onAdd = () => {
+	router.push({
+		name: 'questionEdit',
+		params: {
+			tagsViewName: '新增试题',
+		},
+	});
+};
+// 试题导入
+const fileList = ref<EmptyArrayType>([]);
+const uploadListRef = ref<RefType>(); // 上传组件ref
+const onUpload = async (file: any) => {
+	state.loading = true;
+	let fileObj = file.file; // 相当于input里取得的files
+	let fd = new FormData(); // FormData 对象
+	fd.append('file', fileObj); // 文件对象
+	try {
+        questionImport(fd).then(() => {
+            ElMessage.success('导入成功!');
+		    state.loading = false;
+            queryList();
+        });
+	} catch (e) {
+		console.log(e);
+		state.loading = false;
+	}
+};
+// 模板下载
+const onDownload  = (row: any) => {
+    state.loading = true;
+	questionTemplate().then((res: any) => {
+        state.loading = false;
+        downloadFileByStream(res);
+    })
+    .catch(() => {
+        state.loading = false;
+    });
+};
+// 编辑
+const onEdit  = (row: any) => {
+	router.push({
+		name: 'questionEdit',
+		params: {
+			id: row.id,
+			tagsViewName: '编辑试题',
+		},
+	});
+};
+// 删除当前行
+const onRowDel = (row: any) => {
+	ElMessageBox.confirm(`是否确认删除当前试题?删除后不可恢复`, '提示', {
+		confirmButtonText: '确定',
+		cancelButtonText: '取消',
+		type: 'warning',
+	})
+        .then(() => {
+			deleteQuestion({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);
+	}
+	getTagData();
+});
+</script>

+ 98 - 0
src/views/examTrain/tag/components/Tag-add.vue

@@ -0,0 +1,98 @@
+<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 { addExamTag } 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 = (treeData: any[]) => {
+	state.dialogVisible = true;
+	state.treeData = treeData ?? [];
+};
+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;
+		addExamTag(state.ruleForm)
+			.then(() => {
+				emit('updateList');
+				closeDialog(); // 关闭弹窗
+				ElMessage.success('操作成功');
+				state.loading = false;
+			})
+			.catch(() => {
+				state.loading = false;
+			});
+	});
+};
+// 暴露变量
+defineExpose({
+	openDialog,
+	closeDialog,
+});
+</script>

+ 108 - 0
src/views/examTrain/tag/components/Tag-edit.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>

+ 181 - 0
src/views/examTrain/tag/index.vue

@@ -0,0 +1,181 @@
+<template>
+	<div class="plan-type-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.loading">
+				<el-form-item label="标签名称" prop="keyword" class="mb0">
+					<el-input v-model="state.queryParams.keyword" placeholder="标签名称" clearable @keyup.enter="handleQuery" class="keyword-input" />
+				</el-form-item>
+				<el-form-item class="mb0">
+					<el-button type="primary" @click="handleQuery" :loading="state.loading"> <SvgIcon name="ele-Search" class="mr5" />查询 </el-button>
+					<el-button @click="resetQuery(ruleFormRef)" class="default-button" :loading="state.loading">
+						<SvgIcon name="ele-Refresh" class="mr5" />重置
+					</el-button>
+				</el-form-item>
+			</el-form>
+			<vxe-toolbar
+				ref="toolbarRef"
+				:loading="state.loading"
+				custom
+				:refresh="{
+					queryMethod: handleQuery,
+				}"
+			>
+				<template #buttons>
+					<el-button type="primary" @click="onOpenAdd" v-auth="'examTrain:tag:add'"> <SvgIcon name="ele-Plus" class="mr5" /> 新增标签 </el-button>
+				</template>
+			</vxe-toolbar>
+			<div style="overflow: hidden; width: 100%; height: 100%; flex: 1">
+				<vxe-table
+					:loading="state.loading"
+					:data="list"
+					:column-config="{ resizable: true, useKey: true }"
+					:row-config="{ isCurrent: true, isHover: true, useKey: true, height: 36 }"
+					ref="tableRef"
+					height="auto"
+					auto-resize
+					show-overflow
+					:scrollY="{ enabled: true, gt: 0 }"
+					id="examTag"
+					:custom-config="{ storage: true }"
+					showHeaderOverflow
+					:tree-config="{
+						childrenField: 'children',
+						transform: true,
+						rowField: 'id',
+						parentField: 'parentId',
+					}"
+				>
+					<vxe-column field="name" title="标签名称" min-width="400" treeNode>
+						<template #default="{ row }">
+							<p style="display: flex; align-items: center">
+								<span class="pl5" v-html="row.name"></span>
+							</p>
+						</template>
+					</vxe-column>
+					<vxe-column title="操作" fixed="right" width="180" align="center" :show-overflow="false">
+						<template #default="{ row }">
+							<el-button link type="primary" @click="onOpenEdit(row)" title="编辑" v-auth="'examTrain:tag:edit'"> 编辑 </el-button>
+							<el-button link type="danger" @click="onDelete(row)" title="删除" v-auth="'examTrain:tag:delete'"> 删除 </el-button>
+						</template>
+					</vxe-column>
+				</vxe-table>
+			</div>
+		</div>
+		<tag-add ref="tagAddRef" @updateList="queryList" />
+		<tag-edit ref="tagEditRef" @updateList="queryList" />
+	</div>
+</template>
+<script lang="tsx" setup name="examTag">
+import { defineAsyncComponent, nextTick, onMounted, reactive, ref } from 'vue';
+import type { FormInstance } from 'element-plus';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import { throttle } from '@/utils/tools';
+import XEUtils from 'xe-utils';
+import { examTagTreeList, delExamTag } from '@/api/examTrain/tag';
+// 引入组件
+const TagAdd = defineAsyncComponent(() => import('@/views/examTrain/tag/components/Tag-add.vue')); // 新增组件
+const TagEdit = defineAsyncComponent(() => import('@/views/examTrain/tag/components/Tag-edit.vue')); // 修改组件
+// 定义变量内容
+const ruleFormRef = ref<RefType>(); // 表单ref
+const state = reactive<any>({
+	loading: false, // 加载状态
+	queryParams: {
+		// 查询参数
+		keyword: null,
+	},
+});
+/** 搜索按钮操作 节流操作 */
+const handleQuery = () => {
+	queryList();
+};
+/** 重置按钮操作 */
+const resetQuery = throttle((formEl: FormInstance | undefined) => {
+	if (!formEl) return;
+	formEl.resetFields();
+	queryList();
+}, 300);
+// 打开新增分类弹窗
+const tagAddRef = ref<RefType>(); // 新增ref
+const onOpenAdd = () => {
+	tagAddRef.value.openDialog(tableData.value);
+};
+// 打开编辑分类弹窗
+const tagEditRef = ref<RefType>(); // 修改ref
+const onOpenEdit = (row: any) => {
+	tagEditRef.value.openDialog(row, tableData.value);
+};
+
+// 删除当前行
+const onDelete = (row: any) => {
+  const typeName = row.name.replace(/<span class="keyword-highlight">(.*?)<\/span>/g, '$1');
+	ElMessageBox.confirm(`是否确认删除:${typeName}?删除后不可恢复`, '提示', {
+		confirmButtonText: '确定',
+		cancelButtonText: '取消',
+		type: 'warning',
+	})
+		.then(() => {
+			delExamTag(row.id).then(() => {
+				ElMessage.success('删除成功');
+				queryList();
+			});
+		})
+		.catch(() => {});
+};
+// 获取标签树形数据
+const tableRef = ref<RefType>();
+const tableData = ref<EmptyArrayType>([]);
+const list = ref<any[]>([]);
+const queryList = () => {
+	state.loading = true;
+	examTagTreeList()
+		.then((res: any) => {
+			tableData.value = res?.result ?? [];
+			const filterVal = XEUtils.toValueString(state.queryParams.keyword)?.trim().toLowerCase();
+			if (filterVal) {
+				const filterRE = new RegExp(filterVal, 'gi');
+				const options = { children: 'children' };
+				const searchProps = ['name'];
+				// 搜索为克隆数据,不会污染源数据
+				const rest = XEUtils.searchTree(
+					tableData.value,
+					(item) => searchProps.some((key) => String(item[key]).toLowerCase().indexOf(filterVal) > -1),
+					options
+				);
+				XEUtils.eachTree(
+					rest,
+					(item) => {
+						searchProps.forEach((key) => {
+							item[key] = String(item[key]).replace(filterRE, (match) => `<span class="keyword-highlight">${match}</span>`);
+						});
+					},
+					options
+				);
+				list.value = rest;
+				list.value = XEUtils.toTreeArray(list.value);
+				nextTick(() => {
+					// 搜索之后默认展开所有子节点
+					if (tableRef.value) {
+						tableRef.value.setAllTreeExpand(true);
+					}
+				});
+			} else {
+				list.value = tableData.value;
+				list.value = XEUtils.toTreeArray(list.value);
+			}
+
+			state.loading = false;
+		})
+		.catch((err) => {
+			console.log(err);
+			state.loading = false;
+		});
+};
+// 页面加载时
+const toolbarRef = ref<RefType>();
+onMounted(() => {
+	queryList();
+	if (tableRef.value && toolbarRef.value) {
+		tableRef.value.connect(toolbarRef.value);
+	}
+});
+</script>