Browse Source

报表导出图片

zhuth 5 years ago
parent
commit
913072b534

+ 1 - 0
package.json

@@ -19,6 +19,7 @@
     "echarts": "^4.1.0",
     "echarts-for-react": "^2.0.14",
     "fetch-abort": "^1.0.2",
+    "html2canvas": "^1.0.0-rc.3",
     "jszip": "^3.2.2",
     "lodash": "^4.17.15",
     "moment": "^2.22.2",

+ 2 - 1
src/components/dashboardDesigner/content.jsx

@@ -72,7 +72,7 @@ class DashboardDesignerContent extends React.Component {
                         这里直接用main标签而不用antd组件是为了让ref能够定位到对应的dom元素
                     */}
                     <main ref={this.contentRef} className='viewlayout ant-layout-content'>
-                        {(editMode || (filters && filters.length > 0) )&& <Header>
+                        {((esMobile && filters && filters.length > 0) || (!esMobile) ) && <Header>
                             <FilterBar
                                 filters={filters}
                                 contentSize={contentSize}
@@ -85,6 +85,7 @@ class DashboardDesignerContent extends React.Component {
                                 afterRefresh={afterRefresh}
                                 dataSources={dataSources}
                                 relationColumns={relationColumns}
+                                exportable={dashboardDesigner.items.length > 0}
                             />
                         </Header>}
                         <ViewLayout isOwner={isOwner} isShareView={isShareView} isShareKeyView={isShareKeyView} isViewMode={isViewMode} contentSize={contentSize} editMode={editMode} esMobile={esMobile}/>

+ 17 - 2
src/components/dashboardDesigner/filterBar.jsx

@@ -1,6 +1,6 @@
 import React from 'react';
 import { connect } from 'dva';
-import { Tag, Icon } from 'antd';
+import { Tag, Icon, Dropdown, Menu, Divider} from 'antd';
 import Filter from '../common/filterBox/filter2';
 import Loadable from 'utils/loadable';
 
@@ -138,7 +138,7 @@ class FilterBar extends React.Component {
 
     render() {
         const { filters, contentSize, esMobile, isOwner, editMode, isShareView, isShareKeyView,
-            isViewMode, dataSources, relationColumns } = this.props;
+            isViewMode, dataSources, relationColumns, exportable, dispatch } = this.props;
         const { visibleFilterBox, filterContentHeight, filtersOpened, filterOvered } = this.state;
 
         return <div ref = {node => this.filtersRef = node} className={`filters`} style={{ height: `${filterContentHeight}px`, width: `${contentSize.width + (contentSize._scroll ? 10 : 0)}px` }}>
@@ -172,6 +172,21 @@ class FilterBar extends React.Component {
                     filtersOpened ? this.closeFilters() : this.expandFilters();
                 }}/>
             </div>}
+            {!esMobile && <Divider type="vertical" style={{ height: 'calc(100% - 16px)', margin: '10px 8px' }}/>}
+            {!esMobile && <Dropdown overlay={(
+                <Menu onClick={m => {
+                    if(m.key === 'excel') {
+                        dispatch({ type: 'dashboardDesigner/exportToExcel' });
+                    }else if(m.key === 'img') {
+                        dispatch({ type: 'dashboardDesigner/exportToImage' });
+                    }
+                }}>
+                    <Menu.Item key='excel' disabled={!exportable}>Excel</Menu.Item>
+                    <Menu.Item key='img' disabled={!exportable}>图片</Menu.Item>
+                </Menu>
+            )} trigger={['click']}>
+                <a style={{ marginRight: '12px', height: '40px' }}>导出</a>
+            </Dropdown>}
             {visibleFilterBox && <FilterBox key={Math.random()} dataSources={dataSources} relationColumns={relationColumns} filterData={filters} visibleFilterBox={visibleFilterBox} showFilterBox={this.showFilterBox} hideFilterBox={this.hideFilterBox} createFilters={this.createFilters} />}
         </div>
     }

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

