React工程化实践之UI组件库

时间:2022-12-24 17:54:40

分享日期: 2022-11-08

分享内容: 组件不是 React 特有的概念,但是 React 将组件化的思想发扬光大,可谓用到了极致。良好的组件设计会是良好的应用开发基础,这一讲就让我们谈一谈React工

程化开发实践中用到的一些UI组件库,并 结合企企项目中各位大佬们设计的组 件,更深入的了解组件设计思想,以便更好的在项目业务各类场景中应用。

分享主题由来:

React工程化实践之UI组件库

React工程化实践之UI组件库

一、常见React·UI库

如果您不想从头开始构建所有必要的 React UI 组件,您可以选择 React UI Library 来完成这项工作。所有这些都有一些基本的组件,比如按钮,下拉菜单,对话框和列表。有很多 UI 库可供 React 选择:

 

PC端UI库

 

移动端UI库

 

 

FAQ 常用的HTML标签有哪些? 行内元素和块级元素有什么区别?

 

二、企企React·UI库

/front-theory/packages/kaleido/packages/uikit/athena-ui (提供了57个UI库)

  • AdvanceTree
  • ag-grid
  • Alert
  • Athena
  • Button
  • Calendar
  • Check
  • Checkbox
  • DatePicker
  • Dialog
  • Drawer
  • FocusTrapZone
  • FocusZone
  • FormLayout
  • Grid
  • GridLayout
  • GroupedList
  • Image
  • Input
  • Label
  • LayoutGroup
  • List
  • ListView
  • Menu
  • MouseWheelListener
  • NavBar
  • NonIdealState
  • NxTree
  • Overlay
  • PageLayout
  • Popover
  • ProgressBar
  • QwertZone
  • Radio
  • react
  • react-cool-virtual
  • ReactOverflowList
  • ReactTree
  • ScrollablePane
  • Scrollbars
  • SearchBox
  • Select
  • Spinner
  • SplitterLayout
  • StatisticalReport
  • Sticky
  • SvgIcon
  • Switch
  • Tabs
  • Text
  • TimeInput
  • Toaster
  • Tooltip
  • TooltipElement
  • Tree
  • Viewer
  • WindowSizeListener

1. Alert 警告

Alerts notify users of important information and force them to acknowledge the alert content before continuing.

Although similar to dialogs, alerts are more restrictive and should only be used for important information. By default, the user can only exit the alert by clicking one of the confirmation buttons—clicking the overlay or pressing the ​​esc​​ key will not close the alert. These interactions can be enabled via props.

import * as React from 'react';
import { Alert } from '@athena-ui/components/Alert'


constructor(props) {
super(props);


this.state = {
canEscapeKeyCancel: false,
canOutsideClickCancel: false,
isOpen: false,
isOpenError: false,
};


this.handleEscapeKeyChange = handleBooleanChange(canEscapeKeyCancel => this.setState({ canEscapeKeyCancel }));
this.handleOutsideClickChange = handleBooleanChange(click => this.setState({ canOutsideClickCancel: click }));
this.handleErrorOpen = () => this.setState({ isOpenError: true });
this.handleErrorClose = () => this.setState({ isOpenError: false });


this.handleMoveOpen = () => this.setState({ isOpen: true });
this.handleMoveConfirm = () => {
this.setState({ isOpen: false });
};
this.handleMoveCancel = () => this.setState({ isOpen: false });
}


render() {
const { isOpen, isOpenError, ...alertProps } = this.state;
const options = (
<React.Fragment>
<H5>Props</H5>
<Switch
checked={this.state.canEscapeKeyCancel}
label="Can escape key cancel"
onChange={this.handleEscapeKeyChange}
/>
<Switch
checked={this.state.canOutsideClickCancel}
label="Can outside click cancel"
onChange={this.handleOutsideClickChange}
/>
</React.Fragment>
);
return (
<Example options={options} {...this.props} className="docs-example-frame-row">
<Button onClick={this.handleErrorOpen} text="Open file error alert" />
<Alert
{...alertProps}
confirmButtonText="Okay"
isOpen={isOpenError}
onClose={this.handleErrorClose}
>
<p>
Couldn't create the file because the containing folder doesn't exist anymore. You will be
redirected to your user folder.
</p>
</Alert>


<Button onClick={this.handleMoveOpen} text="Open file deletion alert" />
<Alert
{...alertProps}
cancelButtonText="Cancel"
confirmButtonText="Move to Trash"
icon="trash"
intent={Intent.DANGER}
isOpen={isOpen}
onCancel={this.handleMoveCancel}
onConfirm={this.handleMoveConfirm}
>
<p>
Are you sure you want to move <b>filename</b> to Trash? You will be able to restore it later,
but it will become private to you.
</p>
</Alert>
</Example>
);
}

2. Button 按钮

2.1 基础用法

基础的按钮用法。

Button 组件默认提供7种主题,由​​color​​​属性来定义,默认为​​default​​。

render() {
return (
<React.Fragment>
<div className="at-row">
<Button intent="default">默认按钮</Button>
<Button intent="primary">主要按钮</Button>
<Button intent="success">成功按钮</Button>
<Button intent="info">信息按钮</Button>
<Button intent="warning">警告按钮</Button>
<Button intent="danger">危险按钮</Button>
</div>
<div className="at-row">
<Button intent="default" minimal>朴素按钮</Button>
<Button intent="primary" minimal>主要按钮</Button>
<Button intent="success" minimal>成功按钮</Button>
<Button intent="info" minimal>信息按钮</Button>
<Button intent="warning" minimal>警告按钮</Button>
<Button intent="danger" minimal>危险按钮</Button>
</div>
<div className="at-row">
<Button intent="default" outline>Outline按钮</Button>
<Button intent="primary" outline>主要按钮</Button>
<Button intent="success" outline>成功按钮</Button>
<Button intent="info" outline>信息按钮</Button>
<Button intent="warning" outline>警告按钮</Button>
<Button intent="danger" outline>危险按钮</Button>
</div>
<div className="at-row">
<Button intent="default" round>圆角按钮</Button>
<Button intent="primary" round>主要按钮</Button>
<Button intent="success" round>成功按钮</Button>
<Button intent="info" round>信息按钮</Button>
<Button intent="warning" round>警告按钮</Button>
<Button intent="danger" round>危险按钮</Button>
</div>
<div className="at-row">
<Button intent="default" icon="search" circle></Button>
<Button intent="primary" icon="edit" circle></Button>
<Button intent="success" icon="help" circle></Button>
<Button intent="info" icon="repeat" circle></Button>
<Button intent="warning" icon="star" circle></Button>
<Button intent="danger" icon="delete" circle></Button>
</div>
</React.Fragment>
)
}

2.2 禁用状态

按钮不可用状态。

你可以使用​​disabled​​​属性来定义按钮是否可用,它接受一个​​Boolean​​值。

render() {
return (
<React.Fragment>
<div className="at-row">
<Button intent="default" disabled>默认按钮</Button>
<Button intent="primary" disabled>主要按钮</Button>
<Button intent="success" disabled>成功按钮</Button>
<Button intent="info" disabled>信息按钮</Button>
<Button intent="warning" disabled>警告按钮</Button>
<Button intent="danger" disabled>危险按钮</Button>
</div>


<div className="at-row">
<Button intent="default" minimal disabled>朴素按钮</Button>
<Button intent="primary" minimal disabled>主要按钮</Button>
<Button intent="success" minimal disabled>成功按钮</Button>
<Button intent="info" minimal disabled>信息按钮</Button>
<Button intent="warning" minimal disabled>警告按钮</Button>
<Button intent="danger" minimal disabled>危险按钮</Button>
</div>
</React.Fragment>
)
}

2.3 文字按钮

没有边框和背景色的按钮。

render() {
return (
<div>
<Button intent="primary" link>文字按钮</Button>
<Button intent="success" link disabled>文字按钮</Button>
</div>
)
}

2.4 图标按钮

带图标的按钮可增强辨识度(有文字)或节省空间(无文字)。

设置​​icon​​属性即可,也可以设置在文字右边的 icon。

render() {
return (
<div style={{display: "flex"}}>
<Button intent="primary" icon="edit"></Button>
<Button intent="primary" icon="share"></Button>
<Button intent="primary" icon="delete"></Button>
<Button intent="primary" icon="search">搜索</Button>
<Button intent="primary" rightIcon="upload">上传</Button>
</div>
)
}

2.5 按钮组

以按钮组的方式出现,常用于多项类似操作。

使用​​Button.Group​​标签来嵌套你的按钮。

render() {
return (
<div>
<Button.Group>
<Button intent="primary" icon="at-icon-arrow-left">上一页</Button>
<Button intent="primary">下一页<i className="el-icon-arrow-right el-icon-right"></i></Button>
</Button.Group>

<Button.Group>
<Button intent="primary" icon="at-icon-edit"></Button>
<Button intent="primary" icon="at-icon-share"></Button>
<Button intent="primary" icon="at-icon-delete"></Button>
</Button.Group>
</div>
)
}

2.6 加载中

点击按钮后进行数据加载操作,在按钮上显示加载状态。

要设置为 loading 状态,只要设置​​loading​​​属性为​​true​​即可。

render() {
return <Button intent="primary" loading={true}>加载中</Button>
}

2.7 不同尺寸

Button 组件提供除了默认值以外的三种尺寸,可以在不同场景下选择合适的按钮尺寸。

额外的尺寸:​​large​​​、​​small​​​、​​mini​​​,通过设置​​size​​属性来配置它们。

render() {
return (
<div>
<div className="at-row">
<Button>默认按钮</Button>
<Button large>中等按钮</Button>
<Button small>小型按钮</Button>
</div>
<div className="at-row">
<Button round>默认按钮</Button>
<Button large round>中等按钮</Button>
<Button small round>小型按钮</Button>
</div>
</div>
)
}

2.8 组合按钮

CompoundButton 可以对按钮的功能提供一行描述信息。

通过设置 secondaryText 和 text。

render() {
return (
<div>
<div className="at-row">
<CompoundButton intent="info" secondaryText="You can create a new account here." text="Hi!" loading></CompoundButton>
</div>
</div>
)
}

2.9 Split Button

按钮包含下拉菜单

render() {
return (
<div>
<div className="at-row">
<Button
intent="primary"
text="新建"
menuProps={{
items: [
{
key: 'emailMessage',
text: 'Email message',
icon: 'envelope',
},
{
key: 'calendarEvent',
text: 'Calendar event',
icon: 'timeline-events',
}
],
directionalHintFixed: true
}}
></Button>
</div>
</div>
)
}

2.10 属性Attributes

参数

说明

类型

可选值

默认值

size

尺寸

string

large,small,mini

intent

类型

string

primary,success,warning,danger,info,text

minimal

是否朴素按钮

Boolean

true,false

false

loading

是否加载中状态

Boolean

false

disabled

禁用

boolean

true, false

false

icon

图标,已有的图标库中的图标名

string

nativeType

原生 type 属性

string

button,submit,reset

button

3. ButtonGroup 按钮组

Button groups arrange multiple buttons in a horizontal or vertical group.

3.1 基础用法

constructor(props) {
super(props);


this.state = {
alignText: Alignment.CENTER,
fill: false,
iconOnly: false,
large: false,
minimal: false,
vertical: false,
};


this.handleFillChange = handleBooleanChange(fill => this.setState({ fill }));
this.handleIconOnlyChange = handleBooleanChange(iconOnly => this.setState({ iconOnly }));
this.handleLargeChange = handleBooleanChange(large => this.setState({ large }));
this.handleMinimalChange = handleBooleanChange(minimal => this.setState({ minimal }));
this.handleVerticalChange = handleBooleanChange(vertical => this.setState({ vertical }));
this.handleAlignChange = (alignText) => this.setState({ alignText });
}
render() {
const { iconOnly, ...bgProps } = this.state;
const options = (
<React.Fragment>
<H5>Props</H5>
<Switch checked={this.state.fill} label="Fill" onChange={this.handleFillChange} />
<Switch checked={this.state.large} label="Large" onChange={this.handleLargeChange} />
<Switch checked={this.state.minimal} label="Minimal" onChange={this.handleMinimalChange} />
<Switch checked={this.state.vertical} label="Vertical" onChange={this.handleVerticalChange} />
<AlignmentSelect align={this.state.alignText} onChange={this.handleAlignChange} />
<H5>Example</H5>
<Switch checked={this.state.iconOnly} label="Icons only" onChange={this.handleIconOnlyChange} />
</React.Fragment>
);


return (
<Example options={options} {...this.props}>
<ButtonGroup style={{ minWidth: 200 }} {...bgProps}>
<Button icon="database">{!iconOnly && "Queries"}</Button>
<Button icon="function">{!iconOnly && "Functions"}</Button>
<Button icon="cog" rightIcon="settings">
{!iconOnly && "Options"}
</Button>
<Button
intent="primary"
text={!iconOnly && "Create account"}
split
menuProps={{
items: [
{
key: 'emailMessage',
text: 'Email message',
},
{
key: 'calendarEvent',
text: 'Calendar event',
}
],
directionalHintFixed: true
}}
/>
</ButtonGroup>
</Example>
);
}

3.2 和popovers搭配使用

​Buttons​​​ inside a ​​ButtonGroup​​​ can trivially be wrapped with a ​​Popover​​ to create complex toolbars.

