diff --git a/apps/web-antd/src/api/system/menu/index.ts b/apps/web-antd/src/api/system/menu/index.ts new file mode 100644 index 00000000..7754e730 --- /dev/null +++ b/apps/web-antd/src/api/system/menu/index.ts @@ -0,0 +1,61 @@ +import type { Menu, MenuOption, MenuResp } from './model'; + +import type { ID, IDS, PageQuery } from '#/api/common'; + +import { requestClient } from '#/api/request'; + +enum Api { + menuList = '/system/menu/list', + menuTreeSelect = '/system/menu/treeselect', + roleMenuTree = '/system/menu/roleMenuTreeselect', + root = '/system/menu', + tenantPackageMenuTreeselect = '/system/menu/tenantPackageMenuTreeselect', +} + +export function menuList(params?: PageQuery) { + return requestClient.get(Api.menuList, { params }); +} + +export function menuInfo(menuId: ID) { + return requestClient.get(`${Api.root}/${menuId}`); +} + +export function menuAdd(data: any) { + return requestClient.postWithMsg(Api.root, data); +} + +export function menuUpdate(data: any) { + return requestClient.putWithMsg(Api.root, data); +} + +export function menuRemove(menuIds: IDS) { + return requestClient.deleteWithMsg(`${Api.root}/${menuIds}`); +} + +/** + * 返回对应角色的菜单 + * @param roleId id + * @returns resp + */ +export function roleMenuTreeSelect(roleId: ID) { + return requestClient.get(`${Api.roleMenuTree}/${roleId}`); +} + +/** + * 下拉框使用 返回所有的菜单 + * @returns [] + */ +export function menuTreeSelect() { + return requestClient.get(Api.menuTreeSelect); +} + +/** + * 租户套餐使用 + * @param packageId packageId + * @returns resp + */ +export function tenantPackageMenuTreeSelect(packageId: ID) { + return requestClient.get( + `${Api.tenantPackageMenuTreeselect}/${packageId}`, + ); +} diff --git a/apps/web-antd/src/api/system/menu/model.d.ts b/apps/web-antd/src/api/system/menu/model.d.ts new file mode 100644 index 00000000..0234425f --- /dev/null +++ b/apps/web-antd/src/api/system/menu/model.d.ts @@ -0,0 +1,46 @@ +export interface Menu { + createBy?: any; + createTime: string; + updateBy?: any; + updateTime?: any; + remark?: any; + menuId: number; + menuName: string; + parentName?: string; + parentId: number; + orderNum: number; + path: string; + component?: string; + query: string; + isFrame: string; + isCache: string; + menuType: string; + visible: string; + status: string; + perms: string; + icon: string; + children: Menu[]; +} + +/** + * @description 菜单信息 + * @param label 菜单名称 + */ +export interface MenuOption { + id: number; + parentId: number; + label: string; + weight: number; + children: MenuOption[]; + key: string; // 实际上不存在 ide报错 +} + +/** + * @description 菜单返回 + * @param checkedKeys 选中的菜单id + * @param menus 菜单信息 + */ +export interface MenuResp { + checkedKeys: number[]; + menus: MenuOption[]; +} diff --git a/apps/web-antd/src/views/system/menu/data.tsx b/apps/web-antd/src/views/system/menu/data.tsx new file mode 100644 index 00000000..426626bc --- /dev/null +++ b/apps/web-antd/src/views/system/menu/data.tsx @@ -0,0 +1,252 @@ +import { DictEnum } from '@vben/constants'; + +import { type FormSchemaGetter, z } from '#/adapter'; +import { getDictOptions } from '#/utils/dict'; + +// 菜单类型(M目录 C菜单 F按钮) +export const menuTypeOptions = [ + { label: '目录', value: 'M' }, + { label: '菜单', value: 'C' }, + { label: '按钮', value: 'F' }, +]; + +export const yesNoOptions = [ + { label: '是', value: '0' }, + { label: '否', value: '1' }, +]; + +export const drawerSchema: FormSchemaGetter = () => [ + { + component: 'Input', + dependencies: { + show: () => false, + triggerFields: [''], + }, + fieldName: 'menuId', + }, + { + component: 'TreeSelect', + componentProps: { + placeholder: '请选择', + }, + defaultValue: 0, + fieldName: 'parentId', + label: '上级菜单', + rules: 'selectRequired', + }, + { + component: 'RadioGroup', + componentProps: { + buttonStyle: 'solid', + options: menuTypeOptions, + optionType: 'button', + placeholder: '请选择', + }, + defaultValue: 'M', + dependencies: { + componentProps: (_, api) => { + // 切换时清空校验 + // 直接抄的源码 没有清空校验的方法 + Object.keys(api.errors.value).forEach((key) => { + api.setFieldError(key, undefined); + }); + return {}; + }, + triggerFields: ['menuType'], + }, + fieldName: 'menuType', + label: '菜单类型', + }, + { + component: 'Input', + componentProps: { + placeholder: '请输入', + }, + dependencies: { + // 类型不为按钮时显示 + show: (values) => values.menuType !== 'F', + triggerFields: ['menuType'], + }, + fieldName: 'icon', + help: '选择或者从 https://icon-sets.iconify.design/ 查找名称粘贴', + label: '菜单图标', + }, + { + component: 'Input', + componentProps: { + placeholder: '请输入', + }, + fieldName: 'name', + label: '菜单名称', + rules: 'required', + }, + { + component: 'InputNumber', + componentProps: { + placeholder: '请输入', + }, + fieldName: 'orderNum', + help: '排序, 数字越小越靠前', + label: '显示排序', + rules: 'required', + }, + { + component: 'Input', + componentProps: (model) => { + const placeholder = + model.isFrame === '0' + ? '填写链接地址http(s):// 使用新页面打开' + : '填写`路由地址`或者`链接地址` 链接默认使用内部iframe内嵌打开'; + return { + placeholder, + }; + }, + dependencies: { + rules: (model) => { + if (model.isFrame !== '0') { + return z + .string({ message: '请输入路由地址' }) + .refine((val) => !val.startsWith('/'), { + message: '路由地址不需要带/', + }); + } + // 为链接 + return z + .string({ message: '请输入链接地址' }) + .regex(/^https?:\/\//, { message: '请输入正确的链接地址' }); + }, + // 类型不为按钮时显示 + show: (values) => values.menuType !== 'F', + triggerFields: ['isFrame'], + }, + fieldName: 'path', + help: `路由地址不带/, 如: menu, user 链接为http(s)://开头 链接默认使用内部iframe打开, 可通过{是否外链}控制打开方式`, + label: '路由地址', + }, + { + component: 'Input', + componentProps: (model) => { + return { + // 为链接时组件disabled + disabled: model.isFrame === '0', + placeholder: '请输入', + }; + }, + defaultValue: '', + dependencies: { + rules: (model) => { + // 非链接时为必填项 + if (model.path && !/^https?:\/\//.test(model.path)) { + // TODO 有bug 不会显示此处的校验信息 + console.log('非链接时必填组件路径'); + return z.string({ message: '非链接时必填组件路径' }); + } + return z.string({ message: '请输入' }).optional(); + }, + // 类型为菜单时显示 + show: (values) => values.menuType === 'C', + triggerFields: ['menuType', 'path'], + }, + fieldName: 'component', + help: '填写./src/views下的组件路径, 如system/menu/index', + label: '组件路径', + }, + { + component: 'RadioGroup', + componentProps: { + buttonStyle: 'solid', + options: yesNoOptions, + optionType: 'button', + }, + defaultValue: '1', + dependencies: { + // 类型不为按钮时显示 + show: (values) => values.menuType !== 'F', + triggerFields: ['menuType'], + }, + fieldName: 'isFrame', + help: '外链为http(s)://开头 选择否时, 使用iframe从内部打开页面, 否则新窗口打开', + label: '是否外链', + }, + { + component: 'RadioGroup', + componentProps: { + buttonStyle: 'solid', + options: getDictOptions(DictEnum.SYS_SHOW_HIDE), + optionType: 'button', + }, + defaultValue: '0', + dependencies: { + // 类型不为按钮时显示 + show: (values) => values.menuType !== 'F', + triggerFields: ['menuType'], + }, + fieldName: 'visible', + help: '隐藏后不会出现在菜单栏, 但仍然可以访问', + label: '是否显示', + }, + { + component: 'RadioGroup', + componentProps: { + buttonStyle: 'solid', + options: getDictOptions(DictEnum.SYS_NORMAL_DISABLE), + optionType: 'button', + }, + defaultValue: '0', + dependencies: { + // 类型不为按钮时显示 + show: (values) => values.menuType !== 'F', + triggerFields: ['menuType'], + }, + fieldName: 'status', + help: '停用后不会出现在菜单栏, 也无法访问', + label: '菜单状态', + }, + { + component: 'Input', + componentProps: { + placeholder: '请输入', + }, + dependencies: { + // 类型为菜单/按钮时显示 + show: (values) => values.menuType !== 'M', + triggerFields: ['menuType'], + }, + fieldName: 'perms', + help: `控制器中定义的权限字符, 如: @SaCheckPermission("system:user:import")`, + label: '权限标识', + }, + { + component: 'Input', + componentProps: (model) => ({ + // 为链接时组件disabled + disabled: model.isFrame === '0', + placeholder: '请输入', + }), + dependencies: { + // 类型为菜单时显示 + show: (values) => values.menuType === 'C', + triggerFields: ['menuType'], + }, + fieldName: 'queryParam', + help: 'vue-router中的query属性, 如{"name": "xxx", "age": 16}', + label: '路由参数', + }, + { + component: 'RadioGroup', + componentProps: { + buttonStyle: 'solid', + options: yesNoOptions, + optionType: 'button', + }, + defaultValue: '0', + dependencies: { + // 类型为菜单时显示 + show: (values) => values.menuType === 'C', + triggerFields: ['menuType'], + }, + fieldName: 'isCache', + help: '路由的keepAlive属性', + label: '是否缓存', + }, +]; diff --git a/apps/web-antd/src/views/system/menu/index.vue b/apps/web-antd/src/views/system/menu/index.vue index 06372a15..7a3f2a7c 100644 --- a/apps/web-antd/src/views/system/menu/index.vue +++ b/apps/web-antd/src/views/system/menu/index.vue @@ -1,9 +1,24 @@ diff --git a/apps/web-antd/src/views/system/menu/menu-drawer.vue b/apps/web-antd/src/views/system/menu/menu-drawer.vue new file mode 100644 index 00000000..826fd6f1 --- /dev/null +++ b/apps/web-antd/src/views/system/menu/menu-drawer.vue @@ -0,0 +1,122 @@ + + +