kafka-manager 2.0

This commit is contained in:
zengqiao
2020-09-28 15:46:34 +08:00
parent 28d985aaf1
commit c6e4b60424
1253 changed files with 82183 additions and 37179 deletions

View File

@@ -0,0 +1,139 @@
import message from 'antd/es/message';
import 'antd/es/message/style';
import Input from 'antd/es/input';
import 'antd/es/input/style';
import InputNumber from 'antd/es/input-number';
import 'antd/es/input-number/style';
import Table from 'antd/es/table';
import 'antd/es/table/style';
import Tabs from 'antd/es/tabs';
import 'antd/es/tabs/style';
import Select from 'antd/es/select';
import 'antd/es/select/style';
import DatePicker from 'antd/es/date-picker';
import 'antd/es/date-picker/style';
import Button from 'antd/es/button';
import 'antd/es/button/style';
import Modal from 'antd/es/modal';
import 'antd/es/modal/style';
import Form from 'antd/es/form';
import 'antd/es/form/style';
import Row from 'antd/es/row';
import 'antd/es/row/style';
import Col from 'antd/es/col';
import 'antd/es/col/style';
import Switch from 'antd/es/switch';
import 'antd/es/switch/style';
import Alert from 'antd/es/alert';
import 'antd/es/alert/style';
import { PaginationConfig, ColumnProps } from 'antd/es/table/interface';
import notification from 'antd/es/notification';
import 'antd/es/notification/style';
import Tooltip from 'antd/es/tooltip';
import 'antd/es/tooltip/style';
import Radio from 'antd/es/radio';
import 'antd/es/radio';
import { RadioChangeEvent } from 'antd/es/radio';
import Collapse from 'antd/es/collapse';
import 'antd/es/collapse/style';
import Icon from 'antd/es/icon';
import 'antd/es/icon/style';
import Dropdown from 'antd/es/dropdown';
import 'antd/es/dropdown/style';
import Spin from 'antd/es/spin';
import 'antd/es/spin/style';
import Drawer from 'antd/es/drawer';
import 'antd/es/drawer/style';
import Checkbox from 'antd/es/checkbox';
import 'antd/es/checkbox/style';
import Affix from 'antd/es/affix';
import 'antd/es/affix/style';
import Popconfirm from 'antd/es/popconfirm';
import 'antd/es/popconfirm/style';
import PageHeader from 'antd/es/page-header';
import 'antd/es/page-header/style';
import Descriptions from 'antd/es/descriptions';
import 'antd/es/descriptions/style';
import Steps from 'antd/es/steps';
import 'antd/es/steps/style';
import Divider from 'antd/es/divider';
import 'antd/es/divider/style';
import Upload from 'antd/es/upload';
import 'antd/es/upload/style';
import TimePicker from 'antd/es/time-picker';
import 'antd/es/time-picker/style';
import Badge from 'antd/es/badge';
import 'antd/es/badge/style';
import { RangePickerValue } from 'antd/es/date-picker/interface';
export {
PaginationConfig,
notification,
ColumnProps,
DatePicker,
message,
Tooltip,
Button,
Select,
Switch,
Modal,
Input,
Table,
Radio,
Alert,
Tabs,
Form,
Row,
Col,
RadioChangeEvent,
InputNumber,
Collapse,
Icon,
Dropdown,
Spin,
Drawer,
Checkbox,
Affix,
Popconfirm,
PageHeader,
Descriptions,
Steps,
Divider,
Upload,
TimePicker,
RangePickerValue,
Badge,
};

View File

