Browse Source

图表缩略图/看板列表/看板编辑界面/

zhuth 7 years ago
parent
commit
0b63847cdc
40 changed files with 1423 additions and 573 deletions
  1. 0 4
      public/fonts/iconfont/custom/iconfont.css
  2. BIN
      public/fonts/iconfont/custom/iconfont.eot
  3. 0 0
      public/fonts/iconfont/custom/iconfont.js
  4. 3 7
      public/fonts/iconfont/custom/iconfont.svg
  5. BIN
      public/fonts/iconfont/custom/iconfont.ttf
  6. BIN
      public/fonts/iconfont/custom/iconfont.woff
  7. BIN
      public/images/chart-default.png
  8. BIN
      public/images/table-default.png
  9. 1 1
      src/components/chart/chooseDataSourceBox.jsx
  10. 12 7
      src/components/chart/list.jsx
  11. 21 1
      src/components/chart/list.less
  12. 57 53
      src/components/chart/resolveChartOption.js
  13. 10 4
      src/components/chart/thumbnail.jsx
  14. 1 1
      src/components/chartDesigner/content.less
  15. 4 4
      src/components/chartDesigner/header.jsx
  16. 2 2
      src/components/common/navigator.jsx
  17. 205 0
      src/components/dashboard/list.jsx
  18. 173 0
      src/components/dashboard/list.less
  19. 33 13
      src/components/dashboardDesigner/chartView.jsx
  20. 216 0
      src/components/dashboardDesigner/chooseChartBox.jsx
  21. 14 0
      src/components/dashboardDesigner/chooseChartBox.less
  22. 76 4
      src/components/dashboardDesigner/content.jsx
  23. 0 79
      src/components/dashboardDesigner/elementConfig.jsx
  24. 20 30
      src/components/dashboardDesigner/header.jsx
  25. 5 116
      src/components/dashboardDesigner/layout.jsx
  26. 24 6
      src/components/dashboardDesigner/layout.less
  27. 21 0
      src/components/dashboardDesigner/richTextEditor.jsx
  28. 45 37
      src/components/dashboardDesigner/viewLayout.jsx
  29. 99 10
      src/components/dashboardDesigner/viewLayout.less
  30. 84 16
      src/components/datasource/columnConfig.jsx
  31. 1 1
      src/components/datasource/dataSource.jsx
  32. 14 2
      src/constants/url.js
  33. 3 3
      src/index.js
  34. 22 22
      src/models/chart.js
  35. 5 1
      src/models/chartDesigner.js
  36. 131 99
      src/models/dashboard.js
  37. 101 45
      src/models/dashboardDesigner.js
  38. 8 4
      src/models/dataSource.js
  39. 11 0
      src/models/main.js
  40. 1 1
      src/routes/mainLayout.js

File diff suppressed because it is too large
+ 0 - 4
public/fonts/iconfont/custom/iconfont.css


BIN
public/fonts/iconfont/custom/iconfont.eot


File diff suppressed because it is too large
+ 0 - 0
public/fonts/iconfont/custom/iconfont.js


+ 3 - 7
public/fonts/iconfont/custom/iconfont.svg

@@ -20,13 +20,6 @@ Created by iconfont
   />
     <missing-glyph />
     
-    <glyph glyph-name="x" unicode="x" horiz-adv-x="1001"
-d="M281 543q-27 -1 -53 -1h-83q-18 0 -36.5 -6t-32.5 -18.5t-23 -32t-9 -45.5v-76h912v41q0 16 -0.5 30t-0.5 18q0 13 -5 29t-17 29.5t-31.5 22.5t-49.5 9h-133v-97h-438v97zM955 310v-52q0 -23 0.5 -52t0.5 -58t-10.5 -47.5t-26 -30t-33 -16t-31.5 -4.5q-14 -1 -29.5 -0.5
-t-29.5 0.5h-32l-45 128h-439l-44 -128h-29h-34q-20 0 -45 1q-25 0 -41 9.5t-25.5 23t-13.5 29.5t-4 30v167h911zM163 247q-12 0 -21 -8.5t-9 -21.5t9 -21.5t21 -8.5q13 0 22 8.5t9 21.5t-9 21.5t-22 8.5zM316 123q-8 -26 -14 -48q-5 -19 -10.5 -37t-7.5 -25t-3 -15t1 -14.5
-t9.5 -10.5t21.5 -4h37h67h81h80h64h36q23 0 34 12t2 38q-5 13 -9.5 30.5t-9.5 34.5q-5 19 -11 39h-368zM336 498v228q0 11 2.5 23t10 21.5t20.5 15.5t34 6h188q31 0 51.5 -14.5t20.5 -52.5v-227h-327z" />
-    
-
-    
     <glyph glyph-name="chart-pie" unicode="&#60947;" d="M547.904 365.952l306.112 307.008c65.92-78.016 105.856-178.752 105.856-288.96 0-119.36-46.976-227.584-123.072-307.904L547.904 365.952zM585.984-57.34400000000005c71.872 12.864 137.792 43.776 193.728 85.76L447.872 361.28 447.872 826.944C231.488 795.904 64.96 610.688 64 385.92 65.024 138.81600000000003 265.088-64 511.872-64c24.96 0 49.344 2.24 73.152 6.528C585.344-57.408000000000015 585.664-57.408000000000015 585.984-57.34400000000005zM806.08 721.344C727.296 790.08 624.64 832 511.872 832l0-405.824L806.08 721.344z"  horiz-adv-x="1024" />
 
     
@@ -51,6 +44,9 @@ t9.5 -10.5t21.5 -4h37h67h81h80h64h36q23 0 34 12t2 38q-5 13 -9.5 30.5t-9.5 34.5q-
     <glyph glyph-name="drag" unicode="&#60951;" d="M582.336 755.392a92.8 92.8 0 1 1 185.664-0.064v0.064a92.8 92.8 0 1 1-185.664 0z m-309.568 0a92.864 92.864 0 1 1 185.792 0 92.864 92.864 0 0 1-185.792 0zM582.336 384A92.8 92.8 0 1 1 768 384a92.864 92.864 0 0 1-185.664 0z m-309.568 0c0-51.264 41.6-92.928 92.864-92.928A92.864 92.864 0 1 1 272.768 384z m309.568-371.392A92.8 92.8 0 1 1 768 12.480000000000018v0.128a92.8 92.8 0 0 1-92.864 92.736c-51.264 0-92.8-41.472-92.8-92.736z m-309.568 0a92.864 92.864 0 1 1 185.792 0 92.864 92.864 0 0 1-185.792 0z"  horiz-adv-x="1024" />
 
     
+    <glyph glyph-name="chart-default" unicode="&#60952;" d="M0 832v-896h1024V832z m960-832H64V768h896zM704 640h128v-512H704zM448 320h128v-192H448zM192 512h128v-384H192z"  horiz-adv-x="1024" />
+
+    
 
 
   </font>

BIN
public/fonts/iconfont/custom/iconfont.ttf


BIN
public/fonts/iconfont/custom/iconfont.woff


BIN
public/images/chart-default.png


BIN
public/images/table-default.png


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