constructor(props) {
super(props);


this.state = {
alignText: Alignment.CENTER,
large: false,
minimal: false,
vertical: false,
};


this.handleLargeChange = handleBooleanChange(large => this.setState({ large }));
this.handleMinimalChange = handleBooleanChange(minimal => this.setState({ minimal }));
this.handleVerticalChange = handleBooleanChange(vertical => this.setState({ vertical }));
this.handleAlignChange = (alignText) => this.setState({ alignText });
}


render() {
const options = (
<React.Fragment>
<H5>Props</H5>
<Switch label="Large" checked={this.state.large} onChange={this.handleLargeChange} />
<Switch label="Minimal" checked={this.state.minimal} onChange={this.handleMinimalChange} />
<Switch label="Vertical" checked={this.state.vertical} onChange={this.handleVerticalChange} />
<AlignmentSelect align={this.state.alignText} onChange={this.handleAlignChange} />
</React.Fragment>
);
return (
<Example options={options} {...this.props}>
<ButtonGroup {...this.state} style={{ minWidth: 120 }}>
{this.renderButton("File", "document")}
{this.renderButton("Edit", "edit")}
{this.renderButton("View", "eye-open")}
</ButtonGroup>
</Example>
);
}


renderButton(text, iconName) {
const { vertical } = this.state;
const rightIconName: IconName = vertical ? "caret-right" : "caret-down";
const position = vertical ? Position.RIGHT_TOP : Position.BOTTOM_LEFT;
return (
<Popover content={<FileMenu />} position={position}>
<Button rightIcon={rightIconName} icon={iconName} text={text} />
</Popover>
);
}

4. Calendar 日历

4.1 default calendar

constructor(props) {
super(props);


this.state = {
selectedDate: null,
};


this._onSelectDate = this._onSelectDate.bind(this);
}


render() {
const divStyle = {
height: '340px'
};


const buttonStyle = {
margin: '17px 10px 0 0'
};


let dateRangeString = null;


if (this.state.selectedDateRange) {
const rangeStart = this.state.selectedDateRange[0];
const rangeEnd = this.state.selectedDateRange[this.state.selectedDateRange.length - 1];
dateRangeString = rangeStart.toLocaleDateString() + '-' + rangeEnd.toLocaleDateString();
}


return (
<Example {...this.props}>
<div style={divStyle}>
<div>
选择的日期:{' '}
<span>{!this.state.selectedDate ? '无' : this.state.selectedDate.toLocaleString()}</span>
</div>
<Calendar
isMonthPickerVisible={false}
value={this.state.selectedDate}
onSelectDate={this._onSelectDate}
/>
</div>
</Example>
);
}


_onSelectDate(date: Date, dateRangeArray: Date[]) {
this.setState({
selectedDate: date,
});
}

4.2 单击标题时带有重叠月份选择器内联日历

通过设置 ​​showMonthPickerAsOverlay​​​ 为 ​​true​​ 可以点击抬头区的月来选择月份。

constructor(props) {
super(props);


this.state = {
selectedDate: null,
};


this._onSelectDate = this._onSelectDate.bind(this);
}


render() {
const divStyle = {
height: '340px'
};


const buttonStyle = {
margin: '17px 10px 0 0'
};


let dateRangeString = null;


if (this.state.selectedDateRange) {
const rangeStart = this.state.selectedDateRange[0];
const rangeEnd = this.state.selectedDateRange[this.state.selectedDateRange.length - 1];
dateRangeString = rangeStart.toLocaleDateString() + '-' + rangeEnd.toLocaleDateString();
}


return (
<Example {...this.props}>
<div style={divStyle}>
<div>
选择的日期:{' '}
<span>{!this.state.selectedDate ? '无' : this.state.selectedDate.toLocaleString()}</span>
</div>
<Calendar
isMonthPickerVisible={false}
showMonthPickerAsOverlay
value={this.state.selectedDate}
onSelectDate={this._onSelectDate}
/>
</div>
</Example>
);
}


_onSelectDate(date: Date, dateRangeArray: Date[]) {
this.setState({
selectedDate: date,
});
}

4.3 带月份选择器的内联日历

通过设置 ​​isMonthPickerVisible​​​ 为 ​​true​​ 可以点击抬头区的月来选择月份。

constructor(props) {
super(props);


this.state = {
selectedDate: null,
};


this._onSelectDate = this._onSelectDate.bind(this);
}


render() {
const divStyle = {
height: '340px'
};


const buttonStyle = {
margin: '17px 10px 0 0'
};


let dateRangeString = null;


if (this.state.selectedDateRange) {
const rangeStart = this.state.selectedDateRange[0];
const rangeEnd = this.state.selectedDateRange[this.state.selectedDateRange.length - 1];
dateRangeString = rangeStart.toLocaleDateString() + '-' + rangeEnd.toLocaleDateString();
}


return (
<Example {...this.props}>
<div style={divStyle}>
<div>
选择的日期:{' '}
<span>{!this.state.selectedDate ? '无' : this.state.selectedDate.toLocaleString()}</span>
</div>
<Calendar
isMonthPickerVisible
value={this.state.selectedDate}
onSelectDate={this._onSelectDate}
/>
</div>
</Example>
);
}


_onSelectDate(date: Date, dateRangeArray: Date[]) {
this.setState({
selectedDate: date,
});
}

4.4 带周选择的内联日历

通过设置 ​​dateRangeType​​​ 可以按特定类型选择一个区间的日期。​​dateRangeType​​​ 可以选择的类型有:​​Day​​​, ​​Week​​​, ​​Month​​​, ​​WorkWeek​

constructor(props) {
super(props);


this.state = {
selectedDate: null,
selectedDateRange: null,
dateRangeType: DateRangeType.Week,
};


this.DATE_RANGE_TYPES = [
{label: 'Day', value: DateRangeType.Day},
{label: 'Week', value: DateRangeType.Week},
{label: 'Month', value: DateRangeType.Month},
{label: 'WorkWeek', value: DateRangeType.WorkWeek},
]


this._onSelectDate = this._onSelectDate.bind(this);
this._goNext = this._goNext.bind(this);
this._goPrevious = this._goPrevious.bind(this);
this._handleDateRangeTypeChange = this._handleDateRangeTypeChange.bind(this);
}


render() {
const divStyle = {
height: '340px'
};


const buttonStyle = {
margin: '17px 10px 0 0'
};


let dateRangeString = null;


if (this.state.selectedDateRange) {
const rangeStart = this.state.selectedDateRange[0];
const rangeEnd = this.state.selectedDateRange[this.state.selectedDateRange.length - 1];
dateRangeString = rangeStart.toLocaleDateString() + '-' + rangeEnd.toLocaleDateString();
}


return (
<Example options={this._renderOptions()} {...this.props}>
<div style={divStyle}>
<div>
选择的日期:{' '}
<span>{!this.state.selectedDate ? '无' : this.state.selectedDate.toLocaleString()}</span>
</div>
<div>
选择的日期范围:
<span> {!dateRangeString ? '无' : dateRangeString}</span>
</div>
<Calendar
isMonthPickerVisible
dateRangeType={this.state.dateRangeType}
value={this.state.selectedDate}
onSelectDate={this._onSelectDate}
/>
<div>
<Button style={buttonStyle} onClick={this._goPrevious} text="上一区间" />
<Button style={buttonStyle} onClick={this._goNext} text="下一区间" />
</div>
</div>
</Example>
);
}


_onSelectDate(date: Date, dateRangeArray: Date[]) {
this.setState({
selectedDate: date,
selectedDateRange: dateRangeArray
});
}


_goPrevious() {
this.setState((prevState) => {
const selectedDate = prevState.selectedDate || new Date();
const dateRangeArray = this.props.getDateRangeArray(selectedDate, this.state.dateRangeType, DayOfWeek.Sunday);


let subtractFrom = dateRangeArray[0];
let daysToSubtract = dateRangeArray.length;


if (this.state.dateRangeType === DateRangeType.Month) {
subtractFrom = new Date(subtractFrom.getFullYear(), subtractFrom.getMonth(), 1);
daysToSubtract = 1;
}


const newSelectedDate = this.props.addDays(subtractFrom, -daysToSubtract);


return {
selectedDate: newSelectedDate
};
});
}


_goNext() {
this.setState((prevState) => {
const selectedDate = prevState.selectedDate || new Date();
const dateRangeArray = this.props.getDateRangeArray(selectedDate, this.state.dateRangeType, DayOfWeek.Sunday);
const newSelectedDate = this.props.addDays(dateRangeArray.pop(), 1);


return {
selectedDate: newSelectedDate
};
});
}


_handleDateRangeTypeChange(evt) {
this.setState({
dateRangeType: parseInt(evt.target.value)
})
}


_renderOptions() {
const { dateRangeType } = this.state;
return (
<React.Fragment>
<H5>Props</H5>
<Label>
Position
<HTMLSelect value={dateRangeType} onChange={this._handleDateRangeTypeChange} options={this.DATE_RANGE_TYPES} />
</Label>
</React.Fragment>
);
}

5. Checkbox 多选框

一组备选项中进行多选。

5.1 基本使用方法

单独使用可以表示两种状态之间的切换,写在标签中的内容为 checkbox 按钮后的介绍。

render() {
return (
<Example alignLeft compact {...this.props}>
<div>
<Checkbox label="Gilad Gray" defaultIndeterminate={true} />
<Checkbox label="Jason Killian" />
<Checkbox label="Antoine Llorca" />
<Checkbox>
<SvgIcon use="#user" color="success" size={16} />
<span style={{marginRight: 8}}></span>
Gilad <strong>Gray</strong>
</Checkbox>
</div>
</Example>
)
}

5.2 禁用状态

render() {
return (
<Example alignLeft {...this.props}>
<div>
<Checkbox disabled>备选项</Checkbox>
<Checkbox checked={true} disabled>选中且禁用</Checkbox>
</div>
</Example>
)
}

5.3 多选框组

适用于在多个互斥的选项中选择的场景

constructor(props) {
super(props);


this.state = {
mealTypes: ["one", "three"]
}
}


handleMealChange(values) {
this.setState({ mealTypes: values });
};


render() {
return (
<Example alignLeft {...this.props}>
<div>
<CheckboxGroup
label="Meal Choice"
onValuesChange={this.handleMealChange.bind(this)}
selectedValues={this.state.mealTypes}
>
<Checkbox label="Soup" value="one" />
<Checkbox label="Salad" value="two" />
<Checkbox label="Sandwich" value="three" />
<Checkbox label="Franchy" value="four" disabled />
</CheckboxGroup>
</div>
</Example>
)
}

5.3 自定义状态图标

render() {
return (
<Example alignLeft {...this.props}>
<Checkbox checkedIcon="heart" unCheckedIcon="heart-broken">备选项</Checkbox>
</Example>
)
}

6. Collapse 折叠面板

The Collapse element shows and hides content with a built-in slide in/out animation. You might use this to create a panel of settings for your application, with sub-sections that can be expanded and collapsed.

constructor(props) {
super(props);


this.state = {
isOpen: false,
keepChildrenMounted: false,
};


this.handleChildrenMountedChange = handleBooleanChange(keepChildrenMounted => {
this.setState({ keepChildrenMounted });
});
this.handleClick = () => this.setState({ isOpen: !this.state.isOpen });
}


render() {
const options = (
<React.Fragment>
<H5>Props</H5>
<Switch
checked={this.state.keepChildrenMounted}
label="Keep children mounted"
onChange={this.handleChildrenMountedChange}
/>
</React.Fragment>
);


return (
<Example options={options} {...this.props}>
<div style={{ width: "100%" }}>
<Button onClick={this.handleClick}>{this.state.isOpen ? "Hide" : "Show"} build logs</Button>
<Collapse isOpen={this.state.isOpen} keepChildrenMounted={this.state.keepChildrenMounted}>
<Pre>
[11:53:30] Finished 'typescript-bundle-blueprint' after 769 ms<br />
[11:53:30] Starting 'typescript-typings-blueprint'...<br />
[11:53:30] Finished 'typescript-typings-blueprint' after 198 ms<br />
[11:53:30] write ./blueprint.css<br />
[11:53:30] Finished 'sass-compile-blueprint' after 2.84 s
</Pre>
</Collapse>
</div>
</Example>
);
}

7. DatePicker 日期输入框

7.1 基础用法

constructor(props) {
super(props);


this.state = {
firstDayOfWeek: DayOfWeek.Sunday
};
}


render() {
const { firstDayOfWeek } = this.state;


return (
<Example {...this.props}>
<DatePicker
firstDayOfWeek={firstDayOfWeek}
placeholder="Select a date..."
onAfterMenuDismiss={() => console.log('onAfterMenuDismiss called')}
/>
</Example>
);
}

7.2 DatePicker允许输入日期字符串

Text input allowed by default when use keyboard navigation. Mouse click the TextField will popup DatePicker, click the TextField again will dismiss the DatePicker and allow text input.

constructor(props) {
super(props);


this.state = {
};
this._onSelectDate = this._onSelectDate.bind(this);
this._onClick = this._onClick.bind(this);
}


render() {
const { value } = this.state;


return (
<Example {...this.props}>
<DatePicker
allowTextInput
placeholder="Select a date..."
value={value}
onSelectDate={this._onSelectDate}
/>
<Button onClick={this._onClick} text="清除" />
</Example>
);
}


_onSelectDate(date) {
this.setState({ value: date });
}


_onClick() {
this.setState({ value: null });
}

7.3 DatePicker允许格式化日期

Applications can customize how dates are formatted and parsed. Formatted dates can be ambiguous, so the control will avoid parsing the formatted strings of dates selected using the UI when text input is allowed. In this example, we are formatting and parsing dates as dd/MM/yy.

