chore: improve code style (#27266)

This commit is contained in:
Tom Xu 2020-10-21 10:35:06 +08:00 committed by GitHub
parent d44f404eae
commit b5dc079228
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 62 additions and 192 deletions

View File

@ -10,7 +10,6 @@
一套企业级 UI 设计语言和 React 组件库。 一套企业级 UI 设计语言和 React 组件库。
[![CircleCI status][circleci-image]][circleci-url] [![CI status][github-action-image]][github-action-url] [![codecov][codecov-image]][codecov-url] [![NPM version][npm-image]][npm-url] [![NPM downloads][download-image]][download-url] [![CircleCI status][circleci-image]][circleci-url] [![CI status][github-action-image]][github-action-url] [![codecov][codecov-image]][codecov-url] [![NPM version][npm-image]][npm-url] [![NPM downloads][download-image]][download-url]
[![david deps][david-image]][david-url] [![david devDeps][david-dev-image]][david-dev-url] [![Total alerts][lgtm-image]][lgtm-url] [![FOSSA Status][fossa-image]][fossa-url] [![Issues need help][help-wanted-image]][help-wanted-url] [![david deps][david-image]][david-url] [![david devDeps][david-dev-image]][david-dev-url] [![Total alerts][lgtm-image]][lgtm-url] [![FOSSA Status][fossa-image]][fossa-url] [![Issues need help][help-wanted-image]][help-wanted-url]

View File

@ -24,10 +24,7 @@ describe('AutoComplete children could be focus', () => {
it('focus() and onFocus', () => { it('focus() and onFocus', () => {
const handleFocus = jest.fn(); const handleFocus = jest.fn();
const wrapper = mount(<AutoComplete onFocus={handleFocus} />, { attachTo: container }); const wrapper = mount(<AutoComplete onFocus={handleFocus} />, { attachTo: container });
wrapper wrapper.find('input').instance().focus();
.find('input')
.instance()
.focus();
jest.runAllTimers(); jest.runAllTimers();
expect(handleFocus).toHaveBeenCalled(); expect(handleFocus).toHaveBeenCalled();
}); });
@ -35,15 +32,9 @@ describe('AutoComplete children could be focus', () => {
it('blur() and onBlur', () => { it('blur() and onBlur', () => {
const handleBlur = jest.fn(); const handleBlur = jest.fn();
const wrapper = mount(<AutoComplete onBlur={handleBlur} />, { attachTo: container }); const wrapper = mount(<AutoComplete onBlur={handleBlur} />, { attachTo: container });
wrapper wrapper.find('input').instance().focus();
.find('input')
.instance()
.focus();
jest.runAllTimers(); jest.runAllTimers();
wrapper wrapper.find('input').instance().blur();
.find('input')
.instance()
.blur();
jest.runAllTimers(); jest.runAllTimers();
expect(handleBlur).toHaveBeenCalled(); expect(handleBlur).toHaveBeenCalled();
}); });

View File

@ -72,23 +72,12 @@ describe('react router', () => {
</MemoryRouter>, </MemoryRouter>,
); );
expect(wrapper.find('BreadcrumbItem').length).toBe(1); expect(wrapper.find('BreadcrumbItem').length).toBe(1);
expect( expect(wrapper.find('BreadcrumbItem .ant-breadcrumb-link').at(0).text()).toBe('Home');
wrapper wrapper.find('.demo-nav a').at(1).simulate('click');
.find('BreadcrumbItem .ant-breadcrumb-link')
.at(0)
.text(),
).toBe('Home');
wrapper
.find('.demo-nav a')
.at(1)
.simulate('click');
expect(wrapper.find('BreadcrumbItem').length).toBe(2); expect(wrapper.find('BreadcrumbItem').length).toBe(2);
expect( expect(wrapper.find('BreadcrumbItem .ant-breadcrumb-link').at(1).text()).toBe(
wrapper 'Application List',
.find('BreadcrumbItem .ant-breadcrumb-link') );
.at(1)
.text(),
).toBe('Application List');
}); });
it('react router 3', () => { it('react router 3', () => {

View File

@ -52,10 +52,7 @@ describe('Card', () => {
xxx xxx
</Card>, </Card>,
); );
wrapper wrapper.find('.ant-tabs-tab').at(1).simulate('click');
.find('.ant-tabs-tab')
.at(1)
.simulate('click');
expect(onTabChange).toHaveBeenCalledWith('tab2'); expect(onTabChange).toHaveBeenCalledWith('tab2');
}); });

View File

@ -38,7 +38,6 @@ describe('Cascader.typescript', () => {
}, },
]; ];
const result = <Cascader options={options} defaultValue={[1, 'hangzhou']} />; const result = <Cascader options={options} defaultValue={[1, 'hangzhou']} />;
expect(result).toBeTruthy(); expect(result).toBeTruthy();

