瀏覽代碼

reactor:对接案例库;

zhangchong 3 月之前
父節點
當前提交
b5911b3b5f

+ 190 - 0
src/api/case/index.ts

@@ -0,0 +1,190 @@
+/*
+ * @Author: zc
+ * @description 案例库 - 案例管理
+ */
+import request from '@/utils/request';
+/**
+ * @description 获取案例库列表基础信息
+ * @param {object} params
+ */
+export const getCaseBaseData = (params?: object) => {
+	return request({
+		url: '/api/v1/Case/list/status-data',
+		method: 'get',
+		params,
+	});
+};
+/**
+ * @description 获取案例库列表
+ * @param {object} params
+ */
+export const getCaseList = (params?: object) => {
+	return request({
+		url: '/api/v1/Case/list',
+		method: 'get',
+		params,
+	});
+};
+/**
+ * @description 获取案例库列表导出
+ * @param {object} data
+ */
+export const getCaseExport = (data: object) => {
+	return request(
+		{
+			url: '/api/v1/Case/export',
+			method: 'post',
+			data,
+			responseType: 'blob',
+		},
+		{
+			reduce_data_format: false,
+		}
+	);
+};
+/**
+ * @description 新增案例库草稿
+ * @param {object} data
+ */
+export const addCaseDraft = (data: object) => {
+	return request({
+		url: '/api/v1/Case/list/draft',
+		method: 'post',
+		data,
+	});
+};
+/**
+ * @description 新增案例库草稿直接到审核
+ * @param {object} data
+ */
+export const addCaseDraftAudit = (data: object) => {
+	return request({
+		url: '/api/v1/Case/list/add',
+		method: 'post',
+		data,
+	});
+};
+/**
+ * @description 案例库详情
+ * @param {object} params
+ */
+export const caseDetail = (params: object) => {
+	return request({
+		url: '/api/v1/Case/list/info',
+		method: 'get',
+		params,
+	});
+};
+/**
+ * @description 案例库编辑草稿
+ * @param {object} data
+ */
+export const editCaseDraft = (data: object) => {
+	return request({
+		url: '/api/v1/Case/list/draftupdate',
+		method: 'put',
+		data,
+	});
+};
+/**
+ * @description 案例库编辑草稿到审核
+ * @param {object} data
+ */
+export const editCaseAudit = (data: object) => {
+	return request({
+		url: '/api/v1/Case/list/update',
+		method: 'put',
+		data,
+	});
+};
+/**
+ * @description 案例库评分
+ * @param {object} data
+ */
+export const caseScore = (data: object) => {
+	return request({
+		url: '/api/v1/Case/list/score',
+		method: 'put',
+		data,
+	});
+};
+/**
+ * @description 案例库审核
+ * @param {object} data
+ */
+export const caseAudit = (data: object) => {
+	return request({
+		url: '/api/v1/Case/list/examin',
+		method: 'put',
+		data,
+	});
+};
+/**
+ * @description 案例查重
+ * @param {object} data
+ */
+export const caseRepeat = (data: object) => {
+	return request({
+		url: '/api/v1/Case/list/exist',
+		method: 'post',
+		data,
+	});
+};
+/**
+ * @description 案例库详情导出
+ * @param {object} data
+ */
+export const caseDetailExport = (data: object) => {
+	return request(
+		{
+			url: '/api/v1/Case/list/info/export',
+			method: 'post',
+			data,
+			responseType: 'blob',
+		},
+		{
+			reduce_data_format: false,
+		}
+	);
+};
+/**
+ * @description 案例库删除提交到审核
+ * @param {object} data
+ */
+export const caseDelAudit = (data: object) => {
+	return request({
+		url: '/api/v1/Case/list/remove',
+		method: 'delete',
+		data,
+	});
+};
+/**
+ * @description 案例库上架
+ * @param {string} id
+ */
+export const caseOnShelf = (id: string) => {
+	return request({
+		url: `/api/v1/Case/list/onshelf/${id}`,
+		method: 'get',
+	});
+};
+/**
+ * @description 案例库下架
+ * @param {string} id
+ */
+export const caseOffShelf = (id: string) => {
+	return request({
+		url: `/api/v1/Case/list/offshelf/${id}`,
+		method: 'get',
+	});
+};
+/**
+ * @description 查询案例库申请理由
+ * @param {string} id
+ */
+export const caseReason = (id: string) => {
+	return request({
+		url: `/api/v1/Case/list/reason/${id}`,
+		method: 'get',
+	});
+};

+ 29 - 0
src/api/case/retrieval.ts

@@ -0,0 +1,29 @@
+/*
+ * @Author: zc
+ * @description 案例库 - 案例检索
+ */
+import request from '@/utils/request';
+/**
+ * @description 检索列表
+ * @param params
+ * @return {*}
+ */
+export const caseRetrieval = (params?: object) =>{
+	return request({
+		url: '/api/v1/Case/search',
+		method: 'get',
+		params,
+	});
+}
+/**
+ * @description 案例检索-top10
+ * @param params
+ * @return {*}
+ */
+export const caseRetrievalTop10 = (params?: object) =>{
+	return request({
+		url: '/api/v1/Case/search/top10',
+		method: 'get',
+		params,
+	});
+}

+ 60 - 0
src/api/case/type.ts

@@ -0,0 +1,60 @@
+/*
+ * @Author: zc
+ * @description 案例库 - 类型管理
+ */
+import request from '@/utils/request';
+/**
+ * @description 案例管理类型树形
+ * @param {object} params 如果传入参数 IsEnable 表示查询已启用
+ */
+
+export const caseTreeList = (params?: object) => {
+	return request({
+		url: '/api/v1/Case/type/treelist',
+		method: 'get',
+		params,
+	});
+};
+/**
+ * @description 新增案例库类型
+ * @param {object} data
+ */
+
+export const addCaseType = (data: object) => {
+	return request({
+		url: '/api/v1/Case/type/add',
+		method: 'post',
+		data,
+	});
+};
+/**
+ * @description 编辑案例库类型
+ * @param {object} data
+ */
+export const updateCaseType = (data: object) => {
+	return request({
+		url: '/api/v1/Case/type/update',
+		method: 'put',
+		data,
+	});
+};
+/**
+ * @description 查询案例库类型详情
+ * @param {object} Id
+ */
+export const caseTypeDetail = (Id: string) => {
+	return request({
+		url: `/api/v1/Case/type/info/${Id}`,
+		method: 'get',
+	});
+};
+/**
+ * @description 删除案例库类型
+ * @param {object} Id
+ */
+export const delCaseType = (Id: string) => {
+	return request({
+		url: `/api/v1/Case/type/remove/${Id}`,
+		method: 'delete',
+	});
+};

+ 21 - 1
src/router/route.ts

@@ -530,7 +530,7 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
 		name: 'planEdit',
 		component: () => import('@/views/plan/index/edit.vue'),
 		meta: {
-			title: '库新增/编辑',
+			title: '预案库新增/编辑',
 			isKeepAlive: true,
 			isDynamic: true,
 		},
@@ -545,6 +545,26 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
 			isDynamic: true,
 		},
 	},
+	{
+		path: '/case/index/edit/:tagsViewName/:id?',
+		name: 'caseEdit',
+		component: () => import('@/views/case/index/edit.vue'),
+		meta: {
+			title: '案例库新增/编辑',
+			isKeepAlive: true,
+			isDynamic: true,
+		},
+	},
+	{
+		path: '/case/index/preview/:tagsViewName/:id/:isAddPv?',
+		name: 'casePreview',
+		component: () => import('@/views/case/index/preview.vue'),
+		meta: {
+			title: '案例查看',
+			isKeepAlive: true,
+			isDynamic: true,
+		},
+	},
 ];
 /**
  * 定义404、401界面

+ 135 - 0
src/views/case/index/components/Case-audit.vue

@@ -0,0 +1,135 @@
+<template>
+	<el-dialog :title="dialogTitle" v-model="state.dialogVisible" draggable append-to-body destroy-on-close @close="close">
+		<el-form :model="state.ruleForm" ref="ruleFormRef" label-width="80px" v-loading="state.loading">
+			<el-form-item :label="textTip" v-if="[1, 2].includes(planInfo.applyStatus)">{{ applyReason }}</el-form-item>
+			<el-form-item label="审批结果" prop="state" :rules="[{ required: true, message: '请选择审批结果', trigger: 'change' }]">
+				<el-radio-group v-model="state.ruleForm.state">
+					<el-radio :value="1">同意</el-radio>
+					<el-radio :value="0">不同意</el-radio>
+				</el-radio-group>
+			</el-form-item>
+			<el-form-item label="审批意见" prop="examinOpinion" :rules="[{ required: true, message: '请填写审批意见', trigger: 'blur' }]">
+				<el-input v-model="state.ruleForm.examinOpinion" type="textarea" :autosize="{ minRows: 6, maxRows: 10 }" placeholder="请填写审批意见">
+				</el-input>
+			</el-form-item>
+		</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 { computed, reactive, ref } from 'vue';
+import { ElMessage, FormInstance } from 'element-plus';
+import { caseAudit, caseReason } from '@/api/case';
+
+// 定义子组件向父组件传值/事件
+const emit = defineEmits(['updateList']);
+
+// 定义变量内容
+const state = reactive<any>({
+	dialogVisible: false, // 弹窗
+	ruleForm: {
+		state: 1, // 默认同意
+		examinOpinion: null, //审核意见
+	},
+	loading: false, // 加载
+});
+const planType = ref('update');
+const textTip = computed(() => {
+	if (planType.value === 'update') {
+		return '更新说明';
+	} else if (planType.value === 'delete') {
+		return '删除说明';
+	}
+});
+// 查询申请审批意见
+const applyReason = ref<any>('');
+const getTips = async (row: any) => {
+	try {
+		const { result } = await caseReason(row.id);
+		applyReason.value = result.applyReason;
+	} catch (e) {
+		console.log(e);
+	}
+};
+// 打开弹窗
+const ruleFormRef = ref<any>(); // 表单ref
+const planInfo = ref<any>();
+const dialogTitle = ref('预案更新审批');
+/*[
+	{
+		"key": -1,
+		"value": "全部"
+	},
+	{
+		"key": 0,
+		"value": "新增审核"
+	},
+	{
+		"key": 1,
+		"value": "修改审核"
+	},
+	{
+		"key": 2,
+		"value": "删除审核"
+	}
+]*/
+const openDialog = (row: any) => {
+	switch (row.applyStatus) {
+		case 0:
+			dialogTitle.value = '预案新增审批';
+			break;
+		case 1:
+			planType.value = 'update';
+			dialogTitle.value = '预案修改审批';
+			break;
+		case 2:
+			planType.value = 'delete';
+			dialogTitle.value = '预案删除审批';
+			break;
+	}
+	planInfo.value = row;
+	getTips(row);
+	state.dialogVisible = true;
+};
+const close = () => {
+	ruleFormRef.value?.clearValidate();
+	ruleFormRef.value?.resetFields();
+};
+// 关闭弹窗
+const closeDialog = () => {
+	state.dialogVisible = false;
+};
+// 新增
+const onSubmit = async (formEl: FormInstance | undefined) => {
+	if (!formEl) return;
+	await formEl.validate((valid: boolean) => {
+		if (!valid) return;
+		state.loading = true;
+		caseAudit({
+			id: planInfo.value.id,
+			state: state.ruleForm.state,
+			examinOpinion: state.ruleForm.examinOpinion,
+		})
+			.then(() => {
+				emit('updateList');
+				closeDialog(); // 关闭弹窗
+				ElMessage.success('审核成功');
+				state.loading = false;
+			})
+			.catch(() => {
+				state.loading = false;
+			});
+	});
+};
+// 暴露变量
+defineExpose({
+	openDialog,
+	closeDialog,
+});
+</script>

