You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

330 lines
11 KiB
TypeScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import './index.less'
import { menuProps } from 'ant-design-vue/es/menu/src/Menu'
import { pureSettingsProps, defaultSettings } from '../../defaultSettings'
import { isImg, isUrl } from '../../utils/checkUtils'
import { Menu, Skeleton } from 'ant-design-vue'
import Icon, { createFromIconfontCN } from '@ant-design/icons-vue'
import type { MenuDataItem, MessageDescriptor, WithFalse } from '../../types'
import type { MenuProps, MenuTheme } from 'ant-design-vue'
import type { MenuItemRender, SubMenuItemRender } from '../../renderTypes'
import type { PropType, ExtractPropTypes } from 'vue'
import type { SelectEventHandler, SelectInfo } from 'ant-design-vue/es/menu/src/interface'
import type { Key } from 'ant-design-vue/es/_util/type'
import type { RouteLocationNormalizedLoaded } from 'vue-router'
import omit from 'ant-design-vue/es/_util/omit'
import { WithFalseVueNodeOrRenderPropType } from '#/types'
import type { VueNodeOrRender } from '#/types'
import { redirectPath } from '@/config'
import AntIcon from '#/layout/components/AntIcon/index'
export const baseMenuProps = () => ({
...omit(menuProps(), ['openKeys', 'onOpenChange']),
...pureSettingsProps,
/** 默认的是否展开,会受到 breakpoint 的影响 */
defaultCollapsed: { type: Boolean, default: undefined },
collapsed: { type: Boolean, default: undefined },
splitMenus: { type: Boolean, default: undefined },
isMobile: { type: Boolean, default: undefined },
menuData: Array as PropType<MenuDataItem[]>,
onCollapse: Function as PropType<(collapsed: boolean) => void>,
openKeys: [Array, Boolean] as PropType<WithFalse<string[]> | undefined>,
handleOpenChange: Function as PropType<(openKeys: Key[]) => void>,
iconPrefixes: String,
/** 要给菜单的props, 参考antd-menu的属性。https://ant.design/components/menu-cn/ */
menuProps: Object as PropType<MenuProps>,
theme: String as PropType<MenuTheme>,
formatMessage: Function as PropType<(message: MessageDescriptor) => string>,
/**
* 处理父级菜单的 props可以复写菜单的点击功能一般用于埋点
* @see 子级的菜单要使用 menuItemRender 来处理
*
* @example 使用 a 标签跳转到特殊的地址 subMenuItemRender={(item, defaultDom) => { return <a onClick={()=> history.push(item.path) }>{defaultDom}</a> }}
* @example 增加埋点 subMenuItemRender={(item, defaultDom) => { return <a onClick={()=> log.click(item.name) }>{defaultDom}</a> }}
*/
subMenuItemRender: WithFalseVueNodeOrRenderPropType as PropType<SubMenuItemRender>,
/**
* 处理菜单的 props可以复写菜单的点击功能一般结合 Router 框架使用
* @see 非子级的菜单要使用 subMenuItemRender 来处理
*
* @example 使用 a 标签 menuItemRender={(item, defaultDom) => { return <a onClick={()=> history.push(item.path) }>{defaultDom}</a> }}
* @example 使用 Link 标签 menuItemRender={(item, defaultDom) => { return <Link to={item.path}>{defaultDom}</Link> }}
*/
menuItemRender: WithFalseVueNodeOrRenderPropType as PropType<MenuItemRender>,
/**
* 处理 menuData 的方法,与 menuDataRender 不同postMenuData处理完成后会直接渲染不再进行国际化和拼接处理
*
* @example 增加菜单图标 postMenuData={(menuData) => { return menuData.map(item => { return { ...item, icon: <Icon type={item.icon} /> } }) }}
*/
postMenuData: {
type: Function as PropType<(menusData?: MenuDataItem[]) => MenuDataItem[]>,
default: (data?: MenuDataItem[]) => data || []
}
})
export type BaseMenuProps = Partial<ExtractPropTypes<ReturnType<typeof baseMenuProps>>>
let IconFont = createFromIconfontCN({
scriptUrl: defaultSettings.iconfontUrl
})
// Allow menu.js config icon as string or ReactNode
// icon: 'setting',
// icon: 'icon-geren' #For Iconfont ,
// icon: 'http://demo.com/icon.png',
// icon: '/favicon.png',
// icon: <Icon type="setting" />,
const getIcon = (icon?: string | VueNodeOrRender, iconPrefixes = 'icon-'): VueNodeOrRender => {
if (typeof icon === 'string' && icon !== '') {
if (isUrl(icon) || isImg(icon)) {
return (
<Icon component={() => <img src={icon} alt="icon" class="ant-pro-sider-menu-icon" />} />
)
}
if (icon.startsWith(iconPrefixes)) {
return <IconFont type={icon} />
}
// @ts-ignore
return <AntIcon type={icon} />
}
return icon
}
class MenuUtil {
constructor(props: BaseMenuProps) {
this.props = props
}
props: BaseMenuProps
getNavMenuItems = (menusData: MenuDataItem[] = [], isChildren: boolean) =>
menusData.map(item => this.getSubMenuOrItem(item, isChildren)).filter(item => item)
/** Get SubMenu or Item */
getSubMenuOrItem = (item: MenuDataItem, isChildren: boolean): any => {
const children = item?.children || item?.routes
if (Array.isArray(children) && children.length > 0) {
const name = this.getIntlName(item)
const { subMenuItemRender, prefixCls, menu, iconPrefixes } = this.props
// get defaultTitle by menuItemRender
const defaultTitle = item.icon ? (
<span class={`${prefixCls}-menu-item`} title={name}>
{getIcon(item.icon, iconPrefixes)}
<span class={`${prefixCls}-menu-item-title`}>{name}</span>
</span>
) : (
<span class={`${prefixCls}-menu-item`} title={name}>
{name}
</span>
)
// subMenu only title render
const title = subMenuItemRender
? subMenuItemRender({ ...item, isUrl: false }, defaultTitle, this.props)
: defaultTitle
const MenuParent = menu?.type === 'group' ? Menu.ItemGroup : Menu.SubMenu
return (
<MenuParent key={item.key || item.path} title={title} onTitleClick={item.onTitleClick}>
{this.getNavMenuItems(children, true)}
</MenuParent>
)
}
return (
<Menu.Item
key={item.key || item.path}
disabled={item.disabled}
onClick={(e: Event) => {
if (isUrl(item?.path)) {
window.open(item.path, '_blank')
}
item.onTitleClick?.(e)
}}
>
{{
default: () => this.getMenuItemPath(item),
icon: () => getIcon(item.icon, this.props.iconPrefixes)
}}
</Menu.Item>
)
}
getIntlName = (item: MenuDataItem) => {
const { name, locale } = item
const { menu, formatMessage } = this.props
if (locale && menu?.locale !== false) {
return formatMessage?.({
id: locale,
defaultMessage: name
})
}
return name
}
/**
* 判断是否是http链接.返回 Link 或 a Judge whether it is http link.return a or Link
*
* @memberof SiderMenu
*/
getMenuItemPath = (item: MenuDataItem): VueNodeOrRender => {
const itemPath = this.conversionPath(item.path || '/')
// TODO 这个 location 的传递问题
//const { location = { pathname: '/' } } = this.props
const location = { pathname: '/' }
// if local is true formatMessage all name。
const name = this.getIntlName(item)
const { prefixCls } = this.props
const isHttpUrl = isUrl(itemPath)
const defaultItem = (
<span class={[`${prefixCls}-menu-item`, { [`${prefixCls}-menu-item-link`]: isHttpUrl }]}>
<span class={`${prefixCls}-menu-item-title`}>{name}</span>
</span>
)
if (this.props.menuItemRender) {
const renderItemProps = {
...item,
isUrl: isHttpUrl,
itemPath,
isMobile: this.props.isMobile,
replace: itemPath === location.pathname,
onClick: () => {
if (isHttpUrl) window.open(itemPath)
if (this.props.onCollapse) this.props.onCollapse(true)
},
children: undefined
}
return this.props.menuItemRender(renderItemProps, defaultItem, this.props)
}
return defaultItem
}
conversionPath = (path: string) => {
if (path && path.indexOf('http') === 0) {
return path
}
return `/${path || ''}`.replace(/\/+/g, '/')
}
}
function getOpenKeys(props: BaseMenuProps, route: RouteLocationNormalizedLoaded) {
// 折叠的时候,或者 top 菜单模式的时候openKeys 需要置空
if (!props.collapsed && ['side', 'mix'].includes(props.layout || 'mix')) {
return route.matched.filter(r => r.path !== route.path && r.path !== '/').map(r => r.path)
}
return []
}
export default defineComponent({
name: 'BaseMenu',
inheritAttrs: false,
props: baseMenuProps(),
setup(props, { attrs }) {
// TODO defaultOpenAll 支持,目前 react 版本的示例中是无效果的,所以这里暂时不处理
// const initOpenKeys = () => {
// if (props.menu?.defaultOpenAll) {
// return getOpenKeysFromMenuData(props.menuData) || []
// }
// return props.openKeys || []
// }
// 根据路由赋值当前选中和打开的菜单
const route = useRoute()
const localOpenKeys = ref<Key[]>([])
const localSelectedKeys = ref<string[]>([])
watchEffect(() => {
// 进行 redirect 的时候不处理,如果要把高级组件剥离,这个前缀不能写死需要透传过来
if (route.path.startsWith(redirectPath)) return
localOpenKeys.value = getOpenKeys(props, route)
localSelectedKeys.value = route.matched.filter(x => x.path !== '/').map(x => x.path)
})
console.log(props.menuData)
// 选中菜单的时候进行路由切换
const router = useRouter()
const handleSelect: SelectEventHandler = (args: SelectInfo): void => {
// 忽略外链类型
if (isUrl(args.key as string)) {
return
}
router.push(args.key as string)
localSelectedKeys.value = args.selectedKeys as string[]
// emit('update:selectedKeys', args.selectedKeys)
}
// 打开菜单时候的触发事件 TODO 支持排他展开
const defaultHandleOpenChange = (openKeys: Key[]) => {
localOpenKeys.value = openKeys
}
const handleOpenChange = props.handleOpenChange ?? defaultHandleOpenChange
watchEffect(() => {
// reset IconFont
if (props.iconfontUrl) {
IconFont = createFromIconfontCN({
scriptUrl: props.iconfontUrl
})
}
})
const cls = computed(() => [attrs.class, { 'top-nav-menu': props.mode === 'horizontal' }])
// sync props
const menuUtils = new MenuUtil(props)
return () => {
if (props.menu?.loading) {
return (
<div
style={
props.mode?.includes('inline')
? { padding: 24 }
: {
marginTop: 16
}
}
>
<Skeleton
active
title={false}
paragraph={{
rows: props.mode?.includes('inline') ? 6 : 1
}}
/>
</div>
)
}
const finallyData = props.postMenuData ? props.postMenuData(props.menuData) : props.menuData
if (finallyData && finallyData?.length < 1) {
return null
}
return (
<Menu
key="Menu"
mode={props.mode}
inlineIndent={16}
theme={props.theme}
style={attrs.style}
class={cls.value}
openKeys={localOpenKeys.value}
selectedKeys={localSelectedKeys.value}
onSelect={handleSelect}
onOpenChange={handleOpenChange}
{...props.menuProps}
>
{menuUtils.getNavMenuItems(finallyData, false)}
</Menu>
)
}
}
})