feat:增加 menu 菜单的列表(新增、修改、删除 80%)
							parent
							
								
									09d0cfa87e
								
							
						
					
					
						commit
						18ac4cb14c
					
				|  | @ -1,14 +1,14 @@ | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import type { SystemDeptApi } from '#/api/system/dept'; | import type { SystemDeptApi } from '#/api/system/dept'; | ||||||
| 
 | 
 | ||||||
| import { computed, ref } from 'vue'; |  | ||||||
| 
 | 
 | ||||||
| import { useVbenModal } from '@vben/common-ui'; | import { useVbenModal } from '@vben/common-ui'; | ||||||
| import { message } from 'ant-design-vue'; | import { message } from 'ant-design-vue'; | ||||||
| 
 | 
 | ||||||
|  | import { computed, ref } from 'vue'; | ||||||
|  | import { $t } from '#/locales'; | ||||||
| import { useVbenForm } from '#/adapter/form'; | import { useVbenForm } from '#/adapter/form'; | ||||||
| import { createDept, updateDept, getDept } from '#/api/system/dept'; | import { createDept, updateDept, getDept } from '#/api/system/dept'; | ||||||
| import { $t } from '#/locales'; |  | ||||||
| 
 | 
 | ||||||
| import { useFormSchema } from '../data'; | import { useFormSchema } from '../data'; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,25 +1,8 @@ | ||||||
| import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table'; | import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table'; | ||||||
| // import type { SystemMenuApi } from '#/api/system/menu';
 | import type { SystemMenuApi } from '#/api/system/menu'; | ||||||
| 
 | 
 | ||||||
| import { $t } from '#/locales'; | import { $t } from '#/locales'; | ||||||
| 
 | import {DICT_TYPE} from '#/utils/dict'; | ||||||
