Browse Source

数据源、图表分组功能涉及完善/搜索过滤正则转义

zhuth 7 years ago
parent
commit
76cc061aea

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


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


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

@@ -48,6 +48,9 @@ t9.5 -10.5t21.5 -4h37h67h81h80h64h36q23 0 34 12t2 38q-5 13 -9.5 30.5t-9.5 34.5q-
     <glyph glyph-name="chart-bar" unicode="&#60944;" d="M464 96h96a48 48 0 0 1 48 48V544a48 48 0 0 1-48 48h-96a48 48 0 0 1-48-48v-400a48 48 0 0 1 48-48z m272 0h96a48 48 0 0 1 48 48V320a48 48 0 0 1-48 48h-96a48 48 0 0 1-48-48v-176a48 48 0 0 1 48-48z m248-48h-944A40 40 0 0 1 0 8v-16A40 40 0 0 1 40-48h944a40 40 0 0 1 40 40v16a40 40 0 0 1-40 40zM192 96h96a48 48 0 0 1 48 48V768a48 48 0 0 1-48 48h-96a48 48 0 0 1-48-48v-624a48 48 0 0 1 48-48z"  horiz-adv-x="1024" />
 
     
+    <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" />
+
+    
 
 
   </font>

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


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


+ 8 - 2
src/components/chart/chooseDataSourceBox.jsx

@@ -1,8 +1,9 @@
 import React from 'react'
-import { Modal, Table, Radio, message } from 'antd'
+import { Modal, Table, Radio, message, Input, Row, Col } from 'antd'
 import { connect } from 'dva'
 import './chooseDataSourceBox.less'
 import { dateFormat } from '../../utils/baseUtils'
