Browse Source

合并冲突

hy 6 years ago
parent
commit
91cb7e8013
69 changed files with 3255 additions and 1177 deletions
  1. 1 1
      .eslintrc.js
  2. 0 3
      .roadhogrc.mock.js
  3. 6 1
      .webpackrc
  4. 7 1
      package.json
  5. 0 0
      public/favicon.png
  6. 24 1
      src/components/admin/userGroupDetailBox.jsx
  7. 1 1
      src/components/chart/list.jsx
  8. 8 6
      src/components/chartDesigner/header.less
  9. 1 0
      src/components/chartDesigner/sections/toolbar.less
  10. 39 21
      src/components/common/filterBox/filterBox.jsx
  11. 500 0
      src/components/common/filterBox/filterBox2.jsx
  12. 10 4
      src/components/common/filterBox/filterOperators.js
  13. 32 81
      src/components/common/login/login.jsx
  14. 113 0
      src/components/common/login/relogin.jsx
  15. 18 0
      src/components/common/login/relogin.less
  16. 4 1
      src/components/common/navigator.jsx
  17. 0 54
      src/components/common/rootLayout.jsx
  18. 0 35
      src/components/common/rootLayout.less
  19. 3 4
      src/components/common/shareQR/shareQR.jsx
  20. 72 0
      src/components/dashboard/copyBox.jsx
  21. 190 133
      src/components/dashboard/list.jsx
  22. 115 97
      src/components/dashboard/list.less
  23. 93 0
      src/components/dashboard/shareBox.jsx
  24. 10 0
      src/components/dashboard/shareKeyView.jsx
  25. 10 0
      src/components/dashboard/shareView.jsx
  26. 0 184
      src/components/dashboardDesigner/configForm.jsx
  27. 122 0
      src/components/dashboardDesigner/configSider.jsx
  28. 23 0
      src/components/dashboardDesigner/configSider.less
  29. 27 55
      src/components/dashboardDesigner/content.jsx
  30. 215 0
      src/components/dashboardDesigner/cusFilterBox.jsx
  31. 134 0
      src/components/dashboardDesigner/cusFilterBox.less
  32. 12 10
      src/components/dashboardDesigner/header.jsx
  33. 13 8
      src/components/dashboardDesigner/header.less
  34. 8 6
      src/components/dashboardDesigner/layout.jsx
  35. 10 148
      src/components/dashboardDesigner/layout.less
  36. 2 2
      src/components/dashboardDesigner/viewLayout.jsx
  37. 256 0
      src/components/dataConnect/list.jsx
  38. 174 0
      src/components/dataConnect/list.less
  39. 71 0
      src/components/dataSource/copyBox.jsx
  40. 1 1
      src/components/dataSource/groupSelector.jsx
  41. 27 7
      src/components/dataSource/list.jsx
  42. 33 2
      src/components/dataSourceDetail/baseConfig.jsx
  43. 10 3
      src/components/dataSourceDetail/columnConfig.jsx
  44. 1 1
      src/components/dataSourceDetail/columnType.json
  45. 1 1
      src/components/dataSourceDetail/content.jsx
  46. 8 3
      src/components/dataSourceDetail/content.less
  47. 39 17
      src/components/dataSourceDetail/dataConnectBox.jsx
  48. 41 22
      src/components/dataSourceDetail/dataConnectConfig.jsx
  49. 15 2
      src/constants/url.js
  50. 4 0
      src/custom.less
  51. 0 5
      src/index.ejs
  52. 15 7
      src/models/chart.js
  53. 6 11
      src/models/chartDesigner.js
  54. 249 25
      src/models/dashboard.js
  55. 103 61
      src/models/dashboardDesigner.js
  56. 62 66
      src/models/dataConnect.js
  57. 47 34
      src/models/dataSource.js
  58. 2 0
      src/models/dataSourceDetail.js
  59. 1 0
      src/models/defaultColumnType.json
  60. 129 19
      src/models/main.js
  61. 5 3
      src/models/parseChartOption.js
  62. 4 3
      src/models/userGroup.js
  63. 75 0
      src/routes/authLayout.jsx
  64. 0 0
      src/routes/authLayout.less
  65. 3 7
      src/routes/mainLayout.jsx
  66. 4 8
      src/routes/privateRoute.jsx
  67. 6 0
      src/routes/router.js
  68. 38 10
      src/services/index.js
  69. 2 2
      src/utils/request.js

+ 1 - 1
.eslintrc.js

@@ -12,5 +12,5 @@ module.exports = {
             "experimentalObjectRestSpread": true,
             "legacyDecorators": true
         }
-    },
+    }
 };

+ 0 - 3
.roadhogrc.mock.js

@@ -1,3 +0,0 @@
-
-export default {
-};

+ 6 - 1
.webpackrc

@@ -2,5 +2,10 @@
     "extraBabelPlugins": [
         ["import", { "libraryName": "antd", "libraryDirectory": "es", "style": true }]
     ],
-    "disableCSSModules": true
+    "disableCSSModules": true,
+    "hash": true,
+    "html": {
+        "favicon": "./public/favicon.png",
+        "template": "./src/index.ejs"
+    }
 }

+ 7 - 1
package.json

@@ -10,18 +10,21 @@
   },
   "dependencies": {
     "ant-design-pro": "^2.0.0-beta.2",
-    "antd": "^3.0.0",
+    "antd": "^3.12.4",
     "app": "^0.1.0",
     "braft-editor": "^1.9.8",
     "canvas2image": "^1.0.5",
+    "copy-to-clipboard": "^3.1.0",
     "dva": "^2.3.1",
     "dva-loading": "^2.0.3",
     "echarts": "^4.1.0",
     "echarts-for-react": "^2.0.14",
+    "fetch-abort": "^1.0.2",
     "html2canvas": "^1.0.0-alpha.12",
     "jspdf": "^1.4.1",
     "moment": "^2.22.2",
     "prop-types": "^15.6.2",
+    "qrcode.react": "^0.9.3",
     "react": "^16.2.0",
     "react-dom": "^16.2.0",
     "react-grid-layout": "^0.16.6",
@@ -48,7 +51,10 @@
     "husky": "^0.12.0",
     "redbox-react": "^1.4.3",
     "roadhog": "^2.5.0-beta.1",
+<<<<<<< HEAD
     "uglifyjs-webpack-plugin": "^1.0.0-rc.0",
+=======
+>>>>>>> 737790d32d174a62e39370192a167afbc80ff07d
     "webpack": "^4.29.6",
     "webpack-cli": "^3.3.0"
   }

+ 0 - 0
public/images/favicon.png → public/favicon.png


+ 24 - 1
src/components/admin/userGroupDetailBox.jsx