@@ -0,0 +1,90 @@
import * as React from 'react';
import { Spin, notification } from 'component/antd';
import echarts, { EChartOption } from 'echarts/lib/echarts';
// 引入柱状图
import 'echarts/lib/chart/bar';
// 引入提示框和标题组件
import 'echarts/lib/component/tooltip';
import 'echarts/lib/component/title';
import 'echarts/lib/component/legend';
interface IChartProps {
getChartData: any;
customerNode?: React.ReactNode;
}
export class BarChartComponet extends React.Component<IChartProps> {
public id: HTMLDivElement = null;
public chart: echarts.ECharts;
public state = {
loading: false,
noData: false,
};
public componentDidMount() {
this.chart = echarts.init(this.id);
this.getChartData();
window.addEventListener('resize', this.resize);
}
public componentWillUnmount() {
window.removeEventListener('resize', this.resize);
}
public resize = () => {
this.chart.resize();
}
public isHasData = (data: EChartOption) => {
const noData = !(data.series && data.series.length);
this.setState({ noData });
return !noData;
}
public getChartData = () => {
const { getChartData } = this.props;
if (!getChartData) {
return notification.error({ message: '图表信息有误' });
}
this.setState({ loading: true });
const chartOptions = getChartData();
if ((typeof chartOptions.then) === 'function') {
return chartOptions.then((data: EChartOption) => {
this.setState({ loading: false });
if (this.isHasData(data)) {
this.changeChartOptions(data);
}
});
}
if (this.isHasData(chartOptions)) {
this.changeChartOptions(chartOptions);
this.setState({ loading: false });
}
}
public changeChartOptions(options: any) {
this.chart.setOption(options, true);
}
public handleRefreshChart() {
this.getChartData();
}
public render() {
return (
<>
<Spin spinning={this.state.loading} className="chart-content">
{this.state.noData ? <div className="nothing-style"></div> : null}
<div className={this.state.noData ? 'chart-no-data' : 'chart'} ref={(id) => this.id = id} />
</Spin>
</>
);
}
}

View File

@@ -0,0 +1,110 @@
import * as React from 'react';
import { DatePicker, notification, Spin } from 'component/antd';
import moment, { Moment } from 'moment';
import { timeStampStr } from 'constants/strategy';
import { disabledDate } from 'lib/utils';
import echarts from 'echarts';
// 引入柱状图和折线图
import 'echarts/lib/chart/bar';
import 'echarts/lib/chart/line';
// 引入提示框和标题组件
import 'echarts/lib/component/tooltip';
import 'echarts/lib/component/title';
import 'echarts/lib/component/legend';
import './index.less';
const { RangePicker } = DatePicker;
interface IChartProps {
getChartData: (startTime: moment.Moment, endTime: moment.Moment) => any;
customerNode?: React.ReactNode;
}
export class ChartWithDatePicker extends React.Component<IChartProps> {
public state = {
startTime: moment().subtract(1, 'hour'),
endTime: moment(),
loading: false,
noData: false,
};
public id: HTMLDivElement = null;
public chart: echarts.ECharts;
public getData = () => {
const { startTime, endTime } = this.state;
const { getChartData } = this.props;
this.setState({ loading: true });
getChartData(startTime, endTime).then((data: any) => {
this.setState({ loading: false });
this.changeChartOptions(data);
});
}
public componentDidMount() {
this.chart = echarts.init(this.id);
this.getData();
window.addEventListener('resize', this.resize);
}
public componentWillUnmount() {
window.removeEventListener('resize', this.resize);
}
public resize = () => {
this.chart.resize();
}
public changeChartOptions(options: any) {
const noData = options.series.length ? false : true;
this.setState({ noData });
this.chart.setOption(options, true);
}
public handleTimeChange = (dates: Moment[]) => {
this.setState({
startTime: dates[0],
endTime: dates[1],
});
const { getChartData } = this.props;
this.setState({ loading: true });
getChartData(dates[0], dates[1]).then((data: any) => {
this.setState({ loading: false });
this.changeChartOptions(data);
});
}
public render() {
const { customerNode } = this.props;
return (
<div className="status-box" style={{minWidth: '930px'}}>
<div className="status-graph">
<div className="k-toolbar">
{customerNode}
</div>
<ul className="k-toolbar">
<li>
<RangePicker
ranges={{
: [moment().startOf('date'), moment()],
: [moment().subtract(1, 'day'), moment()],
: [moment().subtract(7, 'day'), moment()],
}}
disabledDate={disabledDate}
defaultValue={[moment().subtract(1, 'hour'), moment()]}
format={timeStampStr}
onChange={this.handleTimeChange}
/>
</li>
</ul>
</div>
<Spin spinning={this.state.loading} className="chart-content">
{this.state.noData ? <div className="nothing-style"></div> : null}
<div className={this.state.noData ? 'chart-no-data' : 'chart'} ref={(id) => this.id = id} />
</Spin>
</div>
);
}
}

View File

