Merge pull request #15638 from ant-design/master

Merge master to feature
This commit is contained in:
偏右 2019-03-26 14:34:17 +08:00 committed by GitHub
commit e752ec3414
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 391 additions and 93 deletions

View File

@ -1,8 +1,11 @@
import raf from 'raf';
import React from 'react';
import { mount } from 'enzyme';
import delayRaf from '../raf';
import throttleByAnimationFrame from '../throttleByAnimationFrame';
import getDataOrAriaProps from '../getDataOrAriaProps';
import triggerEvent from '../triggerEvent';
import Wave from '../wave';
describe('Test utils function', () => {
beforeAll(() => {
@ -128,4 +131,33 @@ describe('Test utils function', () => {
triggerEvent(button, 'click');
expect(button.style.width).toBe('100px');
});
describe('wave', () => {
it('bindAnimationEvent should return when node is null', () => {
const wrapper = mount(
<Wave>
<button type="button" disabled />
</Wave>,
).instance();
expect(wrapper.bindAnimationEvent()).toBe(undefined);
});
it('bindAnimationEvent.onClick should return when children is hidden', () => {
const wrapper = mount(
<Wave>
<button type="button" style={{ display: 'none' }} />
</Wave>,
).instance();
expect(wrapper.bindAnimationEvent()).toBe(undefined);
});
it('bindAnimationEvent.onClick should return when children is input', () => {
const wrapper = mount(
<Wave>
<input />
</Wave>,
).instance();
expect(wrapper.bindAnimationEvent()).toBe(undefined);
});
});
});

View File

@ -127,4 +127,14 @@ describe('Affix Render', () => {
jest.runAllTimers();
expect(wrapper.instance().affix.state.affixStyle.top).toBe(10);
});
it('updatePosition when target changed', () => {
const container = '<div id="mounter" />';
const getTarget = () => container;
wrapper = mount(<Affix target={getTarget} />);
wrapper.setProps({ target: null });
expect(wrapper.instance().state.status).toBe(0);
expect(wrapper.instance().state.affixStyle).toBe(undefined);
expect(wrapper.instance().state.placeholderStyle).toBe(undefined);
});
});

View File