constructor(props) {
super(props);


this.state = {
};
this._onSelectDate = this._onSelectDate.bind(this);
this._onClick = this._onClick.bind(this);
}


render() {
const { value } = this.state;


return (
<Example {...this.props}>
<DatePicker
allowTextInput
placeholder="Select a date..."
formatDate='DD/MM/YY'
value={value}
onSelectDate={this._onSelectDate}
/>
<Button onClick={this._onClick} text="清除" />
</Example>
);
}


_onSelectDate(date) {
this.setState({ value: date });
}


_onClick() {
this.setState({ value: null });
}

7.4 带日期边界的DatePicker(最小日期,最大日期)

When date boundaries are set (via minDate and maxDate props) the DatePicker will not allow out-of-bounds dates to be picked or entered. In this example, the allowed dates are 2018/6/30-2019/7/31

constructor(props) {
super(props);
}


render() {
const today: Date = new Date(Date.now());
const minDate: Date = this.props.addMonths(today, -1);
const maxDate: Date = this.props.addYears(today, 1);


return (
<Example {...this.props}>
<DatePicker
allowTextInput
placeholder="Select a date..."
minDate={minDate}
maxDate={maxDate}
/>
</Example>
);
}

8. Dialog 对话框

constructor(props) {
super(props);
this.handleOpen = this.handleOpen.bind(this);
this.handleClose = this.handleClose.bind(this);


this.state = {
autoFocus: true,
canEscapeKeyClose: true,
canOutsideClickClose: true,
enforceFocus: true,
isOpen: false,
usePortal: true,
}
}


handleOpen() {
this.setState({ isOpen: true });
}
handleClose() {
this.setState({ isOpen: false });
}


render() {
return (
<Example options={this.renderOptions()}>
<Button onClick={this.handleOpen}>Show dialog</Button>
<Dialog
onClose={this.handleClose}
title="Palantir Foundry"
{...this.state}
>
<div className="bp3-dialog-body">
<p>
<strong>
Data integration is the seminal problem of the digital age. For over ten years, we’ve
helped the world’s premier organizations rise to the challenge.
</strong>
</p>
<p>
Palantir Foundry radically reimagines the way enterprises interact with data by amplifying
and extending the power of data integration. With Foundry, anyone can source, fuse, and
transform data into any shape they desire. Business analysts become data engineers — and
leaders in their organization’s data revolution.
</p>
<p>
Foundry’s back end includes a suite of best-in-class data integration capabilities: data
provenance, git-style versioning semantics, granular access controls, branching,
transformation authoring, and more. But these powers are not limited to the back-end IT
shop.
</p>
<p>
In Foundry, tables, applications, reports, presentations, and spreadsheets operate as data
integrations in their own right. Access controls, transformation logic, and data quality
flow from original data source to intermediate analysis to presentation in real time. Every
end product created in Foundry becomes a new data source that other users can build upon.
And the enterprise data foundation goes where the business drives it.
</p>
<p>Start the revolution. Unleash the power of data integration with Palantir Foundry.</p>
</div>
<div className='bp3-dialog-footer'>
<div className='bp3-dialog-footer-actions'>
<Button onClick={this.handleClose}>Close</Button>
<Button
color='primary'
>
Visit the Foundry website
</Button>
</div>
</div>
</Dialog>
</Example>
)
}


renderOptions() {
const { autoFocus, enforceFocus, canEscapeKeyClose, canOutsideClickClose, usePortal } = this.state;


return (
<div>
<h5>Props</h5>
<Switch checked={autoFocus} label="Auto focus" onChange={(evt) => this.setState({autoFocus: evt.target.checked})} />
<Switch checked={enforceFocus} label="Enforce focus" onChange={(evt) => this.setState({enforceFocus: evt.target.checked})} />
<Switch checked={usePortal} onChange={(evt) => this.setState({usePortal: evt.target.checked})}>
Use <strong>Portal</strong>
</Switch>
<Switch
checked={canOutsideClickClose}
label="Click outside to close"
onChange={(evt) => this.setState({canOutsideClickClose: evt.target.checked})}
/>
<Switch checked={canEscapeKeyClose} label="Escape key to close" onChange={(evt) => this.setState({canEscapeKeyClose: evt.target.checked})} />
</div>
)
}

9. FocusZone 区域

FocusZones abstract arrow key navigation behaviors. Tabbable elements (buttons, anchors, and elements with data-is-focusable='true' attributes) are considered when pressing directional arrow keys and focus is moved appropriately. Tabbing to a zone sets focus only to the current "active" element, making it simple to use the tab key to transition from one zone to the next, rather than through every focusable element.

Using a FocusZone is simple. Just wrap a bunch of content inside of a FocusZone, and arrows and tabbling will be handled for you! See examples below.

constructor(props) {
super(props);


this.PHOTOS = createArray(25, () => {
const randomWidth = 50 + Math.floor(Math.random() * 150);


return {
url: `http://placehold.it/${randomWidth}x100`,
width: randomWidth,
height: 100
};
});
}


render() {
const log = (): void => {
console.log('clicked');
};


return (
<FocusZone elementType="ul" className="at-FocusZoneExamples-photoList">
{this.PHOTOS.map((photo, index) => (
<li
key={index}
className="at-FocusZoneExamples-photoCell"
aria-posinset={index + 1}
aria-setsize={this.PHOTOS.length}
aria-label="Photo"
data-is-focusable={true}
onClick={log}
>
<Image src={photo.url} width={photo.width} height={photo.height} />
</li>
))}
</FocusZone>
)
}

10. Label 表单标签

表单字段的描述标签。

constructor(props) {
super(props);


this.state = {
disabled: false,
helperText: false,
inline: false,
intent: Intent.NONE,
requiredLabel: true,
};


this.handleDisabledChange = handleBooleanChange(disabled => this.setState({ disabled }));
this.handleHelperTextChange = handleBooleanChange(helperText => this.setState({ helperText }));
this.handleInlineChange = handleBooleanChange(inline => this.setState({ inline }));
this.handleRequiredLabelChange = handleBooleanChange(requiredLabel => this.setState({ requiredLabel }));
this.handleIntentChange = handleStringChange((intent: Intent) => this.setState({ intent }));
}


render() {
const { disabled, helperText, inline, intent, requiredLabel } = this.state;


const options = (
<React.Fragment>
<H5>Props</H5>
<Switch label="Disabled" checked={disabled} onChange={this.handleDisabledChange} />
<Switch label="Inline" checked={inline} onChange={this.handleInlineChange} />
<Switch label="Show helper text" checked={helperText} onChange={this.handleHelperTextChange} />
<Switch label="Show label info" checked={requiredLabel} onChange={this.handleRequiredLabelChange} />
<IntentSelect intent={intent} onChange={this.handleIntentChange} />
</React.Fragment>
);


return (
<Example options={options} {...this.props}>
<FormGroup
disabled={disabled}
helperText={helperText && "Helper text with details..."}
inline={inline}
intent={intent}
label="Label"
labelFor="text-input"
labelInfo={requiredLabel && "(required)"}
>
<Input placeholder="Placeholder text" disabled={disabled} intent={intent} />
</FormGroup>
<FormGroup
disabled={disabled}
helperText={helperText && "Helper text with details..."}
inline={inline}
intent={intent}
label="Label"
labelFor="text-input"
labelInfo={requiredLabel && "(required)"}
>
<Switch label="Engage the hyperdrive" disabled={disabled} />
<Switch label="Initiate thrusters" disabled={disabled} />
</FormGroup>
</Example>
);
}

11. Image 图像

11.1 ImageFit: Not specified

render() {
return (
<div>
<p>
With no imageFit property set, the width and height props control the size of the frame. Depending on which of
those props is used, the image may scale to fit the frame.
</p>
<p>
Without a width or height specified, the frame remains at its natural size and the image will not be scaled.
</p>
<Image
src="http://placehold.it/350x150"
alt="Example implementation with no image fit property and no height or width is specified."
/>
<br />
<p>
If only a width is provided, the frame will be set to that width. The image will scale proportionally to fill
the available width.
</p>
<Image
src="http://placehold.it/350x150"
alt="Example implementation with no image fit property and only width is specified."
width={600}
/>
<br />
<p>
If only a height is provided, the frame will be set to that height. The image will scale proportionally to
fill the available height.
</p>
<Image
src="http://placehold.it/350x150"
alt="Example implementation with no image fit property and only height is specified."
height={100}
/>
<br />
<p>
If both width and height are provided, the frame will be set to that width and height. The image will scale to
fill both the available width and height. <strong>This may result in a distorted image.</strong>
</p>
<Image
src="http://placehold.it/350x150"
alt="Example implementation with no image fit property and height or width is specified."
width={100}
height={100}
/>
</div>
);
}

11.2 ImageFit: None

render() {
const imageProps: IImageProps = {
src: 'http://placehold.it/500x250',
imageFit: ImageFit.none,
width: 350,
height: 150
};


return (
<div>
<p>
By setting the imageFit property to "none", the image will remain at its natural size, even if the frame is
made larger or smaller by setting the width and height props.
</p>
<p>
The image is larger than the frame, so it is cropped to fit. The image is positioned at the upper left of the
frame.
</p>
<Image
{...imageProps}
alt="Example implementation of the property image fit using the none value on an image larger than the frame."
/>
<br />
<p>
The image is smaller than the frame, so there is empty space within the frame. The image is positioned at the
upper left of the frame.
</p>
<Image
{...imageProps}
src="http://placehold.it/100x100"
alt="Example implementation of the property image fit using the none value on an image smaller than the frame."
/>
</div>
);
}

11.3 ImageFit: Center

render() {
const imageProps: IImageProps = {
src: 'http://placehold.it/500x250',
imageFit: ImageFit.center,
width: 350,
height: 150
};


return (
<div>
<p>
Setting the imageFit property to "center" behaves the same as "none", while centering the image within the
frame.
</p>
<p>The image is larger than the frame, so all sides are cropped to center the image.</p>
<Image
{...imageProps}
src="http://placehold.it/800x300"
alt="Example implementation of the property image fit using the center value on an image larger than the frame."
/>
<br />
<p>
The image is smaller than the frame, so there is empty space within the frame. The image is centered in the
available space.
</p>
<Image
{...imageProps}
src="http://placehold.it/100x100"
alt="Example implementation of the property image fit using the center value on an image smaller than the frame."
/>
</div>
);
}

11.4 ImageFit: Contain

render() {
const imageProps: IImageProps = {
src: 'http://placehold.it/700x300',
imageFit: ImageFit.contain,
};


return (
<div>
<p>
Setting the imageFit property to "contain" will scale the image up or down to fit the frame, while maintaining
its natural aspect ratio and without cropping the image.
</p>
<p>
The image has a wider aspect ratio (more landscape) than the frame, so the image is scaled to fit the width
and centered in the available vertical space.
</p>
<Image
{...imageProps}
alt="Example implementation of the property image fit using the contain value on an image wider than the frame."
width={200}
height={200}
/>
<br />
<p>
The image has a taller aspect ratio (more portrait) than the frame, so the image is scaled to fit the height
and centered in the available horizontal space.
</p>
<Image
{...imageProps}
alt="Example implementation of the property image fit using the contain value on an image taller than the frame."
width={300}
height={50}
/>
</div>
);
}

11.5 ImageFit: Cover

render() {
const imageProps: IImageProps = {
src: 'http://placehold.it/500X500',
imageFit: ImageFit.cover,
};


return (
<div>
<p>
Setting the imageFit property to "cover" will cause the image to scale up or down proportionally, while
cropping from either the top and bottom or sides to completely fill the frame.
</p>
<p>
The image has a wider aspect ratio (more landscape) than the frame, so the image is scaled to fit the height
and the sides are cropped evenly.
</p>
<Image
{...imageProps}
alt="Example implementation of the property image fit using the cover value on an image wider than the frame."
width={150}
height={250}
/>
<br />
<p>
The image has a taller aspect ratio (more portrait) than the frame, so the image is scaled to fit the width
and the top and bottom are cropped evenly.
</p>
<Image
{...imageProps}
alt="Example implementation of the property image fit using the cover value on an image taller than the frame."
width={250}
height={150}
/>
</div>
);
}

11.6 Maximizing the image frame

render() {
const imageProps: IImageProps = {
src: 'http://placehold.it/500x500',
imageFit: ImageFit.cover,
maximizeFrame: true
};


return (
<div>
<p>
Where the exact width and height of the image's frame is not known, such as when sizing an image as a
percentage of its parent, you can use the "maximizeFrame" prop to expand the frame to fill the parent element.
</p>
<p>The image is placed within a landscape container.</p>
<div style={{ width: '200px', height: '100px' }}>
<Image
{...imageProps}
alt="Example implementation of the property maximize frame with a landscape container."
/>
</div>
<br />
<p>The image is placed within a portrait container.</p>
<div style={{ width: '100px', height: '200px' }}>
<Image
{...imageProps}
alt="Example implementation of the property maximize frame with a portrait container"
/>
</div>
</div>
);
}

12. Input 输入框

12.1 基本使用方法

render() {
return [
<div className="at-row">
<Input placeholder="Text input"/>
<Input placeholder="Text input" disabled/>
<Input placeholder="Text input" readonly/>
</div>
,
<div className="at-row">
<Input placeholder="Text input" round/>
<Input placeholder="Text input" intent="primary"/>
<Input placeholder="Text input" intent="warning"/>
</div>
,
<div className="at-row" data-modifier=".pt-fill">
<Input placeholder="large and fill" large fill/>
</div>
]
}

