Переглянути джерело

数据源复制/报表过滤条件逻辑

zhuth 7 роки тому
батько
коміт
fc317c1b72

+ 500 - 0
src/components/common/filterBox/filterBox2.jsx

@@ -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);

+ 1 - 1
src/components/common/login/relogin.jsx

@@ -95,7 +95,7 @@ class Relogin extends React.Component {
                     重新登录
                 </Button>
             </div>
-            <div>
+            <div style={{ textAlign: 'end' }}>
                 <a style={{ textDecoration: 'underline' }} onClick={() => {
                     dispatch({ type: 'main/logout' });
                 }}>

+ 3 - 1
src/components/dashboard/copyBox.jsx

@@ -39,7 +39,9 @@ class CopyBox extends React.Component {
             visible={visibleBox}
             onCancel={hideBox}
             onOk={() => {
-                dispatch({ type: 'dashboard/copy', dashboardCode: currentDashboardCode, dataSourceCode: currentDataSource });
+                dispatch({ type: 'dashboard/copy', dashboardCode: currentDashboardCode, dataSourceCode: currentDataSource }).then(() => {
+                    hideBox();
+                });
             }}
             maskClosable={true}
             destroyOnClose={true}

+ 24 - 3
src/components/dashboardDesigner/configSider.jsx

@@ -3,6 +3,7 @@ import { connect } from 'dva'
 import { Form, Input, Divider, Icon, Tooltip, Button } from 'antd'
 import ChooseChartBox from './chooseChartBox'
 import CusFilterBox from './cusFilterBox'
+import copy from 'copy-to-clipboard'
 import './configSider.less'
 const FormItem = Form.Item
 
@@ -12,7 +13,9 @@ class ConfigSider extends React.Component {
         super(props);
         this.state = {
             visibleChooseChartBox: false,
-            visibleCusFilterBox: false
+            visibleCusFilterBox: false,
+            copyDisabled: false,
+            copyText: '复制链接'
         };
     }
 
@@ -65,7 +68,7 @@ class ConfigSider extends React.Component {
 
     render() {
         const { dashboardDesigner, dispatch } = this.props;
-        const { visibleCusFilterBox } = this.state;
+        const { visibleCusFilterBox, copyDisabled, copyText } = this.state;
 
         return <Form className='config-sider' layout={'vertical'}>
             <Divider>报表制作</Divider>
@@ -77,7 +80,8 @@ class ConfigSider extends React.Component {
             {visibleCusFilterBox && <CusFilterBox visibleBox={visibleCusFilterBox} hideBox={this.hideCusFilterBox} />}
             <Divider>其他设置</Divider>
             <FormItem label='说明'>
-                <Input
+                <Input.TextArea
+                    autosize={{ minRows: 2, maxRows: 6 }}
                     value={dashboardDesigner.description}
                     onChange={(e) => {
                         dispatch({ type: 'dashboardDesigner/setField', name: 'description', value: e.target.value });
@@ -90,6 +94,23 @@ class ConfigSider extends React.Component {
                     onChange={(e) => {
                         dispatch({ type: 'dashboardDesigner/setField', name: 'shareCode', value: e.target.value });
                     }}
+                    addonAfter={<span style={{ cursor: copyDisabled ? 'not-allowed' : 'pointer' }} onClick={() => {
+                        if(copyDisabled) {
+                            return;
+                        }
+                        copy(window.location.origin + '/#/dashboard/share/' + dashboardDesigner.shareCode);
+                        this.setState({
+                            copyDisabled: true,
+                            copyText: '已复制'
+                        }, () => {
+                            setTimeout(() => {
+                                this.setState({
+                                    copyDisabled: false,
+                                    copyText: '复制链接'
+                                });
+                            }, 3000)
+                        });
+                    }}>{copyText}</span>}
                 />
             </FormItem>
         </Form>

+ 16 - 19
src/components/dashboardDesigner/content.jsx

@@ -2,7 +2,7 @@ import React from 'react'
 import { Layout, Tag, Icon } from 'antd'
 import { connect } from 'dva'
 import ViewLayout from './viewLayout'
-import FilterBox from '../common/filterBox/filterBox'
+import FilterBox from '../common/filterBox/filterBox2'
 import ConfigSider from './configSider'
 import moment from 'moment'
 
@@ -70,16 +70,17 @@ class DashboardDesignerContent extends React.Component {
         }
     }
 
-    getRelationFilterColumns = () => {
-        const { dashboardDesigner } = this.props;
-        const { relationColumns } = dashboardDesigner;
-        let arr = relationColumns.filter(r => r.relations.length > 0).map(r => ({
-            name: r.relations[0].column.name,
-            label: r.name,
-            type: r.relations[0].column.type
-        }));
-        return arr;
-    }
+    // getRelationFilterColumns = () => {
+    //     const { dashboardDesigner } = this.props;
+    //     const { relationColumns } = dashboardDesigner;
+    //     let arr = relationColumns.filter(r => r.relations.length > 0).map(r => ({
+    //         name: r.relations[0].column.name + (r.relations[1] ? (',' + r.relations[1].column.name) : ''),
+    //         label: r.name,
+    //         type: r.relations[0].column.type + (r.relations[1] ? (',' + r.relations[1].column.type) : ''),
+    //         dataSource: r.relations[0].dataSource + (r.relations[1] ? (',' + r.relations[1].dataSource) : ''),
+    //     }));
+    //     return arr;
+    // }
 
     createFilters = (filters) => {
         const { dispatch } = this.props;
@@ -135,17 +136,13 @@ class DashboardDesignerContent extends React.Component {
         const key = e.target.dataset.key;
         const { dashboardDesigner, dispatch } = this.props;
         const filters = dashboardDesigner.filters;
-        dispatch({ type: 'dashboardDesigner/changeFilters', filters: filters.map( f => {
-            if(+f.key === +key) {
-                f = { ...f, using: f.type ? !f.using : false}
-            }
-            return f;
-        }) });
+        let filter = filters.find(f => +f.key === +key);
+        dispatch({ type: 'dashboardDesigner/changeFilter', filter: { ...filter, using: filter.type ? !filter.using : false} });
     }
 
     render() {
         const { dashboardDesigner, isOwner, isShareView, isShareKeyView } = this.props;
-        const { code, editMode, filters } = dashboardDesigner;
+        const { dataSources, editMode, filters, relationColumns } = dashboardDesigner;
         const { visibleFilterBox } = this.state;
 
         const contentSize = this.getContentSize();
@@ -179,7 +176,7 @@ class DashboardDesignerContent extends React.Component {
                         <Icon type="filter" theme="outlined" />
                     </Tag>
                 </div>
-                {visibleFilterBox && <FilterBox type='dashboard' code={code} columns={this.getRelationFilterColumns()} filterData={filters} visibleFilterBox={visibleFilterBox} showFilterBox={this.showFilterBox} hideFilterBox={this.hideFilterBox} createFilters={this.createFilters} />}
+                {visibleFilterBox && <FilterBox dataSources={dataSources} relationColumns={relationColumns} filterData={filters} visibleFilterBox={visibleFilterBox} showFilterBox={this.showFilterBox} hideFilterBox={this.hideFilterBox} createFilters={this.createFilters} />}
             </Header>
             <Content className='dashboard-content'>
                 <Layout className='content-layout'>

+ 2 - 2
src/components/dashboardDesigner/cusFilterBox.jsx

@@ -34,7 +34,7 @@ class CusFilterBox extends React.Component {
                 header={<Form.Item className='filtercolumn-name' label='名称' labelCol={{ span: 6 }} wrapperCol={{ span: 18 }}>
                     <Input size='small' value={r.name}
                     onChange={(e) => {
-                        dispatch({ type: 'dashboardDesigner/setRelationColumn', code: r.code, relationColumn: { ...r, name: e.target.value } });
+                        dispatch({ type: 'dashboardDesigner/setRelationColumn', code: r.code, relationColumn: { ...r, name: e.target.value, cusName: true } });
                     }} onFocus={() => {
                         this.setState({
                             editing: true,
@@ -170,7 +170,7 @@ class CusFilterBox extends React.Component {
                 }
             }
             let index = relationColumns.findIndex(rc => rc.code === r.code);
-            relationColumns[index] = { ...r, name: relationColumns[index].name === '新字段' ? c.label : relationColumns[index].name, relations };
+            relationColumns[index] = { ...r, name: relationColumns[index].cusName ? relationColumns[index].name : c.label, cusName: true, relations };
             dispatch({ type: 'dashboardDesigner/setField', name: 'relationColumns', value: relationColumns });
         });
     }

+ 71 - 0
src/components/dataSource/copyBox.jsx

@@ -0,0 +1,71 @@
+import React from 'react'
+import { connect } from 'dva'
+import { Modal, Form, Select } from 'antd'
+const { Item: FormItem } = Form
+const { Option: SelectOption } = Select
+
+class CopyBox extends React.Component {
+    constructor(props) {
+        super(props);
+        this.state = {
+            currentDataConnectCode: props.currentDataConnectCode ? props.currentDataConnectCode : null
+        }
+    }
+
+    componentDidMount() {
+        const { dispatch } = this.props;
+        dispatch({ type: 'dataConnect/fetchList' });
+    }
+
+    generateOption() {
+        const { dataConnect } = this.props;
+        const { currentDataConnectCode } = this.state;
+        const { list } = dataConnect;
+
+        return list.filter((l) => l.code !== currentDataConnectCode).map((l) => <SelectOption key={l.code} value={l.code}>{l.name}</SelectOption>);
+    }
+
+    render() {
+        const { visibleBox, hideBox, dispatch, currentDataSourceCode } = this.props;
+        const { currentDataConnectCode } = this.state;
+        const formItemLayout = {
+            labelCol: { span: 6 },
+            wrapperCol: { span: 8 },
+        }
+
+        return <Modal
+            className='copy-box'
+            title={'复制数据源'}
+            visible={visibleBox}
+            onCancel={hideBox}
+            onOk={() => {
+                dispatch({ type: 'dataSource/copy', dataSourceCode: currentDataSourceCode, dataConnectCode: currentDataConnectCode }).then(() => {
+                    hideBox()
+                });
+            }}
+            maskClosable={true}
+            destroyOnClose={true}
+        >
+            <Form>
+                <FormItem
+                    label='切换数据链接'
+                    {...formItemLayout}
+                >
+                    <Select
+                        disabled={!currentDataConnectCode}
+                        placeholder='不切换'
+                        onChange={(value) => {
+                            this.setState({
+                                currentDataConnectCode: value
+                            })
+                        }}
+                    >
+                        { this.generateOption() }
+                    </Select>
+                </FormItem>
+            </Form>
+        </Modal>
+    }
+}
+
+export default connect(({ present: { dataConnect } }) => ({ dataConnect }))(CopyBox)

+ 18 - 1
src/components/dataSource/list.jsx

@@ -4,6 +4,7 @@ import { connect } from 'dva'
 import { dateFormat } from '../../utils/baseUtils'
 import GroupSelector from './groupSelector'
 import TransferBox from '../common/selectUserBox/selectUserBox'
+import CopyBox from './copyBox'
 import DeleteBox from '../common/deleteBox/deleteBox'
 import DataPreview from '../common/dataPreview/dataPreview'
 import './list.less'
@@ -20,6 +21,7 @@ class DataSource extends React.Component {
             visibleSetGroupMenu: false, //
             visibleTransferBox: false,
             visibleDeleteBox: false,
+            visibleCopyBox: false,
             visibleDataPreviewBox: false, // 显示数据预览
             groupEditing: false, // 是否处于编辑状态
         }
@@ -247,7 +249,7 @@ class DataSource extends React.Component {
     render() {
         
         const { main, dataSource, dispatch } = this.props;
-        const { selectedRecord, visibleTransferBox, visibleDeleteBox, visibleDataPreviewBox } = this.state;
+        const { selectedRecord, visibleTransferBox, visibleCopyBox, visibleDeleteBox, visibleDataPreviewBox } = this.state;
         const { currentUser } = main;
 
         const reg = new RegExp('([+ \\- & | ! ( ) { } \\[ \\] ^ \" ~ * ? : ( ) \/])', 'g'); // 需要转义的字符
@@ -289,6 +291,14 @@ class DataSource extends React.Component {
                 >
                     <Icon type="swap" />移交
                 </Menu.Item>}
+                <Menu.Divider />
+                { selectedRecord && <Menu.Item
+                    onClick={()=>{
+                        this.setState({ visibleCopyBox: true})
+                    }}
+                >
+                    <Icon type="copy" />复制
+                </Menu.Item>}
                 { selectedRecord && currentUser.code === selectedRecord.creatorCode && <Menu.Item
                     onClick={(e) => {
                         this.setState({ visibleDeleteBox: true})
@@ -482,6 +492,13 @@ class DataSource extends React.Component {
                             }}
                             onlyAdmin={true}
                         />
+                        {visibleCopyBox && <CopyBox
+                            visibleBox={visibleCopyBox}
+                            currentDataSourceCode={selectedRecord.code}
+                            currentDataConnectCode={selectedRecord.dbConfig.code}
+                            hideBox={() => { this.setState({ visibleCopyBox: false })
+                            }}
+                        />}
                         {visibleDeleteBox && <DeleteBox
                             visibleBox={visibleDeleteBox}
                             text={<div><span>确定要删除数据源【{selectedRecord.name}】吗?</span><br/><span style={{ color: 'red' }}>(此操作将会导致使用该数据源的图表失效!)</span></div>}

+ 1 - 0
src/components/dataSourceDetail/columnConfig.jsx

@@ -293,6 +293,7 @@ class DataSourceColumnConfig extends React.Component {
                                         placeholder={dataSourceDetail.address ? '输入表名或查询SQL,注意不能以分号结尾' : '请点击底部“上一步”按钮返回上一步选择数据库连接'}
                                         autosize={{ minRows: 3 }}
                                         // value={dataSourceDetail.target}
+                                        defaultValue={dataSourceDetail.target}
                                         onBlur={(e) => {
                                             dispatch({ type: 'dataSourceDetail/setFields', fields: [
                                                 { name: 'target', value: e.target.value },

+ 4 - 0
src/constants/url.js

@@ -52,6 +52,8 @@ const URLS = {
 
     DATASOURCE_QUERY_COLUMNDATA: BASE_URL + '/getScreenByBaseId', // 数据源关键字查询获得列去重之后的数据(20条)
 
+    DATASOURCE_QUERY_COLUMNDATA_MUL: BASE_URL + '/getScreenByBaseIds', // 数据源关键字查询获得列去重之后的数据(多个数据源)(20条)
+
     DATASOURCE_POLICY_LIST: BASE_URL + '/getDbStrategys', // 获得数据源的策略
 
     DATASOURCE_POLICY_ADD: BASE_URL + '/addStrategys', // 添加策略
@@ -64,6 +66,8 @@ const URLS = {
 
     DATASOURCE_TRANSFER: BASE_URL + '/Connector/changeDbOrder', // 数据源移交
 
+    DATASOURCE_COPY: BASE_URL + '/Connector/copyDataSource', // 数据源复制
+
     DATASOURCE_DATA_LIST: BASE_URL + '/Connector/getConnectorData', // 获取数据列表
 
     DATASOURCE_DATA_LIST_BY_CHART: BASE_URL + '/getChartsData', // 通过图表code获取数据列表

+ 1 - 0
src/models/dashboard.js

@@ -543,6 +543,7 @@ export default {
             });
             if(!res.err && res.data.code > 0) {
                 message.success('复制成功');
+                yield put({ type: 'fetchList', mandatory: true });
                 return true;
             }else {
                 message.error('复制失败: ' + (res.err || res.data.msg));

+ 63 - 30
src/models/dashboardDesigner.js

@@ -9,26 +9,35 @@ import CHART_TYPE from './chartType.json'
 /**
  * 获得看板中图表的真实过滤规则
  */
-function getTrueFilters(item, filters, relationColumns) {
+function getTrueFilters(item, filters) {
     let trueFilters = [];
     filters.forEach(f => {
-        relationColumns.forEach(rc => {
-            if(f.name === rc.code) {
-                rc.relations.forEach(re => {
-                    if(re.dataSource.code === item.dataSourceCode) {
-                        trueFilters.push({
-                            name: re.column.name,
-                            operator: f.operator,
-                            type: re.column.type,
-                            value1: f.value1,
-                            value2: f.value2,
-                            using: f.using
-                        });
-                    }
-                });
-            }
-        });
+        if(f.combined) {
+            let column = f.dataSource.columns.find(c => c.name === f.name);
+            column.relations.forEach(re => {
+                if(re.dataSource.code === item.dataSourceCode) {
+                    trueFilters.push({
+                        name: re.column.name,
+                        operator: f.operator,
+                        type: f.type,
+                        value1: f.value1,
+                        value2: f.value2,
+                        using: f.using
+                    });
+                }
+            });
+        }else {
+            trueFilters.push({
+                name: f.name,
+                operator: f.operator,
+                type: f.type,
+                value1: f.value1,
+                value2: f.value2,
+                using: f.using
+            });
+        }
     });
+    console.log(trueFilters);
     return trueFilters;
 }
 
@@ -354,25 +363,49 @@ export default {
                 message.error('请求列数据失败: ' + e);
             }
         },
+        /**
+         * 同时更改多个filter
+         */
         *changeFilters(action, { put, call, select }) {
             const { filters } = action;
             const dashboardDesigner = yield select(state => state.present.dashboardDesigner);
-            const { items, relationColumns } = dashboardDesigner;
+            const { items } = dashboardDesigner;
 
-            // 找到filters影响的数据源id
-            let targetDataSource = [];
-            filters.forEach(f => {
-                relationColumns.forEach(rc => {
-                    if(f.name === rc.code) {
-                        let dataSource = rc.relations.map(re => re.dataSource);
-                        targetDataSource = dataSource;
+            yield put({ type: 'silentSetField', name: 'filters', value: filters });
+            
+            for(let i = 0; i < items.length; i++) {
+                yield put({ type:'fetchChartData', item: items[i], mandatory: true });
+            }
+        },
+        /**
+         * 只更改一个filter
+         */
+        *changeFilter(action, { put, call, select }) {
+            const { filter } = action;
+            const dashboardDesigner = yield select(state => state.present.dashboardDesigner);
+            let { filters, items } = dashboardDesigner;
+            let targetDataSourceCodes = [];
+
+            filters = filters.map(f => {
+                if(f.key === filter.key) {
+                    if(f.combined) {
+                        let column = f.dataSource.columns.find(c => c.name === f.name);
+                        targetDataSourceCodes = targetDataSourceCodes.concat(column.relations.map(r => r.dataSource.code));
+                    }else {
+                        if(targetDataSourceCodes.indexOf(f.dataSource.name) === -1) {
+                            targetDataSourceCodes.push(f.dataSource.name);
+                        }
                     }
-                });
+                    return Object.assign({}, f, filter);
+                }else {
+                    return f;
+                }
             });
             // 找到filters有影响的item
-            let targetItems = items.filter(item => !!targetDataSource.find(d => d.code === item.dataSourceCode));
+            let targetItems = items.filter(item => targetDataSourceCodes.indexOf(item.dataSourceCode) !== -1);
+
             yield put({ type: 'silentSetField', name: 'filters', value: filters });
-            
+
             for(let i = 0; i < targetItems.length; i++) {
                 yield put({ type:'fetchChartData', item: targetItems[i], mandatory: true });
             }
@@ -380,12 +413,12 @@ export default {
         *fetchChartData(action, { put, call, select }) {
             const { item, mandatory, page, pageSize } = action;
             const dashboardDesigner = yield select(state => state.present.dashboardDesigner);
-            const { creatorCode, filters, relationColumns } = dashboardDesigner;
+            const { creatorCode, filters } = dashboardDesigner;
             const { chartCode } = item;
             const body = {
                 dashboardCreatorId: creatorCode,
                 chartId: chartCode,
-                filters: getBodyFilters(getTrueFilters(item, filters, relationColumns)),
+                filters: getBodyFilters(getTrueFilters(item, filters)),
                 testPage: {
                     pageNum: page|| 1,
                     pageSize: pageSize || 25, 

+ 32 - 7
src/models/dataSource.js

@@ -136,17 +136,27 @@ export default {
                     body
                 });
                 
-                console.log('请求数据源列表', body, res);
                 if(!res.err && res.data.code > 0) {
                     let list = res.data.data.list.map((r, i) => {
-                        let dbConfig = r.dbConfig;
+                        let dbConfig = {
+                            code: r.dbConfig.id,
+                            name: r.dbConfig.name,
+                            dbName: r.dbConfig.dataName,
+                            dbType: r.dbConfig.databaseType,
+                            address: r.dbConfig.addrass,
+                            port: r.dbConfig.port,
+                            userName: r.dbConfig.userName,
+                            password: r.dbConfig.passWord,
+                            description: r.dbConfig.note,
+                            demo: r.dbConfig.demo
+                        };
                         let tags = JSON.parse(r.dataTag);
                         return {
                             key: r.dataId + '',
                             code: r.dataId + '',
                             name: r.dataName,
                             type: r.type || 'database',
-                            dbType: dbConfig.databaseType,
+                            dbConfig: dbConfig,
                             creatorName: r.createBy,
                             creatorCode: r.createId+'',
                             createTime: new Date(r.createDate),
@@ -161,7 +171,6 @@ export default {
                     message.error('读取数据源列表错误: ' + (res.err || res.data.msg));
                 }
             }catch(e) {
-                console.error(body, e);
                 message.error('读取数据源列表错误: ' + e);
             }
             
@@ -749,7 +758,6 @@ export default {
                     url: URLS.DATASOURCE_DATA_LIST,
                     body
                 });
-                console.log('请求数据列表', body, res);
                 if(!res.err && res.data.code > 0) {
                     const { columnConfig, values } = res.data.data;
                     const columns = JSON.parse(columnConfig).map(c => ({
@@ -772,15 +780,32 @@ export default {
                         window.dispatchEvent(e);
                     }, 20);
                 }else {
-                    console.log(body, (res.err || res.data.msg));
                     message.error('请求数据列表失败: ' + (res.err || res.data.msg));
                 }
             }catch(e) {
-                console.log(body, e);
                 message.error('请求数据列表失败: ' + e);
             }finally {
                 yield put({ type: 'dataList/setField', name: 'loading', value: false });
             }
+        },
+        *copy(action, { select, call, put }) {
+            const { dataSourceCode, dataConnectCode } = action;
+
+            const res = yield call(service.fetch, {
+                url: URLS.DATASOURCE_COPY,
+                body: {
+                    dataSourceId: dataSourceCode,
+                    dataConnectionId: dataConnectCode
+                }
+            });
+            if(!res.err && res.data.code > 0) {
+                message.success('复制成功');
+                yield put({ type: 'fetchList', mandatory: true });
+                return true;
+            }else {
+                message.error('复制失败: ' + (res.err || res.data.msg));
+                return false;
+            }
         }
     },
     subscriptions: {