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.

384 lines
12 KiB
TypeScript

1 year ago
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<RouteRecordRaw[]>,
/**
* layout 西 context
*
* @example pure={true}
*/
pure: { type: Boolean, default: false },
/**
* logo urlReact false
*
* @example logo logo="https://avatars1.githubusercontent.com/u/8186664?s=460&v=4"
* @example logo logo={<img src="https://avatars1.githubusercontent.com/u/8186664?s=460&v=4"/>}
* @example logo false logo logo={false}
* @example logo logo={()=> <img src="https://avatars1.githubusercontent.com/u/8186664?s=460&v=4"/> }
* */
logo: {
type: VueNodeOrRenderPropType as PropType<WithFalse<VueNodeOrRender>>,
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<LocaleType>, 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={() => (<DefaultFooter copyright="这是一条测试文案"/>}
*/
footerRender: {
type: VueNodeOrRenderPropType as PropType<WithFalse<FooterRender>>,
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<CSSProperties>,
/**
* content margin
*
* @example margin disableContentMargin={true}
*/
disableContentMargin: { type: Boolean, default: false },
/** 水印的相关配置 */
waterMarkProps: Object as PropType<WaterMarkProps>,
/** 是否是子布局 */
isChildrenLayout: { type: Boolean, default: false }
})
export type BasicLayoutProps = Partial<ExtractPropTypes<ReturnType<typeof basicLayoutProps>>>
const headerRender = (
props: BasicLayoutProps & { hasSiderMenu: boolean },
slots: Slots,
matchMenuKeys: string[] = []
): VNode | null => {
if (props.headerRender === false || props.pure) {
return null
}
return (
// @ts-ignore TODO
<HeaderView matchMenuKeys={matchMenuKeys} {...props}>
{slots}
</HeaderView>
)
}
const footerRender = (props: BasicLayoutProps, slots: Slots): VueNode => {
if (props.footerRender === false || props.pure) {
return null
}
const render = getRender<FooterRender>(props, slots, 'footerRender')
if (render) {
return render({ ...props }, <FooterView>{slots}</FooterView>)
}
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 = (
<SiderMenuWrapper
matchMenuKeys={matchMenuKeys}
{...props}
style={
props.navTheme === 'realDark'
? {
boxShadow: '0 2px 8px 0 rgba(0, 0, 0, 65%)'
}
: {}
}
// 这里走了可以少一次循环
menuData={clearMenuData}
>
{slots}
</SiderMenuWrapper>
)
const menuRender = getRender<MenuRender>(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<BasicLayoutProps & { hasSiderMenu: boolean }>(() => ({
...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<MultiTabRender>(props, slots, 'multiTabRender')
const multiTabDom = multiTabRender && multiTabRender(headerViewProps.value)
return (
<div class={className.value}>
<Layout style={{ minHeight: '100%', ...(attrs.style as CSSProperties) }}>
{siderMenuDom.value}
<div style={genLayoutStyle} class={'ant-layout'}>
{headerDom.value}
{multiTabDom}
{/*@ts-ignore*/}
<WrapContent class={contentClassName.value} style={props.contentStyle}>
{props.loading ? <PageLoading /> : slots.default?.()}
</WrapContent>
{footerDom.value}
</div>
</Layout>
</div>
)
}
}
})