index.vue 9.1 KB

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