@@ -89,7 +89,7 @@ class ChooseDataSourceBox extends React.Component {
                 return (
                     <span>
                         { filterLabel ?
-                            (text.split(new RegExp(`(${filterLabel})`, 'i')).map((fragment, i) => {
+                            ((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> :

+ 12 - 7
src/components/chart/list.jsx

@@ -93,13 +93,17 @@ class ChartList extends React.Component {
         )).map(g => g.code);
 
 
-        let cards = list.filter(l => groupFilter.indexOf(l.groupCode+'') !== -1).filter(l => {
+        let cards = list.filter(l => groupFilter.indexOf(l.groupCode+'') !== -1).map(l => {
             let reg = new RegExp('(' + filterLabel + '){1}', 'ig');
-            return (l.name || '').search(reg) !== -1 || (l.description || '').search(reg) !== -1;
+            if((l.name || '').search(reg) !== -1 || (l.description || '').search(reg) !== -1) {
+                return { ...l, bf: false };
+            }else {
+                return { ...l, bf: true };
+            }
         }).sort((a, b) => {
             return new Date(b.createTime) - new Date(a.createTime)
         }).map( (l, i) => (
-            <CardGrid className='chart-card' key={i} onClick={() => {
+            <CardGrid className={`chart-card${l.bf?' chart-card-hide':''}`} key={i} onClick={() => {
                 this.setState({ selectedRecord: l })
             }}>
                 <Card
@@ -107,7 +111,7 @@ class ChartList extends React.Component {
                         <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) => {
+                                    ((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> :
@@ -128,13 +132,13 @@ class ChartList extends React.Component {
                                 dispatch({ type: 'chartDesigner/reset' });
                                 dispatch({ type: 'main/redirect', path: '/chart/' + l.code });
                             }}>
-                                <Thumbnail code={l.code} option={l.chartOption}/>
+                                <Thumbnail type={l.type} code={l.code} option={l.chartOption}/>
                             </Row>
                             <Row className='desc'>
-                                <Ellipsis tooltip={l.description.length > 16} lines={2}>{
+                                <Ellipsis tooltip={l.description&&l.description.length > 16} lines={2}>{
                                     <span>
                                     { filterLabel ?
-                                        (l.description.split(new RegExp(`(${filterLabel})`, 'i')).map((fragment, i) => {
+                                        ((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> :
@@ -379,6 +383,7 @@ class ChartList extends React.Component {
                                 <Col style={{ padding: '0 5px' }}>
                                     <Search
                                         placeholder="请输入关键字"
+                                        value={chart.filterLabel}
                                         onChange={e => {
                                             dispatch({ type: 'chart/setFilterLabel', label: e.target.value });
                                         }}

+ 21 - 1
src/components/chart/list.less

@@ -54,11 +54,28 @@
                     padding: 10px;
                     .thumb {
                         height: 132px;
+                        cursor: pointer;
                         canvas {
                             cursor: pointer;
                         }
-                        div {
+                        .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;
+                        }
+                        .chart-default {
+                            background-image: url(/images/chart-default.png);
+                            width: 100%;
                             height: 100%;
+                            background-position: center;
+                            background-size: contain;
+                            background-repeat: no-repeat;
+                            cursor: pointer;
+                            opacity: .3;
                         }
                     }
                     .desc {
@@ -77,6 +94,9 @@
                 }
             }
         }
+        .chart-card-hide {
+            display: none;
+        }
     }
 }
 .popover-group {

+ 57 - 53
src/components/chart/resolveChartOption.js

@@ -1,28 +1,28 @@
-export default (config, silent) => {
+export default (config, silent, thumbnail) => {
     const { viewType, option } = config;
     let o;
     switch(viewType) {
         case 'bar': {
-            o = barConfig(option, silent);
+            o = barConfig(option, silent, thumbnail);
             break;
         }
         case 'pie': {
-            o = pieConfig(option, silent);
+            o = pieConfig(option, silent, thumbnail);
             break;
         }
         case 'line': {
-            o = lineConfig(option, silent);
+            o = lineConfig(option, silent, thumbnail);
             break;
         }
         case 'scatter': {
-            o = scatterConfig(option, silent);
+            o = scatterConfig(option, silent, thumbnail);
             break;
         }
         case 'aggregateTable': {
-            o = tableConfig(option, silent);
+            o = tableConfig(option, silent, thumbnail);
             break;
         }case 'dataView' : {
-            o = tableConfig(option, silent);
+            o = tableConfig(option, silent, thumbnail);
             break;
         }
         default:{
@@ -33,13 +33,13 @@ export default (config, silent) => {
     return o;
 }
 
-function barConfig(option, silent) {
+function barConfig(option, silent, thumbnail) {
     const { xAxis, serieses, xTitle, yTitle } = option;
 
     let o = {
-        animation: !silent,
+        animation: !thumbnail,
         tooltip : {
-            show: !silent,
+            show: !silent && !thumbnail,
             trigger: "axis",
             axisPointer: {
                 type: "cross",
@@ -49,23 +49,24 @@ function barConfig(option, silent) {
             }
         },
         legend: {
-            show: !silent
+            show: !thumbnail,
+            selectedMode: !silent
         },
         grid: {
-            left: silent ? 0 : '10%',
-            right: silent ? 0 : '10%',
-            top: silent ? 0 : 60,
-            bottom: silent ? 0 : 60,
-            containLabel: !silent
+            left: thumbnail ? 10 : '10%',
+            right: thumbnail ? 10 : '10%',
+            top: thumbnail ? 10 : 60,
+            bottom: thumbnail ? 10 : 60,
+            containLabel: !thumbnail
         },
         xAxis: [{
-            show: !silent,
+            show: !thumbnail,
             type: 'category',
             data: xAxis,
             name: xTitle || '横轴',
         }],
         yAxis: [{
-            show: !silent,
+            show: !thumbnail,
             name: yTitle || '纵轴',
             type: 'value'
         }],
@@ -74,7 +75,7 @@ function barConfig(option, silent) {
                 name: s.name,
                 type: 'bar',
                 data: s.value,
-                showSymbol: !silent,
+                showSymbol: !thumbnail,
                 silent,
             }
         }) 
@@ -82,27 +83,28 @@ function barConfig(option, silent) {
     return o;
 }
 
-function pieConfig(option, silent) {
+function pieConfig(option, silent, thumbnail) {
 
     const { xAxis, columnName, serieses } = option;
 
     let o = {
-        animation: !silent,
+        animation: !thumbnail,
         grid: {
-            left: silent ? 0 : '10%',
-            right: silent ? 0 : '10%',
-            top: silent ? 0 : 60,
-            bottom: silent ? 0 : 60,
-            containLabel: !silent
+            left: thumbnail ? 10 : '10%',
+            right: thumbnail ? 10 : '10%',
+            top: thumbnail ? 10 : 60,
+            bottom: thumbnail ? 10 : 60,
+            containLabel: !thumbnail
         },
         tooltip : {
-            show: !silent,
+            show: !silent && !thumbnail,
             trigger: 'item',
             formatter: "{a} <br/>{b} : {c} ({d}%)"
         },
         legend: {
-            show: !silent,
-            data: xAxis
+            show: !thumbnail,
+            data: xAxis,
+            selectedMode: !silent
         },
         series : [
             {
@@ -127,35 +129,36 @@ function pieConfig(option, silent) {
     return o;
 }
 
-function lineConfig(option, silent) {
+function lineConfig(option, silent, thumbnail) {
     const { serieses, xTitle, yTitle } = option;
 
     let o = {
-        animation: !silent,
+        animation: !thumbnail,
         grid: {
-            left: silent ? 0 : '10%',
-            right: silent ? 0 : '10%',
-            top: silent ? 0 : 60,
-            bottom: silent ? 0 : 60,
-            containLabel: !silent
+            left: thumbnail ? 10 : '10%',
+            right: thumbnail ? 10 : '10%',
+            top: thumbnail ? 10 : 60,
+            bottom: thumbnail ? 10 : 60,
+            containLabel: !thumbnail
         },
         tooltip: {
-            show: !silent,
+            show: !silent && !thumbnail,
             trigger: 'axis',
             axisPointer: {
                 type: 'cross'
             }
         },
         legend: {
-            show: !silent
+            show: !thumbnail,
+            selectedMode: !silent
         },
         xAxis:  {
-            show: !silent,
+            show: !thumbnail,
             name: xTitle,
             type: 'time'
         },
         yAxis: {
-            show: !silent,
+            show: !thumbnail,
             name: yTitle,
             type: 'value'
         },
@@ -167,7 +170,7 @@ function lineConfig(option, silent) {
                 data: s.mdata.map(m => {
                     return [m.date, m.value]
                 }),
-                showSymbol: !silent,
+                showSymbol: !thumbnail,
                 silent
             }
         })
@@ -176,19 +179,19 @@ function lineConfig(option, silent) {
     return o;
 }
 
-function scatterConfig(option, silent) {
+function scatterConfig(option, silent, thumbnail) {
     const { serieses, xTitle, yTitle } = option;
     let o = {
-        animation: !silent,
+        animation: !thumbnail,
         grid: {
-            left: silent ? 10 : '10%',
-            right: silent ? 10 : '10%',
-            top: silent ? 10 : 60,
-            bottom: silent ? 10 : 60,
-            containLabel: !silent
+            left: thumbnail ? 10 : '10%',
+            right: thumbnail ? 10 : '10%',
+            top: thumbnail ? 10 : 60,
+            bottom: thumbnail ? 10 : 60,
+            containLabel: !thumbnail
         },
         tooltip : {
-            show: !silent,
+            show: !silent && !thumbnail,
             showDelay : 0,
             axisPointer:{
                 show: true,
@@ -200,11 +203,12 @@ function scatterConfig(option, silent) {
             }
         },
         legend: {
-            show: !silent
+            show: !thumbnail,
+            selectedMode: !silent
         },
         xAxis : [
             {
-                show: !silent,
+                show: !thumbnail,
                 type : 'value',
                 name: xTitle,
                 scale:true,
@@ -215,7 +219,7 @@ function scatterConfig(option, silent) {
         ],
         yAxis : [
             {
-                show: !silent,
+                show: !thumbnail,
                 type : 'value',
                 name: yTitle,
                 scale:true,
@@ -243,7 +247,7 @@ function tableConfig(option, silent) {
     let o = {
         columns: columns.map(c => {
             if(c.dataIndex === 'percent') {
-                return { ...c, render: (value, record, index) => {console.log(record);return ((+value*100).toFixed(2)) + '%'} };
+                return { ...c, render: (value, record, index) => ((+value*100).toFixed(2)) + '%'};
             }else {
                 return c;
             }

+ 10 - 4
src/components/chart/thumbnail.jsx

@@ -1,16 +1,22 @@
 import React from 'react'
 import Echarts from 'echarts-for-react'
+import { Table, Image } from 'antd'
 import resolveChartOption from './resolveChartOption'
 
-const Thumbnail = ({ code, option }) => {
+const Thumbnail = ({ type, code, option }) => {
+    const newOption = resolveChartOption(option || {}, true, true);
+    const { columns, data } = newOption;
+    const viewType = ['bar', 'line', 'pie', 'scatter'].indexOf(type) !== -1 ? 'echarts' :
+        (['aggregateTable', 'dataView'].indexOf(type) !== -1 ? 'table' : 'default');
+        
     return (
         <div style={{ width: '100%', height: '100%' }}>
-            <Echarts
+            {viewType === 'echarts' ? <Echarts
                 key={code}
-                option={resolveChartOption(option || {}, true)}
+                option={newOption}
                 className='rc-echarts'
                 style={{height: '100%'}}
-            />
+            /> : ( viewType === 'table' ? <div className='table-default'></div> : <div className='chart-default'></div>)}
         </div>
     );
 }

+ 1 - 1
src/components/chartDesigner/content.less

@@ -8,7 +8,7 @@
             position: absolute;
             right: -13px;
             top: 6px;
-            z-index: 100;
+            z-index: 3;
         }
         .sider-tabs {
             .ant-tabs-nav .ant-tabs-tab {

+ 4 - 4
src/components/chartDesigner/header.jsx

@@ -43,20 +43,20 @@ class Header extends React.Component {
                                 visibleConfirm: false
                             });
                             dispatch({ type: 'chart/remoteModify' });
-                            dispatch({ type: 'main/redirect', path: '/chart' });
+                            dispatch({ type: 'main/goBack', path: '/chart' });
                         }}
                         onCancel={() => {
                             this.setState({
                                 visibleConfirm: false
                             });
-                            dispatch({ type: 'main/redirect', path: '/chart' });
+                            dispatch({ type: 'main/goBack', path: '/chart' });
                         }}
                         okText="保存"
                         cancelText="不保存"
                     >
-                        <Button onClick={() => {
+                        <Button onClick={(e) => {
                             if(!chartDesigner.dirty) {
-                                dispatch({ type: 'main/redirect', path: '/chart' });
+                                dispatch({ type: 'main/goBack', path: '/chart' });
                             }
                         }}>
                             <Icon type='left' />返回

+ 2 - 2
src/components/common/navigator.jsx

@@ -59,8 +59,8 @@ class Navigator extends React.Component {
             <div className='navigator-right'>
                 <Dropdown overlay={userMenu} trigger={['click']}>
                     <div>
-                        <Avatar size={64} icon="user" />
-                        <span> User Name </span>
+                        {/* <Avatar size={64} icon="user" />
+                        <span> User Name </span> */}
                     
                     </div>
                 </Dropdown>

+ 205 - 0
src/components/dashboard/list.jsx

@@ -0,0 +1,205 @@
+import React from 'react'
+import { Layout, Button, Icon, Input, Menu, Dropdown, Card, Col, Row, Popover, Breadcrumb, Tree, Tag } from 'antd'
+import { connect } from 'dva'
+import { dateFormat } from '../../utils/baseUtils'
+import Ellipsis from 'ant-design-pro/lib/Ellipsis'
+import 'ant-design-pro/dist/ant-design-pro.css'
+import GroupSelector from '../datasource/groupSelector'
+import './list.less'
+const { Content } = Layout
+const { Search } = Input
+const CardGrid = Card.Grid
+const { TreeNode } = Tree
+
+
+class ChartList extends React.Component {
+    constructor(props) {
+        super(props);
+        this.state = {
+            selectedRecord: null,
+            visibleChooseDataSourceBox: false,
+            visibleDistributeBox: false,
+            visibleGroupMenu: false, // 显示分组菜单
+        }
+    }
+
+    componentDidMount() {
+        const { dispatch } = this.props;
+        this.setBodyWidth();
+        dispatch({ type: 'dashboard/fetchList' });
+        // dispatch({ type: 'chart/remoteGroupList' });
+    }
+
+    /**
+     * 设置卡片容器宽度 = 每行最大卡片数量 * 卡片宽度
+     */
+    setBodyWidth() {
+        // const chartBody = document.getElementsByClassName('chart-body')[0]; // 卡片容器
+        // const parent = chartBody.parentNode; // 父级容器
+        // const pWidth = parent.offsetWidth; // 父级容器宽度
+        // const pPadding = 10 + 10; // 父级容器左右padding
+        // const cWidth = 600; // 每个卡片宽度
+        // const cMargin = 5 + 5; // 每个卡片左右margin
+        // const pTrueWidth = pWidth - pPadding; // 父容器实际可用宽度
+        // const cTrueWidth = cWidth + cMargin; // 卡片实际占用宽度
+        // const count = Math.floor(pTrueWidth/cTrueWidth); // 每行最大卡片数量
+
+        // chartBody.style.width = count * cTrueWidth  + 'px';
+    }
+
+    generateCard() {
+        const { dashboard, dispatch } = this.props;
+        const { selectedRecord } = this.state;
+        const list = dashboard.list;
+
+        const reg = new RegExp('([+ \\- & | ! ( ) { } \\[ \\] ^ \" ~ * ? : ( ) \/])', 'g'); // 需要转义的字符
+        let filterLabel = dashboard.filterLabel.replace(new RegExp('(\\\\)', 'g'), '\\$1').replace(reg, '\\$1'); // 添加转义符号
+
+        const operationMenu = (
+            <Menu className='menu-operation'>
+                <Menu.Item onClick={() => {
+                    this.setState({visibleDistributeBox: true})
+                }}> 
+                    <Icon type='share-alt'/>分发
+                </Menu.Item>
+                <Menu.Divider />
+                <Menu.Item onClick={() => {
+                    dispatch({ type: 'dashboard/remoteDelete', code: this.state.selectedRecord.code });
+                }}>
+                    <Icon type='delete'/>删除
+                </Menu.Item>
+            </Menu>
+        )
+
+        let cards = list.filter(l => {
+            let reg = new RegExp('(' + filterLabel + '){1}', 'ig');
+            return (l.title || '').search(reg) !== -1 || (l.description || '').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.title || '').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.title
+                                }
+                            </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 });
+                            }}>
+                                {/* <Thumbnail type={l.type} code={l.code} option={l.chartOption}/> */}
+                            </Row>
+                            <Row className='desc'>
+                                <Ellipsis tooltip={l.description.length > 16} lines={2}>{
+                                    <span>
+                                    { 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
+                                    }
+                                </span>
+                                }</Ellipsis>
+                            </Row>
+                            <Row className='footer' type='flex' justify='end' align='bottom'>
+                                <Col style={{ textAlign: 'left' }} span={22}>
+                                    <Row>{l.creator} {dateFormat(l.createTime, '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
+        });
+    }
+
+    render() {
+        const { visibleChooseDataSourceBox, visibleDistributeBox } = this.state;
+        const { dispatch, dashboard } = this.props;
+        const TAG_COLOR = ['blue'];
+        return (
+            <Layout className='dashboard-list'>
+                <Content>
+                    <Card title={
+                        <Row className='tools' type='flex' justify='space-between'>
+                            <Col style={{ display: 'flex' }}>
+                            </Col>
+                            <Col className='search'>
+                                <Col style={{ padding: '0 5px' }}>
+                                    <Search
+                                        placeholder="请输入关键字"
+                                        value={dashboard.filterLabel}
+                                        onChange={e => {
+                                            dispatch({ type: 'dashboard/setFilterLabel', label: e.target.value });
+                                        }}
+                                    />
+                                </Col>
+                                <Col >
+                                    <Button onClick={() => {
+                                        dispatch({ type: 'main/redirect', path: { pathname: '/dashboard/create' } });
+                                    }}>
+                                        <Icon type="layout" />创建看板
+                                    </Button>
+                                </Col>
+                            </Col>
+                        </Row>
+                    }>
+                        <div className='chart-body'>
+                            { this.generateCard() }
+                        </div>
+                    </Card>
+                </Content>
+            </Layout>
+        )
+    }
+}
+
+
+function mapStateToProps({ present: { chart, dashboard } }) {
+    return { chart, dashboard }
+}
+
+export default connect(mapStateToProps)(ChartList)

+ 173 - 0
src/components/dashboard/list.less

@@ -0,0 +1,173 @@
+.dashboard-list {
+    .ant-card-head {
+        padding: 0 10px;
+        .tools {
+            .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;
+                    }
+                }
+            }
+            .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: 5px;
+            padding: 0;
+
+            .ant-card-head {
+                min-height: 20px;
+                cursor: default;
+                .ant-card-head-title {
+                    font-size: 14px;
+                    padding: 6px 0;
+                    .anticon {
+                        cursor: pointer;
+                    }
+                }
+            }
+
+            .ant-card-cover {
+                height: 247px;
+                .cover-body {
+                    padding: 10px;
+                    .thumb {
+                        height: 182px;
+                        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;
+                        }
+                        .chart-default {
+                            background-image: url(/images/chart-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;
+                    }
+                }
+                .ant-row-flex-bottom {
+                    cursor: default;
+                    .anticon {
+                        cursor: pointer;
+                    }
+                }
+            }
+        }
+    }
+}
+.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;
+}
+.menu-operation {
+    padding: 0;
+    .ant-dropdown-menu-item {
+        .anticon {
+            margin-right: 6px;
+        }
+    }
+    .ant-dropdown-menu-item-divider {
+        margin: 0;
+    }
+    .setgroupmenu {
+        .ant-dropdown-menu-submenu-title {
+            display: flex;
+        }
+    }
+}
+.ant-tooltip-inner {
+    background-color: rgba(255, 255, 255, .9);
+    color: #666
+}

+ 33 - 13
src/components/dashboardDesigner/chartView.jsx

@@ -1,23 +1,43 @@
 import React from 'react'
 import Echarts from 'echarts-for-react'
 import { connect } from 'dva'
+import { Table } from 'antd'
 import resolveChartOption from '../chart/resolveChartOption.js'
+import BraftEditor from 'braft-editor'
+import RichTextEditor from './richTextEditor'
 
-const ChartView = ({ chartCode, chart }) => {
-
-    let targetChart = chart.list.filter(c => c.code === chartCode)[0];
-
-
-    let option = resolveChartOption(targetChart ? targetChart.chartOption : {}, true);
+const ChartView = ({ item, chart, editMode, dispatch }) => {
+    const { viewType, chartCode, content } = item;
+    let children = <div className='chart-default mover'></div>;
+    if(viewType === 'chart') { // 图表类型
+        let targetChart = chart.list.filter(c => c.code === chartCode)[0];
+        if(targetChart) {
+            let option = resolveChartOption(targetChart ? targetChart.chartOption : {}, editMode);
+            let type = ['bar', 'line', 'pie', 'scatter'].indexOf(targetChart.type) !== -1 ? 'echarts' :
+                (['aggregateTable', 'dataView'].indexOf(targetChart.type) !== -1 ? 'table' : 'default');
+            if(type === 'echarts') {
+                children = <Echarts
+                    key={chartCode}
+                    option={option}
+                    className='rc-echarts mover'
+                    style={{height: '100%'}}
+                />
+            }else if(type === 'table') {
+                const { columns, data } = option;
+                children = <Table className='mover' columns={columns} dataSource={data} pagination={false} />
+            }
+        }
+    }else if(viewType === 'richText') { // 富文本类型
+        children = <RichTextEditor content={content} onContentChange={(content) => {
+            dispatch({ type: 'dashboardDesigner/modifyItem', fields: { code: item.code, content } });
+        }}/>
+    }else {
+        children = <div className='mover'>错误类型</div>
+    }
 
     return (
-        <div style={{ width: '100%', height: '100%' }}>
-            <Echarts
-                key={chartCode}
-                option={option}
-                className='rc-echarts'
-                style={{height: '100%'}}
-            />
+        <div className='chartview-content' style={{ width: '100%', height: '100%' }}>
+            {children}
         </div>
     );
 }

+ 216 - 0
src/components/dashboardDesigner/chooseChartBox.jsx

@@ -0,0 +1,216 @@
+import React from 'react'
+import '../../models/dashboardDesigner'
+import { Modal, Select, Checkbox, Row, Col, Table, Input, message } from 'antd'
+import { connect } from 'dva'
+import { dateFormat } from '../../utils/baseUtils'
+import BraftEditor from 'braft-editor'
+import 'braft-editor/dist/braft.css'
+import './chooseChartBox.less'
+const Option = Select.Option
+const { Search } = Input
+
+class ChooseChartBox extends React.Component {
+
+    constructor(props) {
+        super(props);
+        this.state = {
+            filterLabel: '',
+            selectedRecord: []
+        };
+    }
+
+    componentDidMount() {
+        const { dispatch } = this.props;
+        dispatch({ type: 'chart/fetchList' });
+    }
+
+    changeSelected = (record) => {
+        let arr = this.state.selectedRecord;
+        let findIndex = arr.findIndex(a => a.code === record.code);
+        if(findIndex !== -1) {
+            arr.splice(findIndex, 1)
+        }else {
+            arr.push(record);
+        }
+        this.setState({
+            selectedRecord: arr 
+        });
+    }
+
+    okHandler = (model) => {
+        const { selectedRecord } = this.state;
+        const { dispatch, hideBox } = this.props;
+        if(selectedRecord) {
+            dispatch({ type: 'dashboardDesigner/addCharts', charts: selectedRecord });
+            hideBox();
+        }else {
+            message.warning('未选中图表');
+        }
+    }
+
+    handleChange = (content) => {
+    }
+
+    handleRawChange = (rawContent) => {
+    }
+
+    
+    static editorProps = {
+        height: 300,
+        contentFormat: 'raw',
+        initialContent: '<p>Hello World!</p>',
+        onChange: () => {},
+        onRawChange: () => {},
+        image: true, // 开启图片插入功能?
+    }
+
+    render() {
+        const { operation, visibleBox, hideBox, dashboardDesigner, chart, dispatch } = this.props;
+        const { selectedRecord } = this.state;
+        const reg = new RegExp('([+ \\- & | ! ( ) { } \\[ \\] ^ \" ~ * ? : ( ) \/])', 'g'); // 需要转义的字符
+        let filterLabel = this.state.filterLabel.replace(new RegExp('(\\\\)', 'g'), '\\$1').replace(reg, '\\$1'); // 添加转义符号
+        const columns = [{
+            title: '选择',
+            key: 'selected',
+            width: 50,
+            render: (text, record) => {
+                return <Checkbox checked={dashboardDesigner.items.findIndex(i => i.chartCode===record.code)!==-1 || selectedRecord.findIndex(s => s.code === record.code) !== -1 } disabled={dashboardDesigner.items.findIndex(i => i.chartCode===record.code)!==-1}/>
+            }
+        }, {
+            title: '名称',
+            dataIndex: 'name',
+            key: 'name',
+            width: 100,
+            render: (text, record) => {
+                return <div>
+                    <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: 'creator',
+            key: 'creator',
+            width: 100
+        }, {
+            title: '创建时间',
+            dataIndex: 'createTime',
+            key: 'createTime',
+            width: 120,
+            render: (text) => dateFormat(new Date(text), 'yyyy-MM-dd')
+        }, {
+            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>
+                )
+            }
+        }]
+        return (
+            <Modal
+                className='choosechart-box'
+                title={
+                    <Row>
+                        <Col span={14}>选择图表</Col>
+                        <Col span={8}><Search 
+                            placeholder="请输入关键字"
+                            value={this.state.filterLabel}
+                            onChange={e => {
+                                this.setState({
+                                    filterLabel: e.target.value
+                                });
+                            }}
+                        /></Col>
+                    </Row>
+                }
+                visible={visibleBox}
+                onOk={() => {this.setState({ filterLabel: '', selectedRecord: [] });this.okHandler()}}
+                onCancel={() => {this.setState({ filterLabel: '', selectedRecord: [] });hideBox()}}
+                maskClosable={false}
+                destroyOnClose={true}
+            >
+                <Table
+                    className='choosechart-table'
+                    columns={columns}
+                    dataSource={chart.list.map((l, i) => ({ ...l, key: i})).filter(l => {
+                        let regLabel = new RegExp('(' + filterLabel + '){1}', 'ig');
+                        return (l.name || '').search(regLabel) !== -1 || (l.description || '').search(regLabel) !== -1;
+                    })}
+                    size='small'
+                    scroll={{x: false, y: 471}}
+                    pagination={false}
+                    onRow={(record) => {
+                        return {
+                            onClick: () => {
+                                if(dashboardDesigner.items.findIndex(i => i.chartCode===record.code)===-1) {
+                                    this.changeSelected(record);
+                                }
+                            }
+                        };
+                    }}
+                />
+            </Modal>
+        )
+        // return (
+        //     <Modal
+        //         className='element-config'
+        //         title={`${operation==='create'?'新增':'修改'}元素`}
+        //         visible={visibleBox}
+        //         onOk={() => {this.okHandler()}}
+        //         onCancel={hideElementConfigBox}
+        //         maskClosable={false}
+        //         destroyOnClose={true}
+        //     >
+        //         <div>
+        //             <Select defaultValue={dashboardDesigner.configBoxForm.type} style={{ width: 120 }} 
+        //                 onChange={(value) => { dispatch( {type: 'dashboardDesigner/handleFieldChange', name: 'type', value: value})}}>
+        //                 <Option value="chart">图表</Option>
+        //                 <Option value="simple">基础元素</Option>
+        //             </Select>
+        //         </div>
+        //         <div style={{display: `${dashboardDesigner.configBoxForm.type === "simple" ? "inline":"none"}`}}>
+        //             <BraftEditor {...this.editorProps}/>
+        //         </div>
+        //     </Modal>
+        // )
+    } 
+}
+
+
+
+    
+
+
+
+
+function mapStateToProps({ present: { dashboardDesigner, chart } }) {
+    return { dashboardDesigner, chart };
+}
+
+export default connect(mapStateToProps)(ChooseChartBox)

+ 14 - 0
src/components/dashboardDesigner/chooseChartBox.less

@@ -0,0 +1,14 @@
+.choosechart-box {
+    width: 600px !important;
+    .ant-modal-body {
+        padding: 0;
+        max-height: 50vh;
+        overflow-y: auto;
+        .choosechart-table {
+            .ant-table-header {
+                overflow-y: hidden;
+                margin-right: 6px;
+            }
+        }
+    }
+}

+ 76 - 4
src/components/dashboardDesigner/content.jsx

@@ -1,20 +1,92 @@
 import React from 'react'
-import { Layout } from 'antd'
+import { findDOMNode } from 'react-dom'
+import { Layout, Button, Switch, Dropdown, Menu, Icon } from 'antd'
 import { connect } from 'dva'
 import ViewLayout from './viewLayout'
-const { Content } = Layout
+import ChooseChartBox from './chooseChartBox'
+const { Header, Content } = Layout
 
 class DashboardDesignerContent extends React.Component {
     constructor(props) {
         super(props);
         this.state = {
+            viewWidth: 0,
+            editMode: true,
+            operation: 'create',
+            visibleBox: false,
         };
     }
 
+    componentDidMount() {
+        this.refreshContentWidth()
+    }
+
+    showBox = (o) => {
+        this.setState({
+            operation: o,
+            visibleBox: true
+        });
+    }
+
+    hideBox = (o) => {
+        this.setState({
+            visibleBox: false
+        });
+    }
+
+    /**
+     * 重设容器宽度
+     */
+    refreshContentWidth = () => {
+        let contentEl = findDOMNode(this.refs.contentEl);
+        if(!contentEl) { return; }
+        let scroll = contentEl.scrollHeight > contentEl.clientHeight;
+        this.setState({
+            viewWidth: contentEl.offsetWidth - (scroll ? 25 : 0) // 有滚动条时需要减去滚动条的宽度
+        });
+    }
+
     render() {
+        const { viewWidth, editMode, operation, visibleBox } = this.state;
         return <Layout className='content'>
-            <Content>
-                <ViewLayout />
+            <Header>
+                <div>
+                    <Switch
+                        className={`mode-switch ${editMode ? 'edit-mode-switch' : 'view-mode-switch'}`}
+                        checked={editMode}
+                        checkedChildren="编辑"
+                        unCheckedChildren="浏览"
+                        onChange={(checked) => {
+                            this.setState({
+                                editMode: checked
+                            })
+                        }}
+                    />
+                </div>
+                <div>
+                    {editMode && <Dropdown overlay={(
+                        <Menu onClick={(item) => {
+                            const type = item.key;
+                            if(type === 'chart') {
+                                this.showBox("create");
+                            }else {
+                                const { dispatch } = this.props;
+                                dispatch({ type: 'dashboardDesigner/addRichText' });
+                            }
+                        }}>
+                            <Menu.Item key='chart'>图表</Menu.Item>
+                            <Menu.Item key='richText'>富文本</Menu.Item>
+                        </Menu>
+                    )} trigger={['click']}>
+                        <Button>
+                            <Icon type="plus" />添加
+                        </Button>
+                    </Dropdown>}
+                    <ChooseChartBox operation={operation} visibleBox={visibleBox} hideBox={this.hideBox} />
+                </div>
+            </Header>
+            <Content ref='contentEl'>
+                <ViewLayout width={viewWidth} reset={this.refreshContentWidth} editMode={editMode}/>
             </Content>
         </Layout>
     }

+ 0 - 79
src/components/dashboardDesigner/elementConfig.jsx

@@ -1,79 +0,0 @@
-import React from 'react'
-import '../../models/dashboardDesigner'
-import { Modal, Select } from 'antd'
-import { connect } from 'dva'
-import BraftEditor from 'braft-editor'
-import 'braft-editor/dist/braft.css'
-const Option = Select.Option
-
-const ElementConfig = ({operation, visibleBox, hideElementConfigBox, dashboardDesigner, dispatch}) => {
-   
-    const okHandler = (model) => {
-        if(operation === 'create') {
-            return 
-            // TODO
-        }else if(operation === 'modify') {
-            return
-            // TODO
-        }
-    }
-
-    const handleChange = (content) => {
-    console.log(content) 
-        return
-        // TODO
-    }
-
-    const handleRawChange = (rawContent) => {
-    console.log(rawContent)
-        return
-        // TODO
-    }
-
-    
-    const editorProps = {
-        height: 300,
-        contentFormat: 'raw',
-        initialContent: '<p>Hello World!</p>',
-        onChange: handleChange(),
-        onRawChange: handleRawChange()
-    }
-
-        
-    return (
-        <Modal
-            className='element-config'
-            title={`${operation==='create'?'新增':'修改'}元素`}
-            visible={visibleBox}
-            onOk={() => {okHandler()}}
-            onCancel={hideElementConfigBox}
-            maskClosable={false}
-            destroyOnClose={true}
-            width="925px"
-        >
-            <div>
-                <Select defaultValue={dashboardDesigner.configBoxForm.type} style={{ width: 120 }} 
-                    onChange={(value) => { dispatch( {type: 'dashboardDesigner/handleFieldChange', name: 'type', value: value})}}>
-                    <Option value="chart">图表</Option>
-                    <Option value="simple">基础元素</Option>
-                </Select>
-            </div>
-            <div style={{display: `${dashboardDesigner.configBoxForm.type === "simple" ? "inline":"none"}`}}>
-                <BraftEditor {...editorProps}/>
-            </div>
-        </Modal>
-    )
-}
-
-
-
-    
-
-
-
-
-function mapStateToProps({ present: { dashboardDesigner } }) {
-    return { dashboardDesigner: dashboardDesigner };
-}
-
-export default connect(mapStateToProps)(ElementConfig)

+ 20 - 30
src/components/dashboardDesigner/header.jsx

@@ -16,7 +16,8 @@ class Header extends React.Component {
     }
 
     render() {
-        const { dispatch } = this.props;
+        const { dashboardDesigner, dispatch } = this.props;
+        console.log(dashboardDesigner);
         return (
             <div className='dashboarddesigner-header'>
                 <div className='header-item toolbar-back'>
@@ -26,11 +27,11 @@ class Header extends React.Component {
                         visible={this.state.visibleConfirm}
                         onVisibleChange={this.handleVisibleChange}
                         onConfirm={() => {
-                            // this.setState({
-                            //     visibleConfirm: false
-                            // });
-                            // dispatch({ type: 'chart/remoteModify' });
-                            // dispatch({ type: 'main/redirect', path: '/chart' });
+                            this.setState({
+                                visibleConfirm: false
+                            });
+                            dispatch({ type: 'dashboard/remoteModify' });
+                            dispatch({ type: 'main/redirect', path: '/dashboard' });
                         }}
                         onCancel={() => {
                             this.setState({
@@ -42,25 +43,26 @@ class Header extends React.Component {
                         cancelText="不保存"
                     >
                         <Button onClick={() => {
-                            // if(!chartDesigner.dirty) {
-                            //     dispatch({ type: 'main/redirect', path: '/chart' });
-                            // }
+                            console.log(dashboardDesigner.dirty);
+                            if(!dashboardDesigner.dirty) {
+                                dispatch({ type: 'main/redirect', path: '/dashboard' });
+                            }
                         }}>
                             <Icon type='left' />返回
                         </Button>
                     </Popconfirm>
                     <Button onClick={() => {
-                        // if(chartDesigner.code && chartDesigner.code !== -1) {
-                        //     dispatch({ type: 'chart/remoteModify' });
-                        // }else {
-                        //     dispatch({ type: 'chart/remoteAdd' });
-                        // }
-                        // dispatch({ type: 'chartDesigner/setDirty', dirty: false });
+                        if(dashboardDesigner.code && dashboardDesigner.code !== -1) {
+                            dispatch({ type: 'dashboard/remoteModify' });
+                        }else {
+                            dispatch({ type: 'dashboard/remoteAdd' });
+                        }
                     }}><Icon type='save' />保存</Button>
                 </div>
                 <div className='header-item toolbar-title'>
                     <Input
-                        className='input-title' 
+                        className='input-title'
+                        value={dashboardDesigner.title}
                         addonAfter={<Icon type="edit" 
                             onClick={(e) => {
                                 const input = e.currentTarget.parentElement.parentElement.getElementsByTagName('input')[0];
@@ -68,24 +70,12 @@ class Header extends React.Component {
                             }}
                         />}
                         onChange={(e) => {
-                            // dispatch({ type: 'chartDesigner/setField', name: 'header', value: { label: e.target.value } });
+                            dispatch({ type: 'dashboardDesigner/setField', name: 'title', value: e.target.value });
                         }}
                     />
                 </div>
                 <div className='header-item toolbar-buttons'>
-                    <Dropdown overlay={(
-                        <Menu onClick={(item) => {
-                            const type = item.key;
-                            console.log(type);
-                        }}>
-                            <Menu.Item key='chart'>图表</Menu.Item>
-                            <Menu.Item key='richText'>富文本</Menu.Item>
-                        </Menu>
-                    )} trigger={['click']}>
-                        <Button>
-                            <Icon type="plus" />添加
-                        </Button>
-                    </Dropdown>
+                    
                 </div>
             </div>
         );

+ 5 - 116
src/components/dashboardDesigner/layout.jsx

@@ -12,26 +12,17 @@ const { Header, Content } = Layout
 
 class DashboardDesigner extends React.Component {
 
-    // constructor(props) {
-    //     super(props);
-    //     var layout = this.loadLayout()
-    //     this.state = {
-    //         editMode: true,
-    //         operation: 'create',
-    //         visibleBox: false,
-    //         layout: layout,
-    //     };
-    // }
-
     componentDidMount() {
         const { dispatch } = this.props;
         const { code } = this.props.match.params;
-        dispatch({ type: 'chart/fetchList', code: code });
+        dispatch({ type: 'dashboardDesigner/reset' });
+        dispatch({ type: 'chart/fetchList' });
         if(code !== 'create') {
-            dispatch({ type: 'chart/remoteGroupList', code: code });
+            dispatch({ type: 'dashboard/remoteDetail', code: code });
         }
     }
 
+
     saveToPNG = (element) => {
         html2canvas(element).then(function(canvas) {
                 var pageData = canvas.toDataURL('image/png', 1.0);
@@ -46,106 +37,8 @@ class DashboardDesigner extends React.Component {
         });
     }
 
-
-    showElementConfigBox = (o) => {
-        this.setState({
-            operation: o,
-            visibleBox: true
-        });
-    }
-
-    hideElementConfigBox = (o) => {
-        this.setState({
-            visibleBox: false
-        });
-    }
-
-    enableEditMode = (o) => {
-        this.setState({
-            editMode:true
-        })
-    }
-
-    disableEditMode = (o) => {
-        this.setState({
-            editMode:false
-        })
-    }
-
-    generateElementConfigMenu = (key) => {
-        const {dashboardDesigner, dispatch} = this.props;
-        return (
-            <Menu>
-                <Menu.Item
-                    onClick={()=> {
-                        let selectedElement = dashboardDesigner.elementList.find((i) => {return i.key === key});
-                        dispatch({ type: 'dashboardDesigner/resetConfigBoxForm'});
-                        dispatch({ type: 'dashboardDesigner/loadConfigBoxForm', element: selectedElement});
-                        this.showElementConfigBox('modify')
-                }}>
-                    <span>属性设置</span>
-                </Menu.Item>
-            </Menu>
-        
-        
-        )
-    }
-    generateElement() {    
-        const elementList = this.props.dashboardDesigner.elementList;
-        const editMode = this.state.editMode
-        let IconStyle={
-            display: editMode? 'inline-block':'none', 
-            position: 'absolute',
-            right: '3px',
-            top: '3px'
-        };
-
-        
-        return elementList.map ((item) => {
-            if (item.type === 'chart') {
-                return (
-                    <div 
-                        key={item.key.toString()}
-                        className={`grid-item${this.state.editMode ?' grid-item-edit': ''}`} 
-                    >
-                        <Element type="chart" content={item.content} itemKey={item.key}/>  
-                        <Dropdown overlay={this.generateElementConfigMenu(item.key)} trigger={['click']}>
-                            <Icon type="setting" style={IconStyle} />
-                        </Dropdown>
-                    </div>
-                )
-            }else if (item.type === "simple") {
-                return (
-                    <div 
-                        key={item.key.toString()}
-                        className={`grid-item${this.state.editMode ?' grid-item-edit': ''}`} 
-                    >
-                        <Element type="simple" content={item.content} itemKey={item.key}/>
-                        <Dropdown overlay={this.generateElementConfigMenu(item.key)} trigger={['click']}>
-                            <Icon type="setting" style={IconStyle} />
-                        </Dropdown>
-                    </div>
-                )
-            }
-            else{
-                return (<div key="3" >I'm not typed!</div>)
-            
-            }
-        })
-    }
-
-    loadLayout() {
-        const layout = this.props.dashboardDesigner.layout;
-
-        return layout.map((item) => {
-            let { key, x, y, w, h, minW, maxW, minH, maxH } = item;
-            return { i: key.toString(), x: x, y: y, w: w, h: h, minW: minW, maxW: maxW, minH: minH, maxH: maxH}
-        })
-    }
-
     render() {
         const { loading } = this.props;
-        // const { operation, visibleBox, editMode } = this.state;
         return <Layout className='dashboarddesigner-layout'>
             <Header>
                 <DashboardDesignerHeader />
@@ -161,8 +54,4 @@ class DashboardDesigner extends React.Component {
 
 }
 
-function mapStateToProps({present: {dashboardDesigner}}) {
-    return { dashboardDesigner }
-}
-
-export default connect(mapStateToProps)(DashboardDesigner)
+export default connect()(DashboardDesigner)

+ 24 - 6
src/components/dashboardDesigner/layout.less

@@ -1,7 +1,7 @@
 .dashboarddesigner-layout {
     height: 100%;
     overflow: hidden;
-    .ant-layout-header {
+    &>.ant-layout-header {
         background: none;
         padding: 0 10px;
         height: 40px;
@@ -10,13 +10,31 @@
         border-style: solid;
         border-color: #CCCCCC;
     }
-    .ant-layout-content {
+    &>.ant-layout-content {
         flex: 1;
-        .ant-layout {
+        .content {
             height: 100%;
-            .content-header {
-                height: auto;
-                border-top: none;
+            .ant-layout-content {
+                height: 100%;
+                margin: 10px;
+                overflow: auto;
+            }
+            .ant-layout-header {
+                background: none;
+                padding: 0 10px;
+                height: 40px;
+                line-height: 37px;
+                border-width: 1px 0;
+                border-style: solid;
+                border-color: #CCCCCC;
+                display: flex;
+                justify-content: space-between;
+                .edit-mode-switch {
+                    background-color: #1890ff;
+                }
+                .view-mode-switch {
+                    background-color: #51C319;
+                }
             }
         }
     }

+ 21 - 0
src/components/dashboardDesigner/richTextEditor.jsx

@@ -0,0 +1,21 @@
+import React, { Component } from 'react';
+import E from 'wangeditor'
+
+class RichTextEditor extends Component {
+    render() {
+        return (
+            <div className='richtexteditor' ref="editorElem"></div>
+        );
+    }
+    componentDidMount() {
+        const { content, onContentChange } = this.props;
+        const elem = this.refs.editorElem;
+        const editor = new E(elem);
+        editor.customConfig.onchange = onContentChange; // 使用 onchange 函数监听内容的变化
+        editor.customConfig.uploadImgShowBase64 = true; // 上传图片(base64)
+        editor.create()
+        editor.txt.html(content);
+    }
+}
+
+export default RichTextEditor;

+ 45 - 37
src/components/dashboardDesigner/viewLayout.jsx

@@ -1,6 +1,7 @@
 import React from "react"
 import "./viewLayout.less"
 import ReactGridLayout from 'react-grid-layout'
+import { Icon } from 'antd'
 import { connect } from 'dva'
 import ChartView from './chartView'
 
@@ -25,51 +26,58 @@ class ViewLayout extends React.PureComponent {
     }
 
     createElement = (item) => {
-        const { dispatch } = this.props;
-        const { code, layout, chartCode } = item;
-        const removeStyle = {
-            position: "absolute",
-            right: "2px",
-            top: 0,
-            cursor: "pointer"
-        };
-        return (
-            <div key={code} data-grid={{...layout, i: code}}>
-                <ChartView chartCode={chartCode}/>
-                <span className="remove" style={removeStyle} onClick={() => {
-                    dispatch({ type: 'dashboardDesigner/delete', item: item });
-                }}>x</span>
-            </div>
-        );
+        const { dashboardDesigner, dispatch, editMode } = this.props;
+
+        return dashboardDesigner.items.map(item => {
+            const { code, title, viewType, layout, chartCode } = item;
+            return (
+                <div className={`chartview${editMode ? ' chartview-edit' : ''}`} key={code} data-grid={layout}>
+                    <div className='chartview-toolbar mover'>
+                        <div className='chart-title'><span>{title}</span></div>
+                        <div className='chart-tools'>
+                            <Icon type="arrows-alt" />
+                            {editMode && viewType!=='richText' &&  <Icon type='edit' onClick={() => {
+                                dispatch({ type: 'chartDesigner/reset' });
+                                dispatch({ type: 'main/redirect', path: '/chart/' + chartCode });
+                            }}/>}
+                            {editMode && <Icon type='delete' onClick={() => {
+                                dispatch({ type: 'dashboardDesigner/deleteItem', item });
+                            }} />}
+                        </div>
+                    </div>
+                    <ChartView editMode={editMode} item={{...item}}/>
+                </div>
+            )
+        });
     }
 
     onLayoutChange = (layout) => {
-        const { dispatch } = this.props;
+        const { dispatch, reset } = this.props;
         dispatch({ type: 'dashboardDesigner/changeLayout', layout });
+        setTimeout(() => {
+            reset();
+        }, 200)
     }
 
     render() {
-        const { width, editMode } = this.state;
-        const { dashboardDesigner } = this.props;
-        const { items } = dashboardDesigner;
-        const children = items.map(item => this.createElement(item));
+        const { width, editMode } = this.props;
+        const children = this.createElement();
         return (
-            <div>
-                <ReactGridLayout
-                    width={width}
-                    cols={12}
-                    margin = {editMode ? [2, 2] : [0, 0]}
-                    rowHeight = {100}
-                    isDraggable={editMode}
-                    isResizable={editMode}
-                    onLayoutChange={this.onLayoutChange}
-                    onBreakpointChange={this.onBreakpointChange}
-                    verticalCompact={true}
-                    compactType='vertical'
-                >
-                    {children}
-                </ReactGridLayout>
-            </div>
+            <ReactGridLayout
+                width={width}
+                cols={12}
+                margin = {editMode ? [2, 2] : [0, 0]}
+                rowHeight = {100}
+                isDraggable={editMode}
+                isResizable={editMode}
+                draggableHandle='.mover'
+                onLayoutChange={this.onLayoutChange}
+                onBreakpointChange={this.onBreakpointChange}
+                verticalCompact={true}
+                compactType='vertical'
+            >
+                {children}
+            </ReactGridLayout>
         );
     }
 }

+ 99 - 10
src/components/dashboardDesigner/viewLayout.less

@@ -1,6 +1,97 @@
 
   .react-grid-layout {
     background: #eee;
+    .chartview {
+      padding-top: 30px;
+      .chartview-toolbar {
+        height: 30px;
+        margin-top: -30px;
+        display: flex;
+        padding: 0 10px;
+        justify-content: space-between;
+        .chart-title {
+          font-size: 20px;
+        }
+        .chart-tools {
+          display: none;
+          font-size: 20px;
+          .anticon {
+            margin-left: 10px;
+            cursor: pointer;
+          }
+        }
+      }
+      .chartview-content {
+        .chart-default {
+          background-image: url(/images/chart-default.png);
+          width: 100%;
+          height: 100%;
+          background-position: center;
+          background-size: contain;
+          background-repeat: no-repeat;
+        }
+        .richtexteditor {
+          padding-bottom: 10px;
+          height: 100%;
+          .w-e-toolbar {
+            height: 30px;
+            background-color: white !important;
+            border: none !important;
+            position: absolute;
+            top: 0;
+            display: none;
+          }
+          .w-e-text-container {
+            height: 100% !important;
+            pointer-events: none;
+            border: none !important;
+            .w-e-text {
+              overflow-y: auto;
+              padding: 0 14px;
+              &>table {
+                margin: 0;
+              }
+              &>p {
+                margin: 0;
+              }
+            }
+            .w-e-panel-container {
+              z-index:100000;
+            }
+          }
+        }
+      }
+      &:hover {
+        .chart-tools {
+          display: block;
+          .anticon {
+            &:hover {
+              color: red;
+            }
+          }
+        }
+      }
+    }
+    .chartview-edit {
+      .chartview-toolbar {
+        cursor: move;
+      }
+      .chartview-content {
+        canvas {
+          cursor: move;
+        }
+        .richtexteditor {
+          .w-e-text-container {
+            pointer-events: all;
+          }
+          &:hover {
+            .w-e-toolbar {
+              display: flex;
+            }
+          }
+        }
+      }
+    }
   }
   .layoutJSON {
     background: #ddd;
@@ -17,8 +108,8 @@
     box-sizing: border-box;
   }
   .react-grid-item:not(.react-grid-placeholder) {
-    background: #ccc;
-    border: 1px solid black;
+    background: white;
+    border: 1px solid #D6D6D6;
   }
   .react-grid-item.resizing {
     opacity: 0.9;
@@ -101,25 +192,23 @@
     transition-property: transform;
   }
   .react-grid-item.resizing {
-    z-index: 1;
+    z-index: 2;
+    opacity: 0.5;
     will-change: width, height;
   }
   
   .react-grid-item.react-draggable-dragging {
     transition: none;
+    opacity: 0.5;
     z-index: 3;
     will-change: transform;
   }
   
   .react-grid-item.react-grid-placeholder {
-    background: red;
-    opacity: 0.2;
+    background: #dddddd;
+    border: 5px dashed #cccccc;
     transition-duration: 100ms;
-    z-index: 2;
-    -webkit-user-select: none;
-    -moz-user-select: none;
-    -ms-user-select: none;
-    -o-user-select: none;
+    z-index: 1;
     user-select: none;
   }
   

+ 84 - 16
src/components/datasource/columnConfig.jsx

@@ -1,5 +1,5 @@
 import React from 'react'
-import { Form, Input, Button, Select, Table, Checkbox, Divider, Icon, Popconfirm } from 'antd'
+import { Form, Input, Button, Select, Table, Checkbox, Divider, Icon, Popconfirm, Switch } from 'antd'
 import { connect } from 'dva'
 import COLUMN_TYPE from './columnType.json'
 import { Resizable } from 'react-resizable';
@@ -93,18 +93,18 @@ class DataSourceColumnConfig extends React.Component {
             dataIndex: 'name',
             key: 'name',
             width: widths[1],
-        }, {
-            title: '备注',
-            dataIndex: 'description',
-            key: 'description',
-            width: widths[2],
+        // }, {
+        //     title: '备注',
+        //     dataIndex: 'description',
+        //     key: 'description',
+        //     width: widths[2],
+        // }, {
+        //     title: '数据类型',
+        //     dataIndex: 'dataType',
+        //     key: 'dataType',
+        //     width: widths[3]
         }, {
             title: '数据类型',
-            dataIndex: 'dataType',
-            key: 'dataType',
-            width: widths[3]
-        }, {
-            title: '分析类型',
             dataIndex: 'columnType',
             key: 'columnType',
             width: widths[4],
@@ -127,18 +127,86 @@ class DataSourceColumnConfig extends React.Component {
                     >
                     {
                         COLUMN_TYPE.map( c => {
-                            let dataType = record.dataType;
-                            if(c.dataType.indexOf(dataType) !== -1) {
+                            // let dataType = record.dataType;
+                            // if(c.dataType.indexOf(dataType) !== -1) {
                                 return <SelectOption value={c.columnType} key={c.columnType}>{c.label}</SelectOption>
-                            }else {
-                                return null
-                            }
+                            // }else {
+                            //     return null
+                            // }
                             
                         }).filter((s)=>s!==null)
                     }
                     </Select>
                 )
             }
+        }, {
+            title: <div><Checkbox
+                style={{ margin: '0 8px 0 0', display: dataSource.newOne.columns ? (dataSource.newOne.columns.length > 0 ? 'inline-block' : 'none') : 'none'}}
+                indeterminate={dataSource.newOne.columns ? (dataSource.newOne.columns.filter(c => c.groupable).length > 0 && dataSource.newOne.columns.filter(c => c.groupable).length < dataSource.newOne.columns.length) : false}
+                checked={dataSource.newOne.columns ? (dataSource.newOne.columns.filter(c => c.groupable).length === dataSource.newOne.columns.length) : false}
+                onChange={(e) => {
+                    let target = e.target;
+                    let columns = dataSource.newOne.columns ? dataSource.newOne.columns.map(c => {
+                        c.groupable = target.checked;
+                        return c;
+                    }) : [];
+
+                    dispatch({ type: 'dataSource/setNewModelField', name: 'columns', value: columns });
+                }}
+            />允许分组</div>,
+            dataIndex: 'groupable',
+            key: 'groupable',
+            width: 50,
+            render: (v, r) => <Checkbox
+                dataKey={r.key}
+                onChange={(e) => {
+                    let target = e.target;
+                    let key = target.dataKey;
+                    let columns = dataSource.newOne.columns.map(c => {
+                        if(c.key === key) {
+                            c.groupable = target.checked;
+                        }
+                        return c;
+                    });
+
+                    dispatch({ type: 'dataSource/setNewModelField', name: 'columns', value: columns });
+                }}
+                checked={v}
+            />
+        }, {
+            title: <div><Checkbox
+                style={{ margin: '0 8px 0 0', display: dataSource.newOne.columns ? (dataSource.newOne.columns.length > 0 ? 'inline-block' : 'none') : 'none'}}
+                indeterminate={dataSource.newOne.columns ? (dataSource.newOne.columns.filter(c => c.filterable).length > 0 && dataSource.newOne.columns.filter(c => c.filterable).length < dataSource.newOne.columns.length) : false}
+                checked={dataSource.newOne.columns ? (dataSource.newOne.columns.filter(c => c.filterable).length === dataSource.newOne.columns.length) : false}
+                onChange={(e) => {
+                    let target = e.target;
+                    let columns = dataSource.newOne.columns ? dataSource.newOne.columns.map(c => {
+                        c.filterable = target.checked;
+                        return c;
+                    }) : [];
+
+                    dispatch({ type: 'dataSource/setNewModelField', name: 'columns', value: columns });
+                }}
+            />允许过滤</div>,
+            dataIndex: 'filterable',
+            key: 'filterable',
+            width: 50,
+            render: (v, r) => <Checkbox
+                dataKey={r.key}
+                onChange={(e) => {
+                    let target = e.target;
+                    let key = target.dataKey;
+                    let columns = dataSource.newOne.columns.map(c => {
+                        if(c.key === key) {
+                            c.filterable = target.checked;
+                        }
+                        return c;
+                    });
+
+                    dispatch({ type: 'dataSource/setNewModelField', name: 'columns', value: columns });
+                }}
+                checked={v}
+            />
         // }, {
         //     title: '允许分组',
         //     dataIndex: 'groupable',

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

@@ -287,7 +287,7 @@ class DataSource extends React.Component {
                     <div>
                         <span>
                             { filterLabel ?
-                                (text.split(new RegExp(`(${filterLabel})`, 'i')).map((fragment, i) => {
+                                ((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> :

+ 14 - 2
src/constants/url.js

@@ -1,5 +1,5 @@
-const BASE_URL = 'http://192.168.253.189:8081/BI';
-// const BASE_URL = 'http://192.168.253.129:8080';
+// const BASE_URL = 'http://192.168.253.189:8081/BI';
+const BASE_URL = 'http://192.168.253.129:8080';
 
 /**后台接口地址 */
 const URLS = {
@@ -87,5 +87,17 @@ const URLS = {
     GROUP_CHART_LIST_UPDATE: BASE_URL + '/updataListGroup', // 批量修改图表分组信息
 
     GROUP_CHART_DELETE: BASE_URL + '/delChartsGroup', // 删除图表分组/子分组
+
+    /***************************************看板***************************************/
+
+    DASHBOARD_LIST: BASE_URL + '/getListDashboards', // 获得看板列表
+
+    DASHBOARD_DETAIL: BASE_URL + '/getDashboards', // 获得单个看板
+    
+    DASHBOARD_ADD: BASE_URL + '/inputDashboards', // 新增看板
+
+    DASHBOARD_DELETE: BASE_URL + '/delDashboards', // 删除看板
+
+    DASHBOARD_UPDATE: BASE_URL + '/updateDashboards', // 更新看板
 }
 export default URLS

+ 3 - 3
src/index.js

@@ -29,11 +29,11 @@ app.use(createLoading());
 
 // 3. Model
 app.model(mainModel); // 通用action
-app.model(chartDesigner); // 图表设计
-app.model(dataSource); // 数据源
 app.model(dataConnect); // 数据连接
-app.model(dashboard);  //报告与看板
+app.model(dataSource); // 数据源
 app.model(chart);  // 图表
+app.model(chartDesigner); // 图表设计
+app.model(dashboard);  //报告与看板
 app.model(dashboardDesigner); // 看板设计
 app.model(user); // 用户
 

+ 22 - 22
src/models/chart.js

@@ -2,6 +2,24 @@ import { message } from 'antd'
 import * as service from '../services/index'
 import URLS from '../constants/url'
 
+const getViewType = function(type) {
+    if(type === 'Histogram') {
+        return 'bar';
+    }else if(type === 'Line') {
+        return 'line';
+    }else if(type === 'Pie') {
+        return 'pie';
+    }else if(type === 'scatter') {
+        return 'scatter';
+    }else if(type === 'population') {
+        return 'aggregateTable';
+    }else if(type === 'individual') {
+        return 'dataView';
+    }else {
+        return '';
+    }
+}
+
 export default {
     namespace: 'chart',
     state: {
@@ -125,7 +143,7 @@ export default {
                         return {
                             code:  d.chartId+'',
                             name: d.chartName,
-                            type: d.chartType,
+                            type: getViewType(d.chartType),
                             creator: d.createBy,
                             createTime: d.createDate,
                             description: d.describes || '',
@@ -133,6 +151,7 @@ export default {
                             chartOption: chartOption
                         }
                     })
+                    console.log('请求图表列表', list);
                     yield put({ type: 'list', list: list });
                 }else {
                     message.error('请求图表列表失败: ' + (res.err || res.data.msg));
@@ -154,24 +173,6 @@ export default {
                 });
                 console.log('解析图表数据', code, res);
                 if(!res.err && res.data.code > 0) {
-                    const getViewType = function(type) {
-                        if(type === 'Histogram') {
-                            return 'bar';
-                        }else if(type === 'Line') {
-                            return 'line';
-                        }else if(type === 'Pie') {
-                            return 'pie';
-                        }else if(type === 'scatter') {
-                            return 'scatter';
-                        }else if(type === 'population') {
-                            return 'aggregateTable';
-                        }else if(type === 'individual') {
-                            return 'dataView';
-                        }else {
-                            return '';
-                        }
-                    }
-
                     let resData = res.data.data;
                     let groupBy = JSON.parse(resData.groupBy) || [];
                     let chartConfig = JSON.parse(resData.chartConfig || '{ "xAxis": { "column": {}, "granularity": {} }, "yAxis": { "column": {}, "gauge": {} } }');
@@ -772,8 +773,7 @@ export default {
     },
     subscriptions: {
         setup({ dispatch, history }) {
-            return history.listen(({ pathname, query }) => {
-            })
-        }
+            return history.listen(({ pathname, query }) => {}
+        )}
     }
 }

+ 5 - 1
src/models/chartDesigner.js

@@ -191,8 +191,9 @@ export default {
                     describes: '',
                     style: '',
                     chartConfig: '{}',
-                    chartType: ''
+                    chartType: '',
                 };
+                console.log('快速新增图表', body);
                 const res = yield call(service.fetch, {
                     url: URLS.CHART_ADD,
                     body: body
@@ -302,6 +303,9 @@ export default {
                             name: c.columnName,
                             label: c.columnRaname,
                             type: c.columnType,
+                            groupable: c.isGroup==='1'?true:false,
+                            filterable: c.isFilter==='1'?true:false,
+                            bucketizable: c.isSubsection==='1'?true:false,
                             selection: []
                         }
                     })

+ 131 - 99
src/models/dashboard.js

@@ -1,5 +1,6 @@
 import { message } from 'antd'
 import * as service from '../services/index'
+import URLS from '../constants/url'
 
 export default {
     namespace: 'dashboard',
@@ -13,121 +14,152 @@ export default {
         }],
         groupDirty: false,
         
-        currentDashboard: {},
-        dashboardList: [{            //Dynamic Dashboard指看板(动态) Static Dashboard指报告(静态)
-            dashboardID: 1,
-            type: 'dynamic',
-            title: 'Card 1',
-            url: '',
-            description: 'Description 1',
-            coverImg:'https://gw.alipayobjects.com/zos/rmsportal/JiqGstEfoWAOHiTxclqi.png',
-            coverAlt: '',
-            avatar: '',
-            dashboardConfig: {},
-            creator: {}
-        },{
-            dashboardID: 2,
-            type: 'dynamic',
-            title: 'Card 2',
-            url: '',
-            description: 'Description 1',
-            coverImg:'https://gw.alipayobjects.com/zos/rmsportal/JiqGstEfoWAOHiTxclqi.png',
-            coverAlt: '',
-            avatar:{},
-            dashboardConfig: {},
-            creator: {}
-        },{
-            dashboardID: 3,
-            type: 'dynamic',
-            title: 'Card 3',
-            url: '',
-            description: 'Description 1',
-            coverImg:'https://gw.alipayobjects.com/zos/rmsportal/JiqGstEfoWAOHiTxclqi.png',
-            coverAlt: '',
-            avatar:{},
-            dashboardConfig: {},
-            creator: {}
-        },{
-            dashboardID: 6,
-            type: 'dynamic',
-            title: 'Card 6',
-            url: '',
-            description: 'Description 1',
-            coverImg:'https://gw.alipayobjects.com/zos/rmsportal/JiqGstEfoWAOHiTxclqi.png',
-            coverAlt: '',
-            avatar:{},
-            dashboardConfig: {},
-            creator: {}
-        },{
-            dashboardID: 7,
-            type: 'dynamic',
-            title: 'Card 7',
-            url: '',
-            description: 'Description 1',
-            coverImg:'https://gw.alipayobjects.com/zos/rmsportal/JiqGstEfoWAOHiTxclqi.png',
-            coverAlt: '',
-            avatar:{},
-            dashboardConfig: {},
-            creator: {}
-        },{
-            dashboardID: 4,
-            type: 'static',
-            title: 'Card 4',
-            url: '',
-            description: 'Description 1',
-            coverImg:'https://gw.alipayobjects.com/zos/rmsportal/JiqGstEfoWAOHiTxclqi.png',
-            coverAlt: '',
-            avatar:{},
-            dashboardConfig: {},
-            creator: {}
-        },{
-            dashboardID: 5,
-            type: 'static',
-            title: 'Card 5',
-            url: '',
-            description: 'Description 1',
-            coverImg:'https://gw.alipayobjects.com/zos/rmsportal/JiqGstEfoWAOHiTxclqi.png',
-            coverAlt: '',
-            avatar:{},
-            dashboardConfig: {},
-            creator: {}
-        }
-        ],
     },
     reducers: {
         list(state, action) {
-            let data = action.data;
-            return Object.assign({}, state, {dashboardList: data});
+            let list = action.list;
+            return Object.assign({}, state, {list});
+        },
+        setFilterLabel(state, action) {
+            let { label } = action;
+            return Object.assign({}, state, {filterLabel: label});
         }
     },
-    effets: {
+    effects: {
         *fetchList(action, {select, call, put}) {
             try {
-                const dashboard = yield select(state => state.present.dashboard)
+                const dashboard = yield select(state => state.present.dashboard);
                 if(!action.mandatory && dashboard.list.length > 0) {
                     return;
                 }
-                const response = yield call(service.fetch, {
-                    url: "127.0.0.1:5000/dashboard",
-                    method: 'GET',
-                    body: {}
+                const res = yield call(service.fetch, {
+                    url: URLS.DASHBOARD_LIST
                 });
-                if(!response.err && response.data.code > 0) {
-                    // let data = response
+                if(!res.err && res.data.code > 0) {
+                    const resData = res.data.data;
+                    let list = resData.map(d => {
+                        let items = d.bdConfiguration ? JSON.parse(d.bdConfiguration) : [];
+                        return {
+                            code:  d.id+'',
+                            title: d.bdName,
+                            items: items,
+                            description: d.bdNote || '',
+                            thumbnail: d.thumbnail,
+                            creator: d.createBy,
+                            createTime: d.createDate,
+                        }
+                    })
+                    console.log('请求看板列表', list);
+                    yield put({ type: 'list', list: list });
                 }
             }catch(e) {
-                message.error('读取报告与看板错误')
+                console.log(e);
+                message.error('请求看板列表失败')
             }
-        }
+        },
+        *remoteDetail(action, { select, call, put }) {
+            const code = action.code;
+            if(!code){
+                return
+            }
+            try {
+                const res = yield call(service.fetch, {
+                    url: URLS.DASHBOARD_DETAIL,
+                    body: 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 data = {
+                        code:  resData.id+'',
+                        title: resData.bdName,
+                        items: items,
+                        description: resData.bdNote || '',
+                        thumbnail: resData.thumbnail,
+                        creator: resData.createBy,
+                        createTime: resData.createDate,
+                    }
+
+                    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) {
+                console.log(e);
+                message.error('解析看板错误');
+            }
+        },
+        *remoteAdd(action, { select, call, put }) {
+            try {
+                const dashboardDesigner = yield select(state => state.present.dashboardDesigner);
+                const { title, items } = dashboardDesigner;
+                let body = {
+                    bdName: title,
+                    bdNote: '',
+                    configuration: JSON.stringify(items),
+                    thumbnail: '',
+                    createBy: 'zhuth'
+                }
+                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('保存成功');
+                }else {
+                    message.error('保存失败: ' + (res.err || res.data.msg));
+                } 
+            }catch(e) {
+                console.log(e);
+                message.error('保存失败');
+            }
+        },
+        *remoteModify(action, { select, call, put }) {
+            try {
+                const dashboardDesigner = yield select(state => state.present.dashboardDesigner);
+                const { code, title, items } = dashboardDesigner;
+                let body = {
+                    id: code,
+                    bdName: title,
+                    bdNote: '',
+                    bdConfiguration: JSON.stringify(items),
+                    thumbnail: '',
+                    createBy: 'zhuth'
+                }
+                console.log('修改看板', body);
+                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 });
+                    message.success('保存成功');
+                }else {
+                    message.error('保存失败: ' + (res.err || res.data.msg));
+                } 
+            }catch(e) {
+                console.log(e);
+                message.error('保存失败');
+            }
+        },
     },
     subscriptions: {
         setup({ dispatch, history}) {
-            return history.listen(({pathname}) => {
-                if (pathname === '/dashboard') {
-                    dispatch({ type: 'fetchList'});
-                }
-            }
-        )
-        }
+            return history.listen(({pathname}) => {}
+        )}
     }
 }

+ 101 - 45
src/models/dashboardDesigner.js

@@ -1,6 +1,20 @@
+import { message } from 'antd'
+import * as service from '../services/index'
+import URLS from '../constants/url'
+
 export default {
     namespace: 'dashboardDesigner',
     state: {
+        originData: {
+            code: null,
+            title: '标题',
+            items: [],
+            description: '',
+            thumbnail: '',
+            groupCode: '',
+            dirty: false
+        },
+        title: '标题',
         basicConfig: {
             title:'可爱的标题!',  
             description:'',
@@ -15,71 +29,113 @@ export default {
         },
         parameters: {
         },  //全局可用参数
-        items: [{
-            code: 'a001',
-            type: 'chart',
-            layout: { x: 0, y: 0, w: 4, h: 2},
-            content: 'jhadsasda',
-            chartCode: '372',
-        }, {
-            code: 'a002',
-            type: 'simple',
-            layout: { x: 2, y: 2, w: 2, h: 2},
-            content: 'hhhhhhhhh',
-            chartCode: '367'
-        }],
+        items: [],
         configBoxForm: {
-        }
+        },
+        description: '',
+        thumbnail: '',
+        groupCode: '',
+        dirty: false
     },
     
     reducers: {
-        add(state, action) {
+        silentSetField(state, action) {
+            const { name, value } = action;
+            let obj = {};
+            obj[name] = value;
+            let newState = Object.assign({}, state, obj);
+            return newState;
+        },
+        setField(state, action) {
+            const { name, value } = action;
+            let obj = {};
+            obj[name] = value;
+            let newState = Object.assign({}, state, obj);
+            return Object.assign({}, newState, {dirty: true});
+        },
+        silentSetFields(state, action) {
+            const { fields } = action;
+            let obj = {};
+            fields.map(f => (obj[f.name] = f.value));
+            let newState = Object.assign({}, state, obj);
+            return newState;
+        },
+        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, {dirty: true});
+        },
+        addCharts(state, action) {
+            let { items } = state;
+            const { charts } = action;
+
+            items = items.concat(charts.map(c => ({
+                code: c.code,
+                viewType: 'chart',
+                title: c.name,
+                layout: { x: 0, y: 50, w: 12, h: 3},
+                chartCode: c.code
+            })));
+            return Object.assign({}, state, {items, dirty: true});
+        },
+        deleteItem(state, action) {
+            let { items, dirty } = state;
+            const { item } = action;
+            let newItems = items.filter((i) => i.code !== item.code);
+            dirty = items.length !== newItems.length;
+            return { ...state, dirty: dirty, items: newItems };
+        },
+        addRichText(state, action) {
             let { items } = state;
             items.push({
-                code: (Math.random() * 1000)+'',
-                type: 'chart',
-                layout: { x: 0, y: 0, w: 2, h: 2},
-                content: new Date(), 
+                code: Math.random(),
+                viewType: 'richText',
+                title: '',
+                layout: { x: 0, y: 50, w: 12, h: 3}
             });
-            return Object.assign({}, state, {items});
+            return Object.assign({}, state, {items, dirty: true});
         },
-        delete(state, action) {
-            let { items } = state;
-            let { item } = action;
+        changeLayout(state, action) {
+            const { layout } = action;
+            let { items, dirty } = state;
+            const ly = ['x', 'y', 'w', 'h'];
             for(let i = 0; i < items.length; i++) {
-                if(items[i].code === item.code) {
-                    items.splice(i, 1);
-                    break;
+                if(layout[i]) { // 非删除引起
+                    for(let j = 0; j < ly.length; j ++) {
+                        if(items[i].layout[ly[j]] !== layout[i][ly[j]]) {
+                            dirty = true;
+                            items[i].layout[ly[j]] = layout[i][ly[j]];
+                        }
+                    }
+                }else { // 删除引起
+                    dirty = true;
                 }
             }
-            return Object.assign({}, state, {items});
+            return Object.assign({}, state, {items, dirty});
         },
-        changeLayout(state, action) {
-            const { layout } = action;
+        modifyItem(state, action) {
+            let { fields, dirty } = action;
             let { items } = state;
             for(let i = 0; i < items.length; i++) {
-                items[i].layout = layout;
+                if(items[i].code === fields.code) {
+                    for(let k in fields) {
+                        items[i][k] = fields[k];
+                        dirty = true;
+                    }
+                }
             }
-            return Object.assign({}, state, {items});
-        },
-        handleFieldChange(state, action) {
-            const { name, value } = action;
-            let configBoxForm = state.configBoxForm;
-            configBoxForm[name] = value;
-            return Object.assign({}, state, {configBoxForm})
-        },
-        resetConfigBoxForm(state, action) {
-            return Object.assign({}, state, {configBoxForm: {}});
+            return Object.assign({}, state, {items, dirty});
         },
-        loadConfigBoxForm(state, action) {
-            const { element } = action;
-            let configBoxForm = Object.assign({}, element);
-            return Object.assign({}, state, {configBoxForm})
+        reset(state, action) {
+            let newState = Object.assign({}, state, state.originData);
+            return Object.assign({}, newState);
         }
     },
 
     effects: {
-
+        
     },
     subscription: {
 

+ 8 - 4
src/models/dataSource.js

@@ -277,6 +277,7 @@ export default {
                         code: resData.dataId,
                         name: resData.dataName,
                         type: resData.type,
+                        connectCode: dbConfig.id,
                         dbType: dbConfig.databaseType,
                         dbName: dbConfig.dataName,
                         address: dbConfig.addrass,
@@ -298,6 +299,7 @@ export default {
                                 dataType: c.dataType,
                                 columnType: c.columnType,
                                 groupable: c.isGroup==='1'?true:false,
+                                filterable: c.isFilter==='1'?true:false,
                                 bucketizable: c.isSubsection==='1'?true:false,
                                 description: c.remarks
                             }
@@ -318,7 +320,7 @@ export default {
                 const dataSource = yield select(state => state.present.dataSource);
                 const sqlStr = dataSource.newOne.target;
                 let body = {
-                    id: dataSource.newOne.code,
+                    id: dataSource.newOne.connectCode,
                     strSql: sqlStr
                 };
                 const res = yield call(service.fetch, {
@@ -327,7 +329,7 @@ export default {
                 });
     
                 const getColumnType = (dataType) => {
-                    return DEFAULT_COLUMN_TYPE[dataType] || 'string';
+                    return DEFAULT_COLUMN_TYPE[dataType] || 'categorical';
                 }
     
                 console.log('请求列数据', body, res);
@@ -337,11 +339,12 @@ export default {
                         return {
                             key: i,
                             using: true,
-                            name: d.columnName,
-                            alias: d.remarks ? d.remarks.substring(0, 10) : (d.columnName),
+                            name: d,
+                            alias: d.remarks ? d.remarks.substring(0, 10) : (d),
                             dataType: d.columnType,
                             columnType: getColumnType(d.columnType),
                             groupable: d.columnType === 'VARCHAR2',
+                            filterable: true,
                             bucketizable: d.columnType === 'NUMBER',
                             description: d.remarks
                         }
@@ -415,6 +418,7 @@ export default {
                             dataType: c.dataType,
                             columnType: c.columnType,
                             isGroup: c.groupable?'1':'0',
+                            isFilter: c.filterable?'1':'0',
                             isSubsection: c.bucketizable?'1':'0',
                             isOpen: c.using?'1':'0',
                             remarks: c.description

+ 11 - 0
src/models/main.js

@@ -19,6 +19,17 @@ export default {
                 window.location.reload();
             }
         },
+        * goBack (action, { put }) {
+            const { reload, path } = action;
+            if(window.history.length === 1) {
+                yield put(routerRedux.push(path || '/'));
+            }else {
+                yield put(routerRedux.goBack());
+            }
+            if(reload) {
+                window.location.reload();
+            }
+        },
     },
     subscriptions: {
         setup({ dispatch, history }) {

+ 1 - 1
src/routes/mainLayout.js

@@ -6,7 +6,7 @@ import Welcome from '../components/myPage/welcome'
 import Loading from '../components/common/loading'
 import DataSourceDetail from '../components/datasource/dataSourceDetail'
 import DataSource from '../components/datasource/dataSource'
-import Dashboard from '../components/dashboard/dashboard'
+import Dashboard from '../components/dashboard/list'
 import Chart from '../components/chart/list'
 import './mainLayout.less';
 import Demo from '../demo';

Some files were not shown because too many files changed in this diff