menu.vue 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. <template>
  2. <VerticalFlexLayout>
  3. <template #static>
  4. <Breadcrumb />
  5. </template>
  6. <PageWrapper style="background: #fff; height: calc(100% - 70px)">
  7. <el-row :gutter="12">
  8. <el-col :span="6">
  9. <el-card shadow="hover">
  10. <template #header>
  11. <el-space>
  12. <el-button type="primary" icon-placement="left" @click="openCreateDrawer">
  13. 添加菜单
  14. <template #icon>
  15. <div class="flex items-center">
  16. <el-icon size="14">
  17. <FileAddOutlined />
  18. </el-icon>
  19. </div>
  20. </template>
  21. </el-button>
  22. <el-button type="primary" plain icon-placement="left" @click="packHandle">
  23. 全部{{ expandedKeys.length ? '收起' : '展开' }}
  24. <template #icon>
  25. <div class="flex items-center">
  26. <el-icon size="14">
  27. <AlignLeftOutlined />
  28. </el-icon>
  29. </div>
  30. </template>
  31. </el-button>
  32. </el-space>
  33. </template>
  34. <div class="w-full menu">
  35. <div class="menu-list" v-loading="loading">
  36. <el-scrollbar height="620px">
  37. <el-tree
  38. ref="treeRef"
  39. :pattern="pattern"
  40. :data="menuTree"
  41. nodeKey="id"
  42. highlight-current
  43. check-strictly
  44. :expand-on-click-node="false"
  45. @current-change="onSelectTreeItem"
  46. @update:expanded-keys="onExpandedKeys"
  47. />
  48. </el-scrollbar>
  49. </div>
  50. </div>
  51. </el-card>
  52. </el-col>
  53. <el-col :span="18">
  54. <el-card shadow="hover">
  55. <template #header>
  56. <div class="flex justify-between">
  57. <el-space>
  58. <span>编辑菜单 {{ treeItemTitle ? `:${treeItemTitle}` : '' }}</span>
  59. </el-space>
  60. <el-popconfirm
  61. @confirm="handleDeleteMenu"
  62. v-if="isEditMenu"
  63. :title="`确定要删除菜单${treeItemTitle}吗?`"
  64. >
  65. <template #reference>
  66. <el-button type="danger" size="small" style="margin-left: 50px">删除菜单</el-button>
  67. </template>
  68. </el-popconfirm>
  69. </div>
  70. </template>
  71. <!-- 表单 -->
  72. <div class="pt-6">
  73. <MenuForm
  74. v-show="selectedMenuId != null"
  75. ref="menuFormRef"
  76. :parent-menu-tree="menuTree"
  77. @change="handleMenuChange"
  78. class="w-2/3 ml-10"
  79. isShowSubmit
  80. />
  81. <el-empty v-show="selectedMenuId == null" description="从左侧列表选择一项后,进行编辑" />
  82. </div>
  83. </el-card>
  84. </el-col>
  85. </el-row>
  86. <CreateDrawer
  87. ref="createDrawerRef"
  88. :title="drawerTitle"
  89. :parent-menu-tree="menuTree"
  90. @change="handleMenuChange"
  91. />
  92. </PageWrapper>
  93. </VerticalFlexLayout>
  94. </template>
  95. <script lang="ts" setup>
  96. import { ref, onMounted, shallowRef } from 'vue';
  97. import { ElMessage } from 'element-plus';
  98. import { AlignLeftOutlined, FileAddOutlined } from '@vicons/antd';
  99. import { deleteMenu } from '@/api/system/menu';
  100. import { queryFullMenuTree } from '@/api/system/menu';
  101. import { MenuDetailTree, MenuTree, MenuDetail, Menu } from '@/types/menu/type';
  102. import { getTreeItem } from '@/utils';
  103. import CreateDrawer from './CreateDrawer.vue';
  104. import MenuForm from './MenuForm.vue';
  105. import { cloneDeep } from 'lodash-es';
  106. import { PageWrapper } from '@/components/Page';
  107. import Breadcrumb from '@/components/Breadcrumb.vue';
  108. import VerticalFlexLayout from '@/components/VerticalFlexLayout.vue';
  109. const menuDetailTree = shallowRef<MenuDetailTree>([]); // 菜单详情树
  110. const menuTree = shallowRef<MenuTree>([]); // 菜单树,用于展示
  111. const selectedMenuId = ref<MenuDetail['id']>(null); // 选中的菜单
  112. const loading = ref(true); // 左侧菜单树加载
  113. const expandedKeys = ref([]);
  114. const isEditMenu = ref(false);
  115. const treeItemTitle = ref('');
  116. const pattern = ref('');
  117. const drawerTitle = ref('');
  118. // 组件实例引用
  119. const createDrawerRef = ref();
  120. const treeRef = ref();
  121. const menuFormRef = ref();
  122. function clearEditing() {
  123. selectedMenuId.value = null;
  124. isEditMenu.value = false;
  125. treeItemTitle.value = '';
  126. expandedKeys.value = [];
  127. }
  128. function handleDeleteMenu() {
  129. const treeItem = getTreeItem(menuTree.value, selectedMenuId.value!, 'id') as Menu;
  130. if (treeItem?.children && treeItem?.children.length) {
  131. ElMessage.error('当前结节含有子节点,无法删除');
  132. return;
  133. }
  134. deleteMenu(selectedMenuId.value!).then(() => {
  135. ElMessage.success('删除成功');
  136. clearEditing();
  137. menuFormRef.value.handleReset();
  138. getMenuTree();
  139. });
  140. }
  141. function openCreateDrawer() {
  142. drawerTitle.value = '添加菜单';
  143. const { openDrawer } = createDrawerRef.value;
  144. openDrawer();
  145. }
  146. async function onSelectTreeItem(checkedInfo: Menu) {
  147. const currentMenuId = checkedInfo.id as number;
  148. selectedMenuId.value = currentMenuId;
  149. if (currentMenuId) {
  150. const menu: Menu = getTreeItem(menuTree.value, currentMenuId, 'id');
  151. treeItemTitle.value = menu.label;
  152. const menuDetail: MenuDetail = getTreeItem(menuDetailTree.value, currentMenuId, 'id');
  153. const menuFormData = cloneDeep(menuDetail);
  154. delete menuFormData.children;
  155. // console.log('menu form data', menuFormData)
  156. menuFormRef.value.setData(menuFormData);
  157. isEditMenu.value = true;
  158. treeRef.value.setCheckedKeys([currentMenuId]);
  159. }
  160. }
  161. function treeNodeExpand(status) {
  162. for (var i = 0; i < treeRef.value.store._getAllNodes().length; i++) {
  163. treeRef.value.store._getAllNodes()[i].expanded = status;
  164. }
  165. }
  166. function packHandle() {
  167. if (!expandedKeys.value.length) {
  168. treeNodeExpand(true);
  169. expandedKeys.value = menuTree.value?.map((item: any) => item.key as string) as [];
  170. } else {
  171. expandedKeys.value = [];
  172. treeNodeExpand(false);
  173. }
  174. }
  175. /**
  176. * 获取 菜单树。一个菜单详细树,用于创建和编辑菜单。 一个是展示树,用于 el-tree 组件
  177. * 注意:原来的 getPermissionList 重新命名为 getMenuTree
  178. */
  179. async function getMenuTree() {
  180. loading.value = true;
  181. menuDetailTree.value = await queryFullMenuTree();
  182. menuTree.value = transformToSimpleMenus(menuDetailTree.value);
  183. loading.value = false;
  184. }
  185. function transformToSimpleMenus(menus: MenuDetailTree | null): MenuTree {
  186. if (menus == null) {
  187. return [];
  188. }
  189. const tree: MenuTree = [];
  190. for (const item of menus) {
  191. const treeItem: Menu = {
  192. id: item.id!,
  193. label: item.menuName,
  194. routeName: item.routeName,
  195. children: null,
  196. };
  197. if (Array.isArray(item.children) && item.children?.length > 0) {
  198. treeItem.children = transformToSimpleMenus(item.children);
  199. tree.push(treeItem);
  200. } else {
  201. tree.push(treeItem);
  202. }
  203. }
  204. return tree;
  205. }
  206. async function handleMenuChange() {
  207. getMenuTree();
  208. clearEditing();
  209. }
  210. onMounted(async () => {
  211. getMenuTree();
  212. });
  213. function onExpandedKeys(keys) {
  214. expandedKeys.value = keys;
  215. }
  216. </script>