crossSetionData2.vue 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. <template>
  2. <!-- 柱状图容器 -->
  3. <div class="chart-page">
  4. <div ref="chartContainer" class="chart-container"></div>
  5. </div>
  6. </template>
  7. <script setup>
  8. import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
  9. import * as echarts from 'echarts'
  10. // 状态管理
  11. const chartContainer = ref(null)
  12. let chart = null
  13. const state = reactive({
  14. excelData: [], // 存储解析后的断面数据
  15. districtAvgData: [] // 存储按区县分组后的平均数据
  16. })
  17. // 从接口初始化数据(核心修改:异步获取 + 严格数据解析)
  18. const initData = async () => {
  19. try {
  20. // 接口地址(注意修正空格:localhost)
  21. const apiUrl = 'http://localhost:8000/api/vector/export/all?table_name=cross_section'
  22. const response = await fetch(apiUrl)
  23. if (!response.ok) {
  24. throw new Error(`接口请求失败(状态码:${response.status})`)
  25. }
  26. const geoJson = await response.json()
  27. // 逐行解析Feature,确保每个样本都被处理
  28. state.excelData = geoJson.features.map(feature => {
  29. const props = feature.properties || {}
  30. // 强制转换Cd浓度为数值(处理异常值)
  31. let cdValue = parseFloat(props.cd_concentration)
  32. if (isNaN(cdValue)) {
  33. console.warn('发现无效Cd浓度值,已设为0:', props)
  34. cdValue = 0 // 保证参与计算,避免数据缺失
  35. }
  36. return {
  37. id: props.id || '未知ID', // 兜底处理
  38. river: props.river_name || '未知河流',
  39. district: props.county || '未知区县',
  40. cdValue: cdValue
  41. }
  42. })
  43. calculateDistrictAvg() // 计算区县平均值
  44. } catch (err) {
  45. console.error('数据加载失败:', err)
  46. // 可扩展:全局错误提示
  47. }
  48. }
  49. // 按区县计算平均浓度(核心逻辑保持不变)
  50. const calculateDistrictAvg = () => {
  51. const districtGroups = {};
  52. // 1. 分组统计(总和 + 数量)
  53. state.excelData.forEach(item => {
  54. const district = item.district;
  55. if (!districtGroups[district]) {
  56. districtGroups[district] = { total: 0, count: 0 };
  57. }
  58. districtGroups[district].total += item.cdValue;
  59. districtGroups[district].count += 1;
  60. });
  61. // 2. 计算平均值 + 总平均
  62. const districtAvg = [];
  63. let totalAll = 0;
  64. let countAll = 0;
  65. for (const district in districtGroups) {
  66. const avg = districtGroups[district].total / districtGroups[district].count;
  67. districtAvg.push({
  68. district,
  69. avg: parseFloat(avg) // 保留3位小数
  70. });
  71. totalAll += districtGroups[district].total;
  72. countAll += districtGroups[district].count;
  73. }
  74. // 添加总平均值(最后一项)
  75. districtAvg.push({
  76. district: '总平均',
  77. avg: parseFloat((totalAll / countAll))
  78. });
  79. state.districtAvgData = districtAvg;
  80. updateChart(); // 更新图表
  81. }
  82. // 初始化图表
  83. const initChart = () => {
  84. if (chartContainer.value) {
  85. chart = echarts.init(chartContainer.value);
  86. updateChart();
  87. }
  88. }
  89. // 更新图表数据
  90. const updateChart = () => {
  91. if (!chart || state.districtAvgData.length === 0) return;
  92. // 准备图表数据
  93. const districts = state.districtAvgData.map(item => item.district); // x轴:区县名
  94. const avgs = state.districtAvgData.map(item => item.avg); // y轴:平均浓度
  95. // 图表配置
  96. const option = {
  97. tooltip: {
  98. trigger: 'axis',
  99. formatter: '{b}: {c} μg/L' // 悬停提示:区县名 + 浓度值
  100. },
  101. grid: {
  102. left: '5%',
  103. right: '5%',
  104. bottom: '15%', // 底部留空间,防止区县名重叠
  105. containLabel: true
  106. },
  107. xAxis: {
  108. type: 'category',
  109. data: districts,
  110. axisLabel: {
  111. interval: 0,
  112. fontSize: 15
  113. }
  114. },
  115. yAxis: {
  116. type: 'value',
  117. name: 'Cd浓度 (μg/L)',
  118. min: 0, // 从0开始更直观
  119. nameTextStyle:{
  120. fontSize:15,
  121. },
  122. axisLabel: {
  123. formatter: '{value}',
  124. fontSize: 15
  125. }
  126. },
  127. series: [
  128. {
  129. name: '平均浓度',
  130. type: 'bar',
  131. data: avgs,
  132. itemStyle: {
  133. // 为"总平均"设置不同颜色(最后一项)
  134. color: (params) => params.dataIndex === districts.length - 1 ? '#FF4500' : '#1E88E5'
  135. },
  136. label: {
  137. show: true, // 显示数值标签
  138. position: 'top',
  139. formatter: '{c}',
  140. fontSize: 15
  141. },
  142. barWidth: '60%'
  143. }
  144. ],
  145. graphic: [
  146. {
  147. type: 'rect',
  148. left: '5%', // 与 grid.left 对齐
  149. right: '5%', // 与 grid.right 对齐
  150. bottom: '5%',// 与 grid.bottom 对齐(位于绘图区域底部)
  151. height: 30, // 圆弧高度
  152. // 顶部左右圆角(半径 15,与高度 30 配合形成上凸圆弧)
  153. borderRadius: [15, 15, 0, 0],
  154. fill: '#FFFFFF', // 白色填充
  155. z: -1 // 层级低于图表元素(如柱子、坐标轴)
  156. }
  157. ]
  158. };
  159. chart.setOption(option);
  160. }
  161. // 生命周期管理
  162. onMounted(async () => {
  163. await initData(); // 先加载接口数据
  164. initChart(); // 再初始化图表
  165. // 监听窗口 resize,自动调整图表大小
  166. window.addEventListener('resize', () => chart && chart.resize());
  167. })
  168. onBeforeUnmount(() => {
  169. // 组件销毁时释放图表资源
  170. if (chart) chart.dispose();
  171. })
  172. </script>
  173. <style>
  174. .chart-page {
  175. width: 100%;
  176. margin: 0 auto 24px;
  177. background-color: white;
  178. border-radius: 12px;
  179. padding: 20px;
  180. box-sizing: border-box;
  181. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  182. }
  183. .chart-container {
  184. width: 100%;
  185. height: 500px;
  186. }
  187. </style>