@@ -0,0 +1,60 @@
import * as React from 'react';
import { Spin } from 'component/antd';
import echarts from 'echarts/lib/echarts';
// 引入饼状图
import 'echarts/lib/chart/pie';
// 引入提示框和标题组件
import 'echarts/lib/component/tooltip';
import 'echarts/lib/component/title';
import 'echarts/lib/component/legend';
interface IPieProps {
getChartData: any;
}
export class DoughnutChart extends React.Component<IPieProps> {
public id: HTMLDivElement = null;
public chart: echarts.ECharts;
public state = {
loading: true,
isNoData: false,
};
public getChartData = () => {
const { getChartData } = this.props;
this.setState({ loading: true });
const options = getChartData();
if (!options || !options.series || !options.series.length) {
this.setState({
isNoData: true,
loading: false,
});
return;
}
this.changeChartOptions(options);
}
public changeChartOptions(options: any) {
this.chart.setOption(options, true);
this.setState({ loading: false });
}
public componentDidMount() {
this.chart = echarts.init(this.id);
this.getChartData();
}
public render() {
return (
<>
<Spin spinning={this.state.loading} className="chart-content">
{this.state.isNoData ? <div className="nothing-style"></div> : null}
<div className="doughnut-chart" ref={(id) => this.id = id} />
</Spin>
</>
);
}
}

View File

@@ -0,0 +1,80 @@
.status-box{
float: left;
margin: 0 5px;
width: 100%;
.status-graph {
position: relative;
height: 48px;
width: 100%;
background: rgba(255, 255, 255, 255);
display: flex;
justify-content: space-between;
line-height: 48px;
font-family: PingFangSC-Regular;
color: rgba(0, 0, 0, 0.85);
padding: 0 5px;
margin: -15px 0;
.k-toolbar {
&>span.label {
padding: 10px;
font-size: 12px;
}
li {
float: left;
vertical-align: middle;
line-height: 48px;
margin-right: 20px;
&>span.label {
padding-right: 10px;
}
}
.title-toolbar {
float: right;
right: 30px;
span:first-child {
margin-right: 10px;
}
}
}
}
.graph-none{
display: none;
}
}
.nothing-style {
height: 300px;
line-height: 300px;
text-align: center;
}
.chart {
height: 400px;
padding: 10px 20px;
}
.doughnut-chart {
width: 500px;
height: 350px;
}
.chart-no-data {
height: 0px;
display: none;
}
.ant-spin-nested-loading {
margin: 0 auto;
}
.no-footer {
.ant-modal-confirm-btns {
display: none;
}
}
.no-data-info {
text-align: center;
}

View File

@@ -0,0 +1,4 @@
export * from './bar-chart';
export * from './date-picker-chart';
export * from './doughnut-chart';
export * from './line-chart';

View File

@@ -0,0 +1,55 @@
import React from 'react';
import echarts, { EChartOption } from 'echarts/lib/echarts';
import 'echarts/lib/chart/pie';
import 'echarts/lib/chart/line';
import 'echarts/lib/component/legend';
import 'echarts/lib/component/tooltip';
import 'echarts/lib/component/title';
import 'echarts/lib/component/axis';
import './index.less';
export interface IEchartsProps {
width?: number;
height?: number;
options?: EChartOption;
}
export const hasData = (options: EChartOption) => {
if (options && options.series && options.series.length) return true;
return false;
};
export default class LineChart extends React.Component<IEchartsProps> {
public id = null as HTMLDivElement;
public myChart = null as echarts.ECharts;
public componentDidMount() {
const { options } = this.props;
this.myChart = echarts.init(this.id);
this.myChart.setOption(options);
window.addEventListener('resize', this.resize);
}
public componentWillUnmount() {
window.removeEventListener('resize', this.resize);
}
public componentDidUpdate() {
this.refresh();
}
public refresh = () => {
const { options } = this.props;
this.myChart.setOption(options);
}
public resize = () => {
this.myChart.resize();
}
public render() {
const { height, width } = this.props;
return <div ref={id => this.id = id} style={{width: `${width}px`, height: `${height}px`}} />;
}
}

View File

@@ -0,0 +1,55 @@
import * as React from 'react';
import ClipboardJS from 'clipboard';
import {
message,
} from 'component/antd';
const triggerEvent = (eventName: string, element: Element) => {
let event;
const ele = element || document;
event = document.createEvent('HTMLEvents');
event.initEvent(eventName, true, true);
ele.dispatchEvent(event);
};
export class Clipboard extends React.Component<any> {
public state = {
text: '',
};
private clipboard: any = null;
private dom: Element = null;
public componentDidMount() {
const clipboard = this.clipboard = new ClipboardJS('.___clipboard', {
text(trigger: Element) {
return trigger.getAttribute('data-text');
},
});
clipboard.on('success', (e: any) => {
message.success('复制成功!');
e.clearSelection();
});
clipboard.on('error', (e: any) => {
message.error('复制失败!' + e);
});
}
public componentWillUnmount() {
this.clipboard.destroy();
}
public copy(text: string) {
this.setState({ text });
setTimeout(() => triggerEvent('click', this.dom), 0);
}
public render() {
return (
<div className="___clipboard" data-text={this.state.text} ref={dom => this.dom = dom} />
);
}
}

