committed by
GitHub
18 changed files with 1328 additions and 11 deletions
@ -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); |
|||
}); |
|||
@ -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); |
|||
}); |
|||
@ -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); |
|||
}); |
|||
@ -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, |
|||
}; |
|||
@ -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, |
|||
}, |
|||
]; |
|||
} |
|||
@ -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> |
|||
@ -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