crossSetionData1.vue 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. <template>
  2. <div class="chart-page">
  3. <!-- 加载状态 -->
  4. <div v-if="loading" class="loading-indicator">
  5. <div class="spinner"></div>
  6. <p>数据加载中...</p>
  7. </div>
  8. <!-- 错误提示 -->
  9. <div v-else-if="error" class="error-message">
  10. <i class="fa fa-exclamation-circle"></i> {{ error }}
  11. </div>
  12. <!-- 图表容器 -->
  13. <div v-else ref="chartContainer" class="chart-container"></div>
  14. </div>
  15. </template>
  16. <!--按河流划分计算的柱状图-->
  17. <script setup>
  18. import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
  19. import { wgs84togcj02 } from 'coordtransform';
  20. import * as echarts from 'echarts'
  21. // 状态管理
  22. const error = ref(null)
  23. const loading = ref(true) // 新增加载状态
  24. const chartContainer = ref(null)
  25. let chart = null
  26. const state = reactive({
  27. excelData: [], // 存储解析后的断面数据
  28. riverAvgData: [] // 存储按河流分组后的平均数据
  29. })
  30. // 从接口获取数据并初始化
  31. const initData = async () => {
  32. try {
  33. // 接口地址
  34. const apiUrl = 'http://localhost:8000/api/vector/export/all?table_name=cross_section'
  35. // 发起接口请求
  36. const response = await fetch(apiUrl)
  37. if (!response.ok) {
  38. throw new Error(`接口请求失败:HTTP ${response.status}`)
  39. }
  40. // 解析GeoJSON数据
  41. const geoJsonData = await response.json()
  42. // 处理每个Feature的properties
  43. state.excelData = geoJsonData.features
  44. .map(feature => {
  45. const props = feature.properties
  46. // 处理经纬度(保持原有坐标转换逻辑)
  47. const lng = Number(props.longitude)
  48. const lat = Number(props.latitude)
  49. if (isNaN(lat) || isNaN(lng)) {
  50. console.error('无效经纬度数据:', props)
  51. return null
  52. }
  53. // WGS84转GCJ02坐标
  54. const [gcjLng, gcjLat] = wgs84togcj02(lng, lat)
  55. return {
  56. id: props.id,
  57. river: props.river_name || '未知河流',
  58. location: props.position || '未知位置',
  59. district: props.county || '未知区县',
  60. cdValue: props.cd_concentration !== undefined ? props.cd_concentration : '未知',
  61. latitude: gcjLat,
  62. longitude: gcjLng
  63. }
  64. })
  65. .filter(item => item !== null) // 过滤无效数据
  66. // 计算河流平均值
  67. calculateRiverAvg()
  68. } catch (err) {
  69. error.value = `数据加载失败:${err.message}`
  70. console.error('数据处理错误:', err)
  71. } finally {
  72. loading.value = false
  73. }
  74. }
  75. // 按河流分组计算平均值
  76. const calculateRiverAvg = () => {
  77. const riverGroups = {};
  78. // 分组统计
  79. state.excelData.forEach(item => {
  80. if (!riverGroups[item.river]) {
  81. riverGroups[item.river] = { total: 0, count: 0 }
  82. }
  83. riverGroups[item.river].total += item.cdValue
  84. riverGroups[item.river].count += 1
  85. });
  86. // 计算各组平均值
  87. const riverAvg = [];
  88. let totalAll = 0;
  89. let countAll = 0;
  90. for (const river in riverGroups) {
  91. const avg = riverGroups[river].total / riverGroups[river].count;
  92. riverAvg.push({
  93. river,
  94. avg: parseFloat(avg).toFixed(6) //保留6位小数
  95. });
  96. totalAll += riverGroups[river].total;
  97. countAll += riverGroups[river].count;
  98. }
  99. // 添加总平均值
  100. const totalAvg = {
  101. river: '总河流平均',
  102. avg: parseFloat((totalAll / countAll)).toFixed(6)
  103. };
  104. riverAvg.push(totalAvg);
  105. state.riverAvgData = riverAvg;
  106. updateChart(); // 更新图表
  107. }
  108. // 初始化ECharts实例
  109. const initChart = () => {
  110. if (chartContainer.value) {
  111. chart = echarts.init(chartContainer.value)
  112. updateChart()
  113. }
  114. }
  115. // 更新图表数据
  116. const updateChart = () => {
  117. if (!chart || state.riverAvgData.length === 0) return;
  118. // 处理图表数据
  119. const rivers = state.riverAvgData.map(item => item.river)
  120. const avgs = state.riverAvgData.map(item => item.avg)
  121. // ECharts配置项
  122. const option = {
  123. tooltip: {
  124. trigger: 'axis',
  125. axisPointer: { type: 'shadow' },
  126. formatter: '{a} <br/>{b}: {c} ug/L'
  127. },
  128. grid: {
  129. right: '4%',
  130. bottom: '3%',
  131. containLabel: true
  132. },
  133. xAxis: {
  134. type: 'category',
  135. data: rivers,
  136. axisLabel: { interval: 0, fontSize: 15 }
  137. },
  138. yAxis: {
  139. type: 'value',
  140. name: 'Cd浓度 (ug/L)',
  141. min: 0,
  142. nameTextStyle: { fontSize: 15},
  143. axisLabel: { formatter: '{value}', fontSize: 15 }
  144. },
  145. series: [{
  146. name: '平均镉浓度',
  147. type: 'bar',
  148. data: avgs,
  149. itemStyle: {
  150. color: (params) =>
  151. params.dataIndex === rivers.length - 1 ? '#FF4500' : '#1E88E5'
  152. },
  153. label: {
  154. show: true,
  155. position: 'top',
  156. formatter: '{c}',
  157. fontSize: 15
  158. },
  159. emphasis: { focus: 'series' }
  160. }]
  161. };
  162. chart.setOption(option)
  163. }
  164. // 生命周期钩子
  165. onMounted(async () => {
  166. await initData() // 先加载数据
  167. initChart() // 再初始化图表
  168. // 监听窗口变化
  169. window.addEventListener('resize', () => {
  170. if (chart) chart.resize()
  171. })
  172. })
  173. onBeforeUnmount(() => {
  174. if (chart) chart.dispose() // 销毁图表实例
  175. })
  176. </script>
  177. <style scoped>
  178. .chart-page {
  179. width: 100%;
  180. margin: 0 auto 24px;
  181. background-color: white;
  182. border-radius: 12px;
  183. padding: 20px;
  184. box-sizing: border-box;
  185. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  186. }
  187. .chart-container {
  188. width: 100%;
  189. height: 400px;
  190. margin: 0 auto;
  191. border-radius: 12px;
  192. }
  193. /* 加载状态样式 */
  194. .loading-indicator {
  195. text-align: center;
  196. padding: 40px 0;
  197. color: #6b7280;
  198. }
  199. .spinner {
  200. width: 40px;
  201. height: 40px;
  202. margin: 0 auto 16px;
  203. border: 4px solid #e5e7eb;
  204. border-top: 4px solid #3b82f6;
  205. border-radius: 50%;
  206. animation: spin 1s linear infinite;
  207. }
  208. @keyframes spin {
  209. 0% { transform: rotate(0deg); }
  210. 100% { transform: rotate(360deg); }
  211. }
  212. /* 错误提示样式 */
  213. .error-message {
  214. color: #dc2626;
  215. background-color: #fee2e2;
  216. padding: 12px 16px;
  217. border-radius: 6px;
  218. margin-bottom: 16px;
  219. display: flex;
  220. align-items: center;
  221. font-weight: 500;
  222. }
  223. .error-message i {
  224. margin-right: 8px;
  225. font-size: 18px;
  226. }
  227. </style>