View File

@@ -0,0 +1,39 @@
.card-wrapper {
margin: 24px 0 32px;
}
.card-title {
font-family: PingFangSC-Medium;
font-size: 14px;
color: #333333;
height: 22px;
line-height: 22px;
margin: 15px 0;
display: flex;
align-items: center;
cursor: pointer;
i {
font-size: 15px;
margin-right: 8px;
}
}
.card-content {
background-color: #FAFAFA;
padding: 16px;
overflow: auto;
.chart-row {
overflow: hidden;
width: 100%;
}
.chart-row:not(:first-child) {
margin-top: 16px;
}
.chart-wrapper {
background-color: #FFFFFF;
width: calc(50% - 8px);
float: left;
padding: 16px;
}
.chart-wrapper:nth-child(2n) {
margin-left: 16px;
}
}

View File

@@ -0,0 +1,48 @@
import React from 'react';
import './index.less';
import { Icon } from 'component/antd';
interface ICardProps {
title: string;
expand?: boolean;
charts?: JSX.Element[];
}
export class ExpandCard extends React.Component<ICardProps> {
public state = {
innerExpand: true,
};
public handleClick = () => {
this.setState({ innerExpand: !this.state.innerExpand });
}
public render() {
let { expand } = this.props;
if (expand === undefined) expand = this.state.innerExpand;
const { charts } = this.props;
return (
<div className="card-wrapper">
{/* <div className="card-title" onClick={this.handleClick}>
<Icon
type={expand ? 'down' : 'up'}
className={expand ? 'dsui-icon-jiantouxiangxia' : 'dsui-icon-jiantouxiangshang'}
/>
{this.props.title}
</div> */}
{expand ?
<div className="card-content">
{(charts || []).map((c, index) => {
if (index % 2 !== 0) return null;
return (
<div className="chart-row" key={index}>
<div className="chart-wrapper">{c}</div>
{(index + 1 < charts.length) ? <div className="chart-wrapper">{charts[index + 1]}</div> : null}
</div>
);
})}
</div> : null}
</div>
);
}
}

View File

@@ -0,0 +1,91 @@
import * as React from 'react';
import { Table } from 'component/antd';
interface IFlow {
key: string;
avr: number;
pre1: number;
pre5: number;
pre15: number;
}
const flowColumns = [{
title: '名称',
dataIndex: 'key',
key: 'name',
sorter: (a: IFlow, b: IFlow) => a.key.charCodeAt(0) - b.key.charCodeAt(0),
render(t: string) {
return t === 'byteRejected' ? 'byteRejected(B/s)' : (t === 'byteIn' || t === 'byteOut' ? `${t}(KB/s)` : t);
},
},
{
title: '平均数',
dataIndex: 'avr',
key: 'partition_num',
sorter: (a: IFlow, b: IFlow) => b.avr - a.avr,
},
{
title: '前1分钟',
dataIndex: 'pre1',
key: 'byte_input',
sorter: (a: IFlow, b: IFlow) => b.pre1 - a.pre1,
},
{
title: '前5分钟',
dataIndex: 'pre5',
key: 'byte_output',
sorter: (a: IFlow, b: IFlow) => b.pre5 - a.pre5,
},
{
title: '前15分钟',
dataIndex: 'pre15',
key: 'message',
sorter: (a: IFlow, b: IFlow) => b.pre15 - a.pre15,
}];
export interface IFlowInfo {
byteIn: number[];
byteOut: number[];
byteRejected: number[];
failedFetchRequest: number[];
failedProduceRequest: number[];
messageIn: number[];
totalFetchRequest: number[];
totalProduceRequest: number[];
[key: string]: number[];
}
export class StatusGraghCom<T extends IFlowInfo> extends React.Component {
public getData(): T {
return null;
}
public getLoading(): boolean {
return null;
}
public render() {
const statusData = this.getData();
const loading = this.getLoading();
if (!statusData) return null;
const data: any[] = [];
Object.keys(statusData).map((key) => {
if (statusData[key]) {
const v = key === 'byteIn' || key === 'byteOut' ? statusData[key].map(i => i && (i / 1024).toFixed(2)) :
statusData[key].map(i => i && i.toFixed(2));
const obj = {
key,
avr: v[0],
pre1: v[1],
pre5: v[2],
pre15: v[3],
};
data.push(obj);
}
});
return (
<Table columns={flowColumns} dataSource={data} pagination={false} loading={loading}/>
);
}
}

