dashboardDesigner.js 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979
  1. import { message } from 'antd';
  2. import * as service from '../services/index';
  3. import parseChartOption from './parseChartOption';
  4. import moment from 'moment';
  5. import URLS from '../constants/url';
  6. import CHART_TYPE from './chartType.json';
  7. import { arrayEquals, base64ToBlob } from '../utils/baseUtils.js';
  8. import Exportor from '../utils/exportor';
  9. import html2canvas from 'html2canvas';
  10. /**
  11. * 获得报表中图表的真实过滤规则
  12. */
  13. function getTrueFilters(item, filters) {
  14. let trueFilters = [];
  15. filters.forEach(f => {
  16. const { type, operator, value1, value2 } = f;
  17. if(((type === 'index' || type === 'string') && !!value1) || // 因为数字类型会生成数字字符串,所以为0也是可以正常传入条件的
  18. ((type === 'scale' || type === 'time' || type === 'ordinal') && (operator === 'between' ? (!!value1 && !!value2) : (!!value1))) ||
  19. (type === 'categorical' &&
  20. (operator === 'contain' || operator === 'notContain' ?
  21. (value1 && value1.length > 0) : (!!value1)
  22. )
  23. )) {
  24. if(f.operator === 'betweent' ? ( !!f.value1 && (f.value1.length ? f.value1.length > 0 : true) && !!f.value2 && (f.value2.length ? f.value2.length > 0 : true)) : (!!f.value1 && (f.value1.length ? f.value1.length > 0 : true))) {
  25. if(f.combined) {
  26. f.dataSource && f.dataSource.forEach(d => {
  27. if(d.dataSource.code === item.dataSourceCode) {
  28. trueFilters.push({
  29. dataSourceCode: d.dataSource.code,
  30. name: d.column.name,
  31. operator: f.operator,
  32. type: f.type,
  33. value1: f.value1,
  34. value2: f.value2,
  35. using: f.using
  36. });
  37. }
  38. });
  39. }else {
  40. if(f.dataSource.code === item.dataSourceCode) {
  41. trueFilters.push({
  42. dataSourceCode: f.dataSource.code,
  43. name: f.name,
  44. operator: f.operator,
  45. type: f.type,
  46. value1: f.value1,
  47. value2: f.value2,
  48. using: f.using
  49. });
  50. }
  51. }
  52. }
  53. }
  54. });
  55. return trueFilters;
  56. }
  57. function getBodyFilters(filters) {
  58. return filters.filter(f => f.using).map(f => {
  59. let { dataSourceCode, name, operator, type, value1, value2 } = f;
  60. let bodyFilter = {
  61. dataSourceCode,
  62. columnName: name,
  63. columnType: type,
  64. symbol: operator,
  65. value: value1
  66. };
  67. if(type === 'scale' && operator === 'between') {
  68. bodyFilter['value'] = value1 + ',' + value2;
  69. }else if(type === 'time') {
  70. let v1 = value1.dynamic ? value1.name : moment(value1).format('YYYY-MM-DD');
  71. let v2 = value2.dynamic ? value2.name : moment(value2).format('YYYY-MM-DD');
  72. if(operator === 'between') {
  73. bodyFilter['value'] = v1 + ',' + v2;
  74. }else {
  75. bodyFilter['value'] = v1;
  76. }
  77. }else if(type === 'categorical' && (operator === 'contain' || operator === 'notContain')) {
  78. bodyFilter['value'] = JSON.stringify(value1);
  79. }
  80. return bodyFilter;
  81. });
  82. }
  83. function beforeItemExportImage(itemEl) {
  84. let classListBackup = []; // 样式表备份
  85. let tableStyle = {}; // table样式备份
  86. /*
  87. * a.因为部分样式(transition)在html2canvas的表达器中会出现渲染问题,所以需要在截图前将样式暂时移除
  88. * b.发现svg元素样式不能继承,且编辑图标理应不显示,将其暂时隐藏
  89. * c.隐藏工具栏按钮
  90. * d. 处理表格中因为动画、svg造成导出图片样式错误问题
  91. */
  92. for(let i = 0; i < itemEl.classList.length; i++) {
  93. classListBackup.push(itemEl.classList[i]);
  94. }
  95. // a
  96. classListBackup.forEach(c => {
  97. itemEl.classList.remove(c);
  98. });
  99. // b
  100. itemEl.querySelector('.chart-title .tools').style.display = 'none';
  101. // c
  102. itemEl.querySelector('.chart-tools').style.display = 'none';
  103. // d
  104. let tableEl = itemEl.querySelector('.table-view');
  105. if(tableEl) {
  106. tableEl.querySelector('.ant-table-header').style.marginRight = '0';
  107. let svgs = tableEl.querySelectorAll('svg');
  108. let trs = tableEl.querySelectorAll('tr');
  109. let tds = tableEl.querySelectorAll('td');
  110. let paginationActiveItem = tableEl.querySelectorAll('.ant-pagination-item-active')[0];
  111. let paginationActiveItemText = paginationActiveItem.children[0];
  112. for(let x = 0; x < svgs.length; x++) {
  113. if(x === 1) {
  114. // 第二个svg是鼠标放上去才显示的,不需要处理
  115. continue;
  116. }
  117. svgs[x].setAttribute('width', '12px');
  118. svgs[x].setAttribute('height', '12px');
  119. svgs[x].setAttribute('fill', '#ccc');
  120. }
  121. for(let l = 0; l < trs.length; l++) {
  122. trs[l].style['transition'] = 'all 0s';
  123. trs[l].style['-webkit-transition'] = 'all 0s';
  124. }
  125. for(let m = 0; m < tds.length; m++) {
  126. tds[m].style['transition'] = 'all 0s';
  127. tds[m].style['-webkit-transition'] = 'all 0s';
  128. }
  129. paginationActiveItem.style.width = paginationActiveItem.getBoundingClientRect().width + 'px';
  130. paginationActiveItemText.style.position = 'absolute';
  131. tableEl.querySelector('.ant-table-body').getAttribute('style').split(';').forEach(x => {
  132. if(x) {
  133. let arr = x.split(':');
  134. tableStyle[arr[0]] = arr[1];
  135. }
  136. });
  137. let str = '';
  138. for(let k in tableStyle) {
  139. if(k === 'overflow') {
  140. str += k + ':hidden !important;';
  141. }else {
  142. str += k + ':' + tableStyle[k] + ';';
  143. }
  144. }
  145. tableEl.querySelector('.ant-table-body').setAttribute('style', str);
  146. }
  147. return { classListBackup, tableStyle }
  148. }
  149. function afterItemExportImage(itemEl, classListBackup, tableStyle) {
  150. classListBackup.forEach(c => {
  151. itemEl.classList.add(c);
  152. });
  153. itemEl.querySelector('.chart-title .tools').style.display = '';
  154. itemEl.querySelector('.chart-tools').style.display = '';
  155. let tableEl = itemEl.querySelector('.table-view');
  156. if(tableEl) {
  157. tableEl.querySelector('.ant-table-header').style.marginRight = '';
  158. let svgs = tableEl.querySelectorAll('svg');
  159. let trs = tableEl.querySelectorAll('tr');
  160. let tds = tableEl.querySelectorAll('td');
  161. let paginationActiveItem = tableEl.querySelectorAll('.ant-pagination-item-active')[0];
  162. let paginationActiveItemText = paginationActiveItem.children[0];
  163. for(let l = 0; l < trs.length; l++) {
  164. trs[l].style['transition'] = '';
  165. trs[l].style['-webkit-transition'] = '';
  166. }
  167. for(let m = 0; m < tds.length; m++) {
  168. tds[m].style['transition'] = '';
  169. tds[m].style['-webkit-transition'] = '';
  170. }
  171. for(let x = 0; x < svgs.length; x++) {
  172. if(x === 1) {
  173. continue;
  174. }
  175. svgs[x].setAttribute('width', '1em');
  176. svgs[x].setAttribute('height', '1em');
  177. svgs[x].setAttribute('fill', 'currentColor');
  178. }
  179. paginationActiveItem.style.width = '';
  180. paginationActiveItemText.style.position = '';
  181. let str = '';
  182. for(let k in tableStyle) {
  183. str += k + ':' + tableStyle[k] + ';';
  184. }
  185. tableEl.querySelector('.ant-table-body').setAttribute('style', str);
  186. }
  187. }
  188. const _maxLayoutW = 12;
  189. export default {
  190. namespace: 'dashboardDesigner',
  191. state: {
  192. originData: {
  193. code: null,
  194. name: '无标题',
  195. theme: 'default',
  196. minLayoutHeight: 40, // 元素最小高度
  197. maxLayoutW: _maxLayoutW,
  198. layoutMargin: [8, 8], // 元素margin
  199. defaultLayout: { x: 0, y: 50, w: _maxLayoutW, h: 6, minW: 2, maxW: _maxLayoutW, minH: 1 },
  200. items: [],
  201. chartCodes: [], // 报表包含的所有图表
  202. description: '',
  203. thumbnail: '',
  204. dirty: false,
  205. editMode: false,
  206. filterColumns: [],
  207. filters: [],
  208. dataSources: [], // 图表关联的所有数据源
  209. relationColumns: [], // 自定义的列
  210. columnFetching: false,
  211. loading: false,
  212. shareCode: '', // 分享码
  213. demo: false,
  214. filterItems: [ // 可选过滤字段(选择图表时用)
  215. { name: 'name', label: '图表名称', type: 'string' },
  216. { name: 'description', label: '说明', type: 'string' },
  217. { name: 'creatorName', label: '创建人', type: 'string' },
  218. { name: 'createTime', label: '创建时间', type: 'date' },
  219. ],
  220. filterItem: { name: 'name', label: '图表名称', type: 'string' }, // 已选过滤字段(选择图表时用)
  221. styleConfig: {
  222. aggregateTable: { direction: ['vertical', 'horizontal'][0] },
  223. },
  224. },
  225. },
  226. reducers: {
  227. silentSetField(state, action) {
  228. const { name, value } = action;
  229. let obj = {};
  230. obj[name] = value;
  231. return Object.assign({}, state, obj);
  232. },
  233. setField(state, action) {
  234. const { name, value } = action;
  235. let obj = {};
  236. obj[name] = value;
  237. let newState = Object.assign({}, state, obj);
  238. return Object.assign({}, newState, {dirty: true});
  239. },
  240. silentSetFields(state, action) {
  241. const { fields } = action;
  242. let obj = {};
  243. fields.map(f => (obj[f.name] = f.value));
  244. let newState = Object.assign({}, state, obj);
  245. return newState;
  246. },
  247. setFields(state, action) {
  248. const { fields } = action;
  249. let obj = {};
  250. fields.map(f => (obj[f.name] = f.value));
  251. let newState = Object.assign({}, state, obj);
  252. return Object.assign({}, newState, {dirty: true});
  253. },
  254. setFilterItem(state, action) {
  255. const { item } = action;
  256. return Object.assign({}, state, {filterItem: item, filterLabel: ''});
  257. },
  258. setFilterLabel(state, action) {
  259. const { label } = action;
  260. return Object.assign({}, state, {filterLabel: label});
  261. },
  262. addChart(state, action) {
  263. let { items, dataSources, chartCodes, defaultLayout } = state;
  264. const { chart } = action;
  265. items = items.concat([{
  266. code: chart.code,
  267. chartCode: chart.code,
  268. name: chart.name,
  269. creatorCode: chart.creatorCode,
  270. creatorName: chart.creatorName,
  271. dataSourceCode: chart.dataSourceCode+'',
  272. dataSourceName: chart.dataSourceName,
  273. dataConnectCode: chart.dataConnectCode,
  274. dataConnectName: chart.dataConnectName,
  275. viewType: 'chart',
  276. chartType: chart.type,
  277. filters: chart.filters,
  278. layout: { ...defaultLayout },
  279. theme: chart.theme,
  280. styleConfig: chart.styleConfig,
  281. chartOption: chart.type === 'dataView' ? {
  282. total: 0,
  283. page: 1,
  284. pageSize: 0, // 设为0以保证在tableView组件渲染时getTableLayout方法中总能触发取数
  285. columns: [],
  286. dataSource: []
  287. } : null
  288. }]);
  289. chartCodes.push(chart.code);
  290. dataSources.findIndex(d => d.code === chart.dataSourceCode+'') === -1 && dataSources.push({
  291. code: chart.dataSourceCode+'',
  292. name: chart.dataSourceName
  293. });
  294. return Object.assign({}, state, {items, chartCodes, dataSources, dirty: true});
  295. },
  296. deleteItem(state, action) {
  297. let { items, chartCodes, dataSources, relationColumns, dirty, filters } = state;
  298. const { item } = action;
  299. let targetDataSourceCode = item.dataSourceCode;
  300. let count = 0;
  301. let targetIndex = -1;
  302. for(let i = 0; i < items.length; i++) {
  303. let tempItem = items[i];
  304. if(tempItem.code === item.code) {
  305. targetIndex = i;
  306. break;
  307. }
  308. }
  309. for(let i = 0; i < items.length; i++) {
  310. if(targetDataSourceCode && items[i].dataSourceCode === targetDataSourceCode) {
  311. count++;
  312. }
  313. }
  314. // 如果删除图表的数据源是同类数据源的最后一个
  315. if(count === 1) {
  316. let idx = dataSources.findIndex(d => d.code === item.dataSourceCode);
  317. let idx2 = chartCodes.findIndex(c => c === item.chartCode);
  318. dataSources.splice(idx, 1);
  319. chartCodes.splice(idx2, 1);
  320. // 删除已定义的关联字段
  321. relationColumns.forEach(rc => {
  322. rc.relations.forEach((r, x) => {
  323. if(r.dataSourceCode === item.dataSourceCode) {
  324. rc.relations.splice(x, 1);
  325. }
  326. })
  327. });
  328. for(let i = relationColumns.length - 1; i >= 0; i--) {
  329. let r = relationColumns[i];
  330. let l = r.relations;
  331. // 自定义关联条件关联了一个以上数据源,则只删除与该数据源相等的关联数据源列
  332. for(let j = l.length - 1; j >= 0; j--) {
  333. if(l[j].dataSource.code === targetDataSourceCode) {
  334. l.splice(j, 1);
  335. }
  336. }
  337. if(l.length === 0) {
  338. // 自定义关联条件只关联了这一个数据源则直接删除该自定义关联条件
  339. relationColumns.splice(i, 1);
  340. }
  341. }
  342. // 删除过滤条件
  343. for(let i = filters.length - 1; i >= 0; i--) {
  344. let f = filters[i];
  345. if((f.combined && (f.dataSource.length === 1 && f.dataSource[0].dataSource.code === targetDataSourceCode || f.dataSource.length === 0)) ||
  346. (!f.combined && f.dataSource.code === targetDataSourceCode)) {
  347. filters.splice(i, 1);
  348. }
  349. }
  350. }
  351. if(targetIndex !== -1) {
  352. items.splice(targetIndex, 1);
  353. }
  354. return { ...state, dirty: dirty, items, dataSources, relationColumns, filters };
  355. },
  356. addRichText(state, action) {
  357. let { items, defaultLayout } = state;
  358. items.push({
  359. code: Math.random() + '',
  360. viewType: 'richText',
  361. name: '',
  362. layout: JSON.parse(JSON.stringify(defaultLayout)) // 深拷贝
  363. });
  364. return Object.assign({}, state, {items, dirty: true});
  365. },
  366. changeLayout(state, action) {
  367. const { layout } = action;
  368. let { items, dirty, minLayoutHeight } = state;
  369. const ly = ['x', 'y', 'w', 'h'];
  370. for(let i = 0; i < items.length; i++) {
  371. if(layout[i]) { // 非删除引起
  372. for(let j = 0; j < ly.length; j ++) {
  373. if(items[i].layout[ly[j]] !== layout[i][ly[j]]) {
  374. dirty = true;
  375. items[i].layout[ly[j]] = layout[i][ly[j]];
  376. }
  377. }
  378. if(items[i].chartOption) {
  379. items[i].chartOption = { ...items[i].chartOption,
  380. page: 1,
  381. pageSize: ~~((layout[i][ly[3]] * minLayoutHeight + (layout[i][ly[3]] - 1) * 12 - 20 - 40 - 24 - 8 * 2)/38) + 1
  382. }
  383. }
  384. }else { // 删除引起
  385. dirty = true;
  386. }
  387. }
  388. return Object.assign({}, state, {items, dirty});
  389. },
  390. modifyItem(state, action) {
  391. let { item } = action;
  392. let { items } = state;
  393. let dirty = false;
  394. let idx = items.findIndex(i => i.code === item.code);
  395. if(idx > -1) {
  396. dirty = true;
  397. items[idx] = { ...items[idx], ...item };
  398. }
  399. return Object.assign({}, state, {items, dirty});
  400. },
  401. reset(state, action) {
  402. let newState = Object.assign({}, state, state.originData);
  403. return Object.assign({}, newState);
  404. },
  405. setEditMode(state, action) {
  406. const { checked } = action;
  407. return { ...state, editMode: checked };
  408. },
  409. addRelationColumn(state, action) {
  410. const { relationColumns } = state;
  411. relationColumns.push({
  412. code: Math.random()+'',
  413. name: '新字段',
  414. relations: []
  415. });
  416. return { ...state, relationColumns, dirty: true };
  417. },
  418. deleteRelationColumn(state, action) {
  419. const { code } = action;
  420. const { relationColumns } = state;
  421. let index = relationColumns.findIndex(r => r.code === code);
  422. relationColumns.splice(index, 1);
  423. return { ...state, relationColumns, dirty: true };
  424. },
  425. setRelationColumn(state, action) {
  426. const { code, relationColumn } = action
  427. const { relationColumns } = state;
  428. let index = relationColumns.findIndex(r => r.code === code);
  429. relationColumns[index] = relationColumn;
  430. return { ...state, relationColumns, dirty: true };
  431. },
  432. setItemFetching(state, action) {
  433. const { code, fetching } = action
  434. const { items } = state;
  435. let index = items.findIndex(item => item.code === code);
  436. items[index] = {
  437. ...items[index],
  438. fetching
  439. };
  440. return { ...state, items };
  441. },
  442. setItemField(state, action) {
  443. const { code, name, value } = action;
  444. const { items } = state;
  445. let index = items.findIndex(item => item.code === code);
  446. const targetItem = items[index];
  447. targetItem[name] = value;
  448. items[index] = {
  449. ...targetItem
  450. };
  451. return { ...state, items };
  452. },
  453. setItemFields(state, action) {
  454. const { code, fields } = action;
  455. const { items } = state;
  456. let index = items.findIndex(item => item.code === code);
  457. if(index !== -1) {
  458. const targetItem = items[index];
  459. fields.forEach(field => {
  460. targetItem[field.name] = field.value;
  461. });
  462. items[index] = {
  463. ...targetItem
  464. };
  465. }
  466. return { ...state, items };
  467. },
  468. },
  469. effects: {
  470. *addCharts(action, { call, put, select }) {
  471. const { charts } = action;
  472. try{
  473. for(let i = 0; i < charts.length; i++) {
  474. yield put({ type: 'addChart', chart: charts[i] });
  475. }
  476. }catch(e) {
  477. message.error('添加图表错误: ' + e.message);
  478. }
  479. },
  480. /**
  481. * 刷新报表
  482. */
  483. *refresh(action, { call, put, select }) {
  484. const dashboardDesigner = yield select(state => state.dashboardDesigner);
  485. const { items, minLayoutHeight } = dashboardDesigner;
  486. for(let i = 0; i < items.length; i++) {
  487. let item = items[i];
  488. if(item.viewType === 'chart') {
  489. let page = item.chartOption ? item.chartOption.page : 1;
  490. let pageSize = item.chartOption ? item.chartOption.pageSize : (~~((item.layout.h * minLayoutHeight + (item.layout.h - 1) * 12 - 20 - 40 - 24 - 8 * 2)/38) + 1)
  491. yield put({ type:'fetchChartData', item: items[i], mandatory: true, page, pageSize: pageSize });
  492. }
  493. }
  494. },
  495. *remoteGetColumns(action, { call, put, select }) {
  496. const { dataSourceCode, mandatory } = action;
  497. const dashboardDesigner = yield select(state => state.dashboardDesigner);
  498. const { dataSources } = dashboardDesigner;
  499. const body = dataSourceCode;
  500. let idx = dataSources.findIndex(d => d.code === dataSourceCode);
  501. if(!mandatory && dataSources[idx].columns && dataSources[idx].columns.length > 0) {
  502. return;
  503. }
  504. try {
  505. const res = yield call(service.fetch, {
  506. url: URLS.DATASOURCE_QUERY_DATACOLUMNS,
  507. body: body
  508. });
  509. if(res.code > 0) {
  510. let resData = res.data;
  511. let columns = resData.map((c, i) => {
  512. return {
  513. key: i,
  514. name: c.columnName,
  515. label: c.columnRaname,
  516. type: c.columnType,
  517. groupable: c.isGroup==='1'?true:false,
  518. filterable: c.isFilter==='1'?true:false,
  519. bucketizable: c.isSubsection==='1'?true:false,
  520. selection: []
  521. }
  522. }).filter(c => c.filterable);
  523. dataSources[idx] = { ...dataSources[idx], columns }
  524. yield put({ type: 'silentSetField', name: 'dataSources', value: dataSources });
  525. }else {
  526. message.error('请求列数据失败:' + res.msg);
  527. yield put({ type: 'silentSetField', name: 'dataSources', value: [] });
  528. }
  529. }catch(e) {
  530. message.error('请求列数据失败: ' + e.message);
  531. }
  532. },
  533. /**
  534. * 同时更改多个filter
  535. */
  536. *changeFilters(action, { put, call, select }) {
  537. try {
  538. const { filters } = action;
  539. const dashboardDesigner = yield select(state => state.dashboardDesigner);
  540. const { items, minLayoutHeight } = dashboardDesigner;
  541. yield put({ type: 'silentSetField', name: 'filters', value: filters });
  542. for(let i = 0; i < items.length; i++) {
  543. let page = items[i].chartOption ? items[i].chartOption.page : 1;
  544. let pageSize = items[i].chartOption ? items[i].chartOption.pageSize : (~~((items[i].layout.h * minLayoutHeight + (items[i].layout.h - 1) * 12 - 20 - 40 - 24 - 8 * 2)/38) + 1)
  545. yield put({ type:'fetchChartData', item: items[i], mandatory: true, page, pageSize });
  546. }
  547. }catch(e) {
  548. message.error('更改过滤条件失败: ' + e.message);
  549. }
  550. },
  551. /**
  552. * 只更改一个filter
  553. */
  554. *changeFilter(action, { put, call, select }) {
  555. const { filter } = action;
  556. const dashboardDesigner = yield select(state => state.dashboardDesigner);
  557. let { filters, items, minLayoutHeight } = dashboardDesigner;
  558. let targetDataSourceCodes = [];
  559. filters = filters.map(f => {
  560. if(f.key === filter.key) {
  561. if(f.combined) {
  562. targetDataSourceCodes = targetDataSourceCodes.concat(f.dataSource.map(d => d.dataSource.code));
  563. }else {
  564. if(targetDataSourceCodes.indexOf(f.dataSource.code) === -1) {
  565. targetDataSourceCodes.push(f.dataSource.code);
  566. }
  567. }
  568. return Object.assign({}, f, filter);
  569. }else {
  570. return f;
  571. }
  572. });
  573. // 找到filters有影响的item
  574. let targetItems = items.filter(item => targetDataSourceCodes.indexOf(item.dataSourceCode) !== -1);
  575. yield put({ type: 'silentSetField', name: 'filters', value: filters });
  576. for(let i = 0; i < targetItems.length; i++) {
  577. let page = targetItems[i].chartOption ? targetItems[i].chartOption.page : 1;
  578. let pageSize = targetItems[i].chartOption ? targetItems[i].chartOption.pageSize : (~~((targetItems[i].layout.h * minLayoutHeight + (targetItems[i].layout.h - 1) * 12 - 20 - 40 - 24 - 8 * 2)/38) + 1)
  579. yield put({ type:'fetchChartData', item: targetItems[i], mandatory: true, page, pageSize });
  580. }
  581. },
  582. *fetchChartData(action, { put, call, select }) {
  583. const { item, mandatory, page, pageSize } = action;
  584. const dashboardDesigner = yield select(state => state.dashboardDesigner);
  585. const { creatorCode, filters, theme } = dashboardDesigner;
  586. const { chartCode, chartType } = item;
  587. if(!mandatory && !!item.chartOption) {
  588. return false;
  589. }
  590. try {
  591. yield put({ type: 'setItemFetching', code: chartCode, fetching: true });
  592. const itemFilters = getTrueFilters(item, filters);
  593. const body = {
  594. dashboardCreatorId: creatorCode,
  595. chartId: chartCode,
  596. filters: getBodyFilters(itemFilters),
  597. testPage: {
  598. pageNum: page|| 1,
  599. pageSize: chartType === 'indicator' ? 99 : pageSize || 99,
  600. }
  601. };
  602. const res = yield call(service.fetch, {
  603. url: URLS.CHART_OPTION,
  604. token: false,
  605. allow: true,
  606. body,
  607. timeout: 30000
  608. });
  609. if(res.code > 0) {
  610. let resData = res.data;
  611. if(!resData) {
  612. yield put({ type: 'setItemFields', code: chartCode, fields: [
  613. { name: 'chartType', value: '' },
  614. { name: 'chartOption', value: {} }
  615. ] });
  616. return false;
  617. }
  618. const { chartType : ctype, chartConfig: chartConfigStr, chartStyle: styleConfigStr } = resData.chartsColumnConfig;
  619. const chartType = CHART_TYPE[ctype];
  620. const chartConfig = JSON.parse(chartConfigStr);
  621. const styleConfig = JSON.parse(styleConfigStr);
  622. let chartOption = parseChartOption(chartType, resData, chartConfig, theme, styleConfig[chartType] || {});
  623. yield put({ type: 'setItemFields', code: chartCode, fields: [
  624. { name: 'filters', value: itemFilters },
  625. { name: 'chartType', value: chartType },
  626. { name: 'chartOption', value: chartOption }
  627. ] });
  628. return { chartType, chartOption };
  629. }else {
  630. yield put({ type: 'setItemFields', code: chartCode, fields: [
  631. { name: 'chartType', value: '' },
  632. { name: 'chartOption', value: {} }
  633. ] });
  634. message.error('请求图表展示数据失败: ' + res.msg);
  635. return false;
  636. }
  637. }catch(e) {
  638. yield put({ type: 'setItemFields', code: chartCode, fields: [
  639. { name: 'chartType', value: '' },
  640. { name: 'chartOption', value: {} }
  641. ] });
  642. message.error('请求图表展示数据失败: ' + e.message);
  643. return false;
  644. }finally {
  645. yield put({ type: 'setItemFetching', code: chartCode, fetching: false });
  646. }
  647. },
  648. *encryptCode(action, { put, call, select }) {
  649. const { shareCode } = action;
  650. const res = yield call(service.fetch, {
  651. url: URLS.DASHBOARD_ENCRYPT_CODE,
  652. method: 'GET',
  653. body: {
  654. code: shareCode
  655. },
  656. });
  657. if(res.code > 0) {
  658. let resData = res.data;
  659. return resData;
  660. }else {
  661. return false;
  662. }
  663. },
  664. *setRelationColumns(action, { put, call, select }) {
  665. const { relationColumns } = action;
  666. let targetDataSourceCodes = []; // 记录有改动的数据源code
  667. const dashboardDesigner = yield select(state => state.dashboardDesigner);
  668. let { filters, items, minLayoutHeight } = dashboardDesigner;
  669. for(let i = filters.length - 1; i >= 0; i--) {
  670. let f = filters[i];
  671. let idx = relationColumns.findIndex(rc => rc.code === f.name);
  672. if(idx > -1) {
  673. // 如果改变的自定义条件已经添加到了筛选条件区域
  674. let nrc = relationColumns[idx];
  675. let willRemove = false;
  676. if(f.type !== nrc.relations[0].column.type) { // 如果改变了所选的列
  677. willRemove = true;
  678. }
  679. let oldDataSourceCodes = f.dataSource.map(d => d.dataSource.code);
  680. let oldColumns = f.dataSource.map(d => d.column.name);
  681. let newDataSourceCodes = nrc.relations.map(r => r.dataSource.code);
  682. let newColumns = nrc.relations.map(r => r.column.name);
  683. if(!arrayEquals(oldDataSourceCodes, newDataSourceCodes) || !arrayEquals(oldColumns, newColumns)) { // 如果改变了数据源或者数据列
  684. willRemove = true;
  685. }
  686. if(willRemove) {
  687. f.dataSource.forEach(d => {
  688. targetDataSourceCodes.push(d.dataSource.code);
  689. });
  690. filters.splice(i, 1); // 将该过滤字段移除
  691. }else { // 只剩下label会改变了
  692. filters[i] = { ...filters[i], label: nrc.name }
  693. }
  694. }else if(f.combined) { // 自定义字段已被删除
  695. f.dataSource.forEach(d => {
  696. targetDataSourceCodes.push(d.dataSource.code);
  697. });
  698. filters.splice(i, 1)
  699. }
  700. }
  701. yield put({ type: 'setFields', fields: [
  702. { name: 'relationColumns', value: relationColumns },
  703. { name: 'filters', value: filters },
  704. { name: 'dirty', value: true },
  705. ] });
  706. // 找到filters有影响的item
  707. let targetItems = items.filter(item => targetDataSourceCodes.indexOf(item.dataSourceCode) !== -1);
  708. yield put({ type: 'silentSetField', name: 'filters', value: filters });
  709. for(let i = 0; i < targetItems.length; i++) {
  710. let page = 1;
  711. let pageSize = targetItems[i].chartOption ? targetItems[i].chartOption.pageSize : (~~((targetItems[i].layout.h * minLayoutHeight + (targetItems[i].layout.h - 1) * 12 - 20 - 40 - 24 - 8 * 2)/38) + 1)
  712. yield put({ type:'fetchChartData', item: targetItems[i], mandatory: true, page, pageSize });
  713. }
  714. },
  715. *modifyItem(action, { select, call, put }) {
  716. try{
  717. const { item } = action;
  718. const dashboardDesigner = yield select(state => state.dashboardDesigner);
  719. const { items } = dashboardDesigner;
  720. let idx = items.findIndex(i => i.code === item.code);
  721. if(idx > -1) {
  722. items[idx] = { ...items[idx], ...item };
  723. yield put({ type: 'setField', name: 'items', value: items });
  724. }
  725. }catch(e) {
  726. message.error('修改失败: ' + e.message);
  727. }
  728. },
  729. /**
  730. * dataView预览窗口取数
  731. */
  732. *fetchDataList(action, { select, call, put }) {
  733. const { item, page, pageSize } = action;
  734. const dashboardDesigner = yield select(state => state.dashboardDesigner);
  735. const { creatorCode, filters } = dashboardDesigner;
  736. const { chartCode } = item;
  737. try {
  738. yield put({ type: 'dataList/setField', name: 'loading', value: true });
  739. const body = {
  740. dashboardCreatorId: creatorCode,
  741. chartId: chartCode,
  742. filters: getBodyFilters(getTrueFilters(item, filters)),
  743. testPage: {
  744. pageNum: page || 1,
  745. pageSize: pageSize || 25,
  746. }
  747. };
  748. const res = yield call(service.fetch, {
  749. url: URLS.CHART_OPTION,
  750. token: false,
  751. allow: true,
  752. body,
  753. timeout: 30000
  754. });
  755. if(res.code > 0) {
  756. const { chartsColumnConfig, valueList } = res.data;
  757. const { list, pageSize, total } = valueList;
  758. const chartConfig = JSON.parse(chartsColumnConfig.chartConfig);
  759. const columns = chartConfig.viewColumns;
  760. yield put({ type: 'dataList/setFields', fields: [
  761. { name: 'columns', value: columns },
  762. { name: 'dataSource', value: list },
  763. { name: 'pageSize', value: pageSize },
  764. { name: 'total', value: total }
  765. ] });
  766. // 主动触发一次window的resize事件
  767. yield window.setTimeout(() => {
  768. var e = document.createEvent("Event");
  769. e.initEvent("resize", true, true);
  770. window.dispatchEvent(e);
  771. }, 20);
  772. }else {
  773. message.error('请求图表展示数据失败: ' + res.msg);
  774. return false;
  775. }
  776. }catch(e) {
  777. message.error('请求图表展示数据错误: ' + e.message);
  778. return false;
  779. }finally {
  780. yield put({ type: 'dataList/setField', name: 'loading', value: false });
  781. }
  782. },
  783. *exportToExcel(action, { select, take, put }) {
  784. const dashboardDesigner = yield select(state => state.dashboardDesigner);
  785. const { code, name, filters, items } = dashboardDesigner;
  786. try {
  787. yield put({ type: 'setField', name: 'loading', value: true });
  788. let sheets = [];
  789. for(let i = 0; i < items.length; i++) {
  790. let item = items[i];
  791. let { chartCode, name: itemName, chartOption, viewType, chartType, content } = item;
  792. let header;
  793. let columns = [];
  794. let rows = [];
  795. const TYPES = {
  796. time: 'DateTime',
  797. categorical: 'String',
  798. scale: 'Number',
  799. string: 'String'
  800. };
  801. if(viewType === 'chart') {
  802. let sync = yield put({
  803. type: 'chartDesigner/getChartTableData',
  804. chartCode, chartType, chartOption,
  805. filters: getTrueFilters(item, filters),
  806. inDashboard: true,
  807. dashboardCode: code
  808. });
  809. yield take('chartDesigner/getChartTableData/@@end');
  810. yield sync.then(tableData => {
  811. let { columns: tcolumns, dataSource: tdatasource } = tableData;
  812. columns = tcolumns.map(c => ({
  813. name: c.title,
  814. width: c.width || 25,
  815. type: TYPES[c.type]
  816. }));
  817. rows = tdatasource.map(d => {
  818. return tcolumns.map(c => d[c.dataIndex])
  819. })
  820. if(chartType === 'aggregateTable') {
  821. header = `分析目标:${chartOption.originConfig.targetColumn.label}`
  822. }
  823. });
  824. }else if(viewType === 'richText') {
  825. content = content || '';
  826. columns = [{
  827. name: content.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;').replace(/\'/g, '&apos;'),
  828. width: 25,
  829. type: 'String'
  830. }];
  831. rows = [];
  832. }
  833. // 处理重名情况
  834. let name = itemName || `未命名`;
  835. let new_name = name;
  836. let fidx = sheets.findIndex(s => s.name === new_name);
  837. let idx = 1;
  838. while(fidx > -1){
  839. new_name = `${name}(${idx})`;
  840. fidx = -1;
  841. for(let x = 0; x < sheets.length; x++) {
  842. if(sheets[x].name === new_name) {
  843. fidx = x;
  844. }
  845. }
  846. idx++;
  847. }
  848. name = new_name;
  849. sheets.push({
  850. name,
  851. header,
  852. columns,
  853. rows
  854. });
  855. }
  856. let e = yield new Exportor();
  857. e.init({ sheets }, name).then(() => {
  858. e.export()
  859. });
  860. }catch(e) {
  861. message.error('报表导出错误: ' + e);
  862. }finally {
  863. yield put({ type: 'setField', name: 'loading', value: false });
  864. }
  865. },
  866. *exportToImage(action, { select, put }) {
  867. const dashboardDesigner = yield select(state => state.dashboardDesigner);
  868. const { name } = dashboardDesigner;
  869. let viewcontent = document.querySelector('.dashboard-viewcontent');
  870. let reactGridLayout = viewcontent.children[0];
  871. let itemEls = reactGridLayout.children;
  872. let classListBackup = []; // 样式表备份
  873. let tableStyles = []; // 表格样式备份
  874. for(let i = 0; i < itemEls.length; i++) {
  875. let backup = beforeItemExportImage(itemEls[i]);
  876. classListBackup = backup.classListBackup; // 每个itemEl的样式名表是一样的所以可以重复赋值
  877. tableStyles.push({ index: i, ...backup.tableStyle });
  878. }
  879. try {
  880. yield put({ type: 'setField', name: 'loading', value: false });
  881. yield html2canvas(reactGridLayout, { useCORS: true }).then(canvas => {
  882. let aLink = document.createElement('a');
  883. let blob = base64ToBlob(canvas.toDataURL()); //new Blob([content]);
  884. let evt = document.createEvent("HTMLEvents");
  885. evt.initEvent("click", true, true);//initEvent 不加后两个参数在FF下会报错 事件类型,是否冒泡,是否阻止浏览器的默认行为
  886. aLink.download = (name || '未命名') + '.png';
  887. aLink.href = URL.createObjectURL(blob);
  888. aLink.dispatchEvent(new MouseEvent('click', {bubbles: true, cancelable: true, view: window}));//兼容火狐
  889. })
  890. }catch(e) {
  891. message.error('报表图片错误: ' + e);
  892. }finally {
  893. for(let i = 0; i< itemEls.length;i++) {
  894. let tableStyle = tableStyles.find(t => t.index === i);
  895. afterItemExportImage(itemEls[i], classListBackup, tableStyle);
  896. }
  897. yield put({ type: 'setField', name: 'loading', value: false });
  898. }
  899. },
  900. *exportItemToImage(action, { select, put }) {
  901. const { itemEl, item } = action;
  902. const { name } = item;
  903. let { classListBackup, tableStyle } = beforeItemExportImage(itemEl);
  904. try {
  905. yield put({ type: 'setField', name: 'loading', value: false });
  906. yield html2canvas(itemEl, { useCORS: true }).then(canvas => {
  907. let aLink = document.createElement('a');
  908. let blob = base64ToBlob(canvas.toDataURL()); //new Blob([content]);
  909. let evt = document.createEvent("HTMLEvents");
  910. evt.initEvent("click", true, true);//initEvent 不加后两个参数在FF下会报错 事件类型,是否冒泡,是否阻止浏览器的默认行为
  911. aLink.download = (name || '未命名') + '.png';
  912. aLink.href = URL.createObjectURL(blob);
  913. aLink.dispatchEvent(new MouseEvent('click', {bubbles: true, cancelable: true, view: window}));//兼容火狐
  914. })
  915. }catch(e) {
  916. message.error('导出图片错误:' + e);
  917. }finally {
  918. afterItemExportImage(itemEl, classListBackup, tableStyle);
  919. yield put({ type: 'setField', name: 'loading', value: false });
  920. }
  921. }
  922. },
  923. subscriptions: {
  924. setup({ dispatch, history}) {
  925. dispatch({ type: 'reset' });
  926. }
  927. }
  928. };