Просмотр исходного кода

菜单创建和编辑只能从前端路由表选择

lfeish 1 год назад
Родитель
Сommit
f285440762

+ 15 - 5
src/router/full-routes.ts

@@ -158,7 +158,7 @@ const fullRoutes: AppRouteRecordRaw[] = [
     name: 'Algorithm',
     component: 'LAYOUT',
     meta: { 
-      icon: 'SettingOutlined',
+      icon: 'FunctionOutlined',
       title: '算法管理',
     },
     children: [
@@ -193,7 +193,7 @@ const fullRoutes: AppRouteRecordRaw[] = [
     name: 'User',
     component: 'LAYOUT',
     meta: { 
-      icon: '',
+      icon: 'UserOutlined',
       title: '用户管理',
     },
     children: [
@@ -239,7 +239,7 @@ const fullRoutes: AppRouteRecordRaw[] = [
     name: 'Message',
     component: 'LAYOUT',
     meta: { 
-      icon: '',
+      icon: 'SendOutlined',
       title: '消息管理',
     },
     children: [
@@ -325,7 +325,7 @@ const fullRoutes: AppRouteRecordRaw[] = [
     name: 'Data',
     component: 'LAYOUT',
     meta: {
-      icon: '',
+      icon: 'LineChartOutlined',
       title: '数据管理',
     },
     children: [
@@ -340,7 +340,7 @@ const fullRoutes: AppRouteRecordRaw[] = [
         }
       },
       {
-        // 历史视频 (视频看)
+        // 历史视频 (视频看)
         path: 'playback',
         name: 'DataPlayback',
         component: '/datamanager/playback/Playback',
@@ -416,6 +416,16 @@ const fullRoutes: AppRouteRecordRaw[] = [
           title: '平台反馈',
         }
       },
+      {
+        // 反馈处理
+        path: 'feedback-handle',
+        name: 'SystemFeedbackHandle',
+        component: '/feedback/handleFeedback',
+        meta: {
+          icon: '',
+          title: '反馈处理'
+        }
+      },
       {
         // 字典管理
         path: 'dictionary',

+ 8 - 0
src/router/router-icons.ts

@@ -18,6 +18,10 @@ import {
   PictureOutlined,
   CameraOutlined,
   ApartmentOutlined,
+  UserOutlined,
+  FunctionOutlined,
+  SendOutlined,
+  LineChartOutlined,
 } from '@vicons/antd';
 import {
   OptionsSharp,
@@ -49,6 +53,10 @@ export const constantRouterIcon = {
   PictureOutlined: renderIcon(PictureOutlined),
   CameraOutlined: renderIcon(CameraOutlined),
   ApartmentOutlined: renderIcon(ApartmentOutlined),
+  UserOutlined: renderIcon(UserOutlined),
+  FunctionOutlined: renderIcon(FunctionOutlined),
+  SendOutlined: renderIcon(SendOutlined),
+  LineChartOutlined: renderIcon(LineChartOutlined),
   FileTrayFullOutline: renderIcon(FileTrayFullOutline),
   HandleFeedback: renderImg(HandleFeedback),
   Handle: renderImg(Handle),

+ 5 - 1
src/types/menu/type.ts

@@ -66,8 +66,12 @@ export type MenuDetailTree = MenuDetail[];
  * 用于 element-plus 树形控件 展示
  */
 export interface MenuItem {
+  // 菜单名
   label: string;
-  key: number | string;
+  // 菜单主键ID
+  id: number;
+  // 路由 name
+  routeName: string;
 }
 
 export type Menu = MenuItem & { children?: MenuItem[] | null };

+ 15 - 17
src/views/system/menu/CreateDrawer.vue

@@ -1,6 +1,6 @@
 <template>
   <el-drawer v-model="isDrawer" :size="width" :title="title" @close="handleReset">
-    <MenuForm ref="menuFormRef" :permissionList="props.permissionList" @change="menuFormChange" />
+    <MenuForm ref="menuFormRef" :parent-menu-tree="parentMenuTree" @change="menuFormChange" isCreating />
 
     <template #footer>
       <el-space>
@@ -14,27 +14,25 @@
 <script lang="ts" setup>
   import { ref } from 'vue';
   import MenuForm from './MenuForm.vue';
+  import { MenuTree } from '@/types/menu/type';
+
+  withDefaults(
+    defineProps<{
+      title?: string;
+      width?: number;
+      parentMenuTree: MenuTree
+    }>(),
+    {
+      title: '添加顶级菜单',
+      width: 580,
+    }
+  );
 
   const emit = defineEmits(['change']);
 
-  const menuFormRef = ref();
-
-  const props = defineProps({
-    title: {
-      type: String,
-      default: '添加顶级菜单',
-    },
-    width: {
-      type: Number,
-      default: 580,
-    },
-    permissionList: {
-      type: Array,
-    },
-  });
-
   const isDrawer = ref(false);
   const subLoading = ref(false);
+  const menuFormRef = ref();
 
   function openDrawer() {
     isDrawer.value = true;

+ 149 - 89
src/views/system/menu/MenuForm.vue

@@ -12,58 +12,67 @@
     <el-form-item label="上级菜单" prop="parentId">
       <el-tree-select
         rowKey="key"
-        :data="getPermissionList"
+        :data="extendedMenuTree"
         clearable
         check-strictly
         v-model="formParams.parentId"
+        :disabled="isEditing"
       />
     </el-form-item>
 
+    <el-form-item label="菜单名称" required>
+      <el-tree-select
+        v-if="isCreating"
+        rowKey="key"
+        :data="routeViewList"
+        clearable
+        check-strictly
+        v-model="routeViewValue"
+      />
+      <el-input v-else v-model="formParams.menuName" disabled />
+    </el-form-item>
+
+
     <el-row :gutter="24">
       <el-col :span="12">
-        <el-form-item label="菜单名称" prop="menuName">
-          <el-input placeholder="菜单名称" v-model="formParams.menuName" />
-        </el-form-item>
-      </el-col>
-      <el-col :span="12">
-        <el-form-item label="菜单图标" prop="icon">
+        <el-form-item prop="routeUrl">
           <template #label>
             <div class="flex items-center">
               <el-tooltip trigger="hover">
                 <el-icon :size="18" class="mr-1 text-gray-400 cursor-pointer">
                   <QuestionCircleOutlined />
                 </el-icon>
-                <template #content>
-                  菜单图标,填写图标组件名称,需在 `src\router\router-icons.ts` 中导入并映射
-                </template>
+                <template #content> 路由地址,如:user </template>
               </el-tooltip>
-              <span>菜单图标</span>
+              <span>路由地址</span>
             </div>
           </template>
-          <el-input placeholder="菜单图标映射名称" v-model="formParams.icon" />
+          <el-input placeholder="路由地址" v-model="formParams.routeUrl" disabled />
         </el-form-item>
       </el-col>
-    </el-row>
-
-    <el-row :gutter="24">
       <el-col :span="12">
-        <el-form-item prop="routeUrl">
+        <el-form-item prop="routeName">
           <template #label>
             <div class="flex items-center">
               <el-tooltip trigger="hover">
                 <el-icon :size="18" class="mr-1 text-gray-400 cursor-pointer">
                   <QuestionCircleOutlined />
                 </el-icon>
-                <template #content> 路由地址,如:user </template>
+                <template #content>
+                  对应路由配置文件中 `name` 只能是唯一性,配置 `http(s)://` 开头地址 则会新窗口打开
+                </template>
               </el-tooltip>
-              <span>路由地址</span>
+              <span>路由名称</span>
             </div>
           </template>
-          <el-input placeholder="路由地址" v-model="formParams.routeUrl" />
+          <el-input placeholder="路由名称" v-model="formParams.routeName" disabled />
         </el-form-item>
       </el-col>
-      <el-col :span="12">
-        <el-form-item prop="routeName">
+    </el-row>
+
+    <el-row :gutter="24">
+      <el-col>
+        <el-form-item prop="component">
           <template #label>
             <div class="flex items-center">
               <el-tooltip trigger="hover">
@@ -71,20 +80,21 @@
                   <QuestionCircleOutlined />
                 </el-icon>
                 <template #content>
-                  对应路由配置文件中 `name` 只能是唯一性,配置 `http(s)://` 开头地址 则会新窗口打开
+                  访问的组件路径,如:`/system/menu/menu`,默认在`views`目录下,默认 `LAYOUT`
+                  如果是多级菜单 `ParentLayout`
                 </template>
               </el-tooltip>
-              <span>路由名称</span>
+              <span>组件路径</span>
             </div>
           </template>
-          <el-input placeholder="路由名称" v-model="formParams.routeName" />
+          <el-input placeholder="组件路径" v-model="formParams.component" disabled />
         </el-form-item>
       </el-col>
     </el-row>
 
     <el-row :gutter="24">
       <el-col :span="12">
-        <el-form-item prop="redirect">
+        <el-form-item label="菜单图标" prop="icon">
           <template #label>
             <div class="flex items-center">
               <el-tooltip trigger="hover">
@@ -92,25 +102,17 @@
                   <QuestionCircleOutlined />
                 </el-icon>
                 <template #content>
-                  默认跳转路由地址,如:`/system/menu/menu` 多级路由情况下适用
+                  菜单图标,填写图标组件名称,需在 `src\router\router-icons.ts` 中导入并映射
                 </template>
               </el-tooltip>
-              <span>默认路由</span>
+              <span>菜单图标</span>
             </div>
           </template>
-          <el-input placeholder="默认跳转路由地址" v-model="formParams.redirect" />
+          <el-input placeholder="菜单图标映射名称" v-model="formParams.icon" disabled />
         </el-form-item>
       </el-col>
       <el-col :span="12">
-        <el-form-item label="显示排序" prop="orderNum">
-          <el-input-number placeholder="显示排序" v-model="formParams.orderNum" class="w-full" />
-        </el-form-item>
-      </el-col>
-    </el-row>
-
-    <el-row :gutter="24">
-      <el-col>
-        <el-form-item prop="component">
+        <el-form-item prop="redirect">
           <template #label>
             <div class="flex items-center">
               <el-tooltip trigger="hover">
@@ -118,18 +120,29 @@
                   <QuestionCircleOutlined />
                 </el-icon>
                 <template #content>
-                  访问的组件路径,如:`/system/menu/menu`,默认在`views`目录下,默认 `LAYOUT`
-                  如果是多级菜单 `ParentLayout`
+                  默认跳转路由地址,如:`/system/menu/menu` 多级路由情况下适用
                 </template>
               </el-tooltip>
-              <span>组件路径</span>
+              <span>默认路由</span>
             </div>
           </template>
-          <el-input placeholder="组件路径" v-model="formParams.component" />
+          <el-input placeholder="默认跳转路由地址" v-model="formParams.redirect" disabled />
         </el-form-item>
       </el-col>
     </el-row>
 
+
+
+    <el-row :gutter="24">
+      <el-col :span="12">
+        <el-form-item label="显示排序" prop="orderNum">
+          <el-input-number placeholder="显示排序" v-model="formParams.orderNum" class="w-full" />
+        </el-form-item>
+      </el-col>
+    </el-row>
+
+
+
     <el-divider border-style="dashed" class="mb-10">功能设置</el-divider>
 
     <el-row :gutter="24">
@@ -268,28 +281,59 @@
 </template>
 
 <script lang="ts" setup>
-  import { ref, computed } from 'vue';
-  import { ElMessage, FormInstance } from 'element-plus';
+  import { ref, computed, watch, shallowRef } from 'vue';
+  import { ElMessage, FormInstance, FormRules } from 'element-plus';
   import { addMenu, editMenu } from '@/api/system/menu';
   import { QuestionCircleOutlined } from '@vicons/antd';
   import { replaceParams } from '@/utils/helper/treeHelper';
   import { cloneDeep } from 'lodash-es';
-  import { MenuDetailItem } from '@/types/menu/type';
+  import { MenuDetailItem, MenuTree, Menu } from '@/types/menu/type';
+  import { getChildRoutesView, _RouteViewItem, getRouteByName } from '@/router/full-routes';
+  import { getTreeItem } from '@/utils';
+
+  const props = defineProps<{
+    parentMenuTree: MenuTree; // "上级菜单"树
+    isShowSubmit?: boolean;
+    isCreating?: boolean;
+  }>();
 
   const emit = defineEmits(['change']);
 
-  const props = defineProps({
-    permissionList: {
-      type: Array,
-      defalut: () => [],
+  const formRef = ref<FormInstance>();
+  const rules: FormRules = {
+    parentId: {
+      required: true,
+      message: '上级菜单',
+      trigger: 'change',
     },
-    isShowSubmit: {
-      type: Boolean,
-      defalut: false,
+    menuName: {
+      required: true,
+      message: '菜单名称',
+      trigger: 'blur',
     },
-  });
+    routeUrl: {
+      required: true,
+      message: '路由地址不能为空',
+      trigger: 'blur',
+    },
+    component: {
+      required: true,
+      message: '组件路径不能为空',
+      trigger: 'blur',
+    },
+    routeName: {
+      required: true,
+      message: '路由名称不能为空',
+      trigger: 'blur',
+    },
+    frameSrc: {
+      required: true,
+      message: '外部地址不能为空',
+      trigger: 'blur',
+    },
+  };
+
 
-  const formRef = ref<FormInstance>();
   const subLoading = ref(false);
 
   const defaultFormParams: MenuDetailItem = {
@@ -314,52 +358,67 @@
     openType: 1, 
     query: '',
   };
-
   const formParams = ref({ ...defaultFormParams });
 
-  const getPermissionList = computed(() => {
+  const extendedMenuTree = computed(() => {
     // 可根据需要是否需要嵌套这个 避免出现选择器出现 0 的情况
     return [
       {
         label: '顶级目录',
         value: 0,
-        children: replaceParams(cloneDeep(props.permissionList || []), 'label', 'key'),
+        children: replaceParams(cloneDeep(props.parentMenuTree || []), 'label', 'id'),
       },
     ];
   });
 
-  const rules = {
-    parentId: {
-      required: true,
-      message: '上级菜单',
-      trigger: 'change',
-    },
-    menuName: {
-      required: true,
-      message: '菜单名称',
-      trigger: 'blur',
-    },
-    routeUrl: {
-      required: true,
-      message: '路由地址不能为空',
-      trigger: 'blur',
-    },
-    component: {
-      required: true,
-      message: '组件路径不能为空',
-      trigger: 'blur',
-    },
-    routeName: {
-      required: true,
-      message: '路由名称不能为空',
-      trigger: 'blur',
-    },
-    frameSrc: {
-      required: true,
-      message: '外部地址不能为空',
-      trigger: 'blur',
-    },
-  };
+  const isEditing = computed(() => !props.isCreating);
+  const routeViewValue = ref('');
+  const routeViewList = shallowRef<_RouteViewItem[]>([]);
+
+
+  // 根据 "上级菜单",生成可选的"当前菜单"
+  watch(
+    () => formParams.value.parentId,
+    (newVal) => {
+      // 编辑模式下,只是展示,不需要计算数据。
+      if (isEditing.value) return;
+
+      // 若清除选项
+      if (newVal == null) {
+        routeViewList.value = [];
+        return;
+      } 
+
+      // 若选择顶级目录, 返回路由表的 leve1 列表
+      if (newVal === 0) {
+        routeViewList.value = getChildRoutesView(null);
+        return;
+      } 
+
+      // 否则展示 parent 下的 子路由
+      const { routeName } = getTreeItem(props.parentMenuTree!, newVal, 'id') as Menu;
+      if (routeName) {
+        routeViewList.value = getChildRoutesView(routeName);
+      } else {
+        ElMessage.error('无法获取可用路由列表');
+      }
+    }
+  );
+
+  // 根据 routeViewValue,即 routeName,将菜单其余信息补充
+  watch(routeViewValue, (val) => {
+    if (!val) {
+      handleReset();
+      return;
+    }
+
+    const currentRoute = getRouteByName(val);
+    formParams.value.routeName = currentRoute.name;
+    formParams.value.routeUrl = currentRoute.path;
+    formParams.value.component = currentRoute.component;
+    formParams.value.icon = currentRoute.meta.icon as string;
+    formParams.value.menuName = currentRoute.meta.title as string;
+  });
 
   function setData(data: MenuDetailItem) {
     formParams.value = data;
@@ -402,7 +461,8 @@
 
   function handleReset() {
     formRef.value?.resetFields();
-    formParams.value = Object.assign(formParams.value, defaultFormParams);
+    formParams.value = { ...defaultFormParams };
+    routeViewValue.value = '';
   }
 
   defineExpose({

+ 39 - 24
src/views/system/menu/menu.vue

@@ -35,10 +35,11 @@
                   ref="treeRef"
                   :pattern="pattern"
                   :data="menuTree"
-                  nodeKey="key"
+                  nodeKey="id"
                   highlight-current
                   check-strictly
-                  @current-change="selectedTree"
+                  :expand-on-click-node="false"
+                  @current-change="onSelectTreeItem"
                   @update:expanded-keys="onExpandedKeys"
                 />
               </el-scrollbar>
@@ -69,8 +70,8 @@
             <MenuForm
               v-show="selectedMenuId != null"
               ref="menuFormRef"
-              :permissionList="menuTree"
-              @change="menuChange"
+              :parent-menu-tree="menuTree"
+              @change="handleMenuChange"
               class="w-2/3 ml-10"
               isShowSubmit
             />
@@ -82,8 +83,8 @@
     <CreateDrawer
       ref="createDrawerRef"
       :title="drawerTitle"
-      :permissionList="menuTree"
-      @change="menuChange"
+      :parent-menu-tree="menuTree"
+      @change="handleMenuChange"
     />
   </PageWrapper>
 </template>
@@ -115,14 +116,24 @@
   const treeRef = ref();
   const menuFormRef = ref();
 
+  function clearEditing() {
+    selectedMenuId.value = null;
+    isEditMenu.value = false;
+    treeItemTitle.value = '';
+  }
+
   function handleDeleteMenu() {
+    const treeItem = getTreeItem(menuTree.value, selectedMenuId.value!, 'id') as Menu;
+    if (treeItem?.children && treeItem?.children.length) {
+      ElMessage.error('当前结节含有子节点,无法删除');
+      return;
+    }
+
     deleteMenu(selectedMenuId.value!).then(() => {
       ElMessage.success('删除成功');
-      selectedMenuId.value = null;
-      isEditMenu.value = false;
-      treeItemTitle.value = '';
+      clearEditing();
       menuFormRef.value.handleReset();
-      getPermissionList();
+      getMenuTree();
     });
   }
 
@@ -132,22 +143,22 @@
     openDrawer();
   }
 
-  async function selectedTree(checkedInfo: Menu) {
-    const currentKey = checkedInfo.key as number;
-    selectedMenuId.value = currentKey;
+  async function onSelectTreeItem(checkedInfo: Menu) {
+    const currentMenuId = checkedInfo.id as number;
+    selectedMenuId.value = currentMenuId;
 
-    if (currentKey) {
-      const menu: Menu = getTreeItem(menuTree.value, currentKey);
+    if (currentMenuId) {
+      const menu: Menu = getTreeItem(menuTree.value, currentMenuId, 'id');
       treeItemTitle.value = menu.label;
 
-      const menuDetail: MenuDetail = getTreeItem(menuDetailTree.value, currentKey, 'id');
+      const menuDetail: MenuDetail = getTreeItem(menuDetailTree.value, currentMenuId, 'id');
       const menuFormData = cloneDeep(menuDetail);
       delete menuFormData.children;
       // console.log('menu form data', menuFormData)
       menuFormRef.value.setData(menuFormData);
 
       isEditMenu.value = true;
-      treeRef.value.setCheckedKeys([currentKey]);
+      treeRef.value.setCheckedKeys([currentMenuId]);
     }
   }
 
@@ -167,7 +178,11 @@
     }
   }
 
-  async function getPermissionList() {
+  /**
+   * 获取 菜单树。一个菜单详细树,用于创建和编辑菜单。 一个是展示树,用于 el-tree 组件
+   * 注意:原来的 getPermissionList 重新命名为 getMenuTree
+   */
+  async function getMenuTree() {
     loading.value = true;
     menuDetailTree.value = await queryFullMenuTree();
     menuTree.value = transformToSimpleMenus(menuDetailTree.value);
@@ -182,8 +197,9 @@
     const tree: MenuTree = [];
     for (const item of menus) {
       const treeItem: Menu = { 
-        key: item.id!, 
+        id: item.id!, 
         label: item.menuName, 
+        routeName: item.routeName,
         children: null
       };
 
@@ -198,14 +214,13 @@
     return tree;
   }
 
-  async function menuChange() {
-    getPermissionList();
-    treeItemTitle.value = '';
-    isEditMenu.value = false;
+  async function handleMenuChange() {
+    getMenuTree();
+    clearEditing();
   }
 
   onMounted(async () => {
-    getPermissionList();
+    getMenuTree();
   });
 
   function onExpandedKeys(keys) {

+ 1 - 0
src/views/system/permission/PagePermission.vue

@@ -39,6 +39,7 @@
                   nodeKey="value"
                   highlight-current
                   check-strictly
+                  :expand-on-click-node="false"
                   @current-change="onSelectTreeNode"
                   @update:expanded-keys="onExpandedKeys"
                 />

+ 1 - 1
src/views/system/permission/PermissionForm.vue

@@ -9,7 +9,7 @@
   >
     <el-divider border-style="dashed" class="mt-0 mb-10">基本设置</el-divider>
 
-    <el-form-item label="上级目录" prop="parentId">
+    <el-form-item label="上级权限" prop="parentId">
       <el-tree-select
         rowKey="key"
         :data="permissionTree"