commonPieStatisticsCharts.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504
  1. <template>
  2. <div class="statstics-wrapper">
  3. <div ref="headerWrapper" class="statstics-header">
  4. <StatisticsHeader :title="`${chartsTitle}统计`" :custom-items="customFormItems" @getFormData="getFormData" @export="exportHandler" />
  5. </div>
  6. <div class="statstics-content">
  7. <div id="chart" class="statistics-chart" :style="{ height: chartHeight }" />
  8. </div>
  9. </div>
  10. </template>
  11. <script>
  12. import StatisticsHeader from './statisticsHeader.vue'
  13. import { Query } from '@/api/dataIntegration'
  14. import { mapGetters } from 'vuex'
  15. import * as XLSX from 'xlsx'
  16. import XLSX_STYLE from 'xlsx-style'
  17. import FileSaver from 'file-saver'
  18. export default {
  19. name: 'CommonBarStatisticsCharts',
  20. components: { StatisticsHeader },
  21. props: {
  22. chartsTitle: {
  23. type: String,
  24. required: true
  25. },
  26. querySettings: {
  27. type: Object,
  28. required: true
  29. },
  30. categories: {
  31. type: Array,
  32. required: true
  33. },
  34. customFormItems: {
  35. type: Array,
  36. default: () => []
  37. },
  38. pieTitle: {
  39. type: String,
  40. default: '总件数'
  41. }
  42. },
  43. data () {
  44. return {
  45. myChart: null,
  46. debounceTime: 300,
  47. chartHeight: '70vh',
  48. hasChartData: false,
  49. tableData: [],
  50. params: [],
  51. options: {
  52. backgroundColor: '#ffffff',
  53. tooltip: {
  54. trigger: 'item'
  55. },
  56. title: {
  57. text: '',
  58. // 副标题
  59. subtext: '0',
  60. // 主副标题间距
  61. itemGap: 24,
  62. x: 'left',
  63. y: 'center',
  64. left: '30%',
  65. top: '40%',
  66. textAlign: 'center',
  67. // 主标题样式
  68. textStyle: {
  69. fontSize: '48',
  70. color: '#ffffff',
  71. fontWeight: 'bold',
  72. fontFamily: 'Microsoft YaHei'
  73. },
  74. // 副标题样式
  75. subtextStyle: {
  76. fontSize: '80',
  77. color: '#ffffff',
  78. fontWeight: 'bold'
  79. }
  80. },
  81. legend: {
  82. show: true,
  83. left: '60%',
  84. x: 'right',
  85. y: 'center',
  86. icon: 'rect',
  87. itemWidth: 20,
  88. itemHeight: 20,
  89. formatter: name => this.legendFormatter(name),
  90. textStyle: {
  91. backgroundColor: 'transparent',
  92. lineHeight: 0,
  93. rich: {
  94. chartsTitle: {
  95. width: 200,
  96. lineHeight: 100,
  97. fontSize: 32,
  98. fontFamily: 'Microsoft YaHei',
  99. fontWeight: 'bold',
  100. color: '#101116',
  101. padding: [0, 1000, 0, -20]
  102. },
  103. name: {
  104. fontSize: 20,
  105. fontFamily: 'Microsoft YaHei',
  106. fontWeight: 'bold',
  107. color: '#101116',
  108. lineHeight: 100
  109. },
  110. label: {
  111. fontSize: 16,
  112. fontFamily: 'Microsoft YaHei',
  113. color: '#101116'
  114. },
  115. value: {
  116. width: 96,
  117. fontSize: 16,
  118. fontFamily: 'Helvetica',
  119. fontWeight: 'bold',
  120. color: '#101116'
  121. },
  122. ratio: {
  123. width: 80,
  124. fontSize: 16,
  125. fontFamily: 'Helvetica',
  126. fontWeight: 'bold',
  127. color: '#101116'
  128. },
  129. wrap: {
  130. padding: [0, 40, 0, 0]
  131. }
  132. }
  133. },
  134. selected: {
  135. [this.chartsTitle]: false
  136. }
  137. },
  138. series: [
  139. {
  140. name: '',
  141. type: 'pie',
  142. left: '30%',
  143. width: 560,
  144. height: 560,
  145. center: [0, '60%'],
  146. radius: ['60%', '90%'],
  147. avoidLabelOverlap: false,
  148. label: {
  149. show: false,
  150. position: 'center'
  151. },
  152. emphasis: {
  153. label: {
  154. show: false,
  155. fontSize: '40',
  156. fontWeight: 'bold'
  157. }
  158. },
  159. labelLine: {
  160. show: false
  161. },
  162. data: []
  163. },
  164. {
  165. name: '总数',
  166. type: 'pie',
  167. left: '30%',
  168. width: 560,
  169. height: 560,
  170. center: [0, '60%'],
  171. radius: ['0%', '50%'],
  172. avoidLabelOverlap: false,
  173. itemStyle: {
  174. normal: {
  175. color: '#101116'
  176. }
  177. },
  178. label: {
  179. show: false,
  180. position: 'center'
  181. },
  182. // 自定义中心内容的话需要把这个关闭
  183. emphasis: {
  184. label: {
  185. show: false
  186. }
  187. },
  188. labelLine: {
  189. show: false
  190. },
  191. data: []
  192. }
  193. ]
  194. },
  195. totalCount: [{ value: 0 }],
  196. categoryDatas: [],
  197. categoryKey: 'specialnum',
  198. seriesKey: 'special'
  199. }
  200. },
  201. computed: {
  202. ...mapGetters(['sidebar'])
  203. },
  204. watch: {
  205. pieTitle: {
  206. handler (val) {
  207. this.options.title.text = val
  208. },
  209. immediate: true
  210. },
  211. // 监听数据变化 重绘图形
  212. options: {
  213. handler (obj) {
  214. this.myChart.setOption(obj)
  215. this.resizeHandler()
  216. },
  217. deep: true
  218. },
  219. categories: {
  220. handler (arr) {
  221. this.categoryDatas = arr.map(categoryName => ({
  222. name: categoryName,
  223. value: 0
  224. }))
  225. this.categoryDatas.unshift({
  226. name: this.chartsTitle,
  227. value: null
  228. })
  229. },
  230. deep: true,
  231. immediate: true
  232. },
  233. querySettings: {
  234. handler ({ categoryKey, seriesKey }) {
  235. if (seriesKey) {
  236. this.seriesKey = seriesKey
  237. }
  238. if (categoryKey) {
  239. this.categoryKey = categoryKey
  240. }
  241. },
  242. deep: true,
  243. immediate: true
  244. },
  245. 'sidebar.expand' () {
  246. this.setChartHeight()
  247. }
  248. },
  249. mounted () {
  250. this.setChartHeight()
  251. this.myChart = this.$echarts.init(document.getElementById('chart'))
  252. this.options.series[0].data = this.categoryDatas
  253. this.options.legend.data = this.categoryDatas.map(({ name }, index) => {
  254. if (index === 0) {
  255. return {
  256. name,
  257. icon: 'none'
  258. }
  259. } else {
  260. return {
  261. name
  262. }
  263. }
  264. })
  265. this.options.series[1].data = this.totalCount
  266. this.myChart.setOption(this.options)
  267. this.myChart.on('legendselectchanged', ({ name }) => {
  268. if (name === this.chartsTitle) {
  269. this.myChart.dispatchAction({
  270. type: 'legendUnSelect',
  271. name
  272. })
  273. }
  274. })
  275. // 监听页面缩放
  276. this.debouncedChartHeightSetter = this._.debounce(this.setChartHeight, this.debounceTime)
  277. window.addEventListener('resize', this.debouncedChartHeightSetter)
  278. },
  279. beforeDestroy () {
  280. // 销毁实例和移除监听
  281. window.removeEventListener('resize', this.debouncedChartHeightSetter)
  282. if (this.myChart) {
  283. this.myChart.dispose()
  284. this.myChart = null
  285. }
  286. },
  287. methods: {
  288. legendFormatter (name) {
  289. const index = this.categoryDatas.findIndex(category => category.name === name)
  290. if (index === 0) {
  291. return `{chartsTitle|${name}}`
  292. } else {
  293. const value = this.categoryDatas[index].value
  294. const ratio = value && this.totalCount.value ? ((value / this.totalCount.value) * 100).toFixed(2) + '%' : '0%'
  295. const richString = `{name|${name}}\n{label|数量:}{value|${value}}{label|占比:}{ratio|${ratio}}`
  296. return index % 2 ? richString + '{wrap| }' : richString
  297. }
  298. },
  299. resetDatas () {
  300. this.hasChartData = false
  301. this.categoryDatas.forEach(category => {
  302. category && (category.value = 0)
  303. })
  304. this.options.title.subtext = '0'
  305. this.options.series[1].data[0].value = 0
  306. },
  307. getFormData (formData) {
  308. this.resetDatas()
  309. let id
  310. let params = []
  311. if (formData.range === '基地分公司') {
  312. id = this.querySettings.byArea
  313. params = [formData.interval, formData.area, formData.inOrOut, formData.dateTime[0], formData.dateTime[1]]
  314. } else if (formData.range !== '基地分公司' && formData.range !== '') {
  315. id = this.querySettings.byOther
  316. params = [formData.interval, formData.range, formData.inOrOut, formData.dateTime[0], formData.dateTime[1]]
  317. if (formData.airline === '' && formData.airport === '' && formData.terminal === '') {
  318. params.splice(2, 0, '全部')
  319. }
  320. if (formData.airline !== '') {
  321. params.splice(2, 0, formData.airline)
  322. }
  323. if (formData.airport !== '') {
  324. params.splice(2, 0, formData.airport)
  325. }
  326. if (formData.terminal !== '') {
  327. params.splice(2, 0, formData.terminal)
  328. }
  329. }
  330. this.params = params
  331. this.getChartsData(id, params)
  332. },
  333. async getChartsData (id, params) {
  334. try {
  335. const {
  336. code,
  337. returnData: { listValues },
  338. message
  339. } = await Query({
  340. id,
  341. dataContent: params
  342. })
  343. if (Number(code) === 0) {
  344. if (listValues.length === 0) {
  345. this.$message.info('未查询到对应数据')
  346. return
  347. }
  348. let totalCount = 0
  349. listValues.forEach(element => {
  350. this.categoryDatas.forEach(category => {
  351. if (element[this.categoryKey]?.includes(category.name)) {
  352. category.value += element[this.seriesKey]
  353. totalCount += element[this.seriesKey]
  354. }
  355. })
  356. })
  357. this.options.title.subtext = totalCount.toString()
  358. this.totalCount.value = totalCount
  359. this.tableData = listValues
  360. this.hasChartData = true
  361. } else {
  362. this.$message.error(message || '失败')
  363. }
  364. } catch (error) {
  365. this.$message.error("失败");
  366. }
  367. },
  368. setChartHeight () {
  369. const topBarHeight = 80
  370. const headerBlankHeight = 24
  371. const tabsWrapperHeight = 62
  372. const headerHeight = this.$refs['headerWrapper'].offsetHeight
  373. const footerBlankHeight = 24
  374. this.chartHeight = `calc(100vh - ${topBarHeight + headerBlankHeight + tabsWrapperHeight + headerHeight + footerBlankHeight
  375. }px)`
  376. this.$nextTick(() => {
  377. this.resizeHandler()
  378. })
  379. },
  380. resizeHandler () {
  381. if (this.myChart) {
  382. this.myChart.resize()
  383. }
  384. },
  385. exportHandler () {
  386. if (!this.hasChartData) {
  387. this.$message.warning('请查询后再进行导出')
  388. return
  389. }
  390. // const myCanvas = this.myChart._dom.querySelectorAll('canvas')[0]
  391. // const image = myCanvas.toDataURL('image/png')
  392. // const $a = document.createElement('a')
  393. // $a.setAttribute('href', image)
  394. // $a.setAttribute('download', `${this.chartsTitle}统计.png`)
  395. // $a.click()
  396. const xlsxDatas = [['时间', '位置', '分类', '数量', '占比']]
  397. xlsxDatas.push(
  398. ...this.tableData.map(element => [
  399. element['A'],
  400. element['location'],
  401. element[this.categoryKey],
  402. element[this.seriesKey],
  403. ((element[this.seriesKey] / this.totalCount.value) * 100).toFixed(2) + '%'
  404. ])
  405. )
  406. xlsxDatas.push(['合计', '', '', this.totalCount.value, ''])
  407. // 计算列宽
  408. const columnWidths = []
  409. xlsxDatas.forEach((row, rowIndex) => {
  410. // 计算每一列宽度,考虑换行
  411. row.forEach((cell, columnIndex) => {
  412. const cellWidth = Math.max(
  413. ...cell
  414. .toString()
  415. .split('\n')
  416. .map(cellRow =>
  417. cellRow.split('').reduce((pre, curr) => {
  418. const letterSize = curr.charCodeAt(0) > 255 ? 2 : 1
  419. return pre + letterSize
  420. }, 0)
  421. )
  422. )
  423. if ((!columnWidths[columnIndex] && cellWidth > 0) || cellWidth > columnWidths[columnIndex]) {
  424. columnWidths[columnIndex] = cellWidth
  425. }
  426. })
  427. })
  428. // 生成表格
  429. const sheet = XLSX.utils.aoa_to_sheet(xlsxDatas)
  430. // 添加列宽度
  431. sheet['!cols'] = columnWidths.map(width => ({
  432. wch: width + 2
  433. }))
  434. // 样式
  435. const borderStyle = {
  436. style: 'medium',
  437. color: {
  438. rgb: 'FFFFFF'
  439. }
  440. }
  441. const reg = /^[A-Z]+([\d]+$)/
  442. for (const key in sheet) {
  443. const match = reg.test(key)
  444. if (match) {
  445. const rowIndex = reg.exec(key)[1]
  446. let cellStyle = {
  447. alignment: {
  448. horizontal: 'center',
  449. vertical: 'center',
  450. wrapText: true
  451. }
  452. }
  453. if (Number(rowIndex) === 1) {
  454. cellStyle = {
  455. ...cellStyle,
  456. border: {
  457. top: borderStyle,
  458. right: borderStyle,
  459. bottom: borderStyle,
  460. left: borderStyle
  461. },
  462. font: {
  463. color: {
  464. rgb: 'FFFFFF'
  465. }
  466. },
  467. fill: {
  468. fgColor: {
  469. rgb: '3366FF'
  470. }
  471. }
  472. }
  473. } else {
  474. cellStyle.alignment.horizontal = 'left'
  475. }
  476. sheet[key].s = cellStyle
  477. }
  478. }
  479. // 表格数据转换
  480. const workBook = XLSX.utils.book_new()
  481. XLSX.utils.book_append_sheet(workBook, sheet, this.chartsTitle)
  482. const tableWrite = XLSX_STYLE.write(workBook, {
  483. bookType: 'xlsx',
  484. bookSST: true,
  485. type: 'buffer',
  486. cellStyles: true
  487. })
  488. // 下载表格
  489. const fileName = `${this.chartsTitle}统计-${this.params.join('-')}.xlsx`
  490. FileSaver.saveAs(new Blob([tableWrite], { type: 'application/octet-stream' }), fileName)
  491. }
  492. }
  493. }
  494. </script>
  495. <style lang="scss" scoped>
  496. .statistics-chart {
  497. width: 100%;
  498. }
  499. </style>