|
|
@@ -0,0 +1,500 @@
|
|
|
+/**
|
|
|
+ * 针对含有组合条件的报表使用的过滤器
|
|
|
+ */
|
|
|
+import React from 'react'
|
|
|
+import PropTypes from 'prop-types'
|
|
|
+import { Modal, Form, Row, Col, Input, Icon, Button, Cascader, Select, InputNumber, DatePicker, Spin } from 'antd'
|
|
|
+import OPERATORS from './filterOperators.js';
|
|
|
+import './filterBox.less'
|
|
|
+import * as service from '../../../services/index'
|
|
|
+import URLS from '../../../constants/url'
|
|
|
+import moment from 'moment';
|
|
|
+const FormItem = Form.Item
|
|
|
+const SelectOption = Select.Option
|
|
|
+
|
|
|
+let uuid = 0;
|
|
|
+class FilterBox extends React.Component {
|
|
|
+
|
|
|
+ constructor(props) {
|
|
|
+ uuid = 0;
|
|
|
+ super(props);
|
|
|
+ this.state = {
|
|
|
+ dataSources: props.dataSources || [],
|
|
|
+ relationColumns: props.relationColumns || [],
|
|
|
+ filterData: props.filterData || [],
|
|
|
+ fetching: false, // 请求列数据状态
|
|
|
+ columnData: [], // 列数据
|
|
|
+ fieldOptions: props.dataSources.map(d => {
|
|
|
+ return {
|
|
|
+ name: d.code,
|
|
|
+ label: d.name,
|
|
|
+ isLeaf: false,
|
|
|
+ }
|
|
|
+ }).concat(props.relationColumns.length > 0 ? [{
|
|
|
+ name: 'cus',
|
|
|
+ label: '关联字段',
|
|
|
+ combined: true,
|
|
|
+ isLeaf: false,
|
|
|
+ columns: props.relationColumns.map(r => ({
|
|
|
+ name: r.code,
|
|
|
+ type: r.relations[0].column.type,
|
|
|
+ label: r.name,
|
|
|
+ relations: r.relations
|
|
|
+ }))
|
|
|
+ }] : []), // 列选项
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ static propTypes = {
|
|
|
+ relationColumns: PropTypes.array,
|
|
|
+ filterData: PropTypes.array
|
|
|
+ }
|
|
|
+
|
|
|
+ componentDidMount() {
|
|
|
+ // 将原本的过滤条件生成可视化组件
|
|
|
+ this.loadColumnDatas();
|
|
|
+ const { filterData } = this.state;
|
|
|
+ this.addFilter(filterData.map(f => {
|
|
|
+ return {
|
|
|
+ ...f,
|
|
|
+ key: uuid++,
|
|
|
+ name: f.name,
|
|
|
+ label: f.label,
|
|
|
+ type: f.type,
|
|
|
+ operator: f.operator,
|
|
|
+ operatorLabel: f.operatorLabel,
|
|
|
+ value1: f.value1,
|
|
|
+ value2: f.value2,
|
|
|
+ using: f.using
|
|
|
+ }
|
|
|
+ }));
|
|
|
+ }
|
|
|
+
|
|
|
+ removeFilter = (key) => {
|
|
|
+ const { form } = this.props;
|
|
|
+ const filters = form.getFieldValue('filters');
|
|
|
+ form.setFieldsValue({
|
|
|
+ filters: filters.filter(filter => filter.key !== key),
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ removeAllFilters = () => {
|
|
|
+ const { form } = this.props;
|
|
|
+ form.setFieldsValue({
|
|
|
+ filters: [],
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ addFilter = (filtes) => {
|
|
|
+ const { form } = this.props;
|
|
|
+ const filters = form.getFieldValue('filters');
|
|
|
+ const nextFilters = filters.concat(filtes || {
|
|
|
+ key: uuid++,
|
|
|
+ using: true
|
|
|
+ });
|
|
|
+ form.setFieldsValue({
|
|
|
+ filters: nextFilters,
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 改变过滤条件字段
|
|
|
+ */
|
|
|
+ changeFilterName = (filter, value) => {
|
|
|
+ const { form } = this.props;
|
|
|
+ const filters = form.getFieldValue('filters');
|
|
|
+
|
|
|
+ this.setState({ columnData: [] });
|
|
|
+ form.setFieldsValue({
|
|
|
+ filters: filters.map((f) => {
|
|
|
+ if (f.key === filter.key) {
|
|
|
+ f.key = uuid++; // 每次重设key值以保证界面重现渲染,解决Select数据残留的问题
|
|
|
+ f.name = value[1].name;
|
|
|
+ f.combined = value[0].name === 'cus';
|
|
|
+ f.dataSource = value[0];
|
|
|
+ f.label = value[1].label;
|
|
|
+ f.type = value[1].type;
|
|
|
+ f.operator = OPERATORS[f.type][0].value;
|
|
|
+ f.operatorLabel = OPERATORS[f.type][0].label;
|
|
|
+ f.value1 = f.value2 = (f.type === 'time' ? moment() : undefined);
|
|
|
+ }
|
|
|
+ return f;
|
|
|
+ })
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 改变过滤条件连接符
|
|
|
+ */
|
|
|
+ changeFilterOperator = (filter, value) => {
|
|
|
+ const { form } = this.props;
|
|
|
+ const filters = form.getFieldValue('filters');
|
|
|
+ form.setFieldsValue({
|
|
|
+ filters: filters.map((f) => {
|
|
|
+ if (f.key === filter.key) {
|
|
|
+ f.key = uuid++;
|
|
|
+ f.operator = value.key;
|
|
|
+ f.operatorLabel = value.label;
|
|
|
+ f.value1 = f.value2 = (f.type === 'time' ? moment() : undefined);
|
|
|
+ }
|
|
|
+ return f;
|
|
|
+ })
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 改变过滤条件的值
|
|
|
+ */
|
|
|
+ changeFilterValue = (filter, value, index) => {
|
|
|
+ const { form } = this.props;
|
|
|
+ const filters = form.getFieldValue('filters');
|
|
|
+
|
|
|
+ form.setFieldsValue({
|
|
|
+ filters: filters.map((f) => {
|
|
|
+ if (f.key === filter.key) {
|
|
|
+ f.key = uuid++;
|
|
|
+ f[`value${index}`] = value;
|
|
|
+ }
|
|
|
+ return f;
|
|
|
+ })
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获得设定的过滤条件规则
|
|
|
+ */
|
|
|
+ getFilters = () => {
|
|
|
+ const { form, createFilters, hideFilterBox } = this.props;
|
|
|
+ form.validateFields((err, values) => {
|
|
|
+ if(!err) {
|
|
|
+ let filters = values.filters.map(f => ({
|
|
|
+ ...f,
|
|
|
+ filterLabel: this.createFilterLabel(f)
|
|
|
+ }));
|
|
|
+ createFilters && createFilters(filters);
|
|
|
+ hideFilterBox();
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ fetchColumnData = (filter, options) => {
|
|
|
+ const { columnData } = this.state;
|
|
|
+ const { keyword, mandatory } = options || {};
|
|
|
+ const { dataSource } = filter;
|
|
|
+ const isCusMode = dataSource.name === 'cus';
|
|
|
+ let column;
|
|
|
+ if(isCusMode) {
|
|
|
+ column = dataSource.columns.find(c => c.name === filter.name)
|
|
|
+ }
|
|
|
+ if(!columnData || columnData.length === 0 || mandatory) {
|
|
|
+ this.setState({ columnData: [], fetching: true }, () => {
|
|
|
+ const body = isCusMode ? column.relations.map(r => ({
|
|
|
+ id: r.dataSource.code,
|
|
|
+ columnName: r.column.name,
|
|
|
+ keyword,
|
|
|
+ })) : {
|
|
|
+ id: dataSource.name,
|
|
|
+ columnName: filter.name,
|
|
|
+ keyword,
|
|
|
+ };
|
|
|
+ service.fetch({
|
|
|
+ url: isCusMode ? URLS.DATASOURCE_QUERY_COLUMNDATA_MUL : URLS.DATASOURCE_QUERY_COLUMNDATA,
|
|
|
+ allow: true,
|
|
|
+ body: body,
|
|
|
+ }).then(r => {
|
|
|
+ if(!r.err && r.data.code > 0) {
|
|
|
+ return r;
|
|
|
+ }else {
|
|
|
+ let obj = {};
|
|
|
+ throw obj;
|
|
|
+ }
|
|
|
+ }).then(r => {
|
|
|
+ const resData = r.data.data || [];
|
|
|
+ this.setState({
|
|
|
+ columnData: resData.map(d => d || 'null'),
|
|
|
+ fetching: false
|
|
|
+ });
|
|
|
+ }).catch(ex => {
|
|
|
+ this.setState({
|
|
|
+ columnData: [],
|
|
|
+ fetching: false
|
|
|
+ });
|
|
|
+ console.error('fetch error', ex);
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成过滤规则文本
|
|
|
+ */
|
|
|
+ createFilterLabel = (filter) => {
|
|
|
+ let { label, operator, operatorLabel, type, value1, value2 } = filter;
|
|
|
+ let filterLabel;
|
|
|
+
|
|
|
+ if(type === 'string' || type === 'index') {
|
|
|
+ if(operator === 'null' || operator === 'notNull') {
|
|
|
+ filterLabel = `${label} ${operatorLabel}`;
|
|
|
+ }else {
|
|
|
+ filterLabel = `${label} ${operatorLabel} ${value1}`;
|
|
|
+ }
|
|
|
+ }else if(type === 'scale') {
|
|
|
+ if(operator === 'null' || operator === 'notNull') {
|
|
|
+ filterLabel = `${label} ${operatorLabel}`;
|
|
|
+ }else if(operator === 'between') {
|
|
|
+ filterLabel = `${label} ${operatorLabel} ${value1} ~ ${value2}`;
|
|
|
+ }else {
|
|
|
+ filterLabel = `${label} ${operatorLabel} ${value1}`;
|
|
|
+ }
|
|
|
+ }else if(type === 'time') {
|
|
|
+ value1 = moment(value1).format('YYYY/MM/DD');
|
|
|
+ value2 = moment(value2).format('YYYY/MM/DD');
|
|
|
+ if(operator === 'null' || operator === 'notNull') {
|
|
|
+ filterLabel = `${label} ${operatorLabel}`;
|
|
|
+ }else if(operator === 'between') {
|
|
|
+ filterLabel = `${label} ${operatorLabel} ${value1} ~ ${value2}`;
|
|
|
+ }else {
|
|
|
+ filterLabel = `${label} ${operatorLabel} ${value1}`;
|
|
|
+ }
|
|
|
+ }else if(type === 'categorical') {
|
|
|
+ if(operator === 'null' || operator === 'notNull') {
|
|
|
+ filterLabel = `${label} ${operatorLabel}`;
|
|
|
+ }else {
|
|
|
+ filterLabel = `${label} ${operatorLabel} ${value1}`;
|
|
|
+ }
|
|
|
+ }else {
|
|
|
+ filterLabel = '错误条件';
|
|
|
+ }
|
|
|
+ return filterLabel;
|
|
|
+ }
|
|
|
+
|
|
|
+ getFilterItems() {
|
|
|
+ const { getFieldDecorator, getFieldValue } = this.props.form;
|
|
|
+ getFieldDecorator('filters', { initialValue: [] });
|
|
|
+ const filters = getFieldValue('filters');
|
|
|
+
|
|
|
+ const filterItems = filters.map((f, index) => {
|
|
|
+ let { key, name, dataSource, type, operator, value1, value2 } = f;
|
|
|
+ return (
|
|
|
+ <Row key={`filterDiv[${key}]`}>
|
|
|
+ <Col span={22}>
|
|
|
+ <Col span={7}>
|
|
|
+ <FormItem key={key}>
|
|
|
+ {getFieldDecorator(`filterName${key}`, {
|
|
|
+ initialValue: dataSource ? [dataSource.name, name] : undefined,
|
|
|
+ rules: [{ required: true, message: '列名不能为空' }]
|
|
|
+ })(
|
|
|
+ this.generateFieldItem(f)
|
|
|
+ )}
|
|
|
+ </FormItem>
|
|
|
+ </Col>
|
|
|
+ <Col span={5}>
|
|
|
+ <FormItem key={key} className='filterOperator'>
|
|
|
+ {getFieldDecorator(`filterOperator${key}`, {
|
|
|
+ initialValue: operator?{key: operator}:undefined,
|
|
|
+ rules: [{ required: true, message: '操作类型不能为空' }]
|
|
|
+ })(
|
|
|
+ this.generateOperatorItem(f)
|
|
|
+ )}
|
|
|
+ </FormItem>
|
|
|
+ </Col>
|
|
|
+ <Col span={(operator&&operator!=='null'&&operator!=='notNull')?(operator==='between'?6:12):'0'}>
|
|
|
+ <FormItem
|
|
|
+ key={key}
|
|
|
+ className='filterValueOne'
|
|
|
+ >
|
|
|
+ {getFieldDecorator(`filterValueOne${key}`, {
|
|
|
+ initialValue: type==='time' ? ( value1 ? moment(value1) : null) : value1,
|
|
|
+ rules: operator&&operator!=='null'&&operator!=='notNull' ? [{ required: true, message: '该值不能为空' }] : null
|
|
|
+ })(this.generateValueItem(key, type, operator, 1))}
|
|
|
+ </FormItem>
|
|
|
+ </Col>
|
|
|
+ <Col span={operator==='between'?6:0}>
|
|
|
+ <FormItem
|
|
|
+ key={key}
|
|
|
+ className='filterValueTwo'
|
|
|
+ >
|
|
|
+ {getFieldDecorator(`filterValueTwo${key}`, {
|
|
|
+ initialValue: type==='time' ? ( value2 ? moment(value2) : null) : value2,
|
|
|
+ rules: [{ required: operator==='between', message: '该值不能为空' }]
|
|
|
+ })(this.generateValueItem(key, type, operator, 2))}
|
|
|
+ </FormItem>
|
|
|
+ </Col>
|
|
|
+ </Col>
|
|
|
+ <Col span={2} className='filter-remove-col' >
|
|
|
+ <Icon
|
|
|
+ className="dynamic-delete-button"
|
|
|
+ type="minus-circle"
|
|
|
+ onClick={() => { this.removeFilter(key) }}
|
|
|
+ />
|
|
|
+ </Col>
|
|
|
+ </Row>
|
|
|
+ );
|
|
|
+ });
|
|
|
+
|
|
|
+ return filterItems;
|
|
|
+ }
|
|
|
+
|
|
|
+ generateFieldItem(f) {
|
|
|
+ const { fieldOptions } = this.state;
|
|
|
+
|
|
|
+ return <Cascader
|
|
|
+ fieldNames={{ label: 'label', value: 'name', children: 'columns' }}
|
|
|
+ options={fieldOptions}
|
|
|
+ loadData={this.loadColumnData}
|
|
|
+ placeholder=''
|
|
|
+ displayRender={label=>label[1]}
|
|
|
+ onChange={(value, selectedOptions) => {
|
|
|
+ this.changeFilterName(f, selectedOptions)
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ }
|
|
|
+
|
|
|
+ generateOperatorItem(f) {
|
|
|
+ return (<Select
|
|
|
+ labelInValue={true}
|
|
|
+ onChange={(value) => {this.changeFilterOperator(f, value)}}
|
|
|
+ dropdownMatchSelectWidth={false}
|
|
|
+ >
|
|
|
+ {OPERATORS[f.type].map((o, i) => {
|
|
|
+ return <SelectOption key={i} value={o.value}>{o.label}</SelectOption>;
|
|
|
+ })}
|
|
|
+ </Select>)
|
|
|
+ }
|
|
|
+
|
|
|
+ generateValueItem = (key, type, operator, index) => {
|
|
|
+ const { fetching, columnData } = this.state;
|
|
|
+ let field;
|
|
|
+ const { form } = this.props;
|
|
|
+ const filters = form.getFieldValue('filters');
|
|
|
+ let filter = filters.filter((f) => {return f.key === key})[0];
|
|
|
+ // let column = columns.filter((c) => {return c.name === filter.name});
|
|
|
+ // column = column.length > 0 ? column[0] : { selection: [] };
|
|
|
+ // column.selection = column.selection || [];
|
|
|
+ if(['index', 'string'].indexOf(type) !== -1) {
|
|
|
+ field = <Input onBlur={(e) => {this.changeFilterValue(filter, e.target.value, index)}}/>
|
|
|
+ }else if(['scale', 'ordinal'].indexOf(type) !== -1) {
|
|
|
+ field = <InputNumber onBlur={(e) => {this.changeFilterValue(filter, e.target.value, index)}}/>
|
|
|
+ }else if(type === 'time') {
|
|
|
+ field = <DatePicker onChange={(value) => {this.changeFilterValue(filter, value, index)}}/>
|
|
|
+ }else if(type === 'categorical') { // 类别
|
|
|
+ if(operator === 'include' || operator==='notInclude') { // 包含/不包含
|
|
|
+ field = <Input onBlur={(e) => {this.changeFilterValue(filter, e.target.value, index)}}/>
|
|
|
+ }else if(operator === 'contain' || operator === 'notContain') { // 包括/不包括
|
|
|
+ field = (<Select
|
|
|
+ mode='multiple'
|
|
|
+ showSearch
|
|
|
+ dropdownMatchSelectWidth={false}
|
|
|
+ notFoundContent={fetching ? <Spin size="small" /> : '无'}
|
|
|
+ onSearch={(value) => {this.fetchColumnData(filter, { keyword: value, mandatory: true })}}
|
|
|
+ onFocus={() => {this.fetchColumnData(filter)}}
|
|
|
+ onChange={(value) => {this.changeFilterValue(filter, value, index)}}
|
|
|
+ >
|
|
|
+ { columnData.map((s, i) => {
|
|
|
+ return <SelectOption key={i} value={s}>{s}</SelectOption>
|
|
|
+ }) }
|
|
|
+ </Select>)
|
|
|
+ }else { // 等于/不等于
|
|
|
+ field = (<Select
|
|
|
+ mode='single'
|
|
|
+ showSearch
|
|
|
+ dropdownMatchSelectWidth={false}
|
|
|
+ notFoundContent={fetching ? <Spin size="small" /> : '无'}
|
|
|
+ onSearch={(value) => {this.fetchColumnData(filter, { keyword: value, mandatory: true })}}
|
|
|
+ onFocus={() => {this.fetchColumnData(filter)}}
|
|
|
+ onChange={(value) => {this.changeFilterValue(filter, value, index)}}
|
|
|
+ >
|
|
|
+ { columnData.map((s, i) => {
|
|
|
+ return <SelectOption key={i} value={s}>{s}</SelectOption>
|
|
|
+ }) }
|
|
|
+ </Select>)
|
|
|
+ }
|
|
|
+ }else {
|
|
|
+ field = <Input onBlur={(e) => {this.changeFilterValue(filter, e.target.value, index)}}/>
|
|
|
+ }
|
|
|
+
|
|
|
+ return field;
|
|
|
+ }
|
|
|
+
|
|
|
+ loadColumnDatas() {
|
|
|
+ let { fieldOptions } = this.state;
|
|
|
+ fieldOptions.forEach(f => {
|
|
|
+ if(!f.combined) {
|
|
|
+ this.loadColumnData([f]);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ loadColumnData = (selectedOptions) => {
|
|
|
+ let { fieldOptions } = this.state;
|
|
|
+ const targetOption = selectedOptions[selectedOptions.length - 1];
|
|
|
+ const stateIdx = fieldOptions.findIndex(f => f.name === targetOption.name);
|
|
|
+ targetOption.loading = true;
|
|
|
+
|
|
|
+ service.fetch({
|
|
|
+ url: URLS.DATASOURCE_QUERY_DATACOLUMNS,
|
|
|
+ allow: true,
|
|
|
+ body: targetOption.name
|
|
|
+ }).then(r => {
|
|
|
+ targetOption.loading = false;
|
|
|
+ if(!r.err && r.data.code > 0) {
|
|
|
+ return r;
|
|
|
+ }else {
|
|
|
+ let obj = {};
|
|
|
+ throw obj;
|
|
|
+ }
|
|
|
+ }).then(r => {
|
|
|
+ const resData = r.data.data || [];
|
|
|
+ let columns = resData.map((c, i) => {
|
|
|
+ return {
|
|
|
+ key: i,
|
|
|
+ name: c.columnName,
|
|
|
+ label: c.columnRaname,
|
|
|
+ type: c.columnType
|
|
|
+ }
|
|
|
+ })
|
|
|
+ fieldOptions[stateIdx].columns = columns;
|
|
|
+ this.setState({
|
|
|
+ fieldOptions: fieldOptions
|
|
|
+ });
|
|
|
+ }).catch(ex => {
|
|
|
+ console.error('fetch error', ex);
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ render() {
|
|
|
+ const { visibleFilterBox, hideFilterBox } = this.props;
|
|
|
+
|
|
|
+ return (
|
|
|
+ <Modal
|
|
|
+ className='filter-box'
|
|
|
+ title={<div>筛选条件<div className='clear' onClick={()=>{this.removeAllFilters()}}>清空条件<Icon type='delete'/></div></div>}
|
|
|
+ visible={visibleFilterBox}
|
|
|
+ onOk={this.getFilters}
|
|
|
+ onCancel={hideFilterBox}
|
|
|
+ maskClosable={false}
|
|
|
+ destroyOnClose={true}
|
|
|
+ >
|
|
|
+ <Form size='small'>
|
|
|
+ {this.getFilterItems()}
|
|
|
+ <Row>
|
|
|
+ <Col>
|
|
|
+ <FormItem>
|
|
|
+ <Button
|
|
|
+ className='filter-add-button'
|
|
|
+ type="dashed"
|
|
|
+ onClick={() => {this.addFilter()}}
|
|
|
+ >
|
|
|
+ <Icon type="plus" />
|
|
|
+ 添加
|
|
|
+ </Button>
|
|
|
+ </FormItem>
|
|
|
+ </Col>
|
|
|
+ </Row>
|
|
|
+ </Form>
|
|
|
+ </Modal>
|
|
|
+ );
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export default Form.create()(FilterBox);
|