| export function getMenuTypeOptions() { |  | ||||||
|   return [ |  | ||||||
|     { |  | ||||||
|       color: 'processing', |  | ||||||
|       label: $t('system.menu.typeCatalog'), |  | ||||||
|       value: 'catalog', |  | ||||||
|     }, |  | ||||||
|     { color: 'default', label: $t('system.menu.typeMenu'), value: 'menu' }, |  | ||||||
|     { color: 'error', label: $t('system.menu.typeButton'), value: 'button' }, |  | ||||||
|     { |  | ||||||
|       color: 'success', |  | ||||||
|       label: $t('system.menu.typeEmbedded'), |  | ||||||
|       value: 'embedded', |  | ||||||
|     }, |  | ||||||
|     { color: 'warning', label: $t('system.menu.typeLink'), value: 'link' }, |  | ||||||
|   ]; |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| export function useGridColumns( | export function useGridColumns( | ||||||
|   onActionClick: OnActionClickFn<SystemMenuApi.SystemMenu>, |   onActionClick: OnActionClickFn<SystemMenuApi.SystemMenu>, | ||||||
|  | @ -27,55 +10,51 @@ export function useGridColumns( | ||||||
|   return [ |   return [ | ||||||
|     { |     { | ||||||
|       align: 'left', |       align: 'left', | ||||||
|       field: 'meta.title', |       field: 'name', | ||||||
|       fixed: 'left', |       fixed: 'left', | ||||||
|       slots: { default: 'title' }, |       slots: { default: 'name' }, | ||||||
|       title: $t('system.menu.menuTitle'), |       title: '菜单名称', | ||||||
|       treeNode: true, |       treeNode: true, | ||||||
|       width: 250, |       minWidth: 250, | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       align: 'center', |       cellRender: { | ||||||
|       cellRender: { name: 'CellTag', options: getMenuTypeOptions() }, |         name: 'CellDict', | ||||||
|  |         props: { type: DICT_TYPE.SYSTEM_MENU_TYPE }, | ||||||
|  |       }, | ||||||
|       field: 'type', |       field: 'type', | ||||||
|       title: $t('system.menu.type'), |       title: '菜单类型', | ||||||
|       width: 100, |       minWidth: 100, | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       align: 'left', |       field: 'sort', | ||||||
|       field: 'path', |       title: '显示排序', | ||||||
|       title: $t('system.menu.path'), |       minWidth: 100, | ||||||
|       width: 200, |  | ||||||
|     }, |     }, | ||||||
| 
 |  | ||||||
|     { |     { | ||||||
|       align: 'left', |       field: 'permission', | ||||||
|       field: 'component', |       title: '权限标识', | ||||||
|       formatter: ({ row }) => { |  | ||||||
|         switch (row.type) { |  | ||||||
|           case 'catalog': |  | ||||||
|           case 'menu': { |  | ||||||
|             return row.component ?? ''; |  | ||||||
|           } |  | ||||||
|           case 'embedded': { |  | ||||||
|             return row.meta?.iframeSrc ?? ''; |  | ||||||
|           } |  | ||||||
|           case 'link': { |  | ||||||
|             return row.meta?.link ?? ''; |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|         return ''; |  | ||||||
|       }, |  | ||||||
|       minWidth: 200, |       minWidth: 200, | ||||||
|       title: $t('system.menu.component'), |  | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       cellRender: { name: 'CellTag' }, |       field: 'path', | ||||||
|       field: 'status', |       title: '组件路径', | ||||||
|       title: $t('system.menu.status'), |       minWidth: 200, | ||||||
|       width: 100, |     }, | ||||||
|  |     { | ||||||
|  |       field: 'componentName', | ||||||
|  |       minWidth: 200, | ||||||
|  |       title: '组件名称', | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       cellRender: { | ||||||
|  |         name: 'CellDict', | ||||||
|  |         props: { type: DICT_TYPE.COMMON_STATUS }, | ||||||
|  |       }, | ||||||
|  |       field: 'status', | ||||||
|  |       title: '状态', | ||||||
|  |       minWidth: 100, | ||||||
|     }, |     }, | ||||||
| 
 |  | ||||||
|     { |     { | ||||||
|       align: 'right', |       align: 'right', | ||||||
|       cellRender: { |       cellRender: { | ||||||
|  | @ -97,8 +76,8 @@ export function useGridColumns( | ||||||
|       fixed: 'right', |       fixed: 'right', | ||||||
|       headerAlign: 'center', |       headerAlign: 'center', | ||||||
|       showOverflow: false, |       showOverflow: false, | ||||||
|       title: $t('system.menu.operation'), |       title: '操作', | ||||||
|       width: 200, |       minWidth: 200, | ||||||
|     }, |     }, | ||||||
|   ]; |   ]; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -133,7 +133,7 @@ function toggleExpand() { | ||||||
|           {{ isExpanded ? '收缩' : '展开' }} |           {{ isExpanded ? '收缩' : '展开' }} | ||||||
|         </Button> |         </Button> | ||||||
|       </template> |       </template> | ||||||
|       <template #title="{ row }"> |       <template #name="{ row }"> | ||||||
|         <div class="flex w-full items-center gap-1"> |         <div class="flex w-full items-center gap-1"> | ||||||
|           <div class="size-5 flex-shrink-0"> |           <div class="size-5 flex-shrink-0"> | ||||||
|             <IconifyIcon |             <IconifyIcon | ||||||
|  | @ -141,6 +141,7 @@ function toggleExpand() { | ||||||
|               icon="carbon:square-outline" |               icon="carbon:square-outline" | ||||||
|               class="size-full" |               class="size-full" | ||||||
|             /> |             /> | ||||||
|  |             <!-- TODO @芋艿:这里的 空串的情况 --> | ||||||
|             <IconifyIcon |             <IconifyIcon | ||||||
|               v-else-if="row.icon" |               v-else-if="row.icon" | ||||||
|               :icon="row.icon || 'carbon:circle-dash'" |               :icon="row.icon || 'carbon:circle-dash'" | ||||||
|  |  | ||||||
|  | @ -1,208 +1,157 @@ | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import type { ChangeEvent } from 'ant-design-vue/es/_util/EventInterface'; | import type { SystemMenuApi } from '#/api/system/menu'; | ||||||
| 
 |  | ||||||
| import type { Recordable } from '@vben/types'; | import type { Recordable } from '@vben/types'; | ||||||
| 
 |  | ||||||
| import type { VbenFormSchema } from '#/adapter/form'; | import type { VbenFormSchema } from '#/adapter/form'; | ||||||
| 
 | 
 | ||||||
| import { computed, h, ref } from 'vue'; |  | ||||||
| 
 |  | ||||||
| import { useVbenDrawer } from '@vben/common-ui'; |  | ||||||
| import { IconifyIcon } from '@vben/icons'; |  | ||||||
| import { $te } from '@vben/locales'; |  | ||||||
| import { getPopupContainer } from '@vben/utils'; |  | ||||||
| 
 |  | ||||||
| import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'; |  | ||||||
| 
 |  | ||||||
| import { useVbenForm, z } from '#/adapter/form'; |  | ||||||
| // import { |  | ||||||
| //   createMenu, |  | ||||||
| //   getMenuList, |  | ||||||
| //   isMenuNameExists, |  | ||||||
| //   isMenuPathExists, |  | ||||||
| //   SystemMenuApi, |  | ||||||
| //   updateMenu, |  | ||||||
| // } from '#/api/system/menu'; |  | ||||||
| import { $t } from '#/locales'; | import { $t } from '#/locales'; | ||||||
|  | import { computed, h, ref } from 'vue'; | ||||||
|  | import { useVbenForm, z } from '#/adapter/form'; | ||||||
|  | import { createMenu, getMenu, updateMenu } from '#/api/system/menu'; | ||||||
|  | import { getMenuList } from '#/api/system/menu'; | ||||||
|  | import { DICT_TYPE, getDictOptions } from '#/utils/dict'; | ||||||
|  | import { handleTree } from '#/utils/tree'; | ||||||
|  | import { CommonStatusEnum, SystemMenuTypeEnum } from '#/utils/constants'; | ||||||
|  | import { isHttpUrl } from '@vben/utils'; | ||||||
|  | 
 | ||||||
|  | import { useVbenModal } from '@vben/common-ui'; | ||||||
|  | import { IconifyIcon } from '@vben/icons'; | ||||||
|  | import { message } from 'ant-design-vue'; | ||||||
|  | 
 | ||||||
| // import { componentKeys } from '#/router/routes'; // TODO @芋艿:后续搞 | // import { componentKeys } from '#/router/routes'; // TODO @芋艿:后续搞 | ||||||
| 
 | 
 | ||||||
| import { getMenuTypeOptions } from '../data'; | const emit = defineEmits(['success']); | ||||||
| import { getMenuList } from '#/api/system/menu'; |  | ||||||
| 
 |  | ||||||
| const emit = defineEmits<{ |  | ||||||
|   success: []; |  | ||||||
| }>(); |  | ||||||
| const formData = ref<SystemMenuApi.SystemMenu>(); | const formData = ref<SystemMenuApi.SystemMenu>(); | ||||||
| const titleSuffix = ref<string>(); | const getTitle = computed(() => | ||||||
|  |   formData.value?.id | ||||||
|  |     ? $t('ui.actionTitle.edit', ['菜单']) | ||||||
|  |     : $t('ui.actionTitle.create', ['菜单']), | ||||||
|  | ); | ||||||
| const schema: VbenFormSchema[] = [ | const schema: VbenFormSchema[] = [ | ||||||
|   { |  | ||||||
|     component: 'RadioGroup', |  | ||||||
|     componentProps: { |  | ||||||
|       buttonStyle: 'solid', |  | ||||||
|       options: getMenuTypeOptions(), |  | ||||||
|       optionType: 'button', |  | ||||||
|     }, |  | ||||||
|     defaultValue: 'menu', |  | ||||||
|     fieldName: 'type', |  | ||||||
|     formItemClass: 'col-span-2 md:col-span-2', |  | ||||||
|     label: $t('system.menu.type'), |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     component: 'Input', |  | ||||||
|     fieldName: 'name', |  | ||||||
|     label: $t('system.menu.menuName'), |  | ||||||
|     rules: z |  | ||||||
|       .string() |  | ||||||
|       .min(2, $t('ui.formRules.minLength', [$t('system.menu.menuName'), 2])) |  | ||||||
|       .max(30, $t('ui.formRules.maxLength', [$t('system.menu.menuName'), 30])) |  | ||||||
|       .refine( |  | ||||||
|         async (value: string) => { |  | ||||||
|           return !(await isMenuNameExists(value, formData.value?.id)); |  | ||||||
|         }, |  | ||||||
|         (value) => ({ |  | ||||||
|           message: $t('ui.formRules.alreadyExists', [ |  | ||||||
|             $t('system.menu.menuName'), |  | ||||||
|             value, |  | ||||||
|           ]), |  | ||||||
|         }), |  | ||||||
|       ), |  | ||||||
|   }, |  | ||||||
|   { |   { | ||||||
|     component: 'ApiTreeSelect', |     component: 'ApiTreeSelect', | ||||||
|     componentProps: { |     componentProps: { | ||||||
|       api: getMenuList, |       allowClear: true, | ||||||
|  |       api: async () => { | ||||||
|  |         const data = await getMenuList(); | ||||||
|  |         data.unshift({ | ||||||
|  |           id: 0, | ||||||
|  |           name: '顶级部门', | ||||||
|  |         } as SystemMenuApi.SystemMenu); | ||||||
|  |         return handleTree(data); | ||||||
|  |       }, | ||||||
|       class: 'w-full', |       class: 'w-full', | ||||||
|  |       labelField: 'name', | ||||||
|  |       valueField: 'id', | ||||||
|  |       childrenField: 'children', | ||||||
|  |       placeholder: '请选择上级菜单', | ||||||
|       filterTreeNode(input: string, node: Recordable<any>) { |       filterTreeNode(input: string, node: Recordable<any>) { | ||||||
|         if (!input || input.length === 0) { |         if (!input || input.length === 0) { | ||||||
|           return true; |           return true; | ||||||
|         } |         } | ||||||
|         const title: string = node.meta?.title ?? ''; |         const name: string = node.label ?? ''; | ||||||
|         if (!title) return false; |         if (!name) return false; | ||||||
|         return title.includes(input) || $t(title).includes(input); |         return name.includes(input) || $t(name).includes(input); | ||||||
|       }, |       }, | ||||||
|       getPopupContainer, |  | ||||||
|       labelField: 'meta.title', |  | ||||||
|       showSearch: true, |       showSearch: true, | ||||||
|       treeDefaultExpandAll: true, |       treeDefaultExpandedKeys: [0], | ||||||
|       valueField: 'id', |  | ||||||
|       childrenField: 'children', |  | ||||||
|     }, |     }, | ||||||
|     fieldName: 'pid', |     fieldName: 'parentId', | ||||||
|     label: $t('system.menu.parent'), |     label: '上级菜单', | ||||||
|  |     rules: 'selectRequired', | ||||||
|     renderComponentContent() { |     renderComponentContent() { | ||||||
|       return { |       return { | ||||||
|         title({ label, meta }: { label: string; meta: Recordable<any> }) { |         title({ label, icon }: { label: string; icon: string }) { | ||||||
|           const coms = []; |           const components = []; | ||||||
|           if (!label) return ''; |           if (!label) return ''; | ||||||
|           if (meta?.icon) { |           if (icon) { | ||||||
|             coms.push(h(IconifyIcon, { class: 'size-4', icon: meta.icon })); |             components.push(h(IconifyIcon, { class: 'size-4', icon })); | ||||||
|           } |           } | ||||||
|           coms.push(h('span', { class: '' }, $t(label || ''))); |           components.push(h('span', { class: '' }, $t(label || ''))); | ||||||
|           return h('div', { class: 'flex items-center gap-1' }, coms); |           return h('div', { class: 'flex items-center gap-1' }, components); | ||||||
|         }, |         }, | ||||||
|       }; |       }; | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     component: 'Input', |     component: 'Input', | ||||||
|     componentProps() { |     componentProps: { | ||||||
|       // 不需要处理多语言时就无需这么做 |       placeholder: '请输入菜单名称', | ||||||
|       return { |  | ||||||
|         addonAfter: titleSuffix.value, |  | ||||||
|         onChange({ target: { value } }: ChangeEvent) { |  | ||||||
|           titleSuffix.value = value && $te(value) ? $t(value) : undefined; |  | ||||||
|     }, |     }, | ||||||
|       }; |     fieldName: 'name', | ||||||
|     }, |     label: '菜单名称', | ||||||
|     fieldName: 'meta.title', |  | ||||||
|     label: $t('system.menu.menuTitle'), |  | ||||||
|     rules: 'required', |     rules: 'required', | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     component: 'Input', |     component: 'RadioGroup', | ||||||
|  |     componentProps: { | ||||||
|  |       options: getDictOptions(DICT_TYPE.SYSTEM_MENU_TYPE, 'number'), | ||||||
|  |       buttonStyle: 'solid', | ||||||
|  |       optionType: 'button', | ||||||
|  |     }, | ||||||
|  |     fieldName: 'type', | ||||||
|  |     label: '菜单类型', | ||||||
|  |     rules: z.number().default(SystemMenuTypeEnum.DIR), | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     component: 'IconPicker', | ||||||
|  |     componentProps: { | ||||||
|  |       placeholder: '请选择菜单图标', | ||||||
|  |       prefix: 'carbon', | ||||||
|  |     }, | ||||||
|     dependencies: { |     dependencies: { | ||||||
|       show: (values) => { |       show: (values) => { | ||||||
|         return ['catalog', 'embedded', 'menu'].includes(values.type); |         return [SystemMenuTypeEnum.DIR, SystemMenuTypeEnum.MENU].includes( | ||||||
|  |           values.type, | ||||||
|  |         ); | ||||||
|       }, |       }, | ||||||
|       triggerFields: ['type'], |       triggerFields: ['type'], | ||||||
|     }, |     }, | ||||||
|  |     fieldName: 'icon', | ||||||
|  |     label: '菜单图标', | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     component: 'Input', | ||||||
|  |     componentProps: { | ||||||
|  |       placeholder: '请输入路由地址', | ||||||
|  |     }, | ||||||
|  |     dependencies: { | ||||||
|  |       show: (values) => { | ||||||
|  |         return [SystemMenuTypeEnum.DIR, SystemMenuTypeEnum.MENU].includes( | ||||||
|  |           values.type, | ||||||
|  |         ); | ||||||
|  |       }, | ||||||
|  |       triggerFields: ['type', 'parentId'], | ||||||
|  |       rules: (values) => { | ||||||
|  |         let schema = z.string(); | ||||||
|  |         if (isHttpUrl(values.path)) { | ||||||
|  |           return 'required'; | ||||||
|  |         } | ||||||
|  |         if (values.parentId === 0) { | ||||||
|  |           return schema.refine((path) => path.charAt(0) === '/', { | ||||||
|  |             message: '路径必须以 / 开头', | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  |         return schema.refine((path) => path.charAt(0) !== '/', { | ||||||
|  |           message: '路径不能以 / 开头', | ||||||
|  |         }); | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|     fieldName: 'path', |     fieldName: 'path', | ||||||
|     label: $t('system.menu.path'), |     label: '路由地址', | ||||||
|     rules: z |  | ||||||
|       .string() |  | ||||||
|       .min(2, $t('ui.formRules.minLength', [$t('system.menu.path'), 2])) |  | ||||||
|       .max(100, $t('ui.formRules.maxLength', [$t('system.menu.path'), 100])) |  | ||||||
|       .refine( |  | ||||||
|         (value: string) => { |  | ||||||
|           return value.startsWith('/'); |  | ||||||
|         }, |  | ||||||
|         $t('ui.formRules.startWith', [$t('system.menu.path'), '/']), |  | ||||||
|       ) |  | ||||||
|       .refine( |  | ||||||
|         async (value: string) => { |  | ||||||
|           return !(await isMenuPathExists(value, formData.value?.id)); |  | ||||||
|         }, |  | ||||||
|         (value) => ({ |  | ||||||
|           message: $t('ui.formRules.alreadyExists', [ |  | ||||||
|             $t('system.menu.path'), |  | ||||||
|             value, |  | ||||||
|           ]), |  | ||||||
|         }), |  | ||||||
|       ), |  | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     component: 'Input', |     component: 'Input', | ||||||
|     dependencies: { |  | ||||||
|       show: (values) => { |  | ||||||
|         return ['embedded', 'menu'].includes(values.type); |  | ||||||
|       }, |  | ||||||
|       triggerFields: ['type'], |  | ||||||
|     }, |  | ||||||
|     fieldName: 'activePath', |  | ||||||
|     help: $t('system.menu.activePathHelp'), |  | ||||||
|     label: $t('system.menu.activePath'), |  | ||||||
|     rules: z |  | ||||||
|       .string() |  | ||||||
|       .min(2, $t('ui.formRules.minLength', [$t('system.menu.path'), 2])) |  | ||||||
|       .max(100, $t('ui.formRules.maxLength', [$t('system.menu.path'), 100])) |  | ||||||
|       .refine( |  | ||||||
|         (value: string) => { |  | ||||||
|           return value.startsWith('/'); |  | ||||||
|         }, |  | ||||||
|         $t('ui.formRules.startWith', [$t('system.menu.path'), '/']), |  | ||||||
|       ) |  | ||||||
|       .refine(async (value: string) => { |  | ||||||
|         return await isMenuPathExists(value, formData.value?.id); |  | ||||||
|       }, $t('system.menu.activePathMustExist')) |  | ||||||
|       .optional(), |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     component: 'IconPicker', |  | ||||||
|     componentProps: { |     componentProps: { | ||||||
|       prefix: 'carbon', |       placeholder: '请输入组件地址', | ||||||
|     }, |     }, | ||||||
|     dependencies: { |     dependencies: { | ||||||
|       show: (values) => { |       show: (values) => { | ||||||
|         return ['catalog', 'embedded', 'link', 'menu'].includes(values.type); |         return [SystemMenuTypeEnum.MENU].includes(values.type); | ||||||
|       }, |       }, | ||||||
|       triggerFields: ['type'], |       triggerFields: ['type'], | ||||||
|     }, |     }, | ||||||
|     fieldName: 'meta.icon', |     fieldName: 'component', | ||||||
|     label: $t('system.menu.icon'), |     label: '组件地址', | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     component: 'IconPicker', |  | ||||||
|     componentProps: { |  | ||||||
|       prefix: 'carbon', |  | ||||||
|     }, |  | ||||||
|     dependencies: { |  | ||||||
|       show: (values) => { |  | ||||||
|         return ['catalog', 'embedded', 'menu'].includes(values.type); |  | ||||||
|       }, |  | ||||||
|       triggerFields: ['type'], |  | ||||||
|     }, |  | ||||||
|     fieldName: 'meta.activeIcon', |  | ||||||
|     label: $t('system.menu.activeIcon'), |  | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     component: 'AutoComplete', |     component: 'AutoComplete', | ||||||
|  | @ -212,262 +161,154 @@ const schema: VbenFormSchema[] = [ | ||||||
|       filterOption(input: string, option: { value: string }) { |       filterOption(input: string, option: { value: string }) { | ||||||
|         return option.value.toLowerCase().includes(input.toLowerCase()); |         return option.value.toLowerCase().includes(input.toLowerCase()); | ||||||
|       }, |       }, | ||||||
|  |       placeholder: '请选择组件名称', | ||||||
|       // options: componentKeys.map((v) => ({ value: v })), // TODO @芋艿:后续完善 |       // options: componentKeys.map((v) => ({ value: v })), // TODO @芋艿:后续完善 | ||||||
|     }, |     }, | ||||||
|     dependencies: { |     dependencies: { | ||||||
|  |       // TODO @芋艿:后续完善 | ||||||
|       rules: (values) => { |       rules: (values) => { | ||||||
|         return values.type === 'menu' ? 'required' : null; |         return values.type === 'menu' ? 'required' : null; | ||||||
|       }, |       }, | ||||||
|       show: (values) => { |       show: (values) => { | ||||||
|         return values.type === 'menu'; |         return [SystemMenuTypeEnum.MENU].includes(values.type); | ||||||
|       }, |       }, | ||||||
|       triggerFields: ['type'], |       triggerFields: ['type'], | ||||||
|     }, |     }, | ||||||
|     fieldName: 'component', |     fieldName: 'componentName', | ||||||
|     label: $t('system.menu.component'), |     label: '组件名称', | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     component: 'Input', |     component: 'Input', | ||||||
|  |     componentProps: { | ||||||
|  |       placeholder: '请输入菜单描述', | ||||||
|  |     }, | ||||||
|     dependencies: { |     dependencies: { | ||||||
|       show: (values) => { |       show: (values) => { | ||||||
|         return ['embedded', 'link'].includes(values.type); |         return [SystemMenuTypeEnum.MENU, SystemMenuTypeEnum.BUTTON].includes( | ||||||
|  |           values.type, | ||||||
|  |         ); | ||||||
|       }, |       }, | ||||||
|       triggerFields: ['type'], |       triggerFields: ['type'], | ||||||
|     }, |     }, | ||||||
|     fieldName: 'linkSrc', |     fieldName: 'permission', | ||||||
|     label: $t('system.menu.linkSrc'), |     label: '权限标识', | ||||||
|     rules: z.string().url($t('ui.formRules.invalidURL')), |   }, | ||||||
|  |   { | ||||||
|  |     component: 'InputNumber', | ||||||
|  |     componentProps: { | ||||||
|  |       min: 0, | ||||||
|  |       class: 'w-full', | ||||||
|  |       controlsPosition: 'right', | ||||||
|  |       placeholder: '请输入菜单顺序', | ||||||
|  |     }, | ||||||
|  |     fieldName: 'sort', | ||||||
|  |     label: '菜单顺序', | ||||||
|  |     rules: 'required', | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     component: 'RadioGroup', |     component: 'RadioGroup', | ||||||
|     componentProps: { |     componentProps: { | ||||||
|  |       options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), | ||||||
|       buttonStyle: 'solid', |       buttonStyle: 'solid', | ||||||
|       options: [ |  | ||||||
|         { label: $t('common.enabled'), value: 1 }, |  | ||||||
|         { label: $t('common.disabled'), value: 0 }, |  | ||||||
|       ], |  | ||||||
|       optionType: 'button', |       optionType: 'button', | ||||||
|     }, |     }, | ||||||
|     defaultValue: 1, |  | ||||||
|     fieldName: 'status', |     fieldName: 'status', | ||||||
|     label: $t('system.menu.status'), |     label: '菜单状态', | ||||||
|  |     rules: z.number().default(CommonStatusEnum.ENABLE), | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     component: 'Select', |     component: 'RadioGroup', | ||||||
|  |     dependencies: { | ||||||
|  |       show: (values) => { | ||||||
|  |         return [SystemMenuTypeEnum.MENU].includes(values.type); | ||||||
|  |       }, | ||||||
|  |       triggerFields: ['type'], | ||||||
|  |     }, | ||||||
|     componentProps: { |     componentProps: { | ||||||
|       allowClear: true, |  | ||||||
|       class: 'w-full', |  | ||||||
|       options: [ |       options: [ | ||||||
|         { label: $t('system.menu.badgeType.dot'), value: 'dot' }, |         { label: '总是', value: true }, | ||||||
|         { label: $t('system.menu.badgeType.normal'), value: 'normal' }, |         { label: '不是', value: false }, | ||||||
|       ], |       ], | ||||||
|  |       buttonStyle: 'solid', | ||||||
|  |       optionType: 'button', | ||||||
|     }, |     }, | ||||||
|     dependencies: { |     fieldName: 'alwaysShow', | ||||||
|       show: (values) => { |     label: '总是显示', | ||||||
|         return values.type !== 'button'; |     rules: 'required', | ||||||
|       }, |     defaultValue: true, | ||||||
|       triggerFields: ['type'], |  | ||||||
|     }, |  | ||||||
|     fieldName: 'meta.badgeType', |  | ||||||
|     label: $t('system.menu.badgeType.title'), |  | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     component: 'Input', |     component: 'RadioGroup', | ||||||
|     componentProps: (values) => { |  | ||||||
|       return { |  | ||||||
|         allowClear: true, |  | ||||||
|         class: 'w-full', |  | ||||||
|         disabled: values.meta?.badgeType !== 'normal', |  | ||||||
|       }; |  | ||||||
|     }, |  | ||||||
|     dependencies: { |     dependencies: { | ||||||
|       show: (values) => { |       show: (values) => { | ||||||
|         return values.type !== 'button'; |         return [SystemMenuTypeEnum.MENU].includes(values.type); | ||||||
|       }, |       }, | ||||||
|       triggerFields: ['type'], |       triggerFields: ['type'], | ||||||
|     }, |     }, | ||||||
|     fieldName: 'meta.badge', |     componentProps: { | ||||||
|     label: $t('system.menu.badge'), |       options: [ | ||||||
|   }, |         { label: '缓存', value: true }, | ||||||
|   { |         { label: '不缓存', value: false }, | ||||||
|     component: 'Divider', |       ], | ||||||
|     dependencies: { |       buttonStyle: 'solid', | ||||||
|       show: (values) => { |       optionType: 'button', | ||||||
|         return !['button', 'link'].includes(values.type); |  | ||||||
|       }, |  | ||||||
|       triggerFields: ['type'], |  | ||||||
|     }, |  | ||||||
|     fieldName: 'divider1', |  | ||||||
|     formItemClass: 'col-span-2 md:col-span-2 pb-0', |  | ||||||
|     hideLabel: true, |  | ||||||
|     renderComponentContent() { |  | ||||||
|       return { |  | ||||||
|         default: () => $t('system.menu.advancedSettings'), |  | ||||||
|       }; |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     component: 'Checkbox', |  | ||||||
|     dependencies: { |  | ||||||
|       show: (values) => { |  | ||||||
|         return ['menu'].includes(values.type); |  | ||||||
|       }, |  | ||||||
|       triggerFields: ['type'], |  | ||||||
|     }, |  | ||||||
|     fieldName: 'meta.keepAlive', |  | ||||||
|     renderComponentContent() { |  | ||||||
|       return { |  | ||||||
|         default: () => $t('system.menu.keepAlive'), |  | ||||||
|       }; |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     component: 'Checkbox', |  | ||||||
|     dependencies: { |  | ||||||
|       show: (values) => { |  | ||||||
|         return ['embedded', 'menu'].includes(values.type); |  | ||||||
|       }, |  | ||||||
|       triggerFields: ['type'], |  | ||||||
|     }, |  | ||||||
|     fieldName: 'meta.affixTab', |  | ||||||
|     renderComponentContent() { |  | ||||||
|       return { |  | ||||||
|         default: () => $t('system.menu.affixTab'), |  | ||||||
|       }; |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     component: 'Checkbox', |  | ||||||
|     dependencies: { |  | ||||||
|       show: (values) => { |  | ||||||
|         return !['button'].includes(values.type); |  | ||||||
|       }, |  | ||||||
|       triggerFields: ['type'], |  | ||||||
|     }, |  | ||||||
|     fieldName: 'meta.hideInMenu', |  | ||||||
|     renderComponentContent() { |  | ||||||
|       return { |  | ||||||
|         default: () => $t('system.menu.hideInMenu'), |  | ||||||
|       }; |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     component: 'Checkbox', |  | ||||||
|     dependencies: { |  | ||||||
|       show: (values) => { |  | ||||||
|         return ['catalog', 'menu'].includes(values.type); |  | ||||||
|       }, |  | ||||||
|       triggerFields: ['type'], |  | ||||||
|     }, |  | ||||||
|     fieldName: 'meta.hideChildrenInMenu', |  | ||||||
|     renderComponentContent() { |  | ||||||
|       return { |  | ||||||
|         default: () => $t('system.menu.hideChildrenInMenu'), |  | ||||||
|       }; |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     component: 'Checkbox', |  | ||||||
|     dependencies: { |  | ||||||
|       show: (values) => { |  | ||||||
|         return !['button', 'link'].includes(values.type); |  | ||||||
|       }, |  | ||||||
|       triggerFields: ['type'], |  | ||||||
|     }, |  | ||||||
|     fieldName: 'meta.hideInBreadcrumb', |  | ||||||
|     renderComponentContent() { |  | ||||||
|       return { |  | ||||||
|         default: () => $t('system.menu.hideInBreadcrumb'), |  | ||||||
|       }; |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     component: 'Checkbox', |  | ||||||
|     dependencies: { |  | ||||||
|       show: (values) => { |  | ||||||
|         return !['button', 'link'].includes(values.type); |  | ||||||
|       }, |  | ||||||
|       triggerFields: ['type'], |  | ||||||
|     }, |  | ||||||
|     fieldName: 'meta.hideInTab', |  | ||||||
|     renderComponentContent() { |  | ||||||
|       return { |  | ||||||
|         default: () => $t('system.menu.hideInTab'), |  | ||||||
|       }; |  | ||||||
|     }, |     }, | ||||||
|  |     fieldName: 'keepAlive', | ||||||
|  |     label: '缓存状态', | ||||||
|  |     rules: 'required', | ||||||
|  |     defaultValue: true, | ||||||
|   }, |   }, | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
| const breakpoints = useBreakpoints(breakpointsTailwind); |  | ||||||
| const isHorizontal = computed(() => breakpoints.greaterOrEqual('md').value); |  | ||||||
| 
 |  | ||||||
| const [Form, formApi] = useVbenForm({ | const [Form, formApi] = useVbenForm({ | ||||||
|   commonConfig: { |   layout: 'horizontal', | ||||||
|     colon: true, |  | ||||||
|     formItemClass: 'col-span-2 md:col-span-1', |  | ||||||
|   }, |  | ||||||
|   schema, |   schema, | ||||||
|   showDefaultActions: false, |   showDefaultActions: false, | ||||||
|   wrapperClass: 'grid-cols-2 gap-x-4', |  | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| const [Drawer, drawerApi] = useVbenDrawer({ | const [Modal, modalApi] = useVbenModal({ | ||||||
|   onConfirm: onSubmit, |   async onConfirm() { | ||||||
|   onOpenChange(isOpen) { |     const { valid } = await formApi.validate(); | ||||||
|     if (isOpen) { |     if (!valid) { | ||||||
|       const data = drawerApi.getData<SystemMenuApi.SystemMenu>(); |       return; | ||||||
|       if (data?.type === 'link') { |  | ||||||
|         data.linkSrc = data.meta?.link; |  | ||||||
|       } else if (data?.type === 'embedded') { |  | ||||||
|         data.linkSrc = data.meta?.iframeSrc; |  | ||||||
|       } |  | ||||||
|       if (data) { |  | ||||||
|         formData.value = data; |  | ||||||
|         formApi.setValues(formData.value); |  | ||||||
|         titleSuffix.value = formData.value.meta?.title |  | ||||||
|           ? $t(formData.value.meta.title) |  | ||||||
|           : ''; |  | ||||||
|       } else { |  | ||||||
|         formApi.resetForm(); |  | ||||||
|         titleSuffix.value = ''; |  | ||||||
|     } |     } | ||||||
|  |     modalApi.lock(); | ||||||
|  |     const data = (await formApi.getValues()) as SystemMenuApi.SystemMenu; | ||||||
|  |     try { | ||||||
|  |       await (formData.value?.id ? updateMenu(data) : createMenu(data)); | ||||||
|  |       await modalApi.close(); | ||||||
|  |       emit('success'); | ||||||
|  |       message.success({ | ||||||
|  |         content: $t('ui.actionMessage.operationSuccess'), | ||||||
|  |         key: 'action_process_msg', | ||||||
|  |       }); | ||||||
|  |     } finally { | ||||||
|  |       modalApi.lock(false); | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
| }); |   async onOpenChange(isOpen) { | ||||||
| 
 |     if (!isOpen) { | ||||||
| async function onSubmit() { |       return; | ||||||
|   const { valid } = await formApi.validate(); |  | ||||||
|   if (valid) { |  | ||||||
|     drawerApi.lock(); |  | ||||||
|     const data = |  | ||||||
|       await formApi.getValues< |  | ||||||
|         Omit<SystemMenuApi.SystemMenu, 'children' | 'id'> |  | ||||||
|       >(); |  | ||||||
|     if (data.type === 'link') { |  | ||||||
|       data.meta = { ...data.meta, link: data.linkSrc }; |  | ||||||
|     } else if (data.type === 'embedded') { |  | ||||||
|       data.meta = { ...data.meta, iframeSrc: data.linkSrc }; |  | ||||||
|     } |     } | ||||||
|     delete data.linkSrc; |     let data = modalApi.getData<SystemMenuApi.SystemMenu>(); | ||||||
|  |     if (!data) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     if (data.id) { | ||||||
|  |       modalApi.lock(); | ||||||
|       try { |       try { | ||||||
|       await (formData.value?.id |         data = await getMenu(data.id as number); | ||||||
|         ? updateMenu(formData.value.id, data) |  | ||||||
|         : createMenu(data)); |  | ||||||
|       drawerApi.close(); |  | ||||||
|       emit('success'); |  | ||||||
|       } finally { |       } finally { | ||||||
|       drawerApi.unlock(); |         modalApi.lock(false); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| } |     formData.value = data; | ||||||
| const getDrawerTitle = computed(() => |     await formApi.setValues(formData.value); | ||||||
|   formData.value?.id |   }, | ||||||
|     ? $t('ui.actionTitle.edit', ['菜单']) | }); | ||||||
|     : $t('ui.actionTitle.create', ['菜单']), |  | ||||||
| ); |  | ||||||
| </script> | </script> | ||||||
| <template> | <template> | ||||||
|   <Drawer class="w-full max-w-[800px]" :title="getDrawerTitle"> |   <Modal :title="getTitle"> | ||||||
|     <Form class="mx-4" :layout="isHorizontal ? 'horizontal' : 'vertical'" /> |     <Form class="mx-4" /> | ||||||
|   </Drawer> |   </Modal> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	 YunaiV
						YunaiV