waterassaydata2.vue 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. <template>
  2. <div class="region-average-chart">
  3. <div ref="chartRef" class="chart-box"></div>
  4. <div v-if="loading" class="status">数据加载中...</div>
  5. <div v-else-if="error" class="status error">{{ error }}</div>
  6. </div>
  7. </template>
  8. <!--各地区的重金属平均值得柱状图-->
  9. <script setup>
  10. import { ref, onMounted, onUnmounted } from 'vue';
  11. import * as echarts from 'echarts';
  12. import axios from 'axios';
  13. // ========== 接口配置(仅保留新接口) ==========
  14. const NEW_API = 'http://localhost:8000/api/vector/export/all?table_name=water_sampling_data';
  15. // ========== 配置项(调整字段适配新接口) ==========
  16. // 排除的非重金属字段(适配新接口字段名)
  17. const EXCLUDE_FIELDS = [
  18. 'id', 'sample_code', 'assayer_id', 'assay_time',
  19. 'assay_instrument_model', 'sample_number', 'ph_value',
  20. 'latitude', 'longitude', 'sampling_location', 'sampling_time' // 新增采样相关非检测字段
  21. ];
  22. const COLORS = ['#ff4d4f99', '#1890ff', '#ffd700', '#52c41a88', '#722ed199' ];
  23. // 韶关市下属行政区划白名单(保持不变)
  24. const SG_REGIONS = [
  25. '浈江区', '武江区', '曲江区', '乐昌市',
  26. '南雄市', '始兴县', '仁化县', '翁源县',
  27. '新丰县', '乳源瑶族自治县'
  28. ];
  29. // ========== 响应式数据 ==========
  30. const chartRef = ref(null);
  31. const loading = ref(true);
  32. const error = ref('');
  33. let myChart = null;
  34. // ========== 地区提取函数(保持不变) ==========
  35. const extractRegion = (location) => {
  36. if (!location || typeof location !== 'string') return null;
  37. // 1. 精确匹配官方区县名称
  38. const officialMatch = SG_REGIONS.find(region =>
  39. location.includes(region)
  40. );
  41. if (officialMatch) return officialMatch;
  42. // 2. 处理嵌套格式(如"韶关市-浈江区")
  43. const nestedMatch = location.match(/(韶关市)([^市]+?[区市县])/);
  44. if (nestedMatch && nestedMatch[2]) {
  45. const region = nestedMatch[2].replace("韶关市", "").trim();
  46. const validRegion = SG_REGIONS.find(r => r.includes(region));
  47. if (validRegion) return validRegion;
  48. }
  49. // 3. 特殊格式处理(如"韶关市浈江区")
  50. const shortMatch = location.match(/韶关市([区市县][^市]{2,5})/);
  51. if (shortMatch && shortMatch[1]) return shortMatch[1];
  52. // 4. 修正常见拼写错误
  53. if (location.includes('乐昌')) return '乐昌市';
  54. if (location.includes('乳源')) return '乳源瑶族自治县';
  55. console.warn(`⚠️ 未识别地区: ${location}`);
  56. return '未知区县';
  57. };
  58. // ========== 数据处理流程(适配新接口单数据源) ==========
  59. const processData = (allData) => {
  60. // 1. 构建采样点ID到区县的映射(sample_number对应新接口的水样ID)
  61. const regionMap = new Map();
  62. allData.forEach(item => {
  63. const region = extractRegion(item.sampling_location || '');
  64. if (region && region !== '未知区县' && item.sample_number) {
  65. regionMap.set(item.sample_number, region);
  66. }
  67. });
  68. // 2. 关联重金属数据与区县(单条数据已包含所有信息)
  69. const mergedData = allData.map(item => ({
  70. ...item,
  71. // 通过sample_number关联区县
  72. region: regionMap.get(item.sample_number) || '未知区县'
  73. }));
  74. // 3. 识别重金属字段(新接口字段如cr_concentration、as_concentration等)
  75. const metals = Object.keys(mergedData[0] || {})
  76. .filter(key =>
  77. !EXCLUDE_FIELDS.includes(key) && // 排除非重金属字段
  78. !isNaN(parseFloat(mergedData[0][key])) && // 确保是数值
  79. key.includes('concentration') // 新接口重金属字段含concentration
  80. );
  81. // 4. 按区县分组统计
  82. const regionGroups = {};
  83. const cityWideAverages = {}; // 全市平均值
  84. const uniqueSampleIds = new Set();
  85. // 初始化统计计数器
  86. metals.forEach(metal => {
  87. cityWideAverages[metal] = { sum: 0, count: 0 };
  88. });
  89. mergedData.forEach(item => {
  90. const region = item.region;
  91. if (item.sample_number) {
  92. uniqueSampleIds.add(item.sample_number);
  93. }
  94. // 初始化区县分组
  95. if (!regionGroups[region]) {
  96. regionGroups[region] = {};
  97. metals.forEach(metal => {
  98. regionGroups[region][metal] = { sum: 0, count: 0 };
  99. });
  100. }
  101. // 统计各重金属含量
  102. metals.forEach(metal => {
  103. const val = parseFloat(item[metal]);
  104. if (!isNaN(val)) {
  105. // 区县统计
  106. regionGroups[region][metal].sum += val;
  107. regionGroups[region][metal].count++;
  108. // 全市统计
  109. cityWideAverages[metal].sum += val;
  110. cityWideAverages[metal].count++;
  111. }
  112. });
  113. });
  114. const totalSamples = uniqueSampleIds.size;
  115. // 5. 按官方顺序排序区县
  116. const regions = SG_REGIONS.filter(region => regionGroups[region]);
  117. // 6. 添加"全市平均"作为最后一个类别
  118. regions.push("全市平均");
  119. // 7. 构建ECharts数据(处理重金属字段名显示)
  120. const series = metals.map((metal, idx) => {
  121. // 格式化重金属名称(如cr_concentration → Cr)
  122. const prefix = metal.split('_')[0]; // 先获取前缀
  123. const metalName = prefix
  124. ? prefix[0].toUpperCase() + prefix.slice(1) // 首字母大写 + 剩余字符
  125. : ''; // 处理空字符串情况
  126. // 计算全市平均值
  127. const cityWideAvg = cityWideAverages[metal].count
  128. ? (cityWideAverages[metal].sum / cityWideAverages[metal].count).toFixed(2)
  129. : 0;
  130. return {
  131. name: metalName, // 显示简化名称(如Cr、As)
  132. type: 'bar',
  133. data: regions.map(region => {
  134. if (region === "全市平均") {
  135. return cityWideAvg;
  136. }
  137. const group = regionGroups[region][metal];
  138. return group.count ? (group.sum / group.count).toFixed(2) : 0;
  139. }),
  140. itemStyle: {
  141. color: COLORS[idx % COLORS.length],
  142. },
  143. label: {
  144. show: true,
  145. position: 'top',
  146. fontSize: 15,
  147. color: '#333',
  148. }
  149. };
  150. });
  151. return { regions, series, totalSamples };
  152. };
  153. // ========== ECharts 初始化(保持不变) ==========
  154. const initChart = ({ regions, series, totalSamples }) => {
  155. if (!chartRef.value) return;
  156. if (myChart) myChart.dispose();
  157. myChart = echarts.init(chartRef.value);
  158. const option = {
  159. title: {
  160. left: 'center',
  161. subtext: `数据来源: ${totalSamples}个有效检测样本`,
  162. subtextStyle: {
  163. fontSize: 15
  164. }
  165. },
  166. tooltip: {
  167. trigger: 'axis',
  168. formatter: params => {
  169. const regionName = params[0].name;
  170. const isCityWide = regionName === "全市平均";
  171. let content = `${isCityWide ? "全市平均值" : regionName}:`;
  172. if (isCityWide) {
  173. content += `<br><span style="color: #666;">(基于${totalSamples}个样本计算)</span>`;
  174. }
  175. return content + params.map(p => `<br>${p.seriesName}: ${p.value} ug/L`).join('');
  176. },
  177. textStyle: {
  178. fontSize: 15
  179. }
  180. },
  181. xAxis: {
  182. type: 'category',
  183. data: regions,
  184. axisLabel: {
  185. rotate: 45,
  186. formatter: val => val.replace('韶关市', ''),
  187. fontSize: 15
  188. }
  189. },
  190. yAxis: {
  191. type: 'value',
  192. name: '浓度(ug/L)',
  193. nameTextStyle: {
  194. fontSize: 15,
  195. },
  196. axisLabel: {
  197. fontSize: 15,
  198. }
  199. },
  200. dataZoom: [{
  201. type: 'inside',
  202. start: 0,
  203. end: 100
  204. }],
  205. series,
  206. legend: { //图例
  207. data: series.map(s => s.name),
  208. top:'10%',
  209. right:'5%',
  210. textStyle: {
  211. fontSize: 15
  212. }
  213. },
  214. grid: { //整个图的位置
  215. left: '3%',
  216. right: '3%',
  217. bottom: '5%',
  218. containLabel: true
  219. },
  220. };
  221. myChart.setOption(option);
  222. };
  223. // ========== 生命周期钩子(适配新接口) ==========
  224. onMounted(async () => {
  225. try {
  226. // 仅请求新接口
  227. const response = await axios.get(NEW_API, { timeout: 10000 });
  228. // 从GeoJSON中提取数据(features.properties)
  229. const allData = response.data.features.map(feature => feature.properties);
  230. // 处理数据并初始化图表
  231. initChart(processData(allData));
  232. } catch (err) {
  233. error.value = '数据加载失败: ' + (err.message || '未知错误');
  234. console.error('接口错误:', err);
  235. } finally {
  236. loading.value = false;
  237. }
  238. });
  239. // 响应式布局(保持不变)
  240. const resizeHandler = () => myChart && myChart.resize();
  241. onMounted(() => window.addEventListener('resize', resizeHandler));
  242. onUnmounted(() => window.removeEventListener('resize', resizeHandler));
  243. </script>
  244. <style scoped>
  245. .region-average-chart {
  246. width: 100%;
  247. height: 500px;
  248. max-width: 1200px;
  249. margin: 0 auto;
  250. position: relative;
  251. }
  252. .chart-box {
  253. width: 100%;
  254. height: 500px;
  255. min-height: 400px;
  256. background-color: white;
  257. border-radius: 8px;
  258. }
  259. .status {
  260. position: absolute;
  261. top: 50%;
  262. left: 50%;
  263. transform: translate(-50%, -50%);
  264. padding: 15px;
  265. background: rgba(255,255,255,0.8);
  266. border-radius: 4px;
  267. font-size: 16px;
  268. }
  269. .error {
  270. color: #ff4d4f;
  271. font-weight: bold;
  272. }
  273. </style>