+const { Search } = Input
 
 class ChooseDataSourceBox extends React.Component {
     constructor(props) {
@@ -72,7 +73,12 @@ class ChooseDataSourceBox extends React.Component {
         return (
             <Modal
                 className='choosedatasource-box'
-                title='选择数据源'
+                title={
+                    <Row>
+                        <Col span={14}>选择数据源</Col>
+                        <Col span={8}><Search /></Col>
+                    </Row>
+                }
                 visible={visibleBox}
                 onOk={this.okHandler}
                 onCancel={hideBox}

+ 250 - 34
src/components/chart/list.jsx

@@ -1,5 +1,5 @@
 import React from 'react'
-import { Layout, Button, Icon, Input, Menu, Dropdown, Card, Col, Row } from 'antd'
+import { Layout, Button, Icon, Input, Menu, Dropdown, Card, Col, Row, Popover, Breadcrumb, Tree, Tag } from 'antd'
 import { connect } from 'dva'
 import './list.less'
 import ChooseDataSourceBox from './chooseDataSourceBox'
@@ -9,14 +9,16 @@ import 'ant-design-pro/dist/ant-design-pro.css'
 const { Content } = Layout
 const { Search } = Input
 const CardGrid = Card.Grid
+const { TreeNode } = Tree
 
 
 class ChartList extends React.Component {
     constructor(props) {
         super(props);
         this.state = {
-            selectedCode: -1,
-            visibleBox: false
+            selectedRecord: null,
+            visibleBox: false,
+            visibleGrouMenu: false, // 显示分组菜单
         }
     }
 
@@ -43,14 +45,21 @@ class ChartList extends React.Component {
 
     generateCard() {
         const { chart, dispatch } = this.props;
+        const { selectedRecord } = this.state;
         const list = chart.list;
-        const filterLabel = chart.filterLabel;
+
+        const reg = new RegExp('([+ \\- & | ! ( ) { } \\[ \\] ^ \" ~ * ? : ( ) \/])', 'g'); // 需要转义的字符
+        let filterLabel = chart.filterLabel.replace(new RegExp('(\\\\)', 'g'), '\\$1').replace(reg, '\\$1'); // 添加转义符号
 
         const operationMenu = (
-            <Menu className='menu-operation' onClick={() => {
-                dispatch({ type: 'chart/remoteDelete', code: this.state.selectedCode });
-            }}>
-                <Menu.Item>
+            <Menu className='menu-operation'>
+                <Menu.SubMenu className='setgroupmenu' title={<div><Icon style={{ marginRight: '6px' }} type='profile' />分组</div>}>
+                    {this.createGroupMenu(selectedRecord)}
+                </Menu.SubMenu>
+                <Menu.Divider />
+                <Menu.Item onClick={() => {
+                    dispatch({ type: 'chart/remoteDelete', code: this.state.selectedRecord.code });
+                }}>
                     <Icon type='delete'/>删除
                 </Menu.Item>
             </Menu>
@@ -63,12 +72,25 @@ class ChartList extends React.Component {
             return new Date(b.createTime) - new Date(a.createTime)
         }).map( (l, i) => (
             <CardGrid className='chart-card' key={i} onClick={() => {
-                this.setState({ selectedCode: l.code })
+                this.setState({ selectedRecord: l })
             }}>
                 <Card
                     title={
                         <Row type='flex' justify='space-between'>
-                            <Col>{l.name}</Col>
+                            <Col>
+                                <span>
+                                    { filterLabel ?
+                                        (l.name.split(new RegExp(`(${filterLabel})`, 'i')).map((fragment, i) => {
+                                            return (
+                                                fragment.toLowerCase().replace(new RegExp('(\\\\)', 'g'), '\\$1').replace(reg, '\\$1') === filterLabel.toLowerCase() ?
+                                                <span key={i} style={{fontWeight: 'bold', color: 'red'}} className="highlight">{fragment}</span> :
+                                                fragment
+                                            )
+                                        }
+                                        )) : l.name
+                                    }
+                                </span>
+                            </Col>
                             <Col style={{ textAlign: 'right' }}>
                                 <Icon type='star-o'/>
                             </Col>
@@ -107,37 +129,231 @@ class ChartList extends React.Component {
         return cards;
     }
 
-    render() {
-        const { visibleBox } = this.state;
+    createGroupMenu = (selectedRecord) => {
+        const { chart, dispatch } = this.props;
+        const groupList = chart.groupList;
+        const parent = groupList.filter(d => d.pcode === '-1').sort((a, b) => a.index - b.index);
+        const children = groupList.filter(d => d.pcode !== '-1');
+
+        parent.unshift({
+            code: 'all',
+            label: '全部分组'
+        });
+        return parent.map(p => {
+            let c = children.filter(c => c.pcode === p.code).sort((a, b) => a.index - b.index);
+            return c.length > 0 ? (
+                <Menu.SubMenu key={p.code} title={<span style={{ fontWeight: chart.currentGroup[0].code === p.code ? 'bold' : 'normal' }}>{p.label}</span>} onTitleClick={(item) => {
+                    dispatch({ type: 'chart/setCurrentGroup', group1: p });
+                    if(selectedRecord) {
+                        dispatch({ type: 'chart/remoteSetGroup', chart: selectedRecord, group: p });
+                    }
+                    this.hideGourMenu();
+                }}>
+                    {c.map(c => {
+                        return (<Menu.Item key={c.code} onClick={(item) => {
+                            dispatch({ type: 'chart/setCurrentGroup', group1: p, group2: c });
+                            if(selectedRecord) {
+                                dispatch({ type: 'chart/remoteSetGroup', chart: selectedRecord, group: p });
+                            }
+                        }}><span style={{ fontWeight: chart.currentGroup[1] && (chart.currentGroup[1].code === c.code) ? 'bold' : 'normal' }}>{c.label}</span></Menu.Item>)
+                    })}
+                </Menu.SubMenu>
+            ) : (
+                <Menu.Item key={p.code} onClick={() => {
+                    dispatch({ type: 'chart/setCurrentGroup', group1: p });
+                    this.hideGourMenu();
+                }}><span style={{ fontWeight: chart.currentGroup[0] && (chart.currentGroup[0].code === p.code) ? 'bold' : 'normal' }}>{p.label}</span></Menu.Item>
+            );
+        });
+    }
+
+    createSubGroupMenu = () => {
+        const { chart, dispatch } = this.props;
+        const groupList = chart.groupList;
+        const parentGroup = chart.currentGroup[0];
+        const children = groupList.filter(d => d.pcode === parentGroup.code);
+        const subGroup = chart.currentGroup[1];
+
+        return children.map(c => {
+            return (
+                <Menu.Item key={c.code} onClick={() => {
+                    dispatch({ type: 'chart/setCurrentGroup', group1: parentGroup, group2: c });
+                }}><span style={{ fontWeight: subGroup && (subGroup.code === c.code) ? 'bold' : 'normal' }}>{c.label}</span></Menu.Item>
+            );
+        })
+    }
+
+    createGroupTree(modify) {
+        const { dispatch, chart } = this.props;
+        const { groupEditing } = this.state;
+        const groupList = chart.groupList;
+
+        let parent = groupList.filter(d => d.pcode === '-1').sort((a, b) => a.index - b.index);
+        let children = groupList.filter(d => d.pcode !== '-1');
+
+        let groupTree = parent.map(p => {
+            return (
+                <TreeNode disabled={groupEditing} title={
+                modify ? (<div><Icon style={{ cursor: 'move' }} type='drag'/>
+                <Input value={p.label} size='small' focus={'true'} onFocus={() => {
+                    this.setState({
+                        groupEditing: true
+                    });
+                }} onChange={(e) => {
+                    dispatch({ type: 'chart/modifyGroup', group: {...p, label:e.target.value} });
+                }} onBlur={(e) => {
+                    this.setState({
+                        groupEditing: false
+                    });
+                    dispatch({ type: 'chart/remoteModifyGroup', group: {...p, label:e.target.value} });
+                }} onPressEnter={(e) => {
+                    dispatch({ type: 'chart/remoteModifyGroup', group: {...p, label:e.target.value} });
+                }} /><Icon type='plus-circle-o' onClick={() => {
+                    dispatch({ type: 'chart/remoteAddGroup', pgroup: p });
+                }}/><Icon type='minus-circle' onClick={() => {
+                    dispatch({ type: 'chart/remoteDeleteGroup', group: p });
+                }}/></div>) : p.label} key={p.code}>
+                    {
+                        children.filter(c => c.pcode === p.code).sort((a, b) => a.index - b.index).map(c => {
+                            return (
+                                <TreeNode disabled={groupEditing} title={
+                                    modify ? (<div><Icon style={{ cursor: 'move' }} type='drag'/>
+                                    <Input value={c.label} size='small' onFocus={() => {
+                                        this.setState({
+                                            groupEditing: true
+                                        });
+                                    }} onChange={(e) => {
+                                        dispatch({ type: 'chart/modifyGroup', group: {...c, label:e.target.value} });
+                                    }} onBlur={(e) => {
+                                        this.setState({
+                                            groupEditing: false
+                                        });
+                                        dispatch({ type: 'chart/remoteModifyGroup', group: {...c, label:e.target.value} });
+                                    }} onPressEnter={(e) => {
+                                        dispatch({ type: 'chart/remoteModifyGroup', group: {...c, label:e.target.value} });
+                                    }} onCompositionEnd={(e) => {
+                                        console.log(e.target.value);
+                                    }}/><Icon type='minus-circle' onClick={() => {
+                                        dispatch({ type: 'chart/remoteDeleteGroup', group: c });
+                                    }}/></div>) : p.label
+                                } key={c.code} />
+                            )
+                        })
+                    }
+                </TreeNode>
+            )
+        });
+
+        return groupTree;
+    }
+
+    handleVisibleChange = (flag) => {
+        this.setState({ visibleGrouMenu: flag });
+    }
+
+    hideGourMenu = () => {
+        this.setState({
+            visibleGrouMenu: false
+        });
+    }
+
+    onDrop = (info) => {
         const { dispatch } = this.props;
 
+        const dropCode = info.node.props.eventKey;
+        const dragCode = info.dragNode.props.eventKey;
+        const dropPos = info.node.props.pos.split('-');
+        const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]); // -1/0/1 -> 兄/子/弟
+        console.log(dragCode, dropCode, dropPosition);
+        dispatch({ type: 'chart/remoteMoveGroup', dragCode, dropCode, dropPosition });
+    }
+
+    render() {
+        const { visibleBox, visibleGrouMenu } = this.state;
+        const { dispatch, chart } = this.props;
+
         return (
             <Layout className='chart-list'>
                 <Content>
                     <Card title={
-                        <Row type='flex' justify='end'>
-                            <Col style={{ padding: '0 5px' }}>
-                                <Search
-                                    placeholder="请输入关键字"
-                                    onChange={e => {
-                                        dispatch({ type: 'chart/setFilterLabel', label: e.target.value });
-                                    }}
-                                />
+                        <Row className='tools' type='flex' justify='space-between'>
+                            <Col style={{ display: 'flex' }}>
+                            <Popover overlayClassName='popover-group' title={
+                                    <Row className='grouptree-title' type='flex' justify='space-between'>
+                                        <Col>
+                                            分组管理
+                                        </Col>
+                                        <Col>
+                                            <div className='create-group' onClick={() => {
+                                                dispatch({ type: 'chart/remoteAddGroup' });
+                                            }}>添加分组<Icon type="plus-circle-o" /></div>
+                                        </Col>
+                                    </Row>
+                                } trigger="click" placement="bottomLeft" content={(
+                                    <Tree
+                                        className='tree-group'
+                                        showLine
+                                        defaultExpandAll
+                                        draggable
+                                        onDragStart={this.onDragStart}
+                                        onDrop={this.onDrop}
+                                    >
+                                        {
+                                            this.createGroupTree(true)
+                                        }
+                                    </Tree>
+                                )}>
+                                    <Icon type="bars" />
+                                </Popover>
+                                <Breadcrumb className='group' separator=">">
+                                    <Breadcrumb.Item>
+                                        <Dropdown
+                                            trigger={['click']}
+                                            onVisibleChange={this.handleVisibleChange}
+                                            visible={visibleGrouMenu}
+                                            overlay={(
+                                            <Menu>
+                                                {this.createGroupMenu()}
+                                            </Menu>
+                                        )}>
+                                            <Tag color='blue'>{chart.currentGroup[0].label}</Tag>
+                                        </Dropdown>
+                                    </Breadcrumb.Item>
+                                    {chart.currentGroup[1] && (<Breadcrumb.Item>
+                                        <Dropdown trigger={['click']} overlay={(
+                                            <Menu>
+                                                {this.createSubGroupMenu()}
+                                            </Menu>
+                                        )}>
+                                            <Tag color='blue'>{chart.currentGroup[1].label}</Tag>
+                                        </Dropdown>
+                                    </Breadcrumb.Item>)}
+                                </Breadcrumb>
                             </Col>
-                            <Col >
-                                <Button onClick={() => {
-                                    dispatch({ type: 'dataSource/fetchList' });
-                                    this.setState({
-                                        visibleBox: true
-                                    });
-                                }}>
-                                    <Icon type="area-chart" />创建图表
-                                </Button>
-                                <ChooseDataSourceBox visibleBox={visibleBox} hideBox={() => {
-                                    this.setState({
-                                        visibleBox: false
-                                    });
-                                }}/>
+                            <Col className='search'>
+                                <Col style={{ padding: '0 5px' }}>
+                                    <Search
+                                        placeholder="请输入关键字"
+                                        onChange={e => {
+                                            dispatch({ type: 'chart/setFilterLabel', label: e.target.value });
+                                        }}
+                                    />
+                                </Col>
+                                <Col >
+                                    <Button onClick={() => {
+                                        dispatch({ type: 'dataSource/fetchList' });
+                                        this.setState({
+                                            visibleBox: true
+                                        });
+                                    }}>
+                                        <Icon type="area-chart" />创建图表
+                                    </Button>
+                                    <ChooseDataSourceBox visibleBox={visibleBox} hideBox={() => {
+                                        this.setState({
+                                            visibleBox: false
+                                        });
+                                    }}/>
+                                </Col>
                             </Col>
                         </Row>
                     }>

+ 89 - 2
src/components/chart/list.less

@@ -1,6 +1,28 @@
 .chart-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;
@@ -52,10 +74,75 @@
         }
     }
 }
+.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 {
+        .anticon {
+            margin-right: 6px;
+        }
+    }
+    .ant-dropdown-menu-item-divider {
+        margin: 0;
+    }
+    .setgroupmenu {
+        .ant-dropdown-menu-submenu-title {
+            display: flex;
+        }
     }
 }
 .ant-tooltip-inner {

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

@@ -31,7 +31,7 @@ class Header extends React.Component {
                             this.setState({
                                 visibleConfirm: false
                             });
-                            dispatch({ type: 'chartDesigner/remoteModify' });
+                            dispatch({ type: 'chart/remoteModify' });
                             dispatch({ type: 'main/redirect', path: '/chart' });
                         }}
                         onCancel={() => {
@@ -57,9 +57,9 @@ class Header extends React.Component {
                     </Popconfirm>
                     <Button className='button-uodo' onClick={() => {
                         if(chartDesigner.code && chartDesigner.code !== -1) {
-                            dispatch({ type: 'chartDesigner/remoteModify' });
+                            dispatch({ type: 'chart/remoteModify' });
                         }else {
-                            dispatch({ type: 'chartDesigner/remoteAdd' });
+                            dispatch({ type: 'chart/remoteAdd' });
                         }
                         dispatch({ type: 'chartDesigner/setDirty', dirty: false });
                     }}><Icon type='save' />保存</Button>

+ 5 - 3
src/components/datasource/columnConfig.jsx

@@ -20,7 +20,7 @@ class DataSourceColumnConfig extends React.Component {
 
     render() {
 
-        const { dataSource, dataConnect, dispatch } = this.props;
+        const { dataSource, dispatch } = this.props;
 
         const columns = [{
             title: <div><Checkbox
@@ -194,16 +194,18 @@ class DataSourceColumnConfig extends React.Component {
                                 <FormItem className='textarea-target'>
                                     <Input.TextArea
                                         disabled={!dataSource.newOne.address}
-                                        placeholder={dataConnect.selected ? '输入表名或查询SQL' : '请返回上一步选择数据库连接'}
+                                        placeholder={dataSource.newOne.address ? '输入表名或查询SQL,注意不能以分号结尾' : '请返回上一步选择数据库连接'}
                                         autosize={{ minRows: 3 }}
                                         value={dataSource.newOne.target}
                                         onChange={(e) => {
+                                            dispatch({ type: 'dataSource/setNewModelInvalidSQL', value: false });
                                             dispatch({ type: 'dataSource/setNewModelField', name: 'target', value: e.target.value });
                                         }}
                                     />
                                 </FormItem>
                                 <div className='buttons'>
-                                    <Button disabled={!dataConnect.selected} onClick={() => {
+                                    <div className='errormessage' style={{ cursor: dataSource.newOne.invalidSQL ? 'text' : 'default', opacity: dataSource.newOne.invalidSQL ? '1' : '0' }}>未查询到列数据,请检查SQL是否正确</div>
+                                    <Button disabled={!dataSource.newOne.address} onClick={() => {
                                         dispatch({ type: 'dataSource/importNewModelColumns' })
                                     }}>刷新</Button>
                                 </div>

+ 280 - 85
src/components/datasource/dataSource.jsx

@@ -1,20 +1,20 @@
 import React from 'react'
-import { Layout, Row, Col, Input, Button, Table, Icon, Menu, Dropdown, Card } from 'antd'
+import { Layout, Row, Col, Input, Button, Table, Icon, Menu, Dropdown, Card, Breadcrumb, Popover, Tree, Tag } from 'antd'
 import { connect } from 'dva'
 import './dataSource.less'
 import { dateFormat } from '../../utils/baseUtils'
 const { Content } = Layout
 const { Search } = Input
+const { TreeNode } = Tree
 
 class DataSource extends React.Component {
     constructor(props) {
         super(props);
         this.state = {
-            list: props.dataSource.list,
-            loading: false,
             selectedRecord: null, // 当前选中的dataSource
-            filterLabel: '',
-            search: {} // 搜索条件
+            visibleGrouMenu: false, // 显示分组菜单
+            visibleSetGroupMenu: false, //
+            groupEditing: false, // 是否处于编辑状态
         }
     };
 
@@ -33,26 +33,178 @@ class DataSource extends React.Component {
         tableBody.style.maxHeight=`${mainContent.offsetHeight - toolbar.offsetHeight - tableHeader.offsetHeight - 58}px`;
     }
 
-    onInputChange = (name, value) => {
-        const { search } = this.state;
-        let newSearch = Object.assign({}, search );
-        newSearch[name] = value;
-        this.setState({ search: newSearch });
+    onSearch(list, text) {
+        return list.map(l => {
+            let o = Object.assign({}, l);
+            let reg = new RegExp('('+ text +'){1}', 'ig');
+            if(o.name.search(reg) !== -1) {
+                return o;
+            }else if(o.description.search(reg) !== -1) {
+                return o;
+            }else {
+                return null
+            }
+        }).filter(a => a!==null);
+    }
+
+    onSort(list) {
+        return list.sort((a, b) => {
+            return new Date(b.createTime) - new Date(a.createTime);
+        });
+    }
+
+    handleVisibleChange = (flag) => {
+        this.setState({ visibleGrouMenu: flag });
+    }
+
+    createGroupMenu = (selectedRecord) => {
+        const { dataSource, dispatch } = this.props;
+        const groupList = dataSource.groupList;
+        const parent = groupList.filter(d => d.pcode === '-1').sort((a, b) => a.index - b.index);
+        const children = groupList.filter(d => d.pcode !== '-1');
+
+        parent.unshift({
+            code: 'all',
+            label: '全部分组'
+        });
+        return parent.map(p => {
+            let c = children.filter(c => c.pcode === p.code).sort((a, b) => a.index - b.index);
+            return c.length > 0 ? (
+                <Menu.SubMenu key={p.code} title={<span style={{ fontWeight: dataSource.currentGroup[0].code === p.code ? 'bold' : 'normal' }}>{p.label}</span>} onTitleClick={(item) => {
+                    dispatch({ type: 'dataSource/setCurrentGroup', group1: p });
+                    if(selectedRecord) {
+                        dispatch({ type: 'dataSource/remoteSetGroup', chart: selectedRecord, group: p });
+                    }
+                    this.hideGourMenu();
+                }}>
+                    {c.map(c => {
+                        return (<Menu.Item key={c.code} onClick={(item) => {
+                            dispatch({ type: 'dataSource/setCurrentGroup', group1: p, group2: c });
+                            if(selectedRecord) {
+                                dispatch({ type: 'dataSource/remoteSetGroup', chart: selectedRecord, group: p });
+                            }
+                        }}><span style={{ fontWeight: dataSource.currentGroup[1] && (dataSource.currentGroup[1].code === c.code) ? 'bold' : 'normal' }}>{c.label}</span></Menu.Item>)
+                    })}
+                </Menu.SubMenu>
+            ) : (
+                <Menu.Item key={p.code} onClick={() => {
+                    dispatch({ type: 'dataSource/setCurrentGroup', group1: p });
+                    this.hideGourMenu();
+                }}><span style={{ fontWeight: dataSource.currentGroup[0] && (dataSource.currentGroup[0].code === p.code) ? 'bold' : 'normal' }}>{p.label}</span></Menu.Item>
+            );
+        });
+    }
+
+    createSubGroupMenu = () => {
+        const { dataSource, dispatch } = this.props;
+        const groupList = dataSource.groupList;
+        const parentGroup = dataSource.currentGroup[0];
+        const children = groupList.filter(d => d.pcode === parentGroup.code);
+        const subGroup = dataSource.currentGroup[1];
+
+        return children.map(c => {
+            return (
+                <Menu.Item key={c.code} onClick={() => {
+                    dispatch({ type: 'dataSource/setCurrentGroup', group1: parentGroup, group2: c });
+                }}><span style={{ fontWeight: subGroup && (subGroup.code === c.code) ? 'bold' : 'normal' }}>{c.label}</span></Menu.Item>
+            );
+        })
+    }
+
+    createGroupTree(modify) {
+        const { dispatch, dataSource } = this.props;
+        const { groupEditing } = this.state;
+        const groupList = dataSource.groupList;
+
+        let parent = groupList.filter(d => d.pcode === '-1').sort((a, b) => a.index - b.index);
+        let children = groupList.filter(d => d.pcode !== '-1');
+
+        let groupTree = parent.map(p => {
+            return (
+                <TreeNode disabled={groupEditing} title={
+                modify ? (<div><Icon style={{ cursor: 'move' }} type='drag'/>
+                <Input value={p.label} size='small' focus={'true'} onFocus={() => {
+                    this.setState({
+                        groupEditing: true
+                    });
+                }} onChange={(e) => {
+                    dispatch({ type: 'dataSource/modifyGroup', group: {...p, label:e.target.value} });
+                }} onBlur={(e) => {
+                    this.setState({
+                        groupEditing: false
+                    });
+                    dispatch({ type: 'dataSource/remoteModifyGroup', group: {...p, label:e.target.value} });
+                }} onPressEnter={(e) => {
+                    dispatch({ type: 'dataSource/remoteModifyGroup', group: {...p, label:e.target.value} });
+                }} /><Icon type='plus-circle-o' onClick={() => {
+                    dispatch({ type: 'dataSource/remoteAddGroup', pgroup: p });
+                }}/><Icon type='minus-circle' onClick={() => {
+                    dispatch({ type: 'dataSource/remoteDeleteGroup', group: p });
+                }}/></div>) : p.label} key={p.code}>
+                    {
+                        children.filter(c => c.pcode === p.code).sort((a, b) => a.index - b.index).map(c => {
+                            return (
+                                <TreeNode disabled={groupEditing} title={
+                                    modify ? (<div><Icon style={{ cursor: 'move' }} type='drag'/>
+                                    <Input value={c.label} size='small' onFocus={() => {
+                                        this.setState({
+                                            groupEditing: true
+                                        });
+                                    }} onChange={(e) => {
+                                        dispatch({ type: 'dataSource/modifyGroup', group: {...c, label:e.target.value} });
+                                    }} onBlur={(e) => {
+                                        this.setState({
+                                            groupEditing: false
+                                        });
+                                        dispatch({ type: 'dataSource/remoteModifyGroup', group: {...c, label:e.target.value} });
+                                    }} onPressEnter={(e) => {
+                                        dispatch({ type: 'dataSource/remoteModifyGroup', group: {...c, label:e.target.value} });
+                                    }} onCompositionEnd={(e) => {
+                                        console.log(e.target.value);
+                                    }}/><Icon type='minus-circle' onClick={() => {
+                                        dispatch({ type: 'dataSource/remoteDeleteGroup', group: c });
+                                    }}/></div>) : p.label
+                                } key={c.code} />
+                            )
+                        })
+                    }
+                </TreeNode>
+            )
+        });
+
+        return groupTree;
     }
 
-    onSearch = (value) => {
+    hideGourMenu = () => {
         this.setState({
-          filterLabel: value
+            visibleGrouMenu: false
         });
-      }
+    }
+
+    onDrop = (info) => {
+        const { dispatch } = this.props;
+
+        const dropCode = info.node.props.eventKey;
+        const dragCode = info.dragNode.props.eventKey;
+        const dropPos = info.node.props.pos.split('-');
+        const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]); // -1/0/1 -> 兄/子/弟
+        console.log(dragCode, dropCode, dropPosition);
+        dispatch({ type: 'dataSource/remoteMoveGroup', dragCode, dropCode, dropPosition });
+    }
 
     render() {
         
         const { dataSource, dispatch } = this.props;
-        const { loading, selectedRecord } = this.state;
-        
+        const { selectedRecord, visibleGrouMenu } = this.state;
+
+        const reg = new RegExp('([+ \\- & | ! ( ) { } \\[ \\] ^ \" ~ * ? : ( ) \/])', 'g'); // 需要转义的字符
+        let filterLabel = dataSource.filterLabel.replace(new RegExp('(\\\\)', 'g'), '\\$1').replace(reg, '\\$1'); // 添加转义符号
+
+        const TAG_COLOR = ['blue'];
+        // const TAG_COLOR1 = ['magenta', 'red', 'volcano', 'orange', 'gold', 'lime', 'green', 'cyan', 'blue', 'geekblue', 'purple'];
+
         const moreOperatingMenu = (
-            <Menu className='operationmenu'>
+            <Menu className='operationmenu' visible={true}>
                 <Menu.Item
                     onClick={() => {
                         dispatch({ type: 'chartDesigner/remoteQucikAdd', dataSource: selectedRecord });
@@ -70,7 +222,9 @@ class DataSource extends React.Component {
                 </Menu.Item>
                 <Menu.Item><Icon type="search" />预览数据</Menu.Item>
                 <Menu.Divider />
-                <Menu.Item><Icon type="profile" />分组</Menu.Item>
+                <Menu.SubMenu className='setgroupmenu' title={<div><Icon style={{ marginRight: '6px' }} type='profile' />分组</div>}>
+                    {this.createGroupMenu(selectedRecord)}
+                </Menu.SubMenu>
                 <Menu.Divider />
                 <Menu.Item
                     onClick={(e) => {
@@ -89,40 +243,40 @@ class DataSource extends React.Component {
                     <div className={`datasource-type type-${record.type.key}`}></div>
                     <div>
                         <span>
-                            { dataSource.filterLabel? (text.split(new RegExp(`(${dataSource.filterLabel})`, 'i')).map((fragment, i) => (
-                                fragment.toLowerCase() === dataSource.filterLabel.toLowerCase()
-                                ? <span key={i} style={{fontWeight: 'bold', color: 'red'}} className="highlight">{fragment}</span> : fragment)
-                            )) : text }
+                            { 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: 'tags',
-        //     key: 'tag',
-        //     width: 150,
-        //     render: (text, record) => {
-        //         text=text.join(',');
-        //         let tags = text ? text.split(',').map((t, i) => {
-        //             return <Tag className='datasource-tag' key={i}>{t}</Tag>
-        //         }) : '';
-        //         return (<div>
-        //             {tags}
-        //         </div>)
-        //     }
-        // }, {
             title: '说明',
             dataIndex: 'description',
             key: 'description',
             width: 200,
             render: (text, record) => {
-                return <span>
-                    { dataSource.filterLabel? (text.split(new RegExp(`(${dataSource.filterLabel})`, 'i')).map((fragment, i) => (
-                        fragment.toLowerCase() === dataSource.filterLabel.toLowerCase()
-                        ? <span key={i} style={{fontWeight: 'bold', color: 'red'}} className="highlight">{fragment}</span> : fragment
-                    ))) : text }
-                </span>
+                return (
+                    <span>
+                        { filterLabel ?
+                            (text.split(new RegExp(`(${filterLabel})`, 'i')).map((fragment, i) => {
+                                return (
+                                    fragment.toLowerCase().replace(new RegExp('(\\\\)', 'g'), '\\$1').replace(reg, '\\$1') === filterLabel.toLowerCase() ?
+                                    <span key={i} style={{fontWeight: 'bold', color: 'red'}} className="highlight">{fragment}</span> :
+                                    fragment
+                                )
+                            }
+                            )) : text
+                        }
+                    </span>
+                )
             }
         }, {
             title: '创建人',
@@ -136,11 +290,6 @@ class DataSource extends React.Component {
             render: (text, record) => dateFormat(text, 'yyyy-MM-dd hh:mm:ss'),
             width: 100
         }, {
-        //     title: '图表',
-        //     dataIndex: 'chartSum',
-        //     key: 'chartSum',
-        //     width: 80
-        // }, {
             title: '操作',
             key: 'action',
             render: (text, record, index) => (
@@ -155,52 +304,98 @@ class DataSource extends React.Component {
             <Layout className='datasource-view'>
                 <Content>
                     <Card className='datasource-body' title={
-                        <Row className='datasource-tools' type='flex' justify='end'>
-                            <Col className='search'>
-                                <Search
-                                    value={dataSource.filterLabel}
-                                    placeholder="请输入关键字"
-                                    onChange={e => {
-                                        dispatch({ type: 'dataSource/setFilterLabel', label: e.target.value });
-                                    }}
-                                />
+                        <Row className='datasource-tools' type='flex' justify='space-between'>
+                            <Col style={{ display: 'flex' }}>
+                                <Popover overlayClassName='popover-group' title={
+                                    <Row className='grouptree-title' type='flex' justify='space-between'>
+                                        <Col>
+                                            分组管理
+                                        </Col>
+                                        <Col>
+                                            <div className='create-group' onClick={() => {
+                                                dispatch({ type: 'dataSource/remoteAddGroup' });
+                                            }}>添加分组<Icon type="plus-circle-o" /></div>
+                                        </Col>
+                                    </Row>
+                                } trigger="click" placement="bottomLeft" content={(
+                                    <Tree
+                                        className='tree-group'
+                                        showLine
+                                        defaultExpandAll
+                                        draggable
+                                        onDragStart={this.onDragStart}
+                                        onDrop={this.onDrop}
+                                    >
+                                        {
+                                            this.createGroupTree(true)
+                                        }
+                                    </Tree>
+                                )}>
+                                    <Icon type="bars" />
+                                </Popover>
+                                <Breadcrumb className='group' separator=">">
+                                    <Breadcrumb.Item>
+                                        <Dropdown
+                                            trigger={['click']}
+                                            onVisibleChange={this.handleVisibleChange}
+                                            visible={visibleGrouMenu}
+                                            overlay={(
+                                            <Menu>
+                                                {this.createGroupMenu()}
+                                            </Menu>
+                                        )}>
+                                            <Tag color={TAG_COLOR[Math.ceil(Math.random()*TAG_COLOR.length) - 1]}>{dataSource.currentGroup[0].label}</Tag>
+                                        </Dropdown>
+                                    </Breadcrumb.Item>
+                                    {dataSource.currentGroup[1] && (<Breadcrumb.Item>
+                                        <Dropdown trigger={['click']} overlay={(
+                                            <Menu>
+                                                {this.createSubGroupMenu()}
+                                            </Menu>
+                                        )}>
+                                            <Tag color={TAG_COLOR[Math.ceil(Math.random()*TAG_COLOR.length) - 1]}>{dataSource.currentGroup[1].label}</Tag>
+                                        </Dropdown>
+                                    </Breadcrumb.Item>)}
+                                </Breadcrumb>
                             </Col>
-                            <Col>
-                                <Dropdown overlay={(
-                                    <Menu onClick={(item, key, keyPath) => {
-                                        const type = item.key;
-                                        dispatch({ type: 'dataSource/resetNewModel' });
-                                        dispatch({ type: 'dataSource/setNewModelField', name: 'type', value: type });
-                                        dispatch({type: 'main/redirect', path: {pathname: '/datasource/'+ type +'/create'}});
-                                    }}>
-                                        <Menu.Item key='database'>数据库</Menu.Item>
-                                        <Menu.Item key='file'>文件</Menu.Item>
-                                    </Menu>
-                                )} trigger={['click']}>
-                                    <Button>
-                                        <Icon type="plus" />添加数据源
-                                    </Button>
-                                </Dropdown>
+                            <Col className='search'>
+                                <Col>
+                                    <Search
+                                        value={dataSource.filterLabel}
+                                        placeholder="请输入关键字"
+                                        onChange={e => {
+                                            dispatch({ type: 'dataSource/setFilterLabel', label: e.target.value });
+                                        }}
+                                    />
+                                </Col>
+                                <Col>
+                                    <Dropdown overlay={(
+                                        <Menu onClick={(item, key, keyPath) => {
+                                            const type = item.key;
+                                            dispatch({ type: 'dataSource/resetNewModel' });
+                                            dispatch({ type: 'dataSource/setNewModelField', name: 'type', value: type });
+                                            dispatch({type: 'main/redirect', path: {pathname: '/datasource/'+ type +'/create'}});
+                                        }}>
+                                            <Menu.Item key='database'>数据库</Menu.Item>
+                                            <Menu.Item key='file'>文件</Menu.Item>
+                                        </Menu>
+                                    )} trigger={['click']}>
+                                        <Button>
+                                            <Icon type="plus" />添加数据源
+                                        </Button>
+                                    </Dropdown>
+                                </Col>
                             </Col>
                         </Row>
                     }>
                         <Table
                             className='datasource-table datasource-table'
                             columns={dataSourceColumns}
-                            dataSource={dataSource.list.map(a => {
-                                let o = Object.assign({}, a);
-                                let reg = new RegExp('('+dataSource.filterLabel+'){1}', 'ig');
-                                if(o.name.search(reg) !== -1) {
-                                    return o;
-                                }else if(o.description.search(reg) !== -1) {
-                                    return o;
-                                }else {
-                                    return null
-                                }
-                            }).filter(a => a!==null).sort((a, b) => {
-                                return new Date(b.createTime) - new Date(a.createTime);
-                            })}
-                            loading={loading}
+                            dataSource={
+                                this.onSort(
+                                    this.onSearch(dataSource.list, dataSource.filterLabel)
+                                )
+                            }
                             size='small'
                             scroll={{x: false, y: true}}
                             pagination={false}

+ 79 - 1
src/components/datasource/dataSource.less

@@ -4,8 +4,25 @@
         .ant-card-head {
             padding: 0 10px;
             .datasource-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 {
-                    margin-right: 5px;
+                    display: flex;
+                    > div:first-child {
+                        margin-right: 5px;
+                    }
                 }
             }
         }
@@ -85,6 +102,62 @@
         }
     }
 }
+.popover-group {
+    width: 300px;
+    .grouptree-title {
+        .create-group {
+            cursor: pointer;
+            color: #40a9ff;
+        }
+    }
+    .ant-popover-inner-content {
+        cursor: default;
+        max-height: 60vh;
+        overflow: auto;
+    }
+    .tree-group {
+        li.drag-over {
+            input {
+                background-color: #40a9ff;
+            }
+        }
+        li {
+            .ant-tree-node-content-wrapper {
+                width: 90%;
+                height: 28px;
+                margin: 0 !important;
+                padding: 0;
+                background-color: transparent;
+
+                :hover {
+                    background-color: transparent !important;
+                }
+                .ant-tree-node-selected {
+                    background-color: transparent !important;
+                }
+                input {
+                    max-width: 180px;
+                    border: none;
+                }
+                .anticon-plus-circle-o {
+                    margin-left: 5px;
+                }
+                .anticon-minus-circle {
+                    margin-left: 5px;
+                }
+            }
+            .drag-over {
+                span[draggable] {
+                    opacity: .7;
+                }
+            }
+        }
+    }
+}
+.tree-group li.drag-over > span[draggable] {
+    opacity: .5;
+}
+
 .operationmenu {
     padding: 0;
     width: 120px;
@@ -96,6 +169,11 @@
     .ant-dropdown-menu-item-divider {
         margin: 0;
     }
+    .setgroupmenu {
+        .ant-dropdown-menu-submenu-title {
+            display: flex;
+        }
+    }
 }
 
 .ant-table-body::-webkit-scrollbar {/*滚动条整体样式*/

+ 6 - 2
src/components/datasource/dataSourceDetail.less

@@ -14,8 +14,12 @@
             }
             .ant-form {
                 .buttons {
-                    float: right;
-                    margin-top: 4px;
+                    display: flex;
+                    justify-content: space-between;
+                    margin-top: 10px;
+                    .errormessage {
+                        color: #F5222D;
+                    }
                     button {
                         margin-left: 4px;
                     }

+ 26 - 0
src/constants/url.js

@@ -50,5 +50,31 @@ const URLS = {
     CHART_LINE_OPTION: BASE_URL + '/showLine', // 请求折线图展示数据
 
     CHART_SCATTER_OPTION: BASE_URL + '/showScatter', // 请求散点图展示四数据
+
+    /***************************************分组***************************************/
+
+    /** 数据源 **/
+
+    GROUP_DATASOURCE_LIST: BASE_URL + '/getConnectorGroup', // 获得数据源所有分组/子分组
+
+    GROUP_DATASOURCE_ADD: BASE_URL + '/setConnectorGroup', // 新增数据源分组/子分组
+
+    GROUP_DATASOURCE_UPDATE: BASE_URL + '/updataDataConnectorGroup', // 修改数据源分组信息
+
+    GROUP_DATASOURCE_LIST_UPDATE: BASE_URL + '/updataConnectorGroup', // 批量修改数据源分组信息
+
+    GROUP_DATASOURCE_DELETE: BASE_URL + '/delDataConnectorGroup', // 删除数据源分组/子分组
+
+    /** 图表 **/
+
+    GROUP_CHART_LIST: BASE_URL + '/getChartsGroup', // 获得图表所有分组/子分组
+
+    GROUP_CHART_ADD: BASE_URL + '/setChartsGroup', // 新增图表分组/子分组
+
+    GROUP_CHART_UPDATE: BASE_URL + '/updataChartsGroup', // 修改图表分组信息
+
+    GROUP_CHART_LIST_UPDATE: BASE_URL + '/updataListGroup', // 批量修改图表分组信息
+
+    GROUP_CHART_DELETE: BASE_URL + '/delChartsGroup', // 删除图表分组/子分组
 }
 export default URLS

+ 490 - 2
src/models/chart.js

@@ -7,6 +7,12 @@ export default {
     state: {
         list: [],
         filterLabel: '',
+        groupList: [],
+        currentGroup: [{
+            code: 'all',
+            label: '全部分组'
+        }],
+        groupDirty: false
     },
     reducers: {
         add(state, action) {
@@ -20,7 +26,71 @@ export default {
             return Object.assign({}, state, {list: list});
         },
         setFilterLabel(state, action) {
-            return Object.assign({}, state, {filterLabel: action.label});
+            const { label } = action;
+            return Object.assign({}, state, {filterLabel: label});
+        },
+        groupList(state, action) {
+            let data = action.data;
+            return Object.assign({}, state, {groupList: data});
+        },
+        /**
+         * 设置数据源过滤用分组
+         */
+        setCurrentGroup(state, action) {
+            const { group1, group2 } = action;
+            let g = [group1];
+            group2 && g.push(group2);
+            return Object.assign({}, state, {currentGroup: g});
+        },
+        addGroup(state, action) {
+            const { group } = action;
+            let list = state.groupList;
+            list.push(group);
+            return Object.assign({}, state, {groupList: list});
+        },
+        modifyGroup(state, action) {
+            const { group } = action;
+            let list = state.groupList;
+            let dirty = false;
+            for(let i = 0; i < list.length; i++) {
+                let l = list[i];
+                if(l.code === group.code) {
+                    for(let k in l) {
+                        if(group[k] !== undefined && l[k] !== group[k]) {
+                            l[k] = group[k];
+                            dirty = true;
+                        }
+                    }
+                    break;
+                }
+            }
+            return Object.assign({}, state, {groupDirty: dirty, groupList: list});
+        },
+        modifyGroups(state, action) {
+            const { groups } = action;
+            let list = state.groupList;
+            const modifyGroupCodes = groups.map(g => g.code);
+
+            list = list.filter(l => modifyGroupCodes.indexOf(l.code) === -1);
+            list = list.concat(groups);
+
+            return Object.assign({}, state, {groupList: list});
+        },
+        deleteGroup(state, action) {
+            const { group } = action;
+            let list = state.groupList;
+            for(let i = 0; i < list.length; i++) {
+                let l = list[i];
+                if(l.code === group.code) {
+                    list.splice(i, 1);
+                    break;
+                }
+            }
+            return Object.assign({}, state, {groupList: list});
+        },
+        setGroupDirty(state, action) {
+            let dirty = action.dirty;
+            return Object.assign({}, state, {groupDirty: dirty});
         }
     },
     effects: {
@@ -137,6 +207,96 @@ export default {
                 message.error('解析图表错误');
             }
         },
+        *remoteAdd(action, { select, call, put }) {
+            try{
+                const chartDesigner = yield select(state => state.present.chartDesigner);
+                const { header, baseConfig, preparing, barConfig, pieConfig, lineConfig, otherConfig, description } = chartDesigner;
+
+                let body = {
+                    chartName: header.label,
+                    dataId: baseConfig.dataSource,
+                    groupBy: preparing.groupBy && preparing.groupBy.key ? [{
+                        columnName: preparing.groupBy.key,
+                        columnRamane: preparing.groupBy.label
+                    }] : [],
+                    createBy: 'zhuth',
+                    describes: description,
+                    style: '',
+                    otherConfig: JSON.stringify(otherConfig)
+                }; // 基本属性
+                if(baseConfig.viewType === 'bar') {
+                    body.chartType = 'Histogram';
+                    body.chartConfig = JSON.stringify(barConfig);
+                }else if(baseConfig.viewType === 'pie') {
+                    body.chartType = 'Pie';
+                    body.chartConfig = JSON.stringify(pieConfig);
+                }else if(baseConfig.viewType === 'line') {
+                    body.chartType = 'Line';
+                    body.chartConfig = JSON.stringify(lineConfig);
+                }
+                const res = yield call(service.fetch, {
+                    url: URLS.CHART_ADD,
+                    body: body
+                })
+                if(!res.err && res.data.code > 0) {
+                    message.success('新增成功!');
+                    // yield put({ type: 'silentSetField', name: 'code', value: code });
+                    yield put({ type: 'fetchList', mandatory: true });
+                }else {
+                    message.error('新增失败');
+                }
+            }catch(e) {
+                console.error(e);
+                message.error('新增失败');
+            }
+        },
+        *remoteModify(action, { select, call, put }) {
+            try{
+                const chartDesigner = yield select(state => state.present.chartDesigner);
+                const { code, header, baseConfig, pieConfig, lineConfig, preparing,
+                    barConfig, scatterConfig, otherConfig, description } = chartDesigner;
+                let body = {
+                    chartId: code,
+                    chartName: header.label,
+                    dataId: baseConfig.dataSource,
+                    groupBy: preparing.groupBy ? [{
+                        columnName: preparing.groupBy.key,
+                        columnRamane: preparing.groupBy.label
+                    }] : [],
+                    createBy: 'zhuth',
+                    describes: description,
+                    style: '',
+                    otherConfig: JSON.stringify(otherConfig)
+                }; // 基本属性
+                if(baseConfig.viewType === 'bar') {
+                    body.chartType = 'Histogram';
+                    body.chartConfig = JSON.stringify(barConfig);
+                }else if(baseConfig.viewType === 'pie') {
+                    body.chartType = 'Pie';
+                    body.chartConfig = JSON.stringify(pieConfig);
+                }else if(baseConfig.viewType === 'line') {
+                    body.chartType = 'Line';
+                    body.chartConfig = JSON.stringify(lineConfig);
+                }else if(baseConfig.viewType === 'scatter') {
+                    body.chartType = 'scatter';
+                    body.chartConfig = JSON.stringify(scatterConfig);
+                }
+                console.log(body);
+                const res = yield call(service.fetch, {
+                    url: URLS.CHART_UPDATE,
+                    body: body
+                })
+                if(!res.err && res.data.code > 0) {
+                    message.success('修改成功');
+                    yield put({ type: 'fetchList', mandatory: true });
+                }else {
+                    message.error('修改失败');
+                }
+            }catch(e) {
+                console.error(e);
+                message.error('修改失败');
+            }
+        },
         *remoteDelete(action, { select, call, put, takeEvery, takeLatest }) {
             const chart = yield select(state => state.present.chart);
             const code = action.code;
@@ -162,7 +322,332 @@ export default {
                 message.error('删除失败');
                 console.log(e);
             }
-        }
+        },
+        
+        *remoteGroupList(action, { select, call, put }) {
+            try {
+                const chart = yield select(state => state.present.chart);
+                if(!action.mandatory && chart.groupList.length > 0) {
+                    return;
+                }
+                const res = yield call(service.fetch, {
+                    url: URLS.GROUP_CHART_LIST,
+                });
+                
+                console.log(res);
+                if(!res.err && res.data.code > 0) {
+                    const resData = res.data.data;
+                    let data = resData.map(d => {
+                        return {
+                            code: d.id+'',
+                            pcode: d.fatherId+'',
+                            index: +d.groupIndex,
+                            label: d.groupName+'',
+                        }
+                    });
+                    yield put({ type: 'groupList', data });
+                }else {
+                    message.error('读取数据源列表错误');
+                }
+            }catch(e) {
+                console.log(e);
+            }
+        },
+        /**
+         * 新增分组/子分组,需要传入父节点code
+         */
+        *remoteAddGroup(action, { select, call, put }) {
+            try {
+                const chart = yield select((state) => state.present.chart);
+                const group = chart.groupList;
+                const pgroups = group.filter(g => g.pcode === '-1');
+                const cgroups = group.filter(g => g.pcode !== '-1');
+                const { pgroup } = action;
+
+                let body = {};
+                if(pgroup) {
+                    body = {
+                        fatherId: pgroup.code,
+                        groupName: '新子分组',
+                        groupIndex: cgroups.filter(c => c.pcode === pgroup.code).length,
+                        createBy: 'zhuth'
+                    }
+                }else {
+                    body = {
+                        fatherId: '-1',
+                        groupName: '新分组',
+                        groupIndex: pgroups.length,
+                        createBy: 'zhuth'
+                    }
+                }
+                const res = yield call(service.fetch, {
+                    url: URLS.GROUP_CHART_ADD,
+                    body: body
+                });
+                console.log('新增分组', body, res);
+                if(!res.err && res.data.code > 0) {
+                    let group = {
+                        code: res.data.data + '',
+                        pcode: body.fatherId + '',
+                        index: body.groupIndex,
+                        label: body.groupName+'',
+                    }
+                    yield put({ type: 'addGroup', group });
+                }else {
+                    message.error('新增失败');
+                }
+            }catch(e) {
+                console.log(e);
+                message.error('新增失败');
+            }
+        },
+        /**
+         * 修改单个分组信息(因为不涉及顺序号的修改,所以一般只用于label的修改)
+         */
+        *remoteModifyGroup(action, { select, call, put }) {
+            try {
+                const chart = yield select((state) => state.present.chart);
+                const groupDirty = chart.groupDirty;
+                const group = action.group;
+
+                if(!groupDirty) { // 如果属性无改动则取消修改请求
+                    return;
+                }
+                let body = {
+                    id: group.code,
+                    fatherId: group.pcode,
+                    groupName: group.label,
+                    groupIndex: group.index,
+                    createBy: 'zhuth'
+                }
+                const res = yield call(service.fetch, {
+                    url: URLS.GROUP_CHART_UPDATE,
+                    body: body
+                });
+                
+                if(!res.err && res.data.code > 0) {
+                    yield put({ type: 'setGroupDirty', dirty: false });
+                }else {
+                    message.error('修改失败');
+                }
+            }catch(e) {
+                console.log(e);
+                message.error('修改失败');
+            }
+        },
+        /**
+         * 批量修改多个分组信息(在移动位置时用)
+         */
+        *remoteModifyGroups(action, { select, call, put }) {
+            try {
+                const groups = action.groups;
+
+                let body = groups.map(g => {
+                    return {
+                        id: g.code,
+                        groupName: g.label,
+                        groupIndex: g.index,
+                        fatherId: g.pcode,
+                        createBy: 'zhuth'
+                    }
+                });
+                const res = yield call(service.fetch, {
+                    url: URLS.GROUP_CHART_LIST_UPDATE,
+                    body: body
+                });
+                
+                if(!res.err && res.data.code > 0) {
+                    yield put({ type: 'modifyGroups', groups: groups });
+                }else {
+                    message.error('批量更新时发生异常');
+                }
+            }catch(e) {
+                console.log(e);
+                message.error('批量更新时发生异常');
+            }
+        },
+        *remoteDeleteGroup(action, { select, call, put }) {
+            try {
+                const chart = yield select((state) => state.present.chart);
+                const groupList = chart.groupList;
+                const { group } = action;
+
+                let bgroups = groupList.filter(l => l.pcode === group.pcode);
+                bgroups.splice(group.index, 1);
+                bgroups = bgroups.map((b, i) => {
+                    return { ...b, index: i }
+                });
+
+                yield put({ type: 'remoteModifyGroups', groups: bgroups });
+
+                const res = yield call(service.fetch, {
+                    url: URLS.GROUP_CHART_DELETE,
+                    body: [group.code]
+                });
+                
+                console.log(group.code, res);
+                if(!res.err && res.data.code > 0) {
+                    yield put({ type: 'deleteGroup', group});
+                }else {
+                    message.error('删除失败');
+                }
+            }catch(e) {
+                console.log(e);
+                message.error('删除失败');
+            }
+        },
+        *remoteMoveGroup(action, { select, call, put }) {
+            try {
+                const { dragCode, dropCode, dropPosition } = action;
+                const chart = yield select((state) => state.present.chart);
+                let group = chart.groupList;
+
+                const dragGroup = group.filter(g => g.code === dragCode)[0];
+                const dropGroup = group.filter(g => g.code === dropCode)[0];
+
+                let modifyGroups = [];
+                if((dragGroup.pcode === '-1' || dropGroup.pcode === '-1') && (dragGroup.pcode !== dropGroup.pcode)) { // 跨级
+                    console.log('跨级');
+                    if(dropGroup.pcode !== '-1') { // 从父级到子级
+                        console.log('error');
+                        return; // 不允许
+                    }else { // 从子级到父级
+                        if(dragGroup.pcode === dropGroup.code) { // 不跨组
+                            if(dropPosition === -1) { // 目标前
+                                console.log('before');
+                                let dragGroups = group.filter(g => g.pcode === dragGroup.pcode).sort((a, b) => a.index - b.index);
+                                let dropGroups = group.filter(g => g.pcode === dropGroup.pcode).sort((a, b) => a.index - b.index);
+                                dragGroups.splice(dragGroup.index, 1);
+                                dragGroups = dragGroups.map((g, i) => {
+                                    return { ...g, index: i }
+                                });
+                                dropGroups.splice(dropGroup.index, 0, dragGroup);
+                                dropGroups = dropGroups.map((g, i) => {
+                                    return { ...g, index: i, pcode: dropGroup.pcode }
+                                });
+                                modifyGroups = modifyGroups.concat(dragGroups, dropGroups);
+                            }else if(dropPosition === 0) { // 目标内
+                                console.log('nothing');
+                                return; // 无变化
+                            }else if(dropPosition === 1) { // 目标后
+                                console.log('after');
+                                let dragGroups = group.filter(g => g.pcode === dragGroup.pcode).sort((a, b) => a.index - b.index);
+                                let dropGroups = group.filter(g => g.pcode === dropGroup.pcode).sort((a, b) => a.index - b.index);
+                                dragGroups.splice(dragGroup.index, 1);
+                                dragGroups = dragGroups.map((g, i) => {
+                                    return { ...g, index: i }
+                                });
+                                dropGroups.splice(dropGroup.index + 1, 0, dragGroup);
+                                dropGroups = dropGroups.map((g, i) => {
+                                    return { ...g, index: i, pcode: dropGroup.pcode }
+                                });
+                                modifyGroups = modifyGroups.concat(dragGroups, dropGroups);
+                            }
+                        }else { // 跨组
+                            let dragGroups = group.filter(g => g.pcode === dragGroup.pcode).sort((a, b) => a.index - b.index);
+                            let dropGroups = group.filter(g => g.pcode === dropGroup.pcode).sort((a, b) => a.index - b.index);
+                            let dropChildrenGroups = group.filter(g => g.pcode === dropGroup.code).sort((a, b) => a.index - b.index);
+                            
+                            dragGroups.splice(dragGroup.index, 1);
+                            dragGroups = dragGroups.map((g, i) => {
+                                return { ...g, index: i }
+                            });
+                            if(dropPosition === -1) { // 目标前
+                                console.log('before');
+                                dropGroups.splice(dropGroup.index, 0, dragGroup);
+                                dropGroups = dropGroups.map((g, i) => {
+                                    return { ...g, index: i, pcode: dropGroup.pcode }
+                                });
+                                modifyGroups = modifyGroups.concat(dragGroups, dropGroups);
+                            }else if(dropPosition === 0) { // 目标内
+                                console.log('in');
+                                dropChildrenGroups.push({
+                                    ...dragGroup,
+                                    index: dropChildrenGroups.length,
+                                    pcode: dropGroup.code
+                                });
+                                modifyGroups = modifyGroups.concat(dragGroups, dropChildrenGroups);
+                            }else if(dropPosition === 1) { // 目标后
+                                console.log('after');
+                                dropGroups.splice(dropGroup.index + 1, 0, dragGroup);
+                                dropGroups = dropGroups.map((g, i) => {
+                                    return { ...g, index: i, pcode: dropGroup.pcode }
+                                });
+                                modifyGroups = modifyGroups.concat(dragGroups, dropGroups);
+                            }
+                        }
+                    }
+                }else { // 不跨级
+                    console.log('不跨级');
+                    if(dragGroup.pcode === dropGroup.pcode) { // 不跨组
+                        console.log('不跨组');
+                        let dGroups = group.filter(g => g.pcode === dragGroup.pcode).sort((a, b) => a.index - b.index);
+                        dGroups.splice(dragGroup.index, 1, {code: 'temp', index: dragGroup.index});
+                        if(dropPosition === -1) { // 目标前
+                            console.log('before');
+                            dGroups.splice(dropGroup.index, 0, dragGroup);
+                            dGroups = dGroups.filter(g => g.code !== 'temp').map((g, i) => {
+                                return { ...g, index: i }
+                            });
+                            modifyGroups = modifyGroups.concat(dGroups);
+                        }else if(dropPosition === 0) { // 目标内
+                            console.log('in');
+                            return;
+                        }else if(dropPosition === 1) { // 目标后
+                            console.log('after');
+                            dGroups.splice(dropGroup.index + 1, 0, dragGroup);
+                            dGroups = dGroups.filter(g => g.code !== 'temp').map((g, i) => {
+                                return { ...g, index: i }
+                            });
+                            modifyGroups = modifyGroups.concat(dGroups);
+                        }
+                    }else { // 跨组
+                        console.log('跨组');
+                        let dragGroups = group.filter(g => g.pcode === dragGroup.pcode).sort((a, b) => a.index - b.index);
+                        let dropGroups = group.filter(g => g.pcode === dropGroup.pcode).sort((a, b) => a.index - b.index);
+                        
+                        dragGroups.splice(dragGroup.index, 1);
+                        dragGroups = dragGroups.map((g, i) => {
+                            return { ...g, index: i }
+                        });
+                        for(let i = 0; i < dropGroups.length; i++) {
+                            if(dropGroups[i].code === dropGroup.code) {
+                                if(dropPosition === -1) { // 目标前
+                                    console.log('before');
+                                    dropGroups.splice(i, 0, dragGroup);
+                                    dropGroups = dropGroups.map((g, i) => {
+                                        return { ...g, index: i, pcode: dropGroup.pcode }
+                                    });
+                                    modifyGroups = modifyGroups.concat(dragGroups, dropGroups);
+                                }else if(dropPosition === 0) { // 目标内
+                                    console.log('in');
+                                    return; // 不允许
+                                }else if(dropPosition === 1) { // 目标后
+                                    console.log('after');
+                                    dropGroups.splice(i + 1, 0, dragGroup);
+                                    dropGroups = dropGroups.map((g, i) => {
+                                        return { ...g, index: i, pcode: dropGroup.pcode }
+                                    });
+                                    modifyGroups = modifyGroups.concat(dragGroups, dropGroups);
+                                }
+                                break;
+                            }
+                        }
+                    }
+                }
+                console.log(dragGroup, dropGroup, modifyGroups);
+                yield put({ type: 'remoteModifyGroups', groups: modifyGroups });
+            }catch(e) {
+                console.log(e);
+                message.error('位置调整失败');
+            }
+        },
+        /**
+         * 设置图表所属分组
+         */
+        *remoteSetGroup(action, { select, call, put, takeEvery, takeLatest }) {
+            yield console.log('remoteSetGroup', action);
+        },
     },
     subscriptions: {
         setup({ dispatch, history }) {
@@ -170,6 +655,9 @@ export default {
                 if(pathname === '/chart') {
                     dispatch({ type: 'fetchList' });
                 }
+                if(pathname.startsWith('/chart')) {
+                    dispatch({ type: 'remoteGroupList' });
+                }
                 let detail = pathname.match(/chart\/(\w+)/);
                 if(detail) {
                     let code = detail[1];

+ 0 - 90
src/models/chartDesigner.js

@@ -235,96 +235,6 @@ export default {
                 message.error('新增失败');
             }
         },
-        *remoteAdd(action, { select, call, put }) {
-            try{
-                const chartDesigner = yield select(state => state.present.chartDesigner);
-                const { header, baseConfig, preparing, barConfig, pieConfig, lineConfig, otherConfig, description } = chartDesigner;
-
-                let body = {
-                    chartName: header.label,
-                    dataId: baseConfig.dataSource,
-                    groupBy: preparing.groupBy && preparing.groupBy.key ? [{
-                        columnName: preparing.groupBy.key,
-                        columnRamane: preparing.groupBy.label
-                    }] : [],
-                    createBy: 'zhuth',
-                    describes: description,
-                    style: '',
-                    otherConfig: JSON.stringify(otherConfig)
-                }; // 基本属性
-                if(baseConfig.viewType === 'bar') {
-                    body.chartType = 'Histogram';
-                    body.chartConfig = JSON.stringify(barConfig);
-                }else if(baseConfig.viewType === 'pie') {
-                    body.chartType = 'Pie';
-                    body.chartConfig = JSON.stringify(pieConfig);
-                }else if(baseConfig.viewType === 'line') {
-                    body.chartType = 'Line';
-                    body.chartConfig = JSON.stringify(lineConfig);
-                }
-                const res = yield call(service.fetch, {
-                    url: URLS.CHART_ADD,
-                    body: body
-                })
-                if(!res.err && res.data.code > 0) {
-                    message.success('新增成功!');
-                    // yield put({ type: 'silentSetField', name: 'code', value: code });
-                    yield put({ type: 'chart/fetchList', mandatory: true });
-                }else {
-                    message.error('新增失败');
-                }
-            }catch(e) {
-                console.error(e);
-                message.error('新增失败');
-            }
-        },
-        *remoteModify(action, { select, call, put }) {
-            try{
-                const chartDesigner = yield select(state => state.present.chartDesigner);
-                const { code, header, baseConfig, pieConfig, lineConfig, preparing,
-                    barConfig, scatterConfig, otherConfig, description } = chartDesigner;
-                let body = {
-                    chartId: code,
-                    chartName: header.label,
-                    dataId: baseConfig.dataSource,
-                    groupBy: preparing.groupBy ? [{
-                        columnName: preparing.groupBy.key,
-                        columnRamane: preparing.groupBy.label
-                    }] : [],
-                    createBy: 'zhuth',
-                    describes: description,
-                    style: '',
-                    otherConfig: JSON.stringify(otherConfig)
-                }; // 基本属性
-                if(baseConfig.viewType === 'bar') {
-                    body.chartType = 'Histogram';
-                    body.chartConfig = JSON.stringify(barConfig);
-                }else if(baseConfig.viewType === 'pie') {
-                    body.chartType = 'Pie';
-                    body.chartConfig = JSON.stringify(pieConfig);
-                }else if(baseConfig.viewType === 'line') {
-                    body.chartType = 'Line';
-                    body.chartConfig = JSON.stringify(lineConfig);
-                }else if(baseConfig.viewType === 'scatter') {
-                    body.chartType = 'scatter';
-                    body.chartConfig = JSON.stringify(scatterConfig);
-                }
-                console.log(body);
-                const res = yield call(service.fetch, {
-                    url: URLS.CHART_UPDATE,
-                    body: body
-                })
-                if(!res.err && res.data.code > 0) {
-                    message.success('修改成功');
-                    yield put({ type: 'chart/fetchList', mandatory: true });
-                }else {
-                    message.error('修改失败');
-                }
-            }catch(e) {
-                console.error(e);
-                message.error('修改失败');
-            }
-        },
         *remoteDataColumn(action, { select, call, put }) {
             const code = action.code;
             try {

+ 420 - 9
src/models/dataSource.js

@@ -8,7 +8,14 @@ export default {
     state: {
         newOne: {},
         list: [],
-        filterLabel: ''
+        filterLabel: '',
+        invalidSQL: false,
+        groupList: [],
+        currentGroup: [{
+            code: 'all',
+            label: '全部分组'
+        }],
+        groupDirty: false
     },
     reducers: {
         list(state, action) {
@@ -74,6 +81,75 @@ export default {
         setFilterLabel(state, action) {
             const { label } = action;
             return Object.assign({}, state, {filterLabel: label});
+        },
+        setNewModelInvalidSQL(state, action) {
+            const { value } = action;
+            let newOne = state.newOne;
+            newOne.invalidSQL = value;
+            return Object.assign({}, state, {newOne});
+        },
+        groupList(state, action) {
+            let data = action.data;
+            return Object.assign({}, state, {groupList: data});
+        },
+        /**
+         * 设置数据源过滤用分组
+         */
+        setCurrentGroup(state, action) {
+            const { group1, group2 } = action;
+            let g = [group1];
+            group2 && g.push(group2);
+            return Object.assign({}, state, {currentGroup: g});
+        },
+        addGroup(state, action) {
+            const { group } = action;
+            let list = state.groupList;
+            list.push(group);
+            return Object.assign({}, state, {groupList: list});
+        },
+        modifyGroup(state, action) {
+            const { group } = action;
+            let list = state.groupList;
+            let dirty = false;
+            for(let i = 0; i < list.length; i++) {
+                let l = list[i];
+                if(l.code === group.code) {
+                    for(let k in l) {
+                        if(group[k] !== undefined && l[k] !== group[k]) {
+                            l[k] = group[k];
+                            dirty = true;
+                        }
+                    }
+                    break;
+                }
+            }
+            return Object.assign({}, state, {groupDirty: dirty, groupList: list});
+        },
+        modifyGroups(state, action) {
+            const { groups } = action;
+            let list = state.groupList;
+            const modifyGroupCodes = groups.map(g => g.code);
+
+            list = list.filter(l => modifyGroupCodes.indexOf(l.code) === -1);
+            list = list.concat(groups);
+
+            return Object.assign({}, state, {groupList: list});
+        },
+        deleteGroup(state, action) {
+            const { group } = action;
+            let list = state.groupList;
+            for(let i = 0; i < list.length; i++) {
+                let l = list[i];
+                if(l.code === group.code) {
+                    list.splice(i, 1);
+                    break;
+                }
+            }
+            return Object.assign({}, state, {groupList: list});
+        },
+        setGroupDirty(state, action) {
+            let dirty = action.dirty;
+            return Object.assign({}, state, {groupDirty: dirty});
         }
     },
     effects: {
@@ -87,6 +163,7 @@ export default {
                     url: URLS.DATASOURCE_LIST,
                     body: {}
                 });
+                console.log('数据源list', res);
                 if(!res.err && res.data.code > 0) {
                     let data = res.data.data.map((r, i) => {
                         let dbConfig = JSON.parse(r.dbConfig);
@@ -251,15 +328,18 @@ export default {
                             description: d.remarks
                         }
                     });
+                    yield put({ type: 'setNewModelInvalidSQL', value: columns.length === 0 });
                     yield put({ type: 'setNewModelField', name: 'columns', value: columns });
                 }else {
+                    yield put({ type: 'setNewModelInvalidSQL', value: true });
                     yield put({ type: 'setNewModelField', name: 'columns', value: [] });
-                    message.error('请求列数据错误');
+                    // message.error('请求列数据错误');
                 }
             }catch(e) {
                 console.log(e);
+                yield put({ type: 'setNewModelInvalidSQL', value: true });
                 yield put({ type: 'setNewModelField', name: 'columns', value: [] });
-                message.error('请求列数据错误');
+                // message.error('请求列数据错误');
             }
         },
         *remoteDelete(action, { select, call, put }) {
@@ -291,8 +371,8 @@ export default {
         *remoteModify(action, { select, call, put }) {
             try{
                 const dataSource = yield select(state => state.present.dataSource);
-                let model = dataSource.newOne;
-                const code = action.code;
+                let model = action.model || dataSource.newOne;
+                const code = model.code;
                 let list = dataSource.list;
     
     
@@ -304,15 +384,15 @@ export default {
                     dataTag: model.tags,
                     type: model.type,
                     createBy: 'admin',
-                    dbConfig: {
+                    dbConfig: model.address ? {
                         addrass: model.address,
                         port: model.port,
                         databaseType: model.dbType,
                         dataName: model.dbName,
                         userName: model.userName,
                         passWord: model.password
-                    },
-                    columnConfig: model.columns.map((c, i) => {
+                    } : '',
+                    columnConfig: model.columns ? model.columns.map((c, i) => {
                         return {
                             columnName: c.name,
                             columnLable: c.alias,
@@ -323,12 +403,14 @@ export default {
                             isOpen: c.using?'1':'0',
                             remarks: c.description
                         }
-                    })
+                    }) : '',
+                    connectorGroup: model.groupCode
                 };
                 const res = yield call(service.fetch, {
                     url: URLS.DATASOURCE_UPDATE,
                     body: data
                 });
+                console.log('修改数据源', data, res);
                 if(!res.err && res.data.code > 0) {
                     list = list.map(l => {
                         if((l.code + '') === (action.code + '')) {
@@ -343,8 +425,334 @@ export default {
     
                 }
             }catch(e) {
+                console.log(e);
                 message.error('修改失败');
             }
+        },
+
+        *remoteGroupList(action, { select, call, put }) {
+            try {
+                const dataSource = yield select(state => state.present.dataSource);
+                if(!action.mandatory && dataSource.groupList.length > 0) {
+                    return;
+                }
+                const res = yield call(service.fetch, {
+                    url: URLS.GROUP_DATASOURCE_LIST,
+                });
+                
+                console.log(res);
+                if(!res.err && res.data.code > 0) {
+                    const resData = res.data.data;
+                    let data = resData.map(d => {
+                        return {
+                            code: d.id+'',
+                            pcode: d.fatherId+'',
+                            index: +d.groupIndex,
+                            label: d.groupName+'',
+                        }
+                    });
+                    yield put({ type: 'groupList', data });
+                }else {
+                    message.error('读取数据源列表错误');
+                }
+            }catch(e) {
+                console.log(e);
+            }
+        },
+        /**
+         * 新增分组/子分组,需要传入父节点code
+         */
+        *remoteAddGroup(action, { select, call, put }) {
+            try {
+                const dataSource = yield select((state) => state.present.dataSource);
+                const group = dataSource.groupList;
+                const pgroups = group.filter(g => g.pcode === '-1');
+                const cgroups = group.filter(g => g.pcode !== '-1');
+                const { pgroup } = action;
+
+                let body = {};
+                if(pgroup) {
+                    body = {
+                        fatherId: pgroup.code,
+                        groupName: '新子分组',
+                        groupIndex: cgroups.filter(c => c.pcode === pgroup.code).length,
+                        createBy: 'zhuth'
+                    }
+                }else {
+                    body = {
+                        fatherId: '-1',
+                        groupName: '新分组',
+                        groupIndex: pgroups.length,
+                        createBy: 'zhuth'
+                    }
+                }
+                const res = yield call(service.fetch, {
+                    url: URLS.GROUP_DATASOURCE_ADD,
+                    body: body
+                });
+                console.log('新增分组', body, res);
+                if(!res.err && res.data.code > 0) {
+                    let group = {
+                        code: res.data.data + '',
+                        pcode: body.fatherId + '',
+                        index: body.groupIndex,
+                        label: body.groupName+'',
+                    }
+                    yield put({ type: 'addGroup', group });
+                }else {
+                    message.error('新增失败');
+                }
+            }catch(e) {
+                console.log(e);
+                message.error('新增失败');
+            }
+        },
+        /**
+         * 修改单个分组信息(因为不涉及顺序号的修改,所以一般只用于label的修改)
+         */
+        *remoteModifyGroup(action, { select, call, put }) {
+            try {
+                const dataSource = yield select((state) => state.present.dataSource);
+                const groupDirty = dataSource.groupDirty;
+                const group = action.group;
+
+                if(!groupDirty) { // 如果属性无改动则取消修改请求
+                    return;
+                }
+                let body = {
+                    id: group.code,
+                    fatherId: group.pcode,
+                    groupName: group.label,
+                    groupIndex: group.index,
+                    createBy: 'zhuth'
+                }
+                const res = yield call(service.fetch, {
+                    url: URLS.GROUP_DATASOURCE_UPDATE,
+                    body: body
+                });
+                
+                if(!res.err && res.data.code > 0) {
+                    yield put({ type: 'setGroupDirty', dirty: false });
+                }else {
+                    message.error('修改失败');
+                }
+            }catch(e) {
+                console.log(e);
+                message.error('修改失败');
+            }
+        },
+        /**
+         * 批量修改多个分组信息(在移动位置时用)
+         */
+        *remoteModifyGroups(action, { select, call, put }) {
+            try {
+                const groups = action.groups;
+
+                let body = groups.map(g => {
+                    return {
+                        id: g.code,
+                        groupName: g.label,
+                        groupIndex: g.index,
+                        fatherId: g.pcode,
+                        createBy: 'zhuth'
+                    }
+                });
+                const res = yield call(service.fetch, {
+                    url: URLS.GROUP_DATASOURCE_LIST_UPDATE,
+                    body: body
+                });
+                
+                if(!res.err && res.data.code > 0) {
+                    yield put({ type: 'modifyGroups', groups: groups });
+                }else {
+                    message.error('批量更新时发生异常');
+                }
+            }catch(e) {
+                console.log(e);
+                message.error('批量更新时发生异常');
+            }
+        },
+        *remoteDeleteGroup(action, { select, call, put }) {
+            try {
+                const dataSource = yield select((state) => state.present.dataSource);
+                const groupList = dataSource.groupList;
+                const { group } = action;
+
+                let bgroups = groupList.filter(l => l.pcode === group.pcode);
+                bgroups.splice(group.index, 1);
+                bgroups = bgroups.map((b, i) => {
+                    return { ...b, index: i }
+                });
+
+                yield put({ type: 'remoteModifyGroups', groups: bgroups });
+
+                const res = yield call(service.fetch, {
+                    url: URLS.GROUP_DATASOURCE_DELETE,
+                    body: [group.code]
+                });
+                
+                console.log(group.code, res);
+                if(!res.err && res.data.code > 0) {
+                    yield put({ type: 'deleteGroup', group});
+                }else {
+                    message.error('删除失败');
+                }
+            }catch(e) {
+                console.log(e);
+                message.error('删除失败');
+            }
+        },
+        *remoteMoveGroup(action, { select, call, put }) {
+            try {
+                const { dragCode, dropCode, dropPosition } = action;
+                const dataSource = yield select((state) => state.present.dataSource);
+                let group = dataSource.groupList;
+
+                const dragGroup = group.filter(g => g.code === dragCode)[0];
+                const dropGroup = group.filter(g => g.code === dropCode)[0];
+
+                let modifyGroups = [];
+                if((dragGroup.pcode === '-1' || dropGroup.pcode === '-1') && (dragGroup.pcode !== dropGroup.pcode)) { // 跨级
+                    console.log('跨级');
+                    if(dropGroup.pcode !== '-1') { // 从父级到子级
+                        console.log('error');
+                        return; // 不允许
+                    }else { // 从子级到父级
+                        if(dragGroup.pcode === dropGroup.code) { // 不跨组
+                            if(dropPosition === -1) { // 目标前
+                                console.log('before');
+                                let dragGroups = group.filter(g => g.pcode === dragGroup.pcode).sort((a, b) => a.index - b.index);
+                                let dropGroups = group.filter(g => g.pcode === dropGroup.pcode).sort((a, b) => a.index - b.index);
+                                dragGroups.splice(dragGroup.index, 1);
+                                dragGroups = dragGroups.map((g, i) => {
+                                    return { ...g, index: i }
+                                });
+                                dropGroups.splice(dropGroup.index, 0, dragGroup);
+                                dropGroups = dropGroups.map((g, i) => {
+                                    return { ...g, index: i, pcode: dropGroup.pcode }
+                                });
+                                modifyGroups = modifyGroups.concat(dragGroups, dropGroups);
+                            }else if(dropPosition === 0) { // 目标内
+                                console.log('nothing');
+                                return; // 无变化
+                            }else if(dropPosition === 1) { // 目标后
+                                console.log('after');
+                                let dragGroups = group.filter(g => g.pcode === dragGroup.pcode).sort((a, b) => a.index - b.index);
+                                let dropGroups = group.filter(g => g.pcode === dropGroup.pcode).sort((a, b) => a.index - b.index);
+                                dragGroups.splice(dragGroup.index, 1);
+                                dragGroups = dragGroups.map((g, i) => {
+                                    return { ...g, index: i }
+                                });
+                                dropGroups.splice(dropGroup.index + 1, 0, dragGroup);
+                                dropGroups = dropGroups.map((g, i) => {
+                                    return { ...g, index: i, pcode: dropGroup.pcode }
+                                });
+                                modifyGroups = modifyGroups.concat(dragGroups, dropGroups);
+                            }
+                        }else { // 跨组
+                            let dragGroups = group.filter(g => g.pcode === dragGroup.pcode).sort((a, b) => a.index - b.index);
+                            let dropGroups = group.filter(g => g.pcode === dropGroup.pcode).sort((a, b) => a.index - b.index);
+                            let dropChildrenGroups = group.filter(g => g.pcode === dropGroup.code).sort((a, b) => a.index - b.index);
+                            
+                            dragGroups.splice(dragGroup.index, 1);
+                            dragGroups = dragGroups.map((g, i) => {
+                                return { ...g, index: i }
+                            });
+                            if(dropPosition === -1) { // 目标前
+                                console.log('before');
+                                dropGroups.splice(dropGroup.index, 0, dragGroup);
+                                dropGroups = dropGroups.map((g, i) => {
+                                    return { ...g, index: i, pcode: dropGroup.pcode }
+                                });
+                                modifyGroups = modifyGroups.concat(dragGroups, dropGroups);
+                            }else if(dropPosition === 0) { // 目标内
+                                console.log('in');
+                                dropChildrenGroups.push({
+                                    ...dragGroup,
+                                    index: dropChildrenGroups.length,
+                                    pcode: dropGroup.code
+                                });
+                                modifyGroups = modifyGroups.concat(dragGroups, dropChildrenGroups);
+                            }else if(dropPosition === 1) { // 目标后
+                                console.log('after');
+                                dropGroups.splice(dropGroup.index + 1, 0, dragGroup);
+                                dropGroups = dropGroups.map((g, i) => {
+                                    return { ...g, index: i, pcode: dropGroup.pcode }
+                                });
+                                modifyGroups = modifyGroups.concat(dragGroups, dropGroups);
+                            }
+                        }
+                    }
+                }else { // 不跨级
+                    console.log('不跨级');
+                    if(dragGroup.pcode === dropGroup.pcode) { // 不跨组
+                        console.log('不跨组');
+                        let dGroups = group.filter(g => g.pcode === dragGroup.pcode).sort((a, b) => a.index - b.index);
+                        dGroups.splice(dragGroup.index, 1, {code: 'temp', index: dragGroup.index});
+                        if(dropPosition === -1) { // 目标前
+                            console.log('before');
+                            dGroups.splice(dropGroup.index, 0, dragGroup);
+                            dGroups = dGroups.filter(g => g.code !== 'temp').map((g, i) => {
+                                return { ...g, index: i }
+                            });
+                            modifyGroups = modifyGroups.concat(dGroups);
+                        }else if(dropPosition === 0) { // 目标内
+                            console.log('in');
+                            return;
+                        }else if(dropPosition === 1) { // 目标后
+                            console.log('after');
+                            dGroups.splice(dropGroup.index + 1, 0, dragGroup);
+                            dGroups = dGroups.filter(g => g.code !== 'temp').map((g, i) => {
+                                return { ...g, index: i }
+                            });
+                            modifyGroups = modifyGroups.concat(dGroups);
+                        }
+                    }else { // 跨组
+                        console.log('跨组');
+                        let dragGroups = group.filter(g => g.pcode === dragGroup.pcode).sort((a, b) => a.index - b.index);
+                        let dropGroups = group.filter(g => g.pcode === dropGroup.pcode).sort((a, b) => a.index - b.index);
+                        
+                        dragGroups.splice(dragGroup.index, 1);
+                        dragGroups = dragGroups.map((g, i) => {
+                            return { ...g, index: i }
+                        });
+                        for(let i = 0; i < dropGroups.length; i++) {
+                            if(dropGroups[i].code === dropGroup.code) {
+                                if(dropPosition === -1) { // 目标前
+                                    console.log('before');
+                                    dropGroups.splice(i, 0, dragGroup);
+                                    dropGroups = dropGroups.map((g, i) => {
+                                        return { ...g, index: i, pcode: dropGroup.pcode }
+                                    });
+                                    modifyGroups = modifyGroups.concat(dragGroups, dropGroups);
+                                }else if(dropPosition === 0) { // 目标内
+                                    console.log('in');
+                                    return; // 不允许
+                                }else if(dropPosition === 1) { // 目标后
+                                    console.log('after');
+                                    dropGroups.splice(i + 1, 0, dragGroup);
+                                    dropGroups = dropGroups.map((g, i) => {
+                                        return { ...g, index: i, pcode: dropGroup.pcode }
+                                    });
+                                    modifyGroups = modifyGroups.concat(dragGroups, dropGroups);
+                                }
+                                break;
+                            }
+                        }
+                    }
+                }
+                console.log(dragGroup, dropGroup, modifyGroups);
+                yield put({ type: 'remoteModifyGroups', groups: modifyGroups });
+            }catch(e) {
+                console.log(e);
+                message.error('位置调整失败');
+            }
+        },
+        /**
+         * 为数据源设置所属分组
+         */
+        *remoteSetGroup(action, { select, call, put }) {
+            yield console.log('remoteSetGroup', action);
         }
     },
     subscriptions: {
@@ -353,6 +761,9 @@ export default {
                 if(pathname === '/datasource' || pathname.match(/chart\/(\w+)/)) {
                     dispatch({ type: 'fetchList' });
                 }
+                if(pathname === '/datasource') {
+                    dispatch({ type: 'remoteGroupList' });
+                }
                 let detail = pathname.match(/datasource\/(\w+)\/(\w+)/);
                 if(detail) {
                     if(pathname.match(/datasource\/(\w+)\/(\w+)\/(\w+)/)) {

+ 6 - 3
src/models/main.js

@@ -25,9 +25,12 @@ export default {
                 maxCount: 3,
             });
             return history.listen(({ pathname, query }) => {
-                let page = pathname.match(/\/(\w*)/)[1];
-                dispatch({ type: 'setPage', page });
-
+                if(pathname.endsWith('/')) { // 去掉最后的斜杠以正确匹配路由设置
+                    dispatch(routerRedux.push(pathname.substring(0, pathname.length - 1)));
+                }else {
+                    let page = pathname.match(/\/(\w*)/)[1];
+                    dispatch({ type: 'setPage', page });
+                }
             })
         }
     }

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