Browse Source

菜单管理

wangwei 4 years ago
parent
commit
af902e4759

+ 151 - 0
mock/demo/system.ts

@@ -0,0 +1,151 @@
+import { MockMethod } from 'vite-plugin-mock';
+import { resultPageSuccess, resultSuccess } from '../_util';
+
+const accountList = (() => {
+  const result: any[] = [];
+  for (let index = 0; index < 20; index++) {
+    result.push({
+      id: `${index}`,
+      account: '@first',
+      email: '@email',
+      nickname: '@cname()',
+      role: '@first',
+      createTime: '@datetime',
+      remark: '@cword(10,20)',
+      'status|1': ['0', '1'],
+    });
+  }
+  return result;
+})();
+
+const roleList = (() => {
+  const result: any[] = [];
+  for (let index = 0; index < 4; index++) {
+    result.push({
+      id: `${index}`,
+      orderNo: `${index + 1}`,
+      roleName: ['超级管理员', '管理员', '文章管理员', '普通用户'][index],
+      roleValue: '@first',
+      createTime: '@datetime',
+      remark: '@cword(10,20)',
+      'status|1': ['0', '1'],
+    });
+  }
+  return result;
+})();
+
+const deptList = (() => {
+  const result: any[] = [];
+  for (let index = 0; index < 3; index++) {
+    result.push({
+      id: `${index}`,
+      deptName: ['华东分部', '华南分部', '西北分部'][index],
+      orderNo: index + 1,
+      createTime: '@datetime',
+      remark: '@cword(10,20)',
+      'status|1': ['0', '0', '1'],
+      children: (() => {
+        const children: any[] = [];
+        for (let j = 0; j < 4; j++) {
+          children.push({
+            id: `${index}-${j}`,
+            deptName: ['研发部', '市场部', '商务部', '财务部'][j],
+            orderNo: j + 1,
+            createTime: '@datetime',
+            remark: '@cword(10,20)',
+            'status|1': ['0', '1'],
+            parentDept: `${index}`,
+            children: undefined,
+          });
+        }
+        return children;
+      })(),
+    });
+  }
+  return result;
+})();
+
+const menuList = (() => {
+  const result: any[] = [];
+  for (let index = 0; index < 3; index++) {
+    result.push({
+      id: `${index}`,
+      icon: ['ion:layers-outline', 'ion:git-compare-outline', 'ion:tv-outline'][index],
+      component: 'LAYOUT',
+      menuName: ['Dashboard', '权限管理', '功能'][index],
+      permission: '',
+      orderNo: index + 1,
+      createTime: '@datetime',
+      'status|1': ['0', '0', '1'],
+      children: (() => {
+        const children: any[] = [];
+        for (let j = 0; j < 4; j++) {
+          children.push({
+            id: `${index}-${j}`,
+            menuName: ['菜单1', '菜单2', '菜单3', '菜单4'][j],
+            icon: 'ion:document',
+            permission: ['menu1:view', 'menu2:add', 'menu3:update', 'menu4:del'][index],
+            component: [
+              '/dashboard/welcome/index',
+              '/dashboard/analysis/index',
+              '/dashboard/workbench/index',
+              '/dashboard/test/index',
+            ][j],
+            orderNo: j + 1,
+            createTime: '@datetime',
+            'status|1': ['0', '1'],
+            parentMenu: `${index}`,
+            children: undefined,
+          });
+        }
+        return children;
+      })(),
+    });
+  }
+  return result;
+})();
+
+export default [
+  {
+    url: '/api/system/getAccountList',
+    timeout: 100,
+    method: 'get',
+    response: ({ query }) => {
+      const { page = 1, pageSize = 20 } = query;
+      return resultPageSuccess(page, pageSize, accountList);
+    },
+  },
+  {
+    url: '/api/system/getRoleListByPage',
+    timeout: 100,
+    method: 'get',
+    response: ({ query }) => {
+      const { page = 1, pageSize = 20 } = query;
+      return resultPageSuccess(page, pageSize, roleList);
+    },
+  },
+  {
+    url: '/api/system/getAllRoleList',
+    timeout: 100,
+    method: 'get',
+    response: () => {
+      return resultSuccess(roleList);
+    },
+  },
+  {
+    url: '/api/system/getDeptList',
+    timeout: 100,
+    method: 'get',
+    response: () => {
+      return resultSuccess(deptList);
+    },
+  },
+  {
+    url: '/api/system/getMenuList',
+    timeout: 100,
+    method: 'get',
+    response: () => {
+      return resultSuccess(menuList);
+    },
+  },
+] as MockMethod[];

+ 5 - 5
mock/sys/menu.ts

@@ -98,18 +98,18 @@ const permissionRoute = {
   path: '/permission',
   name: 'Permission',
   component: 'LAYOUT',
-  redirect: '/permission/detail',
+  redirect: '/permission/role',
   meta: {
     icon: 'ant-design:lock-outlined',
     title: 'routes.permission.management',
   },
   children: [
     {
-      path: 'detail',
-      name: 'Detail',
-      component: '/permission/index',
+      path: 'role',
+      name: 'Role',
+      component: '/permission/role/index',
       meta: {
-        title: 'routes.permission.detail',
+        title: 'routes.permission.role',
         icon: 'bx:bx-lock',
       },
     },

+ 74 - 0
src/api/demo/model/systemModel.ts

@@ -0,0 +1,74 @@
+import { BasicPageParams, BasicFetchResult } from '/@/api/model/baseModel';
+
+export type AccountParams = BasicPageParams & {
+  account?: string;
+  nickname?: string;
+};
+
+export type RoleParams = {
+  roleName?: string;
+  status?: string;
+};
+
+export type RolePageParams = BasicPageParams & RoleParams;
+
+export type DeptParams = {
+  deptName?: string;
+  status?: string;
+};
+
+export type MenuParams = {
+  menuName?: string;
+  status?: string;
+};
+
+export interface AccountListItem {
+  id: string;
+  account: string;
+  email: string;
+  nickname: string;
+  role: number;
+  createTime: string;
+  remark: string;
+  status: number;
+}
+
+export interface DeptListItem {
+  id: string;
+  orderNo: string;
+  createTime: string;
+  remark: string;
+  status: number;
+}
+
+export interface MenuListItem {
+  id: string;
+  orderNo: string;
+  createTime: string;
+  status: number;
+  icon: string;
+  component: string;
+  permission: string;
+}
+
+export interface RoleListItem {
+  id: string;
+  roleName: string;
+  roleValue: string;
+  status: number;
+  orderNo: string;
+  createTime: string;
+}
+
+/**
+ * @description: Request list return value
+ */
+export type AccountListGetResultModel = BasicFetchResult<AccountListItem>;
+
+export type DeptListGetResultModel = BasicFetchResult<DeptListItem>;
+
+export type MenuListGetResultModel = BasicFetchResult<MenuListItem>;
+
+export type RolePageListGetResultModel = BasicFetchResult<RoleListItem>;
+
+export type RoleListGetResultModel = RoleListItem[];

+ 36 - 0
src/api/demo/system.ts

@@ -0,0 +1,36 @@
+import {
+  AccountParams,
+  DeptListItem,
+  MenuParams,
+  RoleParams,
+  RolePageParams,
+  MenuListGetResultModel,
+  DeptListGetResultModel,
+  AccountListGetResultModel,
+  RolePageListGetResultModel,
+  RoleListGetResultModel,
+} from './model/systemModel';
+import { defHttp } from '/@/utils/http/axios';
+
+enum Api {
+  AccountList = '/system/getAccountList',
+  DeptList = '/system/getDeptList',
+  MenuList = '/system/getMenuList',
+  RolePageList = '/system/getRoleListByPage',
+  GetAllRoleList = '/system/getAllRoleList',
+}
+
+export const getAccountList = (params: AccountParams) =>
+  defHttp.get<AccountListGetResultModel>({ url: Api.AccountList, params });
+
+export const getDeptList = (params?: DeptListItem) =>
+  defHttp.get<DeptListGetResultModel>({ url: Api.DeptList, params });
+
+export const getMenuList = (params?: MenuParams) =>
+  defHttp.get<MenuListGetResultModel>({ url: Api.MenuList, params });
+
+export const getRoleListByPage = (params?: RolePageParams) =>
+  defHttp.get<RolePageListGetResultModel>({ url: Api.RolePageList, params });
+
+export const getAllRoleList = (params?: RoleParams) =>
+  defHttp.get<RoleListGetResultModel>({ url: Api.GetAllRoleList, params });

+ 4 - 0
src/components/Form/src/componentMap.ts

@@ -21,6 +21,8 @@ import {
 import RadioButtonGroup from './components/RadioButtonGroup.vue';
 import ApiSelect from './components/ApiSelect.vue';
 import { BasicUpload } from '/@/components/Upload';
+import { StrengthMeter } from '/@/components/StrengthMeter';
+import { IconPicker } from '/@/components/Icon';
 
 const componentMap = new Map<ComponentType, Component>();
 
@@ -51,6 +53,8 @@ componentMap.set('MonthPicker', DatePicker.MonthPicker);
 componentMap.set('RangePicker', DatePicker.RangePicker);
 componentMap.set('WeekPicker', DatePicker.WeekPicker);
 componentMap.set('TimePicker', TimePicker);
+componentMap.set('StrengthMeter', StrengthMeter);
+componentMap.set('IconPicker', IconPicker);
 
 componentMap.set('Upload', BasicUpload);
 

+ 1 - 1
src/components/Form/src/types/index.ts

@@ -91,7 +91,6 @@ export type ComponentType =
   | 'Select'
   | 'ApiSelect'
   | 'SelectOptGroup'
-  | 'SelectOption'
   | 'TreeSelect'
   | 'Transfer'
   | 'RadioButtonGroup'
@@ -108,4 +107,5 @@ export type ComponentType =
   | 'Switch'
   | 'StrengthMeter'
   | 'Upload'
+  | 'IconPicker'
   | 'Render';

+ 793 - 0
src/components/Icon/data/icons.data.ts

@@ -0,0 +1,793 @@
+export default {
+  prefix: 'ant-design',
+  icons: [
+    'account-book-filled',
+    'account-book-outlined',
+    'account-book-twotone',
+    'aim-outlined',
+    'alert-filled',
+    'alert-outlined',
+    'alert-twotone',
+    'alibaba-outlined',
+    'align-center-outlined',
+    'align-left-outlined',
+    'align-right-outlined',
+    'alipay-circle-filled',
+    'alipay-circle-outlined',
+    'alipay-outlined',
+    'alipay-square-filled',
+    'aliwangwang-filled',
+    'aliwangwang-outlined',
+    'aliyun-outlined',
+    'amazon-circle-filled',
+    'amazon-outlined',
+    'amazon-square-filled',
+    'android-filled',
+    'android-outlined',
+    'ant-cloud-outlined',
+    'ant-design-outlined',
+    'apartment-outlined',
+    'api-filled',
+    'api-outlined',
+    'api-twotone',
+    'apple-filled',
+    'apple-outlined',
+    'appstore-add-outlined',
+    'appstore-filled',
+    'appstore-outlined',
+    'appstore-twotone',
+    'area-chart-outlined',
+    'arrow-down-outlined',
+    'arrow-left-outlined',
+    'arrow-right-outlined',
+    'arrow-up-outlined',
+    'arrows-alt-outlined',
+    'audio-filled',
+    'audio-muted-outlined',
+    'audio-outlined',
+    'audio-twotone',
+    'audit-outlined',
+    'backward-filled',
+    'backward-outlined',
+    'bank-filled',
+    'bank-outlined',
+    'bank-twotone',
+    'bar-chart-outlined',
+    'barcode-outlined',
+    'bars-outlined',
+    'behance-circle-filled',
+    'behance-outlined',
+    'behance-square-filled',
+    'behance-square-outlined',
+    'bell-filled',
+    'bell-outlined',
+    'bell-twotone',
+    'bg-colors-outlined',
+    'block-outlined',
+    'bold-outlined',
+    'book-filled',
+    'book-outlined',
+    'book-twotone',
+    'border-bottom-outlined',
+    'border-horizontal-outlined',
+    'border-inner-outlined',
+    'border-left-outlined',
+    'border-outer-outlined',
+    'border-outlined',
+    'border-right-outlined',
+    'border-top-outlined',
+    'border-verticle-outlined',
+    'borderless-table-outlined',
+    'box-plot-filled',
+    'box-plot-outlined',
+    'box-plot-twotone',
+    'branches-outlined',
+    'bug-filled',
+    'bug-outlined',
+    'bug-twotone',
+    'build-filled',
+    'build-outlined',
+    'build-twotone',
+    'bulb-filled',
+    'bulb-outlined',
+    'bulb-twotone',
+    'calculator-filled',
+    'calculator-outlined',
+    'calculator-twotone',
+    'calendar-filled',
+    'calendar-outlined',
+    'calendar-twotone',
+    'camera-filled',
+    'camera-outlined',
+    'camera-twotone',
+    'car-filled',
+    'car-outlined',
+    'car-twotone',
+    'caret-down-filled',
+    'caret-down-outlined',
+    'caret-left-filled',
+    'caret-left-outlined',
+    'caret-right-filled',
+    'caret-right-outlined',
+    'caret-up-filled',
+    'caret-up-outlined',
+    'carry-out-filled',
+    'carry-out-outlined',
+    'carry-out-twotone',
+    'check-circle-filled',
+    'check-circle-outlined',
+    'check-circle-twotone',
+    'check-outlined',
+    'check-square-filled',
+    'check-square-outlined',
+    'check-square-twotone',
+    'chrome-filled',
+    'chrome-outlined',
+    'ci-circle-filled',
+    'ci-circle-outlined',
+    'ci-circle-twotone',
+    'ci-outlined',
+    'ci-twotone',
+    'clear-outlined',
+    'clock-circle-filled',
+    'clock-circle-outlined',
+    'clock-circle-twotone',
+    'close-circle-filled',
+    'close-circle-outlined',
+    'close-circle-twotone',
+    'close-outlined',
+    'close-square-filled',
+    'close-square-outlined',
+    'close-square-twotone',
+    'cloud-download-outlined',
+    'cloud-filled',
+    'cloud-outlined',
+    'cloud-server-outlined',
+    'cloud-sync-outlined',
+    'cloud-twotone',
+    'cloud-upload-outlined',
+    'cluster-outlined',
+    'code-filled',
+    'code-outlined',
+    'code-sandbox-circle-filled',
+    'code-sandbox-outlined',
+    'code-sandbox-square-filled',
+    'code-twotone',
+    'codepen-circle-filled',
+    'codepen-circle-outlined',
+    'codepen-outlined',
+    'codepen-square-filled',
+    'coffee-outlined',
+    'column-height-outlined',
+    'column-width-outlined',
+    'comment-outlined',
+    'compass-filled',
+    'compass-outlined',
+    'compass-twotone',
+    'compress-outlined',
+    'console-sql-outlined',
+    'contacts-filled',
+    'contacts-outlined',
+    'contacts-twotone',
+    'container-filled',
+    'container-outlined',
+    'container-twotone',
+    'control-filled',
+    'control-outlined',
+    'control-twotone',
+    'copy-filled',
+    'copy-outlined',
+    'copy-twotone',
+    'copyright-circle-filled',
+    'copyright-circle-outlined',
+    'copyright-circle-twotone',
+    'copyright-outlined',
+    'copyright-twotone',
+    'credit-card-filled',
+    'credit-card-outlined',
+    'credit-card-twotone',
+    'crown-filled',
+    'crown-outlined',
+    'crown-twotone',
+    'customer-service-filled',
+    'customer-service-outlined',
+    'customer-service-twotone',
+    'dash-outlined',
+    'dashboard-filled',
+    'dashboard-outlined',
+    'dashboard-twotone',
+    'database-filled',
+    'database-outlined',
+    'database-twotone',
+    'delete-column-outlined',
+    'delete-filled',
+    'delete-outlined',
+    'delete-row-outlined',
+    'delete-twotone',
+    'delivered-procedure-outlined',
+    'deployment-unit-outlined',
+    'desktop-outlined',
+    'diff-filled',
+    'diff-outlined',
+    'diff-twotone',
+    'dingding-outlined',
+    'dingtalk-circle-filled',
+    'dingtalk-outlined',
+    'dingtalk-square-filled',
+    'disconnect-outlined',
+    'dislike-filled',
+    'dislike-outlined',
+    'dislike-twotone',
+    'dollar-circle-filled',
+    'dollar-circle-outlined',
+    'dollar-circle-twotone',
+    'dollar-outlined',
+    'dollar-twotone',
+    'dot-chart-outlined',
+    'double-left-outlined',
+    'double-right-outlined',
+    'down-circle-filled',
+    'down-circle-outlined',
+    'down-circle-twotone',
+    'down-outlined',
+    'down-square-filled',
+    'down-square-outlined',
+    'down-square-twotone',
+    'download-outlined',
+    'drag-outlined',
+    'dribbble-circle-filled',
+    'dribbble-outlined',
+    'dribbble-square-filled',
+    'dribbble-square-outlined',
+    'dropbox-circle-filled',
+    'dropbox-outlined',
+    'dropbox-square-filled',
+    'edit-filled',
+    'edit-outlined',
+    'edit-twotone',
+    'ellipsis-outlined',
+    'enter-outlined',
+    'environment-filled',
+    'environment-outlined',
+    'environment-twotone',
+    'euro-circle-filled',
+    'euro-circle-outlined',
+    'euro-circle-twotone',
+    'euro-outlined',
+    'euro-twotone',
+    'exception-outlined',
+    'exclamation-circle-filled',
+    'exclamation-circle-outlined',
+    'exclamation-circle-twotone',
+    'exclamation-outlined',
+    'expand-alt-outlined',
+    'expand-outlined',
+    'experiment-filled',
+    'experiment-outlined',
+    'experiment-twotone',
+    'export-outlined',
+    'eye-filled',
+    'eye-invisible-filled',
+    'eye-invisible-outlined',
+    'eye-invisible-twotone',
+    'eye-outlined',
+    'eye-twotone',
+    'facebook-filled',
+    'facebook-outlined',
+    'fall-outlined',
+    'fast-backward-filled',
+    'fast-backward-outlined',
+    'fast-forward-filled',
+    'fast-forward-outlined',
+    'field-binary-outlined',
+    'field-number-outlined',
+    'field-string-outlined',
+    'field-time-outlined',
+    'file-add-filled',
+    'file-add-outlined',
+    'file-add-twotone',
+    'file-done-outlined',
+    'file-excel-filled',
+    'file-excel-outlined',
+    'file-excel-twotone',
+    'file-exclamation-filled',
+    'file-exclamation-outlined',
+    'file-exclamation-twotone',
+    'file-filled',
+    'file-gif-outlined',
+    'file-image-filled',
+    'file-image-outlined',
+    'file-image-twotone',
+    'file-jpg-outlined',
+    'file-markdown-filled',
+    'file-markdown-outlined',
+    'file-markdown-twotone',
+    'file-outlined',
+    'file-pdf-filled',
+    'file-pdf-outlined',
+    'file-pdf-twotone',
+    'file-ppt-filled',
+    'file-ppt-outlined',
+    'file-ppt-twotone',
+    'file-protect-outlined',
+    'file-search-outlined',
+    'file-sync-outlined',
+    'file-text-filled',
+    'file-text-outlined',
+    'file-text-twotone',
+    'file-twotone',
+    'file-unknown-filled',
+    'file-unknown-outlined',
+    'file-unknown-twotone',
+    'file-word-filled',
+    'file-word-outlined',
+    'file-word-twotone',
+    'file-zip-filled',
+    'file-zip-outlined',
+    'file-zip-twotone',
+    'filter-filled',
+    'filter-outlined',
+    'filter-twotone',
+    'fire-filled',
+    'fire-outlined',
+    'fire-twotone',
+    'flag-filled',
+    'flag-outlined',
+    'flag-twotone',
+    'folder-add-filled',
+    'folder-add-outlined',
+    'folder-add-twotone',
+    'folder-filled',
+    'folder-open-filled',
+    'folder-open-outlined',
+    'folder-open-twotone',
+    'folder-outlined',
+    'folder-twotone',
+    'folder-view-outlined',
+    'font-colors-outlined',
+    'font-size-outlined',
+    'fork-outlined',
+    'form-outlined',
+    'format-painter-filled',
+    'format-painter-outlined',
+    'forward-filled',
+    'forward-outlined',
+    'frown-filled',
+    'frown-outlined',
+    'frown-twotone',
+    'fullscreen-exit-outlined',
+    'fullscreen-outlined',
+    'function-outlined',
+    'fund-filled',
+    'fund-outlined',
+    'fund-projection-screen-outlined',
+    'fund-twotone',
+    'fund-view-outlined',
+    'funnel-plot-filled',
+    'funnel-plot-outlined',
+    'funnel-plot-twotone',
+    'gateway-outlined',
+    'gif-outlined',
+    'gift-filled',
+    'gift-outlined',
+    'gift-twotone',
+    'github-filled',
+    'github-outlined',
+    'gitlab-filled',
+    'gitlab-outlined',
+    'global-outlined',
+    'gold-filled',
+    'gold-outlined',
+    'gold-twotone',
+    'golden-filled',
+    'google-circle-filled',
+    'google-outlined',
+    'google-plus-circle-filled',
+    'google-plus-outlined',
+    'google-plus-square-filled',
+    'google-square-filled',
+    'group-outlined',
+    'hdd-filled',
+    'hdd-outlined',
+    'hdd-twotone',
+    'heart-filled',
+    'heart-outlined',
+    'heart-twotone',
+    'heat-map-outlined',
+    'highlight-filled',
+    'highlight-outlined',
+    'highlight-twotone',
+    'history-outlined',
+    'home-filled',
+    'home-outlined',
+    'home-twotone',
+    'hourglass-filled',
+    'hourglass-outlined',
+    'hourglass-twotone',
+    'html5-filled',
+    'html5-outlined',
+    'html5-twotone',
+    'idcard-filled',
+    'idcard-outlined',
+    'idcard-twotone',
+    'ie-circle-filled',
+    'ie-outlined',
+    'ie-square-filled',
+    'import-outlined',
+    'inbox-outlined',
+    'info-circle-filled',
+    'info-circle-outlined',
+    'info-circle-twotone',
+    'info-outlined',
+    'insert-row-above-outlined',
+    'insert-row-below-outlined',
+    'insert-row-left-outlined',
+    'insert-row-right-outlined',
+    'instagram-filled',
+    'instagram-outlined',
+    'insurance-filled',
+    'insurance-outlined',
+    'insurance-twotone',
+    'interaction-filled',
+    'interaction-outlined',
+    'interaction-twotone',
+    'issues-close-outlined',
+    'italic-outlined',
+    'key-outlined',
+    'laptop-outlined',
+    'layout-filled',
+    'layout-outlined',
+    'layout-twotone',
+    'left-circle-filled',
+    'left-circle-outlined',
+    'left-circle-twotone',
+    'left-outlined',
+    'left-square-filled',
+    'left-square-outlined',
+    'left-square-twotone',
+    'like-filled',
+    'like-outlined',
+    'like-twotone',
+    'line-chart-outlined',
+    'line-height-outlined',
+    'line-outlined',
+    'link-outlined',
+    'linkedin-filled',
+    'linkedin-outlined',
+    'loading-3-quarters-outlined',
+    'loading-outlined',
+    'lock-filled',
+    'lock-outlined',
+    'lock-twotone',
+    'login-outlined',
+    'logout-outlined',
+    'mac-command-filled',
+    'mac-command-outlined',
+    'mail-filled',
+    'mail-outlined',
+    'mail-twotone',
+    'man-outlined',
+    'medicine-box-filled',
+    'medicine-box-outlined',
+    'medicine-box-twotone',
+    'medium-circle-filled',
+    'medium-outlined',
+    'medium-square-filled',
+    'medium-workmark-outlined',
+    'meh-filled',
+    'meh-outlined',
+    'meh-twotone',
+    'menu-fold-outlined',
+    'menu-outlined',
+    'menu-unfold-outlined',
+    'merge-cells-outlined',
+    'message-filled',
+    'message-outlined',
+    'message-twotone',
+    'minus-circle-filled',
+    'minus-circle-outlined',
+    'minus-circle-twotone',
+    'minus-outlined',
+    'minus-square-filled',
+    'minus-square-outlined',
+    'minus-square-twotone',
+    'mobile-filled',
+    'mobile-outlined',
+    'mobile-twotone',
+    'money-collect-filled',
+    'money-collect-outlined',
+    'money-collect-twotone',
+    'monitor-outlined',
+    'more-outlined',
+    'node-collapse-outlined',
+    'node-expand-outlined',
+    'node-index-outlined',
+    'notification-filled',
+    'notification-outlined',
+    'notification-twotone',
+    'number-outlined',
+    'one-to-one-outlined',
+    'ordered-list-outlined',
+    'paper-clip-outlined',
+    'partition-outlined',
+    'pause-circle-filled',
+    'pause-circle-outlined',
+    'pause-circle-twotone',
+    'pause-outlined',
+    'pay-circle-filled',
+    'pay-circle-outlined',
+    'percentage-outlined',
+    'phone-filled',
+    'phone-outlined',
+    'phone-twotone',
+    'pic-center-outlined',
+    'pic-left-outlined',
+    'pic-right-outlined',
+    'picture-filled',
+    'picture-outlined',
+    'picture-twotone',
+    'pie-chart-filled',
+    'pie-chart-outlined',
+    'pie-chart-twotone',
+    'play-circle-filled',
+    'play-circle-outlined',
+    'play-circle-twotone',
+    'play-square-filled',
+    'play-square-outlined',
+    'play-square-twotone',
+    'plus-circle-filled',
+    'plus-circle-outlined',
+    'plus-circle-twotone',
+    'plus-outlined',
+    'plus-square-filled',
+    'plus-square-outlined',
+    'plus-square-twotone',
+    'pound-circle-filled',
+    'pound-circle-outlined',
+    'pound-circle-twotone',
+    'pound-outlined',
+    'poweroff-outlined',
+    'printer-filled',
+    'printer-outlined',
+    'printer-twotone',
+    'profile-filled',
+    'profile-outlined',
+    'profile-twotone',
+    'project-filled',
+    'project-outlined',
+    'project-twotone',
+    'property-safety-filled',
+    'property-safety-outlined',
+    'property-safety-twotone',
+    'pull-request-outlined',
+    'pushpin-filled',
+    'pushpin-outlined',
+    'pushpin-twotone',
+    'qq-circle-filled',
+    'qq-outlined',
+    'qq-square-filled',
+    'qrcode-outlined',
+    'question-circle-filled',
+    'question-circle-outlined',
+    'question-circle-twotone',
+    'question-outlined',
+    'radar-chart-outlined',
+    'radius-bottomleft-outlined',
+    'radius-bottomright-outlined',
+    'radius-setting-outlined',
+    'radius-upleft-outlined',
+    'radius-upright-outlined',
+    'read-filled',
+    'read-outlined',
+    'reconciliation-filled',
+    'reconciliation-outlined',
+    'reconciliation-twotone',
+    'red-envelope-filled',
+    'red-envelope-outlined',
+    'red-envelope-twotone',
+    'reddit-circle-filled',
+    'reddit-outlined',
+    'reddit-square-filled',
+    'redo-outlined',
+    'reload-outlined',
+    'rest-filled',
+    'rest-outlined',
+    'rest-twotone',
+    'retweet-outlined',
+    'right-circle-filled',
+    'right-circle-outlined',
+    'right-circle-twotone',
+    'right-outlined',
+    'right-square-filled',
+    'right-square-outlined',
+    'right-square-twotone',
+    'rise-outlined',
+    'robot-filled',
+    'robot-outlined',
+    'rocket-filled',
+    'rocket-outlined',
+    'rocket-twotone',
+    'rollback-outlined',
+    'rotate-left-outlined',
+    'rotate-right-outlined',
+    'safety-certificate-filled',
+    'safety-certificate-outlined',
+    'safety-certificate-twotone',
+    'safety-outlined',
+    'save-filled',
+    'save-outlined',
+    'save-twotone',
+    'scan-outlined',
+    'schedule-filled',
+    'schedule-outlined',
+    'schedule-twotone',
+    'scissor-outlined',
+    'search-outlined',
+    'security-scan-filled',
+    'security-scan-outlined',
+    'security-scan-twotone',
+    'select-outlined',
+    'send-outlined',
+    'setting-filled',
+    'setting-outlined',
+    'setting-twotone',
+    'shake-outlined',
+    'share-alt-outlined',
+    'shop-filled',
+    'shop-outlined',
+    'shop-twotone',
+    'shopping-cart-outlined',
+    'shopping-filled',
+    'shopping-outlined',
+    'shopping-twotone',
+    'shrink-outlined',
+    'signal-filled',
+    'sisternode-outlined',
+    'sketch-circle-filled',
+    'sketch-outlined',
+    'sketch-square-filled',
+    'skin-filled',
+    'skin-outlined',
+    'skin-twotone',
+    'skype-filled',
+    'skype-outlined',
+    'slack-circle-filled',
+    'slack-outlined',
+    'slack-square-filled',
+    'slack-square-outlined',
+    'sliders-filled',
+    'sliders-outlined',
+    'sliders-twotone',
+    'small-dash-outlined',
+    'smile-filled',
+    'smile-outlined',
+    'smile-twotone',
+    'snippets-filled',
+    'snippets-outlined',
+    'snippets-twotone',
+    'solution-outlined',
+    'sort-ascending-outlined',
+    'sort-descending-outlined',
+    'sound-filled',
+    'sound-outlined',
+    'sound-twotone',
+    'split-cells-outlined',
+    'star-filled',
+    'star-outlined',
+    'star-twotone',
+    'step-backward-filled',
+    'step-backward-outlined',
+    'step-forward-filled',
+    'step-forward-outlined',
+    'stock-outlined',
+    'stop-filled',
+    'stop-outlined',
+    'stop-twotone',
+    'strikethrough-outlined',
+    'subnode-outlined',
+    'swap-left-outlined',
+    'swap-outlined',
+    'swap-right-outlined',
+    'switcher-filled',
+    'switcher-outlined',
+    'switcher-twotone',
+    'sync-outlined',
+    'table-outlined',
+    'tablet-filled',
+    'tablet-outlined',
+    'tablet-twotone',
+    'tag-filled',
+    'tag-outlined',
+    'tag-twotone',
+    'tags-filled',
+    'tags-outlined',
+    'tags-twotone',
+    'taobao-circle-filled',
+    'taobao-circle-outlined',
+    'taobao-outlined',
+    'taobao-square-filled',
+    'team-outlined',
+    'thunderbolt-filled',
+    'thunderbolt-outlined',
+    'thunderbolt-twotone',
+    'to-top-outlined',
+    'tool-filled',
+    'tool-outlined',
+    'tool-twotone',
+    'trademark-circle-filled',
+    'trademark-circle-outlined',
+    'trademark-circle-twotone',
+    'trademark-outlined',
+    'transaction-outlined',
+    'translation-outlined',
+    'trophy-filled',
+    'trophy-outlined',
+    'trophy-twotone',
+    'twitter-circle-filled',
+    'twitter-outlined',
+    'twitter-square-filled',
+    'underline-outlined',
+    'undo-outlined',
+    'ungroup-outlined',
+    'unlock-filled',
+    'unlock-outlined',
+    'unlock-twotone',
+    'unordered-list-outlined',
+    'up-circle-filled',
+    'up-circle-outlined',
+    'up-circle-twotone',
+    'up-outlined',
+    'up-square-filled',
+    'up-square-outlined',
+    'up-square-twotone',
+    'upload-outlined',
+    'usb-filled',
+    'usb-outlined',
+    'usb-twotone',
+    'user-add-outlined',
+    'user-delete-outlined',
+    'user-outlined',
+    'user-switch-outlined',
+    'usergroup-add-outlined',
+    'usergroup-delete-outlined',
+    'verified-outlined',
+    'vertical-align-bottom-outlined',
+    'vertical-align-middle-outlined',
+    'vertical-align-top-outlined',
+    'vertical-left-outlined',
+    'vertical-right-outlined',
+    'video-camera-add-outlined',
+    'video-camera-filled',
+    'video-camera-outlined',
+    'video-camera-twotone',
+    'wallet-filled',
+    'wallet-outlined',
+    'wallet-twotone',
+    'warning-filled',
+    'warning-outlined',
+    'warning-twotone',
+    'wechat-filled',
+    'wechat-outlined',
+    'weibo-circle-filled',
+    'weibo-circle-outlined',
+    'weibo-outlined',
+    'weibo-square-filled',
+    'weibo-square-outlined',
+    'whats-app-outlined',
+    'wifi-outlined',
+    'windows-filled',
+    'windows-outlined',
+    'woman-outlined',
+    'yahoo-filled',
+    'yahoo-outlined',
+    'youtube-filled',
+    'youtube-outlined',
+    'yuque-filled',
+    'yuque-outlined',
+    'zhihu-circle-filled',
+    'zhihu-outlined',
+    'zhihu-square-filled',
+    'zoom-in-outlined',
+    'zoom-out-outlined',
+  ],
+};

+ 6 - 1
src/components/Icon/index.ts

@@ -1,4 +1,9 @@
 import Icon from './src/index.vue';
+// import IconPicker from './src/IconPicker.vue';
+import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
+
+const IconPicker = createAsyncComponent(() => import('./src/IconPicker.vue'));
+
+export { Icon, IconPicker };
 
-export { Icon };
 export default Icon;

+ 186 - 0
src/components/Icon/src/IconPicker.vue

@@ -0,0 +1,186 @@
+<template>
+  <a-input
+    disabled
+    :style="{ width }"
+    :placeholder="t('component.icon.placeholder')"
+    :class="prefixCls"
+    v-model:value="currentSelect"
+  >
+    <template #addonAfter>
+      <Popover
+        placement="bottomLeft"
+        trigger="click"
+        v-model="visible"
+        :overlayClassName="`${prefixCls}-popover`"
+      >
+        <template #title>
+          <div class="flex justify-between">
+            <a-input
+              :placeholder="t('component.icon.search')"
+              @change="handleSearchChange"
+              allowClear
+            />
+          </div>
+        </template>
+
+        <template #content>
+          <div v-if="getPaginationList.length">
+            <ScrollContainer class="border border-solid border-t-0">
+              <ul class="flex flex-wrap px-2">
+                <li
+                  v-for="icon in getPaginationList"
+                  :key="icon"
+                  :class="currentSelect === icon ? 'bg-primary text-white' : ''"
+                  class="p-2 w-1/8 cursor-pointer mr-1 mt-1 flex justify-center items-center border border-solid hover:bg-primary hover:text-white"
+                  @click="handleClick(icon)"
+                >
+                  <!-- <Icon :icon="icon" :prefix="prefix" /> -->
+                  <Icon :icon="icon" />
+                </li>
+              </ul>
+            </ScrollContainer>
+            <div class="flex py-2 items-center justify-center">
+              <Pagination
+                showLessItems
+                size="small"
+                :pageSize="pageSize"
+                :total="getTotal"
+                @change="handlePageChange"
+              />
+            </div>
+          </div>
+          <template v-else
+            ><div class="p-5"> <Empty /></div>
+          </template>
+        </template>
+        <Icon :icon="currentSelect || 'ion:apps-outline'" class="cursor-pointer px-2 py-1" />
+      </Popover>
+    </template>
+  </a-input>
+</template>
+<script lang="ts">
+  import { defineComponent, ref, watchEffect, watch, unref } from 'vue';
+
+  import { useDesign } from '/@/hooks/web/useDesign';
+  import { ScrollContainer } from '/@/components/Container';
+
+  import { Input, Popover, Pagination, Empty } from 'ant-design-vue';
+  import Icon from './index.vue';
+
+  import iconsData from '../data/icons.data';
+  import { propTypes } from '/@/utils/propTypes';
+  import { usePagination } from '/@/hooks/web/usePagination';
+  import { useDebounce } from '/@/hooks/core/useDebounce';
+  import { useI18n } from '/@/hooks/web/useI18n';
+  import { useCopyToClipboard } from '/@/hooks/web/useCopyToClipboard';
+  import { useMessage } from '/@/hooks/web/useMessage';
+  // import '@iconify/iconify';
+
+  function getIcons() {
+    const data = iconsData as any;
+    const prefix: string = data?.prefix ?? '';
+    let result: string[] = [];
+    if (prefix) {
+      result = (data?.icons ?? []).map((item) => `${prefix}:${item}`);
+    } else if (Array.isArray(iconsData)) {
+      result = iconsData as string[];
+    }
+    return result;
+  }
+
+  const icons = getIcons();
+  export default defineComponent({
+    name: 'IconPicker',
+    inheritAttrs: false,
+    components: { [Input.name]: Input, Icon, Popover, ScrollContainer, Pagination, Empty },
+    props: {
+      value: propTypes.string,
+      width: propTypes.string.def('100%'),
+      pageSize: propTypes.number.def(140),
+      copy: propTypes.bool.def(false),
+    },
+    emits: ['change'],
+    setup(props, { emit }) {
+      const currentSelect = ref('');
+      const visible = ref(false);
+      const currentList = ref(icons);
+
+      const { prefixCls } = useDesign('icon-picker');
+      const { t } = useI18n();
+      const [debounceHandleSearchChange] = useDebounce(handleSearchChange, 100);
+      const { clipboardRef, isSuccessRef } = useCopyToClipboard(props.value);
+      const { createMessage } = useMessage();
+
+      const { getPaginationList, getTotal, setCurrentPage } = usePagination(
+        currentList,
+        props.pageSize
+      );
+
+      watchEffect(() => {
+        currentSelect.value = props.value;
+      });
+
+      watch(
+        () => currentSelect.value,
+        (v) => emit('change', v)
+      );
+
+      function handlePageChange(page: number) {
+        setCurrentPage(page);
+      }
+
+      function handleClick(icon: string) {
+        currentSelect.value = icon;
+        if (props.copy) {
+          clipboardRef.value = icon;
+          if (unref(isSuccessRef)) {
+            createMessage.success(t('component.icon.copy'));
+          }
+        }
+      }
+
+      function handleSearchChange(e: ChangeEvent) {
+        const value = e.target.value;
+        if (!value) {
+          setCurrentPage(1);
+          currentList.value = icons;
+          return;
+        }
+        currentList.value = icons.filter((item) => item.includes(value));
+      }
+
+      return {
+        t,
+        prefixCls,
+        visible,
+        getTotal,
+        getPaginationList,
+        handlePageChange,
+        handleClick,
+        currentSelect,
+        handleSearchChange: debounceHandleSearchChange,
+      };
+    },
+  });
+</script>
+<style lang="less">
+  @prefix-cls: ~'@{namespace}-icon-picker';
+
+  .@{prefix-cls} {
+    .ant-input-group-addon {
+      padding: 0;
+    }
+
+    &-popover {
+      width: 300px;
+
+      .ant-popover-inner-content {
+        padding: 0;
+      }
+
+      .scrollbar {
+        height: 220px;
+      }
+    }
+  }
+</style>

+ 1 - 1
src/components/Icon/src/index.vue

@@ -16,6 +16,7 @@
   import Iconify from '@purge-icons/generated';
   import { isString } from '/@/utils/is';
   import { propTypes } from '/@/utils/propTypes';
+
   export default defineComponent({
     name: 'GIcon',
     props: {
@@ -45,7 +46,6 @@
           const icon = unref(getIconRef);
           if (!icon) return;
           const svg = Iconify.renderSVG(icon, {});
-
           if (svg) {
             el.textContent = '';
             el.appendChild(svg);

+ 2 - 0
src/components/Table/src/BasicTable.vue

@@ -66,6 +66,7 @@
   import { useDesign } from '/@/hooks/web/useDesign';
 
   import { basicProps } from './props';
+  import expandIcon from './components/ExpandIcon';
   import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
 
   import './style/index.less';
@@ -193,6 +194,7 @@
           size: 'middle',
           ...attrs,
           customRow,
+          expandIcon: expandIcon(),
           ...unref(getProps),
           ...unref(getHeaderProps),
           scroll: unref(getScrollRef),

+ 19 - 0
src/components/Table/src/components/ExpandIcon.tsx

@@ -0,0 +1,19 @@
+import { BasicArrow } from '/@/components/Basic';
+
+export default () => {
+  return (props: Recordable) => {
+    if (!props.expandable) {
+      return <span />;
+    }
+    return (
+      <BasicArrow
+        class="mr-1"
+        iconStyle="margin-top: -2px;"
+        onClick={(e: Event) => {
+          props.onExpand(props.record, e);
+        }}
+        expand={props.expanded}
+      />
+    );
+  };
+};

+ 34 - 0
src/hooks/web/usePagination.ts

@@ -0,0 +1,34 @@
+import type { Ref } from 'vue';
+import { ref, unref, computed } from 'vue';
+
+function pagination<T = any>(list: T[], pageNo: number, pageSize: number): T[] {
+  const offset = (pageNo - 1) * Number(pageSize);
+  const ret =
+    offset + Number(pageSize) >= list.length
+      ? list.slice(offset, list.length)
+      : list.slice(offset, offset + Number(pageSize));
+  return ret;
+}
+
+export function usePagination<T = any>(list: Ref<T[]>, pageSize: number) {
+  const currentPage = ref(1);
+  const pageSizeRef = ref(pageSize);
+
+  const getPaginationList = computed(() => {
+    return pagination(unref(list), unref(currentPage), unref(pageSizeRef));
+  });
+
+  const getTotal = computed(() => {
+    return unref(list).length;
+  });
+
+  function setCurrentPage(page: number) {
+    currentPage.value = page;
+  }
+
+  function setPageSize(pageSize: number) {
+    pageSizeRef.value = pageSize;
+  }
+
+  return { setCurrentPage, getTotal, setPageSize, getPaginationList };
+}

+ 5 - 0
src/locales/lang/en/component/icon.ts

@@ -0,0 +1,5 @@
+export default {
+  placeholder: 'Click the select icon',
+  search: 'Search icon',
+  copy: 'Copy icon successfully!',
+};

+ 2 - 1
src/locales/lang/en/routes/permission.ts

@@ -1,4 +1,5 @@
 export default {
   management: 'permission management',
-  detail: 'permission detail',
+  role: 'role',
+  menu: 'menu',
 };

+ 5 - 0
src/locales/lang/zh_CN/component/icon.ts

@@ -0,0 +1,5 @@
+export default {
+  placeholder: '点击选择图标',
+  search: '搜索图标',
+  copy: '复制图标成功!',
+};

+ 2 - 1
src/locales/lang/zh_CN/routes/permission.ts

@@ -1,4 +1,5 @@
 export default {
   management: '权限管理',
-  detail: '详情',
+  role: '角色管理',
+  menu: '菜单管理',
 };

+ 6 - 2
src/router/menus/modules/permission.ts

@@ -8,8 +8,12 @@ const menu: MenuModule = {
     name: t('routes.permission.management'),
     children: [
       {
-        path: 'detail',
-        name: t('routes.permission.detail'),
+        path: 'role',
+        name: t('routes.permission.role'),
+      },
+      {
+        path: 'menu',
+        name: t('routes.permission.menu'),
       },
     ],
   },

+ 14 - 4
src/router/routes/modules/permission.ts

@@ -14,11 +14,21 @@ const dashboard: AppRouteModule = {
   },
   children: [
     {
-      path: 'detail',
-      name: 'Detail',
-      component: () => import('/@/views/permission/index.vue'),
+      path: 'role',
+      name: 'Role',
+      component: () => import('/@/views/permission/role/index.vue'),
       meta: {
-        title: t('routes.permission.detail'),
+        title: t('routes.permission.role'),
+        affix: false,
+        icon: 'bx:bx-lock',
+      },
+    },
+    {
+      path: 'menu',
+      name: 'Menu',
+      component: () => import('/@/views/permission/menu/index.vue'),
+      meta: {
+        title: t('routes.permission.menu'),
         affix: false,
         icon: 'bx:bx-lock',
       },

+ 53 - 7
src/utils/http/axios/Axios.ts

@@ -8,6 +8,8 @@ import { cloneDeep } from 'lodash-es';
 import type { RequestOptions, CreateAxiosOptions, Result, UploadFileParams } from './types';
 import { errorResult } from './const';
 import { ContentTypeEnum } from '/@/enums/httpEnum';
+import qs from 'qs';
+import { RequestEnum } from '../../../enums/httpEnum';
 
 export * from './axiosTransform';
 
@@ -81,9 +83,15 @@ export class VAxios {
     this.axiosInstance.interceptors.request.use((config: AxiosRequestConfig) => {
       // If cancel repeat request is turned on, then cancel repeat request is prohibited
       const {
-        headers: { ignoreCancelToken = false },
+        headers: { ignoreCancelToken },
       } = config;
-      !ignoreCancelToken && axiosCanceler.addPending(config);
+
+      const ignoreCancel =
+        ignoreCancelToken !== undefined
+          ? ignoreCancelToken
+          : this.options.requestOptions?.ignoreCancelToken;
+
+      !ignoreCancel && axiosCanceler.addPending(config);
       if (requestInterceptors && isFunction(requestInterceptors)) {
         config = requestInterceptors(config);
       }
@@ -144,6 +152,41 @@ export class VAxios {
     });
   }
 
+  // support form-data
+  supportFormData(config: AxiosRequestConfig) {
+    const headers = this.options?.headers;
+    const contentType = headers?.['Content-Type'] || headers?.['content-type'];
+
+    if (
+      contentType !== ContentTypeEnum.FORM_URLENCODED ||
+      !Reflect.has(config, 'data') ||
+      config.method?.toUpperCase() === RequestEnum.GET
+    ) {
+      return config;
+    }
+
+    return {
+      ...config,
+      data: qs.stringify(config.data),
+    };
+  }
+
+  get<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
+    return this.request({ ...config, method: 'GET' }, options);
+  }
+
+  post<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
+    return this.request({ ...config, method: 'POST' }, options);
+  }
+
+  put<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
+    return this.request({ ...config, method: 'PUT' }, options);
+  }
+
+  delete<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
+    return this.request({ ...config, method: 'DELETE' }, options);
+  }
+
   request<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
     let conf: AxiosRequestConfig = cloneDeep(config);
     const transform = this.getTransform();
@@ -152,24 +195,27 @@ export class VAxios {
 
     const opt: RequestOptions = Object.assign({}, requestOptions, options);
 
-    const { beforeRequestHook, requestCatch, transformRequestData } = transform || {};
+    const { beforeRequestHook, requestCatchHook, transformRequestHook } = transform || {};
     if (beforeRequestHook && isFunction(beforeRequestHook)) {
       conf = beforeRequestHook(conf, opt);
     }
+
+    conf = this.supportFormData(conf);
+
     return new Promise((resolve, reject) => {
       this.axiosInstance
         .request<any, AxiosResponse<Result>>(conf)
         .then((res: AxiosResponse<Result>) => {
-          if (transformRequestData && isFunction(transformRequestData)) {
-            const ret = transformRequestData(res, opt);
+          if (transformRequestHook && isFunction(transformRequestHook)) {
+            const ret = transformRequestHook(res, opt);
             ret !== errorResult ? resolve(ret) : reject(new Error('request error!'));
             return;
           }
           resolve((res as unknown) as Promise<T>);
         })
         .catch((e: Error) => {
-          if (requestCatch && isFunction(requestCatch)) {
-            reject(requestCatch(e));
+          if (requestCatchHook && isFunction(requestCatchHook)) {
+            reject(requestCatchHook(e));
             return;
           }
           reject(e);

+ 5 - 5
src/utils/http/axios/axiosTransform.ts

@@ -1,25 +1,25 @@
 /**
- * 数据处理类,可以根据项目自行配置
+ * Data processing class, can be configured according to the project
  */
 import type { AxiosRequestConfig, AxiosResponse } from 'axios';
 import type { RequestOptions, Result } from './types';
 
 export abstract class AxiosTransform {
   /**
-   * @description: 请求之前处理配置
+   * @description: Process configuration before request
    * @description: Process configuration before request
    */
   beforeRequestHook?: (config: AxiosRequestConfig, options: RequestOptions) => AxiosRequestConfig;
 
   /**
-   * @description: 请求成功处理
+   * @description: Request successfully processed
    */
-  transformRequestData?: (res: AxiosResponse<Result>, options: RequestOptions) => any;
+  transformRequestHook?: (res: AxiosResponse<Result>, options: RequestOptions) => any;
 
   /**
    * @description: 请求失败处理
    */
-  requestCatch?: (e: Error) => Promise<any>;
+  requestCatchHook?: (e: Error) => Promise<any>;
 
   /**
    * @description: 请求之前的拦截器

+ 5 - 3
src/utils/http/axios/index.ts

@@ -4,7 +4,6 @@
 import type { AxiosResponse } from 'axios';
 import type { CreateAxiosOptions, RequestOptions, Result } from './types';
 import { VAxios } from './Axios';
-import { getToken } from '/@/utils/auth';
 import { AxiosTransform } from './axiosTransform';
 
 import { checkStatus } from './checkStatus';
@@ -20,6 +19,7 @@ import { errorStore } from '/@/store/modules/error';
 import { errorResult } from './const';
 import { useI18n } from '/@/hooks/web/useI18n';
 import { createNow, formatRequestDate } from './helper';
+import { userStore } from '/@/store/modules/user';
 
 const globSetting = useGlobSetting();
 const prefix = globSetting.urlPrefix;
@@ -32,7 +32,7 @@ const transform: AxiosTransform = {
   /**
    * @description: 处理请求数据
    */
-  transformRequestData: (res: AxiosResponse<Result>, options: RequestOptions) => {
+  transformRequestHook: (res: AxiosResponse<Result>, options: RequestOptions) => {
     const { t } = useI18n();
     const { isTransformRequestResult } = options;
     // 不进行任何处理,直接返回
@@ -137,7 +137,7 @@ const transform: AxiosTransform = {
    */
   requestInterceptors: (config) => {
     // 请求之前处理config
-    const token = getToken();
+    const token = userStore.getTokenState;
     if (token) {
       // jwt token
       config.headers.Authorization = token;
@@ -202,6 +202,8 @@ function createAxios(opt?: Partial<CreateAxiosOptions>) {
           apiUrl: globSetting.apiUrl,
           //  是否加入时间戳
           joinTime: true,
+          // 忽略重复请求
+          ignoreCancelToken: true,
         },
       },
       opt || {}

+ 15 - 14
src/utils/http/axios/types.ts

@@ -1,23 +1,23 @@
 import type { AxiosRequestConfig } from 'axios';
-import { AxiosTransform } from './axiosTransform';
-
+import type { AxiosTransform } from './axiosTransform';
 export type ErrorMessageMode = 'none' | 'modal' | 'message' | undefined;
 
 export interface RequestOptions {
-  // 请求参数拼接到url
+  // Splicing request parameters to url
   joinParamsToUrl?: boolean;
-  // 格式化请求参数时间
+  // Format request parameter time
   formatDate?: boolean;
-  //  是否处理请求结果
+  //  Whether to process the request result
   isTransformRequestResult?: boolean;
-  // 是否加入url
+  // Whether to join url
   joinPrefix?: boolean;
-  // 接口地址, 不填则使用默认apiUrl
+  // Interface address, use the default apiUrl if you leave it blank
   apiUrl?: string;
-  // 错误消息提示类型
+  // Error message prompt type
   errorMessageMode?: ErrorMessageMode;
-  // 是否加入时间戳
+  // Whether to add a timestamp
   joinTime?: boolean;
+  ignoreCancelToken?: boolean;
 }
 
 export interface CreateAxiosOptions extends AxiosRequestConfig {
@@ -32,15 +32,16 @@ export interface Result<T = any> {
   message: string;
   result: T;
 }
-// multipart/form-data:上传文件
+
+// multipart/form-data: upload file
 export interface UploadFileParams {
-  // 其他参数
+  // Other parameters
   data?: Indexable;
-  // 文件参数的接口字段名
+  // File parameter interface field name
   name?: string;
-  // 文件
+  // file name
   file: File | Blob;
-  // 文件名
+  // file name
   filename?: string;
   [key: string]: any;
 }

+ 110 - 0
src/views/permission/menu/add.vue

@@ -0,0 +1,110 @@
+<template>
+  <BasicModal v-bind="$attrs" @register="register" @ok="confirm" title="菜单编辑" >
+    <BasicForm @register="registerForm" :model="model" />
+  </BasicModal>
+</template>
+<script lang="ts">
+
+  import { defineComponent, ref, unref } from 'vue';
+  import { BasicModal, useModalInner } from '/@/components/Modal';
+  import { BasicForm, useForm } from '/@/components/Form/index';
+  import { adapt } from '/@/utils/adapt'
+  import { formSchema, dataSource } from './data'
+
+
+  export default defineComponent({
+    components: { BasicModal, BasicForm,  },
+
+    setup() {
+
+
+      const modelRef = ref({});
+      const adaptWidth = adapt()
+      const isUpdate = ref(true);
+
+      const [registerForm, { getFieldsValue, updateSchema, resetFields, setFieldsValue, /* validate*/ }] = useForm({
+        labelWidth: 100,
+        schemas: formSchema,
+        showActionButtonGroup: false,
+        actionColOptions: { span: 24 },
+      });
+      // const [
+      //   registerForm,
+      //   {
+      //     getFieldsValue,
+      //     // setProps
+      //   },
+      // ] = useForm({
+      //   labelWidth: 120,
+      //   schemas,
+      //   showActionButtonGroup: false,
+      //   actionColOptions: {
+      //     span: 24,
+      //   },
+      // });
+      const [register, { closeModal } ] = useModalInner((data) => {
+        resetFields()
+
+        console.log('-------------------data---------------')
+        console.log(data)
+        isUpdate.value = !!data?.isUpdate;
+        // 方式2
+        if (unref(isUpdate)) {
+          setFieldsValue({
+            ...data
+          });
+        }
+
+        // setProps({
+        //   model:{ field2: data.data, field1: data.info }
+        // })
+        updateSchema({
+          field: 'parentMenu',
+          componentProps: { dataSource },
+        });
+
+      });
+
+      function confirm() {
+        console.log('确定')
+        const info = getFieldsValue()
+        console.log(info)
+
+        closeModal()  // 关闭弹窗
+      }
+      return {
+        register,
+        registerForm,
+        model: modelRef,
+        confirm,
+        adaptWidth,
+      };
+    },
+  });
+</script>
+<style lang='less'>
+.ant-form-item-label {
+  text-align: center !important;
+}
+
+.tree-label {
+  width: 20.6%;
+  margin-top: 8px;
+  margin-bottom: 1em;
+  text-align: center;
+}
+
+@media (max-width: 639px) {
+  .ant-form-item-label {
+  line-height: 2.5715 !important;
+  text-align: center !important;
+ }
+
+  .tree-label {
+  width: 33%;
+  margin-top: 8px;
+  margin-bottom: 1em;
+  text-align: center;
+}
+}
+</style>

+ 184 - 0
src/views/permission/menu/data.ts

@@ -0,0 +1,184 @@
+import { FormSchema } from '/@/components/Table';
+
+const isDir = (type: string) => type === '0';
+const isMenu = (type: string) => type === '1';
+const isButton = (type: string) => type === '2';
+
+export const formSchema: FormSchema[] = [
+  {
+    field: 'type',
+    label: '菜单类型',
+    component: 'RadioButtonGroup',
+    defaultValue: '0',
+    componentProps: {
+      options: [
+        { label: '目录', value: '0' },
+        { label: '菜单', value: '1' },
+        { label: '按钮', value: '2' },
+      ],
+    },
+    colProps: { lg: 24, md: 24 },
+  },
+  {
+    field: 'menuName',
+    label: '菜单名称',
+    component: 'Input',
+    required: true,
+  },
+
+  {
+    field: 'parentMenu',
+    label: '上级菜单',
+    component: 'TreeSelect',
+    componentProps: {
+      replaceFields: {
+        title: 'menuName',
+        key: 'id',
+        value: 'id',
+      },
+      getPopupContainer: () => document.body,
+    },
+  },
+
+  // {
+  //   field: 'orderNo',
+  //   label: '排序',
+  //   component: 'InputNumber',
+  //   required: true,
+  // },
+  {
+    field: 'icon',
+    label: '图标',
+    component: 'IconPicker',
+    required: true,
+    show: ({ values }) => !isButton(values.type),
+  },
+
+  {
+    field: 'routePath',
+    label: '路由地址',
+    component: 'Input',
+    required: true,
+    show: ({ values }) => !isButton(values.type),
+  },
+  {
+    field: 'component',
+    label: '组件路径',
+    component: 'Input',
+    show: ({ values }) => isMenu(values.type),
+  },
+  {
+    field: 'permission',
+    label: '权限标识',
+    component: 'Input',
+    show: ({ values }) => !isDir(values.type),
+  },
+  {
+    field: 'status',
+    label: '状态',
+    component: 'RadioButtonGroup',
+    defaultValue: '0',
+    componentProps: {
+      options: [
+        { label: '启用', value: '0' },
+        { label: '禁用', value: '1' },
+      ],
+    },
+  },
+  {
+    field: 'isExt',
+    label: '是否外链',
+    component: 'RadioButtonGroup',
+    defaultValue: '0',
+    componentProps: {
+      options: [
+        { label: '否', value: '0' },
+        { label: '是', value: '1' },
+      ],
+    },
+    show: ({ values }) => !isButton(values.type),
+  },
+
+  {
+    field: 'keepalive',
+    label: '是否缓存',
+    component: 'RadioButtonGroup',
+    defaultValue: '0',
+    componentProps: {
+      options: [
+        { label: '否', value: '0' },
+        { label: '是', value: '1' },
+      ],
+    },
+    show: ({ values }) => isMenu(values.type),
+  },
+
+  {
+    field: 'show',
+    label: '是否显示',
+    component: 'RadioButtonGroup',
+    defaultValue: '0',
+    componentProps: {
+      options: [
+        { label: '是', value: '0' },
+        { label: '否', value: '1' },
+      ],
+    },
+    show: ({ values }) => !isButton(values.type),
+  },
+];
+
+export const dataSource = [
+  {
+    key: '0-1',
+    menuName: 'Dashboard',
+    status: '0',
+    icon: 'ion:layers-outline',
+    create_time: '2021-01-01',
+    detail: '',
+    children: [
+      {
+        key: '0-1-1',
+        menuName: '工作台',
+        status: '0',
+        icon: 'ion:git-compare-outline',
+        create_time: '2021-01-01',
+        detail: '',
+      },
+      {
+        key: '0-1-2',
+        menuName: '分析页',
+        status: '1',
+        icon: 'ion:tv-outline',
+        create_time: '2021-01-01',
+        detail: '',
+      },
+    ],
+  },
+  {
+    key: '1-1',
+    menuName: '权限管理',
+    status: '0',
+    icon: 'bx:bx-lock',
+    create_time: '2021-01-01',
+    detail: '',
+    children: [
+      {
+        key: '1-1-1',
+        menuName: '角色管理',
+        status: '0',
+        icon: 'bx:bx-lock',
+        create_time: '2021-01-01',
+        detail: '',
+      },
+      {
+        key: '1-1-2',
+        menuName: '菜单管理',
+        status: '0',
+        icon: 'bx:bx-lock',
+        create_time: '2021-01-01',
+        detail: '',
+      },
+    ],
+  },
+];

+ 205 - 0
src/views/permission/menu/index.vue

@@ -0,0 +1,205 @@
+<template>
+  <div class="p-4">
+    <BasicTable
+      ref="tableRef"
+      @register="registerTable"
+      rowKey="key"
+      isTreeTable
+    >
+      <template #action="{ record, column }">
+        <TableAction :actions="createActions(record, column)" />
+      </template>
+      <template #form-custom > custom-slot </template>
+
+      <template #toolbar>
+        <a-button type="primary" @click="addColumn" >
+          {{ '新增菜单' }}
+        </a-button>
+      </template>
+    </BasicTable>
+    <Add @register="addRegister"  @saveData="saveData" />
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent, ref, unref } from 'vue';
+  import Add from './add.vue'
+  import { useModal } from '/@/components/Modal';
+  import { h } from 'vue';
+  import { Tag } from 'ant-design-vue';
+  import { Icon } from '/@/components/Icon';
+  import { dataSource } from './data'
+  import {
+    BasicTable,
+    useTable,
+    TableAction,
+    BasicColumn,
+    ActionItem,
+    EditRecordRow,
+    TableActionType
+  } from '/@/components/Table';
+
+
+
+  const columns: BasicColumn[] = [
+    {
+      title: '菜单名称',
+      dataIndex: 'menuName',
+      width: 150,
+      align: 'left',
+    },
+    {
+      title: 'key',
+      dataIndex: 'key',
+      width: 70,
+    },
+    {
+      title: '图标',
+      dataIndex: 'icon',
+      width: 50,
+      customRender: ({ record }) => {
+        return h(Icon, { icon: record.icon });
+      },
+    },
+    {
+      title: '创建时间',
+      dataIndex: 'create_time',
+      width: 150,
+    },
+    {
+      title: '详情',
+      dataIndex: 'detail',
+      width: 200,
+    },
+    {
+      title: '状态',
+      dataIndex: 'status',
+      width: 80,
+      customRender: ({ record }) => {
+        const status = record.status;
+        const enable = ~~status === 0;
+        const color = enable ? 'green' : 'red';
+        const text = enable ? '启用' : '停用';
+        return h(Tag, { color: color }, () => text);
+      },
+    },
+  ];
+
+  export default defineComponent({
+    components: { BasicTable, TableAction, Add,  },
+    setup() {
+      const tableRef = ref<Nullable<TableActionType>>(null);
+      const currentEditKeyRef = ref('');
+
+      const [registerTable] = useTable({
+        title: "菜单列表",
+        titleHelpMessage: "温馨提醒",
+        columns: columns,
+        dataSource: dataSource,
+        bordered: true,
+        showIndexColumn: false,
+        actionColumn: {
+          width: 160,
+          title: '操作',
+          dataIndex: 'action',
+          slots: { customRender: 'action' },
+          fixed: undefined,
+        },
+      });
+      const [addRegister, { openModal: openAdd }] = useModal();
+      const rowClick = (e: any) => {
+        console.log(e)
+      }
+
+      function getTableAction() { // 获取组件
+        const tableAction = unref(tableRef);
+        if (!tableAction) {
+          throw new Error('tableAction is null');
+        }
+        return tableAction;
+      }
+
+       function getSelectRowList() { // 获取选中行
+        console.log(getTableAction().getSelectRows());
+      }
+      function getSelectRowKeyList() { // 获取选中行的key --- id
+        console.log(getTableAction().getSelectRowKeys());
+      }
+
+      function addColumn() {
+
+        openAdd(true, {
+          id: 0,
+          menuName: '',
+          detail: '',
+        });
+      }
+
+      function handleEdit(record: EditRecordRow) {
+        currentEditKeyRef.value = record.id;  // record.key
+        console.log(record)
+
+        // console.log(record.id)
+        // const data = getTableAction().getDataSource()
+        // data.map(item => {
+        //   if (item.id === record.id) {
+        //     record = item
+        //   }
+        // })
+        openAdd(true, {
+          // id: record.id,
+          // name: record.name,
+          // password: record.password,
+          // detail: record.detail,
+          ...record,
+          isUpdate: true,
+        });
+      }
+
+      function handleDelete(record: Recordable) {
+        console.log('点击了删除', record.id);
+        console.log(record)
+        const data = getTableAction().getDataSource()
+        console.log(data)
+        getTableAction().setTableData(data.filter(item => item.id !== record.id))
+      }
+
+      function saveData(data: any) {
+        console.log(data)
+      }
+
+      function createActions(record: EditRecordRow, column: BasicColumn): ActionItem[] {
+        column
+        return [
+          {
+            label: '编辑',
+            icon: 'ant-design:edit-outlined',
+            color: 'warning',
+            onClick: handleEdit.bind(null, record),
+          },
+          {
+            label: '删除',
+            color: 'error',
+            icon: 'ic:outline-delete-outline',
+            popConfirm: {
+              title: '是否确认删除',
+              confirm: handleDelete.bind(null, record),
+            },
+          },
+        ];
+      }
+      return {
+        tableRef,
+        registerTable,
+        rowClick,
+        addColumn,
+        handleEdit,
+        createActions,
+        getTableAction,
+        getSelectRowList,
+        getSelectRowKeyList,
+        addRegister,
+        saveData
+      };
+    },
+  });
+</script>

+ 16 - 2
src/views/permission/add.vue → src/views/permission/role/add.vue

@@ -85,7 +85,21 @@
           component: 'Input',
           label: '详情',
           labelWidth: adaptWidth.labelWidth,
-
+          colProps: {
+            span: adaptWidth.elContainer,
+          },
+        },
+        {
+          field: 'status',
+          label: '状态',
+          component: 'RadioButtonGroup',
+          componentProps: {
+            options: [
+              { label: '启用', value: '0' },
+              { label: '停用', value: '1' },
+            ],
+          },
+          labelWidth: adaptWidth.labelWidth,
           colProps: {
             span: adaptWidth.elContainer,
           },
@@ -114,7 +128,7 @@
         // });
 
         // 方式2
-        modelRef.value = { name: data.name, password: data.password, detail: data.detail };
+        modelRef.value = { name: data.name, password: data.password, status: data.status, detail: data.detail };
 
         // setProps({
         //   model:{ field2: data.data, field1: data.info }

+ 0 - 0
src/views/permission/data.ts → src/views/permission/role/data.ts


+ 32 - 19
src/views/permission/index.vue → src/views/permission/role/index.vue

@@ -5,16 +5,15 @@
       @register="registerTable"
       rowKey="id"
     >
-      <template #action="{ record, column }">
-        <TableAction :actions="createActions(record, column)" />
-      </template>
-      <template #form-custom > custom-slot </template>
-
       <template #toolbar>
         <a-button type="primary" @click="addColumn" >
-          {{ '添加' }}
+          {{ '新增角色' }}
         </a-button>
       </template>
+      <template #action="{ record, column }">
+        <TableAction :actions="createActions(record, column)" />
+      </template>
+      <!-- <template #form-custom > custom-slot </template> -->
     </BasicTable>
     <Add @register="addRegister" :modelData="modelData" @saveData="saveData" />
   </div>
@@ -24,6 +23,8 @@
   import Add from './add.vue'
   import { useModal } from '/@/components/Modal';
   import { treeData } from './data';
+  import { h } from 'vue';
+  import { Tag } from 'ant-design-vue';
 
   import {
     BasicTable,
@@ -50,19 +51,31 @@
       editComponentProps: {
         prefix: '$',
       },
-      width: 200,
+      width: 80,
     },
     {
       title: '账户名',
       dataIndex: 'name',
       // editRow: true,
-      width: 200,
+      width: 160,
     },
     {
       title: '密码',
       dataIndex: 'password',
       // editRow: true,
-      width: 200,
+      width: 160,
+    },
+    {
+      title: '状态',
+      dataIndex: 'status',
+      width: 80,
+      customRender: ({ record }) => {
+        const status = record.status;
+        const enable = ~~status === 0;
+        const color = enable ? 'green' : 'red';
+        const text = enable ? '启用' : '停用';
+        return h(Tag, { color: color }, () => text);
+      },
     },
     {
       title: '详情',
@@ -86,27 +99,26 @@
         expandedKeys: ['0-1']
       })
       const [registerTable] = useTable({
-        title: "基础示例",
+        title: "角色列表",
         titleHelpMessage: "温馨提醒",
         rowSelection: { type: 'checkbox' },
         columns: columns,
         clickToRowSelect: false, // 点击行不勾选
         dataSource: [
-          { id: 1, name: 'vben', detail: 'super admin', password: '123456' },
-          { id:2,name:'test', detail: 'test admin', password: '123456' }
+          { id: 1, name: 'vben', status: '0', detail: 'super admin', password: '123456' },
+          { id:2,name:'test', status: '1', detail: 'test admin', password: '123456' }
         ],
-        showIndexColumn: false,
         actionColumn: {
           width: 160,
-          title: 'Action',
+          title: '操作',
           dataIndex: 'action',
           slots: { customRender: 'action' },
+          fixed: undefined,
         },
+        showIndexColumn: false,
+        bordered: true,
       });
       const [addRegister, { openModal: openAdd }] = useModal();
-      const rowClick = (e: any) => {
-        console.log(e)
-      }
 
       function getTableAction() { // 获取组件
         const tableAction = unref(tableRef);
@@ -196,7 +208,9 @@
       }
 
       function createActions(record: EditRecordRow, column: BasicColumn): ActionItem[] {
-        column
+        if (false) {
+          console.log(column)
+        }
         return [
           {
             label: '编辑',
@@ -220,7 +234,6 @@
         treeData,
         tableRef,
         registerTable,
-        rowClick,
         addColumn,
         handleEdit,
         createActions,

+ 2 - 0
src/views/table/editTable/index.vue

@@ -4,6 +4,7 @@
       ref="tableRef"
       @register="registerTable"
       @edit-end="handleEditEnd"
+      :bordered="true"
       rowKey="id"
     >
       <template #action="{ record, column }">
@@ -147,6 +148,7 @@ import { useModal } from '/@/components/Modal';
           title: 'Action',
           dataIndex: 'action',
           slots: { customRender: 'action' },
+          fixed: undefined,
         },
       });
       const [addRegister, { openModal: openAdd }] = useModal();