commonBarStatisticsCharts.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580
  1. <template>
  2. <div class="statstics-wrapper">
  3. <div
  4. ref="headerWrapper"
  5. class="statstics-header"
  6. >
  7. <StatisticsHeader
  8. :title="`${chartsTitle}统计`"
  9. :custom-items="customFormItems"
  10. @getFormData="getFormData"
  11. @export="exportHandler"
  12. />
  13. </div>
  14. <div class="statstics-content">
  15. <div
  16. id="chart"
  17. class="statistics-chart"
  18. :style="{ height: chartHeight }"
  19. />
  20. </div>
  21. </div>
  22. </template>
  23. <script>
  24. import StatisticsHeader from './statisticsHeader.vue'
  25. import { TempQuery } from '@/api/temp'
  26. import { mapGetters } from 'vuex'
  27. import * as XLSX from 'xlsx'
  28. import XLSX_STYLE from 'xlsx-style'
  29. import FileSaver from 'file-saver'
  30. export default {
  31. name: 'CommonBarStatisticsCharts',
  32. components: { StatisticsHeader },
  33. props: {
  34. chartsTitle: {
  35. type: String,
  36. required: true
  37. },
  38. querySettings: {
  39. type: Object,
  40. required: true
  41. },
  42. customFormItems: {
  43. type: Array,
  44. default: () => []
  45. }
  46. },
  47. data() {
  48. return {
  49. myChart: null,
  50. debounceTime: 300,
  51. chartHeight: '70vh',
  52. hasChartData: false,
  53. seriesKey: 'seriesData',
  54. filters: [],
  55. tableData: [],
  56. params: [],
  57. options: {
  58. backgroundColor: '#fff',
  59. tooltip: {
  60. trigger: 'axis',
  61. axisPointer: {
  62. type: 'cross',
  63. crossStyle: {
  64. color: '#999'
  65. }
  66. }
  67. },
  68. legend: {
  69. top: '5%',
  70. right: '5%',
  71. icon: 'rect',
  72. height: 14,
  73. itemWidth: 14,
  74. itemHeight: 14,
  75. itemGap: 30,
  76. data: [
  77. this.chartsTitle.replace('量', '数量'),
  78. // `${this.chartsTitle}量同比`,
  79. `${this.chartsTitle}环比`
  80. ],
  81. textStyle: {
  82. fontFamily: 'Helvetica, "Microsoft YaHei"',
  83. color: '#101116'
  84. }
  85. },
  86. grid: {
  87. top: '15%',
  88. left: '5%',
  89. right: '5%',
  90. bottom: '5%'
  91. },
  92. xAxis: {
  93. data: [],
  94. axisLine: {
  95. show: true,
  96. lineStyle: {
  97. color: '#000000'
  98. }
  99. },
  100. axisTick: {
  101. show: false // 隐藏X轴刻度
  102. },
  103. axisLabel: {
  104. fontFamily: 'Helvetica, "Microsoft YaHei"',
  105. color: '#101116'
  106. },
  107. axisPointer: {
  108. type: 'shadow'
  109. }
  110. },
  111. yAxis: [
  112. {
  113. min: 0,
  114. max: 60000,
  115. splitLine: {
  116. lineStyle: {
  117. type: 'dashed',
  118. color: '#B0B3C3',
  119. opacity: 0.5
  120. }
  121. },
  122. axisPointer: {
  123. label: {
  124. formatter: ({ value }) => value.toFixed()
  125. }
  126. },
  127. axisLabel: {
  128. fontFamily: 'Helvetica, "Microsoft YaHei"',
  129. color: '#101116'
  130. }
  131. },
  132. {
  133. min: -0.3,
  134. max: 0.5,
  135. axisLabel: {
  136. formatter: value => (value * 100).toFixed(2) + '%',
  137. fontFamily: 'Helvetica, "Microsoft YaHei"',
  138. color: '#101116'
  139. },
  140. axisPointer: {
  141. label: {
  142. formatter: ({ value }) => (value * 100).toFixed(2) + '%'
  143. }
  144. },
  145. splitLine: {
  146. show: false
  147. }
  148. }
  149. ],
  150. series: [
  151. {
  152. name: this.chartsTitle.replace('量', '数量'),
  153. type: 'bar',
  154. z: 2,
  155. itemStyle: {
  156. color: '#6682B5'
  157. },
  158. barWidth: 40,
  159. label: {
  160. show: true,
  161. position: 'top'
  162. },
  163. data: []
  164. },
  165. {
  166. name: `${this.chartsTitle}同比`,
  167. type: 'line',
  168. z: 4,
  169. yAxisIndex: 1,
  170. symbol: 'circle',
  171. itemStyle: {
  172. color: '#F2B849',
  173. borderColor: '#ffffff',
  174. borderWidth: 4
  175. },
  176. lineStyle: {
  177. width: 4,
  178. color: '#F2B849'
  179. },
  180. symbolSize: 32,
  181. tooltip: {
  182. valueFormatter: value => (value * 100).toFixed(2) + '%'
  183. },
  184. data: []
  185. },
  186. {
  187. name: `${this.chartsTitle}环比`,
  188. type: 'line',
  189. z: 3,
  190. yAxisIndex: 1,
  191. symbol: 'circle',
  192. itemStyle: {
  193. color: '#E33D3D',
  194. borderColor: '#ffffff',
  195. borderWidth: 4
  196. },
  197. lineStyle: {
  198. width: 4,
  199. color: '#E33D3D'
  200. },
  201. symbolSize: 32,
  202. tooltip: {
  203. valueFormatter: value => (value * 100).toFixed(2) + '%'
  204. },
  205. data: []
  206. }
  207. ]
  208. }
  209. }
  210. },
  211. computed: {
  212. ...mapGetters(['sidebar'])
  213. },
  214. watch: {
  215. // 监听数据变化 重绘图形
  216. options: {
  217. handler(obj) {
  218. this.myChart.setOption(obj)
  219. this.resizeHandler()
  220. },
  221. deep: true
  222. },
  223. 'sidebar.expand'() {
  224. this.setChartHeight()
  225. },
  226. querySettings: {
  227. handler({ seriesKey, filters }) {
  228. if (seriesKey) {
  229. this.seriesKey = seriesKey
  230. }
  231. if (filters?.length) {
  232. this.filters = filters
  233. }
  234. },
  235. deep: true,
  236. immediate: true
  237. }
  238. },
  239. mounted() {
  240. this.setChartHeight()
  241. this.myChart = this.$echarts.init(document.getElementById('chart'))
  242. this.myChart.setOption(this.options)
  243. // 监听页面缩放
  244. this.debouncedChartHeightSetter = this._.debounce(this.setChartHeight, this.debounceTime)
  245. window.addEventListener('resize', this.debouncedChartHeightSetter)
  246. },
  247. beforeDestroy() {
  248. // 销毁实例和移除监听
  249. window.removeEventListener('resize', this.debouncedChartHeightSetter)
  250. if (this.myChart) {
  251. this.myChart.dispose()
  252. this.myChart = null
  253. }
  254. },
  255. methods: {
  256. resetDatas() {
  257. this.hasChartData = false
  258. this.options.yAxis[0].max = 60000
  259. this.options.xAxis.data = []
  260. this.options.series[0].data = []
  261. this.options.series[2].data = []
  262. this.options.yAxis[1].min = -0.3
  263. this.options.yAxis[1].max = 0.5
  264. },
  265. getFormData(formData) {
  266. this.resetDatas()
  267. let id
  268. let params = []
  269. if (formData.range === '基地分公司') {
  270. if (formData.flightType === '有行李') {
  271. id = this.querySettings.withBaggageByArea
  272. } else if (formData.baggageType === '不包含DEL') {
  273. id = this.querySettings.notDelByArea
  274. } else if (formData.passengerType[0] === '要客类型') {
  275. id = this.querySettings.importantByArea
  276. } else {
  277. id = this.querySettings.byArea
  278. }
  279. params = [formData.interval, formData.area, formData.inOrOut, formData.dateTime[0], formData.dateTime[1]]
  280. } else {
  281. if (formData.flightType === '有行李') {
  282. id = this.querySettings.withBaggageByOther
  283. } else if (formData.baggageType === '不包含DEL') {
  284. id = this.querySettings.notDelByOther
  285. } else if (formData.passengerType[0] === '要客类型') {
  286. id = this.querySettings.importantByOther
  287. } else {
  288. id = this.querySettings.byOther
  289. }
  290. params = [formData.interval, formData.range, formData.inOrOut, formData.dateTime[0], formData.dateTime[1]]
  291. if (formData.airline.length) {
  292. params.splice(2, 0, formData.airline)
  293. } else if (formData.airport.length) {
  294. params.splice(2, 0, formData.airport)
  295. } else if (formData.terminal !== '') {
  296. params.splice(2, 0, formData.terminal)
  297. } else {
  298. params.splice(2, 0, '全部')
  299. }
  300. }
  301. if (formData.passengerType.length) {
  302. this.filters = [
  303. {
  304. key: formData.passengerType[0],
  305. value: formData.passengerType[1]
  306. }
  307. ]
  308. }
  309. this.params = [...params, ...this.filters.map(({ value }) => value)]
  310. if (params[2] instanceof Array) {
  311. const paramsList = params[2].map(param => [...params.slice(0, 2), param, ...params.slice(3)])
  312. this.getMultipleChartsData(id, paramsList)
  313. } else {
  314. this.getSingleChartsData(id, params)
  315. }
  316. },
  317. async getMultipleChartsData(id, paramsList) {
  318. try {
  319. const listValuesArray = await Promise.all(paramsList.map(params => this.getChartsData(id, params)))
  320. const listValues = listValuesArray.reduce((preValues, currentValues) => {
  321. currentValues.forEach(value => {
  322. const preValue = preValues.find(preValue => preValue.A === value.A)
  323. if (preValue) {
  324. preValue[this.seriesKey] += value[this.seriesKey]
  325. } else {
  326. preValues.push({
  327. A: value.A,
  328. [this.seriesKey]: value[this.seriesKey]
  329. })
  330. }
  331. })
  332. return preValues
  333. }, [])
  334. this.setChartsData(this._.sortBy(listValues, 'A'))
  335. } catch (error) {
  336. this.$message.error(error.message)
  337. }
  338. },
  339. async getSingleChartsData(id, params) {
  340. try {
  341. const listValues = await this.getChartsData(id, params)
  342. this.setChartsData(listValues)
  343. } catch (error) {
  344. this.$message.error(error.message)
  345. }
  346. },
  347. async getChartsData(id, params) {
  348. try {
  349. const {
  350. code,
  351. returnData: { listValues },
  352. message
  353. } = await TempQuery({
  354. id,
  355. dataContent: params
  356. })
  357. if (Number(code) === 0) {
  358. return listValues
  359. } else {
  360. return Promise.reject(message || '失败')
  361. }
  362. } catch (error) {
  363. return Promise.reject(error.message || '失败')
  364. }
  365. },
  366. setChartsData(listValues) {
  367. const xAxisData = []
  368. const yAxisData = [0]
  369. const seriesDatas = []
  370. const filteredList = listValues.filter(element =>
  371. this.filters.every((key, value) => {
  372. if (key && value && element[key] !== value) {
  373. return false
  374. } else {
  375. return true
  376. }
  377. })
  378. )
  379. if (filteredList.length === 0) {
  380. this.$message.info('未查询到对应数据')
  381. return
  382. }
  383. for (let i = 0; i < filteredList.length; i++) {
  384. xAxisData.push(filteredList[i].A)
  385. seriesDatas.push(filteredList[i][this.seriesKey])
  386. if (i > 0) {
  387. if (filteredList[i - 1][this.seriesKey] > 0) {
  388. yAxisData.push(
  389. (filteredList[i][this.seriesKey] - filteredList[i - 1][this.seriesKey]) /
  390. filteredList[i - 1][this.seriesKey]
  391. )
  392. } else {
  393. yAxisData.push(0)
  394. }
  395. }
  396. }
  397. let max = Math.max(...seriesDatas)
  398. max = Math.ceil(max / 10) * 10
  399. this.options.yAxis[0].max = max
  400. this.options.xAxis.data = xAxisData
  401. this.options.series[0].data = seriesDatas
  402. this.options.series[2].data = yAxisData
  403. this.options.yAxis[1].min = (Math.min(...yAxisData) - 0.1).toFixed(2)
  404. this.options.yAxis[1].max = (Math.max(...yAxisData) + 0.1).toFixed(2)
  405. this.tableData = [xAxisData, seriesDatas, yAxisData]
  406. this.hasChartData = true
  407. },
  408. setChartHeight() {
  409. const topBarHeight = 80
  410. const headerBlankHeight = 24
  411. const tabsWrapperHeight = 62
  412. const headerHeight = this.$refs['headerWrapper'].offsetHeight
  413. const footerBlankHeight = 24
  414. this.chartHeight = `calc(100vh - ${
  415. topBarHeight + headerBlankHeight + tabsWrapperHeight + headerHeight + footerBlankHeight
  416. }px)`
  417. this.$nextTick(() => {
  418. this.resizeHandler()
  419. })
  420. },
  421. resizeHandler() {
  422. if (this.myChart) {
  423. this.myChart.resize()
  424. }
  425. },
  426. exportHandler() {
  427. if (!this.hasChartData) {
  428. this.$message.warning('请查询后再进行导出')
  429. return
  430. }
  431. // const myCanvas = this.myChart._dom.querySelectorAll('canvas')[0]
  432. // const image = myCanvas.toDataURL('image/png')
  433. // const $a = document.createElement('a')
  434. // $a.setAttribute('href', image)
  435. // $a.setAttribute('download', `${this.chartsTitle}统计.png`)
  436. // $a.click()
  437. // 生成表格数据
  438. const xlsxDatas = [['时间', this.chartsTitle.replace('量', '数量'), `${this.chartsTitle}环比`]]
  439. const transposition = this.tableData[0].map((col, colIndex) => {
  440. return this.tableData.map((row, rowIndex) => {
  441. return rowIndex === 2 ? (row[colIndex] * 100).toFixed(2) + '%' : row[colIndex]
  442. })
  443. })
  444. xlsxDatas.push(...transposition)
  445. // 添加合计行
  446. if (xlsxDatas.length > 2) {
  447. const summaryRow = ['合计']
  448. const colNum = xlsxDatas[0].length
  449. for (let colIndex = 1; colIndex < colNum; colIndex++) {
  450. summaryRow[colIndex] = xlsxDatas.reduce((pre, currentRow, rowIndex) => {
  451. if (colIndex === 1) {
  452. if (rowIndex === 0) {
  453. return 0
  454. } else {
  455. return pre + currentRow[colIndex]
  456. }
  457. } else {
  458. return pre
  459. }
  460. }, '')
  461. }
  462. xlsxDatas.push(summaryRow)
  463. }
  464. // 计算列宽
  465. const columnWidths = []
  466. xlsxDatas.forEach((row, rowIndex) => {
  467. // 计算每一列宽度,考虑换行
  468. row.forEach((cell, columnIndex) => {
  469. const cellWidth = Math.max(
  470. ...cell
  471. .toString()
  472. .split('\n')
  473. .map(cellRow =>
  474. cellRow.split('').reduce((pre, curr) => {
  475. const letterSize = curr.charCodeAt(0) > 255 ? 2 : 1
  476. return pre + letterSize
  477. }, 0)
  478. )
  479. )
  480. if ((!columnWidths[columnIndex] && cellWidth > 0) || cellWidth > columnWidths[columnIndex]) {
  481. columnWidths[columnIndex] = cellWidth
  482. }
  483. })
  484. })
  485. // 生成表格
  486. const sheet = XLSX.utils.aoa_to_sheet(xlsxDatas)
  487. // 添加列宽度
  488. sheet['!cols'] = columnWidths.map(width => ({
  489. wch: width + 2
  490. }))
  491. // 样式
  492. const borderStyle = {
  493. style: 'medium',
  494. color: {
  495. rgb: 'FFFFFF'
  496. }
  497. }
  498. const reg = /^[A-Z]+([\d]+$)/
  499. for (const key in sheet) {
  500. const match = key.match(reg)
  501. if (match) {
  502. const rowIndex = match[1]
  503. let cellStyle = {
  504. alignment: {
  505. horizontal: 'center',
  506. vertical: 'center',
  507. wrapText: true
  508. }
  509. }
  510. if (Number(rowIndex) === 1) {
  511. cellStyle = {
  512. ...cellStyle,
  513. border: {
  514. top: borderStyle,
  515. right: borderStyle,
  516. bottom: borderStyle,
  517. left: borderStyle
  518. },
  519. font: {
  520. color: {
  521. rgb: 'FFFFFF'
  522. }
  523. },
  524. fill: {
  525. fgColor: {
  526. rgb: '3366FF'
  527. }
  528. }
  529. }
  530. } else {
  531. cellStyle.alignment.horizontal = 'left'
  532. }
  533. const cellValue = sheet[key].v
  534. const isNumber = /^[^0]/.test(cellValue) && !isNaN(parseFloat(cellValue)) && isFinite(cellValue)
  535. const isPercentage = /^[0-9]+(\.[0-9]+){0,1}\%$/.test(cellValue)
  536. if (isNumber) {
  537. sheet[key] = {
  538. ...sheet[key],
  539. t: 'n',
  540. z: '0',
  541. v: Number(cellValue)
  542. }
  543. }
  544. if (isPercentage) {
  545. sheet[key] = {
  546. ...sheet[key],
  547. t: 'n',
  548. z: '0.00%',
  549. v: parseFloat(cellValue)
  550. }
  551. }
  552. sheet[key].s = cellStyle
  553. }
  554. }
  555. // 表格数据转换
  556. const workBook = XLSX.utils.book_new()
  557. XLSX.utils.book_append_sheet(workBook, sheet, this.chartsTitle)
  558. const tableWrite = XLSX_STYLE.write(workBook, {
  559. bookType: 'xlsx',
  560. bookSST: true,
  561. type: 'buffer',
  562. cellStyles: true
  563. })
  564. // 下载表格
  565. const fileName = `${this.chartsTitle}统计-${this.params.join('-')}.xlsx`
  566. FileSaver.saveAs(new Blob([tableWrite], { type: 'application/octet-stream' }), fileName)
  567. }
  568. }
  569. }
  570. </script>
  571. <style lang="scss" scoped>
  572. .statistics-chart {
  573. width: 100%;
  574. }
  575. </style>