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.

386 lines
12 KiB
TypeScript

1 year ago
import './index.less'
import { PageHeader, Tabs, Affix, Breadcrumb } from 'ant-design-vue'
import 'ant-design-vue/es/page-header/style/index.less'
import 'ant-design-vue/es/tabs/style/index.less'
import 'ant-design-vue/es/affix/style/index.less'
import 'ant-design-vue/es/breadcrumb/style/index.less'
import type {
TabsProps,
AffixProps,
PageHeaderProps,
TabPaneProps,
SpinProps,
BreadcrumbProps
} from 'ant-design-vue'
import GridContent from '../GridContent'
import FooterToolbar from '../FooterToolbar'
import PageLoading from '../PageLoading'
import type { WithFalse } from '../../types'
import type { WaterMarkProps } from '../WaterMark'
import WaterMark from '../WaterMark'
import type { CSSProperties, ExtractPropTypes, FunctionalComponent, PropType } from 'vue'
import type { VueNode } from 'ant-design-vue/es/_util/type'
import { getPrefixCls, routeContextInjectKey } from '../../RouteContext'
import { reactiveOmit, reactivePick } from '@vueuse/core'
import type { VueNodeOrRender } from '#/types'
import { VueNodeOrRenderPropType, WithFalseVueNodeOrRenderPropType } from '#/types'
import { pageHeaderProps } from 'ant-design-vue/es/page-header'
import omit from 'ant-design-vue/es/_util/omit'
const antvPageHeaderPropsKeys = Object.keys(pageHeaderProps())
export const pageHeaderTabConfig = () => ({
/** tabs 的列表 */
tabList: Array as PropType<(TabPaneProps & { key?: string | number })[]>,
/** 当前选中 tab 的 key */
tabActiveKey: [String, Number] as PropType<TabsProps['activeKey']>,
/** tab 修改时触发 */
onTabChange: Function as PropType<TabsProps['onChange']>,
/** tab 上额外的区域 */
tabBarExtraContent: VueNodeOrRenderPropType as PropType<VueNodeOrRender>,
/** tabs 的其他配置 */
tabProps: Object as PropType<TabsProps>,
/** 固定 PageHeader 到页面顶部 */
fixedHeader: { type: Boolean, default: undefined }
})
export type PageHeaderTabConfig = Partial<ExtractPropTypes<ReturnType<typeof pageHeaderTabConfig>>>
type PageHeaderRender = (props: PageContainerProps) => VueNodeOrRender
export const pageContainerProps = () => ({
...pageHeaderTabConfig(),
...omit(pageHeaderProps(), ['title', 'footer', 'breadcrumb']),
title: {
type: WithFalseVueNodeOrRenderPropType as PropType<WithFalse<VueNodeOrRender>>,
default: undefined
},
content: VueNodeOrRenderPropType as PropType<VueNodeOrRender>,
extraContent: VueNodeOrRenderPropType as PropType<VueNodeOrRender>,
prefixCls: String,
footer: VueNodeOrRenderPropType as PropType<VueNodeOrRender>,
/** 是否显示背景色 */
ghost: { type: Boolean, default: undefined },
/**
* PageHeader ( antd )
*/
header: Object as PropType<Partial<PageHeaderProps>>,
/** pageHeader */
pageHeaderRender: {
type: WithFalseVueNodeOrRenderPropType as PropType<WithFalse<PageHeaderRender>>,
default: undefined
},
/** 固钉的配置 (与 antd 完全相同) */
affixProps: Object as PropType<AffixProps>,
/** 内容是否加载中 (只加载内容区域) */
loading: {
type: [Object, Function, Boolean] as PropType<boolean | SpinProps | VueNodeOrRender>,
default: false
},
// TODO 由于 ant-design-vue 暂时不支持这个属性,所以先注释
/** 自定义 breadcrumb,返回false不展示 */
// breadcrumbRender: {
// type: WithFalseCustomRenderPropType as PropType<WithFalse<PageHeaderProps['breadcrumb']>>,
// default: undefined
// },
/** 水印的配置 */
waterMarkProps: Object as PropType<WaterMarkProps>,
/** 配置面包屑 */
breadcrumb: Object as PropType<BreadcrumbProps>
})
export type PageContainerProps = Partial<ExtractPropTypes<ReturnType<typeof pageContainerProps>>>
function genLoading(spinProps: boolean | SpinProps) {
if (typeof spinProps === 'object') {
return spinProps
}
return { spinning: spinProps }
}
/**
* Render Footer tabList In order to be compatible with the old version of the PageHeader basically
* all the functions are implemented.
*/
const renderFooter = (props: Omit<PageContainerProps & { prefixedClassName: string }, 'title'>) => {
if (Array.isArray(props.tabList) || props.tabBarExtraContent) {
return (
<Tabs
class={`${props.prefixedClassName}-tabs`}
activeKey={props.tabActiveKey}
onChange={key => {
if (props.onTabChange) {
props.onTabChange(key)
}
}}
tabBarExtraContent={props.tabBarExtraContent}
{...props.tabProps}
>
{props.tabList?.map((item, index) => (
<Tabs.TabPane {...item} tab={item.tab} key={item.key || index} />
))}
</Tabs>
)
}
return null
}
const renderPageHeader = (
content: VueNodeOrRender,
extraContent: VueNodeOrRender,
prefixedClassName: string
): VueNode => {
if (!content && !extraContent) {
return null
}
return (
<div class={`${prefixedClassName}-detail`}>
<div class={`${prefixedClassName}-main`}>
<div class={`${prefixedClassName}-row`}>
{content && <div class={`${prefixedClassName}-content`}>{content}</div>}
{extraContent && <div class={`${prefixedClassName}-extraContent`}>{extraContent}</div>}
</div>
</div>
</div>
)
}
/**
* ProLayout 使
*
* @param props
* @returns
*/
const ProBreadcrumb: FunctionalComponent<BreadcrumbProps> = props => {
const routeContext = inject(routeContextInjectKey)
return (
<div
style={{
height: '100%',
display: 'flex',
alignItems: 'center'
}}
>
{/* @ts-ignore TODO 面包屑透传处理 */}
<Breadcrumb {...routeContext?.breadcrumb} {...routeContext?.breadcrumbProps} {...props} />
</div>
)
}
// eslint-disable-next-line vue/one-component-per-file
const ProPageHeader = defineComponent({
name: 'ProPageHeader',
inheritAttrs: false,
props: {
...pageContainerProps(),
prefixedClassName: { type: String, default: '' }
},
setup(props, { slots }) {
const routeContext = inject(routeContextInjectKey, {})
const restProps = reactiveOmit(
props,
'title',
'content',
'pageHeaderRender',
'header',
'prefixedClassName',
'extraContent',
'prefixCls'
)
if (props.pageHeaderRender === false) {
return null
}
if (props.pageHeaderRender) {
return <> {props.pageHeaderRender({ ...props, ...routeContext })}</>
}
const pageHeaderTitle = computed(() => {
if (!props.title && props.title !== false) {
return routeContext.title
} else {
return props.title
}
})
// @ts-ignore
const antdPageHeaderProps = reactivePick(restProps, antvPageHeaderPropsKeys)
// @ts-ignore
const localPageHeaderProps: PageHeaderProps = {
...antdPageHeaderProps,
footer: renderFooter({
...restProps,
prefixedClassName: props.prefixedClassName
}),
...props.header,
title: pageHeaderTitle.value
}
const { breadcrumb } = localPageHeaderProps as {
breadcrumb: BreadcrumbProps
}
const noHasBreadCrumb = !breadcrumb || (!breadcrumb?.itemRender && !breadcrumb?.routes?.length)
if (
['title', 'subTitle', 'extra', 'tags', 'footer', 'avatar', 'backIcon'].every(
// @ts-ignore
item => !localPageHeaderProps[item]
) &&
noHasBreadCrumb &&
!props.content &&
!props.extraContent
) {
return null
}
return () => (
<div class={`${props.prefixedClassName}-warp`}>
<PageHeader
{...localPageHeaderProps}
breadcrumb={{ ...localPageHeaderProps.breadcrumb, ...routeContext.breadcrumbProps }}
prefixCls={props.prefixCls}
>
{{
...slots,
default: () =>
slots.headerContent?.(props) ||
renderPageHeader(props.content, props.extraContent, props.prefixedClassName)
}}
</PageHeader>
</div>
)
}
})
const pageHeaderSlot = [
'backIcon',
'avatar',
'breadcrumb',
'title',
'subTitle',
'tags',
'extra',
'footer'
]
// eslint-disable-next-line vue/one-component-per-file
const PageContainer = defineComponent({
name: 'PageContainer',
inheritAttrs: false,
props: pageContainerProps(),
slots: [...pageHeaderSlot, 'loading', 'pageHeaderRender', 'headerContent', 'extraContent'],
setup(props, { attrs, slots }) {
const restProps = reactiveOmit(props, 'loading', 'footer', 'affixProps', 'ghost', 'fixedHeader')
const value = inject(routeContextInjectKey, {})
const prefixCls = props.prefixCls || getPrefixCls('pro')
const prefixedClassName = computed(() => `${prefixCls}-page-container`)
const containerClassName = computed(() => [
prefixedClassName.value,
attrs.class,
{
[`${prefixCls}-page-container-ghost`]: props.ghost,
[`${prefixCls}-page-container-with-footer`]: props.footer
}
])
const renderLoading = (): VueNode => {
// 当loading时一个合法的ReactNode时说明用户使用了自定义loading,直接返回改自定义loading
if (slots.loading) {
return slots.loading()
}
// 当传递过来的是布尔值并且为false时说明不需要显示loading,返回null
if (typeof props.loading === 'boolean' && !props.loading) {
return null
}
// 如非上述两种情况那么要么用户传了一个true,要么用户传了loading配置使用genLoading生成loading配置后返回PageLoading
const spinProps = genLoading(props.loading as boolean | SpinProps)
// 如果传的是loading配置但spinning传的是false也不需要显示loading
return spinProps.spinning ? <PageLoading {...spinProps} /> : null
}
function renderContent(loadingDom: VueNode, content: VueNode): VueNode {
// 只要loadingDom非空我们就渲染loadingDom,否则渲染内容
const dom = loadingDom || content
if (props.waterMarkProps || value.waterMarkProps) {
const waterMarkProps = {
...value.waterMarkProps,
...props.waterMarkProps
}
return <WaterMark {...waterMarkProps}>{dom}</WaterMark>
}
return dom
}
return () => {
const pageHeaderDom = (
<ProPageHeader
{...restProps}
ghost={props.ghost}
prefixCls={undefined}
prefixedClassName={prefixedClassName.value}
>
{reactiveOmit(slots, 'default', 'loading')}
</ProPageHeader>
)
const content = slots.default ? (
<>
<div class={`${prefixedClassName.value}-children-content`}>{slots.default()}</div>
{value.hasFooterToolbar && <div style={{ height: '48px', marginTop: '24px' }} />}
</>
) : null
const loadingDom = renderLoading()
const renderContentDom = renderContent(loadingDom, content)
return (
<div style={attrs.style as CSSProperties} class={containerClassName.value}>
{props.fixedHeader && pageHeaderDom ? (
// 在 hasHeader 且 fixedHeader 的情况下,才需要设置高度
// @ts-ignore
<Affix
{...props.affixProps}
offsetTop={value.hasHeader && value.fixedHeader ? value.headerHeight : 0}
>
{pageHeaderDom}
</Affix>
) : (
pageHeaderDom
)}
{renderContentDom && <GridContent>{renderContentDom}</GridContent>}
{props.footer && <FooterToolbar prefixCls={prefixCls}>{props.footer}</FooterToolbar>}
</div>
)
}
}
})
export { ProPageHeader, ProBreadcrumb }
export default PageContainer