View File

@ -43,27 +43,13 @@ describe('MonthPicker and WeekPicker', () => {
const birthday = moment('2000-01-01', 'YYYY-MM-DD').locale('zh-cn'); const birthday = moment('2000-01-01', 'YYYY-MM-DD').locale('zh-cn');
const wrapper = mount(<MonthPicker open />); const wrapper = mount(<MonthPicker open />);
wrapper.setProps({ value: birthday }); wrapper.setProps({ value: birthday });
expect( expect(render(wrapper.find('Trigger').instance().getComponent())).toMatchSnapshot();
render(
wrapper
.find('Trigger')
.instance()
.getComponent(),
),
).toMatchSnapshot();
}); });
it('render WeekPicker', () => { it('render WeekPicker', () => {
const birthday = moment('2000-01-01', 'YYYY-MM-DD').locale('zh-cn'); const birthday = moment('2000-01-01', 'YYYY-MM-DD').locale('zh-cn');
const wrapper = mount(<WeekPicker open />); const wrapper = mount(<WeekPicker open />);
wrapper.setProps({ value: birthday }); wrapper.setProps({ value: birthday });
expect( expect(render(wrapper.find('Trigger').instance().getComponent())).toMatchSnapshot();
render(
wrapper
.find('Trigger')
.instance()
.getComponent(),
),
).toMatchSnapshot();
}); });
}); });

View File

@ -18,10 +18,7 @@ export function openPanel(wrapper) {
} }
export function clearInput(wrapper) { export function clearInput(wrapper) {
wrapper wrapper.find('.ant-calendar-picker-clear').hostNodes().simulate('click');
.find('.ant-calendar-picker-clear')
.hostNodes()
.simulate('click');
} }
export function nextYear(wrapper) { export function nextYear(wrapper) {
@ -33,17 +30,10 @@ export function nextMonth(wrapper) {
} }
export function openPicker(wrapper, index = 0) { export function openPicker(wrapper, index = 0) {
wrapper wrapper.find('input').at(index).simulate('mousedown').simulate('focus');
.find('input')
.at(index)
.simulate('mousedown')
.simulate('focus');
} }
export function closePicker(wrapper, index = 0) { export function closePicker(wrapper, index = 0) {
wrapper wrapper.find('input').at(index).simulate('blur');
.find('input')
.at(index)
.simulate('blur');
} }
export function selectCell(wrapper, text, index = 0) { export function selectCell(wrapper, text, index = 0) {

View File

@ -87,7 +87,7 @@
font-size: @font-size-base; font-size: @font-size-base;
line-height: @line-height-base; line-height: @line-height-base;
word-break: break-word; word-break: break-word;
overflow-wrap: break-word; overflow-wrap: break-word;
} }
&-item { &-item {

View File

@ -17,11 +17,6 @@ describe('List', () => {
const dataSource = []; const dataSource = [];
const wrapper = mount(<List renderItem={renderItem} dataSource={dataSource} locale={locale} />); const wrapper = mount(<List renderItem={renderItem} dataSource={dataSource} locale={locale} />);
expect( expect(wrapper.find('div').first().props().locale).toBe(undefined);
wrapper
.find('div')
.first()
.props().locale,
).toBe(undefined);
}); });
}); });

View File

@ -1,3 +1,3 @@
import locale from '../locale/by_BY'; import locale from '../locale/by_BY';
export default locale; export default locale;

View File