@@ -112,8 +112,8 @@
                         }
                     }
                     >.right {
-                        position: fixed;
-                        right: 10px;
+                        position: relative;
+                        // right: 10px;
                         font-size: 20px;
                         .anticon {
                             cursor: pointer;

+ 0 - 6
src/components/homePage/toolbar.jsx

@@ -54,11 +54,6 @@ class Toolbar extends React.Component {
         dispatch({ type: 'home/saveCurrentTabDashboardConfig', code: home.selectedTab.code });
     }
 
-    exportToExcel = () => {
-        const { dispatch } = this.props;
-        dispatch({ type: 'dashboardDesigner/exportToExcel' });
-    }
-
     render() {
         const { home } = this.props;
         const { tabs, selectedTab, collectionDashboards, fixedDashboard } = home;
@@ -77,7 +72,6 @@ class Toolbar extends React.Component {
                         <span onClick={this.onCancelFixed}><CusIcon title="取消固定到首页" type="bi-fixed" /></span> :
                         <span onClick={this.onFixed}><CusIcon title="固定到首页" type="bi-no-fixed" /></span>
                 }</div>
-                <div className='tool'><span onClick={this.exportToExcel}><CusIcon title="导出Excel" type="bi-export-excel" /></span></div>
             </div>
             {/** 全屏展示modal */}
             {this.state.visibleFullscreenBox && <Modal

+ 63 - 8
src/models/dashboardDesigner.js

@@ -1,11 +1,12 @@
-import { message } from 'antd'
-import * as service from '../services/index'
-import parseChartOption from './parseChartOption'
-import moment from 'moment'
-import URLS from '../constants/url'
-import CHART_TYPE from './chartType.json'
-import { arrayEquals } from '../utils/baseUtils.js'
-import Exportor from '../utils/exportor'
+import { message } from 'antd';
+import * as service from '../services/index';
+import parseChartOption from './parseChartOption';
+import moment from 'moment';
+import URLS from '../constants/url';
+import CHART_TYPE from './chartType.json';
+import { arrayEquals, base64ToBlob } from '../utils/baseUtils.js';
+import Exportor from '../utils/exportor';
+import html2canvas from 'html2canvas';
 
 /**
  * 获得报表中图表的真实过滤规则
@@ -782,6 +783,60 @@ export default {
             }finally {
                 yield put({ type: 'setField', name: 'loading', value: false });
             }
+        },
+
+        *exportToImage(action, { select }) {
+            const dashboardDesigner = yield select(state => state.dashboardDesigner);
+            const { name } = dashboardDesigner;
+
+            let viewcontent = document.querySelector('.dashboard-viewcontent');
+            let reactGridLayout = viewcontent.children[0];
+            let items = reactGridLayout.children;
+            let classList = reactGridLayout.children[0].classList; // 元素样式列表
+            let classListBackup = []; // 样式备份
+
+            /*
+             * 1.因为部分样式(transition)在html2canvas的表达器中会出现渲染问题,所以需要在截图前将样式暂时移除
+             * 2.发现svg元素样式不能继承,且编辑图标理应不显示,将其暂时隐藏
+             */
+            for(let i = 0; i < classList.length; i++) {
+                classListBackup.push(classList[i]);
+            }
+
+            for(let i = 0; i< items.length;i++) {
+                classListBackup.forEach(c => {
+                    items[i].classList.remove(c);
+                    items[i].querySelector('.chart-title .tools').style.display = 'none';
+                });
+            }
+
+            yield html2canvas(reactGridLayout).then(canvas => {
+                for(let i = 0; i< items.length;i++) {
+                    classListBackup.forEach(c => {
+                        items[i].classList.add(c);
+                        items[i].querySelector('.chart-title .tools').style.display = '';
+                    });
+                }
+
+                let aLink = document.createElement('a');
+                let blob = base64ToBlob(canvas.toDataURL()); //new Blob([content]);
+
+                let evt = document.createEvent("HTMLEvents");
+                evt.initEvent("click", true, true);//initEvent 不加后两个参数在FF下会报错  事件类型,是否冒泡,是否阻止浏览器的默认行为
+                aLink.download = name;
+                aLink.href = URL.createObjectURL(blob);
+
+                // aLink.dispatchEvent(evt);
+                //aLink.click()
+                aLink.dispatchEvent(new MouseEvent('click', {bubbles: true, cancelable: true, view: window}));//兼容火狐
+            }).catch(e => {
+                for(let i = 0; i< items.length;i++) {
+                    classListBackup.forEach(c => {
+                        items[i].classList.add(c);
+                        items[i].querySelector('.chart-title .tools').style.display = '';
+                    });
+                }
+            });
         }
     },
     subscriptions: {

+ 33 - 34
src/utils/baseUtils.js

@@ -17,6 +17,11 @@ function remove(arr, val) {
  * @param {*} b 
  */
 function isEqual(a,b){
+    var classNameA,
+        classNameB,
+        propsA,
+        propsB,
+        i, j, propName;
     //如果a和b本来就全等
     if(a===b){
         //判断是否为0和-0
@@ -27,7 +32,7 @@ function isEqual(a,b){
         return a===b;
     }
     //接下来判断a和b的数据类型
-    var classNameA=Object.prototype.toString.call(a),
+    classNameA=Object.prototype.toString.call(a);
     classNameB=Object.prototype.toString.call(b);
     //如果数据类型不相等,则返回false
     if(classNameA !== classNameB){
@@ -50,35 +55,16 @@ function isEqual(a,b){
     if(classNameA === '[object Date]' || classNameA === '[object Boolean]') {
         return +a === +b;
     }
-    // switch(classNameA){
-    //     case '[object RegExp]':
-    //     case '[object String]':
-    //     //进行字符串转换比较
-    //     return '' + a ==='' + b;
-    //     case '[object Number]':
-    //     //进行数字转换比较,判断是否为NaN
-    //     if(a.toString !== 'NaN'){
-    //         return a === b;
-    //     }
-    //     //判断是否为0或-0
-    //     return +a === 0?1/ +a === 1/b : +a === +b;
-    //     case '[object Date]':
-    //     case '[object Boolean]':
-    //     return +a === +b;
-    //     default : {
-    //         return false
-    //     }
-    // }
     //如果是对象类型
     if(classNameA === '[object Object]'){
         //获取a和b的属性长度
-        var propsA = Object.getOwnPropertyNames(a),
+        propsA = Object.getOwnPropertyNames(a);
         propsB = Object.getOwnPropertyNames(b);
         if(propsA.length !== propsB.length){
             return false;
         }
-        for(var i=0;i<propsA.length;i++){
-            var propName=propsA[i];
+        for(i=0;i<propsA.length;i++){
+            propName=propsA[i];
             //如果对应属性对应值不相等,则返回false
             //if(a[propName] !== b[propName]){
             if(!isEqual(a[propName], b[propName])){
@@ -89,9 +75,8 @@ function isEqual(a,b){
     }
     //如果是数组类型
     if(classNameA === '[object Array]'){
-        let i = 0;
-        for(i; i < a.length;i++) {
-            if(!isEqual(a[i], b[i])) {
+        for(j = 0; j < a.length;j++) {
+            if(!isEqual(a[j], b[j])) {
                 return false;
             }
         }
@@ -252,12 +237,12 @@ function numberFormat(number, decimals, thousands_sep) {
 };
 
 function _scientificNotationToString(param) {
-    let strParam = String(param);
-    let flag = /e/.test(strParam)
+    var strParam = String(param);
+    var flag = /e/.test(strParam)
     if (!flag) return strParam
 
-    let positive = true; // 正数
-    let sysbol = true; // 指数符号 true: 正,false: 负
+    var positive = true; // 正数
+    var sysbol = true; // 指数符号 true: 正,false: 负
     if (/^-/.test(strParam)) {
         positive = false;
     }
@@ -265,16 +250,30 @@ function _scientificNotationToString(param) {
         sysbol = false
     }
     // 指数
-    let index = Number(strParam.match(/\d+$/)[0])
+    var index = Number(strParam.match(/\d+$/)[0])
     // 基数
-    let basis = strParam.match(/^[+-]{0,1}[\d\.]+/)[0].replace(/[+-.]/g, '');
+    var basis = strParam.match(/^[+-]{0,1}[\d\.]+/)[0].replace(/[+-.]/g, '');
 
-    let absNum = sysbol ? basis.padEnd(index + 1, 0) : basis.padStart(index + basis.length, 0).replace(/^0/, '0.');
+    var absNum = sysbol ? basis.padEnd(index + 1, 0) : basis.padStart(index + basis.length, 0).replace(/^0/, '0.');
     return (positive ? '' : '-') + absNum;
 }
 
+function base64ToBlob(code) {
+    var parts = code.split(';base64,');
+    var contentType = parts[0].split(':')[1];
+    var raw = window.atob(parts[1]);
+    var rawLength = raw.length;
+
+    var uInt8Array = new Uint8Array(rawLength);
+
+    for (var i = 0; i < rawLength; ++i) {
+      uInt8Array[i] = raw.charCodeAt(i);
+    }
+    return new Blob([uInt8Array], {type: contentType});
+}
+
 ;exports = module.exports = (function(){
     return { remove, isEqual, getUrlParam, hashcode, delay, dateFormat, arrayToTree, arrayEquals, deepAssign,
-        HexToRGB, RGBToHex, numberFormat
+        HexToRGB, RGBToHex, numberFormat, base64ToBlob
     }
 })();