View File

@@ -0,0 +1,136 @@
import * as React from 'react';
import debounce from 'lodash.debounce';
import { Select, Tooltip } from 'component/antd';
import { ILabelValue } from 'types/base-type';
import { searchProps } from 'constants/table';
interface IAttars {
mode?: 'multiple' | 'tags' | 'default' | 'combobox';
placeholder?: string;
}
interface ISelectProps {
onChange: (result: string[] | string) => any;
value?: string[] | string;
isDisabled?: boolean;
attrs?: IAttars;
getData: () => any;
refetchData?: boolean; // 有些页面通过store拿数据需要二次更新
}
export class VirtualScrollSelect extends React.Component<ISelectProps> {
public static getDerivedStateFromProps(nextProps: any, prevState: any) {
if (nextProps.refetchData) {
return {
...prevState,
refetchData: true,
};
}
return null;
}
public state = {
optionsData: [] as ILabelValue[],
scrollPage: 0,
keyword: '',
total: 0,
refetchData: false,
};
public componentDidMount() {
this.getData();
}
public getData = async () => {
const { getData } = this.props;
if (!getData) return;
const pageSize = this.state.scrollPage;
let originData = await getData();
if (originData) {
originData = this.state.keyword ?
originData.filter((item: any) => item.label.includes(this.state.keyword)) : originData;
let data = [].concat(originData);
// tslint:disable-next-line:no-bitwise
const total = data.length ? data.length / 30 | 1 : 0;
data = data.splice(pageSize * 30, 30); // 每页展示30条数据
return this.setState({
optionsData: data,
total,
refetchData: false,
});
}
}
public componentDidUpdate(prevProps: any) {
if (this.state.refetchData && !this.state.optionsData.length) {
// this.getData();
}
}
public handleSearch = (e: string) => {
debounce(() => {
this.setState({
keyword: e.trim(),
scrollPage: 0,
}, () => {
this.getData();
});
}, 300)();
}
public handleSelectScroll = (e: any) => {
e.persist();
const { target } = e;
const { scrollPage } = this.state;
debounce(() => {
if (target.scrollTop + target.offsetHeight === target.scrollHeight) {
const nextScrollPage = scrollPage + 1;
if (this.state.total <= nextScrollPage) { // 已全部拉取
return;
}
this.setState({
scrollPage: nextScrollPage,
}, () => {
this.getData();
});
}
if (target.scrollTop === 0 && scrollPage !== 0) { // 往上滚且不是第一页
const nextScrollPage = scrollPage - 1;
this.setState({
scrollPage: nextScrollPage,
}, () => {
this.getData();
});
}
}, 200)();
}
public render() {
// tslint:disable-next-line:prefer-const
let { value, isDisabled, attrs } = this.props;
if (attrs && (attrs.mode === 'multiple' || attrs.mode === 'tags')) {
value = value || [];
}
return (
<>
<Select
{...attrs}
defaultValue={value}
value={value}
onChange={this.props.onChange}
onSearch={this.handleSearch}
disabled={isDisabled}
onPopupScroll={this.handleSelectScroll}
{...searchProps}
>
{this.state.optionsData.map((d: ILabelValue) =>
<Select.Option value={d.value} key={d.value}>
{d.label.length > 25 ? <Tooltip placement="bottomLeft" title={d.label}>
{d.label.substring(0, 25) + '...'}
</Tooltip> : d.label}
</Select.Option>)}
</Select>
</>
);
}
}

View File

