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

1 year ago
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>
)
}
}
})