+ 114 - 0
src/views/case/index/components/Edit-submit-audit.vue

@@ -0,0 +1,114 @@
+<template>
+	<el-dialog :title="dialogTitle" v-model="state.dialogVisible" width="30%" draggable append-to-body destroy-on-close @close="close">
+		<el-form :model="state.ruleForm" ref="ruleFormRef" label-width="80px" v-loading="state.loading">
+			<el-form-item :label="textTip" prop="content" :rules="[{ required: true, message: '请填写'+textTip, trigger: 'blur' }]">
+				<el-input
+					v-model="state.ruleForm.content"
+					type="textarea"
+					:autosize="{ minRows: 6, maxRows: 10 }"
+					:placeholder="'请填写'+textTip"
+				>
+				</el-input>
+			</el-form-item>
+		</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 { computed, reactive, ref } from 'vue';
+import { ElMessage, FormInstance } from 'element-plus';
+import { caseDelAudit, editCaseAudit } from '@/api/case';
+
+// 定义子组件向父组件传值/事件
+const emit = defineEmits(['updateList']);
+
+// 定义变量内容
+const state = reactive<any>({
+	dialogVisible: false, // 弹窗
+	ruleForm: {
+		name: '', // 类型名称
+		sort: 0, // 排序
+		parentId: '', // 上级类型
+	},
+	loading: false, // 加载
+	planInfo:{}, // 预案详情内容
+});
+const planType = ref('update');
+const dialogTitle = computed(()=>{
+	if(planType.value==='update'){
+		return '更新预案'
+	}else if(planType.value==='delete'){
+		return '删除预案'
+	}
+})
+const textTip = computed(()=>{
+	if(planType.value==='update'){
+		return '更新说明'
+	}else if(planType.value==='delete'){
+		return '删除说明'
+	}
+})
+// 打开弹窗
+const ruleFormRef = ref<any>(); // 表单ref
+const openDialog = (row:any,type='update') => {
+	state.planInfo = row;
+	planType.value = type;
+	state.dialogVisible = true;
+};
+const close = () => {
+	ruleFormRef.value?.clearValidate();
+	ruleFormRef.value?.resetFields();
+};
+// 关闭弹窗
+const closeDialog = () => {
+	state.dialogVisible = false;
+};
+// 新增
+const onSubmit = async (formEl: FormInstance | undefined) => {
+	if (!formEl) return;
+	await formEl.validate((valid: boolean) => {
+		if (!valid) return;
+		state.loading = true;
+		if(planType.value==='update'){ // 更新提交审核
+			editCaseAudit({
+				...state.planInfo,
+				applyReason:state.ruleForm.content,
+			})
+				.then(() => {
+					emit('updateList');
+					closeDialog(); // 关闭弹窗
+					ElMessage.success('更新审核提交成功');
+					state.loading = false;
+				})
+				.catch(() => {
+					state.loading = false;
+				});
+		}else{ // 删除提交审核
+			caseDelAudit({
+				id:state.planInfo.id,
+				applyReason:state.ruleForm.content,
+			})
+				.then(() => {
+					emit('updateList');
+					closeDialog(); // 关闭弹窗
+					ElMessage.success('删除审核提交成功');
+					state.loading = false;
+				})
+				.catch(() => {
+					state.loading = false;
+				});
+		}
+	});
+};
+// 暴露变量
+defineExpose({
+	openDialog,
+	closeDialog,
+});
+</script>

+ 456 - 0
src/views/case/index/edit.vue