12.2 带图标的输入框

render() {
return [
<div className="at-row">
<Input placeholder="Filter histogram..." leftElement="filter" />
<Input placeholder="Filter histogram..." leftElement="filter" disabled/>
<Input placeholder="Filter histogram..." leftElement="filter" round/>
</div>
,
<div className="at-row">
<Input placeholder="Enter your password..." rightElement="lock" intent="warning"/>
<Input placeholder="Enter your password..." rightElement="lock" disabled/>
<Input placeholder="Enter your password..." rightElement="lock" intent="warning" round/>
</div>
,
<div className="at-row" data-modifier=".pt-fill">
<Input placeholder="Filter histogram..." fill leftElement="filter" />
</div>
,
<div className="at-row" data-modifier=".pt-fill">
<Input placeholder="Enter your password..." fill rightElement="lock" intent="warning"/>
</div>
,
<div className="at-row" data-modifier=".pt-fill">
<Input placeholder="Search" fill leftElement="search" rightElement="arrow-right" intent="success"/>
</div>
]
}

12.3 多行文本输入框

render() {
return [
<div className="at-row">
<div>
<TextArea placeholder="Writing..." fill/>
</div>
<div>
<TextArea placeholder="Writing..." disabled fill/>
</div>
<div>
<TextArea placeholder="Writing..." intent="success" fill/>
</div>
</div>
,
<div className="at-row">
<div data-modifier=".pt-fill">
<TextArea placeholder="Writing..." rows={3} fill/>
</div>
</div>
,
<div className="at-row">
<div data-modifier=".pt-fill">
<TextArea placeholder="Writing..." fill autosize={{minRows: 2, maxRows: 8}}/>
</div>
</div>
]
}

13. List 列表

constructor(props) {
super(props);


this.items = createArray(25, () => {
const randomWidth = 50 + Math.floor(Math.random() * 150);

return {
url: `http://placehold.it/${randomWidth}x100`,
content: lorem(10),
width: randomWidth,
height: 100
};
});
}


render() {
return (
<Example {...this.props}>
<div className="example-block is-scrollable">
<List
items={this.items}
onRenderCell={this._renderRow.bind(this)}
/>
</div>
</Example>
)
}


_renderRow(item, index) {
return (
<div>
<p className="list-textContent">{item.content}</p>
<Image src={item.url} width={item.width} height={item.height} />
</div>
)
}

14. ListView 列表

​ListView​​​ 和 ​​List​​ 的不同之处在于 ListView 是可交互的列表组件。

​ListView​​​ 可以支持 ​​Selection​​ 进行单选或者多选,行可以有焦点。

ListView 组件同样是虚拟渲染方式,可以支持超大数据量的显示,同时也支持动态行高。

constructor(props) {
super(props);


this.items = createArray(25, () => {
const randomWidth = 50 + Math.floor(Math.random() * 150);

return {
url: `http://placehold.it/${randomWidth}x100`,
content: lorem(20),
width: randomWidth,
height: 100
};
});
}


render() {
return (
<Example {...this.props}>
<div className="example-block is-scrollable">
<ListView
items={this.items}
onRenderItem={this._renderRow.bind(this)}
/>
</div>
</Example>
)
}


_renderRow(item, index) {
return (
<div className="listItem">
<p className="list-textContent">{item.content}</p>
<Image src={item.url} width={item.width} height={item.height} />
</div>
)
}

15. Navbar 工具栏

由于选项默认可见,不宜过多,若选项过多,建议使用 Select 选择器。

render() {
return (
<div>
<Navbar>
<NavbarGroup align={Alignment.LEFT}>
<NavbarHeading>Athena</NavbarHeading>
<NavbarDivider />
<Button minimal icon="home" text="Home" />
<Button minimal icon="document" text="Files" />
</NavbarGroup>
</Navbar>
</div>
)
}

16. NumericInput 数字输入框

16.1 使用方法(Uncontrolled)

render() {
return [
<div className="at-row">
<NumericInput placeholder="Text input"/>
<NumericInput placeholder="Text input" disabled/>
<NumericInput placeholder="Text input" readonly/>
</div>
,
<div className="at-row">
<NumericInput placeholder="Text input" round/>
<NumericInput placeholder="Text input" intent="primary"/>
<NumericInput placeholder="Text input" intent="warning"/>
</div>
,
<div className="at-row" data-modifier=".pt-fill">
<NumericInput placeholder="large and fill" large fill/>
</div>
]
}

16.2 使用方法(Controlled)

constructor(props) {
super(props);
this.state = {
value: 0,
};

this._onValueChanged = this._onValueChanged.bind(this);
}


render() {
const { value } = this.state;

return [
<div className="at-row">
<NumericInput placeholder="Text input" value={value} onValueChanged={this._onValueChanged}/>
</div>
]
}


_onValueChanged(evt, value /* value is number type */) {
console.log(value);
this.setState({
value: value
})
}

17. Overlay 弹层

constructor(props) {
super(props);


this.refHandlers = {
button: (ref) => (this.button = ref),
};


this.handleAutoFocusChange = handleBooleanChange(autoFocus => this.setState({ autoFocus }));
this.handleBackdropChange = handleBooleanChange(hasBackdrop => this.setState({ hasBackdrop }));
this.handleEnforceFocusChange = handleBooleanChange(enforceFocus => this.setState({ enforceFocus }));
this.handleEscapeKeyChange = handleBooleanChange(canEscapeKeyClose => this.setState({ canEscapeKeyClose }));
this.handleUsePortalChange = handleBooleanChange(usePortal => this.setState({ usePortal }));
this.handleOutsideClickChange = handleBooleanChange(val => this.setState({ canOutsideClickClose: val }));
this.handleOpen = () => this.setState({ isOpen: true });
this.handleClose = () => this.setState({ isOpen: false });
this.focusButton = () => this.button.focus();


this.state = {
autoFocus: true,
canEscapeKeyClose: true,
canOutsideClickClose: true,
enforceFocus: true,
hasBackdrop: true,
isOpen: false,
usePortal: true,
};


}


render() {
const classes = classNames(Classes.CARD, Classes.ELEVATION_4, "docs-overlay-example-transition");


return (
<Example options={this.renderOptions()} {...this.props}>
<Button elementRef={this.refHandlers.button} onClick={this.handleOpen} text="Show overlay" />
<Overlay onClose={this.handleClose} className={Classes.OVERLAY_SCROLL_CONTAINER} {...this.state}>
<div className={classes}>
<H3>I'm an Overlay!</H3>
<p>
This is a simple container with some inline styles to position it on the screen. Its CSS
transitions are customized for this example only to demonstrate how easily custom
transitions can be implemented.
</p>
<p>
Click the right button below to transfer focus to the "Show overlay" trigger button outside
of this overlay. If persistent focus is enabled, focus will be constrained to the overlay.
Use the <Code>tab</Code> key to move to the next focusable element to illustrate this
effect.
</p>
<br />
<Button intent={Intent.DANGER} onClick={this.handleClose}>
Close
</Button>
<Button onClick={this.focusButton} style={{ float: "right" }}>
Focus button
</Button>
</div>
</Overlay>
</Example>
);
}


renderOptions() {
const { autoFocus, enforceFocus, canEscapeKeyClose, canOutsideClickClose, hasBackdrop, usePortal } = this.state;
return (
<React.Fragment>
<H5>Props</H5>
<Switch checked={autoFocus} label="Auto focus" onChange={this.handleAutoFocusChange} />
<Switch checked={enforceFocus} label="Enforce focus" onChange={this.handleEnforceFocusChange} />
<Switch checked={usePortal} onChange={this.handleUsePortalChange}>
Use <Code>Portal</Code>
</Switch>
<Switch
checked={canOutsideClickClose}
label="Click outside to close"
onChange={this.handleOutsideClickChange}
/>
<Switch checked={canEscapeKeyClose} label="Escape key to close" onChange={this.handleEscapeKeyChange} />
<Switch checked={hasBackdrop} label="Has backdrop" onChange={this.handleBackdropChange} />
</React.Fragment>
);
}

18. Popover 弹出框

constructor(props) {
super(props);


this.INTERACTION_KINDS = [
{ label: "Click", value: PopoverInteractionKind.CLICK.toString() },
{ label: "Click (target only)", value: PopoverInteractionKind.CLICK_TARGET_ONLY.toString() },
{ label: "Hover", value: PopoverInteractionKind.HOVER.toString() },
{ label: "Hover (target only)", value: PopoverInteractionKind.HOVER_TARGET_ONLY.toString() },
];


this.VALID_POSITIONS = [
"auto",
Position.TOP_LEFT,
Position.TOP,
Position.TOP_RIGHT,
Position.RIGHT_TOP,
Position.RIGHT,
Position.RIGHT_BOTTOM,
Position.BOTTOM_LEFT,
Position.BOTTOM,
Position.BOTTOM_RIGHT,
Position.LEFT_TOP,
Position.LEFT,
Position.LEFT_BOTTOM,
];


this.state = {
canEscapeKeyClose: true,
exampleIndex: 0,
hasBackdrop: false,
inheritDarkTheme: true,
interactionKind: PopoverInteractionKind.CLICK,
isOpen: false,
minimal: false,
modifiers: {
arrow: { enabled: true },
flip: { enabled: true },
keepTogether: { enabled: true },
preventOverflow: { enabled: true, boundariesElement: "scrollParent" },
},
position: "auto",
sliderValue: 5,
usePortal: true,
}


this.getModifierChangeHandler = (name) => {
return (evt => {
const enabled = evt.target.checked;
this.setState({
modifiers: {
...this.state.modifiers,
[name]: { ...this.state.modifiers[name], enabled },
},
});
});
}


this.handleSliderChange = (value) => this.setState({ sliderValue: value });
this.handleExampleIndexChange = handleNumberChange(exampleIndex => this.setState({ exampleIndex }));
this.handleInteractionChange = handleStringChange((interactionKind) => {
const hasBackdrop = this.state.hasBackdrop && interactionKind === PopoverInteractionKind.CLICK;
this.setState({ interactionKind, hasBackdrop });
});
this.handlePositionChange = handleStringChange((position) => this.setState({ position }));
this.handleBoundaryChange = handleStringChange((boundary) =>
this.setState({
modifiers: {
...this.state.modifiers,
preventOverflow: {
boundariesElement: boundary,
enabled: boundary.length > 0,
},
},
}),
);
this.toggleEscapeKey = handleBooleanChange(canEscapeKeyClose => this.setState({ canEscapeKeyClose }));
this.toggleIsOpen = handleBooleanChange(isOpen => this.setState({ isOpen }));
this.toggleMinimal = handleBooleanChange(minimal => this.setState({ minimal }));
this.toggleUsePortal = handleBooleanChange(usePortal => {
if (usePortal) {
this.setState({ hasBackdrop: false, inheritDarkTheme: false });
}
this.setState({ usePortal });
});


}


render() {
const { exampleIndex, sliderValue, ...popoverProps } = this.state;
return (
<Example options={this.renderOptions()} {...this.props}>
<div className="docs-popover-example-scroll" ref={this.centerScroll}>
<Popover
popoverClassName={exampleIndex <= 2 ? Classes.POPOVER_CONTENT_SIZING : ""}
portalClassName="foo"
{...popoverProps}
enforceFocus={false}
isOpen={this.state.isOpen === true ? /* Controlled */ true : /* Uncontrolled */ undefined}
>
<Button intent={Intent.PRIMARY} text="Popover target" />
{this.getContents(exampleIndex)}
</Popover>
<p>
Scroll around this container to experiment<br />
with <Code>flip</Code> and <Code>preventOverflow</Code> modifiers.
</p>
</div>
</Example>
);
}


renderOptions() {
const { arrow, flip, preventOverflow } = this.state.modifiers;
return (
<React.Fragment>
<H5>Appearance</H5>
<FormGroup
helperText="May be overridden to prevent overflow"
label="Position when opened"
labelFor="position"
>
<HTMLSelect
value={this.state.position}
onChange={this.handlePositionChange}
options={this.VALID_POSITIONS}
/>
</FormGroup>
<FormGroup label="Example content">
<HTMLSelect value={this.state.exampleIndex} onChange={this.handleExampleIndexChange}>
<option value="0">Text</option>
<option value="1">Input</option>
<option value="2">Slider</option>
<option value="3">Menu</option>
<option value="4">Empty</option>
</HTMLSelect>
</FormGroup>
<Switch checked={this.state.usePortal} onChange={this.toggleUsePortal}>
Use <Code>Portal</Code>
</Switch>
<Switch checked={this.state.minimal} label="Minimal appearance" onChange={this.toggleMinimal} />
<Switch checked={this.state.isOpen} label="Open (controlled mode)" onChange={this.toggleIsOpen} />


<H5>Interactions</H5>
<RadioGroup
label="Interaction kind"
selectedValue={this.state.interactionKind.toString()}
options={this.INTERACTION_KINDS}
onChange={this.handleInteractionChange}
/>
<Switch
checked={this.state.canEscapeKeyClose}
label="Can escape key close"
onChange={this.toggleEscapeKey}
/>


<H5>Modifiers</H5>
<Switch checked={arrow.enabled} label="Arrow" onChange={this.getModifierChangeHandler("arrow")} />
<Switch checked={flip.enabled} label="Flip" onChange={this.getModifierChangeHandler("flip")} />
<Switch
checked={preventOverflow.enabled}
label="Prevent overflow"
onChange={this.getModifierChangeHandler("preventOverflow")}
>
<br />
<div style={{ marginTop: 5 }} />
<HTMLSelect
disabled={!preventOverflow.enabled}
value={preventOverflow.boundariesElement.toString()}
onChange={this.handleBoundaryChange}
>
<option value="scrollParent">scrollParent</option>
<option value="viewport">viewport</option>
<option value="window">window</option>
</HTMLSelect>
</Switch>
</React.Fragment>
);


}