@@ -4,6 +4,12 @@ import { connect } from 'dva'
 const FormItem = Form.Item
 
 class DetailBox extends React.Component {
+    constructor(props) {
+        super(props);
+        this.state = {
+            okButtonDisabled: false
+        }
+    }
 
     okHandler = () => {
         const { dispatch, userGroup, form } = this.props;
@@ -26,7 +32,9 @@ class DetailBox extends React.Component {
     }
 
     render() {
+        const me = this;
         const { dispatch, userGroup, form } = this.props;
+        const { okButtonDisabled } = this.state;
         const { newOne } = userGroup;
 
         const { getFieldDecorator } = form;
@@ -40,6 +48,7 @@ class DetailBox extends React.Component {
                 title={`${newOne.operate === 'create' ? '新建' : '修改'}用户组`}
                 visible={newOne.visibleDetailBox}
                 onOk={this.okHandler}
+                okButtonProps={{ disabled: okButtonDisabled }}
                 onCancel={this.hideBox}
                 maskClosable={false}
                 destroyOnClose={true}
@@ -49,7 +58,21 @@ class DetailBox extends React.Component {
                         {
                             getFieldDecorator('groupName', {
                                 initialValue: newOne.name,
-                                rules: [{ required: true, whitespace: true, message: '用户组名不能为空' }],
+                                validateFirst: true,
+                                rules: [
+                                    { validator(rule, value, callback, source, options) {
+                                        let msg;
+                                        if(value.trim().length === 0) {
+                                            msg = '用户组名不能为空'
+                                        }else if(value.length > 20) {
+                                            msg = '用户组名长度不能大于20个字符'
+                                        }
+                                        me.setState({
+                                            okButtonDisabled: !!msg
+                                        });
+                                        callback(msg);
+                                    } }
+                                ],
                             })(
                                 <Input
                                     placeholder="请输入用户组名称"

+ 1 - 1
src/components/chart/list.jsx

@@ -206,7 +206,7 @@ class ChartList extends React.Component {
             </CardGrid>
         ));
         if(cards.length === 0) {
-            cards = <div style={{ padding: '7px', textAlign: 'center', fontSize: '14px', color: 'rgba(0, 0, 0, 0.45)' }}>暂无数据</div>
+            return (<div className="ant-empty ant-empty-normal"><div className="ant-empty-image"><img alt="暂无数据" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjQiIGhlaWdodD0iNDEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAxKSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj4KICAgIDxlbGxpcHNlIGZpbGw9IiNGNUY1RjUiIGN4PSIzMiIgY3k9IjMzIiByeD0iMzIiIHJ5PSI3Ii8+CiAgICA8ZyBmaWxsLXJ1bGU9Im5vbnplcm8iIHN0cm9rZT0iI0Q5RDlEOSI+CiAgICAgIDxwYXRoIGQ9Ik01NSAxMi43Nkw0NC44NTQgMS4yNThDNDQuMzY3LjQ3NCA0My42NTYgMCA0Mi45MDcgMEgyMS4wOTNjLS43NDkgMC0xLjQ2LjQ3NC0xLjk0NyAxLjI1N0w5IDEyLjc2MVYyMmg0NnYtOS4yNHoiLz4KICAgICAgPHBhdGggZD0iTTQxLjYxMyAxNS45MzFjMC0xLjYwNS45OTQtMi45MyAyLjIyNy0yLjkzMUg1NXYxOC4xMzdDNTUgMzMuMjYgNTMuNjggMzUgNTIuMDUgMzVoLTQwLjFDMTAuMzIgMzUgOSAzMy4yNTkgOSAzMS4xMzdWMTNoMTEuMTZjMS4yMzMgMCAyLjIyNyAxLjMyMyAyLjIyNyAyLjkyOHYuMDIyYzAgMS42MDUgMS4wMDUgMi45MDEgMi4yMzcgMi45MDFoMTQuNzUyYzEuMjMyIDAgMi4yMzctMS4zMDggMi4yMzctMi45MTN2LS4wMDd6IiBmaWxsPSIjRkFGQUZBIi8+CiAgICA8L2c+CiAgPC9nPgo8L3N2Zz4K"/></div><p className="ant-empty-description">暂无数据</p></div>)
         }
         return cards;
     }

+ 8 - 6
src/components/chartDesigner/header.less

@@ -17,12 +17,14 @@
             width: 300px;
         }
         .input-title {
-            text-align: center;
-            font-size: 18px;
-            border: none;
-            border-bottom-right-radius: 4px;
-            border-top-right-radius: 4px;
-            background-color: transparent;
+            input {
+                text-align: center;
+                font-size: 18px;
+                border: none;
+                border-bottom-right-radius: 4px;
+                border-top-right-radius: 4px;
+                background-color: transparent;
+            }
         }
         .ant-input-group-addon {
             cursor: pointer;

+ 1 - 0
src/components/chartDesigner/sections/toolbar.less

@@ -18,6 +18,7 @@
                 text-overflow: ellipsis;
                 border-style: dashed;
                 margin: 6px 2px 0 2px;
+                white-space: nowrap;
             }
             .filter-tag-using {
                 border-style: solid;

+ 39 - 21
src/components/common/filterBox/filterBox.jsx

@@ -141,9 +141,7 @@ class FilterBox extends React.Component {
      * 通过列名从数据列中获得其类型
      */
     getFilterType = (name) => {
-        let {
-            columns
-        } = this.state, i = 0, type;
+        let { columns } = this.state, i = 0, type;
         for (i; i < columns.length; i++) {
             let column = columns[i];
             if (column.name === name) {
@@ -171,20 +169,21 @@ class FilterBox extends React.Component {
         })
     }
 
-    fetchColumnData = (column) => {
+    fetchColumnData = (column, keyword, mandatory) => {
         const { type, code } = this.props;
         const { columnData } = this.state;
-        if(!columnData || columnData.length === 0) {
+        if(!columnData || columnData.length === 0 || mandatory) {
             this.setState({ columnData: [], fetching: true }, () => {
                 const body = {
                     id: code,
-                    columnName: column.name
+                    columnName: column.name,
+                    keyword, 
                 };
                 service.fetch({
                     url: type === 'chart' ? URLS.CHART_QUERY_COLUMNDATA : URLS.DATASOURCE_QUERY_COLUMNDATA,
+                    allow: true,
                     body: body,
                 }).then(r => {
-                    console.log('获得下拉数据', body, r);
                     if(!r.err && r.data.code > 0) {
                         return r;
                     }else {
@@ -215,7 +214,7 @@ class FilterBox extends React.Component {
         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 = 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)}}/>
@@ -223,19 +222,38 @@ class FilterBox extends React.Component {
             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') {
-            field = (<Select 
-                mode={(operator==='contain' || operator === 'notContain' ) ? 'multiple' : 'single'}
-                showSearch
-                dropdownMatchSelectWidth={false}
-                notFoundContent={fetching ? <Spin size="small" /> : '无'}
-                onFocus={() => {this.fetchColumnData(column)}}
-                onChange={(value) => {this.changeFilterValue(filter, value, index)}}
-            >
-                { columnData.map((s, i) => {
-                    return <SelectOption key={i} value={s}>{s}</SelectOption>
-                }) }
-            </Select>)
+        }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(column, value, true)}}
+                    onFocus={() => {this.fetchColumnData(column)}}
+                    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(column, value, true)}}
+                    onFocus={() => {this.fetchColumnData(column)}}
+                    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)}}/> 
         }

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

+ 10 - 4
src/components/common/filterBox/filterOperators.js

@@ -1,9 +1,9 @@
 export default {
     index: [{
-        value: "contain",
+        value: "include",
         label: "包含"
     }, {
-        value: "notContain",
+        value: "notInclude",
         label: "不包含"
     }, {
         value: "startsWith",
@@ -25,10 +25,10 @@ export default {
         label: "不为空"
     }],
     string: [{
-        value: "contain",
+        value: "include",
         label: "包含"
     }, {
-        value: "notContain",
+        value: "notInclude",
         label: "不包含"
     }, {
         value: "startsWith",
@@ -101,12 +101,18 @@ export default {
         label: "等于"
     }, {
         value: "contain",
+        label: "包括"
+    }, {
+        value: "include",
         label: "包含"
     }, {
         value: "<>",
         label: "不等于"
     }, {
         value: "notContain",
+        label: "不包括"
+    }, {
+        value: "notInclude",
         label: "不包含"
     }, {
         value: "null",

+ 32 - 81
src/components/common/login/login.jsx

@@ -3,37 +3,27 @@ import Login from 'ant-design-pro/lib/Login'
 import { Alert, Checkbox, Icon } from 'antd'
 import { Redirect } from 'dva/router'
 import { connect } from 'dva'
-import * as service from '../../../services/index'
-import URLS from '../../../constants/url'
 import './login.less'
 
 const { UserName, Password, Submit } = Login;
 
-function authenticate(token, expireTime, user, autoLogin, cb ) {
-    window.localStorage.setItem("loginTime", new Date().getTime());
-    window.localStorage.setItem("token", token);
-    window.localStorage.setItem("expireTime", expireTime);
-
-    window.localStorage.setItem("usercode", user.code);
-    autoLogin ? window.localStorage.setItem("autoLogin", 'true') : window.localStorage.setItem("autoLogin", 'false');
-    // autoLogin ? window.localStorage.setItem("account", user.account) : window.localStorage.removeItem('account');
-    window.localStorage.setItem("account", user.account);
-    autoLogin ? window.localStorage.setItem("password", user.password) : window.localStorage.removeItem('password');
-    window.localStorage.setItem("username", user.name);
-    window.localStorage.setItem("userrole", user.role);
-    window.localStorage.setItem("department", user.department);
-    window.localStorage.setItem("job", user.job);
+class LoginComponent extends React.Component {
 
-    setTimeout(cb, 100); // fake async
-}
+    constructor(props) {
+        super(props);
+        const { main } = props;
+        const { currentUser, autoLogin } = main;
+        const { account, password } = currentUser;
 
-class LoginComponent extends React.Component {
-    state = {
-        notice: '',
-        autoLogin: window.localStorage.getItem('autoLogin') ? window.localStorage.getItem('autoLogin') === 'true' : true,
-        redirectToReferrer: false,
-        fetching: false
-    };
+        this.state = {
+            notice: '',
+            currentUserName: account,
+            currentPassword: password,
+            autoLogin: autoLogin,
+            redirectToReferrer: false,
+            fetching: false
+        }
+    }
 
     onSubmit = (err, values) => {
         this.setState({
@@ -54,66 +44,27 @@ class LoginComponent extends React.Component {
         const { dispatch } = this.props;
         const { autoLogin } = this.state;
 
-        let body = {
-            userName: username,
-            passWord: password
-        };
         this.setState({
             fetching: true
-        }, () => {
-            try {
-                service.fetch({
-                    url: URLS.LOGIN,
-                    body: body
-                }).then(r => {
-                    console.log('登录', body, r);
-                    if(!r.err && r.data.code > 0) {
-                        return r.data.data;
-                    }else {
-                        this.setState({
-                            notice: r.err+'' || r.data.msg+'',
-                        });
-                        throw (r.err || r.data.msg);
-                    }
-                }).then(resData => {
-                    const token = resData.token;
-                    const expireTime = resData.times;
-                    const user = resData.user;
-                    const currentUser = {
-                        code: user.id+'',
-                        account: user.userName,
-                        password: user.passWord,
-                        name: user.name,
-                        role: user.role || 'default',
-                        department: user.department,
-                        job: user.post,
-                    };
-                    dispatch({ type: 'main/setCurrentUser', user: currentUser });
-                    authenticate(token, expireTime, currentUser, autoLogin, () => {
-                        this.setState({
-                            redirectToReferrer: true,
-                            fetching: false
-                        });
-                    });
-                }).catch(ex => {
-                    console.error('登录失败', ex);
-                    this.setState({
-                        notice: (ex.message+'' || ex+'') || '登录失败',
-                        fetching: false
-                    });
-                });
-            }catch(e) {
-                console.error(e);
-            }
-        });
+        })
+        dispatch({ type: 'main/login', username, password, autoLogin })
+        .then((d) => {
+            this.setState({
+                notice: '',
+                redirectToReferrer: d,
+                fetching: false
+            })
+        }).catch((r) => {
+            this.setState({
+                notice: r.err+'' || r.data.msg+'',
+                fetching: false
+            });
+        })
     };
 
     render() {
         const { from } = this.props.location.state || { from: { pathname: "/" } };
-        const { notice, autoLogin, fetching, redirectToReferrer } = this.state;
-
-        const defaultAccount = window.localStorage.getItem('account');
-        const defaultPassword = window.localStorage.getItem('password');
+        const { currentUserName, currentPassword, notice, autoLogin, fetching, redirectToReferrer } = this.state;
 
         if (redirectToReferrer) {
             return <Redirect to={from} />;
@@ -140,7 +91,7 @@ class LoginComponent extends React.Component {
                                 disabled={fetching}
                                 placeholder='输入用户名'
                                 // defaultValue={autoLogin ? defaultAccount : ''}
-                                defaultValue={defaultAccount}
+                                defaultValue={currentUserName}
                                 onChange={() => {
                                     this.setState({
                                         notice: ''
@@ -159,7 +110,7 @@ class LoginComponent extends React.Component {
                                 name="password"
                                 disabled={fetching}
                                 placeholder='输入密码'
-                                defaultValue={autoLogin ? defaultPassword : ''}
+                                defaultValue={autoLogin ? currentPassword : ''}
                                 onChange={() => {
                                     this.setState({
                                         notice: ''

+ 113 - 0
src/components/common/login/relogin.jsx

@@ -0,0 +1,113 @@
+import React from 'react'
+import { Modal, Icon, Button, Input, Checkbox  } from 'antd'
+import { connect } from 'dva'
+import './relogin.less'
+
+class Relogin extends React.Component {
+
+    constructor(props) {
+        super(props);
+        const { main } = props;
+        const { currentUser, autoLogin } = main;
+        const { account, password } = currentUser;
+
+        this.state = {
+            currentUserName: account,
+            currentPassword: password,
+            autoLogin: autoLogin,
+            fetching: false
+        }
+    }
+
+    onUsernameChange = (e) => {
+        this.setState({ currentUserName: e.target.value });
+    }
+    
+    onPasswordChange = (e) => {
+        this.setState({ currentPassword: e.target.value });
+    }
+
+    onRemanberChange = (e) => {
+        this.setState({ autoLogin: e.target.checked });
+    }
+
+    onLogin = () => {
+        const { currentUserName, currentPassword, autoLogin } = this.state;
+        const { dispatch } = this.props;
+        this.setState({
+            fetching: true
+        })
+        dispatch({ type: 'main/login', username: currentUserName, password: currentPassword, autoLogin })
+        .then((d) => {
+            this.setState({
+                fetching: false
+            })
+        }).catch((r) => {
+            console.error(r)
+        })
+    }
+
+    render() {
+        const { main, visibleBox, dispatch } = this.props;
+        const { autoLogin, fetching } = this.state;
+        const { currentUser } = main;
+        const { account, password: defaultPassword } = currentUser;
+
+        return <Modal
+            title="重新登录"
+            className='relogin-box'
+            visible={visibleBox}
+            footer={null}
+            closable={false}
+            centered={true}
+            width={380}
+        >
+            <div className="relogin-item">距离上次登录已经超过30分钟,请重新登录</div>
+            <Input
+                placeholder="用户名"
+                prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />}
+                defaultValue={account}
+                disabled={true}
+                className="relogin-item"
+                onChange={this.onUsernameChange}
+            ></Input>
+            <Input.Password
+                placeholder="请输入登录密码"
+                prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />}
+                className="relogin-item"
+                defaultValue={autoLogin ? defaultPassword : ''}
+                rules={[{
+                    required: true,
+                    whitespace: true,
+                    message: "密码不能为空!",
+                }]}
+                onChange={this.onPasswordChange}
+                onPressEnter={(e) => {
+                    this.onLogin()
+                }}
+            ></Input.Password>
+            <div className="relogin-item remanber">
+                <Checkbox defaultChecked={autoLogin} onChange={this.onRemanberChange}>记住密码</Checkbox>
+            </div>
+            <div className="relogin-item buttons">
+                <Button disabled={fetching} type="primary" onClick={this.onLogin}>
+                    {fetching && <Icon type="loading" theme="outlined" />}
+                    重新登录
+                </Button>
+            </div>
+            <div style={{ textAlign: 'end' }}>
+                <a style={{ textDecoration: 'underline' }} onClick={() => {
+                    dispatch({ type: 'main/logout' });
+                }}>
+                    切换用户
+                </a>
+            </div>
+        </Modal>
+    }
+}
+
+function mapStateToProps({ present: { main } }) {
+    return { main };
+}
+
+export default connect(mapStateToProps)(Relogin)

+ 18 - 0
src/components/common/login/relogin.less

@@ -0,0 +1,18 @@
+.relogin-box {
+    .ant-modal-content {
+        .ant-modal-header {
+            padding: 8px 24px;
+        }
+        .ant-modal-body {
+            padding: 16px 24px;
+
+            .relogin-item {
+                margin-bottom: 8px;
+            }
+
+            .buttons {
+                text-align: center;
+            }
+        }
+    }
+}

+ 4 - 1
src/components/common/navigator.jsx

@@ -32,7 +32,7 @@ class Navigator extends React.Component {
                 <Menu.Item key="3" onClick={() => {
                     dispatch({ type: 'main/logout' });
                     dispatch({ type: 'main/redirect', path: '/login' });
-                }}><Icon type="logout" />注销</Menu.Item>
+                }}><Icon type="logout" />退出系统</Menu.Item>
             </Menu>
         ) : (
             <Menu>
@@ -56,6 +56,9 @@ class Navigator extends React.Component {
                     <Menu.Item className='nav-page' key="home">
                         <Link to='/home'><Icon type="home" />我的</Link>
                     </Menu.Item>
+                    <Menu.Item className='nav-page' key="dataconnect">
+                        <Link to='/dataconnect'><Icon type="link" />数据连接</Link>
+                    </Menu.Item>
                     <Menu.Item className='nav-page' key="datasource">
                         <Link to='/datasource'><Icon type="database" />数据源</Link>
                     </Menu.Item>

+ 0 - 54
src/components/common/rootLayout.jsx

@@ -1,54 +0,0 @@
-import React from 'react'
-import { Modal, Icon, Button } from 'antd'
-import { Redirect } from 'dva/router'
-import './rootLayout.less'
-
-class RootLayout extends React.Component {
-    constructor(props) {
-        super(props);
-        this.state = ({
-            goLogin: false
-        });
-    }
-
-    setGoLogin = () => {
-        this.setState({
-            goLogin: true
-        });
-    }
-
-    render() {
-        const { location, isAuthenticated, children } = this.props;
-        const { goLogin } = this.state;
-        const visibleLoginConfimBox = location !=='/login' && !isAuthenticated;
-        const token = window.localStorage.getItem('token');
-
-        return (<div className='root-layout'>
-            { children }
-            {visibleLoginConfimBox && <Modal
-                className='confirm-box'
-                visible={visibleLoginConfimBox}
-                footer={null}
-                closable={false}
-                centered={true}
-            >
-                <div className='confirm-body'>
-                    <div className='confirm-icon'><Icon type="info-circle" /></div>
-                    <div className='confirm-label'>{token ? '登录已过期' : '未登录'}</div>
-                    <div className='confirm-text'>{token ? '距离上次登录已经超过30分钟,请重新登录' : ''}</div>
-                    <div className='confirm-button'>
-                        <Button type="primary" onClick={this.setGoLogin}>{token ? '重新登录' : '登录'}</Button>
-                    </div>
-                </div>
-            </Modal>}
-            {goLogin && <Redirect
-                to={{
-                    pathname: "/login",
-                    state: { from: location }
-                }}
-            />}
-        </div>)
-    }
-}
-
-export default RootLayout;

+ 0 - 35
src/components/common/rootLayout.less

@@ -1,35 +0,0 @@
-.root-layout {
-    width: 100%;
-    height: 100%;
-}
-.confirm-box {
-    .ant-modal-body {
-        padding: 0;
-        .confirm-body {
-            display: flex;
-            flex-direction: column;
-            text-align: center;
-            .confirm-icon {
-                .anticon {
-                    height: 50px;
-                    line-height: 50px;
-                    font-size: 32px;
-                    color: #FF4D4F;
-                    svg {
-                        margin-top: 10px;
-                    }
-                }
-            }
-            .confirm-label {
-                height: 50px;
-                line-height: 50px;
-                font-size: 24px;
-                font-weight: bold;
-            }
-            .confirm-button {
-                height: 48px;
-                line-height: 48px;
-            }
-        }
-    }
-}

+ 3 - 4
src/components/common/shareQR/shareQR.jsx

@@ -9,11 +9,10 @@ class ShareQR extends React.Component {
         }
     }
     componentDidMount(){
-
-    this.setState({
+        this.setState({
             url: "http://www.baidu.com/"
-            })
-        }
+        })
+    }
 
     render(){
         return(

+ 72 - 0
src/components/dashboard/copyBox.jsx

@@ -0,0 +1,72 @@
+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.currentDataConnect ? props.currentDataConnect.code : 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, currentDashboardCode } = this.props;
+        const { currentDataConnectCode } = this.state;
+        const formItemLayout = {
+            labelCol: { span: 4 },
+            wrapperCol: { span: 8 },
+        }
+
+        return <Modal
+            className='copy-box'
+            title={'复制报表'}
+            visible={visibleBox}
+            onCancel={hideBox}
+            onOk={() => {
+                dispatch({ type: 'dashboard/copy', dashboardCode: currentDashboardCode, dataConnectCode: currentDataConnectCode }).then(() => {
+                    hideBox();
+                });
+            }}
+            maskClosable={true}
+            destroyOnClose={true}
+        >
+            <div>复制对象包括报表和报表包含的图表</div>
+            <Form>
+                <FormItem
+                    label='切换数据源'
+                    {...formItemLayout}
+                >
+                    <Select
+                        disabled={!currentDataConnectCode}
+                        placeholder={currentDataConnectCode ? '请选择...' : '无需选择数据源'}
+                        onChange={(value) => {
+                            this.setState({
+                                currentDataConnectCode: value
+                            })
+                        }}
+                    >
+                        { this.generateOption() }
+                    </Select>
+                </FormItem>
+            </Form>
+        </Modal>
+    }
+}
+
+export default connect(({ present: { dataConnect } }) => ({ dataConnect }))(CopyBox)

+ 190 - 133
src/components/dashboard/list.jsx

@@ -1,15 +1,15 @@
 import React from 'react'
-import { Layout, Button, Icon, Input, Menu, Dropdown, Card, Col, Row } from 'antd'
+import { Layout, Button, Icon, Input, Table, Menu, Dropdown, Card, Col, Row } from 'antd'
 import { connect } from 'dva'
-import moment from 'moment'
 import TransferBox from '../common/selectUserBox/selectUserBox';
 import AccessObjectBox from '../common/accessObjectBox/accessObjectBox'
-import Thumbnail from './thumbnail'
-import './list.less'
+import { dateFormat } from '../../utils/baseUtils'
 import DeleteBox from '../common/deleteBox/deleteBox'
+import ShareBox from './shareBox'
+import CopyBox from './copyBox'
+import './list.less'
 const { Content } = Layout
 const { Search } = Input
-const CardGrid = Card.Grid
 
 
 class DashboardList extends React.Component {
@@ -19,9 +19,12 @@ class DashboardList extends React.Component {
             selectedRecord: null,
             visibleChooseDataSourceBox: false,
             visibleDistributeBox: false,
+            visibleShareBox: false,
+            shareUrl: '',
             visibleTransferBox: false,
             visibleGroupMenu: false, // 显示分组菜单
             visibleDeleteBox: false,
+            visibleCopyBox: false,
             defaultSelectedGroups: [],
             defaultSelectedUsers: [],
         }
@@ -29,31 +32,7 @@ class DashboardList extends React.Component {
 
     componentDidMount() {
         const { dispatch } = this.props;
-        this.setBodyWidth();
         dispatch({ type: 'dashboard/fetchList' });
-        window.addEventListener('resize', this.setBodyWidth);
-    }
-
-    componentWillUnmount() {
-        window.removeEventListener('resize', this.setBodyWidth)
-    }
-
-    /**
-     * 设置卡片容器宽度 = 每行最大卡片数量 * 卡片宽度
-     */
-    setBodyWidth() {
-        const cardBody = document.getElementsByClassName('dashboard-body')[0]; // 卡片容器
-        const parent = cardBody.parentNode; // 父级容器
-        const pWidth = parent.offsetWidth; // 父级容器宽度
-        const pPadding = 10 + 10; // 父级容器左右padding
-        const cWidth = 512; // 每个卡片宽度
-        const cMargin = 8 + 8; // 每个卡片左右margin
-        const pTrueWidth = pWidth - pPadding; // 父容器实际可用宽度
-        const cTrueWidth = cWidth + cMargin; // 卡片实际占用宽度
-        const count = Math.floor(pTrueWidth/cTrueWidth); // 每行最大卡片数量
-        const cardBodyWidth = count * cTrueWidth;
-
-        cardBodyWidth > 0 ? cardBody.style.width = cardBodyWidth + 'px' : void(0);
     }
 
     getShareList = () => {
@@ -83,24 +62,70 @@ class DashboardList extends React.Component {
         });
     }
 
-    generateCard() {
-        const { main, dashboard, dispatch } = this.props;
+    handleVisibleChange = (flag) => {
+        this.setState({ visibleGroupMenu: flag });
+    }
+
+    hideGroupMenu = () => {
+        this.setState({
+            visibleGrouMenu: false
+        });
+    }
+
+    distribute = (group, geren) => {
+        const { dispatch } = this.props;
         const { selectedRecord } = this.state;
+        let targets = group.map(g => ({
+            code: g.code,
+            name: g.name,
+            isGroup: true
+        })).concat(geren.map(g => ({
+            code: g.code,
+            name: g.name,
+            isGroup: false
+        })))
+        dispatch({ type: 'dashboard/share', code: selectedRecord.code, targets });
+    }
+
+    onSearch(list, text) {
+        const reg = new RegExp('([+ \\- & | ! ( ) { } \\[ \\] ^ \" ~ * ? : ( ) \/])', 'g'); // 需要转义的字符
+        let filterLabel = (text || '').replace(new RegExp('(\\\\)', 'g'), '\\$1').replace(reg, '\\$1'); // 添加转义符号
+        return list.map(l => {
+            let o = Object.assign({}, l);
+            let reg = new RegExp('('+ filterLabel +'){1}', 'ig');
+            if(o.name && o.name.search(reg) !== -1) {
+                return o;
+            }else if(o.description && o.description.search(reg) !== -1) {
+                return o;
+            }else {
+                return null
+            }
+        }).filter(a => a!==null);
+    }
+
+    onSort(list) {
+        return list.sort((a, b) => {
+            return new Date(b.createTime) - new Date(a.createTime);
+        });
+    }
+
+    render() {
+        const { dispatch, dashboard, main } = this.props;
+        const { visibleShareBox, shareUrl, visibleDistributeBox, visibleTransferBox, visibleDeleteBox, visibleCopyBox, selectedRecord, defaultSelectedGroups, defaultSelectedUsers } = this.state
         const { currentUser } = main;
-        const list = dashboard.list;
 
         const reg = new RegExp('([+ \\- & | ! ( ) { } \\[ \\] ^ \" ~ * ? : ( ) \/])', 'g'); // 需要转义的字符
         let filterLabel = dashboard.filterLabel.replace(new RegExp('(\\\\)', 'g'), '\\$1').replace(reg, '\\$1'); // 添加转义符号
 
-        const operationMenu = (
+        const moreOperatingMenu = (
             <Menu className='menu-operation'>
-                <Menu.Item
-                    disabled
-                    onClick={(e) => {
-                    }}
-                >
-                    <Icon type="delete" />分享
-                </Menu.Item>
+                { selectedRecord && currentUser.code === selectedRecord.creatorCode && <Menu.Item onClick={() => {
+                    dispatch({ type: 'dashboard/getShareKey', record: selectedRecord, delay: 7 }).then((key) => {
+                        this.setState({ visibleShareBox: true, shareUrl: window.location.origin + '/#/dashboard/share_key/' + key })
+                    });
+                }}> 
+                    <Icon type='share-alt'/>分享
+                </Menu.Item>}
                 { selectedRecord && currentUser.code === selectedRecord.creatorCode && <Menu.Divider />}
                 { selectedRecord && currentUser.code === selectedRecord.creatorCode && <Menu.Item onClick={this.getShareList}> 
                     <Icon type='share-alt'/>分发
@@ -112,6 +137,14 @@ class DashboardList extends React.Component {
                 >
                     <Icon type="swap" />移交
                 </Menu.Item>}
+                { selectedRecord && currentUser.code === selectedRecord.creatorCode && <Menu.Divider />}
+                { selectedRecord && (selectedRecord.dataConnects.length <= 1) && <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})
@@ -122,99 +155,79 @@ class DashboardList extends React.Component {
             </Menu>
         )
 
-        let cards = list.filter(l => {
-            let reg = new RegExp('(' + filterLabel + '){1}', 'ig');
-            return (l.name || '').search(reg) !== -1;
-        }).sort((a, b) => {
-            return new Date(b.createTime) - new Date(a.createTime)
-        }).map( (l, i) => (
-            <CardGrid className='dashboard-card' key={i} onClick={() => {
-                this.setState({ selectedRecord: l })
-            }}>
-                <Card
-                    title={
-                        <Row type='flex' justify='space-between'>
-                            <Col span={21} style={{ overflow: 'hidden', textOverflow: 'ellipsis' }} >
-                                { filterLabel ?
-                                    ((l.name || '').split(new RegExp(`(${filterLabel})`, 'i')).map((fragment, i) => {
-                                        return (
-                                            fragment.toLowerCase().replace(new RegExp('(\\\\)', 'g'), '\\$1').replace(reg, '\\$1') === filterLabel.toLowerCase() ?
-                                            <span key={i} style={{fontWeight: 'bold', color: 'red'}} className="highlight">{fragment}</span> :
-                                            fragment
-                                        )
-                                    }
-                                    )) : l.name
-                                }
-                            </Col>
-                            <Col style={{ textAlign: 'right' }} span={3} >
-                                {/* <Icon type='star-o'/> */}
-                            </Col>
-                        </Row>
-                    }
-                    cover={
-                        <Col className='cover-body'>
-                            <Row className='thumb' onClick={() => {
-                                dispatch({ type: 'dashboardDesigner/reset' });
-                                dispatch({ type: 'main/redirect', path: '/dashboard/' + l.code });
-                                dispatch({ type: 'recent/addRecentRecord', tarId: l.code, recordType: 1});
-                            }}>
-                                <Thumbnail type={l.type} code={l.code} option={l.chartOption} thumbnail={l.thumbnail}/>
-                            </Row>
-                            <Row className='footer' type='flex' justify='end' align='bottom'>
-                                <Col style={{ textAlign: 'left' }} span={22}>
-                                    <Row>{l.creatorName} {moment(l.createTime).format('YYYY-MM-DD')}</Row>
-                                </Col>
-                                <Col span={2} style={{ textAlign: 'right' }}>
-                                    <Dropdown overlay={operationMenu} trigger={['click']}>
-                                        <Icon type="ellipsis" />
-                                    </Dropdown>
-                                </Col>
-                            </Row>
-                        </Col>
-                    }
-                >
-                </Card>
-            </CardGrid>
-        ));
-        if(cards.length === 0) {
-            cards = <div style={{ padding: '7px', textAlign: 'center', fontSize: '14px', color: 'rgba(0, 0, 0, 0.45)' }}>暂无数据</div>
-        }
-        return cards;
-    }
-
-    handleVisibleChange = (flag) => {
-        this.setState({ visibleGroupMenu: flag });
-    }
-
-    hideGroupMenu = () => {
-        this.setState({
-            visibleGrouMenu: false
-        });
-    }
+        const dashboardColumns = [{
+            title: '名称',
+            dataIndex: 'name',
+            key: 'name',
+            width: 100,
+            render: (text, record) => {
+                return (
+                    <span style={{ color: '#1890ff', cursor: 'pointer' }} onClick={() => {
+                        dispatch({ type: 'dashboardDesigner/reset' });
+                        dispatch({ type: 'main/redirect', path: '/dashboard/' + record.code });
+                        dispatch({ type: 'recent/addRecentRecord', tarId: record.code, recordType: 1});
+                    }}>
+                        { filterLabel ?
+                            ((text || '').split(new RegExp(`(${filterLabel})`, 'i')).map((fragment, i) => {
+                                return (
+                                    fragment.toLowerCase().replace(new RegExp('(\\\\)', 'g'), '\\$1').replace(reg, '\\$1') === filterLabel.toLowerCase() ?
+                                    <span key={i} style={{fontWeight: 'bold', color: 'red'}} className="highlight">{fragment}</span> :
+                                    fragment
+                                )
+                            }
+                            )) : text
+                        }
+                    </span>
+                )
+            }
+        }, {
+            title: '说明',
+            dataIndex: 'description',
+            key: 'description',
+            width: 200,
+            render: (text, record) => {
+                return (
+                    <span>
+                        { filterLabel ?
+                            ((text || '').split(new RegExp(`(${filterLabel})`, 'i')).map((fragment, i) => {
+                                return (
+                                    fragment.toLowerCase().replace(new RegExp('(\\\\)', 'g'), '\\$1').replace(reg, '\\$1') === filterLabel.toLowerCase() ?
+                                    <span key={i} style={{fontWeight: 'bold', color: 'red'}} className="highlight">{fragment}</span> :
+                                    fragment
+                                )
+                            }
+                            )) : text
+                        }
+                    </span>
+                )
+            }
+        }, {
+            title: '创建人',
+            dataIndex: 'creatorName',
+            key: 'creatorName',
+            width: 100
+        }, {
+            title: '创建时间',
+            dataIndex: 'createTime',
+            key: 'createTime',
+            render: (text, record) => dateFormat(text, 'yyyy-MM-dd hh:mm:ss'),
+            width: 100
+        }, {
+            title: '操作',
+            key: 'action',
+            render: (text, record, index) => (
+                <Dropdown code={record.code} overlay={moreOperatingMenu} trigger={['click']} >
+                    <Icon type="setting" />
+                </Dropdown>
+            ),
+            width: 50
+        }];
 
-    distribute = (group, geren) => {
-        const { dispatch } = this.props;
-        const { selectedRecord } = this.state;
-        let targets = group.map(g => ({
-            code: g.code,
-            name: g.name,
-            isGroup: true
-        })).concat(geren.map(g => ({
-            code: g.code,
-            name: g.name,
-            isGroup: false
-        })))
-        dispatch({ type: 'dashboard/share', code: selectedRecord.code, targets });
-    }
-
-    render() {
-        const { dispatch, dashboard } = this.props;
-        const { visibleDistributeBox, visibleTransferBox, visibleDeleteBox, selectedRecord, defaultSelectedGroups, defaultSelectedUsers } = this.state
         return (
-            <Layout className='dashboard-list'>
+            <Layout className='dashboard-view'>
                 <Content>
-                    <Card title={
-                        <Row className='tools' type='flex' justify='space-between'>
+                    <Card className="dashboard-body" title={
+                        <Row className='dashboard-tools' type='flex' justify='space-between'>
                             <Col style={{ display: 'flex' }}>
                             </Col>
                             <Col className='search'>
@@ -237,9 +250,28 @@ class DashboardList extends React.Component {
                             </Col>
                         </Row>
                     }>
-                        <div className='dashboard-body'>
+                        {/* <div className='dashboard-body'>
                             { this.generateCard() }
-                        </div>
+                        </div> */}
+                        <Table
+                            className='dashboard-table'
+                            columns={dashboardColumns}
+                            dataSource={
+                                this.onSort(
+                                    this.onSearch(dashboard.list, dashboard.filterLabel)
+                                )
+                            }
+                            size='small'
+                            scroll={{x: false, y: true}}
+                            pagination={false}
+                            onRow={(record) => {
+                                return {
+                                    onClick: () => {
+                                        this.setState({ selectedRecord:  record})
+                                    }
+                                }
+                            }}
+                        />
                     </Card>
                 </Content>
                 {visibleDistributeBox && <AccessObjectBox
@@ -275,7 +307,32 @@ class DashboardList extends React.Component {
                     }}
                     okHandler={() => {
                         dispatch({ type: 'dashboard/remoteDelete', code: this.state.selectedRecord.code })
-                    }} />}
+                    }} 
+                />}
+                {visibleShareBox && <ShareBox
+                    visibleBox={visibleShareBox}
+                    shareUrl={shareUrl}
+                    hideBox={() => {
+                        this.setState({
+                            visibleShareBox: false
+                        })
+                    }}
+                    onRefreshKey={(delay) => {
+                        return dispatch({ type: 'dashboard/getShareKey', record: this.state.selectedRecord, delay: delay })
+                        .then((key) => {
+                            this.setState({
+                                shareUrl: window.location.origin + '/#/dashboard/share_key/' + key
+                            })
+                            
+                        })
+                    }}
+                />}
+                {visibleCopyBox && <CopyBox
+                    visibleBox={visibleCopyBox}
+                    hideBox={()=>{this.setState({visibleCopyBox: false})}}
+                    currentDashboardCode={selectedRecord.code}
+                    currentDataConnect={selectedRecord.dataConnects[0]}
+                />}
             </Layout>
         )
     }

+ 115 - 97
src/components/dashboard/list.less

@@ -1,104 +1,97 @@
-.dashboard-list {
-    .ant-card-head {
-        padding: 0 10px;
-        .tools {
-            flex: 1;
-            .anticon-bars {
-                cursor: pointer;
-                line-height: 1.6;
-                font-size: 20px;
-                margin-right: 6px;
-            }
-            .group {
-                line-height: 2.1;
-                .ant-breadcrumb-link {
-                    .ant-tag {
-                        margin: 0;
+.dashboard-view {
+    .dashboard-body {
+        padding: 0;
+        .ant-card-head {
+            padding: 0 10px;
+            .dashboard-tools {
+                flex: 1;
+                .anticon-bars {
+                    cursor: pointer;
+                    line-height: 1.6;
+                    font-size: 20px;
+                    margin-right: 6px;
+                    &> svg {
+                        height: 100%;
                     }
                 }
-            }
-            .search {
-                display: flex;
-                > div:first-child {
-                    margin-right: 5px;
+                .group {
+                    line-height: 2.1;
+                    .ant-breadcrumb-link {
+                        .ant-tag {
+                            margin: 0;
+                        }
+                    }
+                }
+                .search {
+                    display: flex;
+                    > div:first-child {
+                        margin-right: 5px;
+                    }
                 }
-            } 
+            }
         }
     }
     .ant-card-body {
-        padding: 10px;
-        display: flex;
-        flex-wrap: wrap;
-        justify-content: center;
-
-        .dashboard-card {
-            width: 512px;
-            text-align: center;
-            margin: 8px;
-            padding: 0;
-            &>.ant-card {
-                border: 2px solid #CCCCCC;
-                .ant-card-head {
-                    border-bottom: 2px solid #CCCCCC;
-                    min-height: 20px;
-                    cursor: default;
-                    .ant-card-head-title {
-                        font-size: 14px;
-                        padding: 6px 0;
-                        .ant-row-flex {
-                            flex: 1;
-                        }
-                        .anticon {
-                            cursor: pointer;
-                        }
-                    }
-                }
-    
-                .ant-card-cover {
-                    height: 247px;
-                    .cover-body {
-                        padding: 10px;
-                        .thumb {
-                            height: 222px;
-                            cursor: pointer;
-                            canvas {
-                                cursor: pointer;
-                            }
-                            .table-default {
-                                background-image: url(/images/table-default.png);
-                                width: 100%;
-                                height: 100%;
-                                background-position: center;
-                                background-size: contain;
-                                background-repeat: no-repeat;
-                                cursor: pointer;
-                            }
-                            .dashboard-default {
-                                background-image: url(/images/dashboard-default.png);
-                                width: 100%;
-                                height: 100%;
-                                background-position: center;
-                                background-size: contain;
-                                background-repeat: no-repeat;
-                                cursor: pointer;
-                                opacity: .3;
-                            }
-                        }
-                        .desc {
-                            text-align: left;
-                            height: 41px;
-                        }
-                        .footer {
-                            margin-top: 6px;
-                            .anticon {
-                                font-size: 24px;
+        padding: 0;
+        .dashboard-table{
+            .ant-table {
+                .ant-table-scroll {
+                    .ant-table-header {
+                        overflow: hidden;
+                        table {
+                            thead {
+                                th {
+                                    .ant-table-column-sorter, .ant-table-filter-icon {
+                                        opacity: 0;
+                                    }
+                                    :hover {
+                                        .ant-table-column-sorter, .ant-table-filter-icon {
+                                            opacity: 1;
+                                        }
+                                    }
+                                }
+                                .column-filtered {
+                                    .ant-table-filter-icon {
+                                        opacity: 1;
+                                        color: red;
+                                    }
+                                }
                             }
                         }
                     }
-                    .ant-row-flex-bottom {
-                        cursor: default;
-                        .anticon {
-                            cursor: pointer;
+                    .ant-table-body {
+                        margin-top: 17px;
+                        overflow-y: auto !important;
+                        table {
+                            padding: 0;
+                            .ant-table-row {
+                                td {
+                                    padding: 8px;
+                                    .dashboard-name {
+                                        display: flex;
+                                        .dashboard-type {
+                                            margin-right: 5px;
+                                        }
+                                    }
+                                    .dashboard-tag {
+                                        margin: 2px;
+                                        cursor: default;
+                                    }
+                                    .ant-dropdown-trigger {
+                                        font-size: 18px;
+                                        cursor: pointer;
+                                    }
+                                }
+                                .action-col {
+                                    display: flex;
+                                    .operation {
+                                        cursor: pointer;
+                                    }
+                                    .operation:hover {
+                                        color: #40a9ff;
+                                    }
+                                }
+                            }
                         }
                     }
                 }
@@ -161,8 +154,10 @@
 .tree-group li.drag-over > span[draggable] {
     opacity: .5;
 }
-.menu-operation {
+
+.operationmenu {
     padding: 0;
+    width: 120px;
     .ant-dropdown-menu-item {
         .anticon {
             margin-right: 6px;
@@ -177,7 +172,30 @@
         }
     }
 }
-.ant-tooltip-inner {
-    background-color: rgba(255, 255, 255, .9);
-    color: #666
-}
+
+// .ant-table-body::-webkit-scrollbar {/*滚动条整体样式*/
+//     width: 6px;     /*高宽分别对应横竖滚动条的尺寸*/
+//     height: 4px;
+// }
+// .ant-table-body::-webkit-scrollbar-thumb {/*滚动条里面小方块*/
+//     border-radius: 5px;
+//     box-shadow: inset 0 0 5px rgba(0,0,0,0.2);
+//     background: rgba(0,0,0,0.2);
+// }
+// .ant-table-body::-webkit-scrollbar-track {/*滚动条里面轨道*/
+//     box-shadow: inset 0 0 5px rgba(0,0,0,0.2);
+//     border-radius: 0;
+//     background: rgba(0,0,0,0.1);
+// }
+
+.custom-filter-dropdown {
+    padding: 8px;
+    border-radius: 6px;
+    background: #fff;
+    box-shadow: 0 1px 6px rgba(0, 0, 0, .2);
+  }
+  
+  .custom-filter-dropdown input {
+    width: 130px;
+    margin-right: 8px;
+  }

+ 93 - 0
src/components/dashboard/shareBox.jsx

@@ -0,0 +1,93 @@
+import React from 'react'
+import { message, Modal, Form, Input, InputNumber } from 'antd'
+import copy from 'copy-to-clipboard'
+const FormItem = Form.Item
+
+class ShareBox extends React.Component {
+
+    constructor(props) {
+        super(props);
+        this.state = {
+            copyDisabled: false,
+            copyText: '复制链接',
+            delay: 7,
+            dirty: false
+        }
+    }
+
+    render() {
+        const { visibleBox, hideBox, shareUrl, onRefreshKey } = this.props;
+        const { copyDisabled, copyText, delay, dirty } = this.state;
+
+        const formItemLayout = {
+            labelCol: { span: 4 },
+            wrapperCol: { span: 4 },
+        }
+
+        return (
+            <Modal
+                className='share-box'
+                title={'分享报表'}
+                visible={visibleBox}
+                onCancel={hideBox}
+                maskClosable={true}
+                destroyOnClose={true}
+                footer={null}
+            >
+                <Form layout="horizontal" size='small'>
+                    <FormItem>
+                        <Input
+                            disabled
+                            placeholder="分享地址有误,请尝试重新打开此页面"
+                            value={shareUrl}
+                            addonAfter={<span style={{ cursor: copyDisabled ? 'not-allowed' : 'pointer' }} onClick={() => {
+                                if(copyDisabled) {
+                                    return;
+                                }
+                                copy(shareUrl);
+                                this.setState({
+                                    copyDisabled: true,
+                                    copyText: '已复制'
+                                }, () => {
+                                    setTimeout(() => {
+                                        this.setState({
+                                            copyDisabled: false,
+                                            copyText: '复制链接'
+                                        });
+                                    }, 3000)
+                                });
+                            }}>{copyText}</span>}
+                        >
+                        </Input>
+                    </FormItem>
+                    <FormItem label="有效期(天)" {...formItemLayout}>
+                        <InputNumber
+                            min={1}
+                            value={delay}
+                            onChange={(val) => {
+                                this.setState({
+                                    delay: val,
+                                    dirty: true
+                                })
+                            }}
+                            onBlur={() => {
+                                if(!dirty) {
+                                    return;
+                                }
+                                onRefreshKey(delay).then(() => {
+                                    message.success('分享链接已更新');
+                                    this.setState({
+                                        dirty: false
+                                    });
+                                })
+                            }}
+                        >
+                        </InputNumber>
+                    </FormItem>
+                </Form>
+            </Modal>
+        )
+    }
+}
+
+export default ShareBox

+ 10 - 0
src/components/dashboard/shareKeyView.jsx

@@ -0,0 +1,10 @@
+import React from 'react'
+import Layout from '../dashboardDesigner/layout'
+
+class View extends React.Component {
+    render() {
+        return <Layout { ...this.props } isShareKeyView={true} ></Layout>
+    }
+}
+
+export default View

+ 10 - 0
src/components/dashboard/shareView.jsx

@@ -0,0 +1,10 @@
+import React from 'react'
+import Layout from '../dashboardDesigner/layout'
+
+class DashboardShareView extends React.Component {
+    render() {
+        return <Layout { ...this.props } isShareView={true} ></Layout>
+    }
+}
+
+export default DashboardShareView

+ 0 - 184
src/components/dashboardDesigner/configForm.jsx

@@ -1,184 +0,0 @@
-import React from 'react'
-import { connect } from 'dva'
-import { Form, Input, Divider, Button, Icon, Collapse, Spin } from 'antd'
-
-class ConfigForm extends React.Component {
-
-    constructor(props) {
-        super(props);
-        this.state = {
-            selectedDataSource: null,
-            selectedColumn: null,
-            activeKey: [],
-            editing: false
-        };
-    }
-
-
-    addRelationColumn = () => {
-        const { dispatch } = this.props;
-        dispatch({ type: 'dashboardDesigner/addRelationColumn' });
-    }
-
-    deleteRelationColumn = (e) => {
-        const { dispatch } = this.props;
-        const code = e.target.dataset.code;
-        dispatch({ type: 'dashboardDesigner/deleteRelationColumn', code });
-    }
-
-    render() {
-        const { dashboardDesigner, dispatch } = this.props;
-        const { activeKey, editing, selectedDataSource, selectedColumn } = this.state;
-        const { relationColumns, dataSources, columnFetching } = dashboardDesigner;
-
-        return <Form className='config-form'>
-            <Divider>自定义过滤字段</Divider>
-            <div className='filtercolumns'>
-                <Collapse key='filtercolumnscollapse' activeKey={activeKey} onChange={k => {
-                    this.setState({
-                        selectedDataSource: null,
-                        selectedColumn: null,
-                        activeKey: k[k.length - 1], // 保持只有一个展开项
-                    });
-                }}>
-                    {
-                        relationColumns.map((r, ri) => (
-                            <Collapse.Panel
-                                key={r.code}
-                                disabled={editing}
-                                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 } });
-                                    }} onFocus={() => {
-                                        this.setState({
-                                            editing: true,
-                                        })
-                                    }} onBlur={() => {
-                                        this.setState({
-                                            editing: false
-                                        })
-                                    }}/>
-                                </Form.Item>}
-                            >
-                                {(dataSources.length > 0 ? <div className='filtercolumn-relation'>
-                                    {columnFetching && <div className='loading'>
-                                        <Spin />
-                                    </div>}
-                                    <div className='datasources'>
-                                        { dataSources.map((d, di) => (
-                                            <Button
-                                                className='datasource'
-                                                key={d.code}
-                                                type={!!selectedDataSource && selectedDataSource.code === d.code ? 'primary' : 'default'}
-                                                onClick={() => {
-                                                    this.setState({
-                                                        selectedDataSource: {
-                                                            code: d.code,
-                                                            name: d.name
-                                                        },
-                                                        selectedColumn: null
-                                                    });
-                                                    dispatch({ type: 'dashboardDesigner/remoteGetColumns', dataSourceCode: d.code });
-                                                }}
-                                            >
-                                                <span className='label'>{d.name}</span>
-                                                {r.relations.findIndex(r => r.dataSource.code === d.code) !== -1 && <Icon type='check-circle' theme={r.relations[0].dataSource.code === d.code ? 'filled' : 'outlined'} />}
-                                            </Button>
-                                        )) }
-                                    </div>
-                                    <div className='columns'>
-                                        { !!selectedDataSource &&
-                                            !!dataSources.find(d => d.code === selectedDataSource.code) &&
-                                            !!dataSources.find(d => d.code === selectedDataSource.code).columns &&
-                                            dataSources.find(d => d.code === selectedDataSource.code).columns.filter(c => {
-                                                if(!!r.relations[0]) {
-                                                    // 选中的不是第一个
-                                                    if(r.relations[0].dataSource.code !== selectedDataSource.code) {
-                                                        // 只能选用同类型的列
-                                                        return c.type === r.relations[0].column.type;
-                                                    }else {
-                                                        return true;
-                                                    }
-                                                }else {
-                                                    return true;
-                                                }
-                                            }).map(c => (
-                                            <Button
-                                                className='column'
-                                                type={!!selectedColumn && selectedColumn.name === c.name ? 'primary' : 'default'}
-                                                key={c.name}
-                                                onClick={() => {
-                                                    this.setState({
-                                                        selectedColumn: {
-                                                            name: c.name,
-                                                            label: c.label
-                                                        }
-                                                    }, () => {
-                                                        let { selectedColumn } = this.state;
-                                                        const { relations } = r;
-                                                        let idx = relations.findIndex(r => r.dataSource.code === selectedDataSource.code);
-                                                        if(idx === -1){
-                                                            relations.push({
-                                                                dataSource: {
-                                                                    code: selectedDataSource.code,
-                                                                    name: selectedDataSource.name
-                                                                },
-                                                                column: {
-                                                                    label: c.label,
-                                                                    name: c.name,
-                                                                    type: c.type
-                                                                }
-                                                            });
-                                                        }else {
-                                                            let cr = relations[idx];
-                                                            if(cr.column.name === selectedColumn.name) {
-                                                                relations.splice(idx, 1);
-                                                            }else {
-                                                                relations[idx] = {
-                                                                    dataSource: {
-                                                                        code: selectedDataSource.code,
-                                                                        name: selectedDataSource.name
-                                                                    },
-                                                                    column: {
-                                                                        code: c.code,
-                                                                        name: c.name,
-                                                                        type: c.type
-                                                                    }
-                                                                };
-                                                            }
-                                                        }
-                                                        let index = relationColumns.findIndex(rc => rc.code === r.code);
-                                                        relationColumns[index] = { ...r, relations };
-                                                        dispatch({ type: 'dashboardDesigner/setField', name: 'relationColumns', value: relationColumns });
-                                                    });
-                                                }}
-                                            >
-                                                <span className='label'>{c.label}</span>
-                                                {r.relations.findIndex(r => r.dataSource.code === selectedDataSource.code) !== -1 && r.relations[r.relations.findIndex(r => r.dataSource.code === selectedDataSource.code)].column.name === c.name && <Icon type='check-circle' theme={r.relations[0].column.name === c.name ? 'filled' : 'outlined'} />}
-                                            </Button>
-                                        )) }
-                                    </div>
-                                </div> : <div className='filtercolumn-empty'>
-                                    无关联数据源
-                                </div>)}
-                                <div className='filtercolumn-delete'>
-                                    <Button type='danger' className='delbtn' data-code={r.code} onClick={this.deleteRelationColumn}>
-                                        <Icon type='delete' theme='outlined' />删除
-                                    </Button>
-                                </div>
-                            </Collapse.Panel>
-                        ))
-                    }
-                </Collapse>
-                <div className='bottom-btns'>
-                    <Button className='addbtn' onClick={this.addRelationColumn}>
-                        <Icon type='plus' theme='outlined' />添加
-                    </Button>
-                </div>
-            </div>
-        </Form>
-    }
-}
-
-export default connect(({ present: { dashboardDesigner } }) => ({ dashboardDesigner }))(ConfigForm);

+ 122 - 0
src/components/dashboardDesigner/configSider.jsx

@@ -0,0 +1,122 @@
+import React from 'react'
+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
+
+class ConfigSider extends React.Component {
+
+    constructor(props) {
+        super(props);
+        this.state = {
+            visibleChooseChartBox: false,
+            visibleCusFilterBox: false,
+            copyDisabled: false,
+            copyText: '生成链接'
+        };
+    }
+
+    showChooseChartBox = (o) => {
+        this.setState({
+            visibleChooseChartBox: true
+        });
+    }
+
+    hideChooseChartBox = (o) => {
+        this.setState({
+            visibleChooseChartBox: false
+        });
+    }
+
+    showCusFilterBox = (o) => {
+        this.setState({
+            visibleCusFilterBox: true
+        });
+    }
+
+    hideCusFilterBox = (o) => {
+        this.setState({
+            visibleCusFilterBox: false
+        });
+    }
+
+    generateViewTypes = () => {
+        const { dispatch } = this.props;
+        const { visibleChooseChartBox } = this.state;
+
+        return (<div className='view-types'>
+            <Tooltip placement='bottom' title="图表">
+                <div className="view-type-item">
+                    <Icon className='viewtype-icon' type="area-chart" theme="outlined" onClick={(item) => {
+                        this.showChooseChartBox("create");
+                    }}/>
+                </div>
+            </Tooltip >
+            <Tooltip placement='bottom' title="富文本">
+                <div className="view-type-item">
+                    <Icon className='viewtype-icon' type="book" theme="outlined" onClick={() => {
+                        dispatch({ type: 'dashboardDesigner/addRichText' });
+                    }}/>
+                </div>
+            </Tooltip>
+            {visibleChooseChartBox && <ChooseChartBox visibleBox={visibleChooseChartBox} hideBox={this.hideChooseChartBox} />}
+        </div>)
+    }
+
+    render() {
+        const { dashboardDesigner, dispatch } = this.props;
+        const { visibleCusFilterBox, copyDisabled, copyText } = this.state;
+
+        return <Form className='config-sider' layout={'vertical'}>
+            <Divider>报表制作</Divider>
+            {this.generateViewTypes()}
+            <Divider>字段过滤</Divider>
+            <Button className="cus-filter-button" onClick={this.showCusFilterBox}>
+                <Icon type='bulb' theme='outlined' />自定义过滤字段
+            </Button>
+            {visibleCusFilterBox && <CusFilterBox visibleBox={visibleCusFilterBox} hideBox={this.hideCusFilterBox} />}
+            <Divider>其他设置</Divider>
+            <FormItem label='说明'>
+                <Input.TextArea
+                    autosize={{ minRows: 2, maxRows: 6 }}
+                    value={dashboardDesigner.description}
+                    onChange={(e) => {
+                        dispatch({ type: 'dashboardDesigner/setField', name: 'description', value: e.target.value });
+                    }}
+                />
+            </FormItem>
+            <FormItem label='分享码'>
+                <Input
+                    value={dashboardDesigner.shareCode}
+                    onChange={(e) => {
+                        dispatch({ type: 'dashboardDesigner/setField', name: 'shareCode', value: e.target.value });
+                    }}
+                    addonAfter={<span style={{ cursor: (copyDisabled || !dashboardDesigner.shareCode) ? 'not-allowed' : 'pointer' }} onClick={() => {
+                        if(copyDisabled || !dashboardDesigner.shareCode) {
+                            return;
+                        }
+                        dispatch({ type: 'dashboardDesigner/encryptCode', shareCode: dashboardDesigner.shareCode }).then(key => {
+                            copy(window.location.origin + '/#/dashboard/share/' + key);
+                            this.setState({
+                                copyDisabled: true,
+                                copyText: '已复制到剪贴板'
+                            }, () => {
+                                setTimeout(() => {
+                                    this.setState({
+                                        copyDisabled: false,
+                                        copyText: '生成链接'
+                                    });
+                                }, 3000)
+                            });
+                        });
+                    }}>{copyText}</span>}
+                />
+            </FormItem>
+        </Form>
+    }
+}
+
+export default connect(({ present: { dashboardDesigner } }) => ({ dashboardDesigner }))(ConfigSider);

+ 23 - 0
src/components/dashboardDesigner/configSider.less

@@ -0,0 +1,23 @@
+.config-sider {
+    .ant-divider {
+        margin: 10px 0;
+    }
+    .view-types {
+        display: flex;
+        .view-type-item {
+            width: 40px;
+            height: 40px;
+            border: 1px solid #1890ff;
+            border-radius: 4px;
+            margin-left: 8px;
+            i {
+                font-size: 32px;
+                color: #1890ff;
+                padding: 4px;
+            }
+        }
+    }
+    .cus-filter-button {
+        width: 100%;
+    }
+}

+ 27 - 55
src/components/dashboardDesigner/content.jsx

@@ -1,11 +1,11 @@
 import React from 'react'
-import { Layout, Tooltip, Tag, Icon } from 'antd'
+import { Layout, Tag, Icon } from 'antd'
 import { connect } from 'dva'
 import ViewLayout from './viewLayout'
-import ChooseChartBox from './chooseChartBox'
-import FilterBox from '../common/filterBox/filterBox'
-import ConfigForm from './configForm'
+import FilterBox from '../common/filterBox/filterBox2'
+import ConfigSider from './configSider'
 import moment from 'moment'
+
 const { Header, Content, Sider } = Layout
 
 class DashboardDesignerContent extends React.Component {
@@ -17,7 +17,6 @@ class DashboardDesignerContent extends React.Component {
                 width: 0,
                 height: 0,
             },
-            visibleChooseChartBox: false,
             visibleFilterBox: false,
         };
     }
@@ -32,18 +31,6 @@ class DashboardDesignerContent extends React.Component {
         window.removeEventListener('resize', this.onWindowResize);
     }
 
-    showBox = (o) => {
-        this.setState({
-            visibleChooseChartBox: true
-        });
-    }
-
-    hideBox = (o) => {
-        this.setState({
-            visibleChooseChartBox: false
-        });
-    }
-
     showFilterBox = (e) => {
         this.setState({
             visibleFilterBox: true
@@ -66,7 +53,7 @@ class DashboardDesignerContent extends React.Component {
     }
 
     getContentSize = () => {
-        const { dashboardDesigner, isOwner } = this.props;
+        const { dashboardDesigner, isOwner, isShareView, isShareKeyView } = this.props;
         const { editMode } = dashboardDesigner;
         let contentEl = document.getElementsByClassName('viewlayout')[0];
         if(!contentEl) {
@@ -78,20 +65,22 @@ class DashboardDesignerContent extends React.Component {
         let _scroll = contentEl.scrollHeight > contentEl.clientHeight;
 
         return {
-            width: document.body.offsetWidth - 20 - (isOwner && editMode ? 380 : 0) - 10 - (_scroll ? 17 : 2), // 有滚动条时需要减去滚动条的宽度
+            width: document.body.offsetWidth - 20 - ((isOwner && editMode && !isShareView && !isShareKeyView) ? 200 : 0) - 10 - (_scroll ? 17 : 2), // 有滚动条时需要减去滚动条的宽度
             height: contentEl.clientHeight
         }
     }
 
-    getRelationFilterColumns = () => {
-        const { dashboardDesigner } = this.props;
-        const { relationColumns } = dashboardDesigner;
-        return relationColumns.filter(r => r.relations.length > 0).map(r => ({
-            name: r.code,
-            label: r.name,
-            type: r.relations[0].column.type
-        }));
-    }
+    // 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;
@@ -147,18 +136,14 @@ 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, dispatch, isOwner } = this.props;
-        const { code, editMode, filters } = dashboardDesigner;
-        const { visibleChooseChartBox, visibleFilterBox } = this.state;
+        const { dashboardDesigner, isOwner, isShareView, isShareKeyView } = this.props;
+        const { dataSources, editMode, filters, relationColumns } = dashboardDesigner;
+        const { visibleFilterBox } = this.state;
 
         const contentSize = this.getContentSize();
         let tags = filters.map((f, i)=>{
@@ -191,29 +176,16 @@ 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} />}
-                {isOwner && editMode && <div className='viewtype'>
-                    <Tooltip placement='bottom' title="图表">
-                        <Icon className='viewtype-icon' type="area-chart" theme="outlined" onClick={(item) => {
-                            this.showBox("create");
-                        }}/>
-                    </Tooltip >
-                    <Tooltip placement='bottom' title="富文本">
-                        <Icon className='viewtype-icon' type="book" theme="outlined" onClick={() => {
-                            dispatch({ type: 'dashboardDesigner/addRichText' });
-                        }}/>
-                    </Tooltip>
-                    {visibleChooseChartBox && <ChooseChartBox visibleBox={visibleChooseChartBox} hideBox={this.hideBox} />}
-                </div>}
+                {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'>
+                    <Sider className={`config-sider${ (isOwner && editMode) ? '' : ' config-sider-closed' }`} width={(isOwner && editMode && !isShareView && !isShareKeyView) ? 200 : 0}>
+                        <ConfigSider/>
+                    </Sider>
                     <Content className='viewlayout'>
-                        <ViewLayout isOwner={isOwner} contentSize={contentSize} editMode={editMode}/>
+                        <ViewLayout isOwner={isOwner} isShareView={isShareView} isShareKeyView={isShareKeyView} contentSize={contentSize} editMode={editMode}/>
                     </Content>
-                    <Sider className='config-sider' width={(isOwner && editMode) ? 380 : 0}>
-                        <ConfigForm/>
-                    </Sider>
                 </Layout>
             </Content>
         </Layout>

+ 215 - 0
src/components/dashboardDesigner/cusFilterBox.jsx

@@ -0,0 +1,215 @@
+import React from 'react'
+import { connect } from 'dva'
+import { Modal, Input, Form, Button, Icon, Collapse, Spin } from 'antd'
+import './cusFilterBox.less'
+
+class CusFilterBox extends React.Component {
+
+    constructor(props) {
+        super(props);
+        this.state = {
+        };
+    }
+
+    addRelationColumn = () => {
+        const { dispatch } = this.props;
+        dispatch({ type: 'dashboardDesigner/addRelationColumn' });
+    }
+
+    deleteRelationColumn = (e) => {
+        const { dispatch } = this.props;
+        const code = e.target.dataset.code;
+        dispatch({ type: 'dashboardDesigner/deleteRelationColumn', code });
+    }
+
+    generateRelations = () => {
+        const { dashboardDesigner, dispatch } = this.props;
+        const { editing, selectedDataSource } = this.state;
+        const { relationColumns, dataSources, columnFetching } = dashboardDesigner;
+
+        return relationColumns.map((r, ri) => (
+            <Collapse.Panel
+                key={r.code}
+                disabled={editing}
+                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, cusName: true } });
+                    }} onFocus={() => {
+                        this.setState({
+                            editing: true,
+                        })
+                    }} onBlur={() => {
+                        this.setState({
+                            editing: false
+                        })
+                    }}/>
+                </Form.Item>}
+            >
+                {(dataSources.length > 0 ? <div className='filtercolumn-relation'>
+                    {columnFetching && <div className='loading'>
+                        <Spin />
+                    </div>}
+                    <div className='datasources'>
+                        { dataSources.map((d, di) => (
+                            <Button
+                                className='datasource'
+                                key={d.code}
+                                type={!!selectedDataSource && selectedDataSource.code === d.code ? 'primary' : 'default'}
+                                onClick={() => {
+                                    this.setState({
+                                        selectedDataSource: {
+                                            code: d.code,
+                                            name: d.name
+                                        },
+                                        selectedColumn: null
+                                    });
+                                    dispatch({ type: 'dashboardDesigner/remoteGetColumns', dataSourceCode: d.code });
+                                }}
+                            >
+                                <span className='label'>{d.name}</span>
+                                {r.relations.findIndex(r => r.dataSource.code === d.code) !== -1 && <Icon type='check-circle' theme={r.relations[0].dataSource.code === d.code ? 'filled' : 'outlined'} />}
+                            </Button>
+                        )) }
+                    </div>
+                    <div className='columns'>
+                        { this.generateRelationColumns(r) }
+                    </div>
+                </div> : <div className='filtercolumn-empty'>
+                    无关联数据源
+                </div>)}
+                <div className='filtercolumn-delete'>
+                    <Button type='danger' className='delbtn' data-code={r.code} onClick={this.deleteRelationColumn}>
+                        <Icon type='delete' theme='outlined' />删除
+                    </Button>
+                </div>
+            </Collapse.Panel>
+        ))
+    }
+
+    generateRelationColumns = (r) => {
+        const { dashboardDesigner } = this.props;
+        const { selectedDataSource, selectedColumn } = this.state;
+        const { dataSources } = dashboardDesigner;
+
+        let flag = !!selectedDataSource && !!dataSources.find(d => d.code === selectedDataSource.code) && !!dataSources.find(d => d.code === selectedDataSource.code).columns;
+        if(!flag) {
+            return null;
+        }
+        let arr = dataSources.find(d => d.code === selectedDataSource.code).columns.filter(c => {
+            if(!!r.relations[0]) {
+                // 选中的不是第一个
+                if(r.relations[0].dataSource.code !== selectedDataSource.code) {
+                    // 只能选用同类型的列
+                    return c.type === r.relations[0].column.type;
+                }else {
+                    return true;
+                }
+            }else {
+                return true;
+            }
+        });
+
+        return arr.map(c => (
+            <Button
+                className='column'
+                type={!!selectedColumn && selectedColumn.name === c.name ? 'primary' : 'default'}
+                key={c.name}
+                onClick={() => {
+                    this.relationColumnClick(c, r)
+                }}
+            >
+                <span className='label'>{c.label}</span>
+                {r.relations.findIndex(r => r.dataSource.code === selectedDataSource.code) !== -1 && r.relations[r.relations.findIndex(r => r.dataSource.code === selectedDataSource.code)].column.name === c.name && <Icon type='check-circle' theme={r.relations[0].column.name === c.name ? 'filled' : 'outlined'} />}
+            </Button>
+        ))
+    }
+
+    relationColumnClick = (c, r) => {
+        const { dashboardDesigner, dispatch } = this.props;
+        const { selectedDataSource } = this.state;
+        const { relationColumns } = dashboardDesigner;
+
+        this.setState({
+            selectedColumn: {
+                name: c.name,
+                label: c.label
+            }
+        }, () => {
+            let { selectedColumn } = this.state;
+            const { relations } = r;
+            let idx = relations.findIndex(r => r.dataSource.code === selectedDataSource.code);
+            if(idx === -1){
+                relations.push({
+                    dataSource: {
+                        code: selectedDataSource.code,
+                        name: selectedDataSource.name
+                    },
+                    column: {
+                        name: c.name,
+                        label: c.label,
+                        type: c.type
+                    }
+                });
+            }else {
+                let cr = relations[idx];
+                if(cr.column.name === selectedColumn.name) {
+                    relations.splice(idx, 1);
+                }else {
+                    relations[idx] = {
+                        dataSource: {
+                            code: selectedDataSource.code,
+                            name: selectedDataSource.name
+                        },
+                        column: {
+                            name: c.name,
+                            label: c.label,
+                            type: c.type
+                        }
+                    };
+                }
+            }
+            let index = relationColumns.findIndex(rc => rc.code === r.code);
+            relationColumns[index] = { ...r, name: relationColumns[index].cusName ? relationColumns[index].name : c.label, cusName: true, relations };
+            dispatch({ type: 'dashboardDesigner/setField', name: 'relationColumns', value: relationColumns });
+        });
+    }
+
+    render() {
+        const { visibleBox, hideBox } = this.props;
+        const { activeKey } = this.state;
+
+        return <Modal
+            className='cusfilter-box'
+            width={380}
+            height='80%'
+            title='自定义过滤条件'
+            visible={visibleBox}
+            footer={null}
+            onCancel={hideBox}
+            maskClosable={false}
+            destroyOnClose={true}
+        >
+            <Form className='config-form'>
+                <div className='filtercolumns'>
+                    <Collapse key='filtercolumnscollapse' activeKey={activeKey} onChange={k => {
+                        this.setState({
+                            selectedDataSource: null,
+                            selectedColumn: null,
+                            activeKey: k[k.length - 1], // 保持只有一个展开项
+                        });
+                    }}>
+                        {this.generateRelations()}
+                    </Collapse>
+                    <div className='bottom-btns'>
+                        <Button className='addbtn' onClick={this.addRelationColumn}>
+                            <Icon type='plus' theme='outlined' />添加
+                        </Button>
+                    </div>
+                </div>
+            </Form>
+        </Modal>
+    }
+}
+
+export default connect(({ present: { dashboardDesigner } }) => ({ dashboardDesigner }))(CusFilterBox);

+ 134 - 0
src/components/dashboardDesigner/cusFilterBox.less

@@ -0,0 +1,134 @@
+.cusfilter-box {
+    top: 50px;
+    .ant-modal-body {
+        padding: 10px;
+        max-height: 80vh;
+        overflow-y: auto;
+        .config-form {
+            .ant-divider {
+                margin: 10px 0;
+            }
+            .ant-form-item-label {
+                text-align: left;
+            }
+            .filtercolumns {
+                &>.ant-collapse {
+                    &>.ant-collapse-item {
+                        &>.ant-collapse-header {
+                            padding: 0 10px 0 40px;
+                            .ant-form-item {
+                                .ant-form-item-label {
+                                    label {
+                                        cursor: pointer;
+                                    }
+                                }
+                            }
+                        }
+                        .ant-collapse-content {
+                            .ant-collapse-content-box {
+                                padding: 0;
+                                .filtercolumn-empty {
+                                    margin: 5px 5px 0 5px;
+                                    height: 40px;
+                                    line-height: 40px;
+                                    text-align: center;
+                                    font-size: 16px;
+                                    color: red;
+                                    background-color: #d4d7dc;
+                                }
+                                .filtercolumn-relation {
+                                    display: flex;
+                                    max-height: 50vh;
+                                    .loading {
+                                        position: absolute;
+                                        z-index: 1;
+                                        right: 20px;
+                                        margin-top: 10px;
+                                    }
+                                    .datasources {
+                                        padding-bottom: 5px;
+                                        flex: 0.4;
+                                        display: flex;
+                                        flex-direction: column;
+                                        overflow: auto;
+                                        border-right: 1px solid #cccccc;
+                                        border-bottom: 1px solid #cccccc;
+                                        .datasource {
+                                            padding-right: 10px;
+                                            margin: 5px 5px 0 5px;
+                                            flex: none;
+                                            display: flex;
+                                            justify-content: space-between;
+                                            .label {
+                                                overflow: hidden;
+                                                text-overflow: ellipsis;
+                                            }
+                                            .anticon {
+                                                margin-top: 3px;
+                                            }
+                                            &[class="ant-btn-primary"] {
+                                                background-color: #40A9FF;
+                                            }
+                                        }
+                                    }
+                                    .columns {
+                                        padding-bottom: 5px;
+                                        flex: 0.6;
+                                        display: flex;
+                                        flex-direction: column;
+                                        overflow: auto;
+                                        border-bottom: 1px solid #cccccc;
+                                        .column {
+                                            margin: 5px 5px 0 5px;
+                                            padding-right: 10px;
+                                            flex: none;
+                                            display: flex;
+                                            justify-content: space-between;
+                                            .label {
+                                                text-overflow: ellipsis;
+                                                overflow: hidden;
+                                            }
+                                            .anticon {
+                                                margin-top: 3px;
+                                            }
+                                            &[class="ant-btn-primary"] {
+                                                background-color: #40A9FF;
+                                            }
+                                        }
+                                    }
+                                }
+                                .filtercolumn-delete {
+                                    padding: 5px;
+                                    .delbtn {
+                                        width: 100%;
+                                    }
+                                }
+                            }
+                        }
+                    }
+                    .ant-collapse-item-disabled {
+                        &>.ant-collapse-header {
+                            cursor: pointer;
+                            color: rgba(0, 0, 0, 0.85);
+                            &>.arrow {
+                                cursor: pointer;
+                                color: rgba(0, 0, 0, 0.85);
+                            }
+                        }
+                    }
+        
+                }
+                .bottom-btns {
+                    margin-top: 10px;
+                    height: 40px;
+                    .addbtn {
+                        height: 100%;
+                        width: 100%;
+                        border-style: dashed;
+                        border-width: 1px;
+                    }
+                }
+            }
+        }
+    }
+}

+ 12 - 10
src/components/dashboardDesigner/header.jsx

@@ -36,15 +36,18 @@ class Header extends React.Component {
                         visible={this.state.visibleConfirm}
                         onVisibleChange={this.handleVisibleChange}
                         onConfirm={async () => {
+                            let url;
                             this.setState({
                                 visibleConfirm: false
                             });
-                            try {
-                                await dispatch({ type: 'dashboardDesigner/saveWithThumbnail' });
-                            }catch(e){
-                                return e
-                            }                            
-                            dispatch({ type: 'main/redirect', path: '/dashboard' });
+                            if(dashboardDesigner.code && dashboardDesigner.code !== -1) {
+                                url = 'dashboard/remoteModify'
+                            }else {
+                                url = 'dashboard/remoteAdd'
+                            }
+                            dispatch({ type: url }).then(() => {
+                                dispatch({ type: 'main/redirect', path: '/dashboard' });
+                            });
                         }}
                         onCancel={() => {
                             this.setState({
@@ -56,24 +59,23 @@ class Header extends React.Component {
                         cancelText="不保存"
                     >
                         <Button onClick={() => {
-                            console.log(dashboardDesigner.dirty);
                             if(!dashboardDesigner.dirty) {
-                                dispatch({ type: 'dashboardDesigner/reset' });
                                 dispatch({ type: 'main/goBack', path: '/dashboard' });
+                                dispatch({ type: 'dashboardDesigner/reset' });
                             }
                         }}>
                             <Icon type='left' />返回
                         </Button>
                     </Popconfirm>}
                     {!this.isOwner() && <Button onClick={(e) => {
-                        dispatch({ type: 'dashboardDesigner/reset' });
                         dispatch({ type: 'main/goBack', path: '/chart' });
+                        dispatch({ type: 'dashboardDesigner/reset' });
                     }}>
                         <Icon type='left' />返回
                     </Button>}
                     {this.isOwner() && <Button onClick={() => {
                         if(dashboardDesigner.code && dashboardDesigner.code !== -1) {
-                            dispatch({ type: 'dashboardDesigner/saveWithThumbnail' });
+                            dispatch({ type: 'dashboard/remoteModify' });
                         }else {
                             dispatch({ type: 'dashboard/remoteAdd' });
                         }

+ 13 - 8
src/components/dashboardDesigner/header.less

@@ -6,14 +6,19 @@
         flex: 1;
         text-align: center;
         .input-title {
-            text-align: center;
-            font-size: 18px;
-            color: rgba(0, 0, 0, 0.65);
-            cursor: text;
-            border: none;
-            border-bottom-right-radius: 4px;
-            border-top-right-radius: 4px;
-            background-color: transparent;
+            width: 250px;
+            .ant-input-wrapper {
+                input {
+                    text-align: center;
+                    font-size: 18px;
+                    color: rgba(0, 0, 0, 0.65);
+                    cursor: text;
+                    border: none;
+                    border-bottom-right-radius: 4px;
+                    border-top-right-radius: 4px;
+                    background-color: transparent;
+                }
+            }
         }
         .ant-input-group-addon {
             cursor: pointer;

+ 8 - 6
src/components/dashboardDesigner/layout.jsx

@@ -17,11 +17,13 @@ class DashboardDesigner extends React.Component {
     }
 
     componentDidMount() {
-        const { dispatch } = this.props;
+        const { dispatch, isShareView, isShareKeyView } = this.props;
         const { code } = this.props.match.params;
+        let url;
         if (code !== 'create') {
-            dispatch({ type: 'dashboard/remoteDetail', code: code });
+            url = isShareView ? 'dashboard/remoteShareDetail' : ( isShareKeyView ? 'dashboard/remoteShareKeyDetail' : 'dashboard/remoteDetail');
         }
+        dispatch({ type: url, code: code });
     }
 
 
@@ -47,14 +49,14 @@ class DashboardDesigner extends React.Component {
     }
 
     render() {
-        const { dashboardDesigner } = this.props;
+        const { dashboardDesigner, isShareView, isShareKeyView } = this.props;
         const { loading } = dashboardDesigner;
         return <Layout className='dashboarddesigner-layout'>
-            <Header>
+            {!isShareView && !isShareKeyView && <Header>
                 <DashboardDesignerHeader updateThumbnail={this.updateThumbnail} />
-            </Header>
+            </Header>}
             <Content style={{ height: 0 }}>
-                <DashboardDesignerContent isOwner={this.isOwner()} />
+                <DashboardDesignerContent isOwner={this.isOwner()} isShareView={isShareView} isShareKeyView={isShareKeyView}/>
             </Content>
             <div style={{ display: loading ? 'block' : 'none', position: 'absolute', height: '100%', width: '100%', zIndex: '4', background: 'rgba(51,51,51,.1)' }}>
                 <Spin style={{ display: 'inline-block', position: 'absolute', top: '50%', left: '50%', margin: '-10px' }} indicator={<Icon type="loading" style={{ fontSize: 24 }} spin />} />

+ 10 - 148
src/components/dashboardDesigner/layout.less

@@ -24,141 +24,23 @@
                     height: 100%;
                     &>.viewlayout {
                         padding: 5px 5px 50px 5px;
-                        overflow: auto;
+                        overflow-x: hidden;
                         border: 1px solid #CCCCCC;
                     }
                     .config-sider {
                         border: none;
                         .ant-layout-sider-children {
-                            margin: 0 0 0 10px;
+                            margin: 0 10px 0 0;
                             padding: 5px;
                             overflow: auto;
                             border: 1px solid #CCCCCC;
-                            .config-form {
-                                .ant-divider {
-                                    margin: 10px 0;
-                                }
-                                .ant-form-item-label {
-                                    text-align: left;
-                                }
-                                .filtercolumns {
-                                    &>.ant-collapse {
-                                        &>.ant-collapse-item {
-                                            &>.ant-collapse-header {
-                                                padding: 0 10px 0 40px;
-                                                .ant-form-item {
-                                                    .ant-form-item-label {
-                                                        label {
-                                                            cursor: pointer;
-                                                        }
-                                                    }
-                                                }
-                                            }
-                                            .ant-collapse-content {
-                                                .ant-collapse-content-box {
-                                                    padding: 0;
-                                                    .filtercolumn-empty {
-                                                        margin: 5px 5px 0 5px;
-                                                        height: 40px;
-                                                        line-height: 40px;
-                                                        text-align: center;
-                                                        font-size: 16px;
-                                                        color: red;
-                                                        background-color: #d4d7dc;
-                                                    }
-                                                    .filtercolumn-relation {
-                                                        display: flex;
-                                                        max-height: 50vh;
-                                                        .loading {
-                                                            position: absolute;
-                                                            z-index: 1;
-                                                            right: 20px;
-                                                            margin-top: 10px;
-                                                        }
-                                                        .datasources {
-                                                            padding-bottom: 5px;
-                                                            flex: 0.4;
-                                                            display: flex;
-                                                            flex-direction: column;
-                                                            overflow: auto;
-                                                            border-right: 1px solid #cccccc;
-                                                            border-bottom: 1px solid #cccccc;
-                                                            .datasource {
-                                                                padding-right: 10px;
-                                                                margin: 5px 5px 0 5px;
-                                                                flex: none;
-                                                                display: flex;
-                                                                justify-content: space-between;
-                                                                .label {
-                                                                    overflow: hidden;
-                                                                    text-overflow: ellipsis;
-                                                                }
-                                                                .anticon {
-                                                                    margin-top: 3px;
-                                                                }
-                                                                &[class="ant-btn-primary"] {
-                                                                    background-color: #40A9FF;
-                                                                }
-                                                            }
-                                                        }
-                                                        .columns {
-                                                            padding-bottom: 5px;
-                                                            flex: 0.6;
-                                                            display: flex;
-                                                            flex-direction: column;
-                                                            overflow: auto;
-                                                            border-bottom: 1px solid #cccccc;
-                                                            .column {
-                                                                margin: 5px 5px 0 5px;
-                                                                padding-right: 10px;
-                                                                flex: none;
-                                                                display: flex;
-                                                                justify-content: space-between;
-                                                                .label {
-                                                                    text-overflow: ellipsis;
-                                                                    overflow: hidden;
-                                                                }
-                                                                .anticon {
-                                                                    margin-top: 3px;
-                                                                }
-                                                                &[class="ant-btn-primary"] {
-                                                                    background-color: #40A9FF;
-                                                                }
-                                                            }
-                                                        }
-                                                    }
-                                                    .filtercolumn-delete {
-                                                        padding: 5px;
-                                                        .delbtn {
-                                                            width: 100%;
-                                                        }
-                                                    }
-                                                }
-                                            }
-                                        }
-                                        .ant-collapse-item-disabled {
-                                            &>.ant-collapse-header {
-                                                cursor: pointer;
-                                                color: rgba(0, 0, 0, 0.85);
-                                                &>.arrow {
-                                                    cursor: pointer;
-                                                    color: rgba(0, 0, 0, 0.85);
-                                                }
-                                            }
-                                        }
-
-                                    }
-                                    .bottom-btns {
-                                        margin-top: 10px;
-                                        height: 40px;
-                                        .addbtn {
-                                            height: 100%;
-                                            width: 100%;
-                                            border-style: dashed;
-                                            border-width: 1px;
-                                        }
-                                    }
-                                }
+                        }
+                        &-closed {
+                            .ant-layout-sider-children {
+                                margin: 0;
+                                padding: 0;
+                                overflow: hidden;
+                                border: none;
                             }
                         }
                     }
@@ -181,7 +63,7 @@
                         overflow: hidden;
                         text-overflow: ellipsis;
                         border-style: dashed;
-                        margin: 8px 2px 0 2px;
+                        margin: 8px 2px 8px 2px;
                     }
                     .filter-tag-using {
                         border-style: solid;
@@ -194,26 +76,6 @@
                         border-style: solid;
                     }
                 }
-                .viewtype {
-                    display: flex;
-                    align-items: center;
-                    .viewtype-icon {
-                        font-size: 20px;
-                        cursor: pointer;
-                        margin-left: 5px;
-                        height: 30px;
-                        width: 30px;
-                        border: 1px solid black;
-                        border-radius: 4px;
-                        padding: 4px;
-                        border: 1px solid #1890FF;
-                        color: #1890FF;
-                        &:hover {
-                            background-color: #1890FF;
-                            color: white;
-                        }
-                    }
-                }
             }
         }
     }

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

@@ -83,7 +83,7 @@ class ViewLayout extends React.PureComponent {
     }
 
     render() {
-        const { isOwner, dashboardDesigner, contentSize } = this.props;
+        const { isOwner, isShareView, isShareKeyView, dashboardDesigner, contentSize } = this.props;
         const { editMode } = dashboardDesigner;
         const { visiblePreviewBox, previewItem } = this.state;
         const children = dashboardDesigner.items.map((item) => this.createElement(item, false, !item.chartOption));
@@ -105,7 +105,7 @@ class ViewLayout extends React.PureComponent {
                 {(children.length === 0) ? <div key='default-chartview' className='default-chartview' data-grid={{ x: 0, y: 0, w: 12, h: 2, minW: 12, maxW: 12, minH: 2, maxH: 2, static: true }}>
                     <div className='tip'>
                         <Icon type="message" theme="outlined" />
-                        {isOwner ? <span>请在工具栏右侧选择图表或富文本类型添加到看板</span> : <span>未添加图表</span>}
+                        {(isOwner && !isShareView && !isShareKeyView) ? <span>请从左侧选择图表/富文本添加到看板</span> : <span>无图表元素</span>}
                     </div>
                 </div> : children}
             </ReactGridLayout>

+ 256 - 0
src/components/dataConnect/list.jsx

@@ -0,0 +1,256 @@
+import React from 'react'
+import { Layout, Row, Col, Input, Button, Table, Icon, Menu, Dropdown, Card } from 'antd'
+import { connect } from 'dva'
+import DeleteBox from '../common/deleteBox/deleteBox'
+import DataConnectBox from '../dataSourceDetail/dataConnectBox'
+import './list.less'
+const { Content } = Layout
+const { Search } = Input
+
+class DataConnect extends React.Component {
+    constructor(props) {
+        super(props);
+        this.state = {
+            selectedRecord: null, // 当前选中的dataSource
+            visibleDeleteBox: false,
+        }
+    };
+    componentDidMount() {
+        const { dispatch } = this.props;
+        this.setScrollTableHeight();
+        dispatch({ type: 'dataConnect/fetchList' });
+    }
+
+    /**
+     * 根据视图设置表格高度以呈现滚动条
+     */
+    setScrollTableHeight() {
+        const mainContent = document.getElementsByClassName('main-content')[0];
+        const toolbar = mainContent.getElementsByClassName('dataconnect-tools')[0];
+        const tableHeader = mainContent.getElementsByClassName('ant-table-header')[0];
+        const tableBody = mainContent.getElementsByClassName('ant-table-body')[0];
+        tableBody.style.maxHeight=`${mainContent.offsetHeight - toolbar.offsetHeight - tableHeader.offsetHeight - 58}px`;
+    }
+
+    onSearch(list, text) {
+        const reg = new RegExp('([+ \\- & | ! ( ) { } \\[ \\] ^ \" ~ * ? : ( ) \/])', 'g'); // 需要转义的字符
+        let filterLabel = (text || '').replace(new RegExp('(\\\\)', 'g'), '\\$1').replace(reg, '\\$1'); // 添加转义符号
+        return list.map(l => {
+            let o = Object.assign({}, l);
+            let reg = new RegExp('('+ filterLabel +'){1}', 'ig');
+            if(o.name && o.name.search(reg) !== -1) {
+                return o;
+            }else if(o.description && o.description.search(reg) !== -1) {
+                return o;
+            }else {
+                return null
+            }
+        }).filter(a => a!==null);
+    }
+
+    onSort(list) {
+        return list.sort((a, b) => {
+            return new Date(b.createDate) - new Date(a.createDate);
+        });
+    }
+
+    handleVisibleChange = (flag) => {
+        this.setState({ visibleGroupMenu: flag });
+    }
+
+    render() {
+        
+        const { dataConnect, dispatch } = this.props;
+        const { selectedRecord, visibleDeleteBox } = this.state;
+
+        const reg = new RegExp('([+ \\- & | ! ( ) { } \\[ \\] ^ \" ~ * ? : ( ) \/])', 'g'); // 需要转义的字符
+        let filterLabel = dataConnect.filterLabel.replace(new RegExp('(\\\\)', 'g'), '\\$1').replace(reg, '\\$1'); // 添加转义符号
+
+        const moreOperatingMenu = (
+            <Menu className='operationmenu' visible={true}>
+                {selectedRecord && <Menu.Item onClick={(e) => {
+                    dispatch({ type: 'dataConnect/setNewModel', model: selectedRecord });
+                    dispatch({ type: 'dataConnect/setNewModelFields', fields: [
+                        { name: 'visibleBox', value: true },
+                        { name: 'boxOperation', value: 'modify' }
+                    ] });
+                    }}>
+                    <Icon type="info-circle-o" />属性设置
+                </Menu.Item>}
+                <Menu.Divider />
+                { selectedRecord && <Menu.Item
+                    onClick={(e) => {
+                        dispatch({ type: 'dataConnect/setNewModel', model: { ...selectedRecord, code: null } });
+                        dispatch({ type: 'dataConnect/setNewModelFields', fields: [
+                            { name: 'visibleBox', value: true },
+                            { name: 'boxOperation', value: 'create' }
+                        ] });
+                    }}
+                >
+                    <Icon type="copy" />复制新增
+                </Menu.Item>}
+                { selectedRecord && <Menu.Item
+                    onClick={(e) => {
+                        this.setState({ visibleDeleteBox: true})
+                    }}
+                >
+                    <Icon type="delete" />删除
+                </Menu.Item>}
+            </Menu>
+        );
+        const dataConnectColumns = [{
+            title: '连接名',
+            dataIndex: 'name',
+            key: 'name',
+            width: 200,
+            render: (text, record) => {
+                return <div className='dataconnect-name'>
+                    <div>
+                        <span>
+                            { filterLabel ?
+                                ((text || '').split(new RegExp(`(${filterLabel})`, 'i')).map((fragment, i) => {
+                                    return (
+                                        fragment.toLowerCase().replace(new RegExp('(\\\\)', 'g'), '\\$1').replace(reg, '\\$1') === filterLabel.toLowerCase() ?
+                                        <span key={i} style={{fontWeight: 'bold', color: 'red'}} className="highlight">{fragment}</span> :
+                                        fragment
+                                    )
+                                }
+                                )) : text
+                            }
+                        </span>
+                    </div>
+                </div>
+            }
+        }, {
+            title: '数据库类型',
+            dataIndex: 'dbType',
+            key: 'dbType',
+            width: 120
+        }, {
+            title: '数据库地址',
+            dataIndex: 'address',
+            key: 'address',
+            width: 120
+        }, {
+            title: '端口',
+            dataIndex: 'port',
+            key: 'port',
+            width: 100
+        }, {
+            title: '数据库名',
+            dataIndex: 'dbName',
+            key: 'dbName',
+            width: 100
+        }, {
+            title: '用户名',
+            dataIndex: 'userName',
+            key: 'userName',
+            width: 150
+        }, {
+            title: '说明',
+            dataIndex: 'description',
+            key: 'description',
+            render: (text, record) => {
+                return <div className='dataconnect-name'>
+                    <div>
+                        <span>
+                            { filterLabel ?
+                                ((text || '').split(new RegExp(`(${filterLabel})`, 'i')).map((fragment, i) => {
+                                    return (
+                                        fragment.toLowerCase().replace(new RegExp('(\\\\)', 'g'), '\\$1').replace(reg, '\\$1') === filterLabel.toLowerCase() ?
+                                        <span key={i} style={{fontWeight: 'bold', color: 'red'}} className="highlight">{fragment}</span> :
+                                        fragment
+                                    )
+                                }
+                                )) : text
+                            }
+                        </span>
+                    </div>
+                </div>
+            }
+        }, {
+            title: '操作',
+            key: 'action',
+            render: (text, record, index) => (
+                <Dropdown code={record.code} overlay={moreOperatingMenu} trigger={['click']} >
+                    <Icon type="setting" />
+                </Dropdown>
+            ),
+            width: 50
+        }];
+
+        return ( 
+            <Layout className='dataconnect-view'>
+                <Content>
+                    <Card className='dataconnect-body' title={
+                        <Row className='dataconnect-tools' type='flex' justify='space-between'>
+                            <Col style={{ display: 'flex' }}>
+                            </Col>
+                            <Col className='search'>
+                                <Col>
+                                    <Search
+                                        value={dataConnect.filterLabel}
+                                        placeholder="请输入关键字"
+                                        onChange={e => {
+                                            dispatch({ type: 'dataConnect/setFilterLabel', label: e.target.value });
+                                        }}
+                                    />
+                                </Col>
+                                <Col>
+                                    <Button onClick={() => {
+                                        dispatch({ type: 'dataConnect/setNewModel', model: { dbType: 'oracle' } });
+                                        dispatch({ type: 'dataConnect/setNewModelFields', fields: [
+                                            { name: 'visibleBox', value: true },
+                                            { name: 'boxOperation', value: 'create' }
+                                        ] });
+                                    }}>
+                                        <Icon type="plus" />添加数据连接
+                                    </Button>
+                                </Col>
+                            </Col>
+                        </Row>
+                    }>
+                        <Table
+                            className='dataconnect-table'
+                            columns={dataConnectColumns}
+                            dataSource={
+                                this.onSort(
+                                    this.onSearch(dataConnect.list, dataConnect.filterLabel)
+                                )
+                            }
+                            size='small'
+                            scroll={{x: false, y: true}}
+                            pagination={false}
+                            onRow={(record) => {
+                                return {
+                                    onClick: () => {this.setState({ selectedRecord:  record})}
+                                }
+                            }}
+                        />
+                        {visibleDeleteBox && <DeleteBox
+                            visibleBox={visibleDeleteBox}
+                            text={<div><span>确定要删除数据连接【{selectedRecord.name}】吗?</span></div>}
+                            hideBox={() => {
+                                this.setState({
+                                    visibleDeleteBox: false
+                                })
+                            }}
+                            okHandler={() =>{
+                                dispatch({ type: 'dataConnect/remoteDelete', code: selectedRecord.code })
+                            }} 
+                        />}
+                        <DataConnectBox />
+                    </Card>
+                </Content>
+            </Layout>
+        )
+
+
+    }
+}
+
+function mapStateToProps({present: {dataConnect}}) {
+    return { dataConnect }
+}
+
+export default connect(mapStateToProps)(DataConnect)

+ 174 - 0
src/components/dataConnect/list.less

@@ -0,0 +1,174 @@
+.dataconnect-view {
+    .dataconnect-body {
+        padding: 0;
+        .ant-card-head {
+            padding: 0 10px;
+            .dataconnect-tools {
+                flex: 1;
+                .anticon-bars {
+                    cursor: pointer;
+                    line-height: 1.6;
+                    font-size: 20px;
+                    margin-right: 6px;
+                    &> svg {
+                        height: 100%;
+                    }
+                }
+                .group {
+                    line-height: 2.1;
+                    .ant-breadcrumb-link {
+                        .ant-tag {
+                            margin: 0;
+                        }
+                    }
+                }
+                .search {
+                    display: flex;
+                    > div:first-child {
+                        margin-right: 5px;
+                    }
+                }
+            }
+        }
+    }
+    .ant-card-body {
+        padding: 0;
+        .dataconnect-table{
+            .ant-table {
+                .ant-table-scroll {
+                    .ant-table-header {
+                        overflow: hidden;
+                        table {
+                            thead {
+                                th {
+                                    .ant-table-column-sorter, .ant-table-filter-icon {
+                                        opacity: 0;
+                                    }
+                                    :hover {
+                                        .ant-table-column-sorter, .ant-table-filter-icon {
+                                            opacity: 1;
+                                        }
+                                    }
+                                }
+                                .column-filtered {
+                                    .ant-table-filter-icon {
+                                        opacity: 1;
+                                        color: red;
+                                    }
+                                }
+                            }
+                        }
+                    }
+                    .ant-table-body {
+                        margin-top: 17px;
+                        overflow-y: auto !important;
+                        table {
+                            padding: 0;
+                            .ant-table-row {
+                                td {
+                                    padding: 8px;
+                                    .dataconnect-name {
+                                        display: flex;
+                                        .dataconnect-type {
+                                            margin-right: 5px;
+                                        }
+                                    }
+                                    .dataconnect-tag {
+                                        margin: 2px;
+                                        cursor: default;
+                                    }
+                                    .ant-dropdown-trigger {
+                                        font-size: 18px;
+                                        cursor: pointer;
+                                    }
+                                }
+                                .action-col {
+                                    display: flex;
+                                    .operation {
+                                        cursor: pointer;
+                                    }
+                                    .operation:hover {
+                                        color: #40a9ff;
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+.popover-group {
+    width: 300px;
+    .grouptree-title {
+        .create-group {
+            cursor: pointer;
+            color: #40a9ff;
+        }
+    }
+    .ant-popover-inner-content {
+        cursor: default;
+        max-height: 60vh;
+        overflow: auto;
+    }
+    .tree-group {
+        li.drag-over {
+            input {
+                background-color: #40a9ff;
+            }
+        }
+        li {
+            .ant-tree-node-content-wrapper {
+                width: 90%;
+                height: 28px;
+                margin: 0 !important;
+                padding: 0;
+                background-color: transparent;
+
+                :hover {
+                    background-color: transparent !important;
+                }
+                .ant-tree-node-selected {
+                    background-color: transparent !important;
+                }
+                input {
+                    max-width: 180px;
+                    border: none;
+                }
+                .anticon-plus-circle-o {
+                    margin-left: 5px;
+                }
+                .anticon-minus-circle {
+                    margin-left: 5px;
+                }
+            }
+            .drag-over {
+                span[draggable] {
+                    opacity: .7;
+                }
+            }
+        }
+    }
+}
+.tree-group li.drag-over > span[draggable] {
+    opacity: .5;
+}
+
+.operationmenu {
+    padding: 0;
+    width: 120px;
+    .ant-dropdown-menu-item {
+        .anticon {
+            margin-right: 6px;
+        }
+    }
+    .ant-dropdown-menu-item-divider {
+        margin: 0;
+    }
+    .setgroupmenu {
+        .ant-dropdown-menu-submenu-title {
+            display: flex;
+        }
+    }
+}

+ 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)

+ 1 - 1
src/components/dataSource/groupSelector.jsx

@@ -40,7 +40,7 @@ class GroupSelector extends React.Component {
                 content={
                     [{code: 'all', label: '全部分组'}, { code: '-1', label: '未分组' }].concat(pgroups).map(p => {
                         let cgroups = grouplist.filter(g => g.pcode === p.code && p.code !== '-1');
-                        return <Row type='flex' justify='left' key={`gr-${p.code}`}>
+                        return <Row type='flex' justify='start' key={`gr-${p.code}`}>
                             <Col key={`rc-${p.code}`} onClick={() => {
                                 this.hide();
                                 dispatch({ type: modelName + '/setCurrentGroup', group1: p });

+ 27 - 7
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'); // 需要转义的字符
@@ -265,13 +267,13 @@ class DataSource extends React.Component {
                 >
                     <Icon type="file-add" />创建图表
                 </Menu.Item>
-                {selectedRecord && currentUser.code === selectedRecord.creatorCode && <Menu.Item onClick={(e) => {
+                {/* {selectedRecord && currentUser.code === selectedRecord.creatorCode && <Menu.Item onClick={(e) => {
                         dispatch({ type: 'dataSource/resetNewModel' });
                         let selectedModel = dataSource.list.find((i) => { return i.code === selectedRecord.code })
                         dispatch({type: 'main/redirect', path: {pathname: '/datasource/'+ selectedModel.type +'/' + selectedModel.code + '/base'}});
                     }}>
                     <Icon type="info-circle-o" />属性设置
-                </Menu.Item>}
+                </Menu.Item>} */}
                 <Menu.Item onClick={() => {
                     this.setState({
                         visibleDataPreviewBox: true
@@ -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})
@@ -310,7 +320,10 @@ class DataSource extends React.Component {
                         {record.type === 'database' ? <Icon type="database" theme="outlined" /> : <Icon type="file-excel" theme="outlined" />}
                     </div>
                     <div>
-                        <span>
+                        <span style={{ color: '#1890ff', cursor: 'pointer' }} onClick={() => {
+                            dispatch({ type: 'dataSource/resetNewModel' });
+                            dispatch({type: 'main/redirect', path: {pathname: '/datasource/'+ record.type +'/' + record.code + '/base'}});
+                        }}>
                             { filterLabel ?
                                 ((text || '').split(new RegExp(`(${filterLabel})`, 'i')).map((fragment, i) => {
                                     return (
@@ -437,8 +450,8 @@ class DataSource extends React.Component {
                                             dispatch({ type: 'dataSource/setNewModelField', name: 'type', value: type });
                                             dispatch({type: 'main/redirect', path: {pathname: '/datasource/'+ type +'/create/base'}});
                                         }}>
-                                            { currentUser.role === 'admin' && <Menu.Item key='database'>数据库</Menu.Item>}
-                                            <Menu.Item disabled key='file'>文件</Menu.Item>
+                                            { currentUser.role === 'admin' && <Menu.Item key='database'>来自数据库</Menu.Item>}
+                                            <Menu.Item disabled key='file'>来自文件</Menu.Item>
                                         </Menu>
                                     )} trigger={['click']}>
                                         <Button>
@@ -450,7 +463,7 @@ class DataSource extends React.Component {
                         </Row>
                     }>
                         <Table
-                            className='datasource-table datasource-table'
+                            className='datasource-table'
                             columns={dataSourceColumns}
                             dataSource={
                                 this.onSort(
@@ -479,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>}

+ 33 - 2
src/components/dataSourceDetail/baseConfig.jsx

@@ -6,13 +6,25 @@ const SelectOption = Select.Option
 
 class DataSourceBaseConfig extends React.Component {
 
+    generateOptions () {
+        const { dataConnect } = this.props;
+        const { list } = dataConnect;
+
+        return list.map((l) => {
+            return <SelectOption value={l.code} key={l.code}>
+                { l.name }
+            </SelectOption>
+        });
+    }
+
     componentDidMount() {
         const { dispatch } = this.props;
         dispatch({ type: 'dataSource/remoteGroupList' });
+        dispatch({ type: 'dataConnect/fetchList' });
     }
 
     render() {
-        const { dataSource, dataSourceDetail, dispatch } = this.props;
+        const { dataConnect, dataSource, dataSourceDetail, dispatch } = this.props;
 
         const formItemLayout = {
             labelCol: { span: 4 },
@@ -58,6 +70,25 @@ class DataSourceBaseConfig extends React.Component {
                     ):(
                         <div>
                             <Divider orientation="left">连接配置</Divider>
+                            <div style={{ textAlign: 'end', color: '#F5222D' }}>*若只修改数据连接,请确认不同数据库取数逻辑一致</div>
+                            <FormItem label='数据连接' {...formItemLayout}>
+                                <Select
+                                    value={dataSourceDetail.connectCode}
+                                    onChange={(value) => {
+                                        let selectedDataConnect = dataConnect.list.filter((l) => l.code === value)[0];
+                                        dispatch({ type: 'dataSourceDetail/setFields', fields: [
+                                            { name: 'connectCode', value: selectedDataConnect.code },
+                                            { name: 'dbType', value: selectedDataConnect.dbType },
+                                            { name: 'address', value: selectedDataConnect.address },
+                                            { name: 'port', value: selectedDataConnect.port },
+                                            { name: 'dbName', value: selectedDataConnect.dbName },
+                                            { name: 'userName', value: selectedDataConnect.userName },
+                                        ] } );
+                                    }}
+                                >
+                                    { this.generateOptions() }
+                                </Select>
+                            </FormItem>
                             <FormItem label='数据库类型' {...formItemLayout}>
                                 <Select
                                     disabled={true}
@@ -201,4 +232,4 @@ class DataSourceBaseConfig extends React.Component {
     }
 }
 
-export default connect(({ present: { dataSource, dataSourceDetail } }) => ({ dataSource, dataSourceDetail }))(DataSourceBaseConfig);
+export default connect(({ present: { dataConnect, dataSource, dataSourceDetail } }) => ({ dataConnect, dataSource, dataSourceDetail }))(DataSourceBaseConfig);

+ 10 - 3
src/components/dataSourceDetail/columnConfig.jsx

@@ -290,15 +290,22 @@ class DataSourceColumnConfig extends React.Component {
                                 <FormItem className='textarea-target'>
                                     <Input.TextArea
                                         disabled={!dataSourceDetail.address}
-                                        placeholder={dataSourceDetail.address ? '输入表名或查询SQL,注意不能以分号结尾' : '请返回上一步选择数据库连接'}
+                                        placeholder={dataSourceDetail.address ? '输入表名或查询SQL,注意不能以分号结尾' : '请点击底部“上一步”按钮返回上一步选择数据库连接'}
                                         autosize={{ minRows: 3 }}
-                                        value={dataSourceDetail.target}
-                                        onChange={(e) => {
+                                        // value={dataSourceDetail.target}
+                                        defaultValue={dataSourceDetail.target}
+                                        onBlur={(e) => {
                                             dispatch({ type: 'dataSourceDetail/setFields', fields: [
                                                 { name: 'target', value: e.target.value },
                                                 { name: 'notice', value: '' }
                                             ] });
                                         }}
+                                        // onChange={(e) => {
+                                        //     dispatch({ type: 'dataSourceDetail/setFields', fields: [
+                                        //         { name: 'target', value: e.target.value },
+                                        //         { name: 'notice', value: '' }
+                                        //     ] });
+                                        // }}
                                     />
                                 </FormItem>
                                 <div className='buttons'>

+ 1 - 1
src/components/dataSourceDetail/columnType.json

@@ -3,7 +3,7 @@
     "columnType": "index",
     "label": "索引"
 }, {
-    "dataType": ["Date"],
+    "dataType": ["Date", "Timestamp"],
     "columnType": "time",
     "label": "时间"
 }, {

+ 1 - 1
src/components/dataSourceDetail/content.jsx

@@ -93,7 +93,7 @@ class DataSourceDetailContent extends React.Component {
                             {
                                 current < steps.length - 1
                                 && <Button disabled={
-                                    ( current === 0 && ( ( type === 'database' && !dataSourceDetail.address ) || ( type === 'file' && +1 === 2) ) ) ||
+                                    ( current === 0 && ( ( type === 'database' && !dataSourceDetail.connectCode ) || ( type === 'file' && +1 === 2) ) ) ||
                                     (current === 1 && (!dataSourceDetail.columns || dataSourceDetail.columns.length === 0))
                                 } type="primary" onClick={() => this.next()}>下一步</Button>
                             }

+ 8 - 3
src/components/dataSourceDetail/content.less

@@ -86,9 +86,13 @@
                                     display: flex;
                                     flex-direction: column;
                                     .ant-card-head {
+                                        min-height: 32px;
                                         background: #F5F5F5;
+                                        padding: 0;
                                         .ant-card-head-wrapper {
+                                            height: 100%;
                                             .ant-card-head-title {
+                                                padding: 0 16px;
                                                 .ant-row-flex {
                                                     .label {
                                                         overflow: hidden;
@@ -98,9 +102,10 @@
                                             }
                                         }
                                         .selected {
-                                            width: 60px;
-                                            height: 60px;
-                                            background-size: cover;
+                                            width: 32px;
+                                            height: 32px;
+                                            background-repeat: no-repeat;
+                                            background-size: 32px;
                                             background-image: url(/images/selected.png);
                                             position: absolute;
                                             right: 0px;

+ 39 - 17
src/components/dataSourceDetail/dataConnectBox.jsx

@@ -16,14 +16,20 @@ class DataConnectBox extends React.Component {
         const { dispatch, dataConnect} = this.props;
         const operation = dataConnect.newOne.boxOperation;
         if(operation === 'create') {
-            dispatch({ type: 'dataConnect/remoteAdd' });
+            dispatch({ type: 'dataConnect/remoteAdd' }).then((success) => {
+                success && this.hideBox()
+            });
         }else if(operation === 'modify') {
-            dispatch({ type: 'dataConnect/remoteModify', code: dataConnect.newOne.code });
+            dispatch({ type: 'dataConnect/remoteModify', code: dataConnect.newOne.code }).then((success) => {
+                success && this.hideBox();
+            });
         }
     }
 
     render() {
         const { dispatch, dataConnect } = this.props;
+        const operation = dataConnect.newOne.boxOperation;
+        const disabled = operation === 'view';
 
         const formItemLayout = {
             labelCol: { span: 4 },
@@ -33,29 +39,36 @@ class DataConnectBox extends React.Component {
         return (
             <Modal
                 className='dataconnect-box'
-                title={<Row type='flex' justify='space-between'><Col>{`${dataConnect.newOne.boxOperation === 'create'?'新建':'修改'}数据库连接`}</Col><Col style={{ marginRight: '35px', fontSize: '14px', cursor: 'pointer', color: 'red' }} onClick={() => {
+                title={<Row type='flex' justify='space-between'><Col>{`${operation === 'create'?'新建':(operation === 'modify' ? '修改' : '查看')}数据库连接`}</Col><Col style={{ marginRight: '35px', fontSize: '14px', cursor: 'pointer', color: 'red' }} onClick={() => {
                     // 密码input特殊处理
                     document.getElementsByClassName('password')[0].value = '';
                     dispatch({ type:'dataConnect/resetNewModel'})
-                }}><Icon type='delete' />清空</Col></Row>}
+                }}>
+                    {/* <Icon type='delete' />清空 */}
+                </Col></Row>}
                 visible={dataConnect.newOne.visibleBox}
                 onCancel={() => { this.hideBox() }}
                 maskClosable={false}
                 destroyOnClose={true}
                 footer={
-                    <Row>
-                        <Col className='validatemessage' span={12}>
-                            {dataConnect.newOne.invalid !== undefined ? (dataConnect.newOne.invalid ? <span style={{ color: '#F5222D' }}>数据连接配置有误</span> : <span style={{ color: '#52C41A' }}>测试通过</span>) : ''}
-                        </Col>
-                        <Col span={12}>
-                            <Button disabled={dataConnect.newOne.validating} onClick={() => dispatch({ type:'dataConnect/remoteValidate'})}>测试</Button>
-                            {/* <Button onClick={() => dispatch({ type:'dataConnect/resetNewModel'})}>清空</Button> */}
-                            <Button onClick={() => {this.hideBox()}}>取 消</Button>
-                            <Button className={dataConnect.newOne.validating ? 'ant-btn-loading' : ''} type="primary" onClick={() => {this.okHandler()}}>
-                                {dataConnect.newOne.validating ? (<Icon type='loading' />) : ''}{dataConnect.newOne.validating ? '校验中' : '保存'}
-                            </Button>
-                        </Col>
-                    </Row>
+                    operation === 'view' ? null : (
+                        <Row>
+                            <Col className='validatemessage' span={12}>
+                                {/* {dataConnect.newOne.invalid !== undefined ? (dataConnect.newOne.invalid ? <span style={{ color: '#F5222D' }}>{dataConnect.newOne.invalidText}</span> : <span style={{ color: '#52C41A' }}>测试通过</span>) : ''} */}
+                            </Col>
+                            <Col span={12}>
+                                <Button disabled={dataConnect.newOne.validating || dataConnect.newOne.saving} onClick={() => dispatch({ type:'dataConnect/remoteValidate'})}>
+                                {dataConnect.newOne.validating ? (<Icon type='loading' />) : ''}{dataConnect.newOne.validating ? '测试中' : '测试'}
+                                </Button>
+                                <Button disabled={dataConnect.newOne.validating || dataConnect.newOne.saving} onClick={() => dispatch({ type:'dataConnect/resetNewModel'})}>清空</Button>
+                                {/* <Button onClick={() => {this.hideBox()}}>取 消</Button> */}
+                                <Button className={dataConnect.newOne.validating ? 'ant-btn-loading' : ''} type="primary" disabled={dataConnect.newOne.validating || dataConnect.newOne.saving} onClick={() => {this.okHandler()}}>
+                                    {dataConnect.newOne.saving ? (<Icon type='loading' />) : ''}
+                                    {dataConnect.newOne.saving ? '校验中' : '保存'}
+                                </Button>
+                            </Col>
+                        </Row>
+                    )
                 }
             >
                 <Form size='small'>
@@ -66,6 +79,7 @@ class DataConnectBox extends React.Component {
                         help={dataConnect.validInfo.name ? dataConnect.validInfo.name.help : ''}
                     >
                         <Input
+                            disabled={disabled}
                             placeholder="输入数据库连接名称"
                             value={dataConnect.newOne.name}
                             onChange={(e) => { dispatch({ type: 'dataConnect/setNewModelField', name: 'name', value: e.target.value }) }}
@@ -74,7 +88,9 @@ class DataConnectBox extends React.Component {
                     </FormItem>
                     <FormItem label='数据库类型' {...formItemLayout}>
                         <Select
+                            disabled={true}
                             placeholder="选择数据库类型"
+                            defaultValue='oracle'
                             value={dataConnect.newOne.dbType}
                             onChange={(value) => {
                                 dispatch({ type: 'dataConnect/setNewModelField', name: 'dbType', value: value} );
@@ -101,6 +117,7 @@ class DataConnectBox extends React.Component {
                                 wrapperCol: { span: 19 }
                             }}>
                                 <Input
+                                    disabled={disabled}
                                     placeholder="格式:192.168.1.1"
                                     value={dataConnect.newOne.address}
                                     onChange={(e) => {
@@ -115,6 +132,7 @@ class DataConnectBox extends React.Component {
                                 wrapperCol: { span: 12 }
                             }}>
                                 <InputNumber
+                                    disabled={disabled}
                                     value={dataConnect.newOne.port}
                                     onChange={(value) => {
                                         dispatch({ type: 'dataConnect/setNewModelField', name: 'port', value: value });
@@ -125,6 +143,7 @@ class DataConnectBox extends React.Component {
                     </Row>
                     <FormItem label='数据库名(SID)' {...formItemLayout}>
                         <Input
+                            disabled={disabled}
                             value={dataConnect.newOne.dbName}
                             onChange={(e) => {
                                 dispatch({ type: 'dataConnect/setNewModelField', name: 'dbName', value: e.target.value });
@@ -138,6 +157,7 @@ class DataConnectBox extends React.Component {
                                 wrapperCol: { span: 16 }
                             }}>
                                 <Input
+                                    disabled={disabled}
                                     value={dataConnect.newOne.userName}
                                     onChange={(e) => {
                                         dispatch({ type: 'dataConnect/setNewModelField', name: 'userName', value: e.target.value });
@@ -151,6 +171,7 @@ class DataConnectBox extends React.Component {
                                 wrapperCol: { span: 16 }
                             }}>
                                 <Input
+                                    disabled={disabled}
                                     className='password'
                                     type='password'
                                     // value={dataConnect.newOne.password}
@@ -163,6 +184,7 @@ class DataConnectBox extends React.Component {
                     </Row>
                     <FormItem className='textarea-desc' label='说明' {...formItemLayout}>
                         <Input.TextArea
+                            disabled={disabled}
                             autosize={{ minRows: 2 }}
                             value={dataConnect.newOne.description}
                             onChange={(e) => {

+ 41 - 22
src/components/dataSourceDetail/dataConnectConfig.jsx

@@ -27,15 +27,10 @@ class DataConnectConfig extends React.Component {
                         dispatch({ type: 'dataConnect/setNewModel', model: dataConnect.selected });
                         dispatch({ type: 'dataConnect/setNewModelFields', fields: [
                             { name: 'visibleBox', value: true },
-                            { name: 'boxOperation', value: 'modify' }
+                            { name: 'boxOperation', value: 'view' }
                         ] });
                     }}>
-                        <Icon type='edit'/>编辑
-                    </Menu.Item>
-                    <Menu.Item onClick={(e) => {
-                        dispatch({ type: 'dataConnect/remoteDelete', code: dataConnect.selected.code });
-                    }}>
-                        <Icon type='delete'/>删除
+                        <Icon type='edit'/>详情
                     </Menu.Item>
                 </Menu>
             )
@@ -48,7 +43,18 @@ class DataConnectConfig extends React.Component {
                     <Card
                         title={
                             <Row type='flex' justify='start'>
-                                <Col className='label'>{l.name}</Col>
+                                <Col className='label'>
+                                    { filterLabel ?
+                                        ((l.name || '').split(new RegExp(`(${filterLabel})`, 'i')).map((fragment, i) => {
+                                            return (
+                                                fragment.toLowerCase().replace(new RegExp('(\\\\)', 'g'), '\\$1').replace(reg, '\\$1') === filterLabel.toLowerCase() ?
+                                                <span key={i} style={{fontWeight: 'bold', color: 'red'}} className="highlight">{fragment}</span> :
+                                                fragment
+                                            )
+                                        }
+                                        )) : l.name
+                                    }
+                                </Col>
                                 <div style={{ display: (dataConnect.selected && dataConnect.selected.code === l.code) ? 'block' : 'none' }} className='selected'></div>
                             </Row>
                         }
@@ -72,7 +78,16 @@ class DataConnectConfig extends React.Component {
                         <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'space-between', height: '100%' }}>
                             <Row>
                                 <Ellipsis lines={3}>
-                                    {l.description}
+                                    { filterLabel ?
+                                        ((l.description || '').split(new RegExp(`(${filterLabel})`, 'i')).map((fragment, i) => {
+                                            return (
+                                                fragment.toLowerCase().replace(new RegExp('(\\\\)', 'g'), '\\$1').replace(reg, '\\$1') === filterLabel.toLowerCase() ?
+                                                <span key={i} style={{fontWeight: 'bold', color: 'red'}} className="highlight">{fragment}</span> :
+                                                fragment
+                                            )
+                                        }
+                                        )) : l.description
+                                    }
                                 </Ellipsis>
                             </Row>
                             <Row type='flex' justify='space-between'>
@@ -88,19 +103,23 @@ class DataConnectConfig extends React.Component {
                 </CardGrid>
             ))
 
-            cards.unshift(
-                <CardGrid className='dataconnect-card dataconnect-card-create' key='create' onClick={() => {
-                    dispatch({ type: 'dataConnect/setNewModel', model: {} });
-                    dispatch({ type: 'dataConnect/setNewModelFields', fields: [
-                        { name: 'visibleBox', value: true },
-                        { name: 'boxOperation', value: 'create' }
-                    ] });
-                }}>
-                    <Card>
-                        <Icon type="plus-circle-o" />
-                    </Card>
-                </CardGrid>
-            );
+            // cards.unshift(
+            //     <CardGrid className='dataconnect-card dataconnect-card-create' key='create' onClick={() => {
+            //         dispatch({ type: 'dataConnect/setNewModel', model: { dbType: 'oracle' } });
+            //         dispatch({ type: 'dataConnect/setNewModelFields', fields: [
+            //             { name: 'visibleBox', value: true },
+            //             { name: 'boxOperation', value: 'create' }
+            //         ] });
+            //     }}>
+            //         <Card>
+            //             <Icon type="plus-circle-o" />
+            //         </Card>
+            //     </CardGrid>
+            // );
+
+            if(cards.length === 0) {
+                return (<div className="ant-empty ant-empty-normal"><div className="ant-empty-image"><img alt="暂无数据" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjQiIGhlaWdodD0iNDEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAxKSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj4KICAgIDxlbGxpcHNlIGZpbGw9IiNGNUY1RjUiIGN4PSIzMiIgY3k9IjMzIiByeD0iMzIiIHJ5PSI3Ii8+CiAgICA8ZyBmaWxsLXJ1bGU9Im5vbnplcm8iIHN0cm9rZT0iI0Q5RDlEOSI+CiAgICAgIDxwYXRoIGQ9Ik01NSAxMi43Nkw0NC44NTQgMS4yNThDNDQuMzY3LjQ3NCA0My42NTYgMCA0Mi45MDcgMEgyMS4wOTNjLS43NDkgMC0xLjQ2LjQ3NC0xLjk0NyAxLjI1N0w5IDEyLjc2MVYyMmg0NnYtOS4yNHoiLz4KICAgICAgPHBhdGggZD0iTTQxLjYxMyAxNS45MzFjMC0xLjYwNS45OTQtMi45MyAyLjIyNy0yLjkzMUg1NXYxOC4xMzdDNTUgMzMuMjYgNTMuNjggMzUgNTIuMDUgMzVoLTQwLjFDMTAuMzIgMzUgOSAzMy4yNTkgOSAzMS4xMzdWMTNoMTEuMTZjMS4yMzMgMCAyLjIyNyAxLjMyMyAyLjIyNyAyLjkyOHYuMDIyYzAgMS42MDUgMS4wMDUgMi45MDEgMi4yMzcgMi45MDFoMTQuNzUyYzEuMjMyIDAgMi4yMzctMS4zMDggMi4yMzctMi45MTN2LS4wMDd6IiBmaWxsPSIjRkFGQUZBIi8+CiAgICA8L2c+CiAgPC9nPgo8L3N2Zz4K"/></div><p className="ant-empty-description">暂无数据</p></div>)
+            }
             
             return cards;
         }

+ 15 - 2
src/constants/url.js

@@ -1,6 +1,5 @@
-// const BASE_URL = 'http://192.168.253.189:81/BI';
-// const BASE_URL = 'http://192.168.253.129:8011';
 const BASE_URL = 'http://10.1.1.168:8094/BI';
+// const BASE_URL = 'http://10.1.80.36:8011';
 
 /**后台接口地址 */
 const URLS = {
@@ -53,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', // 添加策略
@@ -65,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获取数据列表
@@ -169,6 +172,16 @@ const URLS = {
 
     DASHBOARD_TRANSFER: BASE_URL + '/changeDashOrder', // 看板移交
 
+    DASHBOARD_COPY: BASE_URL + '/copyDashboard', // 看板复制
+
+    DASHBOARD_GET_SHAREKEY: BASE_URL + '/share', // 生成看板分享链接
+    
+    DASHBOARD_SHARE_DETAIL_BY_KEY: BASE_URL + '/getSharedDashboard', // 通过看板分享链接获取看板数据
+
+    DASHBOARD_ENCRYPT_CODE: BASE_URL + '/encryptCode', // 通过看板编号获得加密串
+
+    DASHBOARD_SHARE_DETAIL_BY_CODE: BASE_URL + '/getDashboardByCode', // 通过看板编号获得看板数据
+
     /***************************************浏览记录***************************************/
     
     HISTORY_ADD: BASE_URL + '/addRecord', // 添加图表、看板浏览记录 0 - 图表  1 - 看板

+ 4 - 0
src/custom.less

@@ -15,3 +15,7 @@
 .ant-dropdown-menu {
     padding: 0;
 }
+
+.ant-input[disabled],.ant-select-disabled,.ant-input-number-disabled {
+    color: rgba(0, 0, 0, 0.5);
+}

+ 0 - 5
public/index.html → src/index.ejs

@@ -4,13 +4,8 @@
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1">
   <title>BI 商业智能平台</title>
-  <link rel="shortcut icon" href="/images/favicon.png">
-  <link rel="stylesheet" href="index.css" />
 </head>
 <body>
-
   <div id="root"></div>
-  <script src="index.js"></script>
-
 </body>
 </html>

+ 15 - 7
src/models/chart.js

@@ -129,9 +129,9 @@ export default {
                 }
                 const res = yield call(service.fetch, {
                     url: URLS.CHART_LIST,
+                    method: 'GET',
                     body
                 });
-                console.log('请求图表列表', body, res);
                 if(!res.err && res.data.code > 0) {
                     let list = res.data.data.list.map(d => {
                         let chartOption = d.chartOption ? JSON.parse(d.chartOption) : {};
@@ -140,6 +140,8 @@ export default {
                             name: d.chartName,
                             dataSourceCode: d.dataId + '',
                             dataSourceName: d.dataName,
+                            dataConnectCode: d.dataConnectionId,
+                            dataConnectName: d.dataConnectionName,
                             access: d.authority === '1', // 权限
                             database: d.dbStatus === '0', // 数据源是否还存在
                             type: CHART_TYPE[d.chartType],
@@ -148,7 +150,8 @@ export default {
                             createTime: d.createDate,
                             description: d.describes || '',
                             groupCode: d.chartsGroup + '',
-                            chartOption: chartOption
+                            chartOption: chartOption,
+                            demo: d.demo
                         }
                     })
                     yield put({ type: 'list', list: list });
@@ -156,7 +159,6 @@ export default {
                     message.error('请求图表列表失败: ' + (res.err || res.data.msg));
                 }
             }catch(e) {
-                console.log(body, e);
                 message.error('请求图表列表失败: ' + e);
             }
         },
@@ -168,9 +170,11 @@ export default {
             try {
                 const res = yield call(service.fetch, {
                     url: URLS.CHART_DETAIL,
-                    body: code
+                    method: 'GET',
+                    body: {
+                        id: code
+                    }
                 });
-                console.log('解析图表数据', code, res);
                 if(!res.err && res.data.code > 0) {
                     let resData = res.data.data;
                     let chartConfig = JSON.parse(resData.chartConfig || '{ "xAxis": { "column": {}, "granularity": {} }, "yAxis": { "column": {}, "gauge": {} } }');
@@ -184,6 +188,10 @@ export default {
                         code: resData.chartId + '',
                         creatorCode: resData.createId+'',
                         creatorName: resData.createBy,
+                        dataSourceCode: resData.dataId + '',
+                        dataSourceName: resData.dataName,
+                        dataConnectCode: resData.dataConnectionId,
+                        dataConnectName: resData.dataConnectionName,
                         header: {
                             label: resData.chartName
                         },
@@ -199,7 +207,8 @@ export default {
                         description: resData.describes,
                         group: resData.chartsGroup+'',
                         filters: filters,
-                        chartOption: chartOption
+                        chartOption: chartOption,
+                        demo: resData.demo
                     }
 
                     if(viewType === 'bar') {
@@ -230,7 +239,6 @@ export default {
                     message.error('解析图表错误: ' + (res.err || res.data.msg));
                 }
             }catch(e) {
-                console.log(e);
                 message.error('解析图表错误: ' + e);
             }
         },

+ 6 - 11
src/models/chartDesigner.js

@@ -53,7 +53,8 @@ export default {
             filters: [],
             chartOption: {},
             dirty: false,
-            fetchConfig: {}
+            fetchConfig: {},
+            demo: false
         },
     },
     reducers: {
@@ -351,13 +352,11 @@ export default {
                     filters: getBodyFilters(filters)
                 };
                 
-                console.log('请求柱状图数据', body);
                 let res = yield call(service.fetch, {
                     url: URLS.CHART_BAR_OPTION,
                     body: body,
                     timeout: 30000
                 });
-                console.log('请求柱状图数据', body, res);
                 if(!res.err && res.data.code > 0) {
                     let option = parseChartOption('bar', res.data.data, barConfig);
                     yield put({ type: 'silentSetField', name: 'chartOption', value: option });
@@ -367,7 +366,6 @@ export default {
                 }
                 yield put({ type: 'silentSetField', name: 'fetchConfig', value: body });
             }catch(e) {
-                console.error(e);
                 yield put({ type: 'silentSetField', name: 'chartOption', value: {} });
                 message.error('请求柱状图数据失败: ' + e);
             }
@@ -430,13 +428,11 @@ export default {
                     filters: getBodyFilters(filters)
                 };
 
-                console.log('请求折线图数据', body);
                 let res = yield call(service.fetch, {
                     url: URLS.CHART_LINE_OPTION,
                     body: body,
                     timeout: 30000
                 });
-                console.log('请求折线图数据', body, res);
                 if(!res.err && res.data.code > 0) {
                     let option = parseChartOption('line', res.data.data, lineConfig);
                     yield put({ type: 'silentSetField', name: 'chartOption', value: option });
@@ -446,7 +442,6 @@ export default {
                 }
                 yield put({ type: 'silentSetField', name: 'fetchConfig', value: body });
             }catch(e) {
-                console.error(e);
                 yield put({ type: 'silentSetField', name: 'chartOption', value: {} });
                 message.error('请求折线图数据失败: ' + e);
             }
@@ -518,14 +513,14 @@ export default {
                     let option = parseChartOption('dataView', res.data.data, dataViewConfig);
                     yield put({ type: 'silentSetField', name: 'chartOption', value: option });
                 }else {
-                    // message.error('请求列表数据失败: ' + (res.err || res.data.msg));
-                    // yield put({ type: 'silentSetField', name: 'chartOption', value: {} });
+                    message.error('请求列表数据失败: ' + (res.err || res.data.msg));
+                    yield put({ type: 'silentSetField', name: 'chartOption', value: {} });
                 }
                 yield put({ type: 'silentSetField', name: 'fetchConfig', value: body });
             }catch(e) {
                 console.error(e);
-                // yield put({ type: 'silentSetField', name: 'chartOption', value: {} });
-                // message.error('请求列表数据失败: ' + e);
+                yield put({ type: 'silentSetField', name: 'chartOption', value: {} });
+                message.error('请求列表数据失败: ' + e);
             }
         },
         *fetchAggregateTableData(action, { select, call, put }) {

+ 249 - 25
src/models/dashboard.js

@@ -2,6 +2,10 @@ import { message } from 'antd'
 import * as service from '../services/index'
 import URLS from '../constants/url'
 
+const generateShareCode = () => {
+    return Math.random().toString(36).substr(2).toUpperCase();
+}
+
 export default {
     namespace: 'dashboard',
     state: {
@@ -48,22 +52,47 @@ export default {
                 }
                 const res = yield call(service.fetch, {
                     url: URLS.DASHBOARD_LIST,
+                    method: 'GET',
                     body
                 });
-                console.log('请求看板列表', body, res);
                 if(!res.err && res.data.code > 0) {
                     const resData = res.data.data.list;
                     let list = resData.map(d => {
                         let items = d.bdConfiguration ? JSON.parse(d.bdConfiguration) : [];
+
+                        const dataSources = [];
+                        const dataConnects = [];
+                        items.forEach(item => {
+                            if(item.viewType === 'chart') {
+                                if(!dataSources.find(ad => ad.code === item.dataSourceCode)) {
+                                    dataSources.push({
+                                        code: item.dataSourceCode,
+                                        name: item.dataSourceName
+                                    })
+                                }
+                                if(!dataConnects.find(ad => ad.code === item.dataSourceCode)) {
+                                    dataConnects.push({
+                                        code: item.dataConnectCode,
+                                        name: item.dataConnectName
+                                    })
+                                }
+                            }
+                        })
+    
                         return {
+                            key: d.id + '',
                             code:  d.id+'',
                             name: d.bdName,
                             items: items,
                             description: d.bdNote || '',
-                            thumbnail: d.thumbnail,
                             creatorCode: d.createId + '',
                             creatorName: d.createBy,
                             createTime: d.createDate,
+                            shareCode: d.bdCode,
+                            dataSources: dataSources,
+                            dataConnects: dataConnects,
+                            chartCodes: d.chartIds ? d.chartIds.split(',') : [],
+                            demo: d.demo
                         }
                     })
                     yield put({ type: 'list', list: list });
@@ -84,13 +113,16 @@ export default {
                 yield put({ type: 'dashboardDesigner/silentSetField', name: 'loading', value: true });
                 const res = yield call(service.fetch, {
                     url: URLS.DASHBOARD_DETAIL,
-                    body: code
+                    method: 'GET',
+                    body: {
+                        id: code
+                    }
                 });
-                console.log('解析看板数据', code, res);
                 if(!res.err && res.data.code > 0) {
                     const resData = res.data.data;
                     let items = resData.bdConfiguration ? JSON.parse(resData.bdConfiguration) : [];
                     let relationColumns = resData.relationColumns ? JSON.parse(resData.relationColumns) : [];
+                    let chartCodes = resData.chartIds ? resData.chartIds.split(',') : [];
                     const main = yield select(state => state.present.main);
                     const { currentUser } = main;
 
@@ -117,14 +149,16 @@ export default {
                         name: resData.bdName,
                         items: items,
                         description: resData.bdNote || '',
-                        thumbnail: resData.thumbnail,
                         creatorCode: resData.createId + '',
                         creatorName: resData.createBy,
                         createTime: resData.createDate,
                         dataSources: dataSources,
                         relationColumns: relationColumns,
                         editMode: currentUser.code === resData.createId + '',
-                        filters: JSON.parse((resData.filters|| "[]")) 
+                        filters: JSON.parse((resData.filters|| "[]")),
+                        shareCode: resData.bdCode,
+                        chartCodes: chartCodes,
+                        demo: resData.demo
                     }
 
                     let fields = [];
@@ -139,7 +173,6 @@ export default {
                     message.error('解析看板错误: ' + (res.err || res.data.msg));
                 }
             }catch(e) {
-                console.log(e);
                 message.error('解析看板错误: ' + e);
             }finally {
                 yield put({ type: 'dashboardDesigner/silentSetField', name: 'loading', value: false });
@@ -148,21 +181,21 @@ export default {
         *remoteAdd(action, { select, call, put }) {
             try {
                 const dashboardDesigner = yield select(state => state.present.dashboardDesigner);
-                const { name, items, thumbnail, relationColumns, filters } = dashboardDesigner;
+                const { name, items, description, relationColumns, filters, shareCode, chartCodes } = dashboardDesigner;
                 let body = {
                     bdName: name,
-                    bdNote: '',
+                    bdNote: description,
                     bdConfiguration: JSON.stringify(items),
-                    thumbnail: thumbnail,
                     relationColumns: JSON.stringify(relationColumns),
-                    filters: JSON.stringify(filters) || ''
+                    filters: JSON.stringify(filters) || '',
+                    bdCode: shareCode || generateShareCode(),
+                    thumbnail: '',
+                    chartIds: chartCodes.join(',')
                 }
-                console.log('新增看板', body);
                 const res = yield call(service.fetch, {
                     url: URLS.DASHBOARD_ADD,
                     body: body
                 });
-                console.log('新增看板', body, res);
                 if(!res.err && res.data.code > 0) {
                     yield put({ type: 'fetchList', mandatory: true });
                     message.success('保存成功');
@@ -170,28 +203,27 @@ export default {
                     message.error('保存失败: ' + (res.err || res.data.msg));
                 } 
             }catch(e) {
-                console.log(e);
                 message.error('保存失败: ' + e);
             }
         },
         *remoteQucikAdd(action, { select, call, put }) {
             try {
                 const dashboardDesigner = yield select(state => state.present.dashboardDesigner);
-                const { name, items, thumbnail, description, relationColumns, filters } = dashboardDesigner;
+                const { name, items, description, relationColumns, filters } = dashboardDesigner;
                 let body = {
                     bdName: name,
                     bdNote: description,
                     bdConfiguration: JSON.stringify(items),
-                    thumbnail: thumbnail,
                     relationColumns: JSON.stringify(relationColumns),
-                    filters: JSON.stringify(filters) || ""
+                    filters: JSON.stringify(filters) || "",
+                    bdCode: generateShareCode(),
+                    thumbnail: '',
+                    chartIds: ''
                 }
-                console.log('快速新增看板', body);
                 const res = yield call(service.fetch, {
                     url: URLS.DASHBOARD_ADD,
                     body: body
                 });
-                console.log('快速新增看板', body, res);
                 if(!res.err && res.data.code > 0) {
                     yield put({ type: 'fetchList', mandatory: true });
                     yield put({ type: 'main/redirect', path: '/dashboard/' + res.data.data });
@@ -199,28 +231,27 @@ export default {
                     message.error('保存失败: ' + (res.err || res.data.msg));
                 } 
             }catch(e) {
-                console.log(e);
                 message.error('保存失败: ' + e);
             }
         },
         *remoteModify(action, { select, call, put }) {
             try {
                 const dashboardDesigner = yield select(state => state.present.dashboardDesigner);
-                const { code, name, items, thumbnail, description, relationColumns, filters } = dashboardDesigner;
+                const { code, name, items, description, relationColumns, filters, chartCodes, shareCode } = dashboardDesigner;
                 let body = {
                     id: code,
                     bdName: name,
                     bdNote: description,
                     bdConfiguration: JSON.stringify(items.map(item => ({ ...item, chartOption: null }))),
-                    thumbnail: thumbnail,
                     relationColumns: JSON.stringify(relationColumns),
-                    filters: JSON.stringify(filters) || ""
+                    filters: JSON.stringify(filters) || "",
+                    chartIds: chartCodes.join(','),
+                    bdCode: shareCode
                 }
                 const res = yield call(service.fetch, {
                     url: URLS.DASHBOARD_UPDATE,
                     body: body
                 });
-                console.log('修改看板', body, res);
                 if(!res.err && res.data.code > 0) {
                     yield put({ type: 'fetchList', mandatory: true });
                     yield put({ type: 'dashboardDesigner/silentSetField', name: 'dirty', value: false });
@@ -229,7 +260,6 @@ export default {
                     message.error('保存失败: ' + (res.err || res.data.msg));
                 } 
             }catch(e) {
-                console.error(e);
                 message.error('保存失败: ' + e);
             }
         },
@@ -328,6 +358,200 @@ export default {
                 message.error('移交失败: ' + e);
             }
         },
+        *remoteShareDetail(action, { select, call, put }) {
+            const code = action.code;
+            if(!code){
+                return
+            }
+            try {
+                yield put({ type: 'dashboardDesigner/silentSetField', name: 'loading', value: true });
+                const res = yield call(service.fetch, {
+                    url: URLS.DASHBOARD_SHARE_DETAIL_BY_CODE,
+                    method: 'GET',
+                    allow: true,
+                    body: {
+                        code: code
+                    }
+                });
+                if(!res.err && res.data.code > 0) {
+                    const resData = res.data.data;
+                    let items = resData.bdConfiguration ? JSON.parse(resData.bdConfiguration) : [];
+                    let relationColumns = resData.relationColumns ? JSON.parse(resData.relationColumns) : [];
+                    let chartCodes = resData.chartIds ? resData.chartIds.split(',') : [];
+                    const main = yield select(state => state.present.main);
+                    const { currentUser } = main;
+
+                    const allDataSources = items.map(item => {
+                        if(item.viewType === 'chart') {
+                            return {
+                                code: item.dataSourceCode,
+                                name: item.dataSourceName
+                            }
+                        }else {
+                            return null
+                        }
+                    }).filter(item => !!item);
+
+                    const dataSources = [];
+                    allDataSources.forEach(ad => {
+                        if(!dataSources.find(d => d.code === ad.code)) {
+                            dataSources.push(ad);
+                        }
+                    });
+
+                    let data = {
+                        code:  resData.id+'',
+                        name: resData.bdName,
+                        items: items,
+                        description: resData.bdNote || '',
+                        creatorCode: resData.createId + '',
+                        creatorName: resData.createBy,
+                        createTime: resData.createDate,
+                        dataSources: dataSources,
+                        relationColumns: relationColumns,
+                        editMode: currentUser.code === resData.createId + '',
+                        filters: JSON.parse((resData.filters|| "[]")),
+                        shareCode: resData.bdCode,
+                        chartCodes: chartCodes,
+                        demo: resData.demo
+                    }
+
+                    let fields = [];
+                    for(let key in data) {
+                        fields.push({
+                            name: key,
+                            value: data[key]
+                        })
+                    }
+                    yield put({ type: 'dashboardDesigner/silentSetFields', fields: fields });
+                }else {
+                    message.error('解析看板错误: ' + (res.err || res.data.msg));
+                }
+            }catch(e) {
+                message.error('解析看板错误: ' + e);
+            }finally {
+                yield put({ type: 'dashboardDesigner/silentSetField', name: 'loading', value: false });
+            }
+        },
+        *getShareKey (action, { select, call, put }) {
+            const record = action.record;
+            const delay = action.delay;
+            if(!record){
+                return
+            }
+            try {
+                const res = yield call(service.fetch, {
+                    url: URLS.DASHBOARD_GET_SHAREKEY,
+                    method: 'POST',
+                    body: {
+                        id: record.code,
+                        delay: delay
+                    }
+                });
+                if(!res.err && res.data.code > 0) {
+                    const resData = res.data.data;
+                    return resData;
+                }else {
+                    message.error('生成分享链接失败: ' + (res.err || res.data.msg));
+                }
+            }catch(e) {
+                message.error('生成分享链接失败: ' + e);
+            }
+        },
+        *remoteShareKeyDetail(action, { select, call, put }) {
+            const code = action.code;
+            if(!code){
+                return
+            }
+            try {
+                yield put({ type: 'dashboardDesigner/silentSetField', name: 'loading', value: true });
+                const res = yield call(service.fetch, {
+                    url: URLS.DASHBOARD_SHARE_DETAIL_BY_KEY,
+                    method: 'GET',
+                    allow: true,
+                    body: {
+                        data: code
+                    }
+                });
+                if(!res.err && res.data.code > 0) {
+                    const resData = res.data.data;
+                    let items = resData.bdConfiguration ? JSON.parse(resData.bdConfiguration) : [];
+                    let relationColumns = resData.relationColumns ? JSON.parse(resData.relationColumns) : [];
+                    let chartCodes = resData.chartIds ? resData.chartIds.split(',') : [];
+                    const main = yield select(state => state.present.main);
+                    const { currentUser } = main;
+
+                    const allDataSources = items.map(item => {
+                        if(item.viewType === 'chart') {
+                            return {
+                                code: item.dataSourceCode,
+                                name: item.dataSourceName
+                            }
+                        }else {
+                            return null
+                        }
+                    }).filter(item => !!item);
+
+                    const dataSources = [];
+                    allDataSources.forEach(ad => {
+                        if(!dataSources.find(d => d.code === ad.code)) {
+                            dataSources.push(ad);
+                        }
+                    });
+
+                    let data = {
+                        code:  resData.id+'',
+                        name: resData.bdName,
+                        items: items,
+                        description: resData.bdNote || '',
+                        creatorCode: resData.createId + '',
+                        creatorName: resData.createBy,
+                        createTime: resData.createDate,
+                        dataSources: dataSources,
+                        relationColumns: relationColumns,
+                        editMode: currentUser.code === resData.createId + '',
+                        filters: JSON.parse((resData.filters|| "[]")),
+                        shareCode: resData.bdCode,
+                        chartCodes: chartCodes,
+                        demo: resData.demo
+                    }
+
+                    let fields = [];
+                    for(let key in data) {
+                        fields.push({
+                            name: key,
+                            value: data[key]
+                        })
+                    }
+                    yield put({ type: 'dashboardDesigner/silentSetFields', fields: fields });
+                }else {
+                    message.error('解析看板错误: ' + (res.err || res.data.msg));
+                }
+            }catch(e) {
+                message.error('解析看板错误: ' + e);
+            }finally {
+                yield put({ type: 'dashboardDesigner/silentSetField', name: 'loading', value: false });
+            }
+        },
+        *copy(action, { select, call, put }) {
+            const { dashboardCode, dataConnectCode } = action;
+
+            const res = yield call(service.fetch, {
+                url: URLS.DASHBOARD_COPY,
+                body: {
+                    dashboardId: dashboardCode,
+                    dataSourceId: 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: {
         setup({ dispatch, history}) {

+ 103 - 61
src/models/dashboardDesigner.js

@@ -9,33 +9,46 @@ 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({
+                        dataSourceCode: re.dataSource.code,
+                        name: re.column.name,
+                        operator: f.operator,
+                        type: f.type,
+                        value1: f.value1,
+                        value2: f.value2,
+                        using: f.using
+                    });
+                }
+            });
+        }else {
+            if(f.dataSource.name === item.dataSourceCode) {
+                trueFilters.push({
+                    dataSourceCode: f.dataSource.name,
+                    name: f.name,
+                    operator: f.operator,
+                    type: f.type,
+                    value1: f.value1,
+                    value2: f.value2,
+                    using: f.using
                 });
             }
-        });
+        }
     });
     return trueFilters;
 }
 
 function getBodyFilters(filters) {
     return filters.filter(f => f.using).map(f => {
-        let { name, operator, type, value1, value2 } = f;
+        let { dataSourceCode, name, operator, type, value1, value2 } = f;
         let bodyFilter = {
+            dataSourceCode,
             columnName: name,
             columnType: type,
             symbol: operator,
@@ -67,6 +80,7 @@ export default {
             name: '无标题',
             defaultLayout: { x: 0, y: 50, w: 12, h: 6, minW: 2, maxW: 12, minH: 1 },
             items: [],
+            chartCodes: [], // 报表包含的所有图表
             description: '',
             thumbnail: '',
             dirty: false,
@@ -77,6 +91,8 @@ export default {
             relationColumns: [], // 自定义的列
             columnFetching: false,
             loading: false,
+            shareCode: '', // 分享码
+            demo: false
         },
     },
     
@@ -110,7 +126,7 @@ export default {
             return Object.assign({}, newState, {dirty: true});
         },
         addChart(state, action) {
-            let { items, dataSources, defaultLayout } = state;
+            let { items, dataSources, chartCodes, defaultLayout } = state;
             const { chart } = action;
 
             items = items.concat([{
@@ -121,44 +137,22 @@ export default {
                 creatorName: chart.creatorName,
                 dataSourceCode: chart.dataSourceCode+'',
                 dataSourceName: chart.dataSourceName,
+                dataConnectCode: chart.dataConnectCode,
+                dataConnectName: chart.dataConnectName,
                 viewType: 'chart',
                 chartType: chart.type,
                 filters: chart.filters,
                 layout: defaultLayout,
             }]);
+            chartCodes.push(chart.code);
             dataSources.findIndex(d => d.code === chart.dataSourceCode+'') === -1 && dataSources.push({
                 code: chart.dataSourceCode+'',
                 name: chart.dataSourceName
             });
-            return Object.assign({}, state, {items, dataSources, dirty: true});
-        },
-        addCharts(state, action) {
-            let { items, dataSources, defaultLayout } = state;
-            const { charts } = action;
-
-            items = items.concat(charts.map(c => {
-                dataSources.findIndex(d => d.code === c.dataSourceCode+'') === -1 && dataSources.push({
-                    code: c.dataSourceCode+'',
-                    name: c.dataSourceName
-                });
-                return {
-                    code: c.code,
-                    chartCode: c.code,
-                    name: c.name,
-                    creatorCode: c.creatorCode,
-                    creatorName: c.creatorName,
-                    viewType: 'chart',
-                    chartType: c.type,
-                    dataSourceCode: c.dataSourceCode+'',
-                    dataSourceName: c.dataSourceName,
-                    filters: c.filters,
-                    layout: defaultLayout,
-                }
-            }));
-            return Object.assign({}, state, {items, dataSources, dirty: true});
+            return Object.assign({}, state, {items, chartCodes, dataSources, dirty: true});
         },
         deleteItem(state, action) {
-            let { items, dataSources, relationColumns, dirty } = state;
+            let { items, chartCodes, dataSources, relationColumns, dirty } = state;
             const { item } = action;
 
             let count = 0;
@@ -174,9 +168,10 @@ export default {
             }
 
             if(count === 1) {
-                // 找到只有被删除的item使用的数据源并删除
                 let idx = dataSources.findIndex(d => d.code === item.dataSourceCode);
+                let idx2 = chartCodes.findIndex(c => c === item.chartCode);
                 dataSources.splice(idx, 1);
+                chartCodes.splice(idx2, 1);
                 // 同时删除已定义的关联字段
                 relationColumns.forEach(rc => {
                     rc.relations.forEach((r, x) => {
@@ -348,25 +343,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 });
             }
@@ -374,11 +393,12 @@ export default {
         *fetchChartData(action, { put, call, select }) {
             const { item, mandatory, page, pageSize } = action;
             const dashboardDesigner = yield select(state => state.present.dashboardDesigner);
-            const { filters, relationColumns } = dashboardDesigner;
+            const { creatorCode, filters } = dashboardDesigner;
             const { chartCode } = item;
             const body = {
-                dashCreateId: chartCode,
-                filters: getBodyFilters(getTrueFilters(item, filters, relationColumns)),
+                dashboardCreatorId: creatorCode,
+                chartId: chartCode,
+                filters: getBodyFilters(getTrueFilters(item, filters)),
                 testPage: {
                     pageNum: page|| 1,
                     pageSize: pageSize || 25, 
@@ -392,12 +412,19 @@ export default {
                 yield put({ type: 'setItemFetching', code: chartCode, fetching: true });
                 const res = yield call(service.fetch, {
                     url: URLS.CHART_OPTION,
+                    allow: true,
                     body,
                     timeout: 30000
                 });
-                console.log('看板请求图表展示数据', body, res);
                 if(!res.err && res.data.code > 0) {
                     let resData = res.data.data;
+                    if(!resData) {
+                        yield put({ type: 'setItemFields', code: chartCode, fields: [
+                            { name: 'chartType', value: '' },
+                            { name: 'chartOption', value: {} }
+                        ] });
+                        return false;
+                    }
                     const { chartType : ctype, chartConfig: cfg } = resData.chartsColumnConfig;
                     const chartType = CHART_TYPE[ctype];
                     const chartConfig = JSON.parse(cfg);
@@ -412,7 +439,6 @@ export default {
                         { name: 'chartType', value: '' },
                         { name: 'chartOption', value: {} }
                     ] });
-                    console.error(body, res.err || res.data.msg);
                     message.error('请求图表展示数据失败: ' + (res.err || res.data.msg));
                 }
             }catch(e) {
@@ -439,6 +465,22 @@ export default {
                 console.log(e);
                 message.error('生成缩略图失败: ' + e);
             }
+        },
+        *encryptCode(action, { put, call, select }) {
+            const { shareCode } = action;
+            const res = yield call(service.fetch, {
+                url: URLS.DASHBOARD_ENCRYPT_CODE,
+                method: 'GET',
+                body: {
+                    code: shareCode
+                },
+            });
+            if(!res.err && res.data.code > 0) {
+                let resData = res.data.data;
+                return resData;
+            }else {
+                return false;
+            }
         }
     },
     subscriptions: {

+ 62 - 66
src/models/dataConnect.js

@@ -91,6 +91,7 @@ export default {
             delete newOne.userName;
             delete newOne.password;
             delete newOne.description;
+            delete newOne.demo;
             return Object.assign({}, state, {newOne});
         },
         setNewModelInvalid(state, action) {
@@ -118,6 +119,7 @@ export default {
                 }
                 const res = yield call(service.fetch, {
                     url: URLS.DATACONNECT_LIST,
+                    method: 'GET',
                     body
                 });
                 console.log('请求数据连接配置列表', body,  res);
@@ -133,7 +135,8 @@ export default {
                             port: r.port,
                             userName: r.userName,
                             password: r.passWord,
-                            description: r.note
+                            description: r.note,
+                            demo: r.demo
                         }
                     });
                     yield put({ type: 'list', data });
@@ -167,8 +170,6 @@ export default {
                     body: validBody
                 });
 
-                console.log('校验数据连接配置', validBody, res);
-
                 // 设置validating为false
                 yield put({ type: 'setNewModelField', name: 'validating', value: false });
 
@@ -176,13 +177,14 @@ export default {
                     // 如果合法
                     // 设置valid为true
                     yield put({ type: 'setNewModelInvalid', name: 'invalid', value: false });
+                    message.success('测试通过');
                 }else {
                     message.error('校验失败: ' + (res.err || res.data.msg));
                     yield put({ type: 'setNewModelInvalid', name: 'invalid', value: true });
                 }
             }catch(e) {
-                console.log(e);
                 message.error('校验失败: ' + e);
+                yield put({ type: 'setNewModelField', name: 'validating', value: false });
             }
         },
         *remoteAdd(action, { select, call, put, takeEvery, takeLatest }) {
@@ -190,32 +192,24 @@ export default {
                 const dataConnect = yield select(state => state.present.dataConnect);
                 const model = dataConnect.newOne;
 
-                let flag = false;
-                if(model.invalid === false) {
-                    flag = true;
-                }else {
-                    // 设置validating为true
-                    yield put({ type: 'setNewModelField', name: 'validating', value: true });
-                    // 调用检测接口检测连接配置是否合法
-                    let validBody = {
-                        id: model.code,
-                        name: model.name,
-                        addrass: model.address,
-                        port: model.port,
-                        databaseType: model.dbType,
-                        dataName: model.dbName,
-                        userName: model.userName,
-                        passWord: model.password
-                    }
-                    const validRes = yield call(service.fetch, {
-                        url: URLS.DATACONNECT_VALIDATE,
-                        body: validBody
-                    });
-                    flag = !validRes.err && validRes.data.code > 0
-                    // 设置validating为false
-                    yield put({ type: 'setNewModelField', name: 'validating', value: false });
+                // 设置validating为true
+                yield put({ type: 'setNewModelField', name: 'saving', value: true });
+                // 调用检测接口检测连接配置是否合法
+                let validBody = {
+                    id: model.code,
+                    name: model.name,
+                    addrass: model.address,
+                    port: model.port,
+                    databaseType: model.dbType,
+                    dataName: model.dbName,
+                    userName: model.userName,
+                    passWord: model.password
                 }
-
+                const validRes = yield call(service.fetch, {
+                    url: URLS.DATACONNECT_VALIDATE,
+                    body: validBody
+                });
+                let flag = !validRes.err && validRes.data.code > 0;
                 if(flag) {
                     // 如果合法
                     // 设置valid为true
@@ -236,19 +230,26 @@ export default {
                         url: URLS.DATACONNECT_ADD,
                         body: body
                     });
+                    yield put({ type: 'setNewModelField', name: 'saving', value: false });
+
                     if(!res.err && res.data.code > 0) {
-                        yield put({ type: 'add' });
-                        yield put({ type: 'setNewModelField', name: 'visibleBox', value: false });
+                        yield put({ type: 'fetchList', mandatory: true });
                         message.success('新增成功');
+                        return true;
                     }else {
                         message.error('新增失败: ' + (res.err || res.data.msg));
+                        return false;
                     }
                 }else {
+                    message.error('新增失败: ' + validRes.data.msg);
                     yield put({ type: 'setNewModelInvalid', name: 'invalid', value: true });
+                    yield put({ type: 'setNewModelField', name: 'saving', value: false });
+                    return false;
                 }
             }catch(e) {
-                console.log(e);
                 message.error('新增失败: ' + e);
+                yield put({ type: 'setNewModelField', name: 'saving', value: false });
+                return false;
             }
         },
         *remoteModify(action, { select, call, put }) {
@@ -256,31 +257,25 @@ export default {
                 const dataConnect = yield select(state => state.present.dataConnect);
                 const model = dataConnect.newOne;
 
-                let flag = false;
-                if(model.invalid === false) {
-                    flag = true;
-                }else {
-                    // 设置validating为true
-                    yield put({ type: 'setNewModelField', name: 'validating', value: true });
-                    // 调用检测接口检测连接配置是否合法
-                    let validBody = {
-                        id: model.code,
-                        name: model.name,
-                        addrass: model.address,
-                        port: model.port,
-                        databaseType: model.dbType,
-                        dataName: model.dbName,
-                        userName: model.userName,
-                        passWord: model.password
-                    }
-                    const validRes = yield call(service.fetch, {
-                        url: URLS.DATACONNECT_VALIDATE,
-                        body: validBody
-                    });
-                    flag = !validRes.err && validRes.data.code > 0;
-                    // 设置validating为false
-                    yield put({ type: 'setNewModelField', name: 'validating', value: false });
+                // 设置validating为true
+                yield put({ type: 'setNewModelField', name: 'saving', value: true });
+                // 调用检测接口检测连接配置是否合法
+                let validBody = {
+                    id: model.code,
+                    name: model.name,
+                    addrass: model.address,
+                    port: model.port,
+                    databaseType: model.dbType,
+                    dataName: model.dbName,
+                    userName: model.userName,
+                    passWord: model.password
                 }
+                const validRes = yield call(service.fetch, {
+                    url: URLS.DATACONNECT_VALIDATE,
+                    body: validBody
+                });
+                let flag = !validRes.err && validRes.data.code > 0;
+
                 if(flag) {
                     // 如果合法
                     // 设置valid为true
@@ -304,26 +299,27 @@ export default {
                         url: URLS.DATACONNECT_UPDATE,
                         body: data
                     });
-                    console.log('修改数据连接配置', data, res);
+
+                    yield put({ type: 'setNewModelField', name: 'saving', value: false });
+
                     if(!res.err && res.data.code > 0) {
-                        let list = dataConnect.list;
-                        list = list.map(l => {
-                            if((l.code+'') === (action.code+'')) {
-                                l = model;
-                            }
-                            return l;
-                        });
-                        yield put({ type: 'list', data: list });
+                        yield put({ type: 'fetchList', mandatory: true });
                         message.success('修改成功');
-                        yield put({ type: 'setNewModelField', name: 'visibleBox', value: false });
+                        return true;
                     }else {
                         message.error('修改失败: ' + (res.err || res.data.msg));
+                        return false;
                     }
                 }else {
+                    message.error('修改失败: ' + validRes.data.msg);
                     yield put({ type: 'setNewModelInvalid', name: 'invalid', value: true });
+                    yield put({ type: 'setNewModelField', name: 'saving', value: false });
+                    return false;
                 }
             }catch(e) {
                 message.error('修改失败: ' + e);
+                yield put({ type: 'setNewModelField', name: 'saving', value: false });
+                return false;
             }
         },
         *remoteDelete(action, { select, call, put, takeEvery, takeLatest }) {

+ 47 - 34
src/models/dataSource.js

@@ -132,26 +132,38 @@ export default {
                 }
                 const res = yield call(service.fetch, {
                     url: URLS.DATASOURCE_LIST,
+                    method: 'GET',
                     body
                 });
                 
-                console.log('请求数据源列表', body, res);
                 if(!res.err && res.data.code > 0) {
                     let list = res.data.data.list.map((r, i) => {
-                        let dbConfig = JSON.parse(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),
                             tags: tags,
                             description: r.note,
-                            groupCode: r.connectorGroup+''
+                            groupCode: r.connectorGroup+'',
+                            demo: r.demo
                         }
                     });
                     yield put({ type: 'list', list });
@@ -159,7 +171,6 @@ export default {
                     message.error('读取数据源列表错误: ' + (res.err || res.data.msg));
                 }
             }catch(e) {
-                console.error(body, e);
                 message.error('读取数据源列表错误: ' + e);
             }
             
@@ -167,8 +178,7 @@ export default {
         *remoteAdd(action, { select, call, put }) {
             const dataSourceDetail = yield select(state => state.present.dataSourceDetail);
             const currentUserName = yield select(state => state.present.main.currentUser.name)
-            const { name, description, target, tags, type, connectCode, address, port, dbType, dbName, userName,
-                password, columns, group } = dataSourceDetail;
+            const { name, description, target, tags, type, connectCode, columns, group } = dataSourceDetail;
             try {
                 const data = {
                     dataName: name,
@@ -177,15 +187,7 @@ export default {
                     dataTag: tags,
                     type: type,
                     createBy: currentUserName,
-                    dbConfig: {
-                        id: connectCode,
-                        addrass: address,
-                        port: port,
-                        databaseType: dbType,
-                        dataName: dbName,
-                        userName: userName,
-                        passWord: password
-                    },
+                    dbConId: connectCode,
                     columnConfig: columns.map((c, i) => {
                         return {
                             columnName: c.name,
@@ -224,19 +226,21 @@ export default {
 
                 const res = yield call(service.fetch, {
                     url: URLS.DATASOURCE_DETAIL,
-                    body: code
+                    method: 'GET',
+                    body: {
+                        id: code
+                    }
                 });
-                console.log('解析数据源', code, res);
                 if(!res.err && res.data.code > 0) {
                     let resData = res.data.data;
                     let columnConfig = JSON.parse(resData.columnConfig) || [];
-                    let dbConfig = JSON.parse(resData.dbConfig);
+                    let dbConfig = resData.dbConfig;
                     let tags = JSON.parse(resData.dataTag);
                     let data = {
                         code: resData.dataId + '',
                         name: resData.dataName,
                         type: resData.type,
-                        connectCode: dbConfig.id + '',
+                        connectCode: dbConfig.id,
                         dbType: dbConfig.databaseType,
                         dbName: dbConfig.dataName,
                         address: dbConfig.addrass,
@@ -263,7 +267,8 @@ export default {
                                 bucketizable: c.isSubsection==='1'?true:false,
                                 description: c.remarks
                             }
-                        })
+                        }),
+                        demo: resData.demo
                     }
                     let fields = [];
                     for(let key in data) {
@@ -277,7 +282,6 @@ export default {
                     message.error('数据源解析错误: ' + (res.err || res.data.msg));
                 }
             }catch(e) {
-                console.log(e);
                 message.error('数据源解析错误: ' + e);
                 yield put({ type: 'list', list: [] });
             }
@@ -312,7 +316,7 @@ export default {
             try{
                 const dataSourceDetail = yield select(state => state.present.dataSourceDetail);
                 const currentUserName = yield select(state => state.present.main.currentUser.name)
-                const { code, name, description, target, type, group, address, port, dbType, dbName,userName, password, columns } = dataSourceDetail;
+                const { code, name, description, target, type, group, connectCode, columns } = dataSourceDetail;
     
                 let data = {
                     dataId: code,
@@ -323,14 +327,7 @@ export default {
                     type: type,
                     createBy: currentUserName,
                     connectorGroup: group ? group : '-1',
-                    dbConfig: address ? {
-                        addrass: address,
-                        port: port,
-                        databaseType: dbType,
-                        dataName: dbName,
-                        userName: userName,
-                        passWord: password
-                    } : '',
+                    dbConId: connectCode,
                     columnConfig: columns ? columns.map((c, i) => {
                         return {
                             columnName: c.name,
@@ -761,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 => ({
@@ -784,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: {

+ 2 - 0
src/models/dataSourceDetail.js

@@ -25,6 +25,7 @@ export default {
             group: '-1',
             notice: '',
             columns: [],
+            demo: false
         },
         code: null,
         name: '未命名',
@@ -44,6 +45,7 @@ export default {
         group: '-1',
         notice: '',
         columns: [],
+        demo: false
     },
     reducers: {
         setField(state, action) {

+ 1 - 0
src/models/defaultColumnType.json

@@ -1,6 +1,7 @@
 {
     "String": "categorical",
     "Date": "time",
+    "Timestamp": "time",
     "BigDecimal": "scale",
     "Double": "scale",
     "Long": "scale",

+ 129 - 19
src/models/main.js

@@ -3,20 +3,31 @@
  */
 import { routerRedux } from 'dva/router'
 import { message } from 'antd'
+import * as service from '../services/index'
+import URLS from '../constants/url'
+import moment from 'moment'
 
-
-const code = window.localStorage.getItem('usercode');
+const code = window.sessionStorage.getItem('usercode');
 const account = window.localStorage.getItem('account');
 const password = window.localStorage.getItem('password');
-const name = window.localStorage.getItem('username');
-const role = window.localStorage.getItem('userrole');
-const department = window.localStorage.getItem('department');
-const job = window.localStorage.getItem('job');
+const name = window.sessionStorage.getItem('username');
+const role = window.sessionStorage.getItem('userrole');
+const department = window.sessionStorage.getItem('department');
+const job = window.sessionStorage.getItem('job');
+const expireTime = window.sessionStorage.getItem('expireTime');
+const autoLogin = window.localStorage.getItem('autoLogin') ? window.localStorage.getItem('autoLogin') === 'true' : true;
+
+const t = moment(+expireTime);
+const isLogin = expireTime && t.isValid();
+const authenticated = t.isValid() && t.diff(moment()) > 0;
 
 export default {
     namespace: 'main',
     state: {
-        authenticated: false,
+        authenticated: authenticated,
+        autoLogin: autoLogin,
+        isLogin: isLogin,
+        expireTime,
         currentUser: {
             code,
             account,
@@ -36,10 +47,13 @@ export default {
             const { user } = action;
             return { ...state, currentUser: user };
         },
-        setAuthenticated(state, action) {
-            const { authenticated } = action;
-            return { ...state, authenticated };
-        }
+        setFields(state, action) {
+            const { fields } = action;
+            let obj = {};
+            fields.map(f => (obj[f.name] = f.value));
+            let newState = Object.assign({}, state, obj);
+            return Object.assign({}, newState );
+        },
     },
     effects: {
         * redirect (action, { put }) {
@@ -60,16 +74,78 @@ export default {
                 window.location.reload();
             }
         },
+        *login(action, {put, call}) {
+            const { username, password, autoLogin } = action;
+
+            function authenticate(token, expireTime, user, autoLogin) {
+                window.sessionStorage.setItem("loginTime", new Date().getTime());
+                window.sessionStorage.setItem("token", token);
+                window.sessionStorage.setItem("expireTime", expireTime);
+            
+                window.sessionStorage.setItem("usercode", user.code);
+                autoLogin ? window.localStorage.setItem("autoLogin", 'true') : window.localStorage.setItem("autoLogin", 'false');
+                window.localStorage.setItem("account", user.account)
+                window.sessionStorage.setItem("account", user.account);
+                autoLogin ? window.localStorage.setItem("password", user.password) : window.localStorage.removeItem('password');
+                window.sessionStorage.setItem("username", user.name);
+                window.sessionStorage.setItem("userrole", user.role);
+                window.sessionStorage.setItem("department", user.department);
+                window.sessionStorage.setItem("job", user.job);
+            }
+
+            const body = {
+                userName: username,
+                passWord: password
+            };
+            try {
+                const res = yield call(service.fetch, {
+                    url: URLS.LOGIN,
+                    allow: true,
+                    body
+                });
+                if(!res.err && res.data.code > 0) {
+                    const resData = res.data.data;
+
+                    const token = resData.token;
+                    const expireTime = resData.times;
+                    const user = resData.user;
+                    const currentUser = {
+                        code: user.id+'',
+                        account: user.userName,
+                        password: user.passWord,
+                        name: user.name,
+                        role: user.role || 'default',
+                        department: user.department,
+                        job: user.post,
+                    };
+                    yield put({ type: 'setFields', fields: [
+                        { name: 'isLogin', value: true },
+                        { name: 'autoLogin', value: autoLogin },
+                        { name: 'authenticated', value: true },
+                        { name: 'currentUser', value: currentUser }
+                    ]});
+                    authenticate(token, expireTime, currentUser, autoLogin);
+                    return true;
+                }else {
+                    message.error('登录失败: ' + (res.err || res.data.msg));
+                    return false;
+                }
+            }catch(e) {
+                message.error('登录失败: ' + e);
+                console.error(body, e);
+                return false;
+            }
+        },
         *logout( action, { put, call, select }) {
             try {
-                window.localStorage.removeItem("username");
-                window.localStorage.removeItem("userrole");
-                window.localStorage.removeItem("usercode");
-                window.localStorage.removeItem("department");
-                window.localStorage.removeItem("job");
-                window.localStorage.removeItem("loginTime");
-                window.localStorage.removeItem("token");
-                window.localStorage.removeItem("expireTime");
+                window.sessionStorage.removeItem("username");
+                window.sessionStorage.removeItem("userrole");
+                window.sessionStorage.removeItem("usercode");
+                window.sessionStorage.removeItem("department");
+                window.sessionStorage.removeItem("job");
+                window.sessionStorage.removeItem("loginTime");
+                window.sessionStorage.removeItem("token");
+                window.sessionStorage.removeItem("expireTime");
 
                 yield put({ type: 'dataConnect/reset' });
                 yield put({ type: 'dataSource/reset' });
@@ -78,6 +154,14 @@ export default {
                 yield put({ type: 'user/reset' });
                 yield put({ type: 'userGroup/reset', list: [] });
                 yield put({ type: 'recent/reset' });
+                yield put({ type: 'setFields', fields: [
+                    { name: 'isLogin', value: false },
+                    { name: 'authenticated', value: false },
+                    { name: 'currentUser', value: {
+                        account,
+                        password: autoLogin ? password : null
+                    } }
+                ]});
                 // yield put({ type: 'recent/listRecentDashboard', recentDashboard: [] });
             }catch(e) {
                 console.log(e);
@@ -92,6 +176,32 @@ export default {
                 duration: 2,
                 maxCount: 3,
             });
+
+            let checkExpireTime = () => {
+                let expireTime = window.sessionStorage.getItem('expireTime');
+                let t = moment(+expireTime).diff(moment())
+                return t >= 0 ? t : 0;
+            }
+        
+            let onExpired = () => {
+                dispatch({ type: 'setFields', fields: [{ name: 'authenticated', value: false}] });
+            }
+
+            let hiddenProperty = 'hidden' in document ? 'hidden' :    
+                'webkitHidden' in document ? 'webkitHidden' :    
+                'mozHidden' in document ? 'mozHidden' :    
+                null;
+            let visibilityChangeEvent = hiddenProperty.replace(/hidden/i, 'visibilitychange');
+            let timeoutKey = window.setTimeout(onExpired, checkExpireTime());
+            let onVisibilityChange = () => {
+                if (document[hiddenProperty]) {
+                    window.clearTimeout(timeoutKey);
+                }else{
+                    timeoutKey = window.setTimeout(onExpired, checkExpireTime());
+                }
+            }
+            document.addEventListener(visibilityChangeEvent, onVisibilityChange);
+
             return history.listen(({ pathname, query }) => {
                 let page = pathname.match(/\/(\w*)/)[1];
                 dispatch({ type: 'setCurrentPage', page });

+ 5 - 3
src/models/parseChartOption.js

@@ -45,6 +45,7 @@ export default function(viewType, data, chartConfig) {
 function barOption(data, barConfig) {
     let xTitle = barConfig.xAxis?`${barConfig.xAxis.column.label}${barConfig.xAxis.granularity.value?'('+barConfig.xAxis.granularity.label+')':''}`:null
     let yTitle = barConfig.yAxis?`${barConfig.yAxis.column.label}${barConfig.yAxis.gauge.value?'('+barConfig.yAxis.gauge.label+')':''}`:null
+    data.serieses = data.serieses || [];
 
     let option = {
         tooltip : {
@@ -57,7 +58,7 @@ function barOption(data, barConfig) {
             }
         },
         legend: {
-            show: true
+            show: data.serieses.length > 1
         },
         grid: {
             left: '10%',
@@ -130,7 +131,8 @@ function pieOption(data, pieConfig) {
 function lineOption(data, lineConfig) {
     let xTitle = lineConfig.xAxis?`${lineConfig.xAxis.column.label}${lineConfig.xAxis.granularity.value?'('+lineConfig.xAxis.granularity.label+')':''}`:null
     let yTitle = lineConfig.yAxis?`${lineConfig.yAxis.column.label}${lineConfig.yAxis.gauge.value?'('+lineConfig.yAxis.gauge.label+')':''}`:null
-    
+    data.serieses = data.serieses || [];
+
     let option = {
         tooltip: {
             trigger: 'axis',
@@ -139,7 +141,7 @@ function lineOption(data, lineConfig) {
             }
         },
         legend: {
-            show: true
+            show: data.serieses.length > 1
         },
         xAxis:  {
             name: xTitle || '横轴',

+ 4 - 3
src/models/userGroup.js

@@ -185,7 +185,6 @@ export default {
                     url: URLS.USERGROUP_DELETE,
                     body: [group.code]
                 });
-                console.log('删除用户组', [group.code], res);
                 if(!res.err && res.data.code > 0) {
                     yield put({ type: 'delete', group });
                     const userGroup = yield select(state => state.present.userGroup);
@@ -193,7 +192,6 @@ export default {
                     yield put({ type: 'changeSelectedGroup', group: list[0] });
                     message.success('删除成功');
                 }else {
-                    console.log([group.code], (res.err || res.data.msg));
                     message.error('删除失败: ' + (res.err || res.data.msg));
                 }
             }catch(e) {
@@ -203,7 +201,10 @@ export default {
         },
         *changeSelectedGroup(action, { put, call, select }) {
             const { group } = action;
-            if(!group) { return }
+            if(!group) {
+                yield put({ type: 'setSelectedGroup', group: null });
+                return
+            }
             yield put({ type: 'remoteMemberList', groupCode: group.code });
             const userGroup = yield select(state => state.present.userGroup);
             const { list } = userGroup;

+ 75 - 0
src/routes/authLayout.jsx

@@ -0,0 +1,75 @@
+import React from 'react'
+import { Redirect } from 'dva/router'
+import Loading from '../components/common/loading/loading'
+import Relogin from '../components/common/login/relogin'
+import { connect } from 'dva'
+import './authLayout.less'
+
+class AuthLayout extends React.Component {
+
+    constructor(props) {
+        super(props);
+        this.state = ({
+            checking: true
+        });
+    }
+
+    componentDidMount = () => {
+        this.t = window.setTimeout(() => {
+            this.setState({
+                checking: false
+            })
+        }, 1000);
+    }
+
+    componentWillUnmount = () => {
+        window.clearTimeout(this.t)
+    }
+
+    render() {
+        const { location, children, main } = this.props;
+        const { isLogin, authenticated,  } = main;
+        const { checking } = this.state
+
+        return (
+            isLogin ? (
+                <div style={{ width: '100%', height: '100%' }}>
+                    { children }
+                    { checking ? (
+                        <div style={{ position: 'absolute', top: 0, height: '100%', width: '100%', zIndex: '4', opacity: 0.5, background: 'rgba(51,51,51,.1)' }}>
+                            <Loading></Loading>
+                        </div>
+                    ) : (
+                        !authenticated && (
+                            <Relogin
+                                visibleBox={true}
+                            ></Relogin>
+                        )
+                    ) }
+                </div>
+            ) : (
+                <div style={{ width: '100%', height: '100%' }}>
+                    { children }
+                    { checking ? (
+                        <div style={{ position: 'absolute', top: 0, height: '100%', width: '100%', zIndex: '4', opacity: 0.5, background: 'rgba(51,51,51,.1)' }}>
+                            <Loading></Loading>
+                        </div>
+                    ) : (
+                        <Redirect
+                            to={{
+                                pathname: '/login',
+                                state: { from: location }
+                            }}
+                        ></Redirect>
+                    ) }
+                </div>
+            )
+        )
+    }
+}
+
+function mapStateToProps({ present: { main } }) {
+    return { main };
+}
+
+export default connect(mapStateToProps)(AuthLayout)

+ 0 - 0
src/routes/authLayout.less


+ 3 - 7
src/routes/mainLayout.js → src/routes/mainLayout.jsx

@@ -1,10 +1,10 @@
 import React from 'react'
 import { Layout } from 'antd'
 import { Route, Switch } from 'dva/router'
-import { Redirect } from 'dva/router'
 import Navigator from '../components/common/navigator'
 import HomePage from '../components/homePage/homePage'
 import Loading from '../components/common/loading/loading'
+import DataConnect from '../components/dataConnect/list'
 import DataSource from '../components/dataSource/list'
 import Dashboard from '../components/dashboard/list'
 import Chart from '../components/chart/list'
@@ -25,17 +25,13 @@ const MainLayout = ({ isAuthenticated }) => {
                 <Switch>
                     <Route sensitive path='/demo' component={Demo}/>
                     <Route sensitive path='/home' component={HomePage}/>
+                    <Route sensitive path='/dataconnect' component={DataConnect}/>
                     <Route sensitive path='/datasource' component={DataSource}/>
                     <Route sensitive path='/dashboard' component={Dashboard} />
                     <Route sensitive path='/chart' component={Chart} />
                     <Route sensitive path='/admin' component={Admin} />
                     <Route sensitive path='/userinfo' component={UserInfo} />
-                    <Route path='/' component={isAuthenticated ? HomePage : () => <Redirect
-                        to={{
-                            pathname: "/login",
-                            state: { from: '/' }
-                        }}
-                    />}/>
+                    <Route path='/' component={HomePage}/>
                 </Switch>
             </Content>
         </Layout>

+ 4 - 8
src/routes/privateRoute.jsx

@@ -1,17 +1,13 @@
 import { Route } from 'dva/router'
-import RootLayout from '../components/common/rootLayout'
-import moment from 'moment'
+import AuthLayout from './authLayout'
 
 export default ({ component: Component, ...rest }) => (
     <Route
         {...rest}
         render={props =>{
-
-            const t = moment(+window.localStorage.getItem('expireTime'));
-            const isAuthenticated = t.isValid() && t.diff(moment()) > 0;
-            return <RootLayout location={props.location} isAuthenticated={isAuthenticated}>
-                <Component isAuthenticated={isAuthenticated} {...props} />
-            </RootLayout>
+            return <AuthLayout location={props.location}>
+                <Component {...props} />
+            </AuthLayout>
         }}
     />
 );

+ 6 - 0
src/routes/router.js

@@ -8,10 +8,13 @@ import MainLayout from './mainLayout'
 import DataSourceDetail from '../components/dataSourceDetail/layout'
 import ChartDesigner from '../components/chartDesigner/layout'
 import DashboardDesigner from '../components/dashboardDesigner/layout'
+import DashboardShareView from '../components/dashboard/shareView'
+import DashboardShareKeyView from '../components/dashboard/shareKeyView'
 // 由于 antd 组件的默认文案是英文,所以需要修改为中文
 import zhCN from 'antd/lib/locale-provider/zh_CN'
 import Demo from '../demo';
 import Xiaomi from '../xiaomi'
+import ShareQR from '../components/common/shareQR/shareQR'
 
 
 function RouterConfig({ history }) {
@@ -23,6 +26,9 @@ function RouterConfig({ history }) {
                     <Route sensitive path='/register' component={Register} />
                     <Route sensitive path='/demo' component={Demo} />
                     <Route sensitive path='/xiaomi' component={Xiaomi} />
+                    <Route sensitive path='/sqr' component={ShareQR} />
+                    <Route sensitive path='/dashboard/share/:code' component={DashboardShareView} />
+                    <Route sensitive path='/dashboard/share_key/:code' component={DashboardShareKeyView} />
                     <PrivateRoute sensitive path='/datasource/:type/:code/:tab' component={DataSourceDetail}/>
                     <PrivateRoute sensitive path='/chart/:code' component={ChartDesigner} />
                     <PrivateRoute sensitive path='/dashboard/:code/' component={DashboardDesigner} />

+ 38 - 10
src/services/index.js

@@ -1,14 +1,42 @@
 import request from '../utils/request'
 
 export function fetch(option) {
-  const { url, body, timeout } = option;
-  const token = window.localStorage.getItem("token");
-  return request(url, {
-    method: 'POST',
-    headers: {
-      token: token,
-      'Content-Type': 'application/json'
-    },
-    body: JSON.stringify(body),
-  }, timeout);
+    let { url, method, body, timeout, allow } = option;
+    const token = window.sessionStorage.getItem("token");
+    // 除非特殊许可,Token不存在时不发送请求
+    if(token || allow) {
+        let opt = {
+            method: method || 'POST',
+        };
+        if(method === 'GET') {
+            opt.headers = {
+                token: token,
+                'Content-Type': 'application/json'
+            }
+            if (body) {  
+                let paramsArray = [];  
+                //拼接参数  
+                Object.keys(body).forEach(key => paramsArray.push(key + '=' + body[key]))  
+                if (url.search(/\?/) === -1) {  
+                    url += '?' + paramsArray.join('&')  
+                } else {  
+                    url += '&' + paramsArray.join('&')  
+                }  
+            }
+        }else {
+            opt.headers = {
+                'Content-Type': 'application/json'
+            }
+            opt.body = JSON.stringify(body);
+
+            if(!!token) {
+                opt.headers.token = token
+            }
+        }
+
+        return request(url, opt, timeout);
+    }else {
+        // 使用一个空的Promise组织后续链式方法的调用
+        return new Promise(() => {});
+    }
 }

+ 2 - 2
src/utils/request.js

@@ -5,11 +5,11 @@ function parseJSON(response) {
 }
 
 function checkStatus(response) {
-  if (response.status >= 200 && response.status < 300) {
+  if (response && (response.status >= 200 && response.status < 300)) {
     return response;
   }
 
-  const error = new Error(response.statusText);
+  const error = new Error(response ? response.statusText : 'Error Fetch');
   error.response = response;
   throw error;
 }