@@ -0,0 +1,456 @@
+<template>
+	<div class="case-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="8">
+						<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="caseTypeId" :rules="[{ required: true, message: '请选择案例分类', trigger: 'change' }]">
+									<VTreeDrop
+										:data="state.typeData"
+										checkable
+										keyField="id"
+										titleField="name"
+										v-model="state.ruleForm.caseTypeId"
+										@checked-change="selectType"
+										:dropHeight="400"
+										dropPlaceholder="案例分类"
+										dropdownWidthFixed
+										clearable
+										searchPlaceholder="案例分类名称"
+										checkedButtonText="查看已选"
+										:show-footer="false"
+										:cascade="false"
+									>
+									</VTreeDrop>
+								</el-form-item>
+							</template>
+						</el-skeleton>
+					</el-col>
+					<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="8">
+						<el-skeleton :loading="state.loading" animated>
+							<template #template>
+								<el-form-item>
+									<template #label>
+										<div style="height: 34px; display: flex; align-items: center">
+											失效时间
+											<el-tooltip placement="top-start">
+												<SvgIcon name="ele-QuestionFilled" size="18px" class="ml3" />
+												<template #content> 不设置则代表永久有效;到达预设失效时间,案例将自动下架 </template>
+											</el-tooltip>
+										</div>
+									</template>
+									<el-skeleton-item variant="h1" />
+								</el-form-item>
+							</template>
+							<template #default>
+								<el-form-item label="失效时间" prop="expiredTime" :rules="[{ required: false, message: '请选择失效时间', trigger: 'change' }]">
+									<template #label>
+										<div style="height: 34px; display: flex; align-items: center">
+											失效时间
+											<el-tooltip placement="top-start">
+												<SvgIcon name="ele-QuestionFilled" size="18px" class="ml3" />
+												<template #content> 不设置则代表永久有效;到达预设失效时间,案例将自动下架 </template>
+											</el-tooltip>
+										</div>
+									</template>
+									<el-date-picker
+										v-model="state.ruleForm.expiredTime"
+										type="datetime"
+										placeholder="请选择失效时间"
+										class="w100"
+										value-format="YYYY-MM-DD[T]HH:mm:ss"
+										:disabled-date="disabledDate"
+										popper-class="no-atTheMoment"
+									/>
+								</el-form-item>
+							</template>
+						</el-skeleton>
+					</el-col>
+					<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+						<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, validator: validatePassTitle, trigger: 'blur' }]">
+									<el-input v-model="state.ruleForm.title" placeholder="请填写案例标题" clearable @blur="isRepeat('title')"></el-input>
+								</el-form-item>
+							</template>
+						</el-skeleton>
+					</el-col>
+					<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+						<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="abstract" :rules="[{ required: false, message: '请填写案例摘要', trigger: 'blur' }]">
+									<el-input v-model="state.ruleForm.abstract" placeholder="请填写案例摘要" clearable></el-input>
+								</el-form-item>
+							</template>
+						</el-skeleton>
+					</el-col>
+					<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+						<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="keyword" :rules="[{ required: false, message: '请填写关键词', trigger: 'blur' }]">
+									<el-input v-model="state.ruleForm.keyword" placeholder="请填写关键词" clearable></el-input>
+								</el-form-item>
+							</template>
+						</el-skeleton>
+					</el-col>
+					<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+						<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="keyword" :rules="[{ required: true, message: '请选择关联工单', trigger: 'change' }]">
+								</el-form-item>
+							</template>
+						</el-skeleton>
+					</el-col>
+					<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+						<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="keyword" :rules="[{ required: false, message: '请选择关联知识', trigger: 'blur' }]">
+								</el-form-item>
+							</template>
+						</el-skeleton>
+					</el-col>
+					<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+						<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="describe" :rules="[{ required: true, message: '请填写案例描述', trigger: 'blur' }]">
+									<editor v-model:get-html="state.ruleForm.describe" placeholder="请填写案例描述" />
+								</el-form-item>
+							</template>
+						</el-skeleton>
+					</el-col>
+					<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+						<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="result" :rules="[{ required: true, message: '请填写案例办理结果', trigger: 'blur' }]">
+									<editor v-model:get-html="state.ruleForm.result" placeholder="请填写案例办理结果" />
+								</el-form-item>
+							</template>
+						</el-skeleton>
+					</el-col>
+					<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+						<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="reason" :rules="[{ required: false, message: '请填写推荐理由', trigger: 'blur' }]">
+									<editor v-model:get-html="state.ruleForm.reason" placeholder="请填写推荐理由" />
+								</el-form-item>
+							</template>
+						</el-skeleton>
+					</el-col>
+					<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+						<el-skeleton :loading="state.loading" animated>
+							<template #template>
+								<el-form-item label="是否设为热门案例" label-width="130px">
+									<el-skeleton-item variant="h1" />
+								</el-form-item>
+							</template>
+							<template #default>
+								<el-form-item
+									label-width="130px"
+									label="是否设为热门案例"
+									prop="isPopular"
+									:rules="[{ required: false, message: '请选择是否设为热门案例', trigger: 'change' }]"
+								>
+									<el-radio-group v-model="state.ruleForm.isPopular">
+										<el-radio :value="true">是</el-radio>
+										<el-radio :value="false">否</el-radio>
+									</el-radio-group>
+								</el-form-item>
+							</template>
+						</el-skeleton>
+					</el-col>
+					<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+						<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="files" :rules="[{ required: false, message: '请选择附件', trigger: 'change' }]">
+									<annex-list
+										name="案例附件"
+										v-model="state.ruleForm.files"
+										v-model:format="filesFormat"
+										:businessId="state.ruleForm.id"
+										classify="案例上传"
+									/>
+								</el-form-item>
+							</template>
+						</el-skeleton>
+					</el-col>
+					<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+						<el-form-item>
+							<el-button type="primary" @click="onSubmitReview(ruleFormRef)" :loading="state.loading">提交审核</el-button>
+							<el-button class="default-button" @click="onPreview" :loading="state.loading">预览 </el-button>
+							<el-button @click="onSaveOnly(ruleFormRef)" class="default-button" :loading="state.loading">保存为草稿</el-button>
+							<el-button class="default-button" @click="onCancel" :loading="state.loading">取消 </el-button>
+						</el-form-item>
+					</el-col>
+				</el-row>
+			</el-form>
+		</el-card>
+		<!-- 删除或者更新提交审核 -->
+		<edit-submit-audit ref="editSubmitAuditRef" @updateList="processSuccess" />
+	</div>
+</template>
+
+<script setup lang="ts" name="caseEdit">
+import { defineAsyncComponent, 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 { storeToRefs } from 'pinia';
+import { useUserInfo } from '@/stores/userInfo';
+import { Local } from '@/utils/storage';
+import other from '@/utils/other';
+import { throttle, transformFile } from '@/utils/tools';
+import { disabledDate } from '@/utils/constants';
+import { VTreeDrop } from '@wsfe/vue-tree';
+import { addCaseDraft, addCaseDraftAudit, caseDetail, caseRepeat, editCaseDraft } from '@/api/case';
+import { caseTreeList } from '@/api/case/type';
+// 引入组件
+const Editor = defineAsyncComponent(() => import('@/components/Editor/index.vue')); // 富文本编辑器
+const AnnexList = defineAsyncComponent(() => import('@/components/AnnexList/index.vue')); // 附件组件
+const EditSubmitAudit = defineAsyncComponent(() => import('@/views/case/index/components/Edit-submit-audit.vue')); // 删除或者更新 提交审核
+
+const stores = useUserInfo(); // 用户信息
+const { userInfos } = storeToRefs(stores); // 用户信息
+
+// 定义变量内容
+const state = reactive<any>({
+	dialogVisible: false,
+	ruleForm: {
+		attribution: '中心案例库', // 案例归属
+		files: [], // 附件
+		content: '', // 内容
+		title: null, // 标题
+		keyword: null, // 关键词
+		abstract: null, // 摘要
+		expiredTime: null, // 失效时间
+		isPopular: false, // 是否热门
+		describe: '', // 案例描述
+		result: '', // 案例办理结果
+		reason: '', // 推荐理由
+	},
+	typeData: [], // 分类
+	loading: false,
+});
+const ruleFormRef = ref<any>(); // 表单ref
+const validatePassTitle = (rule: any, value: any, callback: any) => {
+	if (value === '' || value === null) {
+		callback(new Error('请填写案例标题'));
+	} else if (Repeat.value) {
+		callback(new Error('有相似标题,请检查!'));
+	} else {
+		callback();
+	}
+};
+// 校验标题/摘要/内容是否重复  根据输入标题检测出关键词填入
+const Repeat = ref<boolean>(false);
+const isRepeat = (type: string) => {
+	if (state.ruleForm[type]) {
+		caseRepeat({ [type]: state.ruleForm[type], id: state.ruleForm.id })
+			.then((res: any) => {
+				Repeat.value = res.result;
+				ruleFormRef.value.validateField(type);
+			})
+			.catch(() => {
+				state.ruleForm[type] = '';
+			});
+	}
+};
+// 默认配置
+const defaultContent = ref([
+	{
+		type: 'paragraph',
+		children: [{ text: '', fontFamily: '仿宋', fontSize: '20px' }],
+		lineHeight: '2',
+	},
+]);
+const selectType = (value: any) => {
+	state.ruleForm.caseTypes = value.map((item: any) => {
+		return { caseTypeName: item.name, caseTypeId: item.id, caseTypeSpliceName: item.spliceName };
+	});
+	state.ruleForm.caseTypeId = value.map((item: any) => item.id);
+	ruleFormRef.value.validateField('caseTypeId');
+};
+// 提交审核
+const route = useRoute(); // 获取路由
+const router = useRouter(); // 路由跳转
+const filesFormat = ref<EmptyArrayType>([]); // 附件列表格式化
+const editSubmitAuditRef = ref<RefType>(); // 删除或者更新 提交审核
+const onSubmitReview = async (formEl: FormInstance | undefined) => {
+	if (!formEl) return;
+	await formEl.validate((valid: boolean) => {
+		if (!valid) return;
+		state.ruleForm.files = filesFormat.value;
+		const submitObj = other.deepClone(state.ruleForm);
+		Reflect.deleteProperty(submitObj, 'creationTime');
+		//如果已经有ID 说明是已经提交过的数据  更新审核
+		if (route.params.id) {
+			editSubmitAuditRef.value.openDialog(submitObj, 'update');
+		} else {
+			state.loading = true;
+			addCaseDraftAudit(submitObj)
+				.then(() => {
+					ElMessage.success('新增案例审核成功');
+					processSuccess();
+				})
+				.catch(() => {
+					state.loading = false;
+				});
+			state.loading = false;
+		}
+	});
+};
+// 流程提交成功
+const processSuccess = () => {
+	// 关闭当前 tagsView
+	mittBus.emit('onCurrentContextmenuClick', Object.assign({}, { contextMenuClickId: 1, ...route }));
+	mittBus.emit('clearCache', 'caseManage');
+	router.push({
+		path: '/case/index',
+	});
+};
+// 预览
+const onPreview = () => {
+	if (route.params.id) {
+	} else {
+		state.ruleForm.creatorName = userInfos.value?.name ?? '';
+		state.ruleForm.creationTime = new Date();
+		state.ruleForm.creatorOrgName = userInfos.value?.orgName ?? '';
+	}
+	Local.set('casePreviewForm', state.ruleForm);
+	router.push({
+		name: 'casePreview',
+		params: {
+			tagsViewName: '案例预览',
+			id: '0',
+		},
+	});
+};
+// 保存到草稿箱
+const onSaveOnly = throttle(async (formEl: FormInstance | undefined) => {
+	if (!formEl) return;
+	await formEl.validate((valid: boolean) => {
+		if (!valid) return;
+		state.loading = true;
+		state.ruleForm.files = filesFormat.value;
+		const submitObj = other.deepClone(state.ruleForm);
+		Reflect.deleteProperty(submitObj, 'creationTime');
+		if (route.params.id) {
+			// 更新
+			editCaseDraft(submitObj)
+				.then(handleSuccess)
+				.catch(() => {
+					state.loading = false;
+				});
+		} else {
+			// 新增
+			addCaseDraft(submitObj)
+				.then(handleSuccess)
+				.catch(() => {
+					state.loading = false;
+				});
+		}
+	});
+}, 300);
+// 取消
+const onCancel = () => {
+	state.loading = true;
+	mittBus.emit('onCurrentContextmenuClick', Object.assign({}, { contextMenuClickId: 1, ...route }));
+	mittBus.emit('clearCache', 'caseManage');
+	router.push({
+		path: '/case/index',
+	});
+	state.loading = false;
+};
+const handleSuccess = () => {
+	state.loading = false;
+	ElMessage.success('操作成功');
+	// 关闭当前 tagsView
+	mittBus.emit('onCurrentContextmenuClick', Object.assign({}, { contextMenuClickId: 1, ...route }));
+	mittBus.emit('clearCache', 'caseManage');
+	router.push({
+		path: '/case/index',
+	});
+};
+// 获取分类
+const getType = async () => {
+	state.loading = true;
+	try {
+		const [typeDataRes] = await Promise.all([caseTreeList({ IsEnable: true })]);
+		state.typeData = typeDataRes.result ?? [];
+		state.loading = false;
+	} catch (error) {
+		console.log(error);
+		state.loading = false;
+	}
+};
+const editorRef = ref<RefType>();
+const getDetail = async () => {
+	if (route.params.id) {
+		const res: any = await caseDetail({ id: route.params.id }); //详情
+		state.ruleForm = res.result ?? {};
+		state.ruleForm.files = transformFile(state.ruleForm.files);
+		state.ruleForm.caseTypeId = state.ruleForm.caseTypes?.map((item: any) => item.CaseTypeID);
+	}
+};
+onMounted(async () => {
+	await getType();
+	await getDetail();
+});
+</script>
+<style lang="scss">
+.vtree-tree-drop__wrapper {
+	width: 100%;
+}
+</style>

+ 536 - 0
src/views/case/index/index.vue

@@ -0,0 +1,536 @@
+<template>
+	<div class="case-index-container layout-padding">
+		<div class="layout-padding-auto layout-padding-view pd20">
+			<splitpanes class="h100" :horizontal="horizontal">
+				<pane min-size="16" max-size="25" size="16">
+					<el-scrollbar ref="scrollBarRef" noresiz>
+						<el-skeleton :loading="state.typeLoading" animated :rows="10">
+							<template #default>
+								<v-tree-search
+									:data="state.typeOptions"
+									titleField="name"
+									keyField="id"
+									selectable
+									searchPlaceholder="案例分类名称"
+									@select="selectType"
+									@unselect="unSelectType"
+									ref="treeSearchRef"
+								>
+									<template #node="{ node }">
+										<span>{{ node.name }}</span>
+									</template>
+								</v-tree-search>
+							</template>
+						</el-skeleton>
+					</el-scrollbar>
+				</pane>
+				<pane class="h100" style="display: flex; flex: 1; flex-direction: column">
+					<el-tabs v-model="state.queryParams.Status" @tab-change="handleClick" v-loading="state.loading">
+						<el-tab-pane :label="v.value" :name="v.key" v-for="(v, i) in state.statusOptions" :key="i" :disabled="state.tableLoading"></el-tab-pane>
+					</el-tabs>
+					<el-form :model="state.queryParams" ref="ruleFormRef" inline @submit.native.prevent>
+						<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 label="创建时间" prop="cjTime">
+							<el-date-picker
+								v-model="state.queryParams.cjTime"
+								type="datetimerange"
+								unlink-panels
+								range-separator="至"
+								start-placeholder="开始时间"
+								end-placeholder="结束时间"
+								:shortcuts="shortcuts"
+								@change="handleQuery"
+								value-format="YYYY-MM-DD[T]HH:mm:ss"
+								:default-time="defaultTimeStartEnd"
+							/>
+						</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,
+						}"
+						:tools="[{ toolRender: { name: 'exportCurrent' } }, { toolRender: { name: 'exportAll' } }]"
+					>
+						<template #buttons>
+							<el-button type="primary" @click="onAdd" v-auth="'case:index:add'" :loading="state.tableLoading">
+								<SvgIcon name="ele-Plus" class="mr5" />创建案例
+							</el-button>
+							<el-dropdown v-auth="'case:index:export:detail'" @command="onExportDetail" class="ml12 mr12" :disabled="isChecked">
+								<el-button type="primary" :disabled="isChecked" :loading="state.tableLoading">
+									导出案例详情<SvgIcon name="ele-ArrowDown" class="ml3" />
+								</el-button>
+								<template #dropdown>
+									<el-dropdown-menu>
+										<el-dropdown-item :command="item.key" v-for="item in exportType" :key="item.key">{{ item.value }}</el-dropdown-item>
+									</el-dropdown-menu>
+								</template>
+							</el-dropdown>
+						</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="caseManage"
+							:custom-config="{ storage: true }"
+							showHeaderOverflow
+							:params="{ exportMethod: getCaseExport, exportParams: requestParams }"
+							@checkbox-all="selectAllChangeEvent"
+							@checkbox-change="selectChangeEvent"
+						>
+							<vxe-column type="checkbox" width="50" align="center"></vxe-column>
+							<vxe-column field="title" title="标题" min-width="200">
+								<template #default="{ row }">
+									<el-button link type="primary" @click="onPreview(row)">{{ row.title }}</el-button>
+								</template>
+							</vxe-column>
+							<vxe-column field="planTypeText" title="案例分类" width="150"></vxe-column>
+							<vxe-column field="statusName" title="案例状态" width="110"></vxe-column>
+							<vxe-column field="pageView" title="阅读次数" width="100"></vxe-column>
+							<vxe-column field="onShelfTime" title="上架时间" width="160">
+								<template #default="{ row }">
+									{{ formatDate(row.onShelfTime, 'YYYY-mm-dd HH:MM:SS') }}
+								</template>
+							</vxe-column>
+							<vxe-column field="offShelfTime" title="下架时间" width="160">
+								<template #default="{ row }">
+									{{ formatDate(row.offShelfTime, 'YYYY-mm-dd HH:MM:SS') }}
+								</template>
+							</vxe-column>
+							<vxe-column field="expiredTime" title="到期时间" width="160">
+								<template #default="{ row }">
+									{{ formatDate(row.expiredTime, 'YYYY-mm-dd HH:MM:SS') }}
+								</template>
+							</vxe-column>
+							<vxe-column field="creatorName" title="创建人" width="120"></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 field="examinMan.name" title="审批人" width="120"></vxe-column>
+							<vxe-column field="examinTime" title="审批时间" width="160">
+								<template #default="{ row }">
+									{{ formatDate(row.examinTime, 'YYYY-mm-dd HH:MM:SS') }}
+								</template>
+							</vxe-column>
+							<vxe-column field="examinOpinion" title="审批意见" width="200"></vxe-column>
+							<vxe-column title="操作" fixed="right" width="250" align="center">
+								<template #default="{ row }">
+									<el-button link type="primary" @click="onEdit(row)" title="编辑" v-auth="'case:index:edit'" v-if="[4, 7, 5].includes(row.status)">
+										编辑
+									</el-button>
+									<el-button
+										link
+										type="danger"
+										@click="onRowDel(row)"
+										title="删除"
+										v-auth="'case:index:delete'"
+										v-if="[4, 7, 5].includes(row.status)"
+									>
+										删除
+									</el-button>
+									<el-button
+										link
+										type="primary"
+										@click="offShelfFn(row)"
+										title="下架"
+										v-auth="'case:index:undercarriage'"
+										v-if="[3].includes(row.status)"
+									>
+										下架
+									</el-button>
+									<el-button
+										link
+										type="primary"
+										@click="groundingFn(row)"
+										title="上架"
+										v-auth="'case:index:grounding'"
+										v-if="[4].includes(row.status)"
+									>
+										上架
+									</el-button>
+									<el-button link type="primary" @click="onAudit(row)" title="审批" v-if="[1].includes(row.status)" v-auth="'case:index:audit'">
+										审批
+									</el-button>
+									<el-button link type="primary" @click="onPreview(row)" title="查看"> 查看 </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"
+					/>
+				</pane>
+			</splitpanes>
+		</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="sjTime">
+					<el-date-picker
+						v-model="state.queryParams.sjTime"
+						type="datetimerange"
+						unlink-panels
+						range-separator="至"
+						start-placeholder="开始时间"
+						end-placeholder="结束时间"
+						:shortcuts="shortcuts"
+						@change="handleQuery"
+						value-format="YYYY-MM-DD[T]HH:mm:ss"
+						:default-time="defaultTimeStartEnd"
+					/>
+				</el-form-item>
+				<el-form-item label="下架时间" prop="xjTime">
+					<el-date-picker
+						v-model="state.queryParams.xjTime"
+						type="datetimerange"
+						unlink-panels
+						range-separator="至"
+						start-placeholder="开始时间"
+						end-placeholder="结束时间"
+						:shortcuts="shortcuts"
+						@change="handleQuery"
+						value-format="YYYY-MM-DD[T]HH:mm:ss"
+						:default-time="defaultTimeStartEnd"
+					/>
+				</el-form-item>
+				<el-form-item label="更新时间" prop="gxTime">
+					<el-date-picker
+						v-model="state.queryParams.gxTime"
+						type="datetimerange"
+						unlink-panels
+						range-separator="至"
+						start-placeholder="开始时间"
+						end-placeholder="结束时间"
+						:shortcuts="shortcuts"
+						@change="handleQuery"
+						value-format="YYYY-MM-DD[T]HH:mm:ss"
+						:default-time="defaultTimeStartEnd"
+					/>
+				</el-form-item>
+			</el-form>
+			<template #footer>
+				<el-button type="primary" @click="handleQuery" :loading="state.loading"> <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>
+		<!-- 流程审批 -->
+		<process-audit ref="processAuditRef" @orderProcessSuccess="queryList" />
+		<!-- 审核记录 -->
+		<audit-progress ref="auditProgressRef" />
+		<!-- 批量审批 -->
+		<to-end ref="toEndRef" @updateList="queryList" />
+		<!-- 删除或者更新提交审核 -->
+		<case-audit ref="caseAuditRef" @updateList="handleQuery" />
+		<!-- 删除或者更新提交审核 -->
+		<edit-submit-audit ref="editSubmitAuditRef" @updateList="handleQuery" />
+	</div>
+</template>
+
+<script lang="tsx" setup name="caseManage">
+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, Pane } from 'splitpanes';
+import 'splitpanes/dist/splitpanes.css';
+import Other from '@/utils/other';
+import { downloadFileByStream } from '@/utils/tools';
+import { VxeUI } from 'vxe-pc-ui';
+import { defaultTimeStartEnd, shortcuts } from '@/utils/constants';
+import { VTreeSearch } from '@wsfe/vue-tree';
+import { caseTreeList } from '@/api/case/type';
+import { caseDetailExport, caseOffShelf, caseOnShelf, getCaseBaseData, getCaseExport, getCaseList } from '@/api/case';
+
+// 引入组件
+const ProcessAudit = defineAsyncComponent(() => import('@/components/ProcessAudit/index.vue')); // 流程审批
+const AuditProgress = defineAsyncComponent(() => import('@/views/knowledge/components/Audit-progress.vue')); // 编写规范
+const ToEnd = defineAsyncComponent(() => import('@/views/knowledge/index/components/To-end.vue')); // 批量审批
+const pagination = defineAsyncComponent(() => import('@/components/ProTable/components/Pagination.vue')); // 分页
+const CaseAudit = defineAsyncComponent(() => import('@/views/case/index/components/Case-audit.vue')); // 审批新增 修改 删除
+const EditSubmitAudit = defineAsyncComponent(() => import('@/views/case/index/components/Edit-submit-audit.vue')); // 删除或者更新 提交审核
+
+const router = useRouter(); //路由
+const horizontal = ref(false);
+
+// 定义变量内容
+const state = reactive<any>({
+	queryParams: {
+		PageIndex: 1, //页码
+		PageSize: 20, //每页条数
+		Keyword: null, //关键字
+		Status: 3, //状态 默认已上架
+		Title: null, //标题
+		ModuleCode: '',
+		Attribution: '中心案例库',
+		sjTime: [], // 上架时间
+		StartOnShelfTime: null,
+		EndOnShelfTime: null,
+		xjTime: [], // 下架时间
+		StartOffShelfTime: null,
+		EndOffShelfTime: null,
+		cjTime: [], // 创建时间
+		CreationStartTime: null,
+		CreationEndTime: null,
+		gxTime: [], // 更新时间
+		StartUpdateTime: null,
+		EndUpdateTime: null,
+	},
+	activeName: '1', //tab切换 默认案例分类
+	tableData: [], //表格数据
+	total: 0, //总条数
+	loading: false, //表格loading
+	tableLoading: false, //表格loading
+	typeOptions: [], // 类型数据
+	statusOptions: [], //状态数据
+	typeLoading: false, // 类型loading
+});
+// 切换tab 查询列表
+const handleClick = () => {
+	state.queryParams.ModuleCode = '';
+	queryList();
+};
+/** 搜索按钮操作 节流操作 */
+const handleQuery = () => {
+	state.queryParams.PageIndex = 1;
+	queryList();
+};
+// 获取案例分类
+const getCaseType = async () => {
+	state.typeLoading = true;
+	try {
+		const { result } = await caseTreeList({ IsEnable: true, Attribution: state.queryParams.Attribution });
+		state.typeOptions = result ?? [];
+		state.typeLoading = false;
+	} catch (error) {
+		state.typeLoading = false;
+	}
+};
+const exportType = ref<EmptyArrayType>([]);
+const getBaseDataFn = async () => {
+	try {
+		const { result } = await getCaseBaseData();
+		state.statusOptions = result?.tabStatusName ?? []; // 列表状态
+		exportType.value = result?.fileType ?? []; // 导出文件类型
+		state.loading = false;
+	} catch (error) {
+		state.loading = false;
+	}
+};
+/** 获取列表 */
+const requestParams = ref<EmptyObjectType>({});
+const queryList = () => {
+	state.tableLoading = true;
+	requestParams.value = Other.deepClone(state.queryParams);
+	requestParams.value.StartOnShelfTime = state.queryParams.sjTime === null ? null : state.queryParams.sjTime[0]; // 上架时间
+	requestParams.value.EndOnShelfTime = state.queryParams.sjTime === null ? null : state.queryParams.sjTime[1];
+	Reflect.deleteProperty(requestParams.value, 'sjTime'); // 删除无用的参数
+	requestParams.value.StartOffShelfTime = state.queryParams.xjTime === null ? null : state.queryParams.xjTime[0]; // 受理时间
+	requestParams.value.EndOffShelfTime = state.queryParams.xjTime === null ? null : state.queryParams.xjTime[1]; //下架时间
+	Reflect.deleteProperty(requestParams.value, 'xjTime'); // 删除无用的参数
+	requestParams.value.CreationStartTime = state.queryParams.cjTime === null ? null : state.queryParams.cjTime[0]; // 创建时间
+	requestParams.value.CreationEndTime = state.queryParams.cjTime === null ? null : state.queryParams.cjTime[1];
+	Reflect.deleteProperty(requestParams.value, 'cjTime'); // 删除无用的参数
+	requestParams.value.StartUpdateTime = state.queryParams.gxTime === null ? null : state.queryParams.gxTime[0]; // 更新时间
+	requestParams.value.EndUpdateTime = state.queryParams.gxTime === null ? null : state.queryParams.gxTime[1];
+	Reflect.deleteProperty(requestParams.value, 'gxTime'); // 删除无用的参数
+	getCaseList(requestParams.value)
+		.then((response: any) => {
+			state.tableData = response?.result.items ?? [];
+			state.total = response?.result.total;
+			state.tableLoading = false;
+		})
+		.catch(() => {
+			state.tableLoading = false;
+		});
+};
+// 选择分类
+const selectType = (data: any) => {
+	state.queryParams.CaseTypeID = data.id;
+	handleQuery();
+};
+// 取消选择
+const unSelectType = () => {
+	state.queryParams.CaseTypeID = null;
+	handleQuery();
+};
+/** 重置按钮操作 */
+const drawerRuleFormRef = ref();
+const ruleFormRef = ref<RefType>(); // 表单ref
+const drawer = ref(false);
+const treeSearchRef = ref<RefType>();
+const resetQuery = (formEl: FormInstance | undefined) => {
+	if (!formEl) return;
+	formEl.resetFields();
+	ruleFormRef.value?.resetFields();
+	treeSearchRef.value.setSelected(state.queryParams.OrgCode, false); // 清空选择
+	treeSearchRef.value.clearKeyword(); // 清空搜索关键词
+	treeSearchRef.value.search(); // 搜索
+	resetNode();
+	setTimeout(() => {
+		treeSearchRef.value.setExpandAll(false); // 默认全部收起
+	}, 300);
+};
+// 重置选中的节点
+const resetNode = () => {
+	state.queryParams.CaseTypeID = null;
+	handleQuery();
+};
+// 新增案例
+const onAdd = () => {
+	router.push({
+		name: 'caseEdit',
+		params: {
+			tagsViewName: '新增案例',
+		},
+	});
+};
+// 修改
+const onEdit = (row: any) => {
+	router.push({
+		name: 'caseEdit',
+		params: {
+			id: row.id,
+			tagsViewName: '编辑案例',
+		},
+	});
+};
+// 预览
+const onPreview = (row: any) => {
+	router.push({
+		name: 'casePreview',
+		params: {
+			id: row.id,
+			tagsViewName: row.title,
+		},
+	});
+};
+// 审批
+const caseAuditRef = ref<RefType>();
+const onAudit = (row: any) => {
+	caseAuditRef.value.openDialog(row);
+};
+// 下架
+const offShelfFn = (row: any) => {
+	ElMessageBox.confirm(`是否确定要下架【${row.title}】?,案例下架后,将不会被检索到!`, '提示', {
+		confirmButtonText: '确认',
+		cancelButtonText: '取消',
+		type: 'warning',
+		draggable: true,
+		cancelButtonClass: 'default-button',
+	})
+		.then(() => {
+			caseOffShelf(row.id).then(() => {
+				ElMessage.success('下架成功');
+				handleQuery();
+			});
+		})
+		.catch(() => {});
+};
+// 上架
+const groundingFn = (row: any) => {
+	ElMessageBox.confirm(`是否确定要上架【${row.title}】?`, '提示', {
+		confirmButtonText: '确认',
+		cancelButtonText: '取消',
+		type: 'warning',
+		draggable: true,
+		cancelButtonClass: 'default-button',
+	})
+		.then(() => {
+			caseOnShelf(row.id).then(() => {
+				ElMessage.success('上架成功');
+				handleQuery();
+			});
+		})
+		.catch(() => {});
+};
+// 删除案例
+const editSubmitAuditRef = ref<RefType>();
+const onRowDel = (row: any) => {
+	editSubmitAuditRef.value.openDialog(row, 'delete');
+};
+// 导出详情
+const onExportDetail = (command: string | number | object) => {
+	state.loading = true;
+	const ids = checkTable.value.map((item: any) => item.id);
+	VxeUI.modal.message({
+		content: `导出中,请稍等`,
+		status: 'loading',
+		id: 'exportDetail',
+		duration: -1,
+	});
+	caseDetailExport({ ids, fileType: command })
+		.then((res:any) => {
+			downloadFileByStream(res);
+			state.loading = false;
+			VxeUI.modal.close('exportDetail');
+			VxeUI.modal.message({
+				content: `导出成功`,
+				status: 'success',
+			});
+		})
+		.catch((e) => {
+			console.log(e,'导出错误')
+			state.loading = false;
+			VxeUI.modal.close('exportDetail');
+			VxeUI.modal.message({
+				content: `导出失败`,
+				status: 'error',
+			});
+		});
+};
+const tableRef = ref<RefType>();
+const checkTable = ref<EmptyArrayType>([]);
+const selectAllChangeEvent = ({ checked }) => {
+	if (tableRef.value) {
+		const records = tableRef.value.getCheckboxRecords();
+		checkTable.value = records;
+		console.log(checked ? '所有勾选事件' : '所有取消事件', records);
+	}
+};
+
+const selectChangeEvent = ({ checked }) => {
+	if (tableRef.value) {
+		const records = tableRef.value.getCheckboxRecords();
+		checkTable.value = records;
+		console.log(checked ? '勾选事件' : '取消事件', records);
+	}
+};
+const isChecked = computed(() => {
+	return !Boolean(checkTable.value.length);
+});
+const toolbarRef = ref<RefType>();
+onMounted(() => {
+	queryList();
+	if (tableRef.value && toolbarRef.value) {
+		tableRef.value.connect(toolbarRef.value);
+	}
+	getBaseDataFn();
+	getCaseType();
+});
+</script>

