Commit 0bd9c2e0 authored by Sendya's avatar Sendya

fix: layout top-nav-header

parent f6904415
......@@ -58,7 +58,7 @@ const BaseMenuDemo = {
<div class="demo" style="background: rgb(244,244,244);">
<div class="container" style="width: 256px;">
<BaseMenu
menus={menus}
menuData={menus}
theme={state.theme}
mode={state.mode}
collapsed={menuState.collapsed}
......
import 'ant-design-vue/dist/antd.less';
import { createApp, defineComponent, reactive } from 'vue';
import { RouterLink } from './mock-router';
import { Button, message } from 'ant-design-vue';
import { Button, Avatar, message } from 'ant-design-vue';
import { default as ProLayout } from '../src/';
import { menus } from './menus';
import { useMenuState } from '../src/SiderMenu/BaseMenu';
......@@ -34,24 +34,39 @@ const BasicLayout = defineComponent({
{...attrs}
v-model={[menuState.collapsed, 'collapsed']}
title={'Pro Layout'}
layout={'side'}
theme={'dark'}
layout={'mix'}
theme={'light'}
navTheme={'dark'}
i18n={(key: string) => key}
isMobile={false}
menuData={menus}
matchMenuKeys={[]}
contentWidth={'Fixed'}
primaryColor={'#1890ff'}
contentStyle={{ minHeight: '500px' }}
siderWidth={208}
openKeys={menuState.openKeys}
selectedKeys={menuState.selectedKeys}
onOpenChange={$event => {
$event && (menuState.openKeys = $event);
}}
onSelect={$event => {
$event && (menuState.selectedKeys = $event);
{...{
'onUpdate:openKeys':$event => {
$event && (menuState.openKeys = $event);
},
'onUpdate:selectedKeys': $event => {
$event && (menuState.selectedKeys = $event);
}
}}
v-slots={{
rightContentRender: () => (
<div style="color: #FFF;margin-right: 16px;">
<Avatar icon={(<Icon.UserOutlined />)} /> Sendya
</div>
),
menuHeaderRender: () => (
<a>
<img src="https://gw.alipayobjects.com/zos/antfincdn/PmY%24TNNDBI/logo.svg" />
{menuState.collapsed ? null : (<h1>Pro Layout</h1>)}
</a>
),
footerRender: () => (
<div>123</div>
)
......
......@@ -79,7 +79,6 @@ const App = defineComponent({
}}
tabActiveKey={state.tabActiveKey}
onTabChange={(key: string) => {
console.log('onTabChange', key);
state.tabActiveKey = key;
}}
footer={[
......
......@@ -55,9 +55,9 @@ const DemoComponent = {
}} />
</div>
) : null}
menuFooterRender={(props) => (
<div style="color: #fff; padding: 8px 16px; overflow: hidden;">
<span>状态:{JSON.stringify(props.collapsed)}</span>
menuFooterRender={(props) => props.collapsed ? undefined : (
<div style="color: #fff; padding: 8px 16px; overflow: hidden;">
<span>自定义页脚</span>
</div>
)}
/>
......
import { computed, FunctionalComponent, CSSProperties } from 'vue';
import { computed, FunctionalComponent, CSSProperties, VNodeChild } from 'vue';
import 'ant-design-vue/es/layout/style';
import Layout from 'ant-design-vue/es/layout';
import { withInstall } from 'ant-design-vue/es/_util/type';
......@@ -6,13 +6,15 @@ import { default as ProProvider, ProProviderData } from './ProProvider';
import { default as GlobalFooter } from './GlobalFooter';
import { default as SiderMenuWrapper, SiderMenuWrapperProps } from './SiderMenu';
import { WrapContent } from './WrapContent';
import { default as Header, HeaderViewProps } from './Header';
import { RenderVNodeType, WithFalse } from './typings';
import { getComponentOrSlot } from './utils';
import useMergedState from './hooks/useMergedState';
import './BasicLayout.less';
const defaultI18nRender = (key: string) => key;
export interface BasicLayoutProps {
export type BasicLayoutProps = SiderMenuWrapperProps & HeaderViewProps & {
pure?: boolean;
/**
*@name logo url
......@@ -23,6 +25,8 @@ export interface BasicLayoutProps {
i18n?: ProProviderData['i18n'];
defaultCollapsed?: boolean;
onCollapse?: (collapsed: boolean) => void;
footerRender?: WithFalse<
......@@ -44,12 +48,28 @@ export interface BasicLayoutProps {
* 兼用 content的 margin
*/
disableContentMargin?: boolean;
}
};
export type ProLayoutProps = BasicLayoutProps &
SiderMenuWrapperProps /* & HeaderProps & FooterProps */;
const ProLayout: FunctionalComponent<BasicLayoutProps> = (props, { emit, slots, attrs }) => {
const {
onCollapse: propsOnCollapse,
contentStyle,
disableContentMargin,
siderWidth = 208,
menu,
isChildrenLayout: propsIsChildrenLayout,
loading,
layout,
matchMenuKeys,
navTheme,
menuData,
isMobile,
defaultCollapsed,
} = props;
const isTop = computed(() => layout === 'top');
const isSide = computed(() => layout === 'side');
const isMix = computed(() => layout === 'mix');
const ProLayout: FunctionalComponent<ProLayoutProps> = (props, { emit, slots }) => {
const handleCollapse = (collapsed: boolean) => {
emit('update:collapsed', collapsed);
};
......@@ -72,72 +92,97 @@ const ProLayout: FunctionalComponent<ProLayoutProps> = (props, { emit, slots })
};
});
const [collapsed, onCollapse] = useMergedState<boolean>(defaultCollapsed || false, {
value: props.collapsed,
onChange: propsOnCollapse,
});
const headerRender = (
props: BasicLayoutProps & {
hasSiderMenu: boolean;
customHeaderRender: VNodeChild | false;
rightContentRender: VNodeChild | false;
},
matchMenuKeys: string[]
): RenderVNodeType => {
if (props.headerRender === false || props.pure) {
return null;
}
return <Header matchMenuKeys={matchMenuKeys} {...props} />;
return <Header matchMenuKeys={matchMenuKeys} {...props} headerHeight={48} />;
}
const footerRender = getComponentOrSlot(props, slots, 'footerRender');
const rightContentRender = getComponentOrSlot(props, slots, 'rightContentRender');
const customHeaderRender = getComponentOrSlot(props, slots, 'headerRender');;
const headerDom = headerRender({
...props,
hasSiderMenu: isTop.value,
menuData,
isMobile,
collapsed,
onCollapse,
onSelect: handleSelect,
onOpenChange: handleOpenChange,
customHeaderRender,
rightContentRender,
theme: (navTheme || 'dark').toLocaleLowerCase().includes('dark') ? 'dark' : 'light',
}, matchMenuKeys);
// const headerRender = getComponentOrSlot(props, slots, 'headerRender');
const footerRender = getComponentOrSlot(props, slots, 'footerRender');
const menuRender = getComponentOrSlot(props, slots, 'menuRender');
const menuHeaderRender = getComponentOrSlot(props, slots, 'menuHeaderRender');
// const menuHeaderRender = getComponentOrSlot(props, slots, 'menuHeaderRender');
const menuHeaderRenderFunc = props['menuHeaderRender'];
const menuHeaderRenderSlot = slots['menuHeaderRender'];
return (
<ProProvider i18n={defaultI18nRender}>
<div class={className.value}>
<Layout class={baseClassName.value}>
<SiderMenuWrapper
{...props}
onSelect={handleSelect}
onOpenChange={handleOpenChange}
onCollapse={handleCollapse}
/>
<Layout>
<Layout.Header style="background: #fff; padding: 0; height: 48px; line-height: 48px;">
</Layout.Header>
<WrapContent style={props.contentStyle}>
{slots.default?.()}
</WrapContent>
{ footerRender && footerRender || footerRender !== false && (
<GlobalFooter
links={[
{
key: '1',
title: 'Pro Layout',
href: 'https://www.github.com/vueComponent/pro-layout',
blankTarget: true,
},
{
key: '2',
title: 'Github',
href: 'https://www.github.com/vueComponent/ant-design-vue-pro',
blankTarget: true,
},
{
key: '3',
title: '@Sendya',
href: 'https://www.github.com/sendya/',
blankTarget: true,
},
]}
copyright={
<a href="https://github.com/vueComponent" target="_blank">
vueComponent
</a>
}
/>
)}
</Layout>
</Layout>
</div>
{ props.pure
? (slots.default?.())
: (
<div class={className.value}>
<Layout class={baseClassName.value}>
{ !isTop.value && (<SiderMenuWrapper
{...props}
menuHeaderRender={menuHeaderRenderFunc || (menuHeaderRenderSlot && (() => menuHeaderRenderSlot()))}
onSelect={handleSelect}
onOpenChange={handleOpenChange}
onCollapse={handleCollapse}
/>)}
<Layout>
{headerDom}
<WrapContent style={props.contentStyle}>
{slots.default?.()}
</WrapContent>
{ footerRender && footerRender || footerRender !== false && (
<GlobalFooter
links={[
{
key: '1',
title: 'Pro Layout',
href: 'https://www.github.com/vueComponent/pro-layout',
blankTarget: true,
},
{
key: '2',
title: 'Github',
href: 'https://www.github.com/vueComponent/ant-design-vue-pro',
blankTarget: true,
},
{
key: '3',
title: '@Sendya',
href: 'https://www.github.com/sendya/',
blankTarget: true,
},
]}
copyright={
<a href="https://github.com/vueComponent" target="_blank">
vueComponent
</a>
}
/>
)}
</Layout>
</Layout>
</div>
)}
</ProProvider>
);
};
......@@ -153,18 +198,54 @@ ProLayout.props = {
title: String,
colSize: String,
isChildrenLayout: Boolean,
fixSiderbar: Boolean,
isMobile: Boolean,
fixSiderbar: {
type: Boolean,
default: () => false,
},
fixedHeader: {
type: Boolean,
default: () => false,
},
layout: String,
openKeys: Array,
selectedKeys: Array,
collapsed: Boolean,
menuData: Array,
contentStyle: Object,
headerRender: [Function, Boolean],
footerRender: [Function, Boolean],
menuRender: [Function, Boolean],
menuHeaderRender: [Function, Boolean],
rightContent: [Function, Boolean],
theme: String,
headerTheme: {
type: String,
defualt: 'light',
},
navTheme: {
type: String,
default: 'light',
},
headerRender: {
type: [Function, Boolean],
default: () => undefined,
},
footerRender: {
type: [Function, Boolean],
default: () => undefined,
},
menuRender: {
type: [Function, Boolean],
default: () => undefined,
},
menuHeaderRender: {
type: [Function, Boolean],
default: () => undefined,
},
rightContentRender: {
type: [Function, Boolean],
default: () => undefined,
},
rightContent: {
type: [Function, Boolean],
default: () => undefined,
},
} as any;
export default withInstall(ProLayout);
import { computed, CSSProperties, FunctionalComponent, Ref } from 'vue';
import { computed, CSSProperties, FunctionalComponent, toRefs } from 'vue';
import { PureSettings } from '../defaultSettings';
import { RenderVNodeType, MenuDataItem, WithFalse } from '../typings';
import { SiderMenuProps, PrivateSiderMenuProps, defaultRenderLogo, defaultRenderLogoAndTitle, defaultRenderCollapsedButton } from '../SiderMenu/SiderMenu';
......@@ -48,18 +48,17 @@ export const GlobalHeader: FunctionalComponent<GlobalHeaderProps & PrivateSiderM
menuHeaderRender,
onMenuHeaderClick,
className: propClassName,
style,
layout,
headerTheme = 'dark',
splitMenus,
menuData,
prefixCls,
} = props;
const baseClassName = `${prefixCls}-global-header`;
const baseClassName = computed(() => `${prefixCls}-global-header`);
const className = computed(() => {
return {
[baseClassName]: true,
[`${baseClassName}-layout-${layout}`]: layout && headerTheme === 'dark',
[baseClassName.value]: true,
[`${baseClassName.value}-layout-${layout}`]: layout && headerTheme === 'dark',
}
});
if (layout === 'mix' && !isMobile && splitMenus) {
......@@ -80,17 +79,17 @@ export const GlobalHeader: FunctionalComponent<GlobalHeaderProps & PrivateSiderM
}
const logoDom = (
<span class={`${baseClassName}-logo`} key="logo">
<span class={`${baseClassName.value}-logo`} key="logo">
<a>{defaultRenderLogo(logo)}</a>
</span>
);
return (
<div class={className} style={{ ...style }}>
<div class={className.value}>
{isMobile && renderLogo(menuHeaderRender, logoDom)}
{isMobile && collapsedButtonRender && (
<span
class={`${baseClassName}-collapsed-button`}
class={`${baseClassName.value}-collapsed-button`}
onClick={() => {
if (onCollapse) {
onCollapse(!collapsed);
......@@ -102,13 +101,16 @@ export const GlobalHeader: FunctionalComponent<GlobalHeaderProps & PrivateSiderM
)}
{layout === 'mix' && !isMobile && (
<>
<div class={`${baseClassName}-logo`} onClick={onMenuHeaderClick}>
<div class={`${baseClassName.value}-logo`} onClick={onMenuHeaderClick}>
{defaultRenderLogoAndTitle({ ...props, collapsed: false }, 'headerTitleRender')}
</div>
</>
)}
<div style={{ flex: 1 }}>{slots.default?.()}</div>
{rightContentRender && rightContentRender(props)}
{ rightContentRender && typeof rightContentRender === 'function'
? rightContentRender(props)
: rightContentRender
}
</div>
);
}
......
......@@ -29,10 +29,41 @@ export type HeaderViewProps = GlobalHeaderProps & {
siderWidth?: number;
hasSiderMenu?: boolean;
};
export const headerProps = [
'prefixCls',
'collapsed',
'onCollapse',
'openKeys',
'selectedKeys',
'isMobile',
'logo',
'title',
'menuRender',
'rightContentRender',
'menuData',
'menuHeaderRender',
'splitMenus',
'headerRender',
'headerTitleRender',
'headerContentRender',
'siderWidth',
'hasSiderMenu',
'fixedHeader',
'headerHeight',
'headerTheme',
'layout',
'navTheme',
'onSelect',
'onOpenChange',
]
export const HeaderView = defineComponent<HeaderViewProps>({
setup(props) {
export const HeaderView = defineComponent({
inheritAttrs: false,
name: 'HeaderView',
props: headerProps,
setup(props: HeaderViewProps) {
const { prefixCls, headerRender, headerContentRender, isMobile, fixedHeader, hasSiderMenu, headerHeight, layout, navTheme, onCollapse } = toRefs(props);
console.log('HeaderView', props)
const isTop = computed(() => props.layout === 'top');
const needFixedHeader = computed(() => fixedHeader.value || layout.value === 'mix');
const needSettingWidth = computed(() => needFixedHeader.value && hasSiderMenu.value && !isTop.value && !isMobile.value);
......@@ -42,11 +73,11 @@ export const HeaderView = defineComponent<HeaderViewProps>({
[`${prefixCls.value}-fixed-header`]: needFixedHeader.value,
[`${prefixCls.value}-top-menu`]: isTop.value,
}
})
});
const renderContent = () => {
let defaultDom = (
<GlobalHeader {...props} onCollapse={onCollapse.value} menuData={clearMenuData.value}>
{headerContentRender.value && headerContentRender.value(props)}
{headerContentRender && headerContentRender.value && headerContentRender.value(props)}
</GlobalHeader>
);
if (isTop.value && !isMobile.value) {
......@@ -75,10 +106,9 @@ export const HeaderView = defineComponent<HeaderViewProps>({
: '100%';
});
const right = computed(() => needFixedHeader.value ? 0 : undefined);
return () => (
<>
{needFixedHeader && (
{needFixedHeader.value && (
<Header
style={{
height: headerHeight.value,
......@@ -90,13 +120,13 @@ export const HeaderView = defineComponent<HeaderViewProps>({
<Header
style={{
padding: 0,
height: headerHeight,
lineHeight: `${headerHeight}px`,
width,
height: `${headerHeight.value}px`,
lineHeight: `${headerHeight.value}px`,
width: width.value,
zIndex: layout.value === 'mix' ? 100 : 19,
right,
right: right.value,
}}
class={className}
class={className.value}
>
{renderContent()}
</Header>
......
......@@ -16,11 +16,12 @@ import {
} from 'vue';
import { createFromIconfontCN } from '@ant-design/icons-vue';
import 'ant-design-vue/es/menu/style';
import Menu, { MenuProps } from 'ant-design-vue/es/menu';
import Menu from 'ant-design-vue/es/menu';
import defaultSettings, { PureSettings } from '../defaultSettings';
import { isImg, isUrl } from '../utils';
import { MenuMode, SelectInfo, OpenEventHandler } from './typings';
import { MenuDataItem, MenuTheme, FormatMessage, WithFalse } from '../typings';
import { PrivateSiderMenuProps } from './SiderMenu';
import './index.less';
export { MenuMode, SelectInfo, OpenEventHandler };
......@@ -224,12 +225,13 @@ export default defineComponent({
}): void => {
emit('update:selectedKeys', params.selectedKeys);
};
return () => (
<Menu
key="Menu"
inlineCollapsed={(isInline.value && props.collapsed) || undefined}
inlineIndent={16}
mode={props.mode}
theme={props.theme}
theme={props.theme as 'dark' | 'light'}
openKeys={props.openKeys || []}
selectedKeys={props.selectedKeys || []}
onOpenChange={handleOpenChange}
......
......@@ -50,7 +50,7 @@ export const defaultRenderLogo = (logo: RenderVNodeType): RenderVNodeType => {
export const defaultRenderLogoAndTitle = (
props: SiderMenuProps,
renderKey = 'menuHeaderRender',
renderKey: string | undefined = 'menuHeaderRender',
): RenderVNodeType => {
const {
logo = 'https://gw.alipayobjects.com/zos/antfincdn/PmY%24TNNDBI/logo.svg',
......@@ -84,6 +84,7 @@ export const defaultRenderCollapsedButton = (collapsed?: boolean): RenderVNodeTy
const SiderMenu: FunctionalComponent<SiderMenuProps> = (props: SiderMenuProps) => {
const {
theme,
menuData,
collapsed,
siderWidth,
......@@ -173,7 +174,7 @@ const SiderMenu: FunctionalComponent<SiderMenuProps> = (props: SiderMenuProps) =
<Menu
class={`${baseClassName}-link-menu`}
inlineIndent={16}
theme={runtimeTheme.value}
theme={theme}
selectedKeys={[]}
openKeys={[]}
mode="inline"
......
......@@ -7,7 +7,8 @@ import SiderMenu, { SiderMenuProps, PrivateSiderMenuProps } from './SiderMenu';
export type SiderMenuWrapperProps = SiderMenuProps & Partial<PrivateSiderMenuProps>;
const SiderMenuWrapper: FunctionalComponent<SiderMenuWrapperProps> = props => {
const SiderMenuWrapper: FunctionalComponent<SiderMenuWrapperProps> = (props, { attrs }) => {
console.log('SiderMenuWrapper', props, attrs)
return props.isMobile ? (
<Drawer>
<SiderMenu {...props} />
......@@ -18,5 +19,6 @@ const SiderMenuWrapper: FunctionalComponent<SiderMenuWrapperProps> = props => {
};
SiderMenuWrapper.inheritAttrs = false;
SiderMenuWrapper.displayName = 'SiderMenuWrapper';
export default SiderMenuWrapper;
@import '~ant-design-vue/es/style/themes/default.less';
@import '../BasicLayout.less';
@top-nav-header-prefix-cls: ~'@{ant-prefix}-pro-top-nav-header';
.@{top-nav-header-prefix-cls} {
position: relative;
width: 100%;
height: 100%;
box-shadow: 0 1px 4px 0 rgba(0, 21, 41, 0.12);
transition: background 0.3s, width 0.2s;
.@{ant-prefix}-menu {
background: transparent;
}
&.light {
background-color: @component-background;
.@{top-nav-header-prefix-cls}-logo {
h1 {
color: @heading-color;
}
}
.anticon {
color: inherit;
}
}
&-main {
display: flex;
height: 100%;
padding-left: 16px;
&-left {
display: flex;
min-width: 192px;
}
}
.anticon {
color: @btn-primary-color;
}
&-logo {
position: relative;
min-width: 165px;
height: 100%;
overflow: hidden;
img {
display: inline-block;
height: 32px;
vertical-align: middle;
}
h1 {
display: inline-block;
margin: 0 0 0 12px;
color: @btn-primary-color;
font-weight: 400;
font-size: 16px;
vertical-align: top;
}
}
&-menu {
min-width: 0;
.@{ant-prefix}-menu.@{ant-prefix}-menu-horizontal {
height: 100%;
border: none;
.@{ant-prefix}-menu-item {
height: 100%;
}
}
}
}
......@@ -31,29 +31,32 @@ const RightContent: FunctionalComponent<TopNavHeaderProps> = ({ rightContentRend
rightSize.value = width;
}}
>
{rightContentRender && (
{rightContentRender && typeof rightContentRender === 'function' ? (
<div>
{rightContentRender({
...props,
})}
</div>
)}
) : rightContentRender}
</ResizeObserver>
</div>
</div>
);
};
export const TopNavHeader: FunctionalComponent<TopNavHeaderProps> = (props) => {
export const TopNavHeader: FunctionalComponent<TopNavHeaderProps> = (props, { emit }) => {
const headerRef = ref();
const {
theme,
prefixCls: propPrefixCls,
onMenuHeaderClick,
contentWidth,
rightContentRender,
layout,
onOpenChange,
onSelect,
...restProps
} = props;
const prefixCls = `${props.prefixCls || 'ant-pro'}-top-nav-header`;
const prefixCls = `${propPrefixCls || 'ant-pro'}-top-nav-header`;
const headerDom = defaultRenderLogoAndTitle(
{ ...props, collapsed: false },
layout === 'mix' ? 'headerTitleRender' : undefined,
......@@ -61,11 +64,11 @@ export const TopNavHeader: FunctionalComponent<TopNavHeaderProps> = (props) => {
const className = computed(() => {
return {
[prefixCls]: true,
light: theme === 'light',
light: props.theme === 'light',
}
});
return (
<div class={className}>
<div class={className.value}>
<div ref={headerRef} class={`${prefixCls}-main ${contentWidth === 'Fixed' ? 'wide' : ''}`}>
{headerDom && (
<div class={`${prefixCls}-main-left`} onClick={onMenuHeaderClick}>
......@@ -75,7 +78,18 @@ export const TopNavHeader: FunctionalComponent<TopNavHeaderProps> = (props) => {
</div>
)}
<div style={{ flex: 1 }} class={`${prefixCls}-menu`}>
<BaseMenu {...props} />
<BaseMenu
{...restProps}
class={{ 'top-nav-menu': props.mode === 'horizontal' }}
{...{
'onUpdate:openKeys': ($event: any) => {
onOpenChange && onOpenChange($event);
},
'onUpdate:selectedKeys': ($event: any) => {
onSelect && onSelect($event);
},
}}
/>
</div>
{rightContentRender && <RightContent rightContentRender={rightContentRender} {...props} />}
</div>
......
export default function useControlledState<T, R = T>(
defaultStateValue: T | (() => T),
option?: {
defaultValue?: T | (() => T);
value?: T;
onChange?: (value: T, prevValue: T) => void;
postState?: (value: T) => T;
},
): [R, (value: T) => void] {
const triggerChange = () => {};
return [option.value, triggerChange];
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment