Browse Source

报表分享功能

zhuth 6 years ago
parent
commit
5e36888e7c

+ 1 - 0
package.json

@@ -14,6 +14,7 @@
     "app": "^0.1.0",
     "braft-editor": "^1.9.8",
     "canvas2image": "^1.0.5",
+    "copy-to-clipboard": "^3.1.0",
     "dva": "^2.3.1",
     "dva-loading": "^2.0.3",
     "echarts": "^4.1.0",

+ 2 - 4
src/components/common/filterBox/filterBox.jsx

@@ -141,9 +141,7 @@ class FilterBox extends React.Component {
      * 通过列名从数据列中获得其类型
      */
     getFilterType = (name) => {
-        let {
-            columns
-        } = this.state, i = 0, type;
+        let { columns } = this.state, i = 0, type;
         for (i; i < columns.length; i++) {
             let column = columns[i];
             if (column.name === name) {
@@ -182,9 +180,9 @@ class FilterBox extends React.Component {
                 };
                 service.fetch({
                     url: type === 'chart' ? URLS.CHART_QUERY_COLUMNDATA : URLS.DATASOURCE_QUERY_COLUMNDATA,
+                    allow: true,
                     body: body,
                 }).then(r => {
-                    console.log('获得下拉数据', body, r);
                     if(!r.err && r.data.code > 0) {
                         return r;
                     }else {

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

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

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

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

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

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

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

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

+ 0 - 0
src/components/dashboard/dashboardShareView.jsx → src/components/dashboard/shareView.jsx


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

@@ -76,6 +76,14 @@ class ConfigSider extends React.Component {
             </Button>
             {visibleCusFilterBox && <CusFilterBox visibleBox={visibleCusFilterBox} hideBox={this.hideCusFilterBox} />}
             <Divider>其他设置</Divider>
+            <FormItem label='说明'>
+                <Input
+                    value={dashboardDesigner.description}
+                    onChange={(e) => {
+                        dispatch({ type: 'dashboardDesigner/setField', name: 'description', value: e.target.value });
+                    }}
+                />
+            </FormItem>
             <FormItem label='分享码'>
                 <Input
                     value={dashboardDesigner.shareCode}

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

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

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

@@ -17,11 +17,11 @@ class DashboardDesigner extends React.Component {
     }
 
     componentDidMount() {
-        const { dispatch, isShareView } = this.props;
+        const { dispatch, isShareView, isShareKeyView } = this.props;
         const { code } = this.props.match.params;
         let url;
         if (code !== 'create') {
-            url = isShareView ? 'dashboard/remoteShareDetail' : 'dashboard/remoteDetail';
+            url = isShareView ? 'dashboard/remoteShareDetail' : ( isShareKeyView ? 'dashboard/remoteShareKeyDetail' : 'dashboard/remoteDetail');
         }
         dispatch({ type: url, code: code });
     }

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

@@ -105,7 +105,7 @@ class ViewLayout extends React.PureComponent {
                 {(children.length === 0) ? <div key='default-chartview' className='default-chartview' data-grid={{ x: 0, y: 0, w: 12, h: 2, minW: 12, maxW: 12, minH: 2, maxH: 2, static: true }}>
                     <div className='tip'>
                         <Icon type="message" theme="outlined" />
-                        {(isOwner && !isShareView) ? <span>请从左侧【图表添加】栏目中选择图表添加到看板</span> : <span>无图表元素</span>}
+                        {(isOwner && !isShareView) ? <span>请从左侧选择图表/富文本添加到看板</span> : <span>无图表元素</span>}
                     </div>
                 </div> : children}
             </ReactGridLayout>

+ 7 - 4
src/components/dataSource/list.jsx

@@ -265,13 +265,13 @@ class DataSource extends React.Component {
                 >
                     <Icon type="file-add" />创建图表
                 </Menu.Item>
-                {selectedRecord && currentUser.code === selectedRecord.creatorCode && <Menu.Item onClick={(e) => {
+                {/* {selectedRecord && currentUser.code === selectedRecord.creatorCode && <Menu.Item onClick={(e) => {
                         dispatch({ type: 'dataSource/resetNewModel' });
                         let selectedModel = dataSource.list.find((i) => { return i.code === selectedRecord.code })
                         dispatch({type: 'main/redirect', path: {pathname: '/datasource/'+ selectedModel.type +'/' + selectedModel.code + '/base'}});
                     }}>
                     <Icon type="info-circle-o" />属性设置
-                </Menu.Item>}
+                </Menu.Item>} */}
                 <Menu.Item onClick={() => {
                     this.setState({
                         visibleDataPreviewBox: true
@@ -310,7 +310,10 @@ class DataSource extends React.Component {
                         {record.type === 'database' ? <Icon type="database" theme="outlined" /> : <Icon type="file-excel" theme="outlined" />}
                     </div>
                     <div>
-                        <span>
+                        <span style={{ color: '#1890ff', cursor: 'pointer' }} onClick={() => {
+                            dispatch({ type: 'dataSource/resetNewModel' });
+                            dispatch({type: 'main/redirect', path: {pathname: '/datasource/'+ record.type +'/' + record.code + '/base'}});
+                        }}>
                             { filterLabel ?
                                 ((text || '').split(new RegExp(`(${filterLabel})`, 'i')).map((fragment, i) => {
                                     return (
@@ -450,7 +453,7 @@ class DataSource extends React.Component {
                         </Row>
                     }>
                         <Table
-                            className='datasource-table datasource-table'
+                            className='datasource-table'
                             columns={dataSourceColumns}
                             dataSource={
                                 this.onSort(

+ 5 - 1
src/constants/url.js

@@ -168,7 +168,11 @@ const URLS = {
 
     DASHBOARD_TRANSFER: BASE_URL + '/changeDashOrder', // 看板移交
 
-    DASHBOARD_SHARE_DETAIL: BASE_URL + '/XXXX', // 获得看板分享数据
+    DASHBOARD_GET_SHAREKEY: BASE_URL + '/share', // 生成看板分享链接
+    
+    DASHBOARD_SHARE_DETAIL_BY_KEY: BASE_URL + '/getSharedDashboard', // 通过看板分享链接获取看板数据
+
+    DASHBOARD_SHARE_DETAIL_BY_CODE: BASE_URL + '/getDashboardByCode', // 通过看板编号获得看板数据
 
     /***************************************浏览记录***************************************/
     

+ 114 - 19
src/models/dashboard.js

@@ -61,11 +61,10 @@ export default {
                             name: d.bdName,
                             items: items,
                             description: d.bdNote || '',
-                            thumbnail: d.thumbnail,
                             creatorCode: d.createId + '',
                             creatorName: d.createBy,
                             createTime: d.createDate,
-                            shareCode: d.shareCode,
+                            shareCode: d.bdCode,
                             demo: d.demo
                         }
                     })
@@ -97,7 +96,7 @@ export default {
                     const resData = res.data.data;
                     let items = resData.bdConfiguration ? JSON.parse(resData.bdConfiguration) : [];
                     let relationColumns = resData.relationColumns ? JSON.parse(resData.relationColumns) : [];
-                    let chartCodes = resData.chartCodes ? resData.chartCodes.split(',') : [];
+                    let chartCodes = resData.chartIds ? resData.chartIds.split(',') : [];
                     const main = yield select(state => state.present.main);
                     const { currentUser } = main;
 
@@ -124,7 +123,6 @@ export default {
                         name: resData.bdName,
                         items: items,
                         description: resData.bdNote || '',
-                        thumbnail: resData.thumbnail,
                         creatorCode: resData.createId + '',
                         creatorName: resData.createBy,
                         createTime: resData.createDate,
@@ -132,7 +130,7 @@ export default {
                         relationColumns: relationColumns,
                         editMode: currentUser.code === resData.createId + '',
                         filters: JSON.parse((resData.filters|| "[]")),
-                        shareCode: resData.shareCode,
+                        shareCode: resData.bdCode,
                         chartCodes: chartCodes,
                         demo: resData.demo
                     }
@@ -158,15 +156,14 @@ export default {
         *remoteAdd(action, { select, call, put }) {
             try {
                 const dashboardDesigner = yield select(state => state.present.dashboardDesigner);
-                const { name, items, thumbnail, relationColumns, filters, shareCode } = dashboardDesigner;
+                const { name, items, relationColumns, filters, shareCode } = dashboardDesigner;
                 let body = {
                     bdName: name,
                     bdNote: '',
                     bdConfiguration: JSON.stringify(items),
-                    thumbnail: thumbnail,
                     relationColumns: JSON.stringify(relationColumns),
                     filters: JSON.stringify(filters) || '',
-                    shareCode: shareCode,
+                    bdCode: shareCode,
                     chartCodes: []
                 }
                 console.log('新增看板', body);
@@ -189,15 +186,14 @@ export default {
         *remoteQucikAdd(action, { select, call, put }) {
             try {
                 const dashboardDesigner = yield select(state => state.present.dashboardDesigner);
-                const { name, items, thumbnail, description, relationColumns, filters, shareCode } = dashboardDesigner;
+                const { name, items, description, relationColumns, filters, shareCode } = dashboardDesigner;
                 let body = {
                     bdName: name,
                     bdNote: description,
                     bdConfiguration: JSON.stringify(items),
-                    thumbnail: thumbnail,
                     relationColumns: JSON.stringify(relationColumns),
                     filters: JSON.stringify(filters) || "",
-                    shareCode: shareCode,
+                    bdCode: shareCode,
                     chartCodes: []
                 }
                 console.log('快速新增看板', body);
@@ -220,17 +216,16 @@ export default {
         *remoteModify(action, { select, call, put }) {
             try {
                 const dashboardDesigner = yield select(state => state.present.dashboardDesigner);
-                const { code, name, items, thumbnail, description, relationColumns, filters, chartCodes, shareCode } = dashboardDesigner;
+                const { code, name, items, description, relationColumns, filters, chartCodes, shareCode } = dashboardDesigner;
                 let body = {
                     id: code,
                     bdName: name,
                     bdNote: description,
                     bdConfiguration: JSON.stringify(items.map(item => ({ ...item, chartOption: null }))),
-                    thumbnail: thumbnail,
                     relationColumns: JSON.stringify(relationColumns),
                     filters: JSON.stringify(filters) || "",
-                    chartCodes: chartCodes.join(','),
-                    shareCode: shareCode
+                    chartIds: chartCodes.join(','),
+                    bdCode: shareCode
                 }
                 const res = yield call(service.fetch, {
                     url: URLS.DASHBOARD_UPDATE,
@@ -352,10 +347,111 @@ export default {
             try {
                 yield put({ type: 'dashboardDesigner/silentSetField', name: 'loading', value: true });
                 const res = yield call(service.fetch, {
-                    url: URLS.DASHBOARD_SHARE_DETAIL,
+                    url: URLS.DASHBOARD_SHARE_DETAIL_BY_CODE,
                     method: 'GET',
+                    allow: true,
                     body: {
-                        id: code
+                        code: code
+                    }
+                });
+                if(!res.err && res.data.code > 0) {
+                    const resData = res.data.data;
+                    let items = resData.bdConfiguration ? JSON.parse(resData.bdConfiguration) : [];
+                    let relationColumns = resData.relationColumns ? JSON.parse(resData.relationColumns) : [];
+                    let chartCodes = resData.chartCodes ? resData.chartCodes.split(',') : [];
+                    const main = yield select(state => state.present.main);
+                    const { currentUser } = main;
+
+                    const allDataSources = items.map(item => {
+                        if(item.viewType === 'chart') {
+                            return {
+                                code: item.dataSourceCode,
+                                name: item.dataSourceName
+                            }
+                        }else {
+                            return null
+                        }
+                    }).filter(item => !!item);
+
+                    const dataSources = [];
+                    allDataSources.forEach(ad => {
+                        if(!dataSources.find(d => d.code === ad.code)) {
+                            dataSources.push(ad);
+                        }
+                    });
+
+                    let data = {
+                        code:  resData.id+'',
+                        name: resData.bdName,
+                        items: items,
+                        description: resData.bdNote || '',
+                        creatorCode: resData.createId + '',
+                        creatorName: resData.createBy,
+                        createTime: resData.createDate,
+                        dataSources: dataSources,
+                        relationColumns: relationColumns,
+                        editMode: currentUser.code === resData.createId + '',
+                        filters: JSON.parse((resData.filters|| "[]")),
+                        shareCode: resData.bdCode,
+                        chartCodes: chartCodes,
+                        demo: resData.demo
+                    }
+
+                    let fields = [];
+                    for(let key in data) {
+                        fields.push({
+                            name: key,
+                            value: data[key]
+                        })
+                    }
+                    yield put({ type: 'dashboardDesigner/silentSetFields', fields: fields });
+                }else {
+                    message.error('解析看板错误: ' + (res.err || res.data.msg));
+                }
+            }catch(e) {
+                message.error('解析看板错误: ' + e);
+            }finally {
+                yield put({ type: 'dashboardDesigner/silentSetField', name: 'loading', value: false });
+            }
+        },
+        *getShareKey (action, { select, call, put }) {
+            const record = action.record;
+            const delay = action.delay;
+            if(!record){
+                return
+            }
+            try {
+                const res = yield call(service.fetch, {
+                    url: URLS.DASHBOARD_GET_SHAREKEY,
+                    method: 'POST',
+                    body: {
+                        id: record.code,
+                        delay: delay
+                    }
+                });
+                if(!res.err && res.data.code > 0) {
+                    const resData = res.data.data;
+                    return resData;
+                }else {
+                    message.error('生成分享链接失败: ' + (res.err || res.data.msg));
+                }
+            }catch(e) {
+                message.error('生成分享链接失败: ' + e);
+            }
+        },
+        *remoteShareKeyDetail(action, { select, call, put }) {
+            const code = action.code;
+            if(!code){
+                return
+            }
+            try {
+                yield put({ type: 'dashboardDesigner/silentSetField', name: 'loading', value: true });
+                const res = yield call(service.fetch, {
+                    url: URLS.DASHBOARD_SHARE_DETAIL_BY_KEY,
+                    method: 'GET',
+                    allow: true,
+                    body: {
+                        data: code
                     }
                 });
                 if(!res.err && res.data.code > 0) {
@@ -389,7 +485,6 @@ export default {
                         name: resData.bdName,
                         items: items,
                         description: resData.bdNote || '',
-                        thumbnail: resData.thumbnail,
                         creatorCode: resData.createId + '',
                         creatorName: resData.createBy,
                         createTime: resData.createDate,
@@ -397,7 +492,7 @@ export default {
                         relationColumns: relationColumns,
                         editMode: currentUser.code === resData.createId + '',
                         filters: JSON.parse((resData.filters|| "[]")),
-                        shareCode: resData.shareCode,
+                        shareCode: resData.bdCode,
                         chartCodes: chartCodes,
                         demo: resData.demo
                     }

+ 11 - 4
src/models/dashboardDesigner.js

@@ -380,10 +380,11 @@ export default {
         *fetchChartData(action, { put, call, select }) {
             const { item, mandatory, page, pageSize } = action;
             const dashboardDesigner = yield select(state => state.present.dashboardDesigner);
-            const { filters, relationColumns } = dashboardDesigner;
+            const { creatorCode, filters, relationColumns } = dashboardDesigner;
             const { chartCode } = item;
             const body = {
-                dashCreateId: chartCode,
+                dashboardCreatorId: creatorCode,
+                chartId: chartCode,
                 filters: getBodyFilters(getTrueFilters(item, filters, relationColumns)),
                 testPage: {
                     pageNum: page|| 1,
@@ -398,12 +399,19 @@ export default {
                 yield put({ type: 'setItemFetching', code: chartCode, fetching: true });
                 const res = yield call(service.fetch, {
                     url: URLS.CHART_OPTION,
+                    allow: true,
                     body,
                     timeout: 30000
                 });
-                console.log('看板请求图表展示数据', body, res);
                 if(!res.err && res.data.code > 0) {
                     let resData = res.data.data;
+                    if(!resData) {
+                        yield put({ type: 'setItemFields', code: chartCode, fields: [
+                            { name: 'chartType', value: '' },
+                            { name: 'chartOption', value: {} }
+                        ] });
+                        return false;
+                    }
                     const { chartType : ctype, chartConfig: cfg } = resData.chartsColumnConfig;
                     const chartType = CHART_TYPE[ctype];
                     const chartConfig = JSON.parse(cfg);
@@ -418,7 +426,6 @@ export default {
                         { name: 'chartType', value: '' },
                         { name: 'chartOption', value: {} }
                     ] });
-                    console.error(body, res.err || res.data.msg);
                     message.error('请求图表展示数据失败: ' + (res.err || res.data.msg));
                 }
             }catch(e) {

+ 1 - 0
src/models/main.js

@@ -100,6 +100,7 @@ export default {
             try {
                 const res = yield call(service.fetch, {
                     url: URLS.LOGIN,
+                    allow: true,
                     body
                 });
                 if(!res.err && res.data.code > 0) {

+ 3 - 1
src/routes/router.js

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

+ 9 - 6
src/services/index.js

@@ -1,11 +1,10 @@
 import request from '../utils/request'
-import URLS from '../constants/url'
 
 export function fetch(option) {
-    let { url, method, body, timeout } = option;
+    let { url, method, body, timeout, allow } = option;
     const token = window.sessionStorage.getItem("token");
-    // 白名单外,Token不存在时不发送请求
-    if(token || ([URLS.LOGIN, URLS.DASHBOARD_SHARE_DETAIL].indexOf(url) !== -1)) {
+    // 除非特殊许可,Token不存在时不发送请求
+    if(token || allow) {
         let opt = {
             method: method || 'POST',
         };
@@ -26,14 +25,18 @@ export function fetch(option) {
             }
         }else {
             opt.headers = {
-                token: token,
                 'Content-Type': 'application/json'
             }
-            opt.body = JSON.stringify(body)
+            opt.body = JSON.stringify(body);
+
+            if(!!token) {
+                opt.headers.token = token
+            }
         }
 
         return request(url, opt, timeout);
     }else {
+        // 使用一个空的Promise组织后续链式方法的调用
         return new Promise(() => {});
     }
 }