+ 165 - 0
src/views/case/index/preview.vue

@@ -0,0 +1,165 @@
+<template>
+	<div class="case-preview-container layout-pd">
+		<el-card shadow="never">
+			<el-row v-loading="loading">
+				<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb10">
+					<h1 class="font18" style="text-align: center">{{ state.info.title }}</h1></el-col
+				>
+				<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="color-info flex-center-between flex-warp">
+					<div>
+						<span class="mr10">创建时间:{{ formatDate(state.info.creationTime, 'YYYY-mm-dd HH:MM:SS') }}</span>
+						<span class="mr10">创建人:{{ state.info.creatorName }}</span>
+						<span>创建部门:{{ state.info.creatorOrgName }}</span>
+					</div>
+					<div>
+						<span class="mr10" v-if="state.info.planTypeText">案例分类:{{ state.info.planTypeText }}</span>
+						<span>已阅:{{ state.info.pageView }}</span>
+					</div>
+					<div v-if="haveId" style="display: flex; align-items: center">
+						<el-button link class="flex-center-align" type="info" v-auth="'case:index:rate'">
+							<el-rate v-model="state.info.score" @change="onRate" /> 评分</el-button
+						>
+						<el-dropdown v-auth="'case:index:export:detail'" @command="onExportDetail" class="ml12">
+							<el-button type="info" :loading="state.loading" link> 导出详情<SvgIcon name="ele-ArrowDown" class="ml3" /> </el-button>
+							<template #dropdown>
+								<el-dropdown-menu>
+									<el-dropdown-item :command="item.key" v-for="item in exportType" :key="item.key">{{ item.value }}</el-dropdown-item>
+								</el-dropdown-menu>
+							</template>
+						</el-dropdown>
+					</div>
+				</el-col>
+				<el-divider />
+				<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mt5 editor-content-view">
+					<div v-html="state.info.content" class="lineHeight24"></div
+				></el-col>
+				<template v-if="haveId">
+					<el-divider />
+					<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mt10">
+						<el-form-item label="附件">
+							<annex-list name="案例附件" v-model="state.info.files" readonly classify="案例附件" />
+						</el-form-item>
+					</el-col>
+				</template>
+			</el-row>
+		</el-card>
+	</div>
+</template>
+
+<script setup lang="ts" name="casePreview">
+import { reactive, onMounted, onBeforeUnmount, ref, defineAsyncComponent, computed } from 'vue';
+import { useRoute } from 'vue-router';
+import { Local } from '@/utils/storage';
+import { formatDate } from '@/utils/formatTime';
+import { ElMessage } from 'element-plus';
+import { downloadFileByStream, transformFile } from '@/utils/tools';
+import { VxeUI } from 'vxe-pc-ui';
+import { caseDetail, caseDetailExport, caseScore, getCaseBaseData } from '@/api/case';
+
+// 引入组件
+const AnnexList = defineAsyncComponent(() => import('@/components/AnnexList/index.vue')); // 附件列表
+
+// 定义变量内容
+const state = reactive<any>({
+	info: {
+		planTypeText: '', // 类别
+		hotspotName: '', // 热点
+		keywords: '', // 关键词
+		isPublic: false, // 是否公开
+		creatorName: '', // 创建人
+		creationTime: '', // 创建时间
+		summary: '', // 摘要
+		content: '', // 内容
+		collect: false, // 收藏
+		score: 0, // 评分
+	},
+});
+const loading = ref<Boolean>(false); // 加载状态
+// 查询详情
+const route = useRoute(); //  获取路由参数
+// 评分
+const onRate = () => {
+	caseScore({ id: state.info.id, score: state.info.score })
+		.then(() => {
+			ElMessage.success('评分成功');
+			getInfo();
+		})
+		.catch(() => {
+			getInfo();
+		});
+};
+// 查询详情
+const getInfo = async () => {
+	loading.value = true;
+	if (route.params.id && route.params.id !== '0') {
+		try {
+			const { isAddPv } = route.params;
+			let IsAddPv = isAddPv ? 'true' : false;
+			const res: any = await caseDetail({ id: route.params.id, isAddPv: IsAddPv });
+			state.info = res.result ?? {};
+			state.info.files = transformFile(state.info.files);
+			loading.value = false;
+		} catch (error) {
+			loading.value = false;
+		}
+	} else {
+		state.info = Local.get('casePreviewForm') ?? {};
+		state.info.files = transformFile(state.info.files);
+		loading.value = false;
+	}
+};
+// 获取基础数据
+const exportType = ref<EmptyArrayType>([]);
+const getBaseData = async () => {
+	state.loading = true;
+	try {
+		const { result } = await getCaseBaseData();
+		exportType.value = result.fileType ?? []; // 导出文件类型
+		state.loading = false;
+	} catch (error) {
+		state.loading = false;
+	}
+};
+// 详情导出
+const onExportDetail = (command: string | number | object) => {
+	state.loading = true;
+	caseDetailExport({ ids: [state.info.id], fileType: command })
+		.then((res) => {
+			downloadFileByStream(res);
+			state.loading = false;
+			VxeUI.modal.close('exportDetail');
+			VxeUI.modal.message({
+				content: `导出成功`,
+				status: 'success',
+			});
+		})
+		.catch(() => {
+			state.loading = false;
+			VxeUI.modal.close('exportDetail');
+			VxeUI.modal.message({
+				content: `导出失败`,
+				status: 'error',
+			});
+		});
+};
+const haveId = computed(() => {
+	return route.params.id && route.params.id !== '0';
+});
+onMounted(() => {
+	getBaseData();
+	getInfo();
+});
+onBeforeUnmount(() => {
+	Local.remove('casePreviewForm'); // 删除本地缓存
+});
+</script>
+<style lang="scss" scoped>
+.case-preview-container {
+	.relevance {
+		margin-bottom: 5px;
+		&:last-child {
+			margin-bottom: 0;
+		}
+	}
+}
+</style>

+ 336 - 0
src/views/case/retrieval/index.vue

@@ -0,0 +1,336 @@
+<template>
+	<div class="case-retrieval-container layout-padding">
+		<el-card shadow="never" class="h100">
+			<splitpanes class="h100" Vertical>
+				<pane min-size="16" max-size="25" size="16" class="left-container">
+					<el-scrollbar ref="scrollBarRef">
+						<el-skeleton :loading="state.typeLoading" animated :rows="10">
+							<template #default>
+								<v-tree-search
+									:data="state.typeOptions"
+									titleField="name"
+									keyField="id"
+									selectable
+									searchPlaceholder="预案分类名称"
+									@select="selectType"
+									@unselect="unSelectType"
+									ref="treeSearchRef"
+								>
+									<template #node="{ node }">
+										<span>{{ node.name }}</span>
+									</template>
+								</v-tree-search>
+							</template>
+						</el-skeleton>
+					</el-scrollbar>
+				</pane>
+				<pane class="center-container">
+					<div class="input-box">
+						<el-select v-model="state.queryParams.RetrievalType" placeholder="请选择" class="width120" @change="handleQuery">
+							<el-option label="标题" value="0" />
+							<el-option label="关键词" value="1" />
+							<el-option label="摘要" value="2" />
+						</el-select>
+						<div class="input-with-button w100">
+							<div class="flex">
+								<el-input v-model="state.queryParams.text" placeholder="关键词" clearable class="mr10 w100" @keyup.enter="handleQuery"> </el-input>
+								<el-button type="primary" class="btn" :loading="state.loading" @click="handleQuery"
+									><SvgIcon name="ele-Search" class="mr5" />搜索</el-button
+								>
+								<el-button @click="resetQuery" class="default-button"> <SvgIcon name="ele-Refresh" class="mr5" />重置</el-button>
+							</div>
+						</div>
+					</div>
+					<div style="display: flex; margin: 10px 15px 0">
+						<div style="height: 32px; line-height: 32px">排序:</div>
+						<el-radio-group v-model="state.queryParams.SortField" @change="handleQuery" style="align-items: normal">
+							<el-radio value="pageView">浏览量</el-radio>
+							<el-radio value="score">评分</el-radio>
+							<el-radio value="creationTime">创建时间</el-radio>
+						</el-radio-group>
+					</div>
+					<div v-loading="centerLoading" class="center-container-box" style="height: calc(100% - 110px)">
+						<template v-if="state.retrievalList.length">
+							<el-scrollbar>
+								<div v-for="(v, i) in state.retrievalList" :key="i" class="retrieval-content-item" @click="onPreview(v)" title="查看详情">
+									<h4 class="mb10 text-no-wrap retrieval-content-item-title">{{ v.title }}</h4>
+									<!--									<div class="text-ellipsis2">{{ v.summary }}</div>-->
+									<div class="flex-center-between mt10 color-info">
+										<div>
+											<span class="mr10">创建部门:{{ v.creatorOrgName }}</span>
+											<span>创建时间:{{ formatDate(v.creationTime, 'YYYY-mm-dd HH:MM:SS') }}</span>
+										</div>
+										<div class="flex-center-align">
+											<span class="flex-center-align"><SvgIcon name="ele-StarFilled" size="18px" class="mr3" />{{ v.score }}</span>
+											<!--											<span class="flex-center-align ml10"><SvgIcon name="ele-ChatDotSquare" size="16px" class="mr3" />{{ v.commentNum }}</span>-->
+											<span class="flex-center-align ml10"><SvgIcon name="ele-View" size="16px" class="mr3" />{{ v.pageView }}</span>
+										</div>
+									</div>
+								</div>
+							</el-scrollbar>
+						</template>
+						<el-empty v-else description="暂无结果" />
+						<pagination
+							@pagination="queryList"
+							:total="state.total"
+							v-model:current-page="state.queryParams.PageIndex"
+							v-model:page-size="state.queryParams.PageSize"
+							:disabled="centerLoading"
+						/>
+					</div>
+				</pane>
+				<pane min-size="20" max-size="30" size="20" class="right-container">
+					<p class="flex-center-between pt10">
+						<span class="font16">常用预案前10</span>
+						<el-button link type="primary" @click="querySearchNum"><SvgIcon name="ele-Refresh" class="mr4" /> 刷新</el-button>
+					</p>
+					<el-divider />
+					<p class="flex-center-between">
+						<span>排名</span>
+						<span>搜索频率</span>
+					</p>
+					<el-skeleton :loading="rightLoading" animated :rows="10">
+						<template #default>
+							<div class="top10 mt10" style="height: calc(100% - 90px)">
+								<template v-if="topList.length">
+									<el-scrollbar>
+										<div class="flex-center-between top10-items" v-for="(item, index) in topList" :key="item.id">
+											<p class="flex-center-align top10-items-title" @click="onPreview(item)">
+												{{ index + 1 }}.
+												<el-text class="text-no-wrap color-primary cursor-pointer" @click="onPreview(item)">
+													<TextTooltip :content="item.title" placement="top" effect="dark" />
+												</el-text>
+											</p>
+											<span class="top10-items-num">{{ item.searchNum }}</span>
+										</div>
+									</el-scrollbar>
+								</template>
+								<el-empty v-else />
+							</div>
+						</template>
+					</el-skeleton>
+				</pane>
+			</splitpanes>
+		</el-card>
+	</div>
+</template>
+
+<script setup lang="ts" name="caseRetrieval">
+import { onMounted, reactive, ref, defineAsyncComponent } from 'vue';
+import { useRouter } from 'vue-router';
+import { formatDate } from '@/utils/formatTime';
+import { throttle } from '@/utils/tools';
+import { Splitpanes, Pane } from 'splitpanes';
+import 'splitpanes/dist/splitpanes.css';
+import { VTreeSearch } from '@wsfe/vue-tree';
+import { caseTreeList } from '@/api/case/type';
+import { caseRetrieval, caseRetrievalTop10 } from '@/api/case/retrieval';
+
+const pagination = defineAsyncComponent(() => import('@/components/ProTable/components/Pagination.vue')); // 分页
+const TextTooltip = defineAsyncComponent(() => import('@/components/TextTooltip/index.vue'));
+const router = useRouter(); // 路由
+const state = reactive<any>({
+	queryParams: {
+		// 查询条件
+		PageIndex: 1, // 当前页
+		PageSize: 10, // 每页条数
+		Attribution: '中心案例库',
+		text: null, // 关键词
+		RetrievalType: '0', // 检索类型 默认全文
+		OrderByType: 1, // 排序类型  降序
+		SortField: 'pageView',
+	},
+	typeOptions: [], // 预案类型
+	total: 0, // 总条数
+	loading: false, // 加载状态
+	retrievalList: [], // 检索列表
+	typeLoading: false, // 预案类型loading
+});
+const topList = ref<EmptyArrayType>([]); // 常用预案前10
+// 获取分类
+const getType = async () => {
+	state.typeLoading = true;
+	try {
+		const { result } = await caseTreeList({ IsEnable: true, Attribution: state.queryParams.Attribution });
+		state.typeOptions = result ?? [];
+		state.typeLoading = false;
+	} catch (error) {
+		state.typeLoading = false;
+	}
+};
+// 选择分类
+const selectType = (data: any) => {
+	state.queryParams.CaseTypeID = data.id;
+	handleQuery();
+};
+// 取消选择
+const unSelectType = () => {
+	state.queryParams.CaseTypeID = null;
+	handleQuery();
+};
+// 预览
+const onPreview = (row: any) => {
+	router.push({
+		name: 'casePreview',
+		params: {
+			id: row.id,
+			isAddPv: 'isAddPv',
+			tagsViewName: row.title,
+		},
+	});
+};
+// 切换tab 查询列表
+const rightLoading = ref(false); // 右侧加载状态
+// 常用预案前10
+const querySearchNum = () => {
+	rightLoading.value = true;
+	caseRetrievalTop10({ Keyword: state.queryParams.Keyword })
+		.then((res: any) => {
+			topList.value = res.result?.items ?? [];
+			rightLoading.value = false;
+		})
+		.catch(() => {
+			rightLoading.value = false;
+		});
+};
+const centerLoading = ref(false); // 中间加载状态
+/** 搜索按钮操作 */
+const handleQuery = () => {
+	state.queryParams.PageIndex = 1;
+	switch (state.queryParams.RetrievalType) {
+		case '0': // 标题
+			state.queryParams.Title = state.queryParams.text;
+			state.queryParams.Abstract = null;
+			state.queryParams.Keyword = null;
+			break;
+		case '1': // 关键词
+			state.queryParams.Keyword = state.queryParams.text;
+			state.queryParams.Abstract = null;
+			state.queryParams.Title = null;
+			break;
+		case '2': // 摘要
+			state.queryParams.Abstract = state.queryParams.text;
+			state.queryParams.Title = null;
+			state.queryParams.Keyword = null;
+			break;
+	}
+	queryList();
+};
+const queryList = () => {
+	centerLoading.value = true;
+	caseRetrieval(state.queryParams)
+		.then((res: any) => {
+			state.retrievalList = res.result?.items ?? [];
+			state.total = res.result?.total ?? 0;
+			centerLoading.value = false;
+		})
+		.catch(() => {
+			centerLoading.value = false;
+			state.retrievalList = [];
+			state.total = 0;
+		});
+};
+/** 重置按钮操作 */
+const treeSearchRef = ref<RefType>();
+const resetQuery = throttle(() => {
+	state.queryParams.PageIndex = 1;
+	state.queryParams.PageSize = 10;
+	state.queryParams.text = null;
+	state.queryParams.RetrievalType = '0';
+	state.queryParams.SortField = 'pageView';
+	state.queryParams.CaseTypeID = null;
+	state.queryParams.Abstract = null;
+	state.queryParams.Title = null;
+	state.queryParams.Keyword = null;
+	treeSearchRef.value.setSelected(state.queryParams.CaseTypeID, false); // 清空选择
+	treeSearchRef.value.clearKeyword(); // 清空搜索关键词
+	treeSearchRef.value.search(); // 搜索
+	queryList();
+
+	setTimeout(() => {
+		treeSearchRef.value.setExpandAll(false); // 默认全部收起
+	}, 300);
+}, 500);
+onMounted(() => {
+	getType();
+	queryList();
+	querySearchNum();
+});
+</script>
+
+<style scoped lang="scss">
+.case-retrieval-container {
+	.left-container {
+		height: 100%;
+	}
+	.center-container {
+		height: 100%;
+		display: flex;
+		flex-direction: column;
+		.input-box {
+			display: flex;
+		}
+		.retrieval-content {
+			&-item {
+				border-bottom: var(--el-border);
+				padding: 10px 15px;
+				margin-bottom: 10px;
+				cursor: pointer;
+				&:last-child {
+					margin-bottom: 0;
+					border: none;
+				}
+				&:hover {
+					color: var(--el-color-primary);
+				}
+			}
+		}
+	}
+	.right-container {
+		height: 100%;
+		.top10 {
+			&-items {
+				margin-bottom: 20px;
+				&:last-child {
+					margin-bottom: 0;
+				}
+				&-title {
+					flex: 1;
+					overflow: hidden;
+					text-overflow: ellipsis;
+					white-space: nowrap;
+				}
+				&-num {
+					display: inline-block;
+					width: 50px;
+					text-align: center;
+				}
+			}
+		}
+	}
+	:deep(.el-tree-node__content) {
+		height: 32px;
+	}
+	:deep(.el-card__body) {
+		height: 100%;
+	}
+	.keyword-box {
+		display: flex;
+		flex: 1;
+		flex-wrap: wrap;
+		line-height: 18px;
+		.keyword-item {
+			margin-right: 5px;
+			cursor: pointer;
+			color: var(--el-color-info);
+			font-size: var(--el-font-size-extra-small);
+			&:last-child {
+				margin-right: 0;
+			}
+			&:hover {
+				color: var(--el-color-primary);
+			}
+		}
+	}
+}
+</style>