getContents(index) {
return [
<div key="text">
<H5>Confirm deletion</H5>
<p>Are you sure you want to delete these items? You won't be able to recover them.</p>
<div style={{ display: "flex", justifyContent: "flex-end", marginTop: 15 }}>
<Button className={Classes.POPOVER_DISMISS} style={{ marginRight: 10 }}>
Cancel
</Button>
<Button intent={Intent.DANGER} className={Classes.POPOVER_DISMISS}>
Delete
</Button>
</div>
</div>,
<div key="input">
<label className={Classes.LABEL}>
Enter some text
<input autoFocus={true} className={Classes.INPUT} type="text" />
</label>
</div>,
<Slider key="slider" min={0} max={10} onChange={this.handleSliderChange} value={this.state.sliderValue} />,
<Menu key="menu">
<MenuDivider title="Edit" />
<MenuItem icon="cut" text="Cut" label="⌘X" />
<MenuItem icon="duplicate" text="Copy" label="⌘C" />
<MenuItem icon="clipboard" text="Paste" label="⌘V" disabled={true} />
<MenuDivider title="Text" />
<MenuItem icon="align-left" text="Alignment">
<MenuItem icon="align-left" text="Left" />
<MenuItem icon="align-center" text="Center" />
<MenuItem icon="align-right" text="Right" />
<MenuItem icon="align-justify" text="Justify" />
</MenuItem>
<MenuItem icon="style" text="Style">
<MenuItem icon="bold" text="Bold" />
<MenuItem icon="italic" text="Italic" />
<MenuItem icon="underline" text="Underline" />
</MenuItem>
</Menu>,
][index];
}


centerScroll(div) {
if (div != null) {
// if we don't requestAnimationFrame, this function apparently executes
// before styles are applied to the page, so the centering is way off.
requestAnimationFrame(() => {
const container = div.parentElement;
container.scrollTop = div.clientHeight / 4;
container.scrollLeft = div.clientWidth / 4;
});
}
}

19. ProgressBar 进度条

constructor(props) {
super(props);


this.state = {
hasValue: false,
value: 0.7,
}
}


render() {
const { hasValue, intent, value } = this.state;


return (
<Example options={this.renderOptions()}>
<ProgressBar intent={intent} value={hasValue ? value : null} />
</Example>
)
}


renderOptions() {
const { hasValue, intent, value } = this.state;


return (
<div>
<h5>Props</h5>
<IntentSelect intent={intent} onChange={(evt) => this.setState({ intent: evt.target.value})} />
<Switch checked={hasValue} label="Known value" onChange={(evt) => this.setState({ hasValue: evt.target.checked})} />
<Slider
disabled={!hasValue}
labelStepSize={1}
min={0}
max={1}
onChange={(value) => this.setState({ value: value})}
labelRenderer={this.renderLabel}
stepSize={0.1}
showTrackFill={false}
value={value}
/>
</div>
)
}


renderLabel(value) {
return value.toFixed(1);
}

20. Radio 单选框

在一组备选项中进行单选。

20.1 基本使用方法

由于选项默认可见,不宜过多,若选项过多,建议使用 Select 选择器。

constructor(props) {
super(props);


this.state = {
value: 1
}
}


onChange(e) {
this.setState({ value: parseInt(e.target.value) });
}


render() {
return (
<Example {...this.props}>
<div>
<Radio value="1" checked={this.state.value === 1} onChange={this.onChange.bind(this)}>备选项</Radio>
<Radio value="2" checked={this.state.value === 2} onChange={this.onChange.bind(this)}>备选项</Radio>
</div>
</Example>
)
}

20.2 禁用状态

render() {
return (
<Example {...this.props}>
<div>
<Radio value="1" disabled>备选项</Radio>
<Radio value="2" checked={true} disabled>选中且禁用</Radio>
</div>
</Example>
)
}

20.3 单选框组

适用于在多个互斥的选项中选择的场景。

constructor(props) {
super(props);


this.state = {
mealType: "one"
}
}


handleMealChange(e) {
this.setState({ mealType: e.target.value });
};


render() {
return (
<Example {...this.props}>
<div>
<RadioGroup
label="Meal Choice"
onChange={this.handleMealChange.bind(this)}
selectedValue={this.state.mealType}
>
<Radio label="Soup" value="one" />
<Radio label="Salad" value="two" />
<Radio label="Sandwich" value="three" />
</RadioGroup>
</div>
</Example>
)
}

21. Selection 选择

constructor(props) {
super(props);


this._onSelectionChanged = this._onSelectionChanged.bind(this);
this._onToggleSelectAll = this._onToggleSelectAll.bind(this);


this.state = {
items: this._createListItmes(),
selection: new Selection({onSelectionChanged: this._onSelectionChanged}),
selectionMode: SelectionMode.multiple
};


this.state.selection.setItems(this.state.items, false);
}


render() {
const {selection, selectionMode, items} = this.state;


return (
<div className="selection">
<div className="selection-item-check">
<Checkbox
onChange={this._onToggleSelectAll}
checked={selection.isAllSelected()}
indeterminate={selection.getSelectedCount() > 0 && !selection.isAllSelected()}
>全选</Checkbox>
</div>
<SelectionZone
selection={selection}
>
{items.map((item, index) => (
this._renderItem(item, index)
))}
</SelectionZone>
</div>
)
}


_createListItmes() {
const colors = '赤橙黄绿青蓝紫';


return colors.split('').map((c, index) => ({
key: 'k' + index,
name: c,
}))
}


_renderItem(item, index) {
const {selection} = this.state;
let isSelected = false;


if (selection && index !== undefined) {
isSelected = selection.isIndexSelected(index);
}


return (
<div
key={item.key}
className="selection-item"
data-is-focusable={true}
data-selection-toggle={true}
data-selection-index={index}
>
{selection &&
selection.canSelectItem(item) &&
selection.mode !== SelectionMode.none && (
<div className="selection-item-check" data-is-focusable={true}>
<Check checked={isSelected} />
</div>
)}
<span className="selection-item-name">{item.name}</span>
</div>
)
}


_onSelectionChanged() {
console.log('onSelectionChanged')
this.forceUpdate();
}


_onToggleSelectAll(evt) {
const { selection } = this.state;
selection.toggleAllSelected();
}

22. Spinner 加载

Spinners indicate progress in a circular fashion. They're great for ongoing operations and can also represent known progress.

22.1 基本用法

constructor(props) {
super(props);


this.state = {
hasValue: false,
size: Spinner.SIZE_STANDARD,
value: 0.7,
};


this.handleIndeterminateChange = handleBooleanChange(hasValue => this.setState({ hasValue }));
this.handleModifierChange = handleStringChange((intent) => this.setState({ intent }));
this.renderLabel = (value) => value.toFixed(1);
this.handleValueChange = (value) => this.setState({ value });
this.handleSizeChange = (size) => this.setState({ size });
}


render() {
const { size, hasValue, intent, value } = this.state;
return (
<Example options={this.renderOptions()} {...this.props}>
<Spinner intent={intent} size={size} value={hasValue ? value : null} />
</Example>
);
}


renderOptions() {
const { size, hasValue, intent, value } = this.state;
return (
<React.Fragment>
<H5>Props</H5>
<IntentSelect intent={intent} onChange={this.handleModifierChange} />
<Label>Size</Label>
<Slider
labelStepSize={50}
min={0}
max={Spinner.SIZE_LARGE * 2}
showTrackFill={false}
stepSize={5}
value={size}
onChange={this.handleSizeChange}
/>
<Switch checked={hasValue} label="Known value" onChange={this.handleIndeterminateChange} />
<Slider
disabled={!hasValue}
labelStepSize={1}
min={0}
max={1}
onChange={this.handleValueChange}
labelRenderer={this.renderLabel}
stepSize={0.1}
showTrackFill={false}
value={value}
/>
</React.Fragment>
);
}

22.2 Props

​Spinner​​ is a simple stateless component that renders SVG markup. It can be used safely in DOM and SVG containers as it only renders SVG elements.

 

The ​​value​​​prop determines how much of the track is filled by the head. When this prop is defined, the spinner head will smoothly animate as ​​value​​​ changes. Omitting ​​value​​ will result in an "indeterminate" spinner where the head spins indefinitely (this is the default appearance).

 

The ​​size​​​ prop determines the pixel width/height of the spinner. Size constants are provided as static properties: Spinner.SIZE_SMALL, ​​Spinner.SIZE_STANDARD​​​, ​​Spinner.SIZE_LARGE​​​. Small and large sizes can be set by including ​​Classes.SMALL​​​ or ​​Classes.LARGE​​​ in ​​className​​​ instead of the ​​size​​ prop (this prevents an API break when upgrading to 3.x).

23. Sticky

23.1 基本使用方法

render() {
const contentAreas = [];
for (let i = 0; i < 5; i++) {
contentAreas.push(this._createContentArea(i));
}


return (
<Example data-example-id='StickyExample'>
<div
style={{
height: '600px',
width: '400px',
position: 'relative',
maxHeight: 'inherit'
}}
>
<ScrollablePane className="scrollablePaneDefaultExample">
{contentAreas.map(ele => {
return ele;
})}
</ScrollablePane>
</div>
</Example>
);
}


_createContentArea(index) {
const colors = ['#eaeaea', '#dadada', '#d0d0d0', '#c8c8c8', '#a6a6a6', '#c7e0f4', '#71afe5', '#eff6fc', '#deecf9'];
const color = colors.splice(Math.floor(Math.random() * colors.length), 1)[0];


return (
<div
key={index}
style={{
backgroundColor: color
}}
>
<Sticky stickyPosition={StickyPositionType.Both}>
<div className="sticky">Sticky Component #{index + 1}</div>
</Sticky>
<div className="textContent">{this.props.lorem(100)}</div>
</div>
);
}

23.2 和 ListView 组合使用

constructor(props) {
super(props);
this.data = [];
this.state = {
desc: this.props.lorem(30),
expandedRow: undefined,
scrollToIndex: 0,
}


for (let i=0; i<10000; i++) {
this.data.push(`[${i}] ${lorem(10 + Math.round(Math.random() * 50))}`);
}
}


render() {
return (
<Example data-example-id='StickyGridExample'>
<div
style={{
height: '500px',
width: '420px',
position: 'relative',
maxHeight: 'inherit'
}}
>
<ScrollablePane className="scrollablePaneDefaultExample" style={{background: "#f3f3f3"}}>
<Sticky stickyPosition={StickyPositionType.Header}>
<div className="sticky">Search something ...</div>
</Sticky>
<div className="textContent">{this.state.desc}</div>
<Sticky stickyPosition={StickyPositionType.Header}>
<div style={{padding: 4}}>
<Input placeholder="Search" fill leftElement="search" rightElement="arrow-right" intent="success"/>
</div>
</Sticky>
<ListView
items={this.data}
checkboxVisibility={CheckboxVisibility.hidden}
onRenderItem={this._renderRow.bind(this)}
extra={{expandedRow: this.state.expandedRow}}
/>
</ScrollablePane>
</div>
</Example>
);
}


_renderRow(item, index) {
return (
<div className="listItem">
<p className="list-textContent">{item}</p>
{this.state.expandedRow !== index &&
<Button intent="success" small onClick={() => this.setState({expandedRow: index, scrollToIndex: this.state.scrollToIndex + 20})}>展开</Button>
}
{this.state.expandedRow === index &&
<div>
<Button intent="success" small onClick={() => this.setState({expandedRow: undefined, scrollToIndex: 0})} >收起</Button>
<p className="list-textContent">哈哈,增加了一些内容!</p>
</div>
}
</div>
)
}

24. SvgIcon 图标

提供了一套常用的图标集合。

直接通过设置 ​​use​​ 图标名称来使用即可。例如:

render() {
return (
<div>
<SvgIcon intent="primary" use="#add" />
<SvgIcon intent="success" use="#search" />
<SvgIcon intent="warning" use="#caret-down" size={48} />
</div>
)
}

25. Tabs 控件

25.1 水平模式

constructor(props) {
super(props);

this.state = {
focusIndex: 0
}


this.onTabChange = (index: number) => {
this.setState({
focusIndex: index
})
}


}


render() {
return (
<Example {...this.props}>
<TabList
focusIndex={this.state.focusIndex}
onTabChange={this.onTabChange}
>
<Tab>Loki</Tab>
<Tab>Thor</Tab>
<Tab>Iron Man</Tab>
</TabList>
</Example>
)
}

25.2 垂直模式

constructor(props) {
super(props);

this.state = {
focusIndex: 0
}


this.onTabChange = (index: number) => {
this.setState({
focusIndex: index
})
}


}


render() {
return (
<Example {...this.props}>
<div style={{ display: "flex", width: 100, height: 200 }}>
<TabList
isVertical={true}
focusIndex={this.state.focusIndex}
onTabChange={this.onTabChange}
>
<Tab>Loki</Tab>
<Tab>Thor</Tab>
<Tab>Iron Man</Tab>
</TabList>
</div>
</Example>
)
}

26. Text 溢出提示文本

