| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328 |
- <template>
- <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 style="flex: 1">场景分类</span>
- <span style="width: 200px; text-align: center">展示图片</span>
- <span style="width: 200px; text-align: center">状态</span>
- <span style="width: 300px; text-align: center">操作</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-img">
- <el-image
- style="width: 70px; height: 70px"
- v-if="data.imgUrl"
- :src="data.imgUrl"
- class="scene-thumb"
- :preview-src-list="[data.imgUrl]"
- />
- </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 { 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;
- }
- }
- }
- 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 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,
- imgUrl: payload.imgUrl,
- level: 1
- };
- await addScene(params);
- ElMessage.success("新增成功");
- loadSceneList();
- }
- });
- };
- 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 handleEdit = (row: any) => {
- sceneDialogRef.value?.open({
- title: "编辑场景",
- row,
- onSubmit: async payload => {
- let params = {
- name: payload.name,
- id: payload.id,
- status: payload.status,
- level: payload.level,
- imgUrl: payload.imgUrl
- };
- await editScene(params);
- ElMessage.success("编辑成功");
- loadSceneList();
- }
- });
- };
- 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(() => {});
- };
- const filterNode = (value: string, data: any) => {
- if (!value) return true;
- return data.name?.includes(value);
- };
- const handleSearch = () => {
- treeRef.value?.filter(filterText.value);
- };
- const handleReset = () => {
- filterText.value = "";
- treeRef.value?.filter("");
- };
- watch(filterText, val => {
- if (!val) treeRef.value?.filter("");
- });
- onMounted(() => {
- loadSceneList();
- });
- </script>
- <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: flex;
- 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: flex;
- align-items: center;
- width: 100%;
- padding: 12px 16px;
- }
- .scene-name {
- display: flex;
- flex: 1;
- text-align: center;
- }
- .scene-img {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 200px;
- text-align: center;
- }
- .scene-status {
- width: 200px;
- padding-left: 4px;
- text-align: center;
- }
- .scene-actions {
- width: 300px;
- text-align: center;
- }
- :deep(.el-tree-node__content) {
- height: auto;
- }
- </style>
|