Commit d1b6e0fe authored by ι™ˆεΈ…'s avatar ι™ˆεΈ…

use pro-layout Components

parent f5b34755
......@@ -56,6 +56,7 @@
"not ie <= 10"
],
"dependencies": {
"@ant-design/pro-layout": "^4.0.3",
"ant-design-pro": "^2.3.0",
"antd": "^3.15.0",
"classnames": "^2.2.6",
......
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>
);
}
}
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 });
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;
.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;
}
}
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;
@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;
}
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;
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>
);
}
}
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>
);
}
}
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], ['/']);
};
@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;
}
}
}
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);
@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;
}
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>
);
}
}
@import '~antd/lib/style/themes/default.less';
.content {
margin: 24px;
padding-top: @layout-header-height;
}
import SiderMenu, { MenuDataItem, SiderMenuProps } from '@/components/SiderMenu';
import { ConnectProps, ConnectState, SettingModelState } from '@/models/connect';
import getPageTitle from '@/utils/getPageTitle';
import { Layout } from 'antd';
import classNames from 'classnames';
import { ConnectState, ConnectProps } from '@/models/connect';
import { formatMessage } from 'umi-plugin-react/locale';
import { connect } from 'dva';
import React, { Suspense, useState } from 'react';
import { ContainerQuery } from 'react-container-query';
import DocumentTitle from 'react-document-title';
import useMedia from 'react-media-hook2';
import React, { useState } from 'react';
import logo from '../assets/logo.svg';
import styles from './BasicLayout.less';
import Footer from './Footer';
import Header, { HeaderViewProps } from './Header';
import PageHeaderWrapper from '@/components/PageHeaderWrapper';
// lazy load SettingDrawer
const SettingDrawer = React.lazy(() => import('@/components/SettingDrawer'));
import {
BasicLayout as BasicLayoutComponents,
BasicLayoutProps as BasicLayoutComponentsProps,
MenuDataItem,
Settings,
} from '@ant-design/pro-layout';
const { Content } = Layout;
const query = {
'screen-xs': {
maxWidth: 575,
},
'screen-sm': {
minWidth: 576,
maxWidth: 767,
},
'screen-md': {
minWidth: 768,
maxWidth: 991,
},
'screen-lg': {
minWidth: 992,
maxWidth: 1199,
},
'screen-xl': {
minWidth: 1200,
maxWidth: 1599,
},
'screen-xxl': {
minWidth: 1600,
},
};
export interface BasicLayoutProps
extends ConnectProps,
SiderMenuProps,
HeaderViewProps,
Partial<SettingModelState> {
export interface BasicLayoutProps extends BasicLayoutComponentsProps, ConnectProps {
breadcrumbNameMap: { [path: string]: MenuDataItem };
settings: Settings;
}
export type BasicLayoutContext = { [K in 'location']: BasicLayoutProps[K] } & {
......@@ -57,89 +21,43 @@ export type BasicLayoutContext = { [K in 'location']: BasicLayoutProps[K] } & {
};
const BasicLayout: React.FC<BasicLayoutProps> = props => {
const {
breadcrumbNameMap,
dispatch,
children,
collapsed,
fixedHeader,
fixSiderbar,
layout: PropsLayout,
location,
menuData,
navTheme,
route,
} = props;
const { dispatch, children, route } = props;
const { routes, authority } = route!;
/**
* constructor
*/
useState(() => {
dispatch!({ type: 'user/fetchCurrent' });
dispatch!({ type: 'setting/getSetting' });
dispatch!({ type: 'settings/getSetting' });
dispatch!({ type: 'menu/getMenuData', payload: { routes, authority } });
});
/**
* init variables
*/
const isMobile = useMedia({ id: 'BasicLayout', query: '(max-width: 599px)' })[0];
const hasLeftPadding = fixSiderbar && PropsLayout !== 'topmenu' && !isMobile;
const handleMenuCollapse = (payload: boolean) =>
dispatch!({ type: 'global/changeLayoutCollapsed', payload });
// Do not render SettingDrawer in production
// unless it is deployed in preview.pro.ant.design as demo
const renderSettingDrawer = () =>
!(process.env.NODE_ENV === 'production' && APP_TYPE !== 'site') && <SettingDrawer />;
const layout = (
<Layout>
{PropsLayout === 'topmenu' && !isMobile ? null : (
<SiderMenu
logo={logo}
theme={navTheme}
onCollapse={handleMenuCollapse}
menuData={menuData}
isMobile={isMobile}
{...props}
/>
)}
<Layout
style={{
paddingLeft: hasLeftPadding ? (collapsed ? 80 : 256) : void 0,
minHeight: '100vh',
}}
>
<Header
menuData={menuData}
handleMenuCollapse={handleMenuCollapse}
logo={logo}
isMobile={isMobile}
{...props}
/>
<Content className={styles.content} style={!fixedHeader ? { paddingTop: 0 } : {}}>
<PageHeaderWrapper location={location} breadcrumbNameMap={breadcrumbNameMap}>
{children}
</PageHeaderWrapper>
</Content>
<Footer />
</Layout>
</Layout>
);
return (
<React.Fragment>
<DocumentTitle title={getPageTitle(location!.pathname, breadcrumbNameMap)}>
<ContainerQuery query={query}>
{params => <div className={classNames(params)}>{layout}</div>}
</ContainerQuery>
</DocumentTitle>
<Suspense fallback={null}>{renderSettingDrawer()}</Suspense>
</React.Fragment>
<BasicLayoutComponents
formatMessage={formatMessage}
logo={logo}
onChangeSetting={settings =>
dispatch!({
type: 'settings/changeSetting',
payload: settings,
})
}
onChangeLayoutCollapsed={handleMenuCollapse}
{...props}
>
{children}
</BasicLayoutComponents>
);
};
export default connect(({ global, setting, menu: menuModel }: ConnectState) => ({
export default connect(({ global, settings, menu: menuModel }: ConnectState) => ({
collapsed: global.collapsed,
layout: setting.layout,
settings,
menuData: menuModel.menuData,
breadcrumbNameMap: menuModel.breadcrumbNameMap,
...setting,
}))(BasicLayout);
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;
.fixedHeader {
position: fixed;
top: 0;
right: 0;
z-index: 9;
width: 100%;
transition: width 0.2s;
}
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);
import { Route } from '@/components/SiderMenu';
import { EffectsCommandMap } from 'dva';
import { AnyAction } from 'redux';
import { RouterTypes } from 'umi';
......@@ -6,7 +5,7 @@ import { GlobalModelState } from './global';
import { MenuModelState } from './menu';
import { UserModelState } from './user';
import { DefaultSettings as SettingModelState } from '../../config/defaultSettings';
import { MenuDataItem } from '@ant-design/pro-layout';
export { GlobalModelState, MenuModelState, SettingModelState, UserModelState };
export type Effect = (
......@@ -39,11 +38,15 @@ export interface Loading {
export interface ConnectState {
global: GlobalModelState;
loading: Loading;
settings: SettingModelState;
menu: MenuModelState;
setting: SettingModelState;
user: UserModelState;
}
export interface Route extends MenuDataItem {
routes?: Route[];
}
/**
* @type T: Params matched in dynamic routing
*/
......
......@@ -3,7 +3,7 @@ import { Reducer } from 'redux';
import defaultSettings, { DefaultSettings } from '../../config/defaultSettings';
export interface SettingModelType {
namespace: 'setting';
namespace: 'settings';
state: DefaultSettings;
reducers: {
getSetting: Reducer<any>;
......@@ -75,7 +75,7 @@ const updateColorWeak: (colorWeak: string) => void = colorWeak => {
};
const SettingModel: SettingModelType = {
namespace: 'setting',
namespace: 'settings',
state: defaultSettings,
reducers: {
getSetting(state) {
......
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