​Text​​​ 在其内容溢出其容器时使用省略号截断,并且将完整的文本通过添加 ​​title​​ 属性显示。

constructor(props) {
super(props);


this.state = {
textContent:
"You can change the text in the input below. Hover to see full text. " +
"If the text is long enough, then the content will overflow. This is done by setting " +
"ellipsize to true.",
};


this.onInputChange = handleStringChange((textContent) => this.setState({ textContent }));
}


render() {
return (
<Example options={false} {...this.props}>
<Text ellipsize={true}>
{this.state.textContent}
 
</Text>
<TextArea fill={true} onChange={this.onInputChange} value={this.state.textContent} />
</Example>
);
}

27. Toast 消息提示

constructor(props) {
super(props);


this.refHandlers = {
toaster: (ref: Toaster) => (this.toaster = ref),
}


this.POSITIONS = [
Position.TOP_LEFT,
Position.TOP,
Position.TOP_RIGHT,
Position.BOTTOM_LEFT,
Position.BOTTOM,
Position.BOTTOM_RIGHT,
];


this.TOAST_BUILDERS = [
{
action: {
href: "https://www.google.com/search?q=toast&source=lnms&tbm=isch",
target: "_blank",
text: <strong>Yum.</strong>,
},
button: "Procure toast",
intent: Intent.PRIMARY,
message: (
<React.Fragment>
One toast created. <em>Toasty.</em>
</React.Fragment>
),
},
{
action: {
onClick: () =>
this.addToast({
icon: "ban-circle",
intent: Intent.DANGER,
message: "You cannot undo the past.",
}),
text: "Undo",
},
button: "Move files",
icon: "tick",
intent: Intent.SUCCESS,
message: "Moved 6 files.",
},
{
action: {
onClick: () => this.addToast(this.TOAST_BUILDERS[2]),
text: "Retry",
},
button: "Delete root",
icon: "warning-sign",
intent: Intent.DANGER,
message:
"You do not have permissions to perform this action. \
Please contact your system administrator to request the appropriate access rights.",
},
{
action: {
onClick: () => this.addToast({ message: "Isn't parting just the sweetest sorrow?" }),
text: "Adieu",
},
button: "Log out",
icon: "hand",
intent: Intent.WARNING,
message: "Goodbye, old friend.",
},
];


this.handlePositionChange = handleStringChange((position) => this.setState({ position }));
this.toggleAutoFocus = handleBooleanChange(autoFocus => this.setState({ autoFocus }));
this.toggleEscapeKey = handleBooleanChange(canEscapeKeyClear => this.setState({ canEscapeKeyClear }));


this.handleProgressToast = () => {
let progress = 0;
const key = this.toaster.show(this.renderProgress(0));
const interval = setInterval(() => {
if (this.toaster == null || progress > 100) {
clearInterval(interval);
} else {
progress += 10 + Math.random() * 20;
this.toaster.show(this.renderProgress(progress), key);
}
}, 1000);
};


this.state = {
autoFocus: false,
canEscapeKeyClear: true,
position: Position.TOP,
}
}


render() {
return (
<Example options={this.renderOptions()} {...this.props}>
{this.TOAST_BUILDERS.map(this.renderToastDemo, this)}
<Button onClick={this.handleProgressToast} text="Upload file" />
<Toaster {...this.state} ref={this.refHandlers.toaster} />
</Example>
);
}


renderOptions() {
const { autoFocus, canEscapeKeyClear, position } = this.state;
return (
<React.Fragment>
<H5>Props</H5>
<Label>
Position
<HTMLSelect value={position} onChange={this.handlePositionChange} options={this.POSITIONS} />
</Label>
<Switch label="Auto focus" checked={autoFocus} onChange={this.toggleAutoFocus} />
<Switch label="Can escape key clear" checked={canEscapeKeyClear} onChange={this.toggleEscapeKey} />
</React.Fragment>
);
}


renderToastDemo(toast, index) {
// tslint:disable-next-line:jsx-no-lambda
return <Button intent={toast.intent} key={index} text={toast.button} onClick={() => this.addToast(toast)} />;
}


renderProgress(amount) {
return {
icon: "cloud-upload",
message: (
<ProgressBar
className={classNames("docs-toast-progress", { [Classes.PROGRESS_NO_STRIPES]: amount >= 100 })}
intent={amount < 100 ? Intent.PRIMARY : Intent.SUCCESS}
value={amount / 100}
/>
),
timeout: amount < 100 ? 0 : 2000,
};
}


addToast(toast) {
toast.timeout = 5000;
this.toaster.show(toast);
}

28. Tooltip 文字提示

constructor(props) {
super(props);


this.toggleControlledTooltip = this.toggleControlledTooltip.bind(this);


this.state = {
isOpen: false,
}
}


render() {
// using JSX instead of strings for all content so the tooltips will re-render
// with every update for dark theme inheritance.
const lotsOfText = (
<span>
In facilisis scelerisque dui vel dignissim. Sed nunc orci, ultricies congue vehicula quis, facilisis a
orci.
</span>
);
const jsxContent = (
<em>
This tooltip contains an <strong>em</strong> tag.
</em>
);

return (
<Example options={false} {...this.props}>
<div>
Inline text can have{" "}
<Tooltip className={Classes.TOOLTIP_INDICATOR} content={jsxContent}>
a tooltip.
</Tooltip>
</div>
<div>
<Tooltip content={lotsOfText}>Or, hover anywhere over this whole line.</Tooltip>
</div>
<div>
This line's tooltip{" "}
<Tooltip className={Classes.TOOLTIP_INDICATOR} content={<span>disabled</span>} disabled={true}>
is disabled.
</Tooltip>
</div>
<div>
This line's tooltip{" "}
<Tooltip
className={Classes.TOOLTIP_INDICATOR}
content={<span>BRRAAAIINS</span>}
isOpen={this.state.isOpen}
>
is controlled by external state.
</Tooltip>
<Switch
checked={this.state.isOpen}
label="Open"
onChange={this.toggleControlledTooltip}
style={{ display: "inline-block", marginBottom: 0, marginLeft: 20 }}
/>
</div>
<div>
<Tooltip
className={Classes.TOOLTIP_INDICATOR}
content="Color.PRIMARY"
intent='primary'
position={Position.LEFT}
usePortal={false}
>
Available
</Tooltip>{" "}
<Tooltip
className={Classes.TOOLTIP_INDICATOR}
content="Color.SUCCESS"
intent='success'
position={Position.TOP}
usePortal={false}
>
in the full
</Tooltip>{" "}
<Tooltip
className={Classes.TOOLTIP_INDICATOR}
content="Color.WARNING"
intent='warning'
position={Position.BOTTOM}
usePortal={false}
>
range of
</Tooltip>{" "}
<Tooltip
className={Classes.TOOLTIP_INDICATOR}
content="Color.DANGER"
intent='danger'
position={Position.RIGHT}
usePortal={false}
>
visual intents!
</Tooltip>
</div>
<br />
<Popover
content={<H1>Popover!</H1>}
position={Position.RIGHT}
popoverClassName={Classes.POPOVER_CONTENT_SIZING}
>
<Tooltip
content={<span>This button also has a popover!</span>}
position={Position.RIGHT}
usePortal={false}
>
<Button intent='success' text="Hover and click me" />
</Tooltip>
</Popover>
</Example>
)
}


toggleControlledTooltip() {
this.setState({ isOpen: !this.state.isOpen });
}

29. Tree 树形控件

constructor(props) {
super(props);
this.treeData = [
{ key: '0-0', title: 'parent 1', children:
[
{ key: '0-0-0', title: 'parent 1-1', children:
[
{ key: '0-0-0-0', title: 'parent 1-1-0' },
],
},
{ key: '0-0-1', title: 'parent 1-2', children:
[
{ key: '0-0-1-0', title: 'parent 1-2-0', disableCheckbox: true },
{ key: '0-0-1-1', title: 'parent 1-2-1' },
],
},
],
},
];
const keys = ['0-0-0-0'];
this.state = {
defaultExpandedKeys: keys,
defaultSelectedKeys: keys,
defaultCheckedKeys: keys,
}
}


render() {
return (
<Example {...this.props}>
<Tree
showLine
className="myCls"
checkable
defaultExpandAll
defaultExpandedKeys={this.state.defaultExpandedKeys}
defaultSelectedKeys={this.state.defaultSelectedKeys}
defaultCheckedKeys={this.state.defaultCheckedKeys}
>
<TreeNode title="parent 1" key="0-0">
<TreeNode title="parent 1-0" key="0-0-0">
<TreeNode title="leaf" key="0-0-0-0" style={{ background: 'rgba(255, 0, 0, 0.1)' }} />
<TreeNode title="leaf" key="0-0-0-1" />
</TreeNode>
<TreeNode title="parent 1-1" key="0-0-1">
<TreeNode title="parent 1-1-0" key="0-0-1-0" />
<TreeNode title="parent 1-1-1" key="0-0-1-1" />
</TreeNode>
<TreeNode title="parent 1-2" key="0-0-2" disabled>
<TreeNode title="parent 1-2-0" key="0-0-2-0" disabled />
<TreeNode title="parent 1-2-1" key="0-0-2-1" />
</TreeNode>
</TreeNode>
</Tree>
</Example>
)
}

三、构建属于自己的React组件库

  • 单据 + 档案
  • 表单 + 查询列表

1. EasyBizForm.tsx

/src/main/components/easy-bizform/EasyBizForm.tsx

export class EasyBizForm extends BaseComponent<IEasyBizFormProps> implements ITabLifecycle {


get presenter(): EasyBizFormPresenter<any> {
return this.props.presenter as any;
}


....

render() {
return (
<BizFormPage
{...this.presenter.getBizFormOptions()}
>
{...this.presenter.renderTemplateSlot()}
</BizFormPage>
);
}


....
}

2. BizFormPage.tsx

/src/solutions/biz-form/page/BizFormPage.tsx

export class BizFormPage extends React.Component<IBizFormPageOptions & { tabApi?: ITabAPI }> {
render() {
const sections: any[] = [
<Section slot="header" key="header">
<BizFormHeader />
</Section>,
<Section slot="footer" key="footer">
<BizFormFooter />
</Section>,
<Section slot="fixed" key="fiexed">
<BizFormFixedContent />
</Section>,
];


....

const { entityName } = options;
const entity = metadata.getEntity(entityName);


if (!entity) {
return <BizForm {...options}>{sections}</BizForm>;
}


if (entity.isDocument) {
return <Archive sections={sections} {...options} />;
}


if (entity.isVoucherBill) {
return <Bill sections={sections} {...options} />;
}


return <BizForm {...options}>{sections}</BizForm>;


....
}
}

/src/solutions/biz-form/archive/form/Archive.tsx

interface IArchiveOptions extends IBizFormPageOptions {
sections: any[];
}


export function Archive(props: IArchiveOptions) {
const { sections, displayOptions = {}, menuOptions, ...otherProps } = props;


displayOptions.className = cx(displayOptions.className, 'bf-form-entity-archive');


const options = {
...otherProps,
displayOptions,
menuOptions,
};


return <BizForm {...options}>{sections}</BizForm>;
}

/src/solutions/biz-form/bill/form/Bill.tsx

interface IBillOptions extends IBizFormPageOptions {
sections: any[];
}


export function Bill(props: IBillOptions) {
const { sections, displayOptions = {}, ...otherProps } = props;


displayOptions.className = cx(displayOptions.className, 'bf-form-entity-bill');


const options = {
...otherProps,
displayOptions,
};


return <BizForm {...options}>{sections}</BizForm>;
}

3. BizForm.tsx

src/solutions/biz-form/core/components/BizForm.tsx

export class BizForm extends React.Component<IBizFormOptions & ContextProviderProps> {


...

render() {
if (this.props.userScreenLoading) {
if (this.loadingStatus !== LoadingStatus.Complete) {
return null;
}
}


let contentElement;


if (this.props.userScreenLoading) {
contentElement = this.renderContent();
} else {
contentElement = (
<LoadingContainer
loadingStatus={this.loadingStatus}
className={this.loadingStyle.loadingClassName}
style={this.loadingStyle.loadingContainerStyle}
>
{() => this.renderContent()}
</LoadingContainer>
);
}


return <PresenterProvider value={this.presenter}>{contentElement}</PresenterProvider>;
}


private renderContent = () => {
if (this.props.onRenderContent) {
return this.props.onRenderContent({
presenter: this.presenter,
form: this.form,
renderFormContent: this.renderFormContent
});
}
return this.renderFormContent();
};




...


private renderFormContent = () => {
return (
<Observer>
{() => {
...


const entityName = this.presenter.options.entityName;


const className = cx(
displayOptions.className,
`bf-form-layout-${mode}`,
`bf-form-entity-${entityName}`,
);


return (
<Page layout="BizFormLayout" className={className}>
{/* 内容区,根据 template 进行布局并显示主体内容 */}
<Section slot="content">
<Observer render={this.renderFormBody} />
</Section>
</Page>
);
}}
</Observer>
);
}


...


private renderFormBody = () => {
const authController = this.presenter.getBean(BeanNames.AuthController);
if (!authController.hasAuthority) {
return this.renderAuthorizedFailed(this.presenter);
}
return <BizFormBody key={`${this.randomKey}`} />;
};
}

4. BizFormBody.tsx

src/solutions/biz-form/core/components/BizFormBody.tsx

