Forráskód Böngészése

feat: 修改菜单

DESKTOP-USV654P\pc 9 hónapja
szülő
commit
57bb8e1ff5

+ 1 - 0
apps/web-baicai/src/api/system/menu.ts

@@ -17,6 +17,7 @@ export namespace MenuApi {
     sort: number;
     status: number;
     meta: any;
+    type: number;
   }
 
   export interface RecordItem extends BasicRecordItem {

+ 0 - 0
apps/web-baicai/src/components/form/components/icon-picker.vue → apps/web-baicai/src/components/form/components/icon-picker-ignore.vue


+ 1 - 1
apps/web-baicai/src/components/form/components/input-code/input-code-modal-ignore.vue

@@ -20,7 +20,7 @@ const emit = defineEmits(['success']);
 //   language: 'javascript',
 // });
 
-// globalThis.MonacoEnvironment = {
+// self.MonacoEnvironment = {
 //   getWorker: (_: string, label: string) => {
 //     if (label === 'json') {
 //       return new JsonWorker();

+ 13 - 3
apps/web-baicai/src/router/routes/index.ts

@@ -17,12 +17,12 @@ const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
 
 /** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统(不会显示在菜单中) */
 // const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
-/** 不需要权限的菜单列表(会显示在菜单中) */
 // const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
 const staticRoutes: RouteRecordRaw[] = [];
 const externalRoutes: RouteRecordRaw[] = [];
 
-/** 路由列表,由基本路由+静态路由组成 */
+/** 路由列表,由基本路由、外部路由和404兜底路由组成
+ *  无需走权限验证(会一直显示在菜单中) */
 const routes: RouteRecordRaw[] = [
   ...coreRoutes,
   ...externalRoutes,
@@ -32,6 +32,16 @@ const routes: RouteRecordRaw[] = [
 /** 基本路由列表,这些路由不需要进入权限拦截 */
 const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
 
+/** 有权限校验的路由列表,包含动态路由和静态路由 */
 const accessRoutes = [...dynamicRoutes, ...staticRoutes];
 
-export { accessRoutes, coreRouteNames, routes };
+const componentKeys: string[] = Object.keys(
+  import.meta.glob('../../views/**/*.vue'),
+)
+  .filter((item) => !item.includes('/modules/'))
+  .map((v) => {
+    const path = v.replace('../../views/', '/');
+    return path.endsWith('.vue') ? path.slice(0, -4) : path;
+  });
+
+export { accessRoutes, componentKeys, coreRouteNames, routes };

+ 5 - 0
apps/web-baicai/src/views/system/menu/components/edit.vue

@@ -19,7 +19,12 @@ const isUpdate = ref(true);
 
 const [Form, { validate, setValues, getValues }] = useVbenForm(
   useFormOptions({
+    commonConfig: {
+      colon: true,
+      formItemClass: 'col-span-2 md:col-span-1',
+    },
     schema: useSchema(),
+    wrapperClass: 'grid-cols-2 gap-x-4',
   }),
 );
 

+ 243 - 151
apps/web-baicai/src/views/system/menu/data.config.ts

@@ -4,11 +4,20 @@ import type {
   VxeTableGridOptions,
 } from '#/adapter';
 
-import { h } from 'vue';
-
+import { z } from '#/adapter';
 import { EnumApi, MenuApi } from '#/api';
 import { boolOptions } from '#/api/model';
-import { Icon } from '#/components/icon';
+import { componentKeys } from '#/router/routes';
+
+export function getTypeOptions() {
+  return [
+    { color: 'processing', label: '目录', value: 0 },
+    { color: 'default', label: '菜单', value: 1 },
+    { color: 'error', label: '按钮', value: 2 },
+    { color: 'success', label: '内嵌', value: 3 },
+    { color: 'warning', label: '外连', value: 4 },
+  ];
+}
 
 export const useSearchSchema = (): VbenFormSchema[] => {
   return [
@@ -39,49 +48,46 @@ export function useColumns(
     {
       align: 'left',
       field: 'meta.title',
-      title: '菜单名称',
-      width: 200,
+      title: '标题',
+      width: 250,
       treeNode: true,
-      showOverflow: true,
-      slots: {
-        default: ({ row }: any) => {
-          return row.meta.icon
-            ? h(
-                'span',
-                {
-                  style: {
-                    display: 'flex',
-                    alignItems: 'center',
-                  },
-                },
-                [
-                  h(Icon, {
-                    icon: row.meta.icon,
-                  }),
-                  h(
-                    'span',
-                    {
-                      style: {
-                        paddingLeft: '6px',
-                      },
-                    },
-                    row.meta.title,
-                  ),
-                ],
-              )
-            : h('span', {}, row.meta.title);
-        },
-      },
+      slots: { default: 'title' },
+    },
+    {
+      align: 'center',
+      cellRender: { name: 'CellTag', options: getTypeOptions() },
+      field: 'type',
+      title: '类型',
+      width: 100,
     },
-    { align: 'left', field: 'path', title: '路由地址', width: 180 },
-    { align: 'left', field: 'component', title: '组件' },
     {
       align: 'left',
       field: 'permission',
       title: '权限标识',
-      width: 120,
+      width: 200,
       showOverflow: true,
     },
+    { align: 'left', field: 'path', title: '路由地址', width: 200 },
+    {
+      align: 'left',
+      field: 'component',
+      title: '页面组件',
+      formatter: ({ row }) => {
+        switch (row.type) {
+          case 0:
+          case 1: {
+            return row.component ?? '';
+          }
+          case 2: {
+            return row.meta?.iframeSrc ?? '';
+          }
+          case 3: {
+            return row.meta?.link ?? '';
+          }
+        }
+        return '';
+      },
+    },
     {
       field: 'status',
       title: '状态',
@@ -101,7 +107,7 @@ export function useColumns(
         options: [
           {
             code: 'append',
-            label: '添加',
+            label: '添加下级',
             auth: ['menu:add'],
           },
           {
@@ -119,7 +125,7 @@ export function useColumns(
       headerAlign: 'center',
       showOverflow: false,
       title: '操作',
-      width: 100,
+      width: 162,
     },
   ];
 }
@@ -139,22 +145,30 @@ export const useSchema = (): VbenFormSchema[] => {
       fieldName: 'type',
       label: '菜单类型',
       rules: 'required',
+      formItemClass: 'col-span-2 md:col-span-2',
     },
     {
       component: 'Input',
       componentProps: {
-        placeholder: '请输入路由名称',
+        placeholder: '请输入菜单名称',
       },
       fieldName: 'name',
-      label: '路由名称',
+      label: '菜单名称',
       help: 'route.name',
-      rules: 'required',
-      dependencies: {
-        show(values) {
-          return [0, 1].includes(values.type);
-        },
-        triggerFields: ['type'],
+      rules: z
+        .string()
+        .min(2, '菜单名称至少2个字符')
+        .max(30, '菜单名称最多30个字符'),
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: '请输入标题',
       },
+      fieldName: 'meta.title',
+      help: 'meta.title',
+      label: '标题',
+      rules: 'required',
     },
     {
       component: 'Input',
@@ -168,165 +182,111 @@ export const useSchema = (): VbenFormSchema[] => {
       dependencies: {
         triggerFields: ['type'],
         show(values) {
-          return [0, 1].includes(values.type);
-        },
-      },
-    },
-    {
-      component: 'Input',
-      componentProps: {
-        placeholder: '请输入组件路径',
-      },
-      dependencies: {
-        show(values) {
-          return [0, 1].includes(values.type);
+          return [0, 1, 3].includes(values.type);
         },
-        triggerFields: ['type'],
       },
-      fieldName: 'component',
-      label: '组件路径',
-      help: 'route.component',
-      rules: 'required',
     },
     {
       component: 'Input',
       componentProps: {
-        placeholder: '请输入重定向',
-      },
+        placeholder: '请输入激活路径',
+      },
+      fieldName: 'activePath',
+      label: '激活路径',
+      help: 'route.activePath',
+      rules: z
+        .string()
+        .min(2, '激活路径至少2个字符')
+        .max(30, '激活路径最多100个字符')
+        .refine((value: string) => {
+          return value.startsWith('/');
+        }, '激活路径必须以/开头')
+        .optional(),
       dependencies: {
         triggerFields: ['type'],
         show(values) {
-          return [0, 1].includes(values.type);
+          return [1, 3].includes(values.type);
         },
       },
-      fieldName: 'redirect',
-      label: '重定向',
-      help: 'route.redirect',
-    },
-    {
-      component: 'Input',
-      componentProps: {
-        placeholder: '请输入菜单名称',
-      },
-      fieldName: 'meta.title',
-      help: 'meta.title',
-      label: '菜单名称',
-      rules: 'required',
     },
     {
       component: 'IconPicker',
       componentProps: {
         placeholder: '请输入图标',
+        prefix: 'carbon',
       },
       fieldName: 'meta.icon',
       label: '图标',
       help: 'meta.icon',
-      rules: 'required',
       dependencies: {
         show(values) {
-          return [0, 1].includes(values.type);
+          return [0, 1, 3, 4].includes(values.type);
         },
         triggerFields: ['type'],
       },
     },
     {
-      component: 'RadioGroup',
-      defaultValue: true,
+      component: 'IconPicker',
       componentProps: {
-        placeholder: '请输入',
-        options: boolOptions,
-        optionType: 'button',
-        buttonStyle: 'solid',
+        prefix: 'carbon',
       },
       dependencies: {
-        triggerFields: ['type'],
-        show(values) {
-          return [1].includes(values.type);
+        show: (values) => {
+          return [0, 1, 3].includes(values.type);
         },
-      },
-      fieldName: 'meta.keepAlive',
-      label: '缓存',
-      help: 'meta.keepAlive',
-    },
-    {
-      component: 'RadioGroup',
-      defaultValue: false,
-      componentProps: {
-        placeholder: '请输入',
-        options: boolOptions,
-        optionType: 'button',
-        buttonStyle: 'solid',
-      },
-      dependencies: {
         triggerFields: ['type'],
-        show(values) {
-          return [0, 1].includes(values.type);
-        },
       },
-      fieldName: 'meta.hideInTab',
-      label: '隐藏',
-      help: 'meta.keepAlive',
+      fieldName: 'meta.activeIcon',
+      label: '激活图标',
     },
     {
-      component: 'ApiSelect',
-      defaultValue: 0,
+      component: 'AutoComplete',
       componentProps: {
-        placeholder: '请输入',
-        api: {
-          type: 'enum',
-          params: EnumApi.EnumType.PathType,
+        allowClear: true,
+        class: 'w-full',
+        filterOption(input: string, option: { value: string }) {
+          return option.value.toLowerCase().includes(input.toLowerCase());
         },
+        options: componentKeys.map((v: any) => ({ value: v })),
       },
       dependencies: {
-        triggerFields: ['type'],
+        rules: (values) => {
+          return values.type === 0 ? 'required' : null;
+        },
         show(values) {
-          return [0, 1].includes(values.type);
+          return [1].includes(values.type);
         },
+        triggerFields: ['type'],
       },
-      fieldName: 'meta.pathType',
-      label: '路由类型',
+      fieldName: 'component',
+      label: '组件路径',
+      help: 'route.component',
       rules: 'required',
     },
     {
       component: 'Input',
-      componentProps: {
-        placeholder: '请输入内嵌页面',
-      },
       dependencies: {
-        triggerFields: ['pathType'],
-        show(values) {
-          return [1].includes(values.pathType);
+        show: (values) => {
+          return [3].includes(values.type);
         },
+        triggerFields: ['type'],
       },
-      fieldName: 'meta.iframeSrc',
-      help: 'meta.iframeSrc',
-      label: '内嵌页面',
-      rules: 'required',
+      fieldName: 'iframeSrc',
+      label: '内嵌地址',
+      rules: z.string().url('请输入有效的连接'),
     },
     {
       component: 'Input',
-      componentProps: {
-        placeholder: '请输入外链地址',
-      },
       dependencies: {
-        triggerFields: ['pathType'],
-        show(values) {
-          return [2].includes(values.pathType);
+        show: (values) => {
+          return [4].includes(values.type);
         },
+        triggerFields: ['type'],
       },
-      fieldName: 'meta.link',
+      fieldName: 'link',
+      label: '外连地址',
       help: 'meta.link',
-      label: '外链地址',
-      rules: 'required',
-    },
-    {
-      component: 'InputNumber',
-      componentProps: {
-        placeholder: '请输入',
-      },
-      fieldName: 'sort',
-      label: '排序',
-      rules: 'required',
+      rules: z.string().url('请输入有效的连接'),
     },
     {
       component: 'Input',
@@ -356,6 +316,138 @@ export const useSchema = (): VbenFormSchema[] => {
       fieldName: 'status',
       label: '状态',
     },
+    {
+      component: 'InputNumber',
+      componentProps: {
+        placeholder: '请输入',
+      },
+      fieldName: 'sort',
+      label: '排序',
+      rules: 'required',
+    },
+    {
+      component: 'Divider',
+      dependencies: {
+        show: (values) => {
+          return ![2, 4].includes(values.type);
+        },
+        triggerFields: ['type'],
+      },
+      fieldName: 'divider1',
+      formItemClass: 'col-span-2 md:col-span-2 pb-0',
+      hideLabel: true,
+      renderComponentContent() {
+        return {
+          default: () => '其它设置',
+        };
+      },
+    },
+    {
+      component: 'Checkbox',
+      dependencies: {
+        show: (values) => {
+          return [1].includes(values.type);
+        },
+        triggerFields: ['type'],
+      },
+      fieldName: 'meta.keepAlive',
+      renderComponentContent() {
+        return {
+          default: () => '缓存标签页',
+        };
+      },
+    },
+    {
+      component: 'Checkbox',
+      dependencies: {
+        show: (values) => {
+          return [1, 3].includes(values.type);
+        },
+        triggerFields: ['type'],
+      },
+      fieldName: 'meta.affixTab',
+      renderComponentContent() {
+        return {
+          default: () => '固定标签页',
+        };
+      },
+    },
+    {
+      component: 'Checkbox',
+      dependencies: {
+        show: (values) => {
+          return ![2].includes(values.type);
+        },
+        triggerFields: ['type'],
+      },
+      fieldName: 'meta.hideInMenu',
+      renderComponentContent() {
+        return {
+          default: () => '隐藏菜单',
+        };
+      },
+    },
+    {
+      component: 'Checkbox',
+      dependencies: {
+        show: (values) => {
+          return [0, 1].includes(values.type);
+        },
+        triggerFields: ['type'],
+      },
+      fieldName: 'meta.hideChildrenInMenu',
+      renderComponentContent() {
+        return {
+          default: () => '隐藏子菜单',
+        };
+      },
+    },
+    {
+      component: 'Checkbox',
+      dependencies: {
+        show: (values) => {
+          return ![2, 4].includes(values.type);
+        },
+        triggerFields: ['type'],
+      },
+      fieldName: 'meta.hideInBreadcrumb',
+      renderComponentContent() {
+        return {
+          default: () => '在面包屑中隐藏',
+        };
+      },
+    },
+    {
+      component: 'Checkbox',
+      dependencies: {
+        show: (values) => {
+          return ![2, 4].includes(values.type);
+        },
+        triggerFields: ['type'],
+      },
+      fieldName: 'meta.hideInTab',
+      renderComponentContent() {
+        return {
+          default: () => '在标签栏中隐藏',
+        };
+      },
+    },
+
+    // {
+    //   component: 'Input',
+    //   componentProps: {
+    //     placeholder: '请输入重定向',
+    //   },
+    //   dependencies: {
+    //     triggerFields: ['type'],
+    //     show(values) {
+    //       return [0, 1].includes(values.type);
+    //     },
+    //   },
+    //   fieldName: 'redirect',
+    //   label: '重定向',
+    //   help: 'route.redirect',
+    // },
   ];
 };
 

+ 19 - 0
apps/web-baicai/src/views/system/menu/index.vue

@@ -2,6 +2,7 @@
 import type { OnActionClickParams, VxeTableGridOptions } from '#/adapter/';
 
 import { Page, useVbenDrawer } from '@vben/common-ui';
+import { IconifyIcon } from '@vben/icons';
 
 import { Button, message } from 'ant-design-vue';
 
@@ -98,6 +99,24 @@ const [Grid, { reload }] = useVbenVxeGrid(
           新增菜单
         </Button>
       </template>
+      <template #title="{ row }">
+        <div class="flex w-full items-center gap-1">
+          <div class="size-5 flex-shrink-0">
+            <IconifyIcon
+              v-if="row.type === 2"
+              icon="carbon:security"
+              class="size-full"
+            />
+            <IconifyIcon
+              v-else-if="row.meta?.icon"
+              :icon="row.meta?.icon || 'carbon:circle-dash'"
+              class="size-full"
+            />
+          </div>
+          <span class="flex-auto">{{ $t(row.meta?.title) }}</span>
+          <div class="items-center justify-end"></div>
+        </div>
+      </template>
     </Grid>
   </Page>
 </template>