import './BasicLayout.less' import { Layout } from 'ant-design-vue' import 'ant-design-vue/es/layout/style/index.less' import HeaderView, { headerViewProps } from './Header' import useMediaQuery from '../utils/hooks/useMediaQuery' import WrapContent from './WrapContent' import PageLoading from './components/PageLoading' import { getRender } from './utils' import SiderMenuWrapper from './components/SiderMenu' import { clearMenuItem } from './utils/utils' import { privateSiderMenuProps, siderMenuProps } from './components/SiderMenu/SiderMenu' import { transformRouteToMenuItem } from './utils/menuUtils' import { toRefs } from '@vueuse/core' import type { CSSProperties, PropType, Slots, VNode, ExtractPropTypes } from 'vue' import type { MenuDataItem, MessageDescriptor, WithFalse } from './types' import type { LocaleType } from './locales/types' import type { WaterMarkProps } from './components/WaterMark' import type { FooterRender, MenuRender, MultiTabRender } from './renderTypes' import type { VueNode, VueNodeOrRender } from '#/types' import { VueNodeOrRenderPropType } from '#/types' import type { RouteRecordRaw } from 'vue-router' import { getPrefixCls, routeContextInjectKey } from './RouteContext' import FooterView from './Footer' export type LayoutBreadcrumbProps = { minLength?: number } const basicLayoutProps = () => ({ ...privateSiderMenuProps(), // 侧边菜单属性 ...siderMenuProps(), // 头部相关属性 ...headerViewProps(), /** * 路由信息,用来渲染菜单面包屑 */ routes: Array as PropType, /** * 简约模式,设置了之后不渲染的任何 layout 的东西,但是会有 context,可以获取到当前菜单。 * * @example pure={true} */ pure: { type: Boolean, default: false }, /** * logo 的配置,可以配置url,React 组件 和 false * * @example 设置 logo 为网络地址 logo="https://avatars1.githubusercontent.com/u/8186664?s=460&v=4" * @example 设置 logo 为组件 logo={} * @example 设置 logo 为 false 不显示 logo logo={false} * @example 设置 logo 为 方法 logo={()=> } * */ logo: { type: VueNodeOrRenderPropType as PropType>, default: undefined }, /** * layout 的 loading 效果,设置完成之后只展示一个 loading */ loading: { type: Boolean, default: false }, /** * 当前语言 * * @description "zh-CN" | "zh-TW" | "en-US" | "it-IT" | "ko-KR" * @example 中文 layout="zh-CN" * @example 英文 layout="en-US" */ locale: { type: String as PropType, default: 'zh-CN' }, /** * @name layout 是严格受控的,可以 设置为 true,一直收起 * * @example collapsed={true} */ collapsed: { type: Boolean, default: undefined }, /** * 收起和展开的时候触发事件 * * @example onCollapse=(collapsed)=>{ setCollapsed(collapsed) }; */ onCollapse: Function as PropType<(collapsed: boolean) => void>, /** * 页脚的配置, 支持插槽 * * @example 不展示dom footerRender={false} * @example 使用 layout 的 DefaultFooter footerRender={() => (} */ footerRender: { type: VueNodeOrRenderPropType as PropType>, default: undefined }, /** * 处理 menuData 的数据,可以动态的控制数据 * @see 尽量不要用异步数据来处理,否则可能造成更新不及时,如果异步数据推荐使用 menu.request 和 params。 * * @example 删除一些菜单 menuDataRender=((menuData) => { return menuData.filter(item => item.name !== 'test') }) * @example 增加一些菜单 menuDataRender={(menuData) => { return menuData.concat({ path: '/test', name: '测试', icon: 'smile' }) }} * @example 修改菜单 menuDataRender={(menuData) => { return menuData.map(item => { if (item.name === 'test') { item.name = '测试' } return item }) }} * @example 打平数据 menuDataRender={(menuData) => { return menuData.reduce((pre, item) => { return pre.concat(item.children || []) }, []) }} */ menuDataRender: Function as PropType<(menuData: MenuDataItem[]) => MenuDataItem[]>, /** * 国际化消息处理 */ formatMessage: Function as PropType<(message: MessageDescriptor) => string>, /** * 是否禁用移动端模式 * * @see 有的管理系统不需要移动端模式,此属性设置为true即可 * @example disableMobile={true} */ disableMobile: { type: Boolean, default: false }, /** * content 的样式 * * @example 背景颜色为红色 contentStyle={{ backgroundColor: 'red '}} */ contentStyle: Object as PropType, /** * 取消 content的 margin * * @example 取消内容的 margin disableContentMargin={true} */ disableContentMargin: { type: Boolean, default: false }, /** 水印的相关配置 */ waterMarkProps: Object as PropType, /** 是否是子布局 */ isChildrenLayout: { type: Boolean, default: false } }) export type BasicLayoutProps = Partial>> const headerRender = ( props: BasicLayoutProps & { hasSiderMenu: boolean }, slots: Slots, matchMenuKeys: string[] = [] ): VNode | null => { if (props.headerRender === false || props.pure) { return null } return ( // @ts-ignore TODO {slots} ) } const footerRender = (props: BasicLayoutProps, slots: Slots): VueNode => { if (props.footerRender === false || props.pure) { return null } const render = getRender(props, slots, 'footerRender') if (render) { return render({ ...props }, {slots}) } return null } const renderSiderMenu = ( props: BasicLayoutProps, slots: Slots, matchMenuKeys: string[] = [] ): VueNodeOrRender => { // 指定了不渲染或者精简模式直接返回 null if (props.menuRender === false || props.pure) { return null } // 如果是顶部导航,且不是手机模式不渲染 if (props.layout === 'top' && !props.isMobile) { return null } let { menuData } = props /** 如果是分割菜单模式,需要专门实现一下 */ if (props.splitMenus && (props.openKeys !== false || props.layout === 'mix') && !props.isMobile) { const [key] = matchMenuKeys if (key) { menuData = props.menuData?.find(item => item.key === key)?.children || [] } else { menuData = [] } } // 这里走了可以少一次循环 const clearMenuData = clearMenuItem(menuData || []) if (clearMenuData && clearMenuData?.length < 1 && props.splitMenus) { return null } const defaultDom = ( {slots} ) const menuRender = getRender(props, slots, 'menuRender') return menuRender ? menuRender(props, defaultDom) : defaultDom } export default defineComponent({ name: 'BasicLayout', props: basicLayoutProps(), slots: ['default', 'logo', 'menuHeaderRender', 'menuFooterRender'], setup(props, { slots, attrs }) { const prefixCls = props.prefixCls ?? getPrefixCls('pro') const baseClassName = `${prefixCls}-basicLayout` // gen className const className = computed(() => [ 'ant-design-pro', baseClassName, { [`screen-${colSize.value}`]: colSize.value, [`${baseClassName}-top-menu`]: props.layout === 'top', [`${baseClassName}-fix-siderbar`]: props.fixSiderbar, [`${baseClassName}-${props.layout}`]: props.layout } ]) const contentClassName = computed(() => ({ [`${baseClassName}-content`]: true, [`${baseClassName}-has-header`]: !!headerDom.value, [`${baseClassName}-content-disable-margin`]: props.disableContentMargin })) // TODO,这里处理数据转换为题 const menuInfoData = computed(() => transformRouteToMenuItem(props.routes || [])) const colSize = useMediaQuery() const isMobile = computed(() => colSize.value === 'sm' || colSize.value === 'xs') // ToDo collapsed 的双向绑定 // siderMenuDom 为空的时候,不需要 padding const genLayoutStyle: CSSProperties = { position: 'relative' } // if is some layout children, don't need min height watchEffect(() => { if (props.isChildrenLayout || (props.contentStyle && props.contentStyle.minHeight)) { genLayoutStyle.minHeight = '0px' } }) // If it is a fix menu, calculate padding // don't need padding in phone mode const leftSiderWidth = computed(() => { const hasLeftPadding = props.layout !== 'top' && !isMobile.value if (hasLeftPadding) { return props.collapsed ? 48 : props.siderWidth } return 0 }) // render sider dom const siderMenuDom = computed(() => renderSiderMenu( { ...props, menuData: menuInfoData.value, isMobile: isMobile.value, theme: props.navTheme === 'dark' ? 'dark' : 'light', prefixCls: prefixCls }, slots, props.matchMenuKeys ) ) // render header dom const headerViewProps = computed(() => ({ ...props, hasSiderMenu: !!siderMenuDom.value, menuData: menuInfoData.value, isMobile: isMobile.value, theme: props.navTheme === 'dark' ? 'dark' : 'light', prefixCls: prefixCls, siderWidth: leftSiderWidth.value })) const headerDom = computed(() => headerRender(headerViewProps.value, slots, props.matchMenuKeys) ) // render footer dom const footerDom = computed(() => footerRender( { ...props, isMobile: isMobile.value, collapsed: props.collapsed }, slots ) ) const hasFooterToolbar = ref(false) const setHasFooterToolbar = (has: boolean) => { hasFooterToolbar.value = has } // TODO pick 属性,防止传递太多无效数据下去 const routeContext = reactive({ ...toRefs(props), // breadcrumb: breadcrumbProps, // @ts-ignore menuDa: menuInfoData.value!, isMobile: isMobile.value, collapsed: props.collapsed, isChildrenLayout: true, // title: pageTitleInfo.pageName, hasSiderMenu: !!siderMenuDom.value, hasHeader: !!headerDom.value, siderWidth: leftSiderWidth.value, hasFooter: !!footerDom.value, hasFooterToolbar: hasFooterToolbar.value, setHasFooterToolbar // matchMenus, // matchMenuKeys, // currentMenu, }) provide(routeContextInjectKey, routeContext) return () => { const multiTabRender = getRender(props, slots, 'multiTabRender') const multiTabDom = multiTabRender && multiTabRender(headerViewProps.value) return (
{siderMenuDom.value}
{headerDom.value} {multiTabDom} {/*@ts-ignore*/} {props.loading ? : slots.default?.()} {footerDom.value}
) } } })