@ -147,7 +147,7 @@ export default class Header extends React.Component<HeaderProps, any> {
type === 'month'
? this.getMonthSelectElement(prefixCls, value.month(), this.getMonthsLocale(value))
: null;
const size = (fullscreen ? 'default' : 'small') as any;
const size = fullscreen ? 'default' : 'small';
const typeSwitch = (
<Group onChange={this.onTypeChange} value={type} size={size}>
<Button value="month">{locale.month}</Button>

View File

@ -3,6 +3,7 @@ import Moment from 'moment';
import { mount } from 'enzyme';
import MockDate from 'mockdate';
import Calendar from '..';
import Header from '../Header';
describe('Calendar', () => {
it('Calendar should be selectable', () => {
@ -175,4 +176,84 @@ describe('Calendar', () => {
expect(onPanelChange).toBeCalled();
expect(onPanelChange.mock.calls[0][1]).toEqual('year');
});
const createWrapper = (start, end, value, onValueChange) => {
const wrapper = mount(
<Header
onValueChange={onValueChange}
value={value}
validRange={[start, end]}
locale={{ year: '年' }}
/>,
);
wrapper
.find('.ant-fullcalendar-year-select')
.hostNodes()
.simulate('click');
wrapper
.find('.ant-select-dropdown-menu-item')
.at(0)
.simulate('click');
};
it('if value.month > end.month, set value.month to end.month', () => {
const value = new Moment('1990-01-03');
const start = new Moment('2019-04-01');
const end = new Moment('2019-11-01');
const onValueChange = jest.fn();
createWrapper(start, end, value, onValueChange);
expect(onValueChange).toHaveBeenCalledWith(value.year('2019').month('3'));
});
it('if start.month > value.month, set value.month to start.month ', () => {
const value = new Moment('1990-01-03');
const start = new Moment('2019-11-01');
const end = new Moment('2019-03-01');
const onValueChange = jest.fn();
createWrapper(start, end, value, onValueChange);
expect(onValueChange).toHaveBeenCalledWith(value.year('2019').month('10'));
});
it('onMonthChange should work correctly', () => {
const start = new Moment('2018-11-01');
const end = new Moment('2019-03-01');
const value = new Moment('2018-12-03');
const onValueChange = jest.fn();
const wrapper = mount(
<Header
onValueChange={onValueChange}
value={value}
validRange={[start, end]}
locale={{ year: '年' }}
type="month"
/>,
);
wrapper
.find('.ant-fullcalendar-month-select')
.hostNodes()
.simulate('click');
wrapper
.find('.ant-select-dropdown-menu-item')
.at(0)
.simulate('click');
expect(onValueChange).toHaveBeenCalledWith(value.month(10));
});
it('onTypeChange should work correctly', () => {
const onTypeChange = jest.fn();
const value = new Moment('2018-12-03');
const wrapper = mount(
<Header
onTypeChange={onTypeChange}
locale={{ year: '年', month: '月' }}
value={value}
type="date"
/>,
);
wrapper
.find('input')
.at(1)
.simulate('change');
expect(onTypeChange).toBeCalledWith('year');
});
});

View File

@ -55,4 +55,62 @@ describe('Card', () => {
);
expect(wrapper.render()).toMatchSnapshot();
});
it('warning', () => {
const warnSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
mount(<Card noHovering>xxx</Card>);
expect(warnSpy).toBeCalledWith(
'Warning: [antd: Card] `noHovering` is deprecated, you can remove it safely or use `hoverable` instead.',
);
mount(<Card noHovering={false}>xxx</Card>);
expect(warnSpy).toBeCalledWith(
'Warning: [antd: Card] `noHovering={false}` is deprecated, use `hoverable` instead.',
);
warnSpy.mockRestore();
});
it('unmount', () => {
const wrapper = mount(<Card>xxx</Card>);
const removeResizeEventSpy = jest.spyOn(wrapper.instance().resizeEvent, 'remove');
wrapper.unmount();
expect(removeResizeEventSpy).toHaveBeenCalled();
});
it('onTabChange should work', () => {
const tabList = [
{
key: 'tab1',
tab: 'tab1',
},
{
key: 'tab2',
tab: 'tab2',
},
];
const onTabChange = jest.fn();
const wrapper = mount(
<Card onTabChange={onTabChange} tabList={tabList}>
xxx
</Card>,
);
wrapper
.find('.ant-tabs-tab')
.at(1)
.simulate('click');
expect(onTabChange).toBeCalledWith('tab2');
});
it('getCompatibleHoverable should work', () => {
const wrapper = mount(<Card noHovering={false}>xxx</Card>);
expect(wrapper.find('.ant-card-hoverable').length).toBe(1);
});
it('should not render when actions is number', () => {
const wrapper = mount(
<Card title="Card title" actions={11}>
<p>Card content</p>
</Card>,
);
expect(wrapper.find('.ant-card-actions').length).toBe(0);
});
});

View File

@ -129,9 +129,6 @@ export default class Card extends React.Component<CardProps, CardState> {
}
getAction(actions: React.ReactNode[]) {
if (!actions || !actions.length) {
return null;
}
const actionList = actions.map((action, index) => (
<li style={{ width: `${100 / actions.length}%` }} key={`action-${index}`}>
<span>{action}</span>

View File

@ -230,6 +230,7 @@
}
&-disabled-cell &-date {
position: relative;
width: auto;
color: @disabled-color;
background: @disabled-bg;
@ -241,6 +242,18 @@
background: @disabled-bg;
}
}
&-disabled-cell&-selected-day &-date::before {
position: absolute;
top: -1px;
left: 5px;
width: 24px;
height: 24px;
background: rgba(0, 0, 0, 0.1);
border-radius: @border-radius-sm;
content: '';
}
&-disabled-cell&-today &-date {
position: relative;
padding-right: 5px;

View File

@ -348,9 +348,6 @@ export default class FormItem extends React.Component<FormItemProps, any> {
labelAlign === 'left' && `${labelClsBasic}-left`,
mergedLabelCol.className,
);
const labelClassName = classNames({
[`${prefixCls}-item-required`]: required,
});
let labelChildren = label;
// Keep label is original where there should have no colon
@ -361,6 +358,11 @@ export default class FormItem extends React.Component<FormItemProps, any> {
labelChildren = (label as string).replace(/[|:]\s*$/, '');
}
const labelClassName = classNames({
[`${prefixCls}-item-required`]: required,
[`${prefixCls}-item-no-colon`]: !computedColon,
});
return label ? (
<Col {...mergedLabelCol} className={labelColClassName}>
<label
@ -395,28 +397,19 @@ export default class FormItem extends React.Component<FormItemProps, any> {
}
renderFormItem = ({ getPrefixCls }: ConfigConsumerProps) => {
const { prefixCls: customizePrefixCls, style, className } = this.props;
const prefixCls = getPrefixCls('form', customizePrefixCls);
const children = this.renderChildren(prefixCls);
const itemClassName = {
[`${prefixCls}-item`]: true,
[`${prefixCls}-item-with-help`]: this.helpShow,
[`${className}`]: !!className,
};
return (
<FormContext.Consumer key="row">
{({ colon: contextColon }: FormContextProps) => {
const { prefixCls: customizePrefixCls, style, colon, className } = this.props;
const computedColon = colon === true || (contextColon !== false && colon !== false);
const prefixCls = getPrefixCls('form', customizePrefixCls);
const children = this.renderChildren(prefixCls);
const itemClassName = {
[`${prefixCls}-item`]: true,
[`${prefixCls}-item-with-help`]: this.helpShow,
[`${prefixCls}-item-no-colon`]: !computedColon,
[`${className}`]: !!className,
};
return (
<Row className={classNames(itemClassName)} style={style}>
{children}
</Row>
);
}}
</FormContext.Consumer>
<Row className={classNames(itemClassName)} style={style} key="row">
{children}
</Row>
);
};

View File

@ -2121,7 +2121,7 @@ exports[`renders ./components/form/demo/style-check-debug.md correctly 1`] = `
class="ant-form-item-label"
>
<label
class=""
class="ant-form-item-no-colon"
title="input:64.5px"
>
input:64.5px

View File

@ -5,13 +5,13 @@ exports[`Form should props colon of Form.Item override the props colon of Form.
class="ant-form ant-form-horizontal"
>
<div
class="ant-row ant-form-item ant-form-item-no-colon"
class="ant-row ant-form-item"
>
<div
class="ant-form-item-label"
>
<label
class=""
class="ant-form-item-no-colon"
title="label"
>
label
@ -59,13 +59,13 @@ exports[`Form should props colon of Form.Item override the props colon of Form.
</div>
</div>
<div
class="ant-row ant-form-item ant-form-item-no-colon"
class="ant-row ant-form-item"
>
<div
class="ant-form-item-label"
>
<label
class=""
class="ant-form-item-no-colon"
title="label"
>
label

View File

@ -31,6 +31,7 @@ describe('Form', () => {
<Form.Item label="label">input</Form.Item>
</Form>,
);
expect(
wrapper
.find('.ant-form-item-label label')
@ -48,12 +49,12 @@ describe('Form', () => {
it('should disable colon when props colon Form is false', () => {
const wrapper = mount(
<Form colon={false}>
<Form.Item>input</Form.Item>
<Form.Item label="label">input</Form.Item>
</Form>,
);
expect(
wrapper
.find('.ant-form-item')
.find('.ant-form-item-label label')
.at(0)
.hasClass('ant-form-item-no-colon'),
).toBe(true);

View File

@ -74,7 +74,7 @@ class App extends React.Component {
<Form layout="horizontal" onSubmit={this.handleSubmit}>
<Row>
<Col span={span}>
<Item {...itemLayout} label="测试字段">
<Item colon={false} {...itemLayout} label="测试字段">
{getFieldDecorator("item1", {
rules: [{ required: true, message: "请必须填写此字段" }],
})(<Input />)}
@ -121,7 +121,7 @@ class App extends React.Component {
<Form>
<Row gutter={16}>
<Col {...ColSpan}>
<Item label="input:64.5px">
<Item colon={false} label="input:64.5px">
<Input />
</Item>
</Col>

View File

@ -232,6 +232,10 @@ interface UserFormProps extends FormComponentProps {
class UserForm extends React.Component<UserFormProps, any> {
// ...
}
const App = Form.create<UserFormProps>({
// ...
})(UserForm);
```
<style>

View File

@ -234,6 +234,10 @@ interface UserFormProps extends FormComponentProps {
class UserForm extends React.Component<UserFormProps, any> {
// ...
}
const App = Form.create<UserFormProps>({
// ...
})(UserForm);
```
<style>

View File

@ -35,6 +35,27 @@
}
}
.@{form-prefix-cls}-item-label > label {
color: @label-color;
&::after {
& when (@form-item-trailing-colon=true) {
content: ':';
}
& when not (@form-item-trailing-colon=true) {
content: ' ';
}
position: relative;
top: -0.5px;
margin: 0 8px 0 2px;
}
&.@{form-prefix-cls}-item-no-colon::after {
content: ' ';
}
}
// Radio && Checkbox
input[type='radio'],
input[type='checkbox'] {
@ -105,31 +126,11 @@ input[type='checkbox'] {
&-left {
text-align: left;
}
> label {
color: @label-color;
&::after {
& when (@form-item-trailing-colon=true) {
content: ':';
}
& when not (@form-item-trailing-colon=true) {
content: ' ';
}
position: relative;
top: -0.5px;
margin: 0 8px 0 2px;
}
}
}
.@{ant-prefix}-switch {
margin: 2px 0 4px;
}
&-no-colon &-label label::after {
content: ' ';
}
}
.@{form-prefix-cls}-explain,

View File

@ -5,7 +5,7 @@ import Icon from '../icon';
export interface PasswordProps extends InputProps {
readonly inputPrefixCls?: string;
readonly action: string;
readonly action?: string;
visibilityToggle?: boolean;
}
@ -38,7 +38,7 @@ export default class Password extends React.Component<PasswordProps, PasswordSta
getIcon() {
const { prefixCls, action } = this.props;
const iconTrigger = ActionMap[action] || '';
const iconTrigger = ActionMap[action!] || '';
const iconProps = {
[iconTrigger]: this.onChange,
className: `${prefixCls}-icon`,

View File

@ -113,4 +113,18 @@ describe('Mention', () => {
expect(items.length).toBe(1);
expect(items.at(0).props().children).toBe('bamboo');
});
it('check filteredSuggestions', () => {
if (process.env.REACT === '15') {
return;
}
const wrapper = mount(<Mention defaultSuggestions={[<Mention.Nav value="light" />]} />);
wrapper.find('DraftEditorContents').simulate('focus');
const ed = wrapper.find('.public-DraftEditor-content');
ed.simulate('beforeInput', { data: '@l' });
jest.runAllTimers();
const items = wrapper.find('div.ant-mention-dropdown-item');
expect(items.length).toBe(1);
expect(items.at(0).props().value).toBe('light');
});
});

View File

@ -1,6 +1,5 @@
import * as React from 'react';
import { Item } from 'rc-menu';
import * as PropTypes from 'prop-types';
import Tooltip from '../tooltip';
import { ClickParam } from './index';
@ -17,29 +16,22 @@ export interface MenuItemProps {
onMouseLeave?: (e: { key: string; domEvent: MouseEvent }) => void;
}
class MenuItem extends React.Component<MenuItemProps, any> {
static contextTypes = {
inlineCollapsed: PropTypes.bool,
};
class MenuItem extends React.Component<MenuItemProps> {
static isMenuItem = 1;
context: any;
private menuItem: any;
private menuItem: this;
onKeyDown = (e: React.MouseEvent<HTMLElement>) => {
this.menuItem.onKeyDown(e);
};
saveMenuItem = (menuItem: any) => {
saveMenuItem = (menuItem: this) => {
this.menuItem = menuItem;
};
render() {
const { inlineCollapsed } = this.context;
const { level, children, rootPrefixCls } = this.props;
const { title, ...rest } = this.props;
let titleNode;
if (inlineCollapsed) {
titleNode = title || (level === 1 ? children : '');
}
titleNode = title || (level === 1 ? children : '');
return (
<Tooltip
@ -47,7 +39,7 @@ class MenuItem extends React.Component<MenuItemProps, any> {
placement="right"
overlayClassName={`${rootPrefixCls}-inline-collapsed-tooltip`}
>
<Item {...rest} title={inlineCollapsed ? null : title} ref={this.saveMenuItem} />
<Item {...rest} title={title} ref={this.saveMenuItem} />
</Tooltip>
);
}

View File

@ -537,4 +537,55 @@ describe('Menu', () => {
wrapper.update();
expect(wrapper.find('.ant-menu-submenu-popup').length).toBe(0);
});
it('onMouseEnter should work', () => {
const onMouseEnter = jest.fn();
const wrapper = mount(
<Menu onMouseEnter={onMouseEnter} defaultSelectedKeys={['test1']}>
<Menu.Item key="test1">Navigation One</Menu.Item>
<Menu.Item key="test2">Navigation Two</Menu.Item>
</Menu>,
);
wrapper
.find('Menu')
.at(1)
.simulate('mouseenter');
expect(onMouseEnter).toHaveBeenCalled();
});
it('get correct animation type when switched from inline', () => {
const wrapper = mount(<Menu mode="inline" />);
wrapper.setProps({ mode: 'horizontal' });
expect(wrapper.instance().getMenuOpenAnimation('')).toBe('');
expect(wrapper.instance().switchingModeFromInline).toBe(false);
});
it('Menu should not shake when collapsed changed', () => {
const wrapper = mount(
<Menu
defaultSelectedKeys={['5']}
defaultOpenKeys={['sub1']}
mode="inline"
inlineCollapsed={false}
>
<SubMenu
key="sub1"
title={
<span>
<span>Navigation One</span>
</span>
}
>
<Menu.Item key="5">Option 5</Menu.Item>
<Menu.Item key="6">Option 6</Menu.Item>
</SubMenu>
</Menu>,
);
expect(wrapper.instance().contextSiderCollapsed).toBe(true);
wrapper.setProps({ inlineCollapsed: true });
expect(wrapper.instance().contextSiderCollapsed).toBe(false);
jest.runAllTimers();
wrapper.update();
expect(wrapper.instance().contextSiderCollapsed).toBe(false);
});
});

View File

@ -189,6 +189,7 @@ class Menu extends React.Component<MenuProps, MenuState> {
onClick(e);
}
};
handleOpenChange = (openKeys: string[]) => {
this.setOpenKeys(openKeys);
@ -218,6 +219,9 @@ class Menu extends React.Component<MenuProps, MenuState> {
if (this.context.siderCollapsed !== undefined) {
return this.context.siderCollapsed;
}
if (this.contextSiderCollapsed) {
return false;
}
return inlineCollapsed;
}

View File

@ -230,7 +230,7 @@ span.@{radio-prefix-cls} + * {
&-checked {
z-index: 1;
color: @radio-dot-color;
background: @radio-button-bg;
background: @radio-button-checked-bg;
border-color: @radio-dot-color;
box-shadow: -1px 0 0 0 @radio-dot-color;
&::before {

View File

@ -193,6 +193,7 @@
// Radio buttons
@radio-button-bg: @btn-default-bg;
@radio-button-checked-bg: @btn-default-bg;
@radio-button-color: @btn-default-color;
@radio-button-hover-color: @primary-5;
@radio-button-active-color: @primary-7;

View File

@ -1,5 +1,5 @@
import demoTest from '../../../tests/shared/demoTest';
demoTest('table', {
skip: process.env.REACT === '15' ? ['edit-row'] : [],
skip: process.env.REACT === '15' ? ['edit-row', 'drag-sorting'] : [],
});

View File

@ -370,4 +370,39 @@ describe('Upload', () => {
);
expect(typeof wrapper.instance().upload.abort).toBe('function');
});
it('unmount', () => {
const wrapper = mount(
<Upload>
<button type="button">upload</button>
</Upload>,
);
const clearIntervalSpy = jest.spyOn(global, 'clearInterval');
expect(clearIntervalSpy).not.toHaveBeenCalled();
wrapper.unmount();
expect(clearIntervalSpy).toHaveBeenCalled();
clearIntervalSpy.mockRestore();
});
it('corrent dragCls when type is drag', () => {
const fileList = [{ status: 'uploading', uid: 'file' }];
const wrapper = mount(
<Upload type="drag" fileList={fileList}>
<button type="button">upload</button>
</Upload>,
);
expect(wrapper.find('.ant-upload-drag-uploading').length).toBe(1);
});
it('return when targetItem is null', () => {
const fileList = [{ uid: 'file' }];
const wrapper = mount(
<Upload type="drag" fileList={fileList}>
<button type="button">upload</button>
</Upload>,
).instance();
expect(wrapper.onSuccess('', { uid: 'fileItem' })).toBe(undefined);
expect(wrapper.onProgress('', { uid: 'fileItem' })).toBe(undefined);
expect(wrapper.onError('', '', { uid: 'fileItem' })).toBe(undefined);
});
});

View File

@ -72,7 +72,7 @@
}
&.@{upload-prefix-cls}-drag-hover:not(.@{upload-prefix-cls}-disabled) {
border: 2px dashed @primary-5;
border-color: @primary-7;
}
&.@{upload-prefix-cls}-disabled {

View File

@ -44,7 +44,7 @@ Ant Design 通过网格体系来实现视觉体系的秩序。网格的基数为
## 关于栅格
Ant Design 采用 24 栅格体系。以 1440 上下布局的结构为例,对宽度为 1168 的内容区域 进行 24 栅格的划分设置,如下图所示。我们为页面中栅格的 Gutter 设定了定值,即浏览器在一定范围扩大或缩小,栅格的 Column 宽度会随之扩大或缩小,但 Gutter 的宽度值固定不变。
Ant Design 采用 24 栅格体系。以上下布局的结构为例,对内容区域进行 24 栅格的划分设置,如下图所示。我们为页面中栅格的 Gutter 设定了定值,即浏览器在一定范围扩大或缩小,栅格的 Column 宽度会随之扩大或缩小,但 Gutter 的宽度值固定不变。
![栅格 layout](https://gw.alipayobjects.com/zos/rmsportal/YPUZpPCzFgQHVxXCIAzq.png)

View File

@ -7,12 +7,20 @@ footer {
z-index: 9;
clear: both;
margin-left: -1px;
color: rgba(255, 255, 255, 0.65);
color: rgba(255, 255, 255, 0.4);
font-size: 14px;
background-color: #000;
.ant-row {
.footer-wrap {
position: relative;
padding: 86px @padding-space 93px @padding-space;
text-align: center;
border-bottom: 1px solid rgba(255, 255, 255, 0.25);
> .ant-row {
display: inline-block;
}
.footer-center {
display: inline-block;
text-align: left;
@ -36,14 +44,15 @@ footer {
}
> div {
margin: 12px 0;
> span {
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
}
}
}
}
.footer-wrap {
position: relative;
padding: 86px @padding-space 93px @padding-space;
border-bottom: 1px solid rgba(255, 255, 255, 0.25);
}
.bottom-bar {
margin: 0;
padding: 16px @padding-space;

View File

@ -1,6 +1,6 @@
import React from 'react';
import { FormattedMessage, injectIntl } from 'react-intl';
import { Modal, message, Row, Col, Badge, Icon } from 'antd';
import { Modal, message, Row, Col, Icon } from 'antd';
import { Link } from 'bisheng/router';
import { isLocalStorageNameSupported, loadScript, getLocalizedPathname } from '../utils';
import ColorPicker from '../Color/ColorPicker';
@ -105,7 +105,7 @@ class Footer extends React.Component {
return (
<footer id="footer">
<div className="footer-wrap">
<Row>
<Row gutter={16}>
<Col md={6} sm={24} xs={24}>
<div className="footer-center">
<h2>
@ -120,7 +120,7 @@ class Footer extends React.Component {
<div>
<a href="http://ng.ant.design">NG-ZORRO</a>
<span> - </span>
Ant Design of Angular
<span>Ant Design of Angular</span>
</div>
<div>
<a href="http://ng.mobile.ant.design">NG-ZORRO-MOBILE</a>
@ -136,13 +136,11 @@ class Footer extends React.Component {
<FormattedMessage id="app.footer.kitchen" />
</div>
<div>
<Badge dot offset={[3, 0]}>
<a target="_blank" rel="noopener noreferrer" href="http://landing.ant.design">
Ant Design Landing
</a>
<span> - </span>
<FormattedMessage id="app.footer.landing" />
</Badge>
<a target="_blank" rel="noopener noreferrer" href="http://landing.ant.design">
Ant Design Landing
</a>
<span> - </span>
<FormattedMessage id="app.footer.landing" />
</div>
<div>
<a href="http://scaffold.ant.design">Scaffolds</a>