22 changed files with 39 additions and 1527 deletions
@ -1,53 +0,0 @@ |
|||||
import React, { Component } from 'react'; |
|
||||
import { Icon } from 'antd'; |
|
||||
import Link from 'umi/link'; |
|
||||
import debounce from 'lodash/debounce'; |
|
||||
import styles from './index.less'; |
|
||||
import RightContent, { GlobalHeaderRightProps } from './RightContent'; |
|
||||
|
|
||||
type PartialGlobalHeaderRightProps = { |
|
||||
[K in |
|
||||
| 'onMenuClick' |
|
||||
| 'onNoticeClear' |
|
||||
| 'onNoticeVisibleChange' |
|
||||
| 'currentUser']?: GlobalHeaderRightProps[K] |
|
||||
}; |
|
||||
|
|
||||
export interface GlobalHeaderProps extends PartialGlobalHeaderRightProps { |
|
||||
collapsed?: boolean; |
|
||||
onCollapse?: (collapsed: boolean) => void; |
|
||||
isMobile?: boolean; |
|
||||
logo?: string; |
|
||||
} |
|
||||
|
|
||||
export default class GlobalHeader extends Component<GlobalHeaderProps> { |
|
||||
triggerResizeEvent = debounce(() => { |
|
||||
const event = document.createEvent('HTMLEvents'); |
|
||||
event.initEvent('resize', true, false); |
|
||||
window.dispatchEvent(event); |
|
||||
}); |
|
||||
componentWillUnmount() { |
|
||||
this.triggerResizeEvent.cancel(); |
|
||||
} |
|
||||
toggle = () => { |
|
||||
const { collapsed, onCollapse } = this.props; |
|
||||
if (onCollapse) onCollapse(!collapsed); |
|
||||
this.triggerResizeEvent(); |
|
||||
}; |
|
||||
render() { |
|
||||
const { collapsed, isMobile, logo } = this.props; |
|
||||
return ( |
|
||||
<div className={styles.header}> |
|
||||
{isMobile && ( |
|
||||
<Link to="/" className={styles.logo} key="logo"> |
|
||||
<img src={logo} alt="logo" width="32" /> |
|
||||
</Link> |
|
||||
)} |
|
||||
<span className={styles.trigger} onClick={this.toggle}> |
|
||||
<Icon type={collapsed ? 'menu-unfold' : 'menu-fold'} /> |
|
||||
</span> |
|
||||
<RightContent {...this.props} /> |
|
||||
</div> |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
@ -1,9 +0,0 @@ |
|||||
import { Icon } from 'antd'; |
|
||||
import defaultSettings from '../../../config/defaultSettings'; |
|
||||
|
|
||||
const { iconfontUrl } = defaultSettings; |
|
||||
const scriptUrl = iconfontUrl; |
|
||||
// 使用:
|
|
||||
// import IconFont from '@/components/IconFont';
|
|
||||
// <IconFont type='icon-demo' className='xxx-xxx' />
|
|
||||
export default Icon.createFromIconfontCN({ scriptUrl }); |
|
||||
@ -1,31 +0,0 @@ |
|||||
import React from 'react'; |
|
||||
import { Tooltip, Icon } from 'antd'; |
|
||||
import style from './index.less'; |
|
||||
|
|
||||
export interface BlockChecboxProps { |
|
||||
value: string; |
|
||||
onChange: (key: string) => void; |
|
||||
list: any[]; |
|
||||
} |
|
||||
|
|
||||
const BlockChecbox: React.FC<BlockChecboxProps> = ({ value, onChange, list }) => ( |
|
||||
<div className={style.blockChecbox} key={value}> |
|
||||
{list.map(item => ( |
|
||||
<Tooltip title={item.title} key={item.key}> |
|
||||
<div className={style.item} onClick={() => onChange(item.key)}> |
|
||||
<img src={item.url} alt={item.key} /> |
|
||||
<div |
|
||||
className={style.selectIcon} |
|
||||
style={{ |
|
||||
display: value === item.key ? 'block' : 'none', |
|
||||
}} |
|
||||
> |
|
||||
<Icon type="check" /> |
|
||||
</div> |
|
||||
</div> |
|
||||
</Tooltip> |
|
||||
))} |
|
||||
</div> |
|
||||
); |
|
||||
|
|
||||
export default BlockChecbox; |
|
||||
@ -1,21 +0,0 @@ |
|||||
.themeColor { |
|
||||
margin-top: 24px; |
|
||||
overflow: hidden; |
|
||||
.title { |
|
||||
margin-bottom: 12px; |
|
||||
color: rgba(0, 0, 0, 0.65); |
|
||||
font-size: 14px; |
|
||||
line-height: 22px; |
|
||||
} |
|
||||
.colorBlock { |
|
||||
float: left; |
|
||||
width: 20px; |
|
||||
height: 20px; |
|
||||
margin-right: 8px; |
|
||||
color: #fff; |
|
||||
font-weight: bold; |
|
||||
text-align: center; |
|
||||
border-radius: 2px; |
|
||||
cursor: pointer; |
|
||||
} |
|
||||
} |
|
||||
@ -1,80 +0,0 @@ |
|||||
import React from 'react'; |
|
||||
import { Tooltip, Icon } from 'antd'; |
|
||||
import { formatMessage } from 'umi-plugin-locale'; |
|
||||
import styles from './ThemeColor.less'; |
|
||||
|
|
||||
export interface TagProps { |
|
||||
color: string; |
|
||||
check: boolean; |
|
||||
className?: string; |
|
||||
onClick?: () => void; |
|
||||
} |
|
||||
|
|
||||
const Tag: React.FC<TagProps> = ({ color, check, ...rest }) => ( |
|
||||
<div {...rest} style={{ backgroundColor: color }}> |
|
||||
{check ? <Icon type="check" /> : ''} |
|
||||
</div> |
|
||||
); |
|
||||
|
|
||||
export interface ThemeColorProps { |
|
||||
colors?: any[]; |
|
||||
title?: string; |
|
||||
value: string; |
|
||||
onChange: (color: string) => void; |
|
||||
} |
|
||||
|
|
||||
const ThemeColor: React.FC<ThemeColorProps> = ({ colors, title, value, onChange }) => { |
|
||||
const colorList = colors || [ |
|
||||
{ |
|
||||
key: 'dust', |
|
||||
color: '#F5222D', |
|
||||
}, |
|
||||
{ |
|
||||
key: 'volcano', |
|
||||
color: '#FA541C', |
|
||||
}, |
|
||||
{ |
|
||||
key: 'sunset', |
|
||||
color: '#FAAD14', |
|
||||
}, |
|
||||
{ |
|
||||
key: 'cyan', |
|
||||
color: '#13C2C2', |
|
||||
}, |
|
||||
{ |
|
||||
key: 'green', |
|
||||
color: '#52C41A', |
|
||||
}, |
|
||||
{ |
|
||||
key: 'daybreak', |
|
||||
color: '#1890FF', |
|
||||
}, |
|
||||
{ |
|
||||
key: 'geekblue', |
|
||||
color: '#2F54EB', |
|
||||
}, |
|
||||
{ |
|
||||
key: 'purple', |
|
||||
color: '#722ED1', |
|
||||
}, |
|
||||
]; |
|
||||
return ( |
|
||||
<div className={styles.themeColor}> |
|
||||
<h3 className={styles.title}>{title}</h3> |
|
||||
<div className={styles.content}> |
|
||||
{colorList.map(({ key, color }) => ( |
|
||||
<Tooltip key={color} title={formatMessage({ id: `app.setting.themecolor.${key}` })}> |
|
||||
<Tag |
|
||||
className={styles.colorBlock} |
|
||||
color={color} |
|
||||
check={value === color} |
|
||||
onClick={() => onChange && onChange(color)} |
|
||||
/> |
|
||||
</Tooltip> |
|
||||
))} |
|
||||
</div> |
|
||||
</div> |
|
||||
); |
|
||||
}; |
|
||||
|
|
||||
export default ThemeColor; |
|
||||
@ -1,81 +0,0 @@ |
|||||
@import '~antd/lib/style/themes/default.less'; |
|
||||
|
|
||||
.content { |
|
||||
position: relative; |
|
||||
min-height: 100%; |
|
||||
background: #fff; |
|
||||
:global { |
|
||||
.ant-list-item { |
|
||||
span { |
|
||||
flex: 1; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.blockChecbox { |
|
||||
display: flex; |
|
||||
.item { |
|
||||
position: relative; |
|
||||
margin-right: 16px; |
|
||||
// box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.1); |
|
||||
border-radius: @border-radius-base; |
|
||||
cursor: pointer; |
|
||||
img { |
|
||||
width: 48px; |
|
||||
} |
|
||||
} |
|
||||
.selectIcon { |
|
||||
position: absolute; |
|
||||
top: 0; |
|
||||
right: 0; |
|
||||
width: 100%; |
|
||||
height: 100%; |
|
||||
padding-top: 15px; |
|
||||
padding-left: 24px; |
|
||||
color: @primary-color; |
|
||||
font-weight: bold; |
|
||||
font-size: 14px; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.color_block { |
|
||||
display: inline-block; |
|
||||
width: 38px; |
|
||||
height: 22px; |
|
||||
margin: 4px; |
|
||||
margin-right: 12px; |
|
||||
vertical-align: middle; |
|
||||
border-radius: 4px; |
|
||||
cursor: pointer; |
|
||||
} |
|
||||
|
|
||||
.title { |
|
||||
margin-bottom: 12px; |
|
||||
color: @heading-color; |
|
||||
font-size: 14px; |
|
||||
line-height: 22px; |
|
||||
} |
|
||||
|
|
||||
.handle { |
|
||||
position: absolute; |
|
||||
top: 240px; |
|
||||
right: 300px; |
|
||||
z-index: 0; |
|
||||
display: flex; |
|
||||
justify-content: center; |
|
||||
align-items: center; |
|
||||
width: 48px; |
|
||||
height: 48px; |
|
||||
font-size: 16px; |
|
||||
text-align: center; |
|
||||
background: @primary-color; |
|
||||
border-radius: 4px 0 0 4px; |
|
||||
cursor: pointer; |
|
||||
pointer-events: auto; |
|
||||
} |
|
||||
|
|
||||
.productionHint { |
|
||||
margin-top: 16px; |
|
||||
font-size: 12px; |
|
||||
} |
|
||||
@ -1,272 +0,0 @@ |
|||||
import { ConnectProps, ConnectState, SettingModelState } from '@/models/connect'; |
|
||||
import React, { Component } from 'react'; |
|
||||
import { Select, message, Drawer, List, Switch, Divider, Icon, Button, Alert, Tooltip } from 'antd'; |
|
||||
import { formatMessage } from 'umi-plugin-react/locale'; |
|
||||
import { CopyToClipboard } from 'react-copy-to-clipboard'; |
|
||||
import { connect } from 'dva'; |
|
||||
import omit from 'omit.js'; |
|
||||
import styles from './index.less'; |
|
||||
import ThemeColor from './ThemeColor'; |
|
||||
import BlockCheckbox from './BlockCheckbox'; |
|
||||
|
|
||||
const { Option } = Select; |
|
||||
interface BodyProps { |
|
||||
title: string; |
|
||||
style?: React.CSSProperties; |
|
||||
} |
|
||||
|
|
||||
const Body: React.FC<BodyProps> = ({ children, title, style }) => ( |
|
||||
<div style={{ ...style, marginBottom: 24 }}> |
|
||||
<h3 className={styles.title}>{title}</h3> |
|
||||
{children} |
|
||||
</div> |
|
||||
); |
|
||||
|
|
||||
interface SettingItemProps { |
|
||||
title: React.ReactNode; |
|
||||
action: React.ReactElement; |
|
||||
disabled?: boolean; |
|
||||
disabledReason?: React.ReactNode; |
|
||||
} |
|
||||
|
|
||||
export interface SettingDrawerProps extends ConnectProps { |
|
||||
setting?: SettingModelState; |
|
||||
} |
|
||||
|
|
||||
export interface SettingDrawerState extends Partial<SettingModelState> { |
|
||||
collapse: boolean; |
|
||||
} |
|
||||
|
|
||||
@connect(({ setting }: ConnectState) => ({ setting })) |
|
||||
class SettingDrawer extends Component<SettingDrawerProps, SettingDrawerState> { |
|
||||
state: SettingDrawerState = { |
|
||||
collapse: false, |
|
||||
}; |
|
||||
|
|
||||
getLayoutSetting = (): SettingItemProps[] => { |
|
||||
const { setting } = this.props; |
|
||||
const { contentWidth, fixedHeader, layout, autoHideHeader, fixSiderbar } = setting!; |
|
||||
return [ |
|
||||
{ |
|
||||
title: formatMessage({ id: 'app.setting.content-width' }), |
|
||||
action: ( |
|
||||
<Select |
|
||||
value={contentWidth} |
|
||||
size="small" |
|
||||
onSelect={value => this.changeSetting('contentWidth', value)} |
|
||||
style={{ width: 80 }} |
|
||||
> |
|
||||
{layout === 'sidemenu' ? null : ( |
|
||||
<Option value="Fixed"> |
|
||||
{formatMessage({ id: 'app.setting.content-width.fixed' })} |
|
||||
</Option> |
|
||||
)} |
|
||||
<Option value="Fluid"> |
|
||||
{formatMessage({ id: 'app.setting.content-width.fluid' })} |
|
||||
</Option> |
|
||||
</Select> |
|
||||
), |
|
||||
}, |
|
||||
{ |
|
||||
title: formatMessage({ id: 'app.setting.fixedheader' }), |
|
||||
action: ( |
|
||||
<Switch |
|
||||
size="small" |
|
||||
checked={!!fixedHeader} |
|
||||
onChange={checked => this.changeSetting('fixedHeader', checked)} |
|
||||
/> |
|
||||
), |
|
||||
}, |
|
||||
{ |
|
||||
title: formatMessage({ id: 'app.setting.hideheader' }), |
|
||||
disabled: !fixedHeader, |
|
||||
disabledReason: formatMessage({ id: 'app.setting.hideheader.hint' }), |
|
||||
action: ( |
|
||||
<Switch |
|
||||
size="small" |
|
||||
checked={!!autoHideHeader} |
|
||||
onChange={checked => this.changeSetting('autoHideHeader', checked)} |
|
||||
/> |
|
||||
), |
|
||||
}, |
|
||||
{ |
|
||||
title: formatMessage({ id: 'app.setting.fixedsidebar' }), |
|
||||
disabled: layout === 'topmenu', |
|
||||
disabledReason: formatMessage({ id: 'app.setting.fixedsidebar.hint' }), |
|
||||
action: ( |
|
||||
<Switch |
|
||||
size="small" |
|
||||
checked={!!fixSiderbar} |
|
||||
onChange={checked => this.changeSetting('fixSiderbar', checked)} |
|
||||
/> |
|
||||
), |
|
||||
}, |
|
||||
]; |
|
||||
}; |
|
||||
|
|
||||
changeSetting = (key: string, value: any) => { |
|
||||
const { setting } = this.props; |
|
||||
const nextState = { ...setting! }; |
|
||||
nextState[key] = value; |
|
||||
if (key === 'layout') { |
|
||||
nextState.contentWidth = value === 'topmenu' ? 'Fixed' : 'Fluid'; |
|
||||
} else if (key === 'fixedHeader' && !value) { |
|
||||
nextState.autoHideHeader = false; |
|
||||
} |
|
||||
this.setState(nextState, () => { |
|
||||
const { dispatch } = this.props; |
|
||||
dispatch!({ |
|
||||
type: 'setting/changeSetting', |
|
||||
payload: this.state, |
|
||||
}); |
|
||||
}); |
|
||||
}; |
|
||||
|
|
||||
togglerContent = () => { |
|
||||
const { collapse } = this.state; |
|
||||
this.setState({ collapse: !collapse }); |
|
||||
}; |
|
||||
|
|
||||
renderLayoutSettingItem = (item: SettingItemProps) => { |
|
||||
const action = React.cloneElement(item.action, { |
|
||||
disabled: item.disabled, |
|
||||
}); |
|
||||
return ( |
|
||||
<Tooltip title={item.disabled ? item.disabledReason : ''} placement="left"> |
|
||||
<List.Item actions={[action]}> |
|
||||
<span style={{ opacity: item.disabled ? 0.5 : 1 }}>{item.title}</span> |
|
||||
</List.Item> |
|
||||
</Tooltip> |
|
||||
); |
|
||||
}; |
|
||||
|
|
||||
render() { |
|
||||
const { setting } = this.props; |
|
||||
const { navTheme, primaryColor, layout, colorWeak } = setting!; |
|
||||
const { collapse } = this.state; |
|
||||
return ( |
|
||||
<Drawer |
|
||||
visible={collapse} |
|
||||
width={300} |
|
||||
onClose={this.togglerContent} |
|
||||
placement="right" |
|
||||
handler={ |
|
||||
<div className={styles.handle} onClick={this.togglerContent}> |
|
||||
<Icon |
|
||||
type={collapse ? 'close' : 'setting'} |
|
||||
style={{ |
|
||||
color: '#fff', |
|
||||
fontSize: 20, |
|
||||
}} |
|
||||
/> |
|
||||
</div> |
|
||||
} |
|
||||
style={{ |
|
||||
zIndex: 999, |
|
||||
}} |
|
||||
> |
|
||||
<div className={styles.content}> |
|
||||
<Body title={formatMessage({ id: 'app.setting.pagestyle' })}> |
|
||||
<BlockCheckbox |
|
||||
list={[ |
|
||||
{ |
|
||||
key: 'dark', |
|
||||
url: 'https://gw.alipayobjects.com/zos/rmsportal/LCkqqYNmvBEbokSDscrm.svg', |
|
||||
title: formatMessage({ id: 'app.setting.pagestyle.dark' }), |
|
||||
}, |
|
||||
{ |
|
||||
key: 'light', |
|
||||
url: 'https://gw.alipayobjects.com/zos/rmsportal/jpRkZQMyYRryryPNtyIC.svg', |
|
||||
title: formatMessage({ id: 'app.setting.pagestyle.light' }), |
|
||||
}, |
|
||||
]} |
|
||||
value={navTheme} |
|
||||
onChange={value => this.changeSetting('navTheme', value)} |
|
||||
/> |
|
||||
</Body> |
|
||||
|
|
||||
<ThemeColor |
|
||||
title={formatMessage({ id: 'app.setting.themecolor' })} |
|
||||
value={primaryColor} |
|
||||
onChange={color => this.changeSetting('primaryColor', color)} |
|
||||
/> |
|
||||
|
|
||||
<Divider /> |
|
||||
|
|
||||
<Body title={formatMessage({ id: 'app.setting.navigationmode' })}> |
|
||||
<BlockCheckbox |
|
||||
list={[ |
|
||||
{ |
|
||||
key: 'sidemenu', |
|
||||
url: 'https://gw.alipayobjects.com/zos/rmsportal/JopDzEhOqwOjeNTXkoje.svg', |
|
||||
title: formatMessage({ id: 'app.setting.sidemenu' }), |
|
||||
}, |
|
||||
{ |
|
||||
key: 'topmenu', |
|
||||
url: 'https://gw.alipayobjects.com/zos/rmsportal/KDNDBbriJhLwuqMoxcAr.svg', |
|
||||
title: formatMessage({ id: 'app.setting.topmenu' }), |
|
||||
}, |
|
||||
]} |
|
||||
value={layout} |
|
||||
onChange={value => this.changeSetting('layout', value)} |
|
||||
/> |
|
||||
</Body> |
|
||||
|
|
||||
<List |
|
||||
split={false} |
|
||||
dataSource={this.getLayoutSetting()} |
|
||||
renderItem={this.renderLayoutSettingItem} |
|
||||
/> |
|
||||
|
|
||||
<Divider /> |
|
||||
|
|
||||
<Body title={formatMessage({ id: 'app.setting.othersettings' })}> |
|
||||
<List |
|
||||
split={false} |
|
||||
renderItem={this.renderLayoutSettingItem} |
|
||||
dataSource={[ |
|
||||
{ |
|
||||
title: formatMessage({ id: 'app.setting.weakmode' }), |
|
||||
action: ( |
|
||||
<Switch |
|
||||
size="small" |
|
||||
checked={!!colorWeak} |
|
||||
onChange={checked => this.changeSetting('colorWeak', checked)} |
|
||||
/> |
|
||||
), |
|
||||
}, |
|
||||
]} |
|
||||
/> |
|
||||
</Body> |
|
||||
<Divider /> |
|
||||
<CopyToClipboard |
|
||||
text={JSON.stringify(omit(setting, ['colorWeak']), null, 2)} |
|
||||
onCopy={() => message.success(formatMessage({ id: 'app.setting.copyinfo' }))} |
|
||||
> |
|
||||
<Button block icon="copy"> |
|
||||
{formatMessage({ id: 'app.setting.copy' })} |
|
||||
</Button> |
|
||||
</CopyToClipboard> |
|
||||
<Alert |
|
||||
type="warning" |
|
||||
className={styles.productionHint} |
|
||||
message={ |
|
||||
<div> |
|
||||
{formatMessage({ id: 'app.setting.production.hint' })}{' '} |
|
||||
<a |
|
||||
href="https://u.ant.design/pro-v2-default-settings" |
|
||||
target="_blank" |
|
||||
rel="noopener noreferrer" |
|
||||
> |
|
||||
src/defaultSettings.js |
|
||||
</a> |
|
||||
</div> |
|
||||
} |
|
||||
/> |
|
||||
</div> |
|
||||
</Drawer> |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default SettingDrawer; |
|
||||
@ -1,206 +0,0 @@ |
|||||
import IconFont from '@/components/IconFont'; |
|
||||
import { isUrl } from '@/utils/utils'; |
|
||||
import { Icon, Menu } from 'antd'; |
|
||||
import { MenuMode, MenuTheme } from 'antd/es/menu'; |
|
||||
import classNames from 'classnames'; |
|
||||
import React, { Component } from 'react'; |
|
||||
import { RouterTypes } from 'umi'; |
|
||||
import Link from 'umi/link'; |
|
||||
import { urlToList } from '../_utils/pathTools'; |
|
||||
import styles from './index.less'; |
|
||||
import { getMenuMatches } from './SiderMenuUtils'; |
|
||||
|
|
||||
const { SubMenu } = Menu; |
|
||||
|
|
||||
// Allow menu.js config icon as string or ReactNode
|
|
||||
// icon: 'setting',
|
|
||||
// icon: 'icon-geren' #For Iconfont ,
|
|
||||
// icon: 'http://demo.com/icon.png',
|
|
||||
// icon: <Icon type="setting" />,
|
|
||||
const getIcon = (icon?: string | React.ReactNode) => { |
|
||||
if (typeof icon === 'string') { |
|
||||
if (isUrl(icon)) { |
|
||||
return <Icon component={() => <img src={icon} alt="icon" className={styles.icon} />} />; |
|
||||
} |
|
||||
if (icon.startsWith('icon-')) { |
|
||||
return <IconFont type={icon} />; |
|
||||
} |
|
||||
return <Icon type={icon} />; |
|
||||
} |
|
||||
return icon; |
|
||||
}; |
|
||||
|
|
||||
export interface MenuDataItem { |
|
||||
authority?: string[] | string; |
|
||||
children?: MenuDataItem[]; |
|
||||
hideChildrenInMenu?: boolean; |
|
||||
hideInMenu?: boolean; |
|
||||
icon?: string; |
|
||||
locale?: string; |
|
||||
name?: string; |
|
||||
path: string; |
|
||||
[key: string]: any; |
|
||||
} |
|
||||
|
|
||||
export interface Route extends MenuDataItem { |
|
||||
routes?: Route[]; |
|
||||
} |
|
||||
|
|
||||
export interface BaseMenuProps extends Partial<RouterTypes<Route>> { |
|
||||
className?: string; |
|
||||
collapsed?: boolean; |
|
||||
flatMenuKeys?: any[]; |
|
||||
handleOpenChange?: (openKeys: string[]) => void; |
|
||||
isMobile?: boolean; |
|
||||
menuData?: MenuDataItem[]; |
|
||||
mode?: MenuMode; |
|
||||
onCollapse?: (collapsed: boolean) => void; |
|
||||
onOpenChange?: (openKeys: string[]) => void; |
|
||||
openKeys?: string[]; |
|
||||
style?: React.CSSProperties; |
|
||||
theme?: MenuTheme; |
|
||||
} |
|
||||
|
|
||||
export default class BaseMenu extends Component<BaseMenuProps> { |
|
||||
static defaultProps: Partial<BaseMenuProps> = { |
|
||||
flatMenuKeys: [], |
|
||||
onCollapse: () => void 0, |
|
||||
isMobile: false, |
|
||||
openKeys: [], |
|
||||
collapsed: false, |
|
||||
handleOpenChange: () => void 0, |
|
||||
menuData: [], |
|
||||
onOpenChange: () => void 0, |
|
||||
}; |
|
||||
|
|
||||
/** |
|
||||
* 获得菜单子节点 |
|
||||
*/ |
|
||||
getNavMenuItems = (menusData: MenuDataItem[] = []): React.ReactNode[] => { |
|
||||
return menusData |
|
||||
.filter(item => item.name && !item.hideInMenu) |
|
||||
.map(item => this.getSubMenuOrItem(item)) |
|
||||
.filter(item => item); |
|
||||
}; |
|
||||
|
|
||||
// Get the currently selected menu
|
|
||||
getSelectedMenuKeys = (pathname: string): string[] => { |
|
||||
const { flatMenuKeys } = this.props; |
|
||||
return urlToList(pathname) |
|
||||
.map(itemPath => getMenuMatches(flatMenuKeys, itemPath).pop()) |
|
||||
.filter(item => item) as string[]; |
|
||||
}; |
|
||||
|
|
||||
/** |
|
||||
* get SubMenu or Item |
|
||||
*/ |
|
||||
getSubMenuOrItem = (item: MenuDataItem): React.ReactNode => { |
|
||||
if ( |
|
||||
Array.isArray(item.children) && |
|
||||
!item.hideChildrenInMenu && |
|
||||
item.children.some(child => (child.name ? true : false)) |
|
||||
) { |
|
||||
return ( |
|
||||
<SubMenu |
|
||||
title={ |
|
||||
item.icon ? ( |
|
||||
<span> |
|
||||
{getIcon(item.icon)} |
|
||||
<span>{item.name}</span> |
|
||||
</span> |
|
||||
) : ( |
|
||||
item.name |
|
||||
) |
|
||||
} |
|
||||
key={item.path} |
|
||||
> |
|
||||
{this.getNavMenuItems(item.children)} |
|
||||
</SubMenu> |
|
||||
); |
|
||||
} |
|
||||
return <Menu.Item key={item.path}>{this.getMenuItemPath(item)}</Menu.Item>; |
|
||||
}; |
|
||||
|
|
||||
/** |
|
||||
* 判断是否是http链接.返回 Link 或 a |
|
||||
* Judge whether it is http link.return a or Link |
|
||||
* @memberof SiderMenu |
|
||||
*/ |
|
||||
getMenuItemPath = (item: MenuDataItem) => { |
|
||||
const { name } = item; |
|
||||
const itemPath = this.conversionPath(item.path); |
|
||||
const icon = getIcon(item.icon); |
|
||||
const { target } = item; |
|
||||
// Is it a http link
|
|
||||
if (/^https?:\/\//.test(itemPath)) { |
|
||||
return ( |
|
||||
<a href={itemPath} target={target}> |
|
||||
{icon} |
|
||||
<span>{name}</span> |
|
||||
</a> |
|
||||
); |
|
||||
} |
|
||||
const { location, isMobile, onCollapse } = this.props; |
|
||||
return ( |
|
||||
<Link |
|
||||
to={itemPath} |
|
||||
target={target} |
|
||||
replace={itemPath === location!.pathname} |
|
||||
onClick={isMobile ? () => onCollapse!(true) : void 0} |
|
||||
> |
|
||||
{icon} |
|
||||
<span>{name}</span> |
|
||||
</Link> |
|
||||
); |
|
||||
}; |
|
||||
|
|
||||
conversionPath = (path: string) => { |
|
||||
if (path && path.indexOf('http') === 0) { |
|
||||
return path; |
|
||||
} |
|
||||
return `/${path || ''}`.replace(/\/+/g, '/'); |
|
||||
}; |
|
||||
|
|
||||
render() { |
|
||||
const { |
|
||||
openKeys, |
|
||||
theme, |
|
||||
mode, |
|
||||
location, |
|
||||
className, |
|
||||
collapsed, |
|
||||
handleOpenChange, |
|
||||
style, |
|
||||
menuData, |
|
||||
} = this.props; |
|
||||
// if pathname can't match, use the nearest parent's key
|
|
||||
let selectedKeys = this.getSelectedMenuKeys(location!.pathname); |
|
||||
if (!selectedKeys.length && openKeys) { |
|
||||
selectedKeys = [openKeys[openKeys.length - 1]]; |
|
||||
} |
|
||||
let props = {}; |
|
||||
if (openKeys && !collapsed) { |
|
||||
props = { |
|
||||
openKeys: openKeys.length === 0 ? [...selectedKeys] : openKeys, |
|
||||
}; |
|
||||
} |
|
||||
const cls = classNames(className, { |
|
||||
'top-nav-menu': mode === 'horizontal', |
|
||||
}); |
|
||||
|
|
||||
return ( |
|
||||
<Menu |
|
||||
key="Menu" |
|
||||
mode={mode} |
|
||||
theme={theme} |
|
||||
onOpenChange={handleOpenChange} |
|
||||
selectedKeys={selectedKeys} |
|
||||
style={style} |
|
||||
className={cls} |
|
||||
{...props} |
|
||||
> |
|
||||
{this.getNavMenuItems(menuData)} |
|
||||
</Menu> |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
@ -1,124 +0,0 @@ |
|||||
import { Layout } from 'antd'; |
|
||||
import classNames from 'classnames'; |
|
||||
import React, { Component, Suspense } from 'react'; |
|
||||
import Link from 'umi/link'; |
|
||||
import defaultSettings from '../../../config/defaultSettings'; |
|
||||
import PageLoading from '../PageLoading'; |
|
||||
import { BaseMenuProps } from './BaseMenu'; |
|
||||
import styles from './index.less'; |
|
||||
import { getDefaultCollapsedSubMenus } from './SiderMenuUtils'; |
|
||||
|
|
||||
const BaseMenu = React.lazy(() => import('./BaseMenu')); |
|
||||
const { Sider } = Layout; |
|
||||
const { title } = defaultSettings; |
|
||||
let firstMount: boolean = true; |
|
||||
|
|
||||
export interface SiderMenuProps extends BaseMenuProps { |
|
||||
logo?: string; |
|
||||
fixSiderbar?: boolean; |
|
||||
} |
|
||||
|
|
||||
interface SiderMenuState { |
|
||||
pathname?: string; |
|
||||
openKeys?: string[]; |
|
||||
flatMenuKeysLen?: number; |
|
||||
} |
|
||||
|
|
||||
export default class SiderMenu extends Component<SiderMenuProps, SiderMenuState> { |
|
||||
static defaultProps: Partial<SiderMenuProps> = { |
|
||||
flatMenuKeys: [], |
|
||||
onCollapse: () => void 0, |
|
||||
isMobile: false, |
|
||||
openKeys: [], |
|
||||
collapsed: false, |
|
||||
handleOpenChange: () => void 0, |
|
||||
menuData: [], |
|
||||
onOpenChange: () => void 0, |
|
||||
}; |
|
||||
|
|
||||
static getDerivedStateFromProps(props: SiderMenuProps, state: SiderMenuState) { |
|
||||
const { pathname, flatMenuKeysLen } = state; |
|
||||
if (props.location!.pathname !== pathname || props.flatMenuKeys!.length !== flatMenuKeysLen) { |
|
||||
return { |
|
||||
pathname: props.location!.pathname, |
|
||||
flatMenuKeysLen: props.flatMenuKeys!.length, |
|
||||
openKeys: getDefaultCollapsedSubMenus(props), |
|
||||
}; |
|
||||
} |
|
||||
return null; |
|
||||
} |
|
||||
|
|
||||
constructor(props: SiderMenuProps) { |
|
||||
super(props); |
|
||||
this.state = { |
|
||||
openKeys: getDefaultCollapsedSubMenus(props), |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
componentDidMount() { |
|
||||
firstMount = false; |
|
||||
} |
|
||||
|
|
||||
isMainMenu: (key: string) => boolean = key => { |
|
||||
const { menuData } = this.props; |
|
||||
return menuData!.some(item => { |
|
||||
if (key) { |
|
||||
return item.key === key || item.path === key; |
|
||||
} |
|
||||
return false; |
|
||||
}); |
|
||||
}; |
|
||||
|
|
||||
handleOpenChange: (openKeys: string[]) => void = openKeys => { |
|
||||
const moreThanOne = openKeys.filter(openKey => this.isMainMenu(openKey)).length > 1; |
|
||||
if (moreThanOne) { |
|
||||
this.setState({ openKeys: [openKeys.pop()].filter(item => item) as string[] }); |
|
||||
} else { |
|
||||
this.setState({ openKeys: [...openKeys] }); |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
render() { |
|
||||
const { logo, collapsed, onCollapse, fixSiderbar, theme, isMobile } = this.props; |
|
||||
const { openKeys } = this.state; |
|
||||
const defaultProps = collapsed ? {} : { openKeys }; |
|
||||
|
|
||||
const siderClassName = classNames(styles.sider, { |
|
||||
[styles.fixSiderBar]: fixSiderbar, |
|
||||
[styles.light]: theme === 'light', |
|
||||
}); |
|
||||
return ( |
|
||||
<Sider |
|
||||
collapsible |
|
||||
trigger={null} |
|
||||
collapsed={collapsed} |
|
||||
breakpoint="lg" |
|
||||
onCollapse={collapse => { |
|
||||
if (firstMount || !isMobile) { |
|
||||
onCollapse!(collapse); |
|
||||
} |
|
||||
}} |
|
||||
width={256} |
|
||||
theme={theme} |
|
||||
className={siderClassName} |
|
||||
> |
|
||||
<div className={styles.logo} id="logo"> |
|
||||
<Link to="/"> |
|
||||
<img src={logo} alt="logo" /> |
|
||||
<h1>{title}</h1> |
|
||||
</Link> |
|
||||
</div> |
|
||||
<Suspense fallback={<PageLoading />}> |
|
||||
<BaseMenu |
|
||||
{...this.props} |
|
||||
mode="inline" |
|
||||
handleOpenChange={this.handleOpenChange} |
|
||||
onOpenChange={this.handleOpenChange} |
|
||||
style={{ padding: '16px 0', width: '100%' }} |
|
||||
{...defaultProps} |
|
||||
/> |
|
||||
</Suspense> |
|
||||
</Sider> |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
@ -1,33 +0,0 @@ |
|||||
import pathToRegexp from 'path-to-regexp'; |
|
||||
import { urlToList } from '../_utils/pathTools'; |
|
||||
import { MenuDataItem, BaseMenuProps } from './BaseMenu'; |
|
||||
|
|
||||
/** |
|
||||
* Recursively flatten the data |
|
||||
* [{path:string},{path:string}] => {path,path2} |
|
||||
* @param menus |
|
||||
*/ |
|
||||
export const getFlatMenuKeys = (menuData: MenuDataItem[] = []) => { |
|
||||
let keys: string[] = []; |
|
||||
menuData.forEach(item => { |
|
||||
keys.push(item.path); |
|
||||
if (item.children) { |
|
||||
keys = keys.concat(getFlatMenuKeys(item.children)); |
|
||||
} |
|
||||
}); |
|
||||
return keys; |
|
||||
}; |
|
||||
|
|
||||
export const getMenuMatches = (flatMenuKeys: string[] = [], path: string) => |
|
||||
flatMenuKeys.filter(item => item && pathToRegexp(item).test(path)); |
|
||||
|
|
||||
/** |
|
||||
* 获得菜单子节点 |
|
||||
*/ |
|
||||
export const getDefaultCollapsedSubMenus = (props: BaseMenuProps) => { |
|
||||
const { location, flatMenuKeys } = props; |
|
||||
return urlToList(location!.pathname) |
|
||||
.map(item => getMenuMatches(flatMenuKeys, item)[0]) |
|
||||
.filter(item => item) |
|
||||
.reduce((acc, curr) => [...acc, curr], ['/']); |
|
||||
}; |
|
||||
@ -1,105 +0,0 @@ |
|||||
@import '~antd/lib/style/themes/default.less'; |
|
||||
|
|
||||
@nav-header-height: @layout-header-height; |
|
||||
|
|
||||
.logo { |
|
||||
position: relative; |
|
||||
height: @nav-header-height; |
|
||||
padding-left: (@menu-collapsed-width - 32px) / 2; |
|
||||
overflow: hidden; |
|
||||
line-height: @nav-header-height; |
|
||||
background: #002140; |
|
||||
transition: all 0.3s; |
|
||||
img { |
|
||||
display: inline-block; |
|
||||
height: 32px; |
|
||||
vertical-align: middle; |
|
||||
} |
|
||||
h1 { |
|
||||
display: inline-block; |
|
||||
margin: 0 0 0 12px; |
|
||||
color: white; |
|
||||
font-weight: 600; |
|
||||
font-size: 20px; |
|
||||
font-family: Avenir, 'Helvetica Neue', Arial, Helvetica, sans-serif; |
|
||||
vertical-align: middle; |
|
||||
} |
|
||||
} |
|
||||
.sider { |
|
||||
position: relative; |
|
||||
z-index: 10; |
|
||||
min-height: 100vh; |
|
||||
box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35); |
|
||||
&.fixSiderBar { |
|
||||
position: fixed; |
|
||||
top: 0; |
|
||||
left: 0; |
|
||||
box-shadow: 2px 0 8px 0 rgba(29, 35, 41, 0.05); |
|
||||
:global { |
|
||||
.ant-menu-root { |
|
||||
height: ~'calc(100vh - @{nav-header-height})'; |
|
||||
overflow-y: auto; |
|
||||
} |
|
||||
.ant-menu-inline { |
|
||||
border-right: 0; |
|
||||
.ant-menu-item, |
|
||||
.ant-menu-submenu-title { |
|
||||
width: 100%; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
&.light { |
|
||||
background-color: white; |
|
||||
box-shadow: 2px 0 8px 0 rgba(29, 35, 41, 0.05); |
|
||||
.logo { |
|
||||
background: white; |
|
||||
box-shadow: 1px 1px 0 0 @border-color-split; |
|
||||
h1 { |
|
||||
color: @primary-color; |
|
||||
} |
|
||||
} |
|
||||
:global(.ant-menu-light) { |
|
||||
border-right-color: transparent; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.icon { |
|
||||
width: 14px; |
|
||||
vertical-align: baseline; |
|
||||
} |
|
||||
|
|
||||
:global { |
|
||||
.top-nav-menu li.ant-menu-item { |
|
||||
height: @nav-header-height; |
|
||||
line-height: @nav-header-height; |
|
||||
} |
|
||||
.drawer .drawer-content { |
|
||||
background: #001529; |
|
||||
} |
|
||||
.ant-menu-inline-collapsed { |
|
||||
& > .ant-menu-item .sider-menu-item-img + span, |
|
||||
& |
|
||||
> .ant-menu-item-group |
|
||||
> .ant-menu-item-group-list |
|
||||
> .ant-menu-item |
|
||||
.sider-menu-item-img |
|
||||
+ span, |
|
||||
& > .ant-menu-submenu > .ant-menu-submenu-title .sider-menu-item-img + span { |
|
||||
display: inline-block; |
|
||||
max-width: 0; |
|
||||
opacity: 0; |
|
||||
} |
|
||||
} |
|
||||
.ant-menu-item .sider-menu-item-img + span, |
|
||||
.ant-menu-submenu-title .sider-menu-item-img + span { |
|
||||
opacity: 1; |
|
||||
transition: opacity 0.3s @ease-in-out, width 0.3s @ease-in-out; |
|
||||
} |
|
||||
.ant-drawer-left { |
|
||||
.ant-drawer-body { |
|
||||
padding: 0; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,34 +0,0 @@ |
|||||
import React from 'react'; |
|
||||
import { Drawer } from 'antd'; |
|
||||
import { SiderMenuProps } from './SiderMenu'; |
|
||||
import SiderMenu from './SiderMenu'; |
|
||||
import { getFlatMenuKeys } from './SiderMenuUtils'; |
|
||||
|
|
||||
export { SiderMenuProps }; |
|
||||
export { MenuDataItem, Route } from './BaseMenu'; |
|
||||
|
|
||||
const SiderMenuWrapper: React.FC<SiderMenuProps> = props => { |
|
||||
const { isMobile, menuData, collapsed, onCollapse } = props; |
|
||||
const flatMenuKeys = getFlatMenuKeys(menuData); |
|
||||
return isMobile ? ( |
|
||||
<Drawer |
|
||||
visible={!collapsed} |
|
||||
placement="left" |
|
||||
onClose={() => onCollapse!(true)} |
|
||||
style={{ |
|
||||
padding: 0, |
|
||||
height: '100vh', |
|
||||
}} |
|
||||
> |
|
||||
<SiderMenu {...props} flatMenuKeys={flatMenuKeys} collapsed={isMobile ? false : collapsed} /> |
|
||||
</Drawer> |
|
||||
) : ( |
|
||||
<SiderMenu {...props} flatMenuKeys={flatMenuKeys} /> |
|
||||
); |
|
||||
}; |
|
||||
|
|
||||
SiderMenuWrapper.defaultProps = { |
|
||||
onCollapse: () => void 0, |
|
||||
}; |
|
||||
|
|
||||
export default React.memo(SiderMenuWrapper); |
|
||||
@ -1,72 +0,0 @@ |
|||||
@import '~antd/lib/style/themes/default.less'; |
|
||||
|
|
||||
.head { |
|
||||
position: relative; |
|
||||
width: 100%; |
|
||||
height: @layout-header-height; |
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08); |
|
||||
transition: background 0.3s, width 0.2s; |
|
||||
:global { |
|
||||
.ant-menu-submenu.ant-menu-submenu-horizontal { |
|
||||
height: 100%; |
|
||||
line-height: @layout-header-height; |
|
||||
.ant-menu-submenu-title { |
|
||||
height: 100%; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
&.light { |
|
||||
background-color: #fff; |
|
||||
} |
|
||||
.main { |
|
||||
display: flex; |
|
||||
height: @layout-header-height; |
|
||||
padding-left: 24px; |
|
||||
&.wide { |
|
||||
max-width: 1200px; |
|
||||
margin: auto; |
|
||||
padding-left: 0; |
|
||||
} |
|
||||
.left { |
|
||||
display: flex; |
|
||||
flex: 1; |
|
||||
} |
|
||||
.right { |
|
||||
width: 324px; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.logo { |
|
||||
position: relative; |
|
||||
width: 165px; |
|
||||
height: @layout-header-height; |
|
||||
overflow: hidden; |
|
||||
line-height: @layout-header-height; |
|
||||
transition: all 0.3s; |
|
||||
img { |
|
||||
display: inline-block; |
|
||||
height: 32px; |
|
||||
vertical-align: middle; |
|
||||
} |
|
||||
h1 { |
|
||||
display: inline-block; |
|
||||
margin: 0 0 0 12px; |
|
||||
color: #fff; |
|
||||
font-weight: 400; |
|
||||
font-size: 16px; |
|
||||
vertical-align: top; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.light { |
|
||||
h1 { |
|
||||
color: #002140; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.menu { |
|
||||
height: @layout-header-height; |
|
||||
line-height: @layout-header-height; |
|
||||
border: none; |
|
||||
} |
|
||||
@ -1,59 +0,0 @@ |
|||||
import { SiderMenuProps } from '@/components/SiderMenu'; |
|
||||
import React, { Component } from 'react'; |
|
||||
import Link from 'umi/link'; |
|
||||
import RightContent, { GlobalHeaderRightProps } from '../GlobalHeader/RightContent'; |
|
||||
import BaseMenu from '../SiderMenu/BaseMenu'; |
|
||||
import { getFlatMenuKeys } from '../SiderMenu/SiderMenuUtils'; |
|
||||
import styles from './index.less'; |
|
||||
import defaultSettings, { ContentWidth } from '../../../config/defaultSettings'; |
|
||||
|
|
||||
export interface TopNavHeaderProps extends SiderMenuProps, GlobalHeaderRightProps { |
|
||||
contentWidth?: ContentWidth; |
|
||||
} |
|
||||
|
|
||||
interface TopNavHeaderState { |
|
||||
maxWidth?: number; |
|
||||
} |
|
||||
|
|
||||
export default class TopNavHeader extends Component<TopNavHeaderProps, TopNavHeaderState> { |
|
||||
static getDerivedStateFromProps(props: TopNavHeaderProps) { |
|
||||
return { |
|
||||
maxWidth: |
|
||||
(props.contentWidth === 'Fixed' && window.innerWidth > 1200 ? 1200 : window.innerWidth) - |
|
||||
280 - |
|
||||
120 - |
|
||||
40, |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
state: TopNavHeaderState = {}; |
|
||||
|
|
||||
maim: HTMLDivElement | null = null; |
|
||||
|
|
||||
render() { |
|
||||
const { theme, contentWidth, menuData, logo } = this.props; |
|
||||
const { maxWidth } = this.state; |
|
||||
const flatMenuKeys = getFlatMenuKeys(menuData); |
|
||||
return ( |
|
||||
<div className={`${styles.head} ${theme === 'light' ? styles.light : ''}`}> |
|
||||
<div |
|
||||
ref={ref => (this.maim = ref)} |
|
||||
className={`${styles.main} ${contentWidth === 'Fixed' ? styles.wide : ''}`} |
|
||||
> |
|
||||
<div className={styles.left}> |
|
||||
<div className={styles.logo} key="logo" id="logo"> |
|
||||
<Link to="/"> |
|
||||
<img src={logo} alt="logo" /> |
|
||||
<h1>{defaultSettings.title}</h1> |
|
||||
</Link> |
|
||||
</div> |
|
||||
<div style={{ maxWidth }}> |
|
||||
<BaseMenu {...this.props} flatMenuKeys={flatMenuKeys} className={styles.menu} /> |
|
||||
</div> |
|
||||
</div> |
|
||||
<RightContent {...this.props} /> |
|
||||
</div> |
|
||||
</div> |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
@ -1,6 +0,0 @@ |
|||||
@import '~antd/lib/style/themes/default.less'; |
|
||||
|
|
||||
.content { |
|
||||
margin: 24px; |
|
||||
padding-top: @layout-header-height; |
|
||||
} |
|
||||
@ -1,37 +0,0 @@ |
|||||
import { Icon, Layout } from 'antd'; |
|
||||
import React, { Fragment } from 'react'; |
|
||||
import { GlobalFooter } from 'ant-design-pro'; |
|
||||
|
|
||||
const { Footer } = Layout; |
|
||||
const FooterView = () => ( |
|
||||
<Footer style={{ padding: 0 }}> |
|
||||
<GlobalFooter |
|
||||
links={[ |
|
||||
{ |
|
||||
key: 'Pro 首页', |
|
||||
title: 'Pro 首页', |
|
||||
href: 'https://pro.ant.design', |
|
||||
blankTarget: true, |
|
||||
}, |
|
||||
{ |
|
||||
key: 'github', |
|
||||
title: <Icon type="github" />, |
|
||||
href: 'https://github.com/ant-design/ant-design-pro', |
|
||||
blankTarget: true, |
|
||||
}, |
|
||||
{ |
|
||||
key: 'Ant Design', |
|
||||
title: 'Ant Design', |
|
||||
href: 'https://ant.design', |
|
||||
blankTarget: true, |
|
||||
}, |
|
||||
]} |
|
||||
copyright={ |
|
||||
<Fragment> |
|
||||
Copyright <Icon type="copyright" /> 2019 蚂蚁金服体验技术部出品 |
|
||||
</Fragment> |
|
||||
} |
|
||||
/> |
|
||||
</Footer> |
|
||||
); |
|
||||
export default FooterView; |
|
||||
@ -1,8 +0,0 @@ |
|||||
.fixedHeader { |
|
||||
position: fixed; |
|
||||
top: 0; |
|
||||
right: 0; |
|
||||
z-index: 9; |
|
||||
width: 100%; |
|
||||
transition: width 0.2s; |
|
||||
} |
|
||||
@ -1,179 +0,0 @@ |
|||||
import GlobalHeader, { GlobalHeaderProps } from '@/components/GlobalHeader'; |
|
||||
import TopNavHeader, { TopNavHeaderProps } from '@/components/TopNavHeader'; |
|
||||
import { ConnectProps, ConnectState, SettingModelState } from '@/models/connect'; |
|
||||
import React, { Component } from 'react'; |
|
||||
import { formatMessage } from 'umi-plugin-react/locale'; |
|
||||
import { Layout, message } from 'antd'; |
|
||||
import { ClickParam } from 'antd/es/menu'; |
|
||||
import { connect } from 'dva'; |
|
||||
import Animate from 'rc-animate'; |
|
||||
import router from 'umi/router'; |
|
||||
import styles from './Header.less'; |
|
||||
|
|
||||
const { Header } = Layout; |
|
||||
|
|
||||
export interface HeaderViewProps extends ConnectProps, TopNavHeaderProps, GlobalHeaderProps { |
|
||||
isMobile?: boolean; |
|
||||
collapsed?: boolean; |
|
||||
setting?: SettingModelState; |
|
||||
autoHideHeader?: boolean; |
|
||||
handleMenuCollapse?: (collapse: boolean) => void; |
|
||||
} |
|
||||
|
|
||||
interface HeaderViewState { |
|
||||
visible: boolean; |
|
||||
} |
|
||||
|
|
||||
class HeaderView extends Component<HeaderViewProps, HeaderViewState> { |
|
||||
static getDerivedStateFromProps(props: HeaderViewProps, state: HeaderViewState) { |
|
||||
if (!props.autoHideHeader && !state.visible) { |
|
||||
return { |
|
||||
visible: true, |
|
||||
}; |
|
||||
} |
|
||||
return null; |
|
||||
} |
|
||||
state = { |
|
||||
visible: true, |
|
||||
}; |
|
||||
|
|
||||
ticking: boolean = false; |
|
||||
oldScrollTop: number = 0; |
|
||||
|
|
||||
componentDidMount() { |
|
||||
document.addEventListener('scroll', this.handScroll, { passive: true }); |
|
||||
} |
|
||||
|
|
||||
componentWillUnmount() { |
|
||||
document.removeEventListener('scroll', this.handScroll); |
|
||||
} |
|
||||
|
|
||||
getHeadWidth = () => { |
|
||||
const { isMobile, collapsed, setting } = this.props; |
|
||||
const { fixedHeader, layout } = setting!; |
|
||||
if (isMobile || !fixedHeader || layout === 'topmenu') { |
|
||||
return '100%'; |
|
||||
} |
|
||||
return collapsed ? 'calc(100% - 80px)' : 'calc(100% - 256px)'; |
|
||||
}; |
|
||||
|
|
||||
handleNoticeClear = (type: string) => { |
|
||||
const { dispatch } = this.props; |
|
||||
message.success( |
|
||||
`${formatMessage({ id: 'component.noticeIcon.cleared' })} ${formatMessage({ |
|
||||
id: `component.globalHeader.${type}`, |
|
||||
})}`,
|
|
||||
); |
|
||||
dispatch!({ |
|
||||
type: 'global/clearNotices', |
|
||||
payload: type, |
|
||||
}); |
|
||||
}; |
|
||||
|
|
||||
handleMenuClick = ({ key }: ClickParam) => { |
|
||||
const { dispatch } = this.props; |
|
||||
if (key === 'userCenter') { |
|
||||
router.push('/account/center'); |
|
||||
return; |
|
||||
} |
|
||||
if (key === 'triggerError') { |
|
||||
router.push('/exception/trigger'); |
|
||||
return; |
|
||||
} |
|
||||
if (key === 'userinfo') { |
|
||||
router.push('/account/settings/base'); |
|
||||
return; |
|
||||
} |
|
||||
if (key === 'logout') { |
|
||||
dispatch!({ |
|
||||
type: 'login/logout', |
|
||||
}); |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
handleNoticeVisibleChange = (visible: boolean) => { |
|
||||
if (visible) { |
|
||||
const { dispatch } = this.props; |
|
||||
dispatch!({ |
|
||||
type: 'global/fetchNotices', |
|
||||
}); |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
handScroll = () => { |
|
||||
const { autoHideHeader } = this.props; |
|
||||
const { visible } = this.state; |
|
||||
if (!autoHideHeader) { |
|
||||
return; |
|
||||
} |
|
||||
const scrollTop = document.body.scrollTop + document.documentElement.scrollTop; |
|
||||
if (!this.ticking) { |
|
||||
this.ticking = true; |
|
||||
requestAnimationFrame(() => { |
|
||||
if (this.oldScrollTop > scrollTop) { |
|
||||
this.setState({ |
|
||||
visible: true, |
|
||||
}); |
|
||||
} else if (scrollTop > 300 && visible) { |
|
||||
this.setState({ |
|
||||
visible: false, |
|
||||
}); |
|
||||
} else if (scrollTop < 300 && !visible) { |
|
||||
this.setState({ |
|
||||
visible: true, |
|
||||
}); |
|
||||
} |
|
||||
this.oldScrollTop = scrollTop; |
|
||||
this.ticking = false; |
|
||||
}); |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
render() { |
|
||||
const { isMobile, handleMenuCollapse, setting } = this.props; |
|
||||
const { navTheme, layout, fixedHeader } = setting!; |
|
||||
const { visible } = this.state; |
|
||||
const isTop = layout === 'topmenu'; |
|
||||
const width = this.getHeadWidth(); |
|
||||
const HeaderDom = visible ? ( |
|
||||
<Header |
|
||||
style={{ padding: 0, width, zIndex: 2 }} |
|
||||
className={fixedHeader ? styles.fixedHeader : ''} |
|
||||
> |
|
||||
{isTop && !isMobile ? ( |
|
||||
<TopNavHeader |
|
||||
theme={navTheme} |
|
||||
mode="horizontal" |
|
||||
onCollapse={handleMenuCollapse} |
|
||||
onNoticeClear={this.handleNoticeClear} |
|
||||
onMenuClick={this.handleMenuClick} |
|
||||
onNoticeVisibleChange={this.handleNoticeVisibleChange} |
|
||||
{...this.props} |
|
||||
/> |
|
||||
) : ( |
|
||||
<GlobalHeader |
|
||||
onCollapse={handleMenuCollapse} |
|
||||
onNoticeClear={this.handleNoticeClear} |
|
||||
onMenuClick={this.handleMenuClick} |
|
||||
onNoticeVisibleChange={this.handleNoticeVisibleChange} |
|
||||
{...this.props} |
|
||||
/> |
|
||||
)} |
|
||||
</Header> |
|
||||
) : null; |
|
||||
return ( |
|
||||
<Animate component="" transitionName="fade"> |
|
||||
{HeaderDom} |
|
||||
</Animate> |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default connect(({ user, global, setting, loading }: ConnectState) => ({ |
|
||||
currentUser: user.currentUser, |
|
||||
collapsed: global.collapsed, |
|
||||
fetchingMoreNotices: loading.effects['global/fetchMoreNotices'], |
|
||||
fetchingNotices: loading.effects['global/fetchNotices'], |
|
||||
notices: global.notices, |
|
||||
setting, |
|
||||
}))(HeaderView); |
|
||||
Loading…
Reference in new issue