+ 104 - 0
src/views/case/type/components/Type-add.vue

@@ -0,0 +1,104 @@
+<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="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="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="getKnowledgeList"
+						>
+						</el-cascader>
+					</el-form-item>
+				</el-col>
+				<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+					<el-form-item label="排序" prop="sort" :rules="[{ required: false, message: '请填写排序', trigger: 'blur' }]">
+						<el-input-number v-model="state.ruleForm.sort" :min="0" :precision="0" class="w100" placeholder="请填写排序" />
+					</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 { addCaseType } from '@/api/case/type';
+
+// 定义子组件向父组件传值/事件
+const emit = defineEmits(['updateList']);
+
+// 定义变量内容
+const state = reactive<any>({
+	dialogVisible: false, // 弹窗
+	ruleForm: {
+		name: '', // 类型名称
+		sort: 0, // 排序
+		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 getKnowledgeList = () => {
+	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;
+		addCaseType(state.ruleForm)
+			.then(() => {
+				emit('updateList');
+				closeDialog(); // 关闭弹窗
+				ElMessage.success('操作成功');
+				state.loading = false;
+			})
+			.catch(() => {
+				state.loading = false;
+			});
+	});
+};
+// 暴露变量
+defineExpose({
+	openDialog,
+	closeDialog,
+});
+</script>

+ 139 - 0
src/views/case/type/components/Type-connect.vue

@@ -0,0 +1,139 @@
+<template>
+	<el-dialog title="关联组织" v-model="state.dialogVisible" draggable append-to-body destroy-on-close @close="close">
+		<div class="border-box">
+			<el-input v-model="filterText" placeholder="组织名称" clearable class="mb20" />
+			<el-tree-v2
+				:data="orgData"
+				:height="400"
+				:props="{ label: 'name' }"
+				node-key="id"
+				show-checkbox
+				check-strictly
+				check-on-click-node
+				:default-checked-keys="state.ruleForm.orgArray"
+				:default-expanded-keys="state.ruleForm.orgArray"
+				:filter-method="filterMethod"
+				ref="orgRef"
+				v-loading="state.loading"
+				@check="changeOrgData"
+			/>
+		</div>
+		<template #footer>
+			<span class="dialog-footer">
+				<el-button @click="closeDialog" class="default-button">取 消</el-button>
+				<el-button type="primary" @click="onSubmit" :loading="state.loading">确 定 </el-button>
+			</span>
+		</template>
+	</el-dialog>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref, watch } from 'vue';
+import { ElMessage } from 'element-plus';
+import { getCanUseOrg } from '@/api/system/user';
+import { caseTypeDetail, updateCaseType } from '@/api/case/type';
+
+// 定义子组件向父组件传值/事件
+const emit = defineEmits(['updateList']);
+
+// 定义变量内容
+const state = reactive<any>({
+	dialogVisible: false, // 弹窗
+	ruleForm: {
+		name: '', // 类型名称
+		sort: 0, // 排序
+		parentId: '', // 上级类型
+		orgArray: [],
+	},
+	loading: false, // 加载
+});
+const filterText = ref('');
+watch(filterText, (val) => {
+	orgRef.value!.filter(val);
+});
+const filterMethod = (value: string, data: any) => {
+	if (!value) return true;
+	return data.name.includes(value);
+};
+// 获取机构数据
+const orgData = ref<any[]>([]);
+const getOrgData = async () => {
+	state.loading = true;
+	getCanUseOrg()
+		.then((res: any) => {
+			orgData.value = res?.result ?? [];
+			state.loading = false;
+		})
+		.catch(() => {
+			state.loading = false;
+		});
+};
+// 打开弹窗
+const ruleFormRef = ref<any>(); // 表单ref
+const openDialog = async (row: any) => {
+	try {
+		state.dialogVisible = true;
+		state.ruleForm.orgArray = [];
+		await getOrgData();
+		const { result } = await caseTypeDetail(row.id);
+		state.ruleForm = result;
+		state.ruleForm.orgArray = result.caseTypeOrgs.map((item: any) => item.orgId);
+		state.ruleForm.typeOrgDtos = result.caseTypeOrgs.map((item: any) => {
+			return {
+				orgId: item.orgId,
+				orgName: item.orgName,
+			};
+		});
+	} catch (error) {
+		// 打印错误信息
+		console.error(error);
+	}
+};
+const close = () => {
+	ruleFormRef.value?.clearValidate();
+	ruleFormRef.value?.resetFields();
+	filterText.value = '';
+};
+// 关闭弹窗
+const closeDialog = () => {
+	state.dialogVisible = false;
+};
+// 选择适用部门
+const orgRef = ref<RefType>();
+const changeOrgData = (data: any, object: any) => {
+	if (object.checkedNodes.length === 0) state.ruleForm.typeOrgDtos = [];
+	state.ruleForm.typeOrgDtos = object.checkedNodes.map((item: any) => {
+		return {
+			orgId: item.id,
+			orgName: item.name,
+		};
+	});
+};
+//
+// 保存
+const onSubmit = () => {
+	state.loading = true;
+	updateCaseType(state.ruleForm)
+		.then(() => {
+			ElMessage.success('操作成功');
+			state.loading = false;
+			emit('updateList');
+			closeDialog(); // 关闭弹窗
+		})
+		.catch(() => {
+			state.loading = false;
+		});
+};
+// 暴露变量
+defineExpose({
+	openDialog,
+	closeDialog,
+});
+</script>
+<style scoped lang="scss">
+.border-box {
+	border: var(--el-border);
+	border-radius: var(--el-border-radius-base);
+	padding: 15px;
+}
+</style>