@ -68,9 +68,9 @@ const localeValues: Locale = {
}, },
Form: { Form: {
defaultValidateMessages: { defaultValidateMessages: {
"default": 'خطأ في حقل الإدخال ${label}', default: 'خطأ في حقل الإدخال ${label}',
required: 'يرجى إدخال ${label}', required: 'يرجى إدخال ${label}',
"enum": '${label} يجب أن يكون واحدا من [${enum}]', enum: '${label} يجب أن يكون واحدا من [${enum}]',
whitespace: '${label} لا يمكن أن يكون حرفًا فارغًا', whitespace: '${label} لا يمكن أن يكون حرفًا فارغًا',
date: { date: {
format: '${label} تنسيق التاريخ غير صحيح', format: '${label} تنسيق التاريخ غير صحيح',

View File

@ -5,16 +5,8 @@ describe('Result.typescript', () => {
it('status', () => { it('status', () => {
const result = ( const result = (
<> <>
<Result <Result status="404" title="404" subTitle="Sorry, the page you visited does not exist." />
status="404" <Result status={404} title="404" subTitle="Sorry, the page you visited does not exist." />
title="404"
subTitle="Sorry, the page you visited does not exist."
/>
<Result
status={404}
title="404"
subTitle="Sorry, the page you visited does not exist."
/>
</> </>
); );

View File

@ -40,24 +40,14 @@ describe('Select', () => {
it('should support set notFoundContent to null', () => { it('should support set notFoundContent to null', () => {
const wrapper = mount(<Select mode="multiple" notFoundContent={null} />); const wrapper = mount(<Select mode="multiple" notFoundContent={null} />);
toggleOpen(wrapper); toggleOpen(wrapper);
const dropdownWrapper = mount( const dropdownWrapper = mount(wrapper.find('Trigger').instance().getComponent());
wrapper
.find('Trigger')
.instance()
.getComponent(),
);
expect(dropdownWrapper.find('MenuItem').length).toBe(0); expect(dropdownWrapper.find('MenuItem').length).toBe(0);
}); });
it('should not have default notFoundContent when mode is combobox', () => { it('should not have default notFoundContent when mode is combobox', () => {
const wrapper = mount(<Select mode={Select.SECRET_COMBOBOX_MODE_DO_NOT_USE} />); const wrapper = mount(<Select mode={Select.SECRET_COMBOBOX_MODE_DO_NOT_USE} />);
toggleOpen(wrapper); toggleOpen(wrapper);
const dropdownWrapper = mount( const dropdownWrapper = mount(wrapper.find('Trigger').instance().getComponent());
wrapper
.find('Trigger')
.instance()
.getComponent(),
);
expect(dropdownWrapper.find('MenuItem').length).toBe(0); expect(dropdownWrapper.find('MenuItem').length).toBe(0);
}); });
@ -66,19 +56,9 @@ describe('Select', () => {
<Select mode={Select.SECRET_COMBOBOX_MODE_DO_NOT_USE} notFoundContent="not at all" />, <Select mode={Select.SECRET_COMBOBOX_MODE_DO_NOT_USE} notFoundContent="not at all" />,
); );
toggleOpen(wrapper); toggleOpen(wrapper);
const dropdownWrapper = mount( const dropdownWrapper = mount(wrapper.find('Trigger').instance().getComponent());
wrapper
.find('Trigger')
.instance()
.getComponent(),
);
expect(dropdownWrapper.find('.ant-select-item-option').length).toBeFalsy(); expect(dropdownWrapper.find('.ant-select-item-option').length).toBeFalsy();
expect( expect(dropdownWrapper.find('.ant-select-item-empty').at(0).text()).toBe('not at all');
dropdownWrapper
.find('.ant-select-item-empty')
.at(0)
.text(),
).toBe('not at all');
}); });
it('should be controlled by open prop', () => { it('should be controlled by open prop', () => {
@ -88,24 +68,14 @@ describe('Select', () => {
<Option value="1">1</Option> <Option value="1">1</Option>
</Select>, </Select>,
); );
let dropdownWrapper = mount( let dropdownWrapper = mount(wrapper.find('Trigger').instance().getComponent());
wrapper
.find('Trigger')
.instance()
.getComponent(),
);
expect(dropdownWrapper.props().visible).toBe(true); expect(dropdownWrapper.props().visible).toBe(true);
toggleOpen(wrapper); toggleOpen(wrapper);
expect(onDropdownVisibleChange).toHaveBeenLastCalledWith(false); expect(onDropdownVisibleChange).toHaveBeenLastCalledWith(false);
expect(dropdownWrapper.props().visible).toBe(true); expect(dropdownWrapper.props().visible).toBe(true);
wrapper.setProps({ open: false }); wrapper.setProps({ open: false });
dropdownWrapper = mount( dropdownWrapper = mount(wrapper.find('Trigger').instance().getComponent());
wrapper
.find('Trigger')
.instance()
.getComponent(),
);
expect(dropdownWrapper.props().visible).toBe(false); expect(dropdownWrapper.props().visible).toBe(false);
toggleOpen(wrapper); toggleOpen(wrapper);
expect(onDropdownVisibleChange).toHaveBeenLastCalledWith(true); expect(onDropdownVisibleChange).toHaveBeenLastCalledWith(true);

View File

@ -26,7 +26,7 @@ const Paragraph = (props: SkeletonParagraphProps) => {
const { prefixCls, className, style, rows } = props; const { prefixCls, className, style, rows } = props;
const rowList = [...Array(rows)].map((_, index) => ( const rowList = [...Array(rows)].map((_, index) => (
// eslint-disable-next-line react/no-array-index-key // eslint-disable-next-line react/no-array-index-key
<li key={index} style={{ width: getWidth(index) }}/> <li key={index} style={{ width: getWidth(index) }} />
)); ));
return ( return (
<ul className={classNames(prefixCls, className)} style={style}> <ul className={classNames(prefixCls, className)} style={style}>

View File

@ -5,35 +5,20 @@ import Spin from '..';
describe('delay spinning', () => { describe('delay spinning', () => {
it("should render with delay when it's mounted with spinning=true and delay", () => { it("should render with delay when it's mounted with spinning=true and delay", () => {
const wrapper = mount(<Spin spinning delay={500} />); const wrapper = mount(<Spin spinning delay={500} />);
expect( expect(wrapper.find('.ant-spin').at(0).hasClass('ant-spin-spinning')).toEqual(false);
wrapper
.find('.ant-spin')
.at(0)
.hasClass('ant-spin-spinning'),
).toEqual(false);
}); });
it('should render when delay is init set', async () => { it('should render when delay is init set', async () => {
const wrapper = mount(<Spin spinning delay={100} />); const wrapper = mount(<Spin spinning delay={100} />);
expect( expect(wrapper.find('.ant-spin').at(0).hasClass('ant-spin-spinning')).toEqual(false);
wrapper
.find('.ant-spin')
.at(0)
.hasClass('ant-spin-spinning'),
).toEqual(false);
// use await not jest.runAllTimers() // use await not jest.runAllTimers()
// because of https://github.com/facebook/jest/issues/3465 // because of https://github.com/facebook/jest/issues/3465
await new Promise(resolve => setTimeout(resolve, 500)); await new Promise(resolve => setTimeout(resolve, 500));
wrapper.update(); wrapper.update();
expect( expect(wrapper.find('.ant-spin').at(0).hasClass('ant-spin-spinning')).toEqual(true);
wrapper
.find('.ant-spin')
.at(0)
.hasClass('ant-spin-spinning'),
).toEqual(true);
}); });
it('should cancel debounce function when unmount', async () => { it('should cancel debounce function when unmount', async () => {

View File

@ -14,18 +14,8 @@ describe('Spin', () => {
<div>content</div> <div>content</div>
</Spin>, </Spin>,
); );
expect( expect(wrapper.find('.ant-spin-nested-loading').at(0).prop('style')).toBeFalsy();
wrapper expect(wrapper.find('.ant-spin').at(0).prop('style').background).toBe('red');
.find('.ant-spin-nested-loading')
.at(0)
.prop('style'),
).toBeFalsy();
expect(
wrapper
.find('.ant-spin')
.at(0)
.prop('style').background,
).toBe('red');
}); });
it("should render custom indicator when it's set", () => { it("should render custom indicator when it's set", () => {

View File

@ -95,7 +95,9 @@ describe('Statistic', () => {
it('responses hover events for Countdown', () => { it('responses hover events for Countdown', () => {
const onMouseEnter = jest.fn(); const onMouseEnter = jest.fn();
const onMouseLeave = jest.fn(); const onMouseLeave = jest.fn();
const wrapper = mount(<Statistic.Countdown onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} />); const wrapper = mount(
<Statistic.Countdown onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} />,
);
wrapper.simulate('mouseenter'); wrapper.simulate('mouseenter');
expect(onMouseEnter).toHaveBeenCalled(); expect(onMouseEnter).toHaveBeenCalled();
wrapper.simulate('mouseleave'); wrapper.simulate('mouseleave');

View File

@ -1,6 +1,8 @@
// customize dark components background in popover containers(like Modal, Drawer, Card, Popover, Popconfirm, Notification, ...) // customize dark components background in popover containers(like Modal, Drawer, Card, Popover, Popconfirm, Notification, ...)
// for dark theme // for dark theme
.popover-customize-bg(@containerClass, @background: @popover-background, @prefix: @ant-prefix) when (@theme = dark) { .popover-customize-bg(@containerClass, @background: @popover-background, @prefix: @ant-prefix)
when
(@theme = dark) {
@picker-prefix-cls: ~'@{prefix}-picker'; @picker-prefix-cls: ~'@{prefix}-picker';
@slider-prefix-cls: ~'@{prefix}-slider'; @slider-prefix-cls: ~'@{prefix}-slider';
@anchor-prefix-cls: ~'@{prefix}-anchor'; @anchor-prefix-cls: ~'@{prefix}-anchor';
@ -18,7 +20,12 @@
@popover-border: @border-width-base @border-style-base @popover-customize-border-color; @popover-border: @border-width-base @border-style-base @popover-customize-border-color;
.@{containerClass} { .@{containerClass} {
.@{picker-prefix-cls}-clear, .@{slider-prefix-cls}-handle, .@{anchor-prefix-cls}-wrapper, .@{collapse-prefix-cls}-content, .@{timeline-prefix-cls}-item-head, .@{card-prefix-cls} { .@{picker-prefix-cls}-clear,
.@{slider-prefix-cls}-handle,
.@{anchor-prefix-cls}-wrapper,
.@{collapse-prefix-cls}-content,
.@{timeline-prefix-cls}-item-head,
.@{card-prefix-cls} {
background-color: @background; background-color: @background;
} }
@ -77,7 +84,8 @@
> tr { > tr {
> td { > td {
border-bottom: @popover-border; border-bottom: @popover-border;
&.@{table-prefix-cls}-cell-fix-left, &.@{table-prefix-cls}-cell-fix-right { &.@{table-prefix-cls}-cell-fix-left,
&.@{table-prefix-cls}-cell-fix-right {
background-color: @background; background-color: @background;
} }
} }

View File

@ -34,12 +34,9 @@ describe('TimeLine', () => {
}); });
it('its last item is marked as the last item', () => { it('its last item is marked as the last item', () => {
expect( expect(wrapper.find('li.ant-timeline-item').last().hasClass('ant-timeline-item-last')).toBe(
wrapper true,
.find('li.ant-timeline-item') );
.last()
.hasClass('ant-timeline-item-last'),
).toBe(true);
}); });
}); });
@ -65,10 +62,7 @@ describe('TimeLine', () => {
it('its last item is marked as the pending item', () => { it('its last item is marked as the pending item', () => {
const wrapper = wrapperFactory({ pending }); const wrapper = wrapperFactory({ pending });
expect( expect(
wrapper wrapper.find('li.ant-timeline-item').last().hasClass('ant-timeline-item-pending'),
.find('li.ant-timeline-item')
.last()
.hasClass('ant-timeline-item-pending'),
).toBe(true); ).toBe(true);
}); });
@ -119,21 +113,15 @@ describe('TimeLine', () => {
it('its last item is marked as the last item', () => { it('its last item is marked as the last item', () => {
const wrapper = wrapperFactory({ pending, reverse: true }); const wrapper = wrapperFactory({ pending, reverse: true });
expect( expect(wrapper.find('li.ant-timeline-item').last().hasClass('ant-timeline-item-last')).toBe(
wrapper true,
.find('li.ant-timeline-item') );
.last()
.hasClass('ant-timeline-item-last'),
).toBe(true);
}); });
it('its first item is marked as the pending item', () => { it('its first item is marked as the pending item', () => {
const wrapper = wrapperFactory({ pending, reverse: true }); const wrapper = wrapperFactory({ pending, reverse: true });
expect( expect(
wrapper wrapper.find('li.ant-timeline-item').first().hasClass('ant-timeline-item-pending'),
.find('li.ant-timeline-item')
.first()
.hasClass('ant-timeline-item-pending'),
).toBe(true); ).toBe(true);
}); });
}); });

View File

@ -33,12 +33,7 @@ describe('Transfer.List', () => {
it('should check top Checkbox while all available items are checked', () => { it('should check top Checkbox while all available items are checked', () => {
const wrapper = mount(<List {...listCommonProps} checkedKeys={['a', 'b']} />); const wrapper = mount(<List {...listCommonProps} checkedKeys={['a', 'b']} />);
expect( expect(wrapper.find('.ant-transfer-list-header').find(Checkbox).prop('checked')).toBeTruthy();
wrapper
.find('.ant-transfer-list-header')
.find(Checkbox)
.prop('checked'),
).toBeTruthy();
}); });
it('when component has been unmounted, componentWillUnmount should be called', () => { it('when component has been unmounted, componentWillUnmount should be called', () => {

View File

@ -17,7 +17,7 @@
/> />
<link rel="stylesheet" href="https://gw.alipayobjects.com/os/antfincdn/ciEbDYlN1f/umi.css" /> <link rel="stylesheet" href="https://gw.alipayobjects.com/os/antfincdn/ciEbDYlN1f/umi.css" />
<script> <script>
window.routerBase = "/"; window.routerBase = '/';
</script> </script>
<script> <script>
//! umi version: 3.2.14 //! umi version: 3.2.14

View File

@ -79,9 +79,7 @@
</script> </script>
</head> </head>
<body> <body>
<div id="react-content"> <div id="react-content">{{ content | safe }}</div>
{{ content | safe }}
</div>
{% for jsFile in manifest["js"] %} {% for jsFile in manifest["js"] %}
<script src="{{ root }}{{ jsFile }}"></script> <script src="{{ root }}{{ jsFile }}"></script>
{% endfor %} {% endfor %}

View File

@ -9,9 +9,7 @@ const LANGS = {
}; };
const CodePreview = ({ toReactComponent, codes }) => { const CodePreview = ({ toReactComponent, codes }) => {
const langList = Object.keys(codes) const langList = Object.keys(codes).sort().reverse();
.sort()
.reverse();
let content; let content;

View File

@ -17,16 +17,14 @@ interface Recommend {
const LIST_CN: Recommend[] = [ const LIST_CN: Recommend[] = [
{ {
title: '树形控件在生产力工具中的设计', title: '树形控件在生产力工具中的设计',
description: description: '惊半年实践血泪史3000 字深度好文,一个爱树的设计师手把手教你如何设计「树 」!',
'惊半年实践血泪史3000 字深度好文,一个爱树的设计师手把手教你如何设计「树 」!',
img: 'https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*Z4eXS55fMigAAAAAAAAAAAAAARQnAQ', img: 'https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*Z4eXS55fMigAAAAAAAAAAAAAARQnAQ',
href: 'https://zhuanlan.zhihu.com/p/260068653', href: 'https://zhuanlan.zhihu.com/p/260068653',
popularize: true, popularize: true,
}, },
{ {
title: '或许这就是下一代组件库', title: '或许这就是下一代组件库',
description: description: '随着 React hooks、Vue composition API 的推出,或许组件库有了新的突破点。',
'随着 React hooks、Vue composition API 的推出,或许组件库有了新的突破点。',
img: 'https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*SU6hQ5jHVEsAAAAAAAAAAAAAARQnAQ', img: 'https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*SU6hQ5jHVEsAAAAAAAAAAAAAARQnAQ',
href: 'https://zhuanlan.zhihu.com/p/252824872', href: 'https://zhuanlan.zhihu.com/p/252824872',
}, },
@ -41,7 +39,7 @@ const LIST_CN: Recommend[] = [
const LIST_EN: Recommend[] = [ const LIST_EN: Recommend[] = [
{ {
title: "How to Design Tree Component", title: 'How to Design Tree Component',
description: description:
'🌲 Surprise! With half-a-year practice of blood and tears, here comes a designer who deeply loves trees to teach you how to design 「tree」component!', '🌲 Surprise! With half-a-year practice of blood and tears, here comes a designer who deeply loves trees to teach you how to design 「tree」component!',
img: 'https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*Z4eXS55fMigAAAAAAAAAAAAAARQnAQ', img: 'https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*Z4eXS55fMigAAAAAAAAAAAAAARQnAQ',