export class BizFormBody extends React.Component<{presenter?: BizFormPresenter;tabApi?: ITabAPI;}> {


...


render() {
return (
<Observer>
{() => (
<LoadingContainer loadingStatus={this.loadingStatus}>
{() => <QwertRegion circluar={false}>{this.renderZones()}</QwertRegion>}
</LoadingContainer>
)}
</Observer>
);
}


renderZones() {
const zones = this.presenter.template.zones;


const zonesList = zones.map((zone, index) => {
if (zone.type === BizFormZoneType.form) {
return <FormZone key={index} template={zone as any} index={index} />;
} else if (zone.type === BizFormZoneType.grid) {
return <GridZone key={index} template={zone as any} />;
}
});


const gridZones = zones.filter(zone => zone.type === BizFormZoneType.grid);
if (!gridZones.length && this.needRenderEmptyGrid) {
const template: any = {
// TODO 暂时这样 后面调整
layout: FormZoneLayoutType.flow,
type: BizFormZoneType.grid
};
zonesList.push(<GridZone key={zones.length} template={template} />);
}


// 编辑态是否显示表尾区
const footerZone = zones.find(zone => zone.type === BizFormZoneType.footer);
if (footerZone) {
const displayFooterWhenEdit = !footerZone.hiddenInEdit;
if (displayFooterWhenEdit) {
zonesList.push(<FormZone key={zones.length} template={footerZone as any} />);
}
}


return zonesList;
}


...


}

5. FormZone.tsx

src/solutions/biz-form/core/components/form-zone/FormZone.tsx

export class FormZone extends React.Component<FormZoneProps & { presenter?: BizFormPresenter }> {
render() {

...

const content = template.layout === FormZoneLayoutType.flow ?
<FormZoneInFlow key="flow" {...this.props} sections={normalSections} /> :
<FormZoneInTab key="tab" {...this.props} sections={normalSections} />;


return (
<QwertElement>
{() => {
return <>
...
{content}
</>
}}
</QwertElement>
);
}
}

5.1 FormZoneInFlow.tsx

/src/solutions/biz-form/core/components/form-zone/FormZoneInFlow.tsx

export class FormZoneInFlow extends React.Component<FormZoneProps> {
render() {
return (
<QwertRegion>
<Observer>
{() => (
<div className="bf-zone bf-form-zone">
{(this.props.sections || [])
.filter((section, index) => {
if (!section.isCustomized) {
const { id } = section;
if(id){
return this.masterController.isSectionVisibleById(id);
}
return this.masterController.isSectionVisible(index);
}
return true;
})
.map((section, index) => {
if (section.isCustomized) {
return this.renderCustomizedSection(section as ICustomizedSection, index);
}
return this.renderSection(section as IFormZoneSection, index);
})}
</div>
)}
</Observer>
</QwertRegion>
);
}


renderSection(section: IFormZoneSection, index: number) {
const cornerMark = {
name: section.cornerMark,
};


return (
<div key={`${index}`} className="bf-form-section">
<Section title={section.title} icon={'iconbiaotiqianzhui'} cornerMark={cornerMark}>
<MasterForm template={section} />
</Section>
</div>
);
}


renderCustomizedSection(section: ICustomizedSection, index: number) {
return (
<div key={`${index}`} className={cx('bf-form-section', section.warpperClassName)} >
<Section title={section.title} icon="iconbiaotiqianzhui" className={section.className} rightElement={section.rightElement}>
{section.render({
section,
index,
type: FormZoneLayoutType.flow,
presenter: this.props.presenter,
})}
</Section>
</div>
);
}
}

5.2 Section.tsx

/src/components/section/Section.tsx

export class Section extends React.Component<ISectionProps> {
render() {
const {
titleClass,
contentClass,
className,
icon,
title,
children,
rightClass,
rightElement,
isEmpty = false,
dragPreviewRef,
cornerMark = {},
} = this.props;
const { name: cornerMarkName, position = 'top-right' } = cornerMark;
return (
<div className={cx('atx-section', styles.section, className)} onClick={this.props.onClick}>
{cornerMarkName && (
<div className={cx(styles['corner-mark'], styles[position])}>
<span>{cornerMarkName}</span>
</div>
)}
{title && (
<div ref={dragPreviewRef} className={cx('atx-section-header', styles.header, titleClass)}>
{icon && <SvgIcon className={styles.icon} use={`#${icon}`} />}
<div className={cx('atx-section-header-title', styles.title)}>{title}</div>
<div className={cx('atx-section-header-right', styles.right, rightClass)}>
{rightElement}
</div>
</div>
)}
<Observer>
{() =>
!isEmpty && (
<div className={cx('atx-section-content', styles.content, contentClass)}>
{children}
</div>
)
}
</Observer>
</div>
);
}
}

5.3 MasterForm.tsx

/src/solutions/biz-form/core/components/form-zone/MasterForm.tsx

export class MasterForm extends React.Component<MasterFormProps & {presenter?: BizFormPresenter; qwertElementParams?: IQwertElementParams }> {
...

render() {
const { template, presenter } = this.props;


...


return (
<QwertRegion>
<FormLayout columnSize={template.columnSize} disableError={this.disableError}>
<>
{template.fields
.filter((field) => field.visible)
.map((fieldTemplate, index) => {
// 读取字段的 模板信息
const { fieldName, isSlotField } = fieldTemplate;


if (isSlotField) {
return this.renderSlotField(fieldTemplate);
}


const fieldModel = presenter.model.master.fieldIndex[fieldName];
const isExtend = presenter.model.master.isExtendField(fieldName);
const extendModel =
presenter.model.master.extendFieldIndex[fieldName];
const props = {
path: fieldName,
form: formController.form,
template: fieldTemplate,
model: fieldModel,
index: index,
isExtend: isExtend,
extendModel: extendModel
};
return masterRenderController.renderField(props);
})}
</>
</FormLayout>
</QwertRegion>
);
}


renderSlotField(template: IMasterField) {
const formController = this.props.presenter.getBean(BeanNames.FormController);
const { displayOptions = {} } = this.props.presenter.options;
if (displayOptions.masterSlot && displayOptions.masterSlot[template.fieldName]) {
return displayOptions.masterSlot[template.fieldName]({
form: formController.form,
path: template.fieldName
});
}
return <span>`这个插槽还没有被使用: ${template.fieldName}`</span>;
}
}

5.4 FormLayout.tsx

/packages/kaleido/packages/uikit/athena-ui/src/components/FormLayout/FormLayout.tsx

export enum Alignment {
Left = 'Left',
Right = 'Right',
Center = 'Center',
}


export interface FormLayoutProps {
className?: string;
disableError?: boolean;
columnSize?: number;
// labelWidth?: number;
labelAlignment?: Alignment;
horizontalSpacing?: number;
verticalSpacing?: number;
layoutSize?: 'small' | 'normal' | 'large';
children: Array<JSX.Element> | JSX.Element;
}


/**
* 前端组件
*/
export enum ComponentType {
CheckBox = 'CheckBox',
Text = 'Text',
Number = 'Number',
DatePicker = 'DatePicker',
Refer = 'Refer',
Enum = 'Enum',
List = 'List',
MultiLineText = 'MultiLineText',
TimeInput = 'TimeInput',
Unknown = 'Unknown',
}


export class FormLayout extends React.PureComponent<FormLayoutProps> {
static defaultProps = {
columnSize: 1,
layoutSize: 'normal',
horizontalSpacing: 4,
verticalSpacing: 4,
};


private getClassNames() {
const {
columnSize,
className,
disableError,
layoutSize,
horizontalSpacing,
verticalSpacing,
} = this.props;


const runtimeStyle = css`
.formElement{
width: ${100 / columnSize}%;
padding-right: ${horizontalSpacing}px;


&.double{
width: ${(100 / columnSize) * 2}%;
}


&.triple{
width: ${(100 / columnSize) * 3}%;
}


&.quatary{
width: ${(100 / columnSize) * 4}%;
}
&.fivetimes{
width: ${(100 / columnSize) * 5}%;
}
&.sixtimes{
width: ${(100 / columnSize) * 6}%;
}
/* margin-bottom: ${verticalSpacing}px; */
}
`;


return cx(
styles.root,
className,
{
[`at-FormLayout--disable-error`]: disableError,
[`${styles.root}-${layoutSize}`]: layoutSize,
},
runtimeStyle,
);
}


render() {
...

return (
<div className={this.getClassNames()}>
{childrens}
</div>
);
}
}

5.4.1 FormElement

export interface FormElementProps {
className?: string;
disableError?: boolean;
label?: string;
colspan?: number;
isRequired?: boolean;
suppressShowLabel?: boolean;
errorMessage?: string;
description?: string;
suffix?: any;
children?: any | ((options) => any);
showTooltip?: boolean; // 是否是显示 tooltip
labelRenderer?: () => JSX.Element;
contentRenderer?: () => JSX.Element;
componentType?: ComponentType;
}


export class FormElement extends React.Component<FormElementProps> {
private labelRenderer = () => {
if (this.props.labelRenderer) {
return this.props.labelRenderer();
}


const { label, isRequired, disableError, errorMessage } = this.props;


return (
<FormElementLabel
label={label}
isRequired={isRequired}
disableError={disableError}
errorMessage={errorMessage}
/>
);
};


private contentRenderer = () => {
if (this.props.contentRenderer) {
return this.props.contentRenderer();
}


const { disableError, errorMessage, children, suffix, description, ...otherProps } = this.props;


return (
<FormElementContent
disableError={disableError}
errorMessage={errorMessage}
description={description}
children={children}
suffix={suffix}
{...otherProps}
/>
);
};


render() {
const { disableError, suppressShowLabel, colspan = 1, className, componentType, errorMessage } = this.props;


return (
<FormElementSkeleton
classNames={cx(className, { 'formElement--disableError': disableError})}
colspan={colspan}
suppressShowLabel={suppressShowLabel}
labelRenderer={() => this.labelRenderer()}
contentRenderer={() => this.contentRenderer()}
componentType={componentType}
/>
);
}
}

5.4.2 FormElementSkeleton

/**
* 快捷 FormElement 布局组件
*/
export interface FormElementSkeletonProps {
classNames?: string;
colspan?: number;
suppressShowLabel?: boolean;
labelRenderer?: () => JSX.Element;
contentRenderer: () => JSX.Element;
componentType?: ComponentType;
}


export class FormElementSkeleton extends React.PureComponent<FormElementSkeletonProps> {
private getClass = () => {
const { componentType } = this.props;
switch (componentType) {
case ComponentType.MultiLineText:
return 'multiLineTextElement';
}
return '';
};


render() {
const {
colspan = 1,
classNames = '',
suppressShowLabel,
labelRenderer,
contentRenderer,
} = this.props;


const colspanArray = ['', 'double', 'triple', 'quatary', 'fivetimes', 'sixtimes'];


const contentStyle = suppressShowLabel ? { paddingLeft: 0 } : null;
// 多行文本高度自适应


return (
<div className={cx(classNames, 'formElement ' + colspanArray[colspan - 1], this.getClass())}>
{!suppressShowLabel ? <div className="formLabel">{labelRenderer()}</div> : null}
<div style={contentStyle} className="formContent">
{contentRenderer()}
</div>
</div>
);
}
}

5.4.3 FormElementLabel

export interface FormElementLabelProps {
disableError?: boolean;
label: string;
isRequired?: boolean;
errorMessage?: string;
}


/**
* 快捷 FormElementLabel 组件
*/
export class FormElementLabel extends React.PureComponent<FormElementLabelProps> {
render() {
const { label, isRequired, disableError, errorMessage } = this.props;


return (
<React.Fragment>
{disableError && errorMessage && <ErrorTip error={errorMessage} />}
{isRequired && <Text className="required">*</Text>}
<Text ellipsize={true}>{label}</Text>
</React.Fragment>
);
}
}

5.4.5 FormElementContent

export interface FormElementContentProps {
disableError?: boolean;
errorMessage?: string;
description?: string;
suffix?: any;
children: any | ((options) => any);
showTooltip?: boolean; // 是否是显示 tooltip
}


/**
* 快捷 FormElementContent 组件
*/
export class FormElementContent extends React.PureComponent<FormElementContentProps> {
private formInputRef: any;


render() {
const { disableError, errorMessage, suffix, description } = this.props;
const children = this.renderChildren();


return (
<Observer>
{() => (
<React.Fragment>
<div className="formInput" ref={this.handleRef}>
{children}
{suffix}
</div>
{!disableError && errorMessage && <div className="formError" title={errorMessage}>{errorMessage}</div>}
{!errorMessage && description && <div className="formDescription">{description}</div>}
</React.Fragment>
)}
</Observer>
);
}


renderChildren() {
const { children, showTooltip = false } = this.props;


if (showTooltip) {
return (
<TooltipElement getMaxWidth={this.getMaxWidth}>
{children}
</TooltipElement>
);
}


return children;
}


handleRef = (ref) => {
if (ref) {
this.formInputRef = ref;
}
};


getMaxWidth = () => {
let maxWidth = 0;
if (this.formInputRef) {
maxWidth = this.formInputRef.clientWidth;
}
return maxWidth;
};
}

6. GridZone.tsx

export class GridZone extends React.Component<GridZoneProps & { presenter?: BizFormPresenter }> {
render() {

...

return (
<QwertElement>
{() => {
if (template.layout === FormZoneLayoutType.flow) {
return <GridZoneInFlow {...this.props} />;
} else {
return <GridZoneInTab {...this.props} />;
}
}}
</QwertElement>
);
}
}

6.1 GridZoneInFlow.tsx