@@ -0,0 +1,156 @@
import * as React from 'react';
import { Drawer, Modal, Button, message } from 'component/antd';
import { XFormComponent } from 'component/x-form';
import { IXFormWrapper } from 'types/base-type';
export class XFormWrapper extends React.Component<IXFormWrapper> {
public state = {
confirmLoading: false,
formMap: this.props.formMap || [] as any,
formData: this.props.formData || {},
};
private $formRef: any;
public updateFormMap$(formMap?: any, formData?: any, isResetForm?: boolean, resetFields?: string[]) {
if (isResetForm) {
resetFields ? this.resetForm(resetFields) : this.resetForm();
}
this.setState({
formMap,
formData,
});
}
public render() {
const { type } = this.props;
switch (type) {
case 'drawer':
return this.renderDrawer();
default:
return this.renderModal();
}
}
public renderDrawer() {
const {
visible,
title,
width,
formData,
formMap,
formLayout,
cancelText,
okText,
customRenderElement,
noform,
nofooter,
} = this.props;
return (
<Drawer
title={title}
visible={visible}
width={width}
closable={true}
onClose={this.handleCancel}
destroyOnClose={true}
>
<>
{customRenderElement}
</>
{!noform && (
<XFormComponent
ref={form => this.$formRef = form}
formData={formData}
formMap={formMap}
formLayout={formLayout}
/>)}
{!nofooter && (<div className="footer-btn">
<Button type="primary" onClick={this.handleSubmit}>{okText || '确认'}</Button>
<Button onClick={this.handleCancel}>{cancelText || '取消'}</Button>
</div>)}
<>
</>
</Drawer>
);
}
public renderModal() {
const { visible, title, width, formLayout, cancelText, okText, customRenderElement } = this.props;
const { formMap, formData } = this.state;
return (
<Modal
width={width}
title={title}
visible={visible}
confirmLoading={this.state.confirmLoading}
maskClosable={false}
onOk={this.handleSubmit}
onCancel={this.handleCancel}
okText={okText || '确认'}
cancelText={cancelText || '取消'}
>
<XFormComponent
ref={form => this.$formRef = form}
formData={formData}
formMap={formMap}
formLayout={formLayout}
/>
<>{customRenderElement}</>
</Modal>
);
}
public handleSubmit = () => {
this.$formRef.validateFields((error: Error, result: any) => {
if (error) {
return;
}
const { onSubmit, isWaitting } = this.props;
if (typeof onSubmit === 'function') {
if (isWaitting) {
this.setState({
confirmLoading: true,
});
onSubmit(result).then(() => {
this.setState({
confirmLoading: false,
});
message.success('操作成功');
this.resetForm();
this.closeModalWrapper();
});
return;
}
// tslint:disable-next-line:no-unused-expression
onSubmit && onSubmit(result);
this.resetForm();
this.closeModalWrapper();
}
});
}
public handleCancel = () => {
const { onCancel } = this.props;
// tslint:disable-next-line:no-unused-expression
onCancel && onCancel();
this.resetForm();
this.closeModalWrapper();
}
public resetForm = (resetFields?: string[]) => {
// tslint:disable-next-line:no-unused-expression
this.$formRef && this.$formRef.resetFields(resetFields || '');
}
public closeModalWrapper = () => {
const { onChangeVisible } = this.props;
// tslint:disable-next-line:no-unused-expression
onChangeVisible && onChangeVisible(false);
}
}

View File

@@ -0,0 +1,11 @@
.ant-input-number {
width: 314px
}
.footer-btn {
float: right;
Button:first-child {
margin-right: 16px;
}
}

View File

