|
|
@@ -1,198 +1,310 @@
|
|
|
<template>
|
|
|
- <div class="table-box">
|
|
|
- <ProTable ref="proTable" :columns="columns" :request-api="getTableList" :data-callback="dataCallback">
|
|
|
- <template #expertStatus="scope">
|
|
|
- <el-tag v-if="scope.row.expertStatus == 0" type="success"> 已通过 </el-tag>
|
|
|
- <el-tag v-if="scope.row.expertStatus == 1" type="primary"> 待审核 </el-tag>
|
|
|
- <el-tag v-if="scope.row.expertStatus == 2" type="danger"> 已驳回 </el-tag>
|
|
|
- </template>
|
|
|
- <template #promoteType="scope">
|
|
|
- <el-tag v-for="(item, index) in getPromoteTypes(scope.row.promoteType)" :key="index">
|
|
|
- {{ item }}
|
|
|
- </el-tag>
|
|
|
- </template>
|
|
|
-
|
|
|
- <template #operation="scope">
|
|
|
- <el-button type="primary" :icon="Search" link @click="handleDetail(scope.row)"> 查看详情 </el-button>
|
|
|
- <el-button type="primary" :icon="Setting" v-if="scope.row.expertStatus == 1" link @click="handleReview(scope.row)">
|
|
|
- 审核
|
|
|
- </el-button>
|
|
|
- </template>
|
|
|
- </ProTable>
|
|
|
-
|
|
|
- <ReviewDialog ref="reviewDialog" @approve="handleApprove" @reject="handleReject" />
|
|
|
- <DetailDialog ref="detailDialog" />
|
|
|
+ <div class="scene-page">
|
|
|
+ <div class="scene-toolbar">
|
|
|
+ <el-button type="primary" :icon="CirclePlus" @click="handleCreate"> 新增场景 </el-button>
|
|
|
+ <div class="scene-search">
|
|
|
+ <el-input v-model="filterText" placeholder="请输入场景分类" clearable />
|
|
|
+ <el-button type="primary" @click="handleSearch"> 搜索 </el-button>
|
|
|
+ <el-button @click="handleReset"> 重置 </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="scene-header">
|
|
|
+ <span>场景分类</span>
|
|
|
+ <span>状态</span>
|
|
|
+ <span>操作</span>
|
|
|
+ </div>
|
|
|
+ <div class="scene-tree">
|
|
|
+ <el-tree
|
|
|
+ ref="treeRef"
|
|
|
+ :data="treeData"
|
|
|
+ node-key="id"
|
|
|
+ :props="treeProps"
|
|
|
+ :filter-node-method="filterNode"
|
|
|
+ default-expand-all
|
|
|
+ >
|
|
|
+ <template #default="{ data }">
|
|
|
+ <span class="scene-node">
|
|
|
+ <span class="scene-name">{{ data.name }}</span>
|
|
|
+ <span class="scene-status">
|
|
|
+ <el-tag size="small" :type="data.status === 1 ? 'success' : 'info'">
|
|
|
+ {{ data.status === 1 ? "启用" : "停用" }}
|
|
|
+ </el-tag>
|
|
|
+ </span>
|
|
|
+ <span class="scene-actions">
|
|
|
+ <el-button type="primary" link size="small" :icon="CirclePlus" @click.stop="handleAddChild(data)">
|
|
|
+ 新增子场景
|
|
|
+ </el-button>
|
|
|
+ <el-button type="primary" link size="small" :icon="EditPen" @click.stop="handleEdit(data)"> 编辑 </el-button>
|
|
|
+ <el-button type="danger" link size="small" :icon="Delete" @click.stop="handleDelete(data)"> 删除 </el-button>
|
|
|
+ </span>
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ </el-tree>
|
|
|
+ </div>
|
|
|
+ <SceneDialog ref="sceneDialogRef" @success="loadSceneList" />
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
-import ReviewDialog from "./reviewDialog.vue";
|
|
|
-import DetailDialog from "./detailDialog.vue";
|
|
|
-import { ref, reactive, onActivated } from "vue";
|
|
|
-import type { Course } from "@/api/interface";
|
|
|
-import { ElMessage } from "element-plus";
|
|
|
-import ProTable from "@/components/ProTable/index.vue";
|
|
|
-import type { ProTableInstance, ColumnProps } from "@/components/ProTable/interface";
|
|
|
-import { becomeExpert } from "@/api/modules/user";
|
|
|
-import { getApplicationExpertList } from "@/api/modules/masterManagemen";
|
|
|
-import { Search, Setting } from "@element-plus/icons-vue";
|
|
|
-
|
|
|
-const proTable = ref<ProTableInstance>();
|
|
|
-
|
|
|
-const reviewDialog = ref<any>(null);
|
|
|
-const detailDialog = ref<any>(null);
|
|
|
-// 门店审核状态列表
|
|
|
-const applyStatus = ref<any[]>([
|
|
|
- { value: 0, label: "审核通过" },
|
|
|
- { value: 1, label: "待审核" },
|
|
|
- { value: 2, label: "审核拒绝" }
|
|
|
-]);
|
|
|
-
|
|
|
-const columns = reactive<ColumnProps<Course.ReqCourseParams>[]>([
|
|
|
- {
|
|
|
- label: "序号",
|
|
|
- type: "index",
|
|
|
- width: 60,
|
|
|
- align: "center"
|
|
|
- },
|
|
|
- {
|
|
|
- label: "ID",
|
|
|
- prop: "id",
|
|
|
- width: 80
|
|
|
- },
|
|
|
- {
|
|
|
- label: "用户昵称",
|
|
|
- prop: "userName",
|
|
|
- search: { el: "input", tooltip: "请输入用户昵称" }
|
|
|
- },
|
|
|
- {
|
|
|
- label: "姓名",
|
|
|
- prop: "realName",
|
|
|
- search: { el: "input", tooltip: "请输入姓名" }
|
|
|
- },
|
|
|
- {
|
|
|
- label: "联系电话",
|
|
|
- prop: "userPhone",
|
|
|
- search: { el: "input", tooltip: "请输入手机号码" }
|
|
|
- },
|
|
|
- {
|
|
|
- label: "推广板块",
|
|
|
- prop: "promoteType"
|
|
|
- },
|
|
|
- {
|
|
|
- label: "状态",
|
|
|
- prop: "expertStatus",
|
|
|
- search: { el: "select", tooltip: "请输入状态" },
|
|
|
- enum: applyStatus,
|
|
|
- fieldNames: { label: "label", value: "value" }
|
|
|
- },
|
|
|
- {
|
|
|
- label: "申请时间",
|
|
|
- prop: "createdTime"
|
|
|
- },
|
|
|
- {
|
|
|
- label: "申请时间",
|
|
|
- prop: "time",
|
|
|
- isShow: false, // 关键:不在表格中显示
|
|
|
- search: {
|
|
|
- label: "申请时间范围", // 搜索区域显示的标签
|
|
|
- el: "date-picker",
|
|
|
- props: {
|
|
|
- type: "datetimerange",
|
|
|
- valueFormat: "YYYY-MM-DD HH:mm:ss",
|
|
|
- rangeSeparator: "至",
|
|
|
- startPlaceholder: "开始时间",
|
|
|
- endPlaceholder: "结束时间"
|
|
|
+import { ref, reactive, onMounted, watch, nextTick } from "vue";
|
|
|
+import { ElMessage, ElMessageBox, ElTree } from "element-plus";
|
|
|
+import { CirclePlus, Delete, EditPen } from "@element-plus/icons-vue";
|
|
|
+import { getScenePage, addScene, editScene, deleteScene } from "@/api/modules/lawyer";
|
|
|
+import SceneDialog from "./components/SceneDialog.vue";
|
|
|
+
|
|
|
+const treeProps = { children: "children", label: "name" };
|
|
|
+const treeRef = ref<InstanceType<typeof ElTree>>();
|
|
|
+const sceneDialogRef = ref<InstanceType<typeof SceneDialog>>();
|
|
|
+const treeData = ref<any[]>([]);
|
|
|
+const filterText = ref("");
|
|
|
+
|
|
|
+const getLevelValue = (level: string | number | null | undefined) => {
|
|
|
+ if (typeof level === "number") return level;
|
|
|
+ if (typeof level === "string") {
|
|
|
+ const match = level.match(/(\d+)/);
|
|
|
+ return match ? Number(match[1]) : 1;
|
|
|
+ }
|
|
|
+ return 1;
|
|
|
+};
|
|
|
+
|
|
|
+// 通过parentId 和 level 判断层级
|
|
|
+const buildSceneTree = (list: any[] = []) => {
|
|
|
+ if (!Array.isArray(list)) return [];
|
|
|
+
|
|
|
+ const nodeMap = new Map<string | number, any>();
|
|
|
+ const levelStore = new Map<string | number, number>();
|
|
|
+ const groupLevelMap = new Map<string | number | symbol, Map<number, any[]>>();
|
|
|
+ const ROOT_KEY = Symbol("scene_root_key");
|
|
|
+
|
|
|
+ const getGroupKey = (parentId: string | number | null | undefined): string | number | symbol => {
|
|
|
+ if (parentId === undefined || parentId === null || parentId === "") return ROOT_KEY;
|
|
|
+ return parentId;
|
|
|
+ };
|
|
|
+
|
|
|
+ list.forEach(item => {
|
|
|
+ const levelValue = getLevelValue(item.level);
|
|
|
+ const node = { ...item, children: [] as any[] };
|
|
|
+ nodeMap.set(item.id, node);
|
|
|
+ levelStore.set(item.id, levelValue);
|
|
|
+
|
|
|
+ const key = getGroupKey(item.parentId);
|
|
|
+ if (!groupLevelMap.has(key)) {
|
|
|
+ groupLevelMap.set(key, new Map());
|
|
|
+ }
|
|
|
+ const levelMap = groupLevelMap.get(key)!;
|
|
|
+ if (!levelMap.has(levelValue)) {
|
|
|
+ levelMap.set(levelValue, []);
|
|
|
+ }
|
|
|
+ levelMap.get(levelValue)!.push(node);
|
|
|
+ });
|
|
|
+
|
|
|
+ const findParentByGroup = (node: any) => {
|
|
|
+ const nodeLevel = levelStore.get(node.id) || 1;
|
|
|
+ if (nodeLevel <= 1) return undefined;
|
|
|
+ const key = getGroupKey(node.parentId);
|
|
|
+ const levelMap = groupLevelMap.get(key);
|
|
|
+ if (!levelMap) return undefined;
|
|
|
+ const candidates = levelMap.get(nodeLevel - 1);
|
|
|
+ if (!candidates || !candidates.length) return undefined;
|
|
|
+ // 默认选取该层级中最后一个节点作为父节点
|
|
|
+ return candidates[candidates.length - 1];
|
|
|
+ };
|
|
|
+
|
|
|
+ const roots: any[] = [];
|
|
|
+ list.forEach(item => {
|
|
|
+ const node = nodeMap.get(item.id);
|
|
|
+ if (!node) return;
|
|
|
+ const nodeLevel = levelStore.get(item.id) || 1;
|
|
|
+ let parent: any | undefined;
|
|
|
+
|
|
|
+ if (item.parentId && nodeMap.has(item.parentId)) {
|
|
|
+ const candidate = nodeMap.get(item.parentId);
|
|
|
+ if (candidate) {
|
|
|
+ const candidateLevel = levelStore.get(candidate.id) || 1;
|
|
|
+ if (candidate !== node && candidateLevel === nodeLevel - 1) {
|
|
|
+ parent = candidate;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
- },
|
|
|
- {
|
|
|
- label: "操作",
|
|
|
- prop: "operation",
|
|
|
- width: 200
|
|
|
- }
|
|
|
-]);
|
|
|
-const getTableList = async (params: any) => {
|
|
|
- let tempParams = JSON.parse(JSON.stringify(params));
|
|
|
- delete tempParams.time;
|
|
|
- // 深拷贝原始参数
|
|
|
- let newParams = JSON.parse(JSON.stringify(tempParams));
|
|
|
- newParams.page = newParams.pageNum;
|
|
|
- newParams.size = newParams.pageSize;
|
|
|
- delete newParams.pageNum;
|
|
|
- delete newParams.pageSize;
|
|
|
- if (params.time) {
|
|
|
- newParams.createdTime = params.time[0];
|
|
|
- newParams.endTime = params.time[1];
|
|
|
- }
|
|
|
|
|
|
- const res = await getApplicationExpertList(newParams);
|
|
|
- return res;
|
|
|
+ if (!parent) {
|
|
|
+ const groupParent = findParentByGroup(node);
|
|
|
+ if (groupParent && groupParent !== node) {
|
|
|
+ parent = groupParent;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (parent) {
|
|
|
+ parent.children = parent.children || [];
|
|
|
+ parent.children.push(node);
|
|
|
+ node.parentName = parent.name;
|
|
|
+ } else {
|
|
|
+ roots.push(node);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ return roots;
|
|
|
};
|
|
|
-const dataCallback = (data: any) => {
|
|
|
- return {
|
|
|
- list: data.records,
|
|
|
- total: data.total
|
|
|
- };
|
|
|
+
|
|
|
+const loadSceneList = async () => {
|
|
|
+ const res: any = await getScenePage({ page: 1, size: 999 });
|
|
|
+ const flatList = res?.records || res?.list || res?.data?.records || [];
|
|
|
+ treeData.value = buildSceneTree(flatList);
|
|
|
+ nextTick(() => {
|
|
|
+ if (filterText.value && treeRef.value) {
|
|
|
+ treeRef.value.filter(filterText.value);
|
|
|
+ }
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+const handleCreate = () => {
|
|
|
+ sceneDialogRef.value?.open({
|
|
|
+ title: "新增场景",
|
|
|
+ onSubmit: async payload => {
|
|
|
+ let params = {
|
|
|
+ name: payload.name,
|
|
|
+ status: payload.status,
|
|
|
+ level: 1
|
|
|
+ };
|
|
|
+ await addScene(params);
|
|
|
+ ElMessage.success("新增成功");
|
|
|
+ loadSceneList();
|
|
|
+ }
|
|
|
+ });
|
|
|
};
|
|
|
|
|
|
-// 处理推广板块字符串
|
|
|
-const getPromoteTypes = (promoteType: string) => {
|
|
|
- if (!promoteType) return [];
|
|
|
- return promoteType.split(",");
|
|
|
+const handleAddChild = (row: any) => {
|
|
|
+ console.log("row", row);
|
|
|
+ sceneDialogRef.value?.open({
|
|
|
+ title: `给【${row.name}】新增子场景`,
|
|
|
+ row,
|
|
|
+ type: "add",
|
|
|
+ onSubmit: async payload => {
|
|
|
+ console.log("payload add", payload);
|
|
|
+ let params = {
|
|
|
+ name: payload.name,
|
|
|
+ parentId: row.id,
|
|
|
+ status: payload.status,
|
|
|
+ level: payload.level ? payload.level + 1 : 1,
|
|
|
+ parentName: payload.parentName
|
|
|
+ };
|
|
|
+ await addScene(params);
|
|
|
+ ElMessage.success("新增子场景成功");
|
|
|
+ loadSceneList();
|
|
|
+ }
|
|
|
+ });
|
|
|
};
|
|
|
|
|
|
-//详情
|
|
|
-const handleDetail = row => {
|
|
|
- detailDialog.value?.open(row.userId);
|
|
|
+const handleEdit = (row: any) => {
|
|
|
+ sceneDialogRef.value?.open({
|
|
|
+ title: "编辑场景",
|
|
|
+ row,
|
|
|
+ onSubmit: async payload => {
|
|
|
+ console.log("payload edit", payload);
|
|
|
+ let params = {
|
|
|
+ name: payload.name,
|
|
|
+ id: payload.id,
|
|
|
+ status: payload.status,
|
|
|
+ level: payload.level
|
|
|
+ };
|
|
|
+ await editScene(params);
|
|
|
+ ElMessage.success("编辑成功");
|
|
|
+ loadSceneList();
|
|
|
+ }
|
|
|
+ });
|
|
|
};
|
|
|
|
|
|
-// 审核
|
|
|
-const handleReview = (row: any) => {
|
|
|
- // 打开弹窗并传入当前行数据
|
|
|
- reviewDialog.value?.open(row);
|
|
|
+const handleDelete = (row: any) => {
|
|
|
+ const hasChildren = Array.isArray(row.children) && row.children.length > 0;
|
|
|
+ const message = hasChildren
|
|
|
+ ? `场景【${row.name}】包含子场景,删除后将一并删除所有子场景,是否继续?`
|
|
|
+ : `确认删除场景【${row.name}】吗?`;
|
|
|
+ ElMessageBox.confirm(message, "提示", {
|
|
|
+ type: "warning"
|
|
|
+ })
|
|
|
+ .then(async () => {
|
|
|
+ await deleteScene({ id: row.id });
|
|
|
+ ElMessage.success("删除成功");
|
|
|
+ loadSceneList();
|
|
|
+ })
|
|
|
+ .catch(() => {});
|
|
|
};
|
|
|
|
|
|
-// 处理同意操作 expertStatus: 0 包含佣金和预付款比例
|
|
|
-const handleApprove = async (payload: { id: number; commissionRate: number; advanceRate: number; userPhone: string }) => {
|
|
|
- try {
|
|
|
- await becomeExpert({
|
|
|
- id: payload.id,
|
|
|
- expertStatus: 0,
|
|
|
- commissionRate: payload.commissionRate,
|
|
|
- advanceRate: payload.advanceRate,
|
|
|
- userPhone: payload.userPhone
|
|
|
- });
|
|
|
-
|
|
|
- ElMessage.success("审核通过");
|
|
|
- proTable.value?.getTableList();
|
|
|
- } catch (error) {
|
|
|
- console.error("审核通过失败:", error);
|
|
|
- ElMessage.error("审核通过失败");
|
|
|
- }
|
|
|
+const filterNode = (value: string, data: any) => {
|
|
|
+ if (!value) return true;
|
|
|
+ return data.name?.includes(value);
|
|
|
};
|
|
|
|
|
|
-// 处理驳回操作 expertStatus: 2 包含驳回原因
|
|
|
-const handleReject = async (payload: { id: number; comment: string; userPhone: string }) => {
|
|
|
- try {
|
|
|
- await becomeExpert({
|
|
|
- id: payload.id,
|
|
|
- expertStatus: 2,
|
|
|
- comment: payload.comment,
|
|
|
- userPhone: payload.userPhone
|
|
|
- });
|
|
|
-
|
|
|
- ElMessage.success("审核驳回");
|
|
|
- proTable.value?.getTableList();
|
|
|
- } catch (error) {
|
|
|
- console.error("审核驳回失败:", error);
|
|
|
- ElMessage.error("审核驳回失败");
|
|
|
- }
|
|
|
+const handleSearch = () => {
|
|
|
+ treeRef.value?.filter(filterText.value);
|
|
|
};
|
|
|
|
|
|
-onActivated(() => {
|
|
|
- proTable.value?.getTableList();
|
|
|
+const handleReset = () => {
|
|
|
+ filterText.value = "";
|
|
|
+ treeRef.value?.filter("");
|
|
|
+};
|
|
|
+
|
|
|
+watch(filterText, val => {
|
|
|
+ if (!val) treeRef.value?.filter("");
|
|
|
+});
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ loadSceneList();
|
|
|
});
|
|
|
</script>
|
|
|
|
|
|
-<style scoped lang="scss"></style>
|
|
|
+<style scoped lang="scss">
|
|
|
+.scene-page {
|
|
|
+ padding: 16px;
|
|
|
+}
|
|
|
+.scene-toolbar {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 12px;
|
|
|
+}
|
|
|
+.scene-search {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ width: 360px;
|
|
|
+}
|
|
|
+.scene-header {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 3fr 1fr 1fr;
|
|
|
+ padding: 12px 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ background-color: var(--el-fill-color-light);
|
|
|
+ border: 1px solid var(--el-border-color);
|
|
|
+ border-bottom: none;
|
|
|
+ border-radius: 4px 4px 0 0;
|
|
|
+}
|
|
|
+.scene-tree {
|
|
|
+ max-height: 680px;
|
|
|
+ overflow: auto;
|
|
|
+ border: 1px solid var(--el-border-color);
|
|
|
+ border-top: none;
|
|
|
+ border-radius: 0 0 4px 4px;
|
|
|
+}
|
|
|
+.scene-node {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 3fr 1fr 1fr;
|
|
|
+ align-items: center;
|
|
|
+ width: 100%;
|
|
|
+ padding: 12px 16px;
|
|
|
+}
|
|
|
+.scene-name {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+.scene-status {
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-start;
|
|
|
+}
|
|
|
+.scene-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+:deep(.el-tree-node__content) {
|
|
|
+ height: auto;
|
|
|
+}
|
|
|
+</style>
|