GroupAreaCompareBarChart.vue 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. <template>
  2. <div ref="chartRef" class="group-area-compare-bar-chart" :style="rootStyle"></div>
  3. </template>
  4. <script setup>
  5. import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue';
  6. import * as echarts from 'echarts';
  7. import { defineProps, defineEmits } from 'vue';
  8. import { useEnterpriseStore } from '@/store/enterprise';
  9. import { useFilterStore } from '@/store/filter';
  10. import { GROUP_ORDER, GROUP_DISPLAY_NAMES, normalizeGroupName } from '@/utils/aggregateReportRecords';
  11. import { toWanSquareMeters } from '@/utils/areaMath';
  12. const props = defineProps({
  13. clickable: {
  14. type: Boolean,
  15. default: false,
  16. },
  17. activeGroup: {
  18. type: String,
  19. default: '',
  20. },
  21. height: {
  22. type: [Number, String],
  23. default: 300,
  24. },
  25. });
  26. const emit = defineEmits(['group-click']);
  27. const SERIES_CONFIG = [
  28. { name: '实际建筑面积', color: 'rgba(1, 118, 255, 1)', dimColor: 'rgba(1, 118, 255, 0.22)' },
  29. { name: '已使用面积', color: 'rgba(255, 121, 98, 1)', dimColor: 'rgba(255, 121, 98, 0.22)' },
  30. ];
  31. const rootStyle = computed(() => ({
  32. width: '100%',
  33. height: typeof props.height === 'number' ? `${props.height}px` : props.height,
  34. }));
  35. const enterpriseStore = useEnterpriseStore();
  36. const filterStore = useFilterStore();
  37. const chartRef = ref(null);
  38. let chartInstance = null;
  39. const filteredList = computed(() =>
  40. enterpriseStore.filteredByGroup('全部', filterStore.selectDate)
  41. );
  42. const getUsedAreaSqm = (item) =>
  43. Number(item.c_zymj || 0) + Number(item.c_cjmj || 0) + Number(item.c_czmj || 0);
  44. const resolveGroupIndex = (item) => {
  45. const raw = normalizeGroupName(item.c_ssqpjt || item.c_ssjt || '');
  46. return GROUP_ORDER.indexOf(raw);
  47. };
  48. const chartData = computed(() => {
  49. const buildingSqm = GROUP_ORDER.map(() => 0);
  50. const usedSqm = GROUP_ORDER.map(() => 0);
  51. filteredList.value.forEach((item) => {
  52. const groupIndex = resolveGroupIndex(item);
  53. if (groupIndex < 0) return;
  54. buildingSqm[groupIndex] += Number(item.c_sjjzmj || 0);
  55. usedSqm[groupIndex] += getUsedAreaSqm(item);
  56. });
  57. return {
  58. xAxisData: GROUP_ORDER.map((group) => GROUP_DISPLAY_NAMES[group] ?? group),
  59. series: [
  60. buildingSqm.map((val) => toWanSquareMeters(val)),
  61. usedSqm.map((val) => toWanSquareMeters(val)),
  62. ],
  63. };
  64. });
  65. const buildBarData = (values, seriesIndex) => {
  66. const series = SERIES_CONFIG[seriesIndex];
  67. return values.map((val, dataIndex) => {
  68. const category = chartData.value.xAxisData[dataIndex];
  69. const isActive = props.activeGroup && category === props.activeGroup;
  70. const isDimmed = props.activeGroup && !isActive;
  71. return {
  72. value: val,
  73. itemStyle: {
  74. color: isDimmed ? series.dimColor : series.color,
  75. borderRadius: [4, 4, 0, 0],
  76. ...(isActive
  77. ? { shadowBlur: 8, shadowColor: 'rgba(0, 117, 255, 0.35)' }
  78. : {}),
  79. },
  80. };
  81. });
  82. };
  83. const buildOption = () => ({
  84. legend: {
  85. data: SERIES_CONFIG.map((item) => item.name),
  86. top: 0,
  87. right: 0,
  88. itemWidth: 12,
  89. itemHeight: 12,
  90. textStyle: {
  91. fontSize: 14,
  92. color: 'rgba(102, 112, 133, 1)',
  93. fontFamily: 'PingFangSC-Regular',
  94. },
  95. },
  96. tooltip: {
  97. trigger: 'axis',
  98. axisPointer: { type: 'shadow' },
  99. formatter(params) {
  100. const lines = [`${params[0]?.axisValue ?? ''}`];
  101. params.forEach((item) => {
  102. lines.push(`${item.marker}${item.seriesName}: ${item.value} 万m²`);
  103. });
  104. return lines.join('<br/>');
  105. },
  106. },
  107. grid: {
  108. left: 0,
  109. right: 0,
  110. top: 36,
  111. bottom: 0,
  112. containLabel: true,
  113. },
  114. xAxis: {
  115. type: 'category',
  116. boundaryGap: true,
  117. axisTick: {
  118. show: false,
  119. alignWithLabel: true,
  120. },
  121. axisLine: {
  122. show: true,
  123. lineStyle: {
  124. width: 1,
  125. color: 'rgba(51, 71, 230, 1)',
  126. type: 'solid',
  127. },
  128. },
  129. data: chartData.value.xAxisData,
  130. },
  131. yAxis: {
  132. type: 'value',
  133. axisLabel: {
  134. show: true,
  135. color: 'rgba(102, 112, 133, 1)',
  136. fontSize: 14,
  137. fontWeight: 400,
  138. fontFamily: 'PingFangSC-Regular',
  139. },
  140. axisLine: { show: false },
  141. splitLine: { show: false },
  142. },
  143. series: SERIES_CONFIG.map((item, index) => ({
  144. name: item.name,
  145. type: 'bar',
  146. color: item.color,
  147. barWidth: 24,
  148. barGap: '15%',
  149. barCategoryGap: '28%',
  150. data: buildBarData(chartData.value.series[index], index),
  151. cursor: props.clickable ? 'pointer' : 'default',
  152. emphasis: props.clickable
  153. ? {
  154. itemStyle: {
  155. shadowBlur: 8,
  156. shadowColor: 'rgba(0, 117, 255, 0.35)',
  157. },
  158. }
  159. : undefined,
  160. })),
  161. });
  162. const renderChart = async () => {
  163. await nextTick();
  164. if (!chartRef.value) return;
  165. if (!chartInstance) {
  166. chartInstance = echarts.init(chartRef.value);
  167. }
  168. chartInstance.setOption(buildOption(), true);
  169. bindChartClick();
  170. };
  171. const bindChartClick = () => {
  172. if (!chartInstance || !props.clickable) return;
  173. chartInstance.off('click');
  174. chartInstance.on('click', (params) => {
  175. if (params?.seriesType !== 'bar' || params.dataIndex == null) return;
  176. const name = chartData.value.xAxisData[params.dataIndex];
  177. if (name) emit('group-click', name);
  178. });
  179. };
  180. const resizeChart = () => {
  181. chartInstance?.resize();
  182. };
  183. watch(chartData, () => {
  184. renderChart();
  185. });
  186. watch(
  187. () => [props.clickable, props.activeGroup],
  188. () => {
  189. renderChart();
  190. }
  191. );
  192. onMounted(() => {
  193. renderChart();
  194. window.addEventListener('resize', resizeChart);
  195. });
  196. onBeforeUnmount(() => {
  197. window.removeEventListener('resize', resizeChart);
  198. chartInstance?.dispose();
  199. });
  200. </script>