index.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  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. },
  73. mounted () {
  74. const dragSelect = document.querySelector('#' + this.chatId)
  75. const resizeObserver = new ResizeObserver(entries => {
  76. if (this.chart) {
  77. this.chart.resize()
  78. let config = this.observeChart(entries)
  79. config = this.seriesStyle(config)
  80. config.option && this.chart.setOption(config.option)
  81. }
  82. })
  83. resizeObserver.observe(dragSelect)
  84. },
  85. beforeDestroy () {
  86. if (this.chart) {
  87. this.chart.dispose()
  88. }
  89. },
  90. methods: {
  91. ...mapMutations('bigScreen', ['changeChartConfig', 'changeActiveItemConfig', 'changeChartLoading']),
  92. chartInit () {
  93. let config = this.config
  94. // key和code相等,说明是一进来刷新,调用list接口
  95. if (this.config.code === this.config.key || this.isPreview) {
  96. // 改变样式
  97. config = this.changeStyle(config)
  98. // 改变数据
  99. config.loading = true
  100. this.changeChartLoading(config)
  101. this.changeDataByCode(config).then((res) => {
  102. // 初始化图表
  103. config.loading = false
  104. this.changeChartLoading(config)
  105. this.newChart(res)
  106. }).catch(() => {
  107. })
  108. } else {
  109. config.loading = true
  110. this.changeChartLoading(config)
  111. // 否则说明是更新,这里的更新只指更新数据(改变样式时是直接调取changeStyle方法),因为更新数据会改变key,调用chart接口
  112. this.changeData(config).then((res) => {
  113. config.loading = false
  114. this.changeChartLoading(config)
  115. // 初始化图表
  116. this.newChart(res)
  117. })
  118. }
  119. },
  120. /**
  121. * 构造chart
  122. */
  123. newChart (config) {
  124. const chartDom = document.getElementById(this.chatId)
  125. this.chart = echarts.init(chartDom)
  126. config.option && this.chart.setOption(config.option)
  127. },
  128. /**
  129. * 控制底部阴影大小
  130. */
  131. observeChart (entries) {
  132. const width = entries[0].contentRect.width
  133. const height = entries[0].contentRect.height
  134. const option = this.config.option
  135. // 调整长方形的大小
  136. option.graphic.children[0].shape.width = width * 0.9
  137. // 调整多边形的大小
  138. option.graphic.children[1].shape.points = [[width / 10, -height / 6], [width - width / 6, -height / 6], [width * 0.9, 0], [0, 0]]
  139. return this.config
  140. },
  141. /**
  142. * 注册事件
  143. */
  144. registerEvent () {
  145. // 图表添加事件进行数据联动
  146. let formData = {}
  147. // eslint-disable-next-line no-unused-vars
  148. this.chart.on('tooltip:change', (...args) => {
  149. formData = {}
  150. formData = cloneDeep(args[0].data.items[0].data)
  151. })
  152. // eslint-disable-next-line no-unused-vars
  153. this.chart.on('plot:click', (...args) => {
  154. this.linkage(formData)
  155. })
  156. },
  157. // 将config.setting的配置转化为option里的配置,这里之所以将转化的方法提出来,是因为在改变维度指标和样式的时候都需要转化
  158. transformSettingToOption (config, type) {
  159. let option = null
  160. config.setting.forEach(set => {
  161. if (set.optionField) {
  162. const optionField = set.optionField.split('.')
  163. option = config.option
  164. // 判断是不是关于x轴的相关配置,x轴叠加了两层坐标轴,如果是x轴相关配置则作用于xAxis[0]
  165. if (optionField[0] === 'xAxis') {
  166. optionField.forEach((field, index) => {
  167. if (index === 0) {
  168. option = option.xAxis[0]
  169. } else if (index === optionField.length - 1) {
  170. // 数据配置时,必须有值才更新
  171. if ((set.tabName === type && type === 'data' && set.value) || (set.tabName === type && type === 'custom')) {
  172. option[field] = set.value
  173. }
  174. } else {
  175. option = option[field]
  176. }
  177. })
  178. } else if (optionField[0] === 'series') {
  179. let changeObject = []
  180. let beforeChange = []
  181. // 如果要配置数据标签相关信息
  182. optionField.forEach((field, index) => {
  183. if (index === 0) {
  184. option = option[field]
  185. } else if (index === 1) {
  186. // 筛选出需要修改的series对象
  187. changeObject = option.filter(item => item.id.includes(field))
  188. beforeChange = [...changeObject]
  189. option = option.filter(item => !(item.id.includes(field)))
  190. } else if (index === optionField.length - 1) {
  191. if ((set.tabName === type && type === 'data' && set.value) || (set.tabName === type && type === 'custom')) {
  192. changeObject.map(item => {
  193. item[field] = set.value
  194. })
  195. }
  196. } else {
  197. const changeResult = []
  198. changeObject.forEach(item => {
  199. const result = { ...item[field] }
  200. changeResult.push(result)
  201. })
  202. changeObject = [...changeResult]
  203. }
  204. })
  205. // 合并修改后的series对象
  206. changeObject.forEach(
  207. (item, index) => {
  208. beforeChange[index].label = _.cloneDeep(item)
  209. option.push(beforeChange[index])
  210. }
  211. )
  212. } else if (optionField[0] === 'graphic') {
  213. // 配置底部阴影颜色
  214. option.graphic.children.forEach(item => {
  215. item.style.fill = set.value
  216. })
  217. } else {
  218. optionField.forEach((field, index) => {
  219. if (index === optionField.length - 1) {
  220. // 数据配置时,必须有值才更新
  221. if ((set.tabName === type && type === 'data' && set.value) || (set.tabName === type && type === 'custom')) {
  222. option[field] = set.value
  223. }
  224. } else {
  225. option = option[field]
  226. }
  227. })
  228. }
  229. }
  230. })
  231. return config
  232. },
  233. dataFormatting (config, data) {
  234. // config = this.config
  235. // 数据返回成功则赋值
  236. if (data.success) {
  237. data = data.data
  238. // 获取到后端返回的数据,有则赋值
  239. const option = config.option
  240. const setting = config.setting
  241. if (config.dataHandler) {
  242. try {
  243. // 此处函数处理data
  244. eval(config.dataHandler)
  245. } catch (e) {
  246. console.error(e)
  247. }
  248. }
  249. config.option = this.echartsOptionFormatting(config, data)
  250. config = this.transformSettingToOption(config, 'data')
  251. } else {
  252. // 数据返回失败则赋前端的模拟数据
  253. // config.option.data = this.plotList?.find(plot => plot.name === config.name)?.option?.data || config?.option?.data
  254. }
  255. config = this.seriesStyle(config)
  256. return config
  257. },
  258. getxDataAndYData (xField, yField, data) {
  259. let xData = []
  260. let yData = []
  261. // 获取所有x轴的分类
  262. data.forEach(item => {
  263. xData.push(item[xField])
  264. })
  265. xData = [...new Set(xData)]
  266. xData.forEach(x => {
  267. let max = 0
  268. data.forEach(item => {
  269. if (item[xField] === x) {
  270. max = item[yField] > max ? item[yField] : max
  271. }
  272. })
  273. yData.push(max)
  274. })
  275. return { xData, yData }
  276. },
  277. // 格式化echarts的配置
  278. echartsOptionFormatting (config, data) {
  279. const option = config.option
  280. // 分组字段
  281. const xField = config.setting.find(item => item.optionField === 'xField')?.value
  282. const yField = config.setting.find(item => item.optionField === 'yField')?.value
  283. // 判断是否存在分组
  284. const hasSeries = config.setting.find(item => item.optionField === 'seriesField' && item.value !== '')
  285. const { xData, yData } = this.getxDataAndYData(xField, yField, data)
  286. const maxY = Math.max(...yData) + Math.max(...yData) * 0.2
  287. // 生成阴影柱子的值
  288. const shadowData = Array.from({ length: xData.length }, () => maxY)
  289. option.xAxis = option.xAxis.map(item => {
  290. return {
  291. ...item,
  292. data: xData
  293. }
  294. })
  295. // 存在分组字段
  296. if (hasSeries) {
  297. const seriesField = config.setting.find(item => item.optionField === 'seriesField')?.value
  298. const seriesFieldList = [...new Set(data.map(item => item[seriesField]))]
  299. option.series = []
  300. const barWidth = option.seriesCustom.barWidth
  301. // 获取数据标签展示情况
  302. let labelShow = 0
  303. let labelPosition = 'inside'
  304. let labelColor = '#fff'
  305. let labelSize = 12
  306. config.setting.forEach(set => {
  307. if (set.field === 'series_barColor_label_show') {
  308. labelShow = set.value
  309. } else if (set.field === 'series_barColor_label_position') {
  310. labelPosition = set.value
  311. } else if (set.field === 'series_barColor_label_color') {
  312. labelColor = set.value
  313. } else if (set.field === 'series_barColor_label_fontSize') {
  314. labelSize = set.value
  315. }
  316. })
  317. // 偏移量数组
  318. const offsetArr = []
  319. let index = 0
  320. if (seriesFieldList.length % 2 === 0) {
  321. const length = seriesFieldList.length / 2
  322. for (let i = 0; i < length; i++) {
  323. const offsetX = (parseInt('10%') + parseInt('50%')) * (2 * i + 1)
  324. offsetArr.push(offsetX)
  325. offsetArr.unshift(-offsetX)
  326. }
  327. } else {
  328. const length = Math.ceil(seriesFieldList.length / 2)
  329. for (let i = 0; i < length; i++) {
  330. if (i === 0) {
  331. offsetArr.push(0)
  332. } else {
  333. const offsetX = (parseInt('20%') + parseInt('100%')) * i
  334. offsetArr.push(offsetX)
  335. offsetArr.unshift(-offsetX)
  336. }
  337. }
  338. }
  339. for (const seriesFieldItem of seriesFieldList) {
  340. const seriesData = (data.filter(item => item[seriesField] === seriesFieldItem))?.map(item => item[yField])
  341. const seriesItem = [
  342. {
  343. id: 'barTopColor' + seriesFieldItem,
  344. type: 'pictorialBar',
  345. tooltip: { show: false },
  346. symbol: 'diamond',
  347. symbolSize: [barWidth, barWidth / 2],
  348. symbolOffset: [offsetArr[index] + '%', -barWidth / 4],
  349. symbolPosition: 'end',
  350. z: 15,
  351. zlevel: 2,
  352. color: '#ffff33',
  353. data: seriesData
  354. },
  355. {
  356. id: 'barColor' + seriesFieldItem,
  357. type: 'bar',
  358. barGap: '20%',
  359. barWidth: barWidth,
  360. color: '#115ba6',
  361. label: {
  362. fontStyle: 'normal',
  363. show: labelShow,
  364. position: labelPosition,
  365. color: labelColor,
  366. fontSize: labelSize
  367. },
  368. zlevel: 2,
  369. z: 12,
  370. data: seriesData
  371. },
  372. {
  373. id: 'barBottomColor' + seriesFieldItem,
  374. type: 'pictorialBar',
  375. tooltip: { show: false },
  376. symbol: 'diamond',
  377. symbolSize: [barWidth, barWidth / 2],
  378. symbolOffset: [offsetArr[index] + '%', barWidth / 4],
  379. zlevel: 2,
  380. z: 15,
  381. color: 'rgb(2, 192, 255)',
  382. data: seriesData
  383. },
  384. {
  385. id: 'shadowColor' + seriesFieldItem,
  386. type: 'bar',
  387. tooltip: { show: false },
  388. xAxisIndex: 1,
  389. barGap: '20%',
  390. data: shadowData,
  391. zlevel: 1,
  392. barWidth: barWidth,
  393. color: 'rgba(9, 44, 76,.8)'
  394. },
  395. {
  396. id: 'shadowTopColor' + seriesFieldItem,
  397. type: 'pictorialBar',
  398. tooltip: { show: false },
  399. symbol: 'diamond',
  400. symbolSize: [barWidth, barWidth / 2],
  401. symbolOffset: [offsetArr[index] + '%', -barWidth / 4],
  402. symbolPosition: 'end',
  403. z: 15,
  404. color: 'rgb(15, 69, 133)',
  405. zlevel: 1,
  406. data: shadowData
  407. }
  408. ]
  409. index++
  410. option.series.push(...seriesItem)
  411. }
  412. } else {
  413. // 没有分组,不需要修改配置,直接修改series中每个对象的series
  414. option.series.forEach(item => {
  415. if (item.id.includes('shadow')) {
  416. item.data = shadowData
  417. } else {
  418. item.data = yData
  419. }
  420. })
  421. }
  422. return option
  423. },
  424. // 对series里面的样式进行配置
  425. seriesStyle (config) {
  426. const _config = CloneDeep(config)
  427. const seriesCustom = _config.option.seriesCustom
  428. const ids = Object.keys(config.option.seriesCustom)
  429. // 判断是否是分组柱状图
  430. const isGroup = _config.option.series.length !== 5
  431. // 宽度配置
  432. _config.option.series.forEach(item => {
  433. if (item.type === 'pictorialBar') {
  434. item.symbolSize = [seriesCustom.barWidth, seriesCustom.barWidth / 2]
  435. } else if (item.type === 'bar') {
  436. item.barWidth = seriesCustom.barWidth
  437. }
  438. })
  439. // 如果是基础柱状图
  440. if (!isGroup) {
  441. _config.option.series.forEach(item => {
  442. // 配置颜色
  443. if (ids.includes(item.id)) {
  444. item.color = seriesCustom[item.id]
  445. }
  446. })
  447. } else {
  448. // 如果是分组柱状图
  449. ids.forEach(id => {
  450. if (id !== 'barWidth') {
  451. let index = 0
  452. _config.option.series.forEach(item => {
  453. if (item.id.includes(id)) {
  454. item.color = _config.option.seriesCustom[id][index]
  455. index++
  456. }
  457. })
  458. }
  459. })
  460. }
  461. return _config
  462. },
  463. // 组件的样式改变,返回改变后的config
  464. changeStyle (config, isUpdateTheme) {
  465. config = { ...this.config, ...config }
  466. config = this.transformSettingToOption(config, 'custom')
  467. // 这里定义了option和setting是为了保证在执行eval时,optionHandler、dataHandler里面可能会用到,
  468. const option = config.option
  469. const setting = config.setting
  470. if (this.config.optionHandler) {
  471. try {
  472. // 此处函数处理config
  473. eval(this.config.optionHandler)
  474. } catch (e) {
  475. console.error(e)
  476. }
  477. }
  478. // 此时,setting中的部分变量映射到了option.seriesCustom中,但未映射到series的具体配置中
  479. config = this.seriesStyle(config)
  480. // 只有样式改变时更新主题配置,切换主题时不需要保存
  481. if (!isUpdateTheme) {
  482. config.theme = settingToTheme(_.cloneDeep(config), this.customTheme)
  483. }
  484. this.changeChartConfig(config)
  485. if (config.code === this.activeCode) {
  486. this.changeActiveItemConfig(config)
  487. }
  488. if (this.chart) {
  489. this.chart.setOption(config.option)
  490. }
  491. return config
  492. }
  493. }
  494. }
  495. </script>
  496. <style lang="scss" scoped>
  497. @import '../assets/style/echartStyle';
  498. .light-theme{
  499. background-color: #FFFFFF;
  500. color: #000000;
  501. }
  502. .auto-theme{
  503. background-color: transparent;
  504. }
  505. </style>