parent
1d8676f456
commit
5e421ce607
12
apps/backend-mock/api/system/menu/list.ts
Normal file
12
apps/backend-mock/api/system/menu/list.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||||
|
import { MOCK_MENU_LIST } from '~/utils/mock-data';
|
||||||
|
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
||||||
|
|
||||||
|
export default eventHandler(async (event) => {
|
||||||
|
const userinfo = verifyAccessToken(event);
|
||||||
|
if (!userinfo) {
|
||||||
|
return unAuthorizedResponse(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
return useResponseSuccess(MOCK_MENU_LIST);
|
||||||
|
});
|
28
apps/backend-mock/api/system/menu/name-exists.ts
Normal file
28
apps/backend-mock/api/system/menu/name-exists.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||||
|
import { MOCK_MENU_LIST } from '~/utils/mock-data';
|
||||||
|
import { unAuthorizedResponse } from '~/utils/response';
|
||||||
|
|
||||||
|
const namesMap: Record<string, any> = {};
|
||||||
|
|
||||||
|
function getNames(menus: any[]) {
|
||||||
|
menus.forEach((menu) => {
|
||||||
|
namesMap[menu.name] = String(menu.id);
|
||||||
|
if (menu.children) {
|
||||||
|
getNames(menu.children);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
getNames(MOCK_MENU_LIST);
|
||||||
|
|
||||||
|
export default eventHandler(async (event) => {
|
||||||
|
const userinfo = verifyAccessToken(event);
|
||||||
|
if (!userinfo) {
|
||||||
|
return unAuthorizedResponse(event);
|
||||||
|
}
|
||||||
|
const { id, name } = getQuery(event);
|
||||||
|
|
||||||
|
return (name as string) in namesMap &&
|
||||||
|
(!id || namesMap[name as string] !== String(id))
|
||||||
|
? useResponseSuccess(true)
|
||||||
|
: useResponseSuccess(false);
|
||||||
|
});
|
28
apps/backend-mock/api/system/menu/path-exists.ts
Normal file
28
apps/backend-mock/api/system/menu/path-exists.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||||
|
import { MOCK_MENU_LIST } from '~/utils/mock-data';
|
||||||
|
import { unAuthorizedResponse } from '~/utils/response';
|
||||||
|
|
||||||
|
const pathMap: Record<string, any> = { '/': 0 };
|
||||||
|
|
||||||
|
function getPaths(menus: any[]) {
|
||||||
|
menus.forEach((menu) => {
|
||||||
|
pathMap[menu.path] = String(menu.id);
|
||||||
|
if (menu.children) {
|
||||||
|
getPaths(menu.children);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
getPaths(MOCK_MENU_LIST);
|
||||||
|
|
||||||
|
export default eventHandler(async (event) => {
|
||||||
|
const userinfo = verifyAccessToken(event);
|
||||||
|
if (!userinfo) {
|
||||||
|
return unAuthorizedResponse(event);
|
||||||
|
}
|
||||||
|
const { id, path } = getQuery(event);
|
||||||
|
|
||||||
|
return (path as string) in pathMap &&
|
||||||
|
(!id || pathMap[path as string] !== String(id))
|
||||||
|
? useResponseSuccess(true)
|
||||||
|
: useResponseSuccess(false);
|
||||||
|
});
|
@ -185,3 +185,195 @@ export const MOCK_MENUS = [
|
|||||||
username: 'jack',
|
username: 'jack',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const MOCK_MENU_LIST = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Workspace',
|
||||||
|
status: 1,
|
||||||
|
type: 'menu',
|
||||||
|
icon: 'mdi:dashboard',
|
||||||
|
path: '/workspace',
|
||||||
|
component: '/dashboard/workspace/index',
|
||||||
|
meta: {
|
||||||
|
icon: 'carbon:workspace',
|
||||||
|
title: 'page.dashboard.workspace',
|
||||||
|
affixTab: true,
|
||||||
|
order: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
meta: {
|
||||||
|
icon: 'carbon:settings',
|
||||||
|
order: 9997,
|
||||||
|
title: 'system.title',
|
||||||
|
badge: 'new',
|
||||||
|
badgeType: 'normal',
|
||||||
|
badgeVariants: 'primary',
|
||||||
|
},
|
||||||
|
status: 1,
|
||||||
|
type: 'catalog',
|
||||||
|
name: 'System',
|
||||||
|
path: '/system',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 201,
|
||||||
|
pid: 2,
|
||||||
|
path: '/system/menu',
|
||||||
|
name: 'SystemMenu',
|
||||||
|
authCode: 'System:Menu:List',
|
||||||
|
status: 1,
|
||||||
|
type: 'menu',
|
||||||
|
meta: {
|
||||||
|
icon: 'carbon:menu',
|
||||||
|
title: 'system.menu.title',
|
||||||
|
},
|
||||||
|
component: '/system/menu/list',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 20_101,
|
||||||
|
pid: 201,
|
||||||
|
name: 'SystemMenuCreate',
|
||||||
|
status: 1,
|
||||||
|
type: 'button',
|
||||||
|
authCode: 'System:Menu:Create',
|
||||||
|
meta: { title: 'common.create' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 20_102,
|
||||||
|
pid: 201,
|
||||||
|
name: 'SystemMenuEdit',
|
||||||
|
status: 1,
|
||||||
|
type: 'button',
|
||||||
|
authCode: 'System:Menu:Edit',
|
||||||
|
meta: { title: 'common.edit' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 20_103,
|
||||||
|
pid: 201,
|
||||||
|
name: 'SystemMenuDelete',
|
||||||
|
status: 1,
|
||||||
|
type: 'button',
|
||||||
|
authCode: 'System:Menu:Delete',
|
||||||
|
meta: { title: 'common.delete' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 202,
|
||||||
|
pid: 2,
|
||||||
|
path: '/system/dept',
|
||||||
|
name: 'SystemDept',
|
||||||
|
status: 1,
|
||||||
|
type: 'menu',
|
||||||
|
authCode: 'System:Dept:List',
|
||||||
|
meta: {
|
||||||
|
icon: 'carbon:container-services',
|
||||||
|
title: 'system.dept.title',
|
||||||
|
},
|
||||||
|
component: '/system/dept/list',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 20_401,
|
||||||
|
pid: 201,
|
||||||
|
name: 'SystemDeptCreate',
|
||||||
|
status: 1,
|
||||||
|
type: 'button',
|
||||||
|
authCode: 'System:Dept:Create',
|
||||||
|
meta: { title: 'common.create' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 20_402,
|
||||||
|
pid: 201,
|
||||||
|
name: 'SystemDeptEdit',
|
||||||
|
status: 1,
|
||||||
|
type: 'button',
|
||||||
|
authCode: 'System:Dept:Edit',
|
||||||
|
meta: { title: 'common.edit' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 20_403,
|
||||||
|
pid: 201,
|
||||||
|
name: 'SystemDeptDelete',
|
||||||
|
status: 1,
|
||||||
|
type: 'button',
|
||||||
|
authCode: 'System:Dept:Delete',
|
||||||
|
meta: { title: 'common.delete' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
meta: {
|
||||||
|
badgeType: 'dot',
|
||||||
|
order: 9998,
|
||||||
|
title: 'demos.vben.title',
|
||||||
|
icon: 'carbon:data-center',
|
||||||
|
},
|
||||||
|
name: 'Project',
|
||||||
|
path: '/vben-admin',
|
||||||
|
type: 'catalog',
|
||||||
|
status: 1,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 901,
|
||||||
|
pid: 9,
|
||||||
|
name: 'VbenDocument',
|
||||||
|
path: '/vben-admin/document',
|
||||||
|
component: 'IFrameView',
|
||||||
|
type: 'embedded',
|
||||||
|
status: 1,
|
||||||
|
meta: {
|
||||||
|
icon: 'carbon:book',
|
||||||
|
iframeSrc: 'https://doc.vben.pro',
|
||||||
|
title: 'demos.vben.document',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 902,
|
||||||
|
pid: 9,
|
||||||
|
name: 'VbenGithub',
|
||||||
|
path: '/vben-admin/github',
|
||||||
|
component: 'IFrameView',
|
||||||
|
type: 'link',
|
||||||
|
status: 1,
|
||||||
|
meta: {
|
||||||
|
icon: 'carbon:logo-github',
|
||||||
|
link: 'https://github.com/vbenjs/vue-vben-admin',
|
||||||
|
title: 'Github',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 903,
|
||||||
|
pid: 9,
|
||||||
|
name: 'VbenAntdv',
|
||||||
|
path: '/vben-admin/antdv',
|
||||||
|
component: 'IFrameView',
|
||||||
|
type: 'link',
|
||||||
|
status: 0,
|
||||||
|
meta: {
|
||||||
|
icon: 'carbon:hexagon-vertical-solid',
|
||||||
|
badgeType: 'dot',
|
||||||
|
link: 'https://ant.vben.pro',
|
||||||
|
title: 'demos.vben.antdv',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
component: '_core/about/index',
|
||||||
|
type: 'menu',
|
||||||
|
status: 1,
|
||||||
|
meta: {
|
||||||
|
icon: 'lucide:copyright',
|
||||||
|
order: 9999,
|
||||||
|
title: 'demos.vben.about',
|
||||||
|
},
|
||||||
|
name: 'About',
|
||||||
|
path: '/about',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
@ -93,9 +93,9 @@ export class FormApi {
|
|||||||
return this.state;
|
return this.state;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getValues() {
|
async getValues<T = Recordable<any>>() {
|
||||||
const form = await this.getForm();
|
const form = await this.getForm();
|
||||||
return form.values ? this.handleRangeTimeValue(form.values) : {};
|
return (form.values ? this.handleRangeTimeValue(form.values) : {}) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
async isFieldValid(fieldName: string) {
|
async isFieldValid(fieldName: string) {
|
||||||
|
@ -16,5 +16,7 @@
|
|||||||
"disabled": "Disabled",
|
"disabled": "Disabled",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"create": "Create"
|
"create": "Create",
|
||||||
|
"yes": "Yes",
|
||||||
|
"no": "No"
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,10 @@
|
|||||||
"selectRequired": "Please select {0}",
|
"selectRequired": "Please select {0}",
|
||||||
"minLength": "{0} must be at least {1} characters",
|
"minLength": "{0} must be at least {1} characters",
|
||||||
"maxLength": "{0} can be at most {1} characters",
|
"maxLength": "{0} can be at most {1} characters",
|
||||||
"length": "{0} must be {1} characters long"
|
"length": "{0} must be {1} characters long",
|
||||||
|
"alreadyExists": "{0} `{1}` already exists",
|
||||||
|
"startWith": "{0} must start with `{1}`",
|
||||||
|
"invalidURL": "Please input a valid URL"
|
||||||
},
|
},
|
||||||
"actionTitle": {
|
"actionTitle": {
|
||||||
"edit": "Modify {0}",
|
"edit": "Modify {0}",
|
||||||
|
@ -16,5 +16,7 @@
|
|||||||
"disabled": "已禁用",
|
"disabled": "已禁用",
|
||||||
"edit": "修改",
|
"edit": "修改",
|
||||||
"delete": "删除",
|
"delete": "删除",
|
||||||
"create": "新增"
|
"create": "新增",
|
||||||
|
"yes": "是",
|
||||||
|
"no": "否"
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,10 @@
|
|||||||
"selectRequired": "请选择{0}",
|
"selectRequired": "请选择{0}",
|
||||||
"minLength": "{0}至少{1}个字符",
|
"minLength": "{0}至少{1}个字符",
|
||||||
"maxLength": "{0}最多{1}个字符",
|
"maxLength": "{0}最多{1}个字符",
|
||||||
"length": "{0}长度必须为{1}个字符"
|
"length": "{0}长度必须为{1}个字符",
|
||||||
|
"alreadyExists": "{0} `{1}` 已存在",
|
||||||
|
"startWith": "{0}必须以 {1} 开头",
|
||||||
|
"invalidURL": "请输入有效的链接"
|
||||||
},
|
},
|
||||||
"actionTitle": {
|
"actionTitle": {
|
||||||
"edit": "修改{0}",
|
"edit": "修改{0}",
|
||||||
|
@ -5,7 +5,7 @@ import { h } from 'vue';
|
|||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
import { $te } from '@vben/locales';
|
import { $te } from '@vben/locales';
|
||||||
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
|
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
|
||||||
import { isFunction, isString } from '@vben/utils';
|
import { get, isFunction, isString } from '@vben/utils';
|
||||||
|
|
||||||
import { objectOmit } from '@vueuse/core';
|
import { objectOmit } from '@vueuse/core';
|
||||||
import { Button, Image, Popconfirm, Tag } from 'ant-design-vue';
|
import { Button, Image, Popconfirm, Tag } from 'ant-design-vue';
|
||||||
@ -77,8 +77,8 @@ setupVbenVxeTable({
|
|||||||
// 单元格渲染: Tag
|
// 单元格渲染: Tag
|
||||||
vxeUI.renderer.add('CellTag', {
|
vxeUI.renderer.add('CellTag', {
|
||||||
renderTableDefault({ options, props }, { column, row }) {
|
renderTableDefault({ options, props }, { column, row }) {
|
||||||
const value = row[column.field];
|
const value = get(row, column.field);
|
||||||
const tagOptions = options || [
|
const tagOptions = options ?? [
|
||||||
{ color: 'success', label: $t('common.enabled'), value: 1 },
|
{ color: 'success', label: $t('common.enabled'), value: 1 },
|
||||||
{ color: 'error', label: $t('common.disabled'), value: 0 },
|
{ color: 'error', label: $t('common.disabled'), value: 0 },
|
||||||
];
|
];
|
||||||
@ -87,7 +87,7 @@ setupVbenVxeTable({
|
|||||||
Tag,
|
Tag,
|
||||||
{
|
{
|
||||||
...props,
|
...props,
|
||||||
...objectOmit(tagItem, ['label']),
|
...objectOmit(tagItem ?? {}, ['label']),
|
||||||
},
|
},
|
||||||
{ default: () => tagItem?.label ?? value },
|
{ default: () => tagItem?.label ?? value },
|
||||||
);
|
);
|
||||||
|
158
playground/src/api/system/menu.ts
Normal file
158
playground/src/api/system/menu.ts
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
import type { Recordable } from '@vben/types';
|
||||||
|
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
export namespace SystemMenuApi {
|
||||||
|
/** 徽标颜色集合 */
|
||||||
|
export const BadgeVariants = [
|
||||||
|
'default',
|
||||||
|
'destructive',
|
||||||
|
'primary',
|
||||||
|
'success',
|
||||||
|
'warning',
|
||||||
|
] as const;
|
||||||
|
/** 徽标类型集合 */
|
||||||
|
export const BadgeTypes = ['dot', 'normal'] as const;
|
||||||
|
/** 菜单类型集合 */
|
||||||
|
export const MenuTypes = [
|
||||||
|
'catalog',
|
||||||
|
'menu',
|
||||||
|
'embedded',
|
||||||
|
'link',
|
||||||
|
'button',
|
||||||
|
] as const;
|
||||||
|
/** 系统菜单 */
|
||||||
|
export interface SystemMenu {
|
||||||
|
[key: string]: any;
|
||||||
|
/** 后端权限标识 */
|
||||||
|
authCode: string;
|
||||||
|
/** 子级 */
|
||||||
|
children?: SystemMenu[];
|
||||||
|
/** 组件 */
|
||||||
|
component?: string;
|
||||||
|
/** 菜单ID */
|
||||||
|
id: string;
|
||||||
|
/** 菜单元数据 */
|
||||||
|
meta?: {
|
||||||
|
/** 激活时显示的图标 */
|
||||||
|
activeIcon?: string;
|
||||||
|
/** 作为路由时,需要激活的菜单的Path */
|
||||||
|
activePath?: string;
|
||||||
|
/** 固定在标签栏 */
|
||||||
|
affixTab?: boolean;
|
||||||
|
/** 在标签栏固定的顺序 */
|
||||||
|
affixTabOrder?: number;
|
||||||
|
/** 徽标内容(当徽标类型为normal时有效) */
|
||||||
|
badge?: string;
|
||||||
|
/** 徽标类型 */
|
||||||
|
badgeType?: (typeof BadgeTypes)[number];
|
||||||
|
/** 徽标颜色 */
|
||||||
|
badgeVariants?: (typeof BadgeVariants)[number];
|
||||||
|
/** 在菜单中隐藏下级 */
|
||||||
|
hideChildrenInMenu?: boolean;
|
||||||
|
/** 在面包屑中隐藏 */
|
||||||
|
hideInBreadcrumb?: boolean;
|
||||||
|
/** 在菜单中隐藏 */
|
||||||
|
hideInMenu?: boolean;
|
||||||
|
/** 在标签栏中隐藏 */
|
||||||
|
hideInTab?: boolean;
|
||||||
|
/** 菜单图标 */
|
||||||
|
icon?: string;
|
||||||
|
/** 内嵌Iframe的URL */
|
||||||
|
iframeSrc?: string;
|
||||||
|
/** 是否缓存页面 */
|
||||||
|
keepAlive?: boolean;
|
||||||
|
/** 外链页面的URL */
|
||||||
|
link?: string;
|
||||||
|
/** 同一个路由最大打开的标签数 */
|
||||||
|
maxNumOfOpenTab?: number;
|
||||||
|
/** 无需基础布局 */
|
||||||
|
noBasicLayout?: boolean;
|
||||||
|
/** 是否在新窗口打开 */
|
||||||
|
openInNewWindow?: boolean;
|
||||||
|
/** 菜单排序 */
|
||||||
|
order?: number;
|
||||||
|
/** 额外的路由参数 */
|
||||||
|
query?: Recordable<any>;
|
||||||
|
/** 菜单标题 */
|
||||||
|
title?: string;
|
||||||
|
};
|
||||||
|
/** 菜单名称 */
|
||||||
|
name: string;
|
||||||
|
/** 路由路径 */
|
||||||
|
path: string;
|
||||||
|
/** 父级ID */
|
||||||
|
pid: string;
|
||||||
|
/** 重定向 */
|
||||||
|
redirect?: string;
|
||||||
|
/** 菜单类型 */
|
||||||
|
type: (typeof MenuTypes)[number];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取菜单数据列表
|
||||||
|
*/
|
||||||
|
async function getMenuList() {
|
||||||
|
return requestClient.get<Array<SystemMenuApi.SystemMenu>>(
|
||||||
|
'/system/menu/list',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isMenuNameExists(
|
||||||
|
name: string,
|
||||||
|
id?: SystemMenuApi.SystemMenu['id'],
|
||||||
|
) {
|
||||||
|
return requestClient.get<boolean>('/system/menu/name-exists', {
|
||||||
|
params: { id, name },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isMenuPathExists(
|
||||||
|
path: string,
|
||||||
|
id?: SystemMenuApi.SystemMenu['id'],
|
||||||
|
) {
|
||||||
|
return requestClient.get<boolean>('/system/menu/path-exists', {
|
||||||
|
params: { id, path },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建菜单
|
||||||
|
* @param data 菜单数据
|
||||||
|
*/
|
||||||
|
async function createMenu(
|
||||||
|
data: Omit<SystemMenuApi.SystemMenu, 'children' | 'id'>,
|
||||||
|
) {
|
||||||
|
return requestClient.post('/system/menu', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新菜单
|
||||||
|
*
|
||||||
|
* @param id 菜单 ID
|
||||||
|
* @param data 菜单数据
|
||||||
|
*/
|
||||||
|
async function updateMenu(
|
||||||
|
id: string,
|
||||||
|
data: Omit<SystemMenuApi.SystemMenu, 'children' | 'id'>,
|
||||||
|
) {
|
||||||
|
return requestClient.put(`/system/menu/${id}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除菜单
|
||||||
|
* @param id 菜单 ID
|
||||||
|
*/
|
||||||
|
async function deleteMenu(id: string) {
|
||||||
|
return requestClient.delete(`/system/menu/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
createMenu,
|
||||||
|
deleteMenu,
|
||||||
|
getMenuList,
|
||||||
|
isMenuNameExists,
|
||||||
|
isMenuPathExists,
|
||||||
|
updateMenu,
|
||||||
|
};
|
@ -9,5 +9,44 @@
|
|||||||
"remark": "Remark",
|
"remark": "Remark",
|
||||||
"operation": "Operation",
|
"operation": "Operation",
|
||||||
"parentDept": "Parent Department"
|
"parentDept": "Parent Department"
|
||||||
|
},
|
||||||
|
"menu": {
|
||||||
|
"title": "Menu Management",
|
||||||
|
"parent": "Parent Menu",
|
||||||
|
"menuTitle": "Title",
|
||||||
|
"menuName": "Menu Name",
|
||||||
|
"name": "Menu",
|
||||||
|
"type": "Type",
|
||||||
|
"typeCatalog": "Catalog",
|
||||||
|
"typeMenu": "Menu",
|
||||||
|
"typeButton": "Button",
|
||||||
|
"typeLink": "Link",
|
||||||
|
"typeEmbedded": "Embedded",
|
||||||
|
"icon": "Icon",
|
||||||
|
"activeIcon": "Active Icon",
|
||||||
|
"activePath": "Active Path",
|
||||||
|
"path": "Route Path",
|
||||||
|
"component": "Component",
|
||||||
|
"status": "Status",
|
||||||
|
"authCode": "Auth Code",
|
||||||
|
"badge": "Badge",
|
||||||
|
"operation": "Operation",
|
||||||
|
"linkSrc": "Link Address",
|
||||||
|
"affixTab": "Affix In Tabs",
|
||||||
|
"keepAlive": "Keep Alive",
|
||||||
|
"hideInMenu": "Hide In Menu",
|
||||||
|
"hideInTab": "Hide In Tabbar",
|
||||||
|
"hideChildrenInMenu": "Hide Children In Menu",
|
||||||
|
"hideInBreadcrumb": "Hide In Breadcrumb",
|
||||||
|
"advancedSettings": "Other Settings",
|
||||||
|
"activePathMustExist": "The path could not find a valid menu",
|
||||||
|
"activePathHelp": "When jumping to the current route, \nthe menu path that needs to be activated must be specified when it does not display in the navigation menu.",
|
||||||
|
"badgeType": {
|
||||||
|
"title": "Badge Type",
|
||||||
|
"dot": "Dot",
|
||||||
|
"normal": "Text",
|
||||||
|
"none": "None"
|
||||||
|
},
|
||||||
|
"badgeVariants": "Badge Style"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,5 +9,44 @@
|
|||||||
"status": "状态",
|
"status": "状态",
|
||||||
"title": "部门管理"
|
"title": "部门管理"
|
||||||
},
|
},
|
||||||
|
"menu": {
|
||||||
|
"activeIcon": "激活图标",
|
||||||
|
"activePath": "激活路径",
|
||||||
|
"activePathHelp": "跳转到当前路由时,需要激活的菜单路径。\n当不在导航菜单中显示时,需要指定激活路径",
|
||||||
|
"activePathMustExist": "该路径未能找到有效的菜单",
|
||||||
|
"advancedSettings": "其它设置",
|
||||||
|
"affixTab": "固定在标签",
|
||||||
|
"authCode": "权限标识",
|
||||||
|
"badge": "徽章内容",
|
||||||
|
"badgeVariants": "徽标样式",
|
||||||
|
"badgeType": {
|
||||||
|
"dot": "点",
|
||||||
|
"none": "无",
|
||||||
|
"normal": "文字",
|
||||||
|
"title": "徽标类型"
|
||||||
|
},
|
||||||
|
"component": "页面组件",
|
||||||
|
"hideChildrenInMenu": "隐藏子菜单",
|
||||||
|
"hideInBreadcrumb": "在面包屑中隐藏",
|
||||||
|
"hideInMenu": "隐藏菜单",
|
||||||
|
"hideInTab": "在标签栏中隐藏",
|
||||||
|
"icon": "图标",
|
||||||
|
"keepAlive": "缓存标签页",
|
||||||
|
"linkSrc": "链接地址",
|
||||||
|
"menuName": "菜单名称",
|
||||||
|
"menuTitle": "标题",
|
||||||
|
"name": "菜单",
|
||||||
|
"operation": "操作",
|
||||||
|
"parent": "上级菜单",
|
||||||
|
"path": "路由地址",
|
||||||
|
"status": "状态",
|
||||||
|
"title": "菜单管理",
|
||||||
|
"type": "类型",
|
||||||
|
"typeButton": "按钮",
|
||||||
|
"typeCatalog": "目录",
|
||||||
|
"typeEmbedded": "内嵌",
|
||||||
|
"typeLink": "外链",
|
||||||
|
"typeMenu": "菜单"
|
||||||
|
},
|
||||||
"title": "系统管理"
|
"title": "系统管理"
|
||||||
}
|
}
|
||||||
|
@ -34,4 +34,14 @@ const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
|
|||||||
|
|
||||||
/** 有权限校验的路由列表,包含动态路由和静态路由 */
|
/** 有权限校验的路由列表,包含动态路由和静态路由 */
|
||||||
const accessRoutes = [...dynamicRoutes, ...staticRoutes];
|
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 };
|
||||||
|
@ -12,6 +12,15 @@ const routes: RouteRecordRaw[] = [
|
|||||||
name: 'System',
|
name: 'System',
|
||||||
path: '/system',
|
path: '/system',
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
path: '/system/menu',
|
||||||
|
name: 'SystemMenu',
|
||||||
|
meta: {
|
||||||
|
icon: 'mdi:menu',
|
||||||
|
title: $t('system.menu.title'),
|
||||||
|
},
|
||||||
|
component: () => import('#/views/system/menu/list.vue'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/system/dept',
|
path: '/system/dept',
|
||||||
name: 'SystemDept',
|
name: 'SystemDept',
|
||||||
|
109
playground/src/views/system/menu/data.ts
Normal file
109
playground/src/views/system/menu/data.ts
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { SystemMenuApi } from '#/api/system/menu';
|
||||||
|
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
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 useColumns(
|
||||||
|
onActionClick: OnActionClickFn<SystemMenuApi.SystemMenu>,
|
||||||
|
): VxeTableGridOptions<SystemMenuApi.SystemMenu>['columns'] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
align: 'left',
|
||||||
|
field: 'meta.title',
|
||||||
|
fixed: 'left',
|
||||||
|
slots: { default: 'title' },
|
||||||
|
title: $t('system.menu.menuTitle'),
|
||||||
|
treeNode: true,
|
||||||
|
width: 250,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
align: 'center',
|
||||||
|
cellRender: { name: 'CellTag', options: getMenuTypeOptions() },
|
||||||
|
field: 'type',
|
||||||
|
title: $t('system.menu.type'),
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'authCode',
|
||||||
|
title: $t('system.menu.authCode'),
|
||||||
|
width: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
align: 'left',
|
||||||
|
field: 'path',
|
||||||
|
title: $t('system.menu.path'),
|
||||||
|
width: 200,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
align: 'left',
|
||||||
|
field: 'component',
|
||||||
|
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,
|
||||||
|
title: $t('system.menu.component'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cellRender: { name: 'CellTag' },
|
||||||
|
field: 'status',
|
||||||
|
title: $t('system.menu.status'),
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
align: 'right',
|
||||||
|
cellRender: {
|
||||||
|
attrs: {
|
||||||
|
nameField: 'name',
|
||||||
|
onClick: onActionClick,
|
||||||
|
},
|
||||||
|
name: 'CellOperation',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
code: 'append',
|
||||||
|
text: '新增下级',
|
||||||
|
},
|
||||||
|
'edit', // 默认的编辑按钮
|
||||||
|
'delete', // 默认的删除按钮
|
||||||
|
],
|
||||||
|
},
|
||||||
|
field: 'operation',
|
||||||
|
fixed: 'right',
|
||||||
|
headerAlign: 'center',
|
||||||
|
showOverflow: false,
|
||||||
|
title: $t('system.menu.operation'),
|
||||||
|
width: 200,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
162
playground/src/views/system/menu/list.vue
Normal file
162
playground/src/views/system/menu/list.vue
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type {
|
||||||
|
OnActionClickParams,
|
||||||
|
VxeTableGridOptions,
|
||||||
|
} from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { Page, useVbenDrawer } from '@vben/common-ui';
|
||||||
|
import { IconifyIcon, Plus } from '@vben/icons';
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
import { MenuBadge } from '@vben-core/menu-ui';
|
||||||
|
|
||||||
|
import { Button, message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { deleteMenu, getMenuList, SystemMenuApi } from '#/api/system/menu';
|
||||||
|
|
||||||
|
import { useColumns } from './data';
|
||||||
|
import Form from './modules/form.vue';
|
||||||
|
|
||||||
|
const [FormDrawer, formDrawerApi] = useVbenDrawer({
|
||||||
|
connectedComponent: Form,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
gridOptions: {
|
||||||
|
columns: useColumns(onActionClick),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
pagerConfig: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async (_params) => {
|
||||||
|
return await getMenuList();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
custom: true,
|
||||||
|
export: false,
|
||||||
|
refresh: { code: 'query' },
|
||||||
|
zoom: true,
|
||||||
|
},
|
||||||
|
treeConfig: {
|
||||||
|
parentField: 'pid',
|
||||||
|
rowField: 'id',
|
||||||
|
transform: false,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
function onActionClick({
|
||||||
|
code,
|
||||||
|
row,
|
||||||
|
}: OnActionClickParams<SystemMenuApi.SystemMenu>) {
|
||||||
|
switch (code) {
|
||||||
|
case 'append': {
|
||||||
|
onAppend(row);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
onDelete(row);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'edit': {
|
||||||
|
onEdit(row);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRefresh() {
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
function onEdit(row: SystemMenuApi.SystemMenu) {
|
||||||
|
formDrawerApi.setData(row).open();
|
||||||
|
}
|
||||||
|
function onCreate() {
|
||||||
|
formDrawerApi.setData({}).open();
|
||||||
|
}
|
||||||
|
function onAppend(row: SystemMenuApi.SystemMenu) {
|
||||||
|
formDrawerApi.setData({ pid: row.id }).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDelete(row: SystemMenuApi.SystemMenu) {
|
||||||
|
const hideLoading = message.loading({
|
||||||
|
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||||
|
duration: 0,
|
||||||
|
key: 'action_process_msg',
|
||||||
|
});
|
||||||
|
deleteMenu(row.id)
|
||||||
|
.then(() => {
|
||||||
|
message.success({
|
||||||
|
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
|
||||||
|
key: 'action_process_msg',
|
||||||
|
});
|
||||||
|
onRefresh();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
hideLoading();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<FormDrawer @success="onRefresh" />
|
||||||
|
<Grid>
|
||||||
|
<template #toolbar-tools>
|
||||||
|
<Button type="primary" @click="onCreate">
|
||||||
|
<Plus class="size-5" />
|
||||||
|
{{ $t('ui.actionTitle.create', [$t('system.menu.name')]) }}
|
||||||
|
</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 === 'button'"
|
||||||
|
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>
|
||||||
|
<MenuBadge
|
||||||
|
v-if="row.meta?.badgeType"
|
||||||
|
class="menu-badge"
|
||||||
|
:badge="row.meta.badge"
|
||||||
|
:badge-type="row.meta.badgeType"
|
||||||
|
:badge-variants="row.meta.badgeVariants"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.menu-badge {
|
||||||
|
top: 50%;
|
||||||
|
right: 0;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
|
||||||
|
& > :deep(div) {
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
521
playground/src/views/system/menu/modules/form.vue
Normal file
521
playground/src/views/system/menu/modules/form.vue
Normal file
@ -0,0 +1,521 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { ChangeEvent } from 'ant-design-vue/es/_util/EventInterface';
|
||||||
|
|
||||||
|
import type { Recordable } from '@vben/types';
|
||||||
|
|
||||||
|
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 { componentKeys } from '#/router/routes';
|
||||||
|
|
||||||
|
import { getMenuTypeOptions } from '../data';
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
success: [];
|
||||||
|
}>();
|
||||||
|
const formData = ref<SystemMenuApi.SystemMenu>();
|
||||||
|
const loading = ref(false);
|
||||||
|
const titleSuffix = ref<string>();
|
||||||
|
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',
|
||||||
|
componentProps: {
|
||||||
|
api: getMenuList,
|
||||||
|
class: 'w-full',
|
||||||
|
filterTreeNode(input: string, node: Recordable<any>) {
|
||||||
|
if (!input || input.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const title: string = node.meta?.title ?? '';
|
||||||
|
if (!title) return false;
|
||||||
|
return title.includes(input) || $t(title).includes(input);
|
||||||
|
},
|
||||||
|
getPopupContainer,
|
||||||
|
labelField: 'meta.title',
|
||||||
|
showSearch: true,
|
||||||
|
treeDefaultExpandAll: true,
|
||||||
|
valueField: 'id',
|
||||||
|
childrenField: 'children',
|
||||||
|
},
|
||||||
|
fieldName: 'pid',
|
||||||
|
label: $t('system.menu.parent'),
|
||||||
|
renderComponentContent() {
|
||||||
|
return {
|
||||||
|
title({ label, meta }: { label: string; meta: Recordable<any> }) {
|
||||||
|
const coms = [];
|
||||||
|
if (!label) return '';
|
||||||
|
if (meta?.icon) {
|
||||||
|
coms.push(h(IconifyIcon, { class: 'size-4', icon: meta.icon }));
|
||||||
|
}
|
||||||
|
coms.push(h('span', { class: '' }, $t(label || '')));
|
||||||
|
return h('div', { class: 'flex items-center gap-1' }, coms);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
componentProps() {
|
||||||
|
// 不需要处理多语言时就无需这么做
|
||||||
|
return {
|
||||||
|
addonAfter: titleSuffix.value,
|
||||||
|
onChange({ target: { value } }: ChangeEvent) {
|
||||||
|
titleSuffix.value = value && $te(value) ? $t(value) : undefined;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
fieldName: 'meta.title',
|
||||||
|
label: $t('system.menu.menuTitle'),
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
dependencies: {
|
||||||
|
show: (values) => {
|
||||||
|
return ['catalog', 'embedded', 'menu'].includes(values.type);
|
||||||
|
},
|
||||||
|
triggerFields: ['type'],
|
||||||
|
},
|
||||||
|
fieldName: 'path',
|
||||||
|
label: $t('system.menu.path'),
|
||||||
|
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',
|
||||||
|
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: {
|
||||||
|
prefix: 'carbon',
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
show: (values) => {
|
||||||
|
return ['catalog', 'embedded', 'link', 'menu'].includes(values.type);
|
||||||
|
},
|
||||||
|
triggerFields: ['type'],
|
||||||
|
},
|
||||||
|
fieldName: 'meta.icon',
|
||||||
|
label: $t('system.menu.icon'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
class: 'w-full',
|
||||||
|
filterOption(input: string, option: { value: string }) {
|
||||||
|
return option.value.toLowerCase().includes(input.toLowerCase());
|
||||||
|
},
|
||||||
|
options: componentKeys.map((v) => ({ value: v })),
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
rules: (values) => {
|
||||||
|
return values.type === 'menu' ? 'required' : null;
|
||||||
|
},
|
||||||
|
show: (values) => {
|
||||||
|
return values.type === 'menu';
|
||||||
|
},
|
||||||
|
triggerFields: ['type'],
|
||||||
|
},
|
||||||
|
fieldName: 'component',
|
||||||
|
label: $t('system.menu.component'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
dependencies: {
|
||||||
|
show: (values) => {
|
||||||
|
return ['embedded', 'link'].includes(values.type);
|
||||||
|
},
|
||||||
|
triggerFields: ['type'],
|
||||||
|
},
|
||||||
|
fieldName: 'linkSrc',
|
||||||
|
label: $t('system.menu.linkSrc'),
|
||||||
|
rules: z.string().url($t('ui.formRules.invalidURL')),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
dependencies: {
|
||||||
|
rules: (values) => {
|
||||||
|
return values.type === 'button' ? 'required' : null;
|
||||||
|
},
|
||||||
|
show: (values) => {
|
||||||
|
return ['button', 'catalog', 'embedded', 'menu'].includes(values.type);
|
||||||
|
},
|
||||||
|
triggerFields: ['type'],
|
||||||
|
},
|
||||||
|
fieldName: 'authCode',
|
||||||
|
label: $t('system.menu.authCode'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'RadioGroup',
|
||||||
|
componentProps: {
|
||||||
|
buttonStyle: 'solid',
|
||||||
|
options: [
|
||||||
|
{ label: $t('common.enabled'), value: 1 },
|
||||||
|
{ label: $t('common.disabled'), value: 0 },
|
||||||
|
],
|
||||||
|
optionType: 'button',
|
||||||
|
},
|
||||||
|
defaultValue: 1,
|
||||||
|
fieldName: 'status',
|
||||||
|
label: $t('system.menu.status'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
class: 'w-full',
|
||||||
|
options: [
|
||||||
|
{ label: $t('system.menu.badgeType.dot'), value: 'dot' },
|
||||||
|
{ label: $t('system.menu.badgeType.normal'), value: 'normal' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
show: (values) => {
|
||||||
|
return values.type !== 'button';
|
||||||
|
},
|
||||||
|
triggerFields: ['type'],
|
||||||
|
},
|
||||||
|
fieldName: 'meta.badgeType',
|
||||||
|
label: $t('system.menu.badgeType.title'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: (values) => {
|
||||||
|
return {
|
||||||
|
allowClear: true,
|
||||||
|
class: 'w-full',
|
||||||
|
disabled: values.meta?.badgeType !== 'normal',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
show: (values) => {
|
||||||
|
return values.type !== 'button';
|
||||||
|
},
|
||||||
|
triggerFields: ['type'],
|
||||||
|
},
|
||||||
|
fieldName: 'meta.badge',
|
||||||
|
label: $t('system.menu.badge'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
class: 'w-full',
|
||||||
|
options: SystemMenuApi.BadgeVariants.map((v) => ({
|
||||||
|
label: v,
|
||||||
|
value: v,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
show: (values) => {
|
||||||
|
return values.type !== 'button';
|
||||||
|
},
|
||||||
|
triggerFields: ['type'],
|
||||||
|
},
|
||||||
|
fieldName: 'meta.badgeVariants',
|
||||||
|
label: $t('system.menu.badgeVariants'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Divider',
|
||||||
|
dependencies: {
|
||||||
|
show: (values) => {
|
||||||
|
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'),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const breakpoints = useBreakpoints(breakpointsTailwind);
|
||||||
|
const isHorizontal = computed(() => breakpoints.greaterOrEqual('md').value);
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
colon: true,
|
||||||
|
formItemClass: 'col-span-2 md:col-span-1',
|
||||||
|
},
|
||||||
|
schema,
|
||||||
|
showDefaultActions: false,
|
||||||
|
wrapperClass: 'grid-cols-2 gap-x-4',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Drawer, drawerApi] = useVbenDrawer({
|
||||||
|
onBeforeClose() {
|
||||||
|
if (loading.value) return false;
|
||||||
|
},
|
||||||
|
onConfirm: onSubmit,
|
||||||
|
onOpenChange(isOpen) {
|
||||||
|
if (isOpen) {
|
||||||
|
const data = drawerApi.getData<SystemMenuApi.SystemMenu>();
|
||||||
|
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 = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
async function onSubmit() {
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
if (valid) {
|
||||||
|
loading.value = true;
|
||||||
|
drawerApi.setState({
|
||||||
|
closeOnClickModal: false,
|
||||||
|
closeOnPressEscape: false,
|
||||||
|
confirmLoading: true,
|
||||||
|
loading: true,
|
||||||
|
});
|
||||||
|
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;
|
||||||
|
try {
|
||||||
|
await (formData.value?.id
|
||||||
|
? updateMenu(formData.value.id, data)
|
||||||
|
: createMenu(data));
|
||||||
|
drawerApi.close();
|
||||||
|
emit('success');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
drawerApi.setState({
|
||||||
|
closeOnClickModal: true,
|
||||||
|
closeOnPressEscape: true,
|
||||||
|
confirmLoading: false,
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const getDrawerTitle = computed(() =>
|
||||||
|
formData.value?.id
|
||||||
|
? $t('ui.actionTitle.edit', [$t('system.menu.name')])
|
||||||
|
: $t('ui.actionTitle.create', [$t('system.menu.name')]),
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Drawer class="w-full max-w-[800px]" :title="getDrawerTitle">
|
||||||
|
<Form class="mx-4" :layout="isHorizontal ? 'horizontal' : 'vertical'" />
|
||||||
|
</Drawer>
|
||||||
|
</template>
|
Loading…
Reference in New Issue
Block a user