export interface GridZoneProps {
template: IGridZone;
}


// 在 GridZone 中,一个 Grid 的定义
export interface IGridZoneSection {
// 子表字段
fieldName: string;
// 标题
title?: string;
// 图标
icon?: string;
// 表体字段
fields: Array<IDetailField>;
// 自定义渲染
customerRender?: (presenter: any) => JSX.Element;
// 自定义类名
className?: string;
// 右侧自定义渲染
rightElement?: (() => JSX.Element) | JSX.Element;
}


export class GridZoneInFlow extends React.Component<GridZoneProps> {
render() {
return (
<div className="bf-zone bg-grid-zone-wrapper">
<div className="bf-zone bf-grid-zone">
{(this.props.sections || []).map((section, index) => {
return this.renderGridSection(section as IGridZoneSection, index);
})}
</div>
</div>
);
}


renderGridSection(section: IGridZoneSection, index: number) {
const contentView = (
<ul className='atx-grid'>
{(section.fields || []).map((field, index) => {
const styleRules = { width: field.width }
return <li style={styleRules}>{field.title}</li>
})}
</ul>
);
return (
<Section title={section.title}>
{contentView}
</Section>
);
}
}

6.2 Section.tsx

/src/components/section/Section.tsx 同5.2 

四、手写一个自己的Tree组件

1. 初始化项目

1.1 创建项目

mkdir customize_components  
cd customize_components
cnpm init -y
touch .gitignore

1.2 安装依赖

​@types​​开头的包都是typeScript的声明文件,可以进入node_modules/@types/XX/index.d.ts进行查看

npm i react @types/react react-dom @types/react-dom -S
npm i webpack webpack-cli webpack-dev-server -D
npm i typescript ts-loader source-map-loader style-loader css-loader less-loader less file-loader url-loader html-webpack-plugin -D
npm i axios express qs @types/qs -D

模块名

使用方式

react

React is a JavaScript library for creating user interfaces.

react-dom

This package serves as the entry point to the DOM and server renderers for React. It is intended to be paired with the generic React package, which is shipped as react to npm.

webpack

webpack is a module bundler. Its main purpose is to bundle JavaScript files for usage in a browser, yet it is also capable of transforming, bundling, or packaging just about any resource or asset.

webpack-cli

The official CLI of webpack

webpack-dev-server

Use webpack with a development server that provides live reloading. This should be used for development only.

typescript

TypeScript is a language for application-scale JavaScript.

ts-loader

This is the TypeScript loader for webpack.

source-map-loader

Extracts source maps from existing source files (from their sourceMappingURL).

style-loader

Inject CSS into the DOM.

css-loader

The css-loader interprets @import and url() like import/require() and will resolve them.

less-loader

A Less loader for webpack. Compiles Less to CSS.

less

This is the JavaScript, official, stable version of Less.

file-loader

The file-loader resolves import/require() on a file into a url and emits the file into the output directory.

url-loader

A loader for webpack which transforms files into base64 URIs.

html-webpack-plugin

Plugin that simplifies creation of HTML files to serve your bundles

1.3 支持typescript

首先需要生成一个tsconfig.json文件来告诉ts-loader如何编译代码TypeScript代码

tsc --init
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"jsx": "react",
"outDir": "./dist",
"rootDir": "./src",
"noImplicitAny":true,
"esModuleInterop": true
},
"include": [
"./src/**/*",
"./typings/**/*"
]
}

参数

含义

target

转换成es5

module

代码规范

jsx

react模式会生成React.createElement,在使用前不需要再进行转换操作了,输出文件的扩展名为.js

outDir

指定输出目录

rootDir

指定根目录

sourceMap

把 ts 文件编译成 js 文件的时候,同时生成对应的sourceMap文件

noImplicitAny

如果为true的话,TypeScript 编译器无法推断出类型时,它仍然会生成 JS文件,但是它也会报告一个错误

esModuleInterop

是否转译common.js模块

include

需要编译的目录

1.4 webpack.config.js

const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
module.exports = {
mode: 'development',
entry: "./src/index.tsx",
output: {
path: path.join(__dirname, 'dist')
},
devtool: "source-map",
devServer: {
hot: true,
contentBase: path.join(__dirname, 'dist'),
historyApiFallback: {
index: './index.html'
}
},
resolve: {
extensions: [".ts", ".tsx", ".js", ".json"]
},


module: {
rules: [{
test: /\.tsx?$/,
loader: "ts-loader"
},
{
enforce: "pre",
test: /\.tsx$/,
loader: "source-map-loader"
},
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader']
},
{
test: /\.(jpg|png|gif|svg)$/,
loader: "url-loader"
}
]
},


plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
}),
new webpack.HotModuleReplacementPlugin()
],
};

1.5 package.json

"scripts": {
"build": "webpack",
"dev": "webpack-dev-server",
}

2.创建和渲染树形菜单

2.1 src\index.html

<body>
<div ></div>
</body>

2.2 src\index.tsx

import React from 'react';
import ReactDOM from 'react-dom';
import Tree from './components/tree';
import data from './data';


ReactDOM.render(, document.getElementById('root'));

2.3 src\typings.tsx

export interface TreeData {
name: string;
key: string;
type: string;
collapsed: boolean;
children?: Array<TreeData>;
parent?: TreeData;
checked?: boolean;
loading?: boolean;
}

2.4 src\data.tsx

import { TreeData } from './typings';


const data: TreeData = {
name: '父亲',
key: '1',
type: 'folder',
collapsed: false,
children: [
{
name: '儿子1',
key: '1-1',
type: 'folder',
collapsed: false,
children: [
{
name: '孙子1',
key: '1-1-1',
type: 'folder',
collapsed: false,
children: [
{
name: '重孙1',
key: '1-1-1-1',
type: 'file',
collapsed: false,
children: []
}
]
}
]
},
{
name: '儿子2',
key: '1-2',
type: 'folder',
collapsed: true
}
]
}
export default data;

2.5 src\components\tree.tsx

import React from 'react';
import './index.less';
import { TreeData } from '../typings';
import TreeNode from './tree-node';
import { getChildren } from '../api';


interface Props {
data: TreeData;
}
interface KeyToNodeMap {
[key: string]: TreeData
}
interface State {
data: TreeData;
fromNode?: TreeData;
}
class Tree extends React.Component<Props, State> {
data: TreeData;
keyToNodeMap: KeyToNodeMap;
constructor(props: Props) {
super(props);
this.state = { data: this.props.data };
this.data = props.data;
this.buildKeyMap();
}
buildKeyMap = () => {
let data = this.data;
this.keyToNodeMap = {};
this.keyToNodeMap[data.key] = data;
if (data.children && data.children.length > 0) {
this.walk(data.children, data);
}
this.setState({ data: this.state.data });
}
walk = (children: Array<TreeData>, parent: TreeData): void => {
children.map((item: TreeData) => {
item.parent = parent;
this.keyToNodeMap[item.key] = item;
if (item.children && item.children.length > 0) {
this.walk(item.children, item);
}
});
}
onCollapse = async (key: string) => {
let data = this.keyToNodeMap[key];
if (data) {
let { children } = data;
if (!children) {
data.loading = true;
this.setState({ data: this.state.data });
let result = await getChildren(data);
if (result.code == 0) {
data.children = result.data;
data.collapsed = false;
data.loading = false;
this.buildKeyMap();
} else {
alert('加载失败');
}
} else {
data.collapsed = !data.collapsed;
this.setState({ data: this.state.data });
}
}
}
onCheck = (key: string) => {
let data: TreeData = this.keyToNodeMap[key];
if (data) {
data.checked = !data.checked;
if (data.checked) {
this.checkChildren(data.children, true);
this.checkParentCheckAll(data.parent);
} else {
this.checkChildren(data.children, false);
this.checkParent(data.parent, false);
}
this.setState({ data: this.state.data });
}
}
checkParentCheckAll = (parent: TreeData) => {
while (parent) {
parent.checked = parent.children.every(item => item.checked);
parent = parent.parent;
}
}
checkParent = (parent: TreeData, checked: boolean) => {
while (parent) {
parent.checked = checked;
parent = parent.parent;
}
}
checkChildren = (children: Array<TreeData> = [], checked: boolean) => {
children.forEach((item: TreeData) => {
item.checked = checked;
this.checkChildren(item.children, checked);
});
}
setFromNode = (fromNode: TreeData) => {
this.setState({ ...this.state, fromNode });
}
onMove = (toNode: TreeData) => {
let fromNode = this.state.fromNode;
let fromChildren = fromNode.parent.children, toChildren = toNode.parent.children;
let fromIndex = fromChildren.findIndex((item: TreeData) => item === fromNode);
let toIndex = toChildren.findIndex(item => item === toNode);
fromChildren.splice(fromIndex, 1, toNode);
toChildren.splice(toIndex, 1, fromNode);
this.buildKeyMap();
}
render() {
return (
<div className="tree">
<div className="tree-nodes">
<TreeNode
data={this.props.data}
onCollapse={this.onCollapse}
onCheck={this.onCheck}
setFromNode={this.setFromNode}
onMove={this.onMove}
/>
</div>
</div>
)
}
}
export default Tree;

2.6 src\components\tree-node.tsx

import React from 'react';
import { TreeData } from '../typings';
import file from '../assets/file.png';
import closedFolder from '../assets/closed-folder.png';
import openedFolder from '../assets/opened-folder.png';
import loadingSrc from '../assets/loading.gif';
interface Props {
data: TreeData,
onCollapse: any,
onCheck: any;
setFromNode: any;
onMove: any
}
class TreeNode extends React.Component<Props> {
treeNodeRef: React.RefObject<HTMLDivElement>;
constructor(props: Props) {
super(props);
this.treeNodeRef = React.createRef();
}
componentDidMount() {
this.treeNodeRef.current.addEventListener('dragstart', (event: DragEvent): void => {
this.props.setFromNode(this.props.data);
event.stopPropagation();
}, false);//useCapture=false
this.treeNodeRef.current.addEventListener('dragenter', (event: DragEvent) => {
event.preventDefault();
event.stopPropagation();
}, false);
this.treeNodeRef.current.addEventListener('dragover', (event: DragEvent) => {
event.preventDefault();
event.stopPropagation();
}, false);
this.treeNodeRef.current.addEventListener('drop', (event: DragEvent) => {
event.preventDefault();
this.props.onMove(this.props.data);
event.stopPropagation();
}, false);
}
render() {
let { data: { name, children, collapsed = false, key, checked = false, loading } } = this.props;
let caret, icon;
if (children) {
if (children.length > 0) {
caret = (
<span className={`collapse ${collapsed ? 'caret-right' : 'caret-down'}`}
onClick={() => this.props.onCollapse(key)}
/>
)
icon = collapsed ? closedFolder : openedFolder;
} else {
caret = null;
icon = file;
}
} else {
caret = (
loading ? <img className="collapse" src={loadingSrc} style={{ width: 14, top: '50%', marginTop: -7 }} /> : <span className={`collapse caret-right`}
onClick={() => this.props.onCollapse(key)}
/>
)
icon = closedFolder;
}
return (
<div className="tree-node" draggable={true} ref={this.treeNodeRef}>
<div className="inner">
{caret}
<span className="content">
<input type="checkbox" checked={checked} onChange={() => this.props.onCheck(key)} />
<img style={{ width: 20 }} src={icon} />
{name}
</span>
</div>
{
(children && children.length > 0 && !collapsed) && (
<div className="children">
{
children.map((item: TreeData) => (
<TreeNode
onCollapse={this.props.onCollapse}
onCheck={this.props.onCheck}
key={item.key}
setFromNode={this.props.setFromNode}
onMove={this.props.onMove}
data={item} />
))
}
</div>
)
}
</div>
)
}
}
export default TreeNode;

2.7 src\components\index.scss

.tree {
width: 80%;
overflow-x: hidden;
overflow-y: auto;
background-color: #fff;


.tree-nodes {
position: relative;
overflow: hidden;


.tree-node {
.inner {
color: #000;
font-size: 16px;
position: relative;
cursor: pointer;
padding-left: 10px;


.collapse {
position: absolute;
left: 0;
cursor: pointer;
}


.caret-right:before {
content: '\25B8';
}


.caret-down:before {
content: '\25BE';
}


.content {
display: inline-block;
width: 100%;
padding: 4px 5px;
}
}


.children {
padding-left: 20px;
}
}


}
}

2.8 src\typings\images.d.ts

declare module '*.svg';
declare module '*.png';
declare module '*.jpg';
declare module '*.jpeg';
declare module '*.gif';
declare module '*.bmp';
declare module '*.tiff';

2.9 src\api.tsx

import axios from 'axios';
import qs from 'qs';
axios.defaults.baseURL = 'http://localhost:3000';
export const getChildren = (data: any) => {
return axios.get(`/getChildren?${qs.stringify({ key: data.key, name: data.name })}`).then(res => res.data).catch(function (error) {
console.log(error);
});
}

2.10 api.js

let express = require('express');
let app = express();
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
if (req.method === 'OPTIONS') {
return res.sendStatus(200);
}
next();
});
app.get('/getChildren', (req, res) => {
let data = req.query;
setTimeout(function () {
res.json({
code: 0,
data: [
{
name: data.name',
key: `${data.key}-1`,
type: 'folder',
collapsed: true
},
{
name: data.name',
key: `${data.key}-2`,
type: 'folder',
collapsed: true
}
]
});
}, 2000)


});
app.listen(3000, () => {
console.log(`接口服务器在${3000}上启动`);
});