@@ -0,0 +1,197 @@
import * as React from 'react';
import { Select, Input, InputNumber, Form, Switch, Checkbox, DatePicker, Radio, Upload, Button, Icon, Tooltip } from 'component/antd';
import { searchProps } from 'constants/table';
import './index.less';
const TextArea = Input.TextArea;
const { RangePicker } = DatePicker;
export enum FormItemType {
input = 'input',
inputPassword = 'input_password',
inputNumber = 'input_number',
textArea = 'text_area',
select = 'select',
_switch = '_switch',
custom = 'custom',
checkBox = 'check_box',
datePicker = 'date_picker',
rangePicker = 'range_picker',
radioGroup = 'radio_group',
upload = 'upload',
}
export interface IFormItem {
key: string;
label: string;
type: FormItemType;
value?: string;
// 内部组件属性注入
attrs?: any;
// form属性注入
formAttrs?: any;
defaultValue?: string | number | any[];
rules?: any[];
invisible?: boolean;
renderExtraElement?: () => JSX.Element;
}
export interface IFormSelect extends IFormItem {
options: Array<{ key?: string | number, value: string | number, label: string, text?: string }>;
}
interface IFormCustom extends IFormItem {
customFormItem: React.Component;
}
interface IXFormProps {
formMap: IFormItem[];
formData: any;
form: any;
formLayout?: any;
layout?: 'inline' | 'horizontal' | 'vertical';
}
class XForm extends React.Component<IXFormProps> {
private defaultFormLayout = {
labelCol: { span: 6 },
wrapperCol: { span: 16 },
};
public onUploadFileChange = (e: any) => {
if (Array.isArray(e)) {
return e;
}
return e && e.fileList;
}
public handleFormItem(formItem: any, formData: any) {
let initialValue = formData[formItem.key] === 0 ? 0 : (formData[formItem.key] || formItem.defaultValue || '');
let valuePropName = 'value';
if (formItem.type === FormItemType.datePicker) {
initialValue = initialValue || null;
}
// if (formItem.type === FormItemType.checkBox) {
// initialValue = formItem.defaultValue ? [formItem.defaultValue] : [];
// }
if (formItem.type === FormItemType._switch) {
initialValue = false;
}
// if (formItem.type === FormItemType.select && formItem.attrs
// && ['tags'].includes(formItem.attrs.mode)) {
// initialValue = formItem.defaultValue ? [formItem.defaultValue] : [];
// }
if (formItem.type === FormItemType._switch) {
valuePropName = 'checked';
}
if (formItem.type === FormItemType.upload) {
valuePropName = 'fileList';
}
return { initialValue, valuePropName };
}
public render() {
const { form, formData, formMap, formLayout, layout } = this.props;
const { getFieldDecorator } = form;
return (
<Form layout={layout || 'horizontal'} onSubmit={() => ({})}>
{formMap.map(formItem => {
const { initialValue, valuePropName } = this.handleFormItem(formItem, formData);
const getFieldValue = {
initialValue,
rules: formItem.rules || [{ required: false, message: '' }],
valuePropName,
};
if (formItem.type === FormItemType.upload) {
Object.assign(getFieldValue, {
getValueFromEvent: this.onUploadFileChange,
});
}
return (
!formItem.invisible &&
<Form.Item
key={formItem.key}
label={formItem.label}
{...(formLayout || (layout === 'inline' ? {} : this.defaultFormLayout))}
{...formItem.formAttrs}
>
{getFieldDecorator(formItem.key, getFieldValue)(
this.renderFormItem(formItem),
)}
{formItem.renderExtraElement ? formItem.renderExtraElement() : null}
</Form.Item>
);
})}
</Form>
);
}
public renderFormItem(item: IFormItem) {
switch (item.type) {
default:
case FormItemType.input:
return <Input key={item.key} {...item.attrs} />;
case FormItemType.inputPassword:
return <Input.Password key={item.key} {...item.attrs} />;
case FormItemType.inputNumber:
return <InputNumber {...item.attrs} />;
case FormItemType.textArea:
return <TextArea rows={5} {...item.attrs} />;
case FormItemType.select:
return (
<Select
key={item.key}
{...item.attrs}
{...searchProps}
>
{(item as IFormSelect).options && (item as IFormSelect).options.map((v, index) => (
<Select.Option
key={v.value || v.key || index}
value={v.value}
>
{v.label.length > 35 ? <Tooltip placement="bottomLeft" title={v.text || v.label}>
{v.label}
</Tooltip> : v.label}
</Select.Option>
))}
</Select>
);
case FormItemType._switch:
return <Switch {...item.attrs} />;
case FormItemType.custom:
return (item as IFormCustom).customFormItem;
case FormItemType.checkBox:
return <Checkbox.Group options={(item as IFormSelect).options} />;
case FormItemType.datePicker:
return <DatePicker key={item.key} {...item.attrs} />;
case FormItemType.rangePicker:
return <RangePicker key={item.key} {...item.attrs} />;
case FormItemType.radioGroup:
return (
<Radio.Group key={item.key} {...item.attrs}>
{(item as IFormSelect).options.map((v, index) => (
<Radio.Button key={v.value || v.key || index} value={v.value}>{v.label}</Radio.Button>
))}
</Radio.Group>);
case FormItemType.upload:
return (
<Upload beforeUpload={(file: any) => false} {...item.attrs}>
<Button><Icon type="upload" /></Button>
</Upload>
);
}
}
}
export const XFormComponent = Form.create<IXFormProps>()(XForm);