index.vue 8.9 KB

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