index.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588
  1. <template>
  2. <div
  3. v-loading="config.loading"
  4. element-loading-text="图表加载中"
  5. :element-loading-background="loadingBackground"
  6. style="width: 100%;height: 100%"
  7. class="bs-design-wrap bs-custom-component"
  8. :class="{'light-theme':customTheme === 'light','auto-theme':customTheme !=='light'}"
  9. >
  10. <div
  11. :id="chatId"
  12. style="width: 100%;height: 100%"
  13. />
  14. </div>
  15. </template>
  16. <script>
  17. import 'insert-css'
  18. import cloneDeep from 'lodash/cloneDeep'
  19. import linkageMixins from 'data-room-ui/js/mixins/linkageMixins'
  20. import commonMixins from 'data-room-ui/js/mixins/commonMixins'
  21. import { mapState, mapMutations } from 'vuex'
  22. import { settingToTheme } from 'data-room-ui/js/utils/themeFormatting'
  23. import _ from 'lodash'
  24. import * as echarts from 'echarts'
  25. import CloneDeep from 'lodash-es/cloneDeep'
  26. export default {
  27. name: 'EchartsCustomComponent',
  28. mixins: [commonMixins, linkageMixins],
  29. props: {
  30. config: {
  31. type: Object,
  32. default: () => ({})
  33. }
  34. },
  35. data () {
  36. return {
  37. chart: null,
  38. hasData: false
  39. }
  40. },
  41. computed: {
  42. ...mapState('bigScreen', {
  43. pageInfo: state => state.pageInfo,
  44. customTheme: state => state.pageInfo.pageConfig.customTheme,
  45. activeCode: state => state.activeCode
  46. }),
  47. chatId () {
  48. let prefix = 'chart_'
  49. if (this.$route.path === window?.BS_CONFIG?.routers?.previewUrl) {
  50. prefix = 'preview_chart_'
  51. }
  52. if (this.$route.path === window?.BS_CONFIG?.routers?.designUrl) {
  53. prefix = 'design_chart_'
  54. }
  55. if (this.$route.path === window?.BS_CONFIG?.routers?.pageListUrl) {
  56. prefix = 'management_chart_'
  57. }
  58. return prefix + this.config.code
  59. }
  60. },
  61. created () {
  62. },
  63. watch: {
  64. // 监听主题变化手动触发组件配置更新
  65. 'config.option.theme': {
  66. handler (val) {
  67. if (val) {
  68. this.changeStyle(this.config, true)
  69. }
  70. }
  71. },
  72. 'config.w': {
  73. handler (val) {
  74. if (val) {
  75. // console.log('this.config',this.config);
  76. const chartDom = document.getElementById(this.chatId)
  77. this.observeChart(chartDom, this.chart, this.config.option)
  78. }
  79. }
  80. },
  81. 'config.h': {
  82. handler (val) {
  83. if (val) {
  84. const chartDom = document.getElementById(this.chatId)
  85. this.observeChart(chartDom, this.chart, this.config.option)
  86. }
  87. }
  88. }
  89. },
  90. mounted () {
  91. },
  92. beforeDestroy () {
  93. if (this.chart) {
  94. this.chart.dispose()
  95. }
  96. },
  97. methods: {
  98. ...mapMutations('bigScreen', ['changeChartConfig', 'changeActiveItemConfig', 'changeChartLoading']),
  99. chartInit () {
  100. let config = this.config
  101. // key和code相等,说明是一进来刷新,调用list接口
  102. if (this.config.code === this.config.key || this.isPreview) {
  103. // 改变样式
  104. config = this.changeStyle(config)
  105. // 改变数据
  106. config.loading = true
  107. this.changeChartLoading(config)
  108. this.changeDataByCode(config).then((res) => {
  109. // 初始化图表
  110. config.loading = false
  111. this.changeChartLoading(config)
  112. this.newChart(res)
  113. }).catch(() => {
  114. })
  115. } else {
  116. config.loading = true
  117. this.changeChartLoading(config)
  118. // 否则说明是更新,这里的更新只指更新数据(改变样式时是直接调取changeStyle方法),因为更新数据会改变key,调用chart接口
  119. this.changeData(config).then((res) => {
  120. config.loading = false
  121. this.changeChartLoading(config)
  122. // 初始化图表
  123. this.newChart(res)
  124. })
  125. }
  126. },
  127. /**
  128. * 构造chart
  129. */
  130. newChart (config) {
  131. const chartDom = document.getElementById(this.chatId)
  132. this.chart = echarts.init(chartDom)
  133. config.option && this.chart.setOption(config.option)
  134. },
  135. /**
  136. * 控制底部阴影大小
  137. */
  138. observeChart (container, myChart, option) {
  139. const resizeObserver = new ResizeObserver(entries => {
  140. myChart.resize()
  141. // entries[0].contentRect.width:此时监测的盒子的宽度
  142. // entries[0].contentRect.height:此时监测的盒子的高度
  143. const width = entries[0].contentRect.width
  144. const height = entries[0].contentRect.height
  145. // 调整长方形的大小
  146. option.graphic.children[0].shape.width = width * 0.9
  147. // 调整多边形的大小
  148. option.graphic.children[1].shape.points = [
  149. [width / 10, -height / 6],
  150. [width - width / 6, -height / 6],
  151. [width * 0.9, 0],
  152. [0, 0]
  153. ]
  154. myChart.setOption(option)
  155. })
  156. resizeObserver.observe(container)
  157. },
  158. /**
  159. * 注册事件
  160. */
  161. registerEvent () {
  162. // 图表添加事件进行数据联动
  163. let formData = {}
  164. // eslint-disable-next-line no-unused-vars
  165. this.chart.on('tooltip:change', (...args) => {
  166. formData = {}
  167. formData = cloneDeep(args[0].data.items[0].data)
  168. })
  169. // eslint-disable-next-line no-unused-vars
  170. this.chart.on('plot:click', (...args) => {
  171. this.linkage(formData)
  172. })
  173. },
  174. // 将config.setting的配置转化为option里的配置,这里之所以将转化的方法提出来,是因为在改变维度指标和样式的时候都需要转化
  175. transformSettingToOption (config, type) {
  176. let option = null
  177. config.setting.forEach(set => {
  178. if (set.optionField) {
  179. const optionField = set.optionField.split('.')
  180. option = config.option
  181. // 判断是不是关于x轴的相关配置,x轴叠加了两层坐标轴,如果是x轴相关配置则作用于xAxis[0]
  182. if (optionField[0] === 'xAxis') {
  183. optionField.forEach((field, index) => {
  184. if (index === 0) {
  185. option = option.xAxis[0]
  186. } else if (index === optionField.length - 1) {
  187. // 数据配置时,必须有值才更新
  188. if ((set.tabName === type && type === 'data' && set.value) || (set.tabName === type && type === 'custom')) {
  189. option[field] = set.value
  190. }
  191. } else {
  192. option = option[field]
  193. }
  194. })
  195. } else {
  196. optionField.forEach((field, index) => {
  197. if (index === optionField.length - 1) {
  198. // 数据配置时,必须有值才更新
  199. if ((set.tabName === type && type === 'data' && set.value) || (set.tabName === type && type === 'custom')) {
  200. option[field] = set.value
  201. }
  202. } else {
  203. option = option[field]
  204. }
  205. })
  206. }
  207. }
  208. })
  209. config.option = { ...config.option, ...option }
  210. return config
  211. },
  212. dataFormatting (config, data) {
  213. console.log('dataFormatting')
  214. // 数据返回成功则赋值
  215. if (data.success) {
  216. data = data.data
  217. config = this.transformSettingToOption(config, 'data')
  218. // 获取到后端返回的数据,有则赋值
  219. const option = config.option
  220. const setting = config.setting
  221. if (config.dataHandler) {
  222. try {
  223. // 此处函数处理data
  224. eval(config.dataHandler)
  225. } catch (e) {
  226. console.error(e)
  227. }
  228. }
  229. config.option = this.echartsOptionFormatting(config, data)
  230. } else {
  231. // 数据返回失败则赋前端的模拟数据
  232. // config.option.data = this.plotList?.find(plot => plot.name === config.name)?.option?.data || config?.option?.data
  233. }
  234. return config
  235. },
  236. getxDataAndYData (xField, yField, data, hasSeries) {
  237. let list = []
  238. let xData = []
  239. let yData = []
  240. const uniqueData = {}
  241. // 遍历原始数据数组
  242. data.forEach((item) => {
  243. // 使用城市名称作为键,覆盖旧数据,始终保留最后一条数据
  244. uniqueData[item[xField]] = item
  245. })
  246. // 将唯一数据对象的值(即去重后的数据)转换回数组
  247. list = Object.values(uniqueData)
  248. xData = list.map(item => item[xField])
  249. yData = list.map(item => item[yField])
  250. return { xData, yData }
  251. },
  252. // 格式化echarts的配置
  253. echartsOptionFormatting (config, data) {
  254. console.log('echartsOptionFormatting');
  255. const option = config.option
  256. // 分组字段
  257. const xField = config.setting.find(item => item.optionField === 'xField')?.value
  258. const yField = config.setting.find(item => item.optionField === 'yField')?.value
  259. const hasSeries = config.setting.find(item => item.optionField === 'seriesField' && item.value !== '')
  260. const { xData, yData } = this.getxDataAndYData(xField, yField, data, hasSeries)
  261. // const xData = [...new Set(data.map(item => item[xField]))]
  262. // const yData = data.map(item => item[yField])
  263. const maxY = Math.max(...yData) + Math.max(...yData) * 0.2
  264. // 生成阴影柱子的值
  265. const shadowData = Array.from({ length: xData.length }, () => maxY)
  266. option.xAxis = option.xAxis.map(item => {
  267. return {
  268. ...item,
  269. data: xData
  270. }
  271. })
  272. // 判断是否存在分组字段
  273. if (hasSeries) {
  274. const seriesField = config.setting.find(item => item.optionField === 'seriesField')?.value
  275. const seriesFieldList = [...new Set(data.map(item => item[seriesField]))]
  276. option.series = []
  277. const offsetArr = []
  278. let index = 0
  279. let barWidth = 10
  280. if (seriesFieldList.length % 2 === 0) {
  281. const length = seriesFieldList.length / 2
  282. for (let i = 0; i < length; i++) {
  283. const offsetX = (parseInt('10%') + parseInt('50%')) * (2 * i + 1)
  284. offsetArr.push(offsetX)
  285. offsetArr.unshift(-offsetX)
  286. }
  287. } else {
  288. const length = Math.ceil(seriesFieldList.length / 2)
  289. for (let i = 0; i < length; i++) {
  290. if (i === 0) {
  291. offsetArr.push(0)
  292. } else {
  293. const offsetX = (parseInt('20%') + parseInt('100%')) * i
  294. offsetArr.push(offsetX)
  295. offsetArr.unshift(-offsetX)
  296. }
  297. }
  298. }
  299. for (const seriesFieldItem of seriesFieldList) {
  300. const seriesData = (data.filter(item => item[seriesField] === seriesFieldItem))?.map(item => item[yField])
  301. const seriesItem = [
  302. {
  303. name: seriesFieldItem + '柱子顶部',
  304. type: 'pictorialBar',
  305. tooltip: { show: false },
  306. symbol: 'diamond',
  307. symbolSize: [barWidth, barWidth / 2],
  308. symbolOffset: [offsetArr[index] + '%', -barWidth / 4],
  309. symbolPosition: 'end',
  310. z: 15,
  311. zlevel: 2,
  312. color: 'rgba(2, 175, 249,1)',
  313. data: seriesData
  314. },
  315. {
  316. name: seriesFieldItem,
  317. type: 'bar',
  318. barGap: '20%',
  319. barWidth: barWidth,
  320. itemStyle: {
  321. normal: {
  322. color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  323. {
  324. offset: 0,
  325. color: '#115ba6'
  326. },
  327. {
  328. offset: 1,
  329. color: '#1db0dd'
  330. }
  331. ]),
  332. opacity: 0.8,
  333. shadowColor: 'rgba(0, 0, 0, 0.5)', // 阴影颜色
  334. shadowBlur: 0 // 阴影模糊值
  335. }
  336. },
  337. label: {
  338. show: false
  339. },
  340. zlevel: 2,
  341. z: 12,
  342. data: seriesData
  343. },
  344. {
  345. name: seriesFieldItem + '柱子底部',
  346. type: 'pictorialBar',
  347. tooltip: { show: false },
  348. symbol: 'diamond',
  349. symbolSize: [barWidth, barWidth / 2],
  350. symbolOffset: [offsetArr[index] + '%', barWidth / 4],
  351. zlevel: 2,
  352. z: 15,
  353. color: 'rgb(2, 192, 255)',
  354. data: seriesData
  355. },
  356. {
  357. name: seriesFieldItem + '背景柱子',
  358. type: 'bar',
  359. tooltip: { show: false },
  360. xAxisIndex: 1,
  361. barGap: '20%',
  362. data: shadowData,
  363. zlevel: 1,
  364. barWidth: barWidth,
  365. itemStyle: {
  366. normal: {
  367. color: 'rgba(9, 44, 76,.8)'
  368. }
  369. }
  370. },
  371. {
  372. name: seriesFieldItem + '背景柱子顶部',
  373. type: 'pictorialBar',
  374. tooltip: { show: false },
  375. symbol: 'diamond',
  376. symbolSize: [barWidth, barWidth / 2],
  377. symbolOffset: [offsetArr[index] + '%', -barWidth / 4],
  378. symbolPosition: 'end',
  379. z: 15,
  380. color: 'rgb(15, 69, 133)',
  381. zlevel: 1,
  382. data: shadowData
  383. }
  384. ]
  385. index++
  386. option.series.push(...seriesItem)
  387. }
  388. } else {
  389. option.series = [
  390. {
  391. id: 'barTopColor', // 用于区分是图表的什么部分
  392. type: 'pictorialBar', // 象形柱图
  393. symbol: 'diamond',
  394. symbolOffset: [0, '-50%'], // 上部菱形
  395. symbolSize: [30, 15],
  396. // symbolOffset: [0, -6], // 上部椭圆
  397. symbolPosition: 'end',
  398. z: 12,
  399. label: {
  400. normal: {
  401. show: true,
  402. position: 'top',
  403. fontSize: 15,
  404. fontWeight: 'bold',
  405. color: '#27a7ce'
  406. }
  407. },
  408. color: option.seriesCustom.barTopColor,
  409. data: yData
  410. },
  411. {
  412. id: 'barBottomColor', // 用于区分是图表的什么部分
  413. type: 'pictorialBar',
  414. symbol: 'diamond',
  415. symbolSize: [30, 15],
  416. symbolOffset: ['0%', '50%'], // 下部菱形
  417. // symbolOffset: [0, 7], // 下部椭圆
  418. z: 12,
  419. color: option.seriesCustom.barBottomColor,
  420. data: yData
  421. },
  422. {
  423. id: 'barColor', // 用于区分是图表的什么部分
  424. type: 'bar',
  425. barWidth: 30,
  426. z: 10,
  427. itemStyle: {
  428. normal: {
  429. color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  430. {
  431. offset: 0,
  432. color: option.seriesCustom.barColor1
  433. },
  434. {
  435. offset: 1,
  436. color: option.seriesCustom.barColor2
  437. }
  438. ]),
  439. opacity: 0.8,
  440. shadowColor: 'rgba(0, 0, 0, 0.5)', // 阴影颜色
  441. shadowBlur: 0 // 阴影模糊值
  442. }
  443. },
  444. data: yData
  445. },
  446. {
  447. id: 'shadowColor', // 用于区分是图表的什么部分
  448. type: 'bar',
  449. barWidth: 30,
  450. xAxisIndex: 1,
  451. itemStyle: {
  452. normal: {
  453. color: option.seriesCustom.shadowColor,
  454. opacity: 0.8,
  455. shadowColor: 'rgba(0, 0, 0, 0.5)', // 阴影颜色
  456. shadowBlur: 0 // 阴影模糊值
  457. }
  458. },
  459. label: {
  460. show: false
  461. },
  462. tooltip: {
  463. show: false
  464. },
  465. data: shadowData
  466. },
  467. {
  468. id: 'shadowTopColor', // 用于区分是图表的什么部分
  469. type: 'pictorialBar', // 象形柱图
  470. xAxisIndex: 1,
  471. symbol: 'diamond',
  472. symbolOffset: [0, '-50%'], // 上部菱形
  473. symbolSize: [30, 15],
  474. // symbolOffset: [0, -6], // 上部椭圆
  475. symbolPosition: 'end',
  476. z: 12,
  477. label: {
  478. normal: {
  479. show: false,
  480. position: 'top',
  481. fontSize: 15,
  482. fontWeight: 'bold',
  483. color: '#27a7ce'
  484. }
  485. },
  486. color: option.seriesCustom.shadowTopColor,
  487. tooltip: {
  488. show: false
  489. },
  490. data: shadowData
  491. }
  492. ]
  493. }
  494. return option
  495. },
  496. // 对series里面的样式进行配置
  497. seriesStyle (config) {
  498. const _config = CloneDeep(config)
  499. const seriesCustom = _config.option.seriesCustom
  500. const ids = Object.keys(config.option.seriesCustom)
  501. // const ids = ['barTopColor', 'barBottomColor', 'shadowColor', 'shadowTopColor']
  502. const hasSeries = _config.setting.find(item => item.optionField === 'seriesField' && item.value !== '')
  503. // 如果是基础柱状图
  504. if (!hasSeries) {
  505. _config.option.series.forEach(item => {
  506. // 配置颜色
  507. if (ids.includes(item.id)) {
  508. item.color = seriesCustom[item.id]
  509. }
  510. // 配置宽度
  511. if (item.type === 'pictorialBar') {
  512. item.symbolSize = [seriesCustom.barWidth, seriesCustom.barWidth / 2]
  513. } else if (item.type === 'bar') {
  514. item.barWidth = seriesCustom.barWidth
  515. }
  516. })
  517. }
  518. _config.option.series.forEach((item) => {
  519. if (ids.includes(item.id)) {
  520. item.color = _config.option.seriesCustom[item.id]
  521. } else {
  522. // item.itemStyle.normal.color = new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  523. // {
  524. // offset: 0,
  525. // color: _config.option.seriesCustom.barColor1
  526. // },
  527. // {
  528. // offset: 1,
  529. // color: _config.option.seriesCustom.barColor2
  530. // }
  531. // ])
  532. }
  533. })
  534. return _config
  535. },
  536. // 组件的样式改变,返回改变后的config
  537. changeStyle (config, isUpdateTheme) {
  538. console.log('changeStyle');
  539. config = { ...this.config, ...config }
  540. config = this.transformSettingToOption(config, 'custom')
  541. // 这里定义了option和setting是为了保证在执行eval时,optionHandler、dataHandler里面可能会用到,
  542. const option = config.option
  543. const setting = config.setting
  544. if (this.config.optionHandler) {
  545. try {
  546. // 此处函数处理config
  547. eval(this.config.optionHandler)
  548. } catch (e) {
  549. console.error(e)
  550. }
  551. }
  552. // 此时,setting中的部分变量映射到了option.seriesCustom中,但未映射到series的具体配置中
  553. config = this.seriesStyle(config)
  554. // 只有样式改变时更新主题配置,切换主题时不需要保存
  555. if (!isUpdateTheme) {
  556. config.theme = settingToTheme(_.cloneDeep(config), this.customTheme)
  557. }
  558. this.changeChartConfig(config)
  559. if (config.code === this.activeCode) {
  560. this.changeActiveItemConfig(config)
  561. }
  562. if (this.chart) {
  563. this.chart.setOption(config.option)
  564. }
  565. return config
  566. }
  567. }
  568. }
  569. </script>
  570. <style lang="scss" scoped>
  571. @import '../assets/style/echartStyle';
  572. .light-theme{
  573. background-color: #FFFFFF;
  574. color: #000000;
  575. }
  576. .auto-theme{
  577. background-color: transparent;
  578. }
  579. </style>