Selaa lähdekoodia

Merge branch 'all-v4-liufei' into 'all-v4'

新菜单、权限、角色页面主要功能

See merge request skyeye/skyeye_frontend/skyeye-admin!205
Fei Liu 1 vuosi sitten
vanhempi
commit
ee4ef73e4f

+ 16 - 6
src/api/system/menu.ts

@@ -1,8 +1,8 @@
 import { http } from '@/utils/http/axios';
-import { MenuDetail } from '@/types/menu/type';
+import { MenuDetailTree, MenuDetailItem } from '@/types/menu/type';
 
 /**
- * @description: 获取动态菜单
+ * 获取当前用户可访问态菜单
  */
 export function getRouters() {
   return http.request({
@@ -13,20 +13,30 @@ export function getRouters() {
 
 
 /**
- * v4:获取整个菜单树
+ * 获取整个菜单树
  */
 export function queryFullMenuTree() {
-  return http.request<MenuDetail[]>({
+  return http.request<MenuDetailTree>({
     url: '/admin/menu/queryAllMenuTree',
     method: 'post',
   });
 }
 
+/**
+ * 获取当前用户拥有的菜单树
+ */
+export function queryOwnedMenuTree() {
+  return http.request<MenuDetailTree>({
+    url: '/admin/menu/queryOwnedMenuTree',
+    method: 'post',
+  }); 
+}
+
 /**
  * 添加菜单
  * @param params
  */
-export function addMenu(data: MenuDetail) {
+export function addMenu(data: MenuDetailItem) {
   return http.request({
     url: '/admin/menu/saveMenu',
     method: 'POST',
@@ -38,7 +48,7 @@ export function addMenu(data: MenuDetail) {
  * 编辑菜单
  * @param params
  */
-export function editMenu(data: MenuDetail) {
+export function editMenu(data: MenuDetailItem) {
   return http.request({
     url: '/admin/menu/updateMenu',
     method: 'POST',

+ 55 - 0
src/api/system/permission.ts

@@ -0,0 +1,55 @@
+import { http } from '@/utils/http/axios';
+import { PermissionTree, PermissionItem } from '@/types/permission/type';
+
+/**
+ * 获取整个功能权限树
+ */
+export function getPermissionTree() {
+  return http.request<PermissionTree>({
+    url: '/admin/perm/queryAllPermissionTree',
+    method: 'post'
+  });
+};
+
+/**
+ * 获取当前用户拥有的功能权限树
+ */
+export function getOwnedPermissionTree() {
+  return http.request<PermissionTree>({
+    url: '/admin/perm/queryOwnedPermissionList',
+    method: 'post'
+  });
+}
+
+
+/**
+ * 添加权限
+ */
+export function addPermission(data: PermissionItem) {
+  return http.request({
+    url: '/admin/perm/savePermission',
+    method: 'POST',
+    data,
+  });
+}
+
+/**
+ * 修改权限
+ */
+export function updatePermission(data: PermissionItem) {
+  return http.request({
+    url: '/admin/perm/updatePermission',
+    method: 'POST',
+    data,
+  });
+}
+
+/**
+ * 删除权限
+ */
+export function deletePermission(permissionId: number) {
+  return http.request({
+    url: `/admin/perm/deletePermission?permId=${permissionId}`,
+    method: 'POST',
+  });
+}

+ 65 - 1
src/api/system/role.ts

@@ -1,4 +1,5 @@
 import { http } from '@/utils/http/axios';
+import { Role, RolePageRequest, RolePageResponse, RoleForm, AssignedPermissions } from '@/types/role/type';
 
 /**
  * @description: 添加管理角色
@@ -107,7 +108,6 @@ export function delUserRole(params) {
 export function roleList(params?) {
   return http.request({
     url: '/role/pageList',
-
     params,
   });
 }
@@ -140,3 +140,67 @@ export function getFeaturePermissions() {
 
   });
 }
+
+
+/**
+ * v4: 获取自己拥有的和自己创建的角色(分页)
+ */
+export function getRolesPage(params: RolePageRequest) {
+  return http.request<RolePageResponse>({
+    url: '/admin/role/queryRolePage',
+    method: 'POST',
+    data: params,
+  });
+}
+
+/**
+ * v4: 获取自己拥有和自己创建的角色(全量)
+ */
+export function getRoles() {
+  return http.request<Role[]>({
+    url: '/admin/role/getRoleList',
+    method: 'GET'
+  });
+}
+
+/**
+ * v4: 创建角色
+ */
+export function createRole(data: RoleForm) {
+  return http.request({
+    url: '/admin/role/addRole',
+    method: 'POST',
+    data,
+  });
+}
+
+/**
+ * v4: 编辑角色
+ */
+export function editRole(data: RoleForm) {
+  return http.request({
+    url: '/admin/role/updateRole',
+    method: 'POST',
+    data,
+  });
+}
+
+/**
+ * v4:删除菜单角色
+ */
+export function deleteRole(roleId: number) {
+  return http.request({
+    url: `/admin/role/deleteRole?roleId=${roleId}`,
+    method: 'POST',
+  });
+}
+
+/**
+ * v4:获取角色已分配的权限(相机、菜单、功能)
+ */
+export function getAssignedPerms(roleId: number) {
+  return http.request<AssignedPermissions>({
+    url: `/admin/role/getSelectedPerm?roleId=${roleId}`,
+    method: 'GET',
+  }); 
+}

+ 588 - 0
src/router/full-routes.ts

@@ -0,0 +1,588 @@
+/**
+ * 1. 前端维护的完整路由表, 用于创建"菜单"
+ * 2. component 是 string,并不是 组件对象
+ * 3. 生成菜单,关注的 path,name,component,icon
+ * 4. 以用户填写的数据为准
+ */
+import { AppRouteRecordRaw } from './types';
+import { getTreeItem } from '@/utils';
+import { cloneDeep } from 'lodash-es';
+
+const fullRoutes: AppRouteRecordRaw[] = [
+  /**
+   * Dashboard
+   */
+  {
+    path: '/dashboard',
+    name: 'Dashboard',
+    component: 'LAYOUT',
+    meta: {
+      icon: 'DashboardOutlined',
+      title: 'Dashboard',
+    },
+    children: [
+      {
+        // 主控台
+        path: 'console',
+        name: 'DashboardConsole',
+        component: '/dashboard/home/Home',
+        meta: { 
+          icon: '',
+          title: '主控台',
+        },
+      }
+    ]
+  },
+
+
+  /**
+   * 场景管理
+   */
+  {
+    path: '/scene',
+    name: 'Scene',
+    component: 'LAYOUT',
+    meta: { 
+      icon: 'ApartmentOutlined',
+      title: '场景管理',
+    },
+    children: [
+      {
+        // 模板管理
+        path: 'template',
+        name: 'SceneTemplate',
+        component: '/templateManage/templateManage',
+        meta: { 
+          icon: '',
+          title: '模板管理',
+        },
+      },
+      {
+        // 车间管理
+        path: 'workshop',
+        name: 'SceneWorkshop',
+        component: '/system-config/scene-manage/SceneManage',
+        meta: { 
+          icon: '',
+          title: '车间管理' 
+        },
+      },
+      {
+        // 业务场景管理
+        path: 'business',
+        name: 'SceneBusiness',
+        component: '/system-config/business-scene/PageBusinessScene',
+        meta: { 
+          icon: '',
+          title: '业务创建管理',
+        },
+      }
+    ]
+  },
+
+  /**
+   * 设备管理
+   */
+  {
+    path: '/device',
+    name: 'Device',
+    component: 'LAYOUT',
+    meta: { 
+      icon: 'CameraOutlined',
+      title: '设备管理'
+    },
+    children: [
+      {
+        // 相机设备
+        path: 'camera',
+        name: 'DeviceCamera',
+        component: '/cameras/overview/CamerasOverview',
+        meta: { 
+          icon: '',
+          title: '相机设备'
+        },
+      },
+      {
+        // NVR设备
+        path: 'nvr',
+        name: 'DeviceNVR',
+        component: '/cameras/nvrlist/NvrList',
+        meta: { 
+          icon: '',
+          title: 'NVR设备'
+        },
+      }
+    ]
+  },
+
+  /**
+   * 布局管理
+   */
+  {
+    path: '/layout',
+    name: 'Layout',
+    component: 'LAYOUT',
+    meta: { 
+      icon: 'PictureOutlined',
+      title: '布局管理',
+    },
+    children: [
+      {
+        // 场景布局
+        path: 'scene',
+        name: 'LayoutScene',
+        component: '/page-config/PageConfig',
+        meta: { 
+          icon: '',
+          title: '场景布局', 
+        },
+      },
+      {
+        // 相机布局
+        path: 'camera',
+        name: 'LayoutCamera',
+        component: '/map-config/mini-map/MiniMapConfig',
+        meta: { 
+          icon: '',
+          title: '相机布局',
+        }
+      }
+    ]
+  },
+
+  /**
+   * 算法管理
+   */
+  {
+    path: '/algorithm',
+    name: 'Algorithm',
+    component: 'LAYOUT',
+    meta: { 
+      icon: 'FunctionOutlined',
+      title: '算法管理',
+    },
+    children: [
+      {
+        // 算法预览
+        path: 'preview',
+        name: 'AlgorithmPreview',
+        component: '/cameras/algo-management/algoManagement',
+        meta: { 
+          icon: '',
+          title: '算法预览',
+        },
+      },
+      {
+        // 算法配置
+        path: 'config',
+        name: 'AlgorithmConfig',
+        component: '/cameras/preview/CameraPreview',
+        meta: { 
+          ico: '',
+          title: '算法配置',
+        },
+      }
+    ]
+  },
+
+  /**
+   * 用户管理
+   */
+  {
+    path: '/user',
+    name: 'User',
+    component: 'LAYOUT',
+    meta: { 
+      icon: 'UserOutlined',
+      title: '用户管理',
+    },
+    children: [
+      {
+        // 账号管理
+        path: 'account',
+        name: 'UserAccount',
+        component: '/system/user/user',
+        meta: { 
+          icon: '',
+          title: '账号管理',
+        }
+      },
+      {
+        // 角色管理
+        path: 'role',
+        name: 'UserRole',
+        component: '/system/role/role',
+        meta: { 
+          icon: '',
+          title: '角色管理',
+        }
+      },
+      {
+        // 组织管理
+        path: 'department',
+        name: 'UserDepartment',
+        component: '/auth/dept/dept',
+        meta: { 
+          icon: '',
+          title: '组织管理',
+        }
+      }
+    ]
+  },
+
+
+  /**
+   * 消息管理
+   */
+  {
+    path: '/message',
+    name: 'Message',
+    component: 'LAYOUT',
+    meta: { 
+      icon: 'SendOutlined',
+      title: '消息管理',
+    },
+    children: [
+      {
+        // 报表推送 
+        path: 'report',
+        name: 'MessageReport',
+        component: '/message/reportmessage/ReportMessage',
+        meta: { 
+          icon: '',
+          title: '报表推送',
+        },
+      },
+      {
+        // 报表推送配置(菜单不可见)
+        path: 'report-config',
+        name: 'MessageReportConfig',
+        component: '/message/reportmessage/ReportOperation',
+        meta: { 
+          icon: '',
+          title: '报表推送配置(菜单不可见)' 
+        },
+      },
+      {
+        // 报警推送
+        path: 'alarm',
+        name: 'MessageAlarm',
+        component: '/message/alarmMessages/alarmMessages',
+        meta: { 
+          icon: '',
+          title: '报警推送',
+        },
+      },
+      {
+        // 报警推送配置 (菜单不可见)
+        path: 'alarm-config',
+        name: 'MessageAlarmConfig',
+        component: '/message/alarm-config/AlarmConfig',
+        meta: { 
+          icon: '',
+          title: '报警推送配置(菜单不可见)',
+        },
+      },
+      {
+        // 系统通知
+        path: 'sys-notification',
+        name: 'MessageSysNotification',
+        component: '/message/systemNotifications/systemNotifications',
+        meta: { 
+          icon: '',
+          title: '系统通知',
+        },
+      },
+      {
+        // 系统通知配置(菜单不可见)
+        path: 'sys-notification-config',
+        name: 'MessageSysNotificationConfig',
+        component: '/message/sysnotion-config/SysnotionConfig',
+        meta: { 
+          icon: '',
+          title: '系统通知配置(菜单不可见)',
+        },
+      }, 
+      {
+        // 人员分组
+        path: 'personnel-group',
+        name: 'MessagePersonnelGroup',
+        component: '/message/persongroup/UserGroup',
+        meta: { 
+          icon: '',
+          title: '人员分组',
+        },
+      }
+
+    ]
+  },
+
+  /**
+   * 数据管理
+   */
+  {
+    path: '/data',
+    name: 'Data',
+    component: 'LAYOUT',
+    meta: {
+      icon: 'LineChartOutlined',
+      title: '数据管理',
+    },
+    children: [
+      {
+        // 平台统计
+        path: 'platform',
+        name: 'DataPlatform',
+        component: '/datamanager/platformdata/PlatformData',
+        meta: {
+          icon: '',
+          title: '平台统计'
+        }
+      },
+      {
+        // 历史视频 (视频回看)
+        path: 'playback',
+        name: 'DataPlayback',
+        component: '/datamanager/playback/Playback',
+        meta: {
+          icon: '',
+          title: '历史视频'
+        }
+      },
+      {
+        // 违规问题
+        path: 'violation',
+        name: 'DataViolation',
+        component: '/datamanager/alertformdata/AlertformData',
+        meta: {
+          icon: '',
+          title: '违规问题'
+        }
+      }
+    ]
+  },
+
+
+
+  /**
+   * 系统管理 (只有超管可见)
+   */
+  {
+    path: '/system',
+    name: 'System',
+    component: 'LAYOUT',
+    meta: { 
+      icon: 'OptionsSharp',
+      title: '系统管理',
+    },
+    children: [
+      {
+        // 租户管理
+        path: 'tenant',
+        name: 'SystemTenant',
+        component: '/system/tenant/tenant',
+        meta: { 
+          icon: '',
+          title: '租户管理',
+        }
+      },
+      {
+        // 菜单管理
+        path: 'menu',
+        name: 'SystemMenu',
+        component: '/system/menu/menu',
+        meta: { 
+          icon: '',
+          title: '菜单管理',
+        }
+      },
+      {
+        // 权限管理
+        path: 'permission',
+        name: 'SystemPermission',
+        component: '/system/permssion/PagePermission',
+        meta: { 
+          icon: '',
+          title: '权限管理',
+        }
+      },
+      {
+        // 平台反馈
+        path: 'feedback',
+        name: 'SystemFeedback',
+        component: '/feedback/feedback',
+        meta: { 
+          icon: '',
+          title: '平台反馈',
+        }
+      },
+      {
+        // 反馈处理
+        path: 'feedback-handle',
+        name: 'SystemFeedbackHandle',
+        component: '/feedback/handleFeedback',
+        meta: {
+          icon: '',
+          title: '反馈处理'
+        }
+      },
+      {
+        // 字典管理
+        path: 'dictionary',
+        name: 'SystemDictionary',
+        component: '/system/dictionary/dictionary',
+        meta: { 
+          icon: '',
+          title: '字典管理',
+        }
+      },
+      {
+        // 日志管理
+        path: 'logs',
+        name: 'SytemLogs',
+        component: 'ParentLayout',
+        meta: { 
+          icon: '',
+          title: '日志管理',
+        },
+        children: [
+          {
+            // 操作日志
+            path: 'operation',
+            name: 'SystemLogsOperation',
+            component: '/system/logs/operlog',
+            meta: { 
+              icon: '',
+              title: '操作日志',
+            },
+          },
+          {
+            // 登录日志
+            path: 'login',
+            name: 'SystemLogsLogin',
+            component: '/system/logs/logininfor',
+            meta: { 
+              icon: '',
+              title: '登录日志',
+            }
+          }
+        ]
+      }
+    ]
+  },
+
+  /**
+ * 异常页面
+ */
+  {
+    path: '/exception',
+    name: 'Exception',
+    component: 'LAYOUT',
+    meta: {
+      icon: 'ExclamationCircleOutlined',
+      title: '异常页面', 
+    },
+    children: [
+      {
+        // 403,
+        path: '403',
+        name: 'Exception403',
+        component: '/exception/403',
+        meta: {
+          icon: '',
+          title: '403',
+        },
+      }
+    ]
+  },
+
+  /**
+   * 测试
+   */
+  {
+    path: '/lf-test',
+    name: 'LFTest',
+    component: 'LAYOUT',
+    meta: {
+      icon: '',
+      title: '测试菜单,可删除'
+    },
+    children: [
+      {
+        path: 'test1',
+        name: 'LFTest-1',
+        component: '/exception/403',
+        meta: {
+          icon: '',
+          title: '403',
+        }, 
+      }
+    ]
+  }
+] as const;
+
+export interface _RouteViewItem {
+  label: string;
+  value: string;
+}
+export type _RouteView = _RouteViewItem & { children?: _RouteViewItem[] | null };
+export type _RouteViewTree = _RouteView[];
+
+/**
+ * 获取指定 父路由 下的子路由。
+ * 若 父级路由 未指定,则返回一级路由
+ */
+export function getChildRoutesView(parentRouteName?: string | null ): _RouteViewItem[] {
+  // 未指定 parentRouteName, 则返回 level1 的路由
+  if (parentRouteName == null) {
+    return fullRoutes.map(route => ({
+      label: route.meta.title as string,
+      value: route.name,
+    }));
+  }
+
+  // 获取指定父路由的子路由
+  const parentRoute: AppRouteRecordRaw = cloneDeep(getTreeItem(fullRoutes, parentRouteName, 'name'));
+  if (parentRoute.children && parentRoute.children.length > 0) {
+    return parentRoute.children.map(route => ({
+      label: route.meta.title as string,
+      value: route.name,
+    }));
+  } else {
+    return [];
+  }
+}
+
+/**
+ * 获取指定的 路由信息
+ */
+export function getRouteByName(routeName: string) {
+  return cloneDeep(getTreeItem(fullRoutes, routeName, 'name')) as AppRouteRecordRaw;
+}
+
+/**
+ * 转换成 el-tree-select 的数据结构形式
+ */
+export function transformToRouteViewTree(routes?: AppRouteRecordRaw[] | null) {
+  const tree: _RouteViewTree = [];
+  if (routes == null) return tree;
+
+  for(const route of routes) {
+    const treeItem: _RouteView = {
+      value: route.name,
+      label: route.meta.title as string,
+      children: null
+    }
+
+    if (route.children && route.children.length > 0) {
+      treeItem.children = transformToRouteViewTree(route.children);
+      tree.push(treeItem);
+    } else {
+      tree.push(treeItem);
+    }
+  }
+  
+  return tree;
+}
+
+export const routeViewTree = transformToRouteViewTree(fullRoutes);

+ 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),

+ 16 - 7
src/types/menu/type.ts

@@ -2,7 +2,7 @@
  * 后端保存菜单的详细信息
  * 其中 0 - false,1 - true
  */
-export interface MenuDetail {
+export interface MenuDetailItem {
   // 主键ID
   id: number | null;
   // 父级菜单主键ID
@@ -55,15 +55,24 @@ export interface MenuDetail {
   updatedBy?: string;
   // 路由是否删除(逻辑删除)
   isDeleted?: number;
-  // 子菜单
-  children?: MenuDetail[];
 }
 
+export type MenuDetail = MenuDetailItem & { children?: MenuDetailItem[] | null };
+export type MenuDetailTree = MenuDetail[];
+
+
+
 /**
  * 用于 element-plus 树形控件 展示
  */
-export interface MenuSimple {
+export interface MenuItem {
+  // 菜单名
   label: string;
-  key: number | string;
-  children?: MenuSimple[] | null;
-}
+  // 菜单主键ID
+  id: number;
+  // 路由 name
+  routeName: string;
+}
+
+export type Menu = MenuItem & { children?: MenuItem[] | null };
+export type MenuTree = Menu[];

+ 46 - 0
src/types/permission/type.ts

@@ -0,0 +1,46 @@
+/**
+ * 权限项 详细信息
+ */
+export interface PermissionItem {
+  // 权限ID
+  id: number | null;
+  // 上级ID
+  parentId: number | null;
+  // 权限编码
+  permissionCode: string;
+  // 权限名字
+  permissionName: string;
+  // 排序
+  orderNum: number;
+  // 是否禁用, 0 - 禁用, 1 - 启用
+  isDisabled: 0 | 1;
+  // 是否删除  
+  isDeleted?: number;
+  // 创建日期
+  createdAt?: string;
+  // 创建人
+  createdBy?: string;
+  // 更新日期
+  updatedAt?: string;
+  // 更新人
+  updatedBy?: string;
+}
+
+// 权限是可以嵌套的
+export type Permission = PermissionItem & { children?: PermissionItem[] | null };
+// 权限树
+export type PermissionTree = Permission[];
+
+/**
+ * 权限显示项目,用于生成 el-tree
+ */
+export type PermissionTreeKey = number | string;
+
+export interface PermissionViewItem {
+  label: string;
+  value: PermissionTreeKey;
+}
+
+export type PermissionView = PermissionViewItem & { children?: PermissionViewItem[] | null };
+export type PermissionViewTree = PermissionView[];
+

+ 53 - 0
src/types/role/type.ts

@@ -0,0 +1,53 @@
+import { PaginationRequest, PaginationResponse } from '@/types/common/type';
+import { PermissionTreeKey } from '../permission/type';
+
+/**
+ * 角色列表row
+ */
+export interface Role {
+  // 主键ID
+  id: number;
+  // 角色名
+  roleName: string;
+  // 备注
+  remark: string;
+  // 租户ID
+  tenantId?: number; 
+  isDeleted?: number;
+  createdAt?: string;
+  createdBy?: string;
+  updatedAt?: string;
+  updatedBy?: string;
+}
+
+/**
+ * 角色列表请求参数
+ */
+export interface RolePageRequest extends PaginationRequest {
+  queryParam?: {
+    roleName: Role['roleName'];
+    tenantId?: Role['tenantId'];
+  };
+}
+
+/**
+ * 角色列表数据
+ */
+export type RolePageResponse = PaginationResponse<Role>;
+
+
+/**
+ * 角色表单
+ */
+export interface RoleForm {
+  id: number | null;
+  roleName: string;
+  remark: string;
+  cameraIds: PermissionTreeKey[];
+  menuIds: PermissionTreeKey[];
+  permIds: PermissionTreeKey[];
+  tenantId?: number;
+}
+
+export type AssignedPermissions = Pick<RoleForm, 'cameraIds' | 'menuIds' | 'permIds'>;
+

+ 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;

+ 160 - 96
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">
@@ -147,8 +160,8 @@
             </div>
           </template>
           <el-radio-group v-model="formParams.isDisabled">
-            <el-radio-button :label="0">正常</el-radio-button>
-            <el-radio-button :label="1">停用</el-radio-button>
+            <el-radio-button label="正常" :value="0" />
+            <el-radio-button label="停用" :value="1" />
           </el-radio-group>
         </el-form-item>
       </el-col>
@@ -185,8 +198,8 @@
             </div>
           </template>
           <el-radio-group v-model="formParams.isHidden">
-            <el-radio-button :label="0">显示</el-radio-button>
-            <el-radio-button :label="1">隐藏</el-radio-button>
+            <el-radio-button label="显示" :value="0" />
+            <el-radio-button label="隐藏" :value="1" />
           </el-radio-group>
         </el-form-item>
       </el-col>
@@ -206,9 +219,9 @@
             </div>
           </template>
           <el-radio-group v-model="formParams.isCache" name="isCacheGroup">
-            <el-radio-button :label="0">不缓存</el-radio-button>
-            <el-radio-button :label="1">缓存</el-radio-button>
-          </el-radio-group>
+            <el-radio-button label="不缓存" :value="0" />
+            <el-radio-button label="缓存" :value="1" />
+          </el-radio-group>  
         </el-form-item>
       </el-col>
     </el-row>
@@ -228,8 +241,8 @@
             </div>
           </template>
           <el-radio-group v-model="formParams.isFrame">
-            <el-radio-button :label="0">否</el-radio-button>
-            <el-radio-button :label="1">是</el-radio-button>
+            <el-radio-button label="否" :value="0" />
+            <el-radio-button label="是" :value="1" />
           </el-radio-group>
         </el-form-item>
       </el-col>
@@ -268,31 +281,62 @@
 </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 { MenuDetail } 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',
+    },
+    menuName: {
+      required: true,
+      message: '菜单名称',
+      trigger: 'blur',
     },
-    isShowSubmit: {
-      type: Boolean,
-      defalut: false,
+    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: MenuDetail = {
+  const defaultFormParams: MenuDetailItem = {
     id: null,
     parentId: null,
     menuName: '',
@@ -313,51 +357,70 @@
     frameSrc: '',
     openType: 1, 
     query: '',
-    children: []
   };
-
   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 = {
-    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: MenuDetail) {
+  function setData(data: MenuDetailItem) {
     formParams.value = data;
     formRef.value?.resetFields();
   }
@@ -398,7 +461,8 @@
 
   function handleReset() {
     formRef.value?.resetFields();
-    formParams.value = Object.assign(formParams.value, defaultFormParams);
+    formParams.value = { ...defaultFormParams };
+    routeViewValue.value = '';
   }
 
   defineExpose({

+ 48 - 33
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>
@@ -93,14 +94,14 @@
   import { AlignLeftOutlined, FileAddOutlined } from '@vicons/antd';
   import { deleteMenu } from '@/api/system/menu';
   import { queryFullMenuTree } from '@/api/system/menu';
-  import { MenuDetail, MenuSimple } from '@/types/menu/type';
+  import { MenuDetailTree, MenuTree, MenuDetail, Menu } from '@/types/menu/type';
   import { getTreeItem } from '@/utils';
   import CreateDrawer from './CreateDrawer.vue';
   import MenuForm from './MenuForm.vue';
   import { cloneDeep } from 'lodash-es';
 
-  const menuDetailTree = shallowRef<MenuDetail[]>([]); // 菜单详情树
-  const menuTree = shallowRef<MenuSimple[]>([]); // 菜单树,用于展示
+  const menuDetailTree = shallowRef<MenuDetailTree>([]); // 菜单详情树
+  const menuTree = shallowRef<MenuTree>([]); // 菜单树,用于展示
   const selectedMenuId = ref<MenuDetail['id']>(null); // 选中的菜单
   const loading = ref(true); // 左侧菜单树加载
   const expandedKeys = ref([]); 
@@ -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: MenuSimple) {
-    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 treeItem: MenuSimple = getTreeItem(menuTree.value, currentKey);
-      treeItemTitle.value = treeItem.label;
+    if (currentMenuId) {
+      const menu: Menu = getTreeItem(menuTree.value, currentMenuId, 'id');
+      treeItemTitle.value = menu.label;
 
-      const info: MenuDetail = getTreeItem(menuDetailTree.value, currentKey, 'id');
-      const menuFormData = cloneDeep(info);
+      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,24 +178,29 @@
     }
   }
 
-  async function getPermissionList() {
+  /**
+   * 获取 菜单树。一个菜单详细树,用于创建和编辑菜单。 一个是展示树,用于 el-tree 组件
+   * 注意:原来的 getPermissionList 重新命名为 getMenuTree
+   */
+  async function getMenuTree() {
     loading.value = true;
     menuDetailTree.value = await queryFullMenuTree();
     menuTree.value = transformToSimpleMenus(menuDetailTree.value);
     loading.value = false;
   }
 
-  function transformToSimpleMenus(menus: MenuDetail[] | null): MenuSimple[] {
+  function transformToSimpleMenus(menus: MenuDetailTree | null): MenuTree {
     if (menus == null) {
       return [];
     }
   
-    let tree: MenuSimple[] = [];
+    const tree: MenuTree = [];
     for (const item of menus) {
-      const treeItem: MenuSimple = { 
-        key: item.id!, 
+      const treeItem: Menu = { 
+        id: item.id!, 
         label: item.menuName, 
-        children: null 
+        routeName: item.routeName,
+        children: null
       };
 
       if (Array.isArray(item.children) && item.children?.length > 0) {
@@ -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) {

+ 63 - 0
src/views/system/permission/CreateDrawer.vue

@@ -0,0 +1,63 @@
+<template>
+  <el-drawer v-model="drawerVisible" :size="width" :title="title" @close="handleReset">
+    <PermissionForm ref="formInstance" :permissionList="props.permissionList" @change="handleFormChange" />
+
+    <template #footer>
+      <el-space>
+        <el-button type="primary" :loading="subLoading" @click="formSubmit">提交</el-button>
+        <el-button @click="handleReset">重置</el-button>
+      </el-space>
+    </template>
+  </el-drawer>
+</template>
+
+<script lang="ts" setup>
+  import { ref } from 'vue';
+  import PermissionForm from './PermissionForm.vue';
+
+  const props = defineProps({
+    title: {
+      type: String,
+      default: '添加权限',
+    },
+    width: {
+      type: Number,
+      default: 580,
+    },
+    permissionList: {
+      type: Array,
+    },
+  })
+
+  const emit = defineEmits(['change']);
+
+  const formInstance = ref<InstanceType<typeof PermissionForm>>();
+  const drawerVisible = ref(false);
+  const subLoading = ref(false);
+
+  function openDrawer() {
+    drawerVisible.value = true;
+  }
+
+  function closeDrawer() {
+    drawerVisible.value = false;
+  }
+
+  function formSubmit() {
+    formInstance.value?.formSubmit();
+  }
+
+  function handleReset() {
+    formInstance.value?.handleReset();
+  }
+
+  function handleFormChange() {
+    closeDrawer();
+    emit('change');
+  }
+
+  defineExpose({
+    openDrawer,
+    closeDrawer,
+  });
+</script>

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

@@ -0,0 +1,239 @@
+<template>
+  <div class="page-permission">
+    <el-row :gutter="12">
+      <!-- 左侧权限树 -->
+      <el-col :span="6">
+        <el-card shadow="hover">
+          <template #header>
+            <el-space>
+              <el-button type="primary" icon-placement="left" @click="openCreateDrawer">
+                添加权限
+                <template #icon>
+                  <div class="flex items-center">
+                    <el-icon size="14">
+                      <FileAddOutlined />
+                    </el-icon>
+                  </div>
+                </template>
+              </el-button>
+
+              <el-button type="primary" plain icon-placement="left" @click="packHandle">
+                全部{{ expandedKeys.length ? '收起' : '展开' }}
+                <template #icon>
+                  <div class="flex items-center">
+                    <el-icon size="14">
+                      <AlignLeftOutlined />
+                    </el-icon>
+                  </div>
+                </template>
+              </el-button>
+            </el-space>
+          </template>
+
+          <div class="w-full">
+            <div v-loading="loading">
+              <el-scrollbar height="620px">
+                <el-tree
+                  ref="treeRef"
+                  :data="permissionViewTree"
+                  nodeKey="value"
+                  highlight-current
+                  check-strictly
+                  :expand-on-click-node="false"
+                  @current-change="onSelectTreeNode"
+                  @update:expanded-keys="onExpandedKeys"
+                />
+              </el-scrollbar>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+
+      <!-- 右侧编辑权限 -->
+      <el-col :span="18">
+        <el-card shadow="hover">
+          <template #header>
+            <div class="flex justify-between">
+              <el-space>
+                <span>编辑权限: </span>
+                <span>{{ treeItemTitle }}</span>
+              </el-space>
+              <el-popconfirm v-if="isEditing" :title="`确定要删除${treeItemTitle}吗?`" width="200" @confirm="handleDeletePermission">
+                <template #reference>
+                  <el-button type="danger" size="small">删除权限</el-button>
+                </template>
+              </el-popconfirm>
+            </div>
+          </template>
+          <!-- 表单 -->
+          <div class="pt-6">
+            <PermissionForm
+              v-show="selectedPermissionId != null"
+              ref="formInstance"
+              :permissionList="permissionViewTree"
+              @change="handleChangePermssion"
+              isShowSubmit
+              class="w-2/3 ml-10"
+            />
+            <el-empty v-show="selectedPermissionId == null" description="从左侧列表选择一项后,进行编辑" />
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- 添加新权限 -->
+    <CreateDrawer
+      ref="drawerInstance"
+      :permissionList="permissionViewTree"
+      @change="handleChangePermssion"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref, shallowRef, onMounted } from 'vue';
+  import PermissionForm from './PermissionForm.vue';
+  import CreateDrawer from './CreateDrawer.vue';
+  import { AlignLeftOutlined, FileAddOutlined } from '@vicons/antd';
+  import { PermissionTree, PermissionView, PermissionViewItem, PermissionViewTree, Permission } from '@/types/permission/type';
+  import { getPermissionTree, deletePermission } from '@/api/system/permission';
+  import { getTreeItem } from '@/utils';
+  import { cloneDeep } from 'lodash-es';
+import { ElMessage } from 'element-plus';
+  
+
+  // 左侧权限树相关
+  const expandedKeys = ref<PermissionViewItem['value'][]>([]);
+  const loading = ref(true);
+  const treeRef = ref();
+  const permissionTree = shallowRef<PermissionTree>([]);
+  const permissionViewTree = shallowRef<PermissionViewTree>([]);
+
+  // 右侧编辑菜单相关
+  const treeItemTitle = ref('');
+  const isEditing = ref(false);
+  const selectedPermissionId = ref<number | null>(null);
+  const formInstance = ref<InstanceType<typeof PermissionForm>>();
+
+  // 抽屉相关
+  const drawerInstance = ref<InstanceType<typeof CreateDrawer>>();
+
+  const queryPermissionTree = async () => {
+    loading.value = true;
+    try {
+      permissionTree.value = await getPermissionTree();
+      permissionViewTree.value = transformToViewTree(permissionTree.value);
+    } catch (e) {
+      console.error(e);
+    } finally {
+      loading.value = false;
+    }
+  };
+
+  const transformToViewTree = (tree: PermissionTree | null): PermissionViewTree => {
+    if (tree == null) return [];
+
+    const viewTree: PermissionViewTree = [];
+    for (const item of tree) {
+      const viewItem: PermissionView = {
+        value: item.id!,
+        label: item.permissionName,
+      };
+
+      if (Array.isArray(item.children) && item.children.length > 0) {
+        viewItem.children = transformToViewTree(item.children);
+        viewTree.push(viewItem);
+      } else {
+        viewTree.push(viewItem);
+      }
+    }
+
+    return viewTree;
+  };
+
+  const onSelectTreeNode = (data: PermissionView, node: any) => {
+    console.log(node);
+
+    const permissionId = data.value as number;
+    selectedPermissionId.value = permissionId;
+    isEditing.value = true;
+    treeItemTitle.value = data.label;
+      
+    const treeItem: Permission = getTreeItem(permissionTree.value, permissionId, 'id');
+    const permission = cloneDeep(treeItem);
+    delete permission.children;
+    formInstance.value?.setData(permission);
+  };
+
+  /**
+   * 左侧权限树 展开 / 收起
+   */
+  const packHandle = () => {
+    if (!expandedKeys.value.length) {
+      treeNodeExpand(true);
+      expandedKeys.value = permissionViewTree.value?.map((item) => item.value);
+    } else {
+      expandedKeys.value = [];
+      treeNodeExpand(false);
+    }
+  };
+
+  const treeNodeExpand = (status) => {
+    for (var i = 0; i < treeRef.value.store._getAllNodes().length; i++) {
+      treeRef.value.store._getAllNodes()[i].expanded = status;
+    }
+  };
+
+  const onExpandedKeys = (keys) => {
+    expandedKeys.value = keys;
+  };
+
+
+  function clearEditing() {
+    isEditing.value = false;
+    treeItemTitle.value = '';
+    selectedPermissionId.value = null;
+  }
+
+  /**
+   * 编辑权限,或者 新建权限 后的 change 事件处理
+   */
+  const handleChangePermssion = () => {
+    clearEditing();
+    queryPermissionTree();
+  };
+
+
+  /**
+   * 不允许删除父节点
+   */
+  const handleDeletePermission = async () => {
+    const treeItem: Permission = getTreeItem(permissionTree.value, selectedPermissionId.value!, 'id');
+    if (treeItem.children && treeItem.children.length > 0) {
+      ElMessage.error('当前权限含有子权限,无法删除');
+      return;
+    }
+
+    try {
+      await deletePermission(selectedPermissionId.value!);
+      clearEditing();
+      queryPermissionTree();
+    } catch(e) {
+      console.error(e);
+    }
+  }
+
+  function openCreateDrawer() {
+    drawerInstance.value?.openDrawer();
+  }
+
+  onMounted(() => {
+    queryPermissionTree();
+  });
+</script>
+
+<style scoped>
+  .page-permission {
+    height: calc(100vh - 100px);
+  }
+</style>

+ 209 - 0
src/views/system/permission/PermissionForm.vue

@@ -0,0 +1,209 @@
+<template>
+  <el-form
+    :model="formParams"
+    :rules="rules"
+    ref="formRef"
+    label-placement="left"
+    :label-width="100"
+    require-mark-placement="left"
+  >
+    <el-divider border-style="dashed" class="mt-0 mb-10">基本设置</el-divider>
+
+    <el-form-item label="上级权限" prop="parentId">
+      <el-tree-select
+        rowKey="key"
+        :data="permissionTree"
+        clearable
+        check-strictly
+        v-model="formParams.parentId"
+      />
+    </el-form-item>
+
+    <el-row :gutter="24">
+      <!-- 权限名称 -->
+      <el-col :span="12">
+        <el-form-item prop="permissionName">
+          <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>
+                  权限名称 对应 权限标识 `中文名称`
+                </template>
+              </el-tooltip>
+              <span>权限名称</span>
+            </div>
+          </template>
+          <el-input placeholder="权限名称" v-model="formParams.permissionName" />
+        </el-form-item>
+      </el-col>
+
+      <!-- 权限标识 -->
+      <el-col :span="12">
+        <el-form-item prop="permissionCode">
+          <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>
+                  权限标识,也是权限字符,比如 `system:menu:list`
+                </template>
+              </el-tooltip>
+              <span>权限标识</span>
+            </div>
+          </template>
+          <el-input placeholder="权限标识" v-model="formParams.permissionCode" />
+        </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">
+      <el-col :span="12">
+        <el-form-item label="权限状态" prop="isDisabled">
+          <el-radio-group v-model="formParams.isDisabled">
+            <el-radio-button label="正常" :value="0" />
+            <el-radio-button label="停用" :value="1" />
+          </el-radio-group>
+        </el-form-item>
+      </el-col>
+    </el-row>
+
+    <el-form-item v-if="isShowSubmit">
+      <el-button type="primary" :loading="subLoading" @click="formSubmit">保存修改</el-button>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script lang="ts" setup>
+  import { ref, computed } from 'vue';
+  import { ElMessage, FormInstance, FormRules } from 'element-plus';
+  import { addPermission, updatePermission } from '@/api/system/permission';
+  import { QuestionCircleOutlined } from '@vicons/antd';
+  import { PermissionItem } from '@/types/permission/type';
+
+  const emit = defineEmits(['change']);
+
+  const props = defineProps({
+    permissionList: {
+      type: Array,
+      defalut: () => [],
+    },
+    isShowSubmit: {
+      type: Boolean,
+      defalut: false,
+    },
+  });
+
+  const formRef = ref<FormInstance>();
+  const subLoading = ref(false);
+
+  const defaultFormParams: PermissionItem = {
+    id: null,
+    parentId: null,
+    permissionCode: '',
+    permissionName: '',
+    orderNum: 0,
+    isDisabled: 0,
+  };
+
+  const formParams = ref({ ...defaultFormParams });
+
+  const permissionTree = computed(() => {
+    // 可根据需要是否需要嵌套这个 避免出现选择器出现 0 的情况
+    return [
+      {
+        label: '顶级目录',
+        value: 0,
+        children: props.permissionList || [],
+      },
+    ];
+  });
+
+  const rules: FormRules = {
+    parentId: {
+      required: true,
+      message: '上级目录',
+      trigger: 'change',
+    },
+    permissionName: {
+      required: true,
+      message: '权限名称',
+      trigger: 'blur',
+    },
+    permissionCode: {
+      required: true,
+      message: '权限标识',
+      trigger: 'blur',
+    },
+  };
+
+  function setData(data: PermissionItem) {
+    formParams.value = data;
+    formRef.value?.resetFields();
+  }
+
+  function formSubmit() {
+    subLoading.value = true;
+    formRef.value?.validate((valid) => {
+      if (valid) {
+        if (!formParams.value.id) {
+          addPermission(formParams.value)
+            .then(() => {
+              subLoading.value = false;
+              ElMessage.success('添加成功');
+              handleReset();
+              emit('change');
+            })
+            .catch(() => {
+              subLoading.value = false;
+            });
+        } else {
+          updatePermission(formParams.value)
+            .then(() => {
+              subLoading.value = false;
+              ElMessage.success('修改成功');
+              handleReset();
+              emit('change');
+            })
+            .catch(() => {
+              subLoading.value = false;
+            });
+        }
+      } else {
+        subLoading.value = false;
+        ElMessage.error('请填写完整信息');
+      }
+    });
+  }
+
+  function handleReset() {
+    formRef.value?.resetFields();
+    formParams.value = Object.assign(formParams.value, defaultFormParams);
+  }
+
+  defineExpose({
+    formSubmit,
+    handleReset,
+    setData,
+  });
+</script>
+
+<style lang="scss" sctep>
+  .submit-form-item {
+    margin-left: 100px;
+  }
+</style>

+ 111 - 0
src/views/system/role/components/PermissionTreeCard.vue

@@ -0,0 +1,111 @@
+<template>
+  <el-card style="width: 100%">
+    <template #header>
+      <section class="title">{{ title }}</section>
+      <section>
+        <el-checkbox label="全部展开 / 全部收起" @change="expandAll"  />
+        <el-checkbox label="全选 / 全不选" @change="checkAll" />
+      </section>
+    </template>
+
+    <el-tree
+      ref="treeInstance"
+      :data="treeData" 
+      node-key="value" 
+      show-checkbox
+      :expand-on-click-node="false"
+      @check="onNodeCheck"
+    />
+  </el-card>
+</template>
+
+<script setup lang="ts">
+  import { ref, shallowRef } from 'vue';
+  import { ElTree } from 'element-plus';
+  import { PermissionViewTree, PermissionTreeKey } from '@/types/permission/type';
+
+  defineProps<{
+    title: string;
+    treeData: PermissionViewTree;
+  }>();
+  
+  const treeInstance = ref<InstanceType<typeof ElTree>>();
+  const leafCheckedKeys = shallowRef<PermissionTreeKey[]>([]);
+
+  /**
+   * 当check树节点时,获取其下的所有leaf节点的值
+   */
+  const onNodeCheck = () => {
+    leafCheckedKeys.value = treeInstance.value?.getCheckedKeys(true)!;
+  };
+
+  /**
+   * 全部展开 / 全部收起
+   * @param expanded 
+   */
+  const expandAll = (expanded: boolean) => {
+    treeInstance.value?.store._getAllNodes().forEach(node => node.expanded = expanded);
+  };
+
+  
+  /**
+   * 全选 / 全不选
+   * @param checked 
+   */
+  const checkAll = (checked: boolean) => {
+    leafCheckedKeys.value = checked ? getAllLeafKeys() : [];
+    treeInstance.value?.setCheckedKeys(leafCheckedKeys.value);
+  };
+
+  /**
+   * 获取所有叶子节点的 node-key
+   */
+  const getAllLeafKeys = () => {
+    return treeInstance.value?.store._getAllNodes()
+      .filter(node => node.isLeaf)
+      .map(node => node.data.value) as PermissionTreeKey[];
+  };
+
+  /**
+   * 编辑角色时,勾选已分配的权限
+   */
+  const setAssignedPermissions = (keys: PermissionTreeKey[]) => {
+    treeInstance.value?.setCheckedKeys(keys, true);
+    leafCheckedKeys.value = keys;
+  };
+
+  
+  /**
+   * 返回选择的权限值.
+   * 权限值可能是 相机ID、菜单ID、或 权限ID
+   */
+  const getSelectedPermissions = (): PermissionTreeKey[] => {
+    return [ ...leafCheckedKeys.value ];
+  };
+
+
+  /**
+   * 重置
+   */
+  const reset = () => {
+    checkAll(false);
+  }
+
+  defineExpose({ getSelectedPermissions, reset, setAssignedPermissions });
+</script>
+
+<style scoped>
+  .el-card + .el-card {
+    margin-top: 10px;
+  }
+
+  :deep(.el-card__header) {
+    padding: 10px;
+  }
+
+  .title:before {
+    content: '';
+    margin-right: 10px;
+    border-left: 3px solid #1890ff;
+  }
+</style>

+ 142 - 0
src/views/system/role/components/RoleDrawer.vue

@@ -0,0 +1,142 @@
+<template>
+  <el-drawer :title="title" v-model="drawerOpened" @close="reset">
+    <el-form 
+      label-position="left" 
+      label-width="80px" 
+      :model="formData" 
+      :rules="formRules"
+      ref="formInstance"
+    >
+      <el-form-item label="角色名称" prop="roleName">
+        <el-input placeholder="角色名称" v-model="formData.roleName" />
+      </el-form-item>
+
+      <el-form-item label="角色权限">
+        <PermissioTreenCard title="相机权限" :tree-data="[]" ref="cameraCardInstance" />
+        <PermissioTreenCard title="菜单权限" :tree-data="menuPermTreeData" ref="menuCardInstance" />
+        <PermissioTreenCard title="功能权限" :tree-data="funcPermTreeData" ref="funcCardInstance" />
+      </el-form-item>
+
+      <el-form-item label="备注" prop="remark">
+        <el-input type="textarea" placeholder="备注" v-model="formData.remark" />
+      </el-form-item>
+    </el-form>
+
+    <template #footer>
+      <el-button @click="reset">重置</el-button>
+      <el-button type="primary" @click="submit">提交</el-button>
+    </template>
+  </el-drawer>
+</template>
+
+<script setup lang="ts">
+  import { reactive, ref, computed, toRaw } from 'vue';
+  import PermissioTreenCard from './PermissionTreeCard.vue';
+  import { RoleForm, Role, AssignedPermissions } from '@/types/role/type';
+  import { FormRules, FormInstance, ElMessage } from 'element-plus';
+  import useMenuPermTree from '../hooks/useMenuPermissionTree';
+  import useFuncPermTree from '../hooks/useFunctionPermissionTree';
+  import { createRole, editRole, getAssignedPerms } from '@/api/system/role';
+
+  defineProps<{
+    title: string;
+  }>();
+
+  const emits = defineEmits<{
+    (e: 'submitted'): void;  // 提交之后触发的事件
+  }>();
+
+  const { menuPermTreeData } = useMenuPermTree();
+  const { funcPermTreeData} = useFuncPermTree();
+
+  const drawerOpened = ref(false);
+  const cameraCardInstance = ref<InstanceType<typeof PermissioTreenCard>>();
+  const menuCardInstance = ref<InstanceType<typeof PermissioTreenCard>>();
+  const funcCardInstance = ref<InstanceType<typeof PermissioTreenCard>>();   
+
+  // 表单相关
+  const defaultFormData = (): RoleForm => ({
+    id: null,
+    roleName: '',
+    remark: '',
+    cameraIds: [],
+    menuIds: [],
+    permIds: [],
+  });
+  const formData = reactive<RoleForm>(defaultFormData());
+  const formRules: FormRules = {
+    roleName: { required: true, trigger: 'blur', message: '请填写角色名称' },
+    remark: {},
+  };
+  const formInstance = ref<FormInstance>();
+  
+  const isEditing = computed(() => formData.id != null);
+
+  /**
+   * 打开 drawer。如果未传递 roleId,表示创建角色;反之,表示编辑角色
+   * @param roleId 可选
+   */
+  const open = (role?: Role) => {
+    if (role) {
+      formData.id = role.id;
+      formData.roleName = role.roleName;
+      formData.remark = role.remark;
+      getAssignedPermissions(role.id);
+
+      // TODO: 获取已拥有的权限
+    }
+
+    drawerOpened.value = true;
+  };
+
+  /**
+   * 获取当前角色已分配的权限数据,并更新相应的权限数
+   */
+  const getAssignedPermissions = async (roleId: number) => {
+    try {
+      const result = await getAssignedPerms(roleId);
+      displayAssignedPermissions(result);
+    } catch (e) {
+      console.error(e);
+    }
+  }
+
+  const displayAssignedPermissions = (result: AssignedPermissions) => {
+    cameraCardInstance.value?.setAssignedPermissions(result.cameraIds ?? []);
+    menuCardInstance.value?.setAssignedPermissions(result.menuIds ?? []);
+    funcCardInstance.value?.setAssignedPermissions(result.permIds ?? []);
+  }
+
+  /**
+   * 重置表单
+   */
+  const reset = () => {
+    formInstance.value?.resetFields();
+    Object.assign(formData, defaultFormData());
+    cameraCardInstance.value?.reset();
+    menuCardInstance.value?.reset();
+    funcCardInstance.value?.reset();
+  };
+
+  /**
+   * 提交。创建和编辑统一
+   */
+  const submit = async () => {
+    formData.cameraIds = cameraCardInstance.value!.getSelectedPermissions();
+    formData.menuIds = menuCardInstance.value!.getSelectedPermissions();
+    formData.permIds = funcCardInstance.value!.getSelectedPermissions();
+
+    const api = isEditing.value ? editRole : createRole;
+    try {
+      await api(toRaw(formData));
+      drawerOpened.value = false;
+      ElMessage.success('提交成功');
+      // 让父组件更新列表
+      emits('submitted');
+    } catch (e) {
+      console.error(e);
+    }
+  }
+
+  defineExpose({ open });
+</script>

+ 0 - 0
src/views/system/role/hooks/useCameraPermissionTree.ts


+ 19 - 0
src/views/system/role/hooks/useFunctionPermissionTree.ts

@@ -0,0 +1,19 @@
+import {shallowRef, onMounted } from 'vue';
+import { getOwnedPermissionTree } from "@/api/system/permission";
+import { PermissionViewTree } from "@/types/permission/type";
+import { transformToViewTree } from '../utils/tree';
+export default function useMenuPermissionTree() {
+  const funcPermTreeData = shallowRef<PermissionViewTree>([]);
+  const buildPermissionTree = async () => {
+    try {
+      const result = await getOwnedPermissionTree();
+      funcPermTreeData.value = transformToViewTree(result, 'permissionName', 'id');
+    } catch (e) {
+      console.error(e);
+    }
+  };
+
+  onMounted(() => buildPermissionTree());
+
+  return { funcPermTreeData };
+}

+ 19 - 0
src/views/system/role/hooks/useMenuPermissionTree.ts

@@ -0,0 +1,19 @@
+import { shallowRef, onMounted } from 'vue';
+import { queryOwnedMenuTree } from "@/api/system/menu";
+import { PermissionViewTree } from "@/types/permission/type";
+import { transformToViewTree } from '../utils/tree';
+export default function useMenuPermissionTree() {
+  const menuPermTreeData = shallowRef<PermissionViewTree>([]);
+  const buildPermissionTree = async () => {
+    try {
+      const result = await queryOwnedMenuTree();
+      menuPermTreeData.value = transformToViewTree(result, 'menuName', 'id');
+    } catch (e) {
+      console.error(e);
+    }
+  };
+
+  onMounted(() => buildPermissionTree());
+
+  return { menuPermTreeData };
+}

+ 65 - 0
src/views/system/role/hooks/useRolesQuery.ts

@@ -0,0 +1,65 @@
+import { reactive, shallowRef, toRaw, ref } from 'vue';
+import { DEFAULT_PAGE_SIZE } from '@/types/common/constants';
+import { Role, RolePageRequest } from '@/types/role/type';
+import { cloneDeep } from 'lodash-es';
+import { getRolesPage, getRoles } from '@/api/system/role';
+
+const defaultRolePageRequest: RolePageRequest = {
+  pageNumber: 1,
+  pageSize: DEFAULT_PAGE_SIZE,
+  queryParam: {
+    roleName: '',
+  }
+}
+
+
+export default function useRolesQuery() {
+  const requestParams = reactive<RolePageRequest>(cloneDeep(defaultRolePageRequest));
+  const roleList = shallowRef<Role[]>([]);
+  const total = ref(0);
+  const loading = ref(false);
+
+  const setRequestParams = (params: Partial<RolePageRequest>) => {
+    Object.assign(requestParams, params);
+  }
+
+  const resetRequestParams = () => {
+    Object.assign(requestParams, defaultRolePageRequest)
+  }
+
+
+  const queryRolesPage = async () => {
+    try {
+      loading.value = true;
+      const result = await getRolesPage(toRaw(requestParams));
+      roleList.value = result.records;
+      total.value = result.totalRow;
+    } catch (e) {
+      console.error(e);
+    } finally {
+      loading.value = false;
+    }
+  };
+
+  const queryRoles = async () => {
+    try {
+      loading.value = true;
+      roleList.value = await getRoles();
+    } catch (e) {
+      console.error(e);
+    } finally {
+      loading.value = false;
+    }
+  };
+
+  return {
+    requestParams,
+    roleList,
+    total,
+    loading,
+    setRequestParams,
+    resetRequestParams,
+    queryRolesPage,
+    queryRoles,
+  };
+}

+ 58 - 167
src/views/system/role/role.vue

@@ -1,190 +1,81 @@
 <template>
-  <page-wrapper>
-    <el-card :bordered="false" class="mb-3 proCard">
+  <div>
+    <el-card class="mb-3 proCard">
       <el-space align="center">
         <el-input
           :style="{ width: '320px' }"
-          v-model="params.roleName"
+          v-model="requestParams.queryParam!.roleName"
           clearable
           placeholder="请输入角色名称"
-          @keyup.enter="reloadTable"
+          @keyup.enter="queryRolesPage"
         />
-        <el-button type="primary" @click="reloadTable">
-          <template #icon>
-            <el-icon>
-              <SearchOutlined />
-            </el-icon>
-          </template>
-          查询
-        </el-button>
+        <el-button type="primary" :icon="Search" @click="queryRolesPage">查询</el-button>
       </el-space>
     </el-card>
-    <el-card :bordered="false" class="proCard">
-      <BasicTable
-        :columns="columns"
-        :request="loadDataTable"
-        :row-key="(row) => row.id"
-        :pagination="{ hideOnSinglePage: false }"
-        :tableSetting="{
-          size: false,
-          redo: false,
-          fullscreen: false,
-          striped: false,
-          setting: false,
-        }"
-        ref="tableRef"
-        :actionColumn="actionColumn"
-        @update:checked-row-keys="onCheckedRow"
-      >
-        <template #tableTitle>
-          <el-button type="primary" @click="openCreateUserDrawer">
-            <template #icon>
-              <el-icon>
-                <FileAddOutlined />
-              </el-icon>
-            </template>
-            用户角色
-          </el-button>
-          <el-button type="primary" @click="openCreateDrawer">
-            <template #icon>
-              <el-icon>
-                <FileAddOutlined />
-              </el-icon>
-            </template>
-            管理员角色
-          </el-button>
-        </template>
 
-        <template #action>
-          <TableAction />
-        </template>
-      </BasicTable>
-    </el-card>
+    <el-card>
+      <template #header>
+        <el-button type="primary" @click="openDrawer()">添加角色</el-button>
+      </template>
+
+      <el-table height="calc(100vh - 340px)" :data="roleList">
+        <el-table-column label="角色ID" width="100" prop="id" />
+        <el-table-column label="角色名称" prop="roleName" />
+        <el-table-column label="备注" prop="remark" />
+        <el-table-column label="创建时间" width="200" prop="createdAt" />
+        <el-table-column label="操作" width="160">
+          <template #default="{ row }">
+            <el-space>
+              <el-button type="primary" :icon="Edit" text @click="openDrawer(row)" />
+              <el-button :icon="Delete" text />
+            </el-space>
+          </template>
+        </el-table-column>
+      </el-table>
 
-    <CreateDrawer
-      ref="createDrawerRef"
-      :title="drawerTitle"
-      :permissionList="treeData"
-      @change="reloadTable"
-    />
+      <section class="mt-4 flex justify-end">
+        <el-pagination
+          background
+          layout="total, sizes, prev, pager, next"
+          :page-sizes="[10, 30, 50]"
+          :total="total"
+          v-model:page-size="requestParams.pageSize"
+          v-model:current-page="requestParams.pageNumber"
+          @change="queryRolesPage"
+        />
+      </section>
+    </el-card>
 
-    <CreateUserDrawer ref="createUserDrawerRef" :title="drawerTitle" @change="reloadTable" />
-  </page-wrapper>
+    <RoleDrawer :title="drawerTitle" ref="drawerInstance" @submitted="onSubmitRole" />
+  </div>
 </template>
 
 <script lang="ts" setup>
-  import { reactive, ref, unref, h, onMounted } from 'vue';
-  import { ElMessage } from 'element-plus';
-  import { BasicTable, TableAction, BasicColumn } from '@/components/Table';
-  import { roleList, permissionList, delAdminRole, delUserRole } from '@/api/system/role';
-  import { columns } from './columns';
-  import { FileAddOutlined, SearchOutlined } from '@vicons/antd';
-  import CreateDrawer from './CreateDrawer.vue';
-  import CreateUserDrawer from './CreateUserDrawer.vue';
-  import { useUserStore } from '@/store/modules/user';
+  import { ref, onMounted } from 'vue';
+  import { Edit, Delete, Search } from '@element-plus/icons-vue';
+  import RoleDrawer from './components/RoleDrawer.vue';
+  import useRolesQuery from './hooks/useRolesQuery';
+  import { Role } from '@/types/role/type';
 
-  const message = ElMessage;
-  const tableRef = ref();
-  const createDrawerRef = ref();
-  const createUserDrawerRef = ref();
-  const drawerTitle = ref('添加角色');
-  const treeData = ref([]);
-  const userStore = useUserStore();
+  const { roleList, total, queryRolesPage, requestParams } = useRolesQuery();
 
-  const params = reactive({
-    roleName: '',
-  });
+  // drawer 相关变量
+  const drawerTitle = ref('');
+  const drawerInstance = ref<InstanceType<typeof RoleDrawer>>();
 
-  const actionColumn: BasicColumn = reactive({
-    width: 150,
-    title: '操作',
-    key: 'action',
-    fixed: 'right',
-    render(record) {
-      return h(TableAction as any, {
-        style: 'button',
-        actions: [
-          {
-            label: '删除',
-            isConfirm: true,
-            popConfirm: {
-              onConfirm: handleDelete.bind(null, record.row),
-              title: '您确定要删除吗?',
-              confirmButtonText: '确定',
-              cancelButtonText: '取消',
-            },
-          },
-          {
-            label: '编辑',
-            onClick: handleEdit.bind(null, record.row),
-          },
-        ],
-      });
-    },
-  });
-
-  const loadDataTable = async (res: any) => {
-    let _params = {
-      ...unref(params),
-      ...res,
-    };
-    return await roleList(_params);
+  const openDrawer = (row?: Role) => {
+    drawerTitle.value = row ? '编辑角色' : '添加角色';
+    drawerInstance.value?.open(row);
   };
 
-  function openCreateDrawer() {
-    drawerTitle.value = '添加角色';
-    const { openDrawer } = createDrawerRef.value;
-    openDrawer();
-  }
-
-  function openCreateUserDrawer() {
-    drawerTitle.value = '添加角色';
-    const { openDrawer } = createUserDrawerRef.value;
-    openDrawer();
-  }
-
-  function onCheckedRow(rowKeys: any[]) {
-    console.log(rowKeys);
-  }
-
-  function reloadTable() {
-    tableRef.value.reload();
-  }
-
-  function handleEdit(record: Recordable) {
-    console.log('点击了编辑', record);
-    console.log('record.roleType', record.roleType);
-    drawerTitle.value = '编辑角色';
-    if (record.roleType === 1 || record.roleType === 2) {
-      const { openDrawer } = createDrawerRef.value;
-      openDrawer(record.roleId);
-    } else {
-      const { openDrawer } = createUserDrawerRef.value;
-      openDrawer(record.roleId);
-    }
-  }
-
-  function handleDelete(record: Recordable) {
-    console.log('record.roleType', record.roleType);
-    // if (userStore.getRoleTypes.includes(String(record.roleId))) {
-    //   message.error('不能删除自己的角色');
-    //   return;
-    // }
-    if (record.roleType === 1 || record.roleType === 2) {
-      delAdminRole({ roleId: record.roleId }).then(() => {
-        message.success('删除成功');
-        reloadTable();
-      });
-    } else {
-      delUserRole({ roleId: record.roleId }).then(() => {
-        message.success('删除成功');
-        reloadTable();
-      });
-    }
-  }
+  /**
+   * 创建或编辑角色后重新获取列表
+   */
+  const onSubmitRole = () => {
+    queryRolesPage();
+  };
 
-  onMounted(async () => {
-    const list = await permissionList();
-    treeData.value = list.filter((item) => item.label !== '功能权限');
+  onMounted(() => {
+    queryRolesPage();
   });
 </script>

+ 30 - 0
src/views/system/role/utils/tree.ts

@@ -0,0 +1,30 @@
+import { PermissionView, PermissionViewTree } from "@/types/permission/type";
+
+export function transformToViewTree<T>(rawTree: Array<T>, labelKey='label', valueKey='value', childrenKey='children'): PermissionViewTree {
+  if (rawTree == null || !Array.isArray(rawTree) ) {
+    throw 'Tree data must be an array';
+  }
+
+  if (!rawTree.length) {
+    return [];
+  }
+
+  const viewTree: PermissionViewTree = [];
+
+  for (const item of rawTree) {
+    const viewItem: PermissionView = {
+      value: item[valueKey],
+      label: item[labelKey],
+      children: [],
+    };
+
+    if (Array.isArray(item[childrenKey]) && item[childrenKey].length) {
+      viewItem.children = transformToViewTree(item[childrenKey], labelKey, valueKey, childrenKey);
+      viewTree.push(viewItem);
+    } else {
+      viewTree.push(viewItem);
+    }
+  }
+
+  return viewTree;
+}