+ 114 - 0
src/views/case/type/components/Type-edit.vue

@@ -0,0 +1,114 @@
+<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="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="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="getKnowledgeList"
+						>
+						</el-cascader>
+					</el-form-item>
+				</el-col>
+				<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+					<el-form-item label="排序" prop="sort" :rules="[{ required: false, message: '请填写排序', trigger: 'blur' }]">
+						<el-input-number v-model="state.ruleForm.sort" :min="0" :precision="0" class="w100" placeholder="请填写排序" />
+					</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 { caseTypeDetail, updateCaseType } from '@/api/case/type';
+
+// 定义子组件向父组件传值/事件
+const emit = defineEmits(['updateList']);
+
+// 定义变量内容
+const state = reactive<any>({
+	dialogVisible: false, // 弹窗
+	ruleForm: {
+		name: '', // 类型名称
+		sort: 0, // 排序
+		parentId: '', // 上级类型
+	},
+	treeData: [], // 上级
+	loading: false, // 加载
+});
+// 打开弹窗
+const ruleFormRef = ref<any>(); // 表单ref
+const openDialog = async (row: any, treeData: any) => {
+	try {
+		state.dialogVisible = true;
+		const { result } = await caseTypeDetail(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 getKnowledgeList = () => {
+	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;
+		updateCaseType(state.ruleForm)
+			.then(() => {
+				closeDialog(); // 关闭弹窗
+				ElMessage.success('操作成功');
+				state.loading = false;
+				emit('updateList');
+			})
+			.catch(() => {
+				state.loading = false;
+			});
+	});
+};
+// 暴露变量
+defineExpose({
+	openDialog,
+	closeDialog,
+});
+</script>

+ 200 - 0
src/views/case/type/index.vue

@@ -0,0 +1,200 @@
+<template>
+	<div class="case-type-container layout-padding">
+		<div class="layout-padding-auto layout-padding-view pd20">
+			<el-form :model="state.queryParams" ref="ruleFormRef" inline @submit.native.prevent>
+				<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="'case:type: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="caseType"
+					: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 field="sort" title="排序" width="80"></vxe-column>
+					<vxe-column field="lastModificationTime" title="更新时间" width="160">
+						<template #default="{ row }">
+							{{ formatDate(row.lastModificationTime, 'YYYY-mm-dd HH:MM:SS') }}
+						</template>
+					</vxe-column>
+					<vxe-column field="isEnable" title="状态" width="120" align="center">
+						<template #default="{ row }">
+							<el-tag :type="row.isEnable ? 'success' : 'info'">{{ row.isEnable ? '启用' : '禁用' }}</el-tag>
+						</template>
+					</vxe-column>
+					<vxe-column title="操作" fixed="right" width="180" align="center" :show-overflow="false">
+						<template #default="{ row }">
+							<el-button link type="primary" @click="onConnect(row)" title="关联组织" v-auth="'case:type:connect'"> 关联组织 </el-button>
+							<el-button link type="primary" @click="onOpenEdit(row)" title="编辑" v-auth="'case:type:edit'"> 编辑 </el-button>
+							<el-button link type="danger" @click="onDelete(row)" title="删除" v-auth="'case:type:delete'"> 删除 </el-button>
+						</template>
+					</vxe-column>
+				</vxe-table>
+			</div>
+		</div>
+		<type-add ref="typeAddRef" @updateList="queryList" />
+		<type-edit ref="typeEditRef" @updateList="queryList" />
+		<type-connect ref="typeConnectRef" @updateList="queryList" />
+	</div>
+</template>
+<script lang="tsx" setup name="caseType">
+import { defineAsyncComponent, nextTick, onMounted, reactive, ref } from 'vue';
+import type { FormInstance } from 'element-plus';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import { formatDate } from '@/utils/formatTime';
+import { throttle } from '@/utils/tools';
+import XEUtils from 'xe-utils';
+import { caseTreeList, delCaseType } from '@/api/case/type';
+// 引入组件
+const TypeAdd = defineAsyncComponent(() => import('@/views/case/type/components/Type-add.vue')); // 新增组件
+const TypeEdit = defineAsyncComponent(() => import('@/views/case/type/components/Type-edit.vue')); // 修改组件
+const TypeConnect = defineAsyncComponent(() => import('@/views/case/type/components/Type-connect.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 typeAddRef = ref<RefType>(); // 新增ref
+const onOpenAdd = () => {
+	typeAddRef.value.openDialog(tableData.value);
+};
+// 打开编辑分类弹窗
+const typeEditRef = ref<RefType>(); // 修改ref
+const onOpenEdit = (row: any) => {
+	typeEditRef.value.openDialog(row, tableData.value);
+};
+
+// 删除当前行
+const onDelete = (row: any) => {
+	ElMessageBox.confirm(`是否确认删除该类型?删除后不可恢复`, '提示', {
+		confirmButtonText: '确定',
+		cancelButtonText: '取消',
+		type: 'warning',
+	})
+		.then(() => {
+			delCaseType(row.id).then(() => {
+				ElMessage.success('删除成功');
+				queryList();
+			});
+		})
+		.catch(() => {});
+};
+// 关联组织
+const typeConnectRef = ref<RefType>();
+const onConnect = (row: any) => {
+	typeConnectRef.value.openDialog(row);
+};
+// 获取知识库类型
+const tableRef = ref<RefType>();
+const tableData = ref<EmptyArrayType>([]);
+const list = ref<any[]>([]);
+const queryList = () => {
+	state.loading = true;
+	caseTreeList()
+		.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>

+ 1 - 1
src/views/plan/index/edit.vue

@@ -274,7 +274,7 @@ const onPreview = () => {
 		state.ruleForm.creationTime = new Date();
 		state.ruleForm.creatorOrgName = userInfos.value?.orgName ?? '';
 	}
-	Local.set('previewForm', state.ruleForm);
+	Local.set('planPreviewForm', state.ruleForm);
 	router.push({
 		name: 'planPreview',
 		params: {

+ 3 - 3
src/views/plan/index/preview.vue

@@ -37,7 +37,7 @@
 					<el-divider />
 					<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mt10">
 						<el-form-item label="附件">
-							<annex-list name="案附件" v-model="state.info.files" readonly classify="案附件" />
+							<annex-list name="案附件" v-model="state.info.files" readonly classify="案附件" />
 						</el-form-item>
 					</el-col>
 				</template>
@@ -103,7 +103,7 @@ const getInfo = async () => {
 			loading.value = false;
 		}
 	} else {
-		state.info = Local.get('previewForm') ?? {};
+		state.info = Local.get('planPreviewForm') ?? {};
 		state.info.files = transformFile(state.info.files);
 		loading.value = false;
 	}
@@ -150,7 +150,7 @@ onMounted(() => {
 	getInfo();
 });
 onBeforeUnmount(() => {
-	Local.remove('previewForm'); // 删除本地缓存
+	Local.remove('planPreviewForm'); // 删除本地缓存
 });
 </script>
 <style lang="scss" scoped>

+ 1 - 1
src/views/plan/retrieval/index.vue

@@ -238,7 +238,7 @@ const resetQuery = throttle(() => {
 	state.queryParams.PlanTypeID = null;
 	state.queryParams.Title = null;
 	state.queryParams.Content = null;
-	treeSearchRef.value.setSelected(state.queryParams.OrgCode, false); // 清空选择
+	treeSearchRef.value.setSelected(state.queryParams.PlanTypeID, false); // 清空选择
 	treeSearchRef.value.clearKeyword(); // 清空搜索关键词
 	treeSearchRef.value.search(); // 搜索
 	queryList();