index.vue 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. <template>
  2. <div class="scene-page">
  3. <div class="scene-toolbar">
  4. <el-button type="primary" :icon="CirclePlus" @click="handleCreate"> 新增场景 </el-button>
  5. <div class="scene-search">
  6. <el-input v-model="filterText" placeholder="请输入场景分类" clearable />
  7. <el-button type="primary" @click="handleSearch"> 搜索 </el-button>
  8. <el-button @click="handleReset"> 重置 </el-button>
  9. </div>
  10. </div>
  11. <div class="scene-header">
  12. <span>场景分类</span>
  13. <span>状态</span>
  14. <span>操作</span>
  15. </div>
  16. <div class="scene-tree">
  17. <el-tree
  18. ref="treeRef"
  19. :data="treeData"
  20. node-key="id"
  21. :props="treeProps"
  22. :filter-node-method="filterNode"
  23. default-expand-all
  24. >
  25. <template #default="{ data }">
  26. <span class="scene-node">
  27. <span class="scene-name">{{ data.name }}</span>
  28. <span class="scene-status">
  29. <el-tag size="small" :type="data.status === 1 ? 'success' : 'info'">
  30. {{ data.status === 1 ? "启用" : "停用" }}
  31. </el-tag>
  32. </span>
  33. <span class="scene-actions">
  34. <el-button type="primary" link size="small" :icon="CirclePlus" @click.stop="handleAddChild(data)">
  35. 新增子场景
  36. </el-button>
  37. <el-button type="primary" link size="small" :icon="EditPen" @click.stop="handleEdit(data)"> 编辑 </el-button>
  38. <el-button type="danger" link size="small" :icon="Delete" @click.stop="handleDelete(data)"> 删除 </el-button>
  39. </span>
  40. </span>
  41. </template>
  42. </el-tree>
  43. </div>
  44. <SceneDialog ref="sceneDialogRef" @success="loadSceneList" />
  45. </div>
  46. </template>
  47. <script setup lang="ts">
  48. import { ref, reactive, onMounted, watch, nextTick } from "vue";
  49. import { ElMessage, ElMessageBox, ElTree } from "element-plus";
  50. import { CirclePlus, Delete, EditPen } from "@element-plus/icons-vue";
  51. import { getScenePage, addScene, editScene, deleteScene } from "@/api/modules/lawyer";
  52. import SceneDialog from "./components/SceneDialog.vue";
  53. const treeProps = { children: "children", label: "name" };
  54. const treeRef = ref<InstanceType<typeof ElTree>>();
  55. const sceneDialogRef = ref<InstanceType<typeof SceneDialog>>();
  56. const treeData = ref<any[]>([]);
  57. const filterText = ref("");
  58. const getLevelValue = (level: string | number | null | undefined) => {
  59. if (typeof level === "number") return level;
  60. if (typeof level === "string") {
  61. const match = level.match(/(\d+)/);
  62. return match ? Number(match[1]) : 1;
  63. }
  64. return 1;
  65. };
  66. // 通过parentId 和 level 判断层级
  67. const buildSceneTree = (list: any[] = []) => {
  68. if (!Array.isArray(list)) return [];
  69. const nodeMap = new Map<string | number, any>();
  70. const levelStore = new Map<string | number, number>();
  71. const groupLevelMap = new Map<string | number | symbol, Map<number, any[]>>();
  72. const ROOT_KEY = Symbol("scene_root_key");
  73. const getGroupKey = (parentId: string | number | null | undefined): string | number | symbol => {
  74. if (parentId === undefined || parentId === null || parentId === "") return ROOT_KEY;
  75. return parentId;
  76. };
  77. list.forEach(item => {
  78. const levelValue = getLevelValue(item.level);
  79. const node = { ...item, children: [] as any[] };
  80. nodeMap.set(item.id, node);
  81. levelStore.set(item.id, levelValue);
  82. const key = getGroupKey(item.parentId);
  83. if (!groupLevelMap.has(key)) {
  84. groupLevelMap.set(key, new Map());
  85. }
  86. const levelMap = groupLevelMap.get(key)!;
  87. if (!levelMap.has(levelValue)) {
  88. levelMap.set(levelValue, []);
  89. }
  90. levelMap.get(levelValue)!.push(node);
  91. });
  92. const findParentByGroup = (node: any) => {
  93. const nodeLevel = levelStore.get(node.id) || 1;
  94. if (nodeLevel <= 1) return undefined;
  95. const key = getGroupKey(node.parentId);
  96. const levelMap = groupLevelMap.get(key);
  97. if (!levelMap) return undefined;
  98. const candidates = levelMap.get(nodeLevel - 1);
  99. if (!candidates || !candidates.length) return undefined;
  100. // 默认选取该层级中最后一个节点作为父节点
  101. return candidates[candidates.length - 1];
  102. };
  103. const roots: any[] = [];
  104. list.forEach(item => {
  105. const node = nodeMap.get(item.id);
  106. if (!node) return;
  107. const nodeLevel = levelStore.get(item.id) || 1;
  108. let parent: any | undefined;
  109. if (item.parentId && nodeMap.has(item.parentId)) {
  110. const candidate = nodeMap.get(item.parentId);
  111. if (candidate) {
  112. const candidateLevel = levelStore.get(candidate.id) || 1;
  113. if (candidate !== node && candidateLevel === nodeLevel - 1) {
  114. parent = candidate;
  115. }
  116. }
  117. }
  118. if (!parent) {
  119. const groupParent = findParentByGroup(node);
  120. if (groupParent && groupParent !== node) {
  121. parent = groupParent;
  122. }
  123. }
  124. if (parent) {
  125. parent.children = parent.children || [];
  126. parent.children.push(node);
  127. node.parentName = parent.name;
  128. } else {
  129. roots.push(node);
  130. }
  131. });
  132. return roots;
  133. };
  134. const loadSceneList = async () => {
  135. const res: any = await getScenePage({ page: 1, size: 999 });
  136. const flatList = res?.records || res?.list || res?.data?.records || [];
  137. treeData.value = buildSceneTree(flatList);
  138. nextTick(() => {
  139. if (filterText.value && treeRef.value) {
  140. treeRef.value.filter(filterText.value);
  141. }
  142. });
  143. };
  144. const handleCreate = () => {
  145. sceneDialogRef.value?.open({
  146. title: "新增场景",
  147. onSubmit: async payload => {
  148. let params = {
  149. name: payload.name,
  150. status: payload.status,
  151. level: 1
  152. };
  153. await addScene(params);
  154. ElMessage.success("新增成功");
  155. loadSceneList();
  156. }
  157. });
  158. };
  159. const handleAddChild = (row: any) => {
  160. console.log("row", row);
  161. sceneDialogRef.value?.open({
  162. title: `给【${row.name}】新增子场景`,
  163. row,
  164. type: "add",
  165. onSubmit: async payload => {
  166. console.log("payload add", payload);
  167. let params = {
  168. name: payload.name,
  169. parentId: row.id,
  170. status: payload.status,
  171. level: payload.level ? payload.level + 1 : 1,
  172. parentName: payload.parentName
  173. };
  174. await addScene(params);
  175. ElMessage.success("新增子场景成功");
  176. loadSceneList();
  177. }
  178. });
  179. };
  180. const handleEdit = (row: any) => {
  181. sceneDialogRef.value?.open({
  182. title: "编辑场景",
  183. row,
  184. onSubmit: async payload => {
  185. console.log("payload edit", payload);
  186. let params = {
  187. name: payload.name,
  188. id: payload.id,
  189. status: payload.status,
  190. level: payload.level
  191. };
  192. await editScene(params);
  193. ElMessage.success("编辑成功");
  194. loadSceneList();
  195. }
  196. });
  197. };
  198. const handleDelete = (row: any) => {
  199. const hasChildren = Array.isArray(row.children) && row.children.length > 0;
  200. const message = hasChildren
  201. ? `场景【${row.name}】包含子场景,删除后将一并删除所有子场景,是否继续?`
  202. : `确认删除场景【${row.name}】吗?`;
  203. ElMessageBox.confirm(message, "提示", {
  204. type: "warning"
  205. })
  206. .then(async () => {
  207. await deleteScene({ id: row.id });
  208. ElMessage.success("删除成功");
  209. loadSceneList();
  210. })
  211. .catch(() => {});
  212. };
  213. const filterNode = (value: string, data: any) => {
  214. if (!value) return true;
  215. return data.name?.includes(value);
  216. };
  217. const handleSearch = () => {
  218. treeRef.value?.filter(filterText.value);
  219. };
  220. const handleReset = () => {
  221. filterText.value = "";
  222. treeRef.value?.filter("");
  223. };
  224. watch(filterText, val => {
  225. if (!val) treeRef.value?.filter("");
  226. });
  227. onMounted(() => {
  228. loadSceneList();
  229. });
  230. </script>
  231. <style scoped lang="scss">
  232. .scene-page {
  233. padding: 16px;
  234. }
  235. .scene-toolbar {
  236. display: flex;
  237. align-items: center;
  238. justify-content: space-between;
  239. margin-bottom: 12px;
  240. }
  241. .scene-search {
  242. display: flex;
  243. gap: 8px;
  244. width: 360px;
  245. }
  246. .scene-header {
  247. display: grid;
  248. grid-template-columns: 3fr 1fr 1fr;
  249. padding: 12px 16px;
  250. font-weight: 600;
  251. background-color: var(--el-fill-color-light);
  252. border: 1px solid var(--el-border-color);
  253. border-bottom: none;
  254. border-radius: 4px 4px 0 0;
  255. }
  256. .scene-tree {
  257. max-height: 680px;
  258. overflow: auto;
  259. border: 1px solid var(--el-border-color);
  260. border-top: none;
  261. border-radius: 0 0 4px 4px;
  262. }
  263. .scene-node {
  264. display: grid;
  265. grid-template-columns: 3fr 1fr 1fr;
  266. align-items: center;
  267. width: 100%;
  268. padding: 12px 16px;
  269. }
  270. .scene-name {
  271. display: flex;
  272. align-items: center;
  273. }
  274. .scene-status {
  275. display: flex;
  276. justify-content: flex-start;
  277. }
  278. .scene-actions {
  279. display: flex;
  280. gap: 8px;
  281. }
  282. :deep(.el-tree-node__content) {
  283. height: auto;
  284. }
  285. </style>