瀏覽代碼

断面水地图的修改,大气污染添加选择框

yes-yes-yes-k 1 周之前
父節點
當前提交
9a1754fc2a

+ 30 - 4
src/views/User/HmOutFlux/atmosDeposition/airSampleData.vue

@@ -1,33 +1,45 @@
 <template>
 <template>
   <div class="page-container">
   <div class="page-container">
     
     
+   <div class="calculation-selector">
+    <span class="selector-title">计算方式:</span>
+    <select 
+      v-model="calculationMethod" 
+      class="calculation-select"
+    >
+      <option value="weight">按重量计算</option>
+      <option value="volume">按体积计算</option>
+    </select>
+  </div>
+
    <div class="point-map">
    <div class="point-map">
     <div class="component-title">采样点地图展示</div>
     <div class="component-title">采样点地图展示</div>
     <div class="map-holder">
     <div class="map-holder">
-      <atmsamplemap/>
+      <atmsamplemap :calculation-method="calculationMethod"/>
      </div>
      </div>
    </div>
    </div>
 
 
   
   
    <div class="point-line">
    <div class="point-line">
     <div class="component-title">采样点数据列表展示</div>
     <div class="component-title">采样点数据列表展示</div>
-    <AirsampleLine/>
+    <AirsampleLine :calculation-method="calculationMethod"/>
    </div>
    </div>
 
 
    <div>
    <div>
     <div class="component-title">各区县企业平均大气颗粒物排放(t/a)</div>
     <div class="component-title">各区县企业平均大气颗粒物排放(t/a)</div>
-    <AirsampleChart/>
+    <AirsampleChart :calculation-method="calculationMethod"/>
    </div>
    </div>
   </div>
   </div>
 </template>
 </template>
 <!--污染企业-->
 <!--污染企业-->
 
 
 <script setup>
 <script setup>
+import {ref} from 'vue'
 import AirsampleLine from './airsampleLine.vue';
 import AirsampleLine from './airsampleLine.vue';
 import atmsamplemap from './atmsamplemap.vue';
 import atmsamplemap from './atmsamplemap.vue';
 import AirsampleChart from './airsampleChart.vue';
 import AirsampleChart from './airsampleChart.vue';
 
 
-
+const calculationMethod = ref('weight')
 
 
 
 
 </script>
 </script>
@@ -44,6 +56,20 @@ import AirsampleChart from './airsampleChart.vue';
   margin: 0;
   margin: 0;
 }
 }
 
 
+.calculation-select {
+  padding: 12px 24px;
+  border: 2px solid #ddd;
+  border-radius: 6px;
+  font-size: 16px;
+  color: #333;
+  background-color: #fff;
+  cursor: pointer;
+}
+.calculation-select:focus {
+  outline: none;
+  border-color: #1e88e5;
+}
+
 
 
 .map-holder {
 .map-holder {
   position: relative;
   position: relative;

+ 310 - 123
src/views/User/HmOutFlux/atmosDeposition/airsampleChart.vue

@@ -1,164 +1,351 @@
 <template>
 <template>
   <div class="atmosphere-summary">
   <div class="atmosphere-summary">
-    <!-- ECharts 容器,需设置宽高 -->
-    <div id="chart" style="width: 1000px; height: 500px;"></div>
+    <div ref="chartRef" class="chart-box"></div>
+    <div v-if="loading" class="status">数据加载中...</div>
+    <div v-else-if="error" class="status error">{{ error }}</div>
   </div>
   </div>
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import { onMounted } from 'vue'
-import axios from 'axios'
+import { ref, onMounted, onUnmounted, watch } from 'vue'
 import * as echarts from 'echarts'
 import * as echarts from 'echarts'
+import axios from 'axios'
 
 
-onMounted(async () => {
-  try {
-    // 1. 请求接口数据
-    const response = await axios.get('http://localhost:3000/table/Atmosphere_summary_data')
-    const samples = response.data 
+// 接收父组件传递的计算方式
+const props = defineProps({
+  calculationMethod: {
+    type: String,
+    required: true,
+    default: 'weight' // 默认按重量计算
+  }
+})
 
 
-    if (!Array.isArray(samples) || samples.length === 0) {
-      console.warn('无有效样本数据')
-      return
-    }
+// 仅使用一个接口(包含位置和数据信息)
+const SUMMARY_API = 'http://localhost:3000/table/Atmosphere_summary_data';
+
+// 配置项
+const COLORS = ['#66CCFF', '#FF9966', '#99CC99', '#CC99CC', '#FFCC66', '#6699CC', '#FF6666', '#99CC66'];
+
+// 地区白名单 - 根据实际情况修改
+const REGIONS = [
+  '浈江区', '武江区', '曲江区', '乐昌市', 
+  '南雄市', '始兴县', '仁化县', '翁源县', 
+  '新丰县', '乳源瑶族自治县'
+];
 
 
-    // 2. 定义需计算平均值的指标(根据实际数据字段调整)
-    const metrics = [
+// 响应式数据
+const chartRef = ref(null);
+const loading = ref(true);
+const error = ref('');
+let myChart = null;
+
+// 根据计算方式获取对应的指标
+const getMetricsByMethod = (method) => {
+  if (method === 'weight') {
+    return [
       'Cr mg/kg',
       'Cr mg/kg',
       'As mg/kg',
       'As mg/kg',
       'Cd mg/kg',
       'Cd mg/kg',
       'Hg mg/kg',
       'Hg mg/kg',
       'Pb mg/kg',
       'Pb mg/kg',
-      '颗粒物的重量 mg',
-      '标准体积 m3',
-      '颗粒物浓度ug/m3',
+    ]
+  } else {
+    return [
       'Cr ug/m3',
       'Cr ug/m3',
       'As ug/m3',
       'As ug/m3',
       'Cd ug/m3',
       'Cd ug/m3',
       'Hg ug/m3',
       'Hg ug/m3',
       'Pb ug/m3'
       'Pb ug/m3'
     ]
     ]
+  }
+}
+
+// 地区提取函数(从位置信息中解析地区)
+const extractRegion = (location) => {
+  if (!location || typeof location !== 'string') return '未知区县';
+
+  // 精确匹配官方区县名称
+  const officialMatch = REGIONS.find(region => 
+    location.includes(region)
+  );
+  if (officialMatch) return officialMatch;
+
+  // 处理嵌套格式(如"韶关市-浈江区")
+  const nestedMatch = location.match(/(韶关市)([^市]+?[区市县])/);
+  if (nestedMatch && nestedMatch[2]) {
+    const region = nestedMatch[2].replace("韶关市", "").trim();
+    const validRegion = REGIONS.find(r => r.includes(region));
+    if (validRegion) return validRegion;
+  }
 
 
-    // 3. 计算总和与平均值
-    const sumMap = metrics.reduce((acc, key) => {
-      acc[key] = 0
-      return acc
-    }, {})
-
-    samples.forEach(sample => {
-      metrics.forEach(key => {
-        const value = parseFloat(sample[key])
-        if (!isNaN(value)) { // 过滤无效值
-          sumMap[key] += value
+  // 特殊格式处理(如"韶关市浈江区")
+  const shortMatch = location.match(/韶关市([区市县][^市]{2,5})/);
+  if (shortMatch && shortMatch[1]) return shortMatch[1];
+
+  // 修正常见拼写错误
+  if (location.includes('乐昌')) return '乐昌市';
+  if (location.includes('乳源')) return '乳源瑶族自治县';
+
+  console.warn(`未识别地区: ${location}`);
+  return '未知区县';
+};
+
+// 数据处理流程(仅依赖单个接口)
+const processData = async () => {
+  try {
+    // 获取当前计算方式对应的指标
+    const metrics = getMetricsByMethod(props.calculationMethod);
+    
+    // 仅请求汇总数据接口(包含位置和指标信息)
+    const response = await axios.get(SUMMARY_API, { timeout: 10000 });
+    const summaryData = response.data;
+
+    if (!Array.isArray(summaryData) || summaryData.length === 0) {
+      throw new Error('没有获取到有效数据');
+    }
+
+    // 1. 为每个数据项添加地区信息(从自身位置字段提取)
+    const dataWithRegion = summaryData.map(item => ({
+      ...item,
+      // 位置信息字段为 采样,根据实际字段名修改
+      region: extractRegion(item.采样 || '')
+    }));
+
+    // 2. 按区县分组统计
+    const regionGroups = {};
+    const cityWideAverages = {}; // 全市平均值
+    const uniqueSampleIds = new Set();
+    
+    // 初始化统计计数器
+    metrics.forEach(metric => {
+      cityWideAverages[metric] = { sum: 0, count: 0 };
+    });
+    
+    dataWithRegion.forEach(item => {
+      const region = item.region;
+      
+      // 样本唯一标识字段为 样品名称,根据实际字段名修改
+      if (item.样品名称) {
+        uniqueSampleIds.add(item.样品名称);
+      }
+
+      // 初始化地区分组
+      if (!regionGroups[region]) {
+        regionGroups[region] = {};
+        metrics.forEach(metric => {
+          regionGroups[region][metric] = { sum: 0, count: 0 };
+        });
+      }
+
+      // 统计各指标值
+      metrics.forEach(metric => {
+        const val = parseFloat(item[metric]);
+        if (!isNaN(val)) {
+          // 更新地区统计
+          regionGroups[region][metric].sum += val;
+          regionGroups[region][metric].count++;
+          
+          // 更新全市统计
+          cityWideAverages[metric].sum += val;
+          cityWideAverages[metric].count++;
         }
         }
-      })
-    })
-
-    const averageMap = {}
-    const sampleCount = samples.length
-    metrics.forEach(key => {
-      averageMap[key] = sampleCount > 0 
-      ?Number((sumMap[key] / sampleCount).toFixed(5)):0; 
+      });
+    });
+    
+    const totalSamples = uniqueSampleIds.size;
+
+    // 3. 按官方顺序排序区县
+    const regions = REGIONS.filter(region => regionGroups[region]);
+    
+    // 4. 添加"全市平均"作为最后一个类别
+    regions.push("全市平均");
+
+    // 5. 构建ECharts数据
+    const series = metrics.map((metric, idx) => {
+      // 计算全市平均值
+      const cityWideAvg = cityWideAverages[metric].count 
+        ? (cityWideAverages[metric].sum / cityWideAverages[metric].count).toFixed(5) 
+        : 0;
       
       
-    })
-
-    // 4. 处理 ECharts 数据格式
-    const xAxisData = Object.keys(averageMap)
-    const seriesData = Object.values(averageMap)
-
-    // 5. 初始化 ECharts 并渲染
-    const chartDom = document.getElementById('chart')
-    if (chartDom) {
-      const myChart = echarts.init(chartDom)
-      const option = {
-        title: { text: '空气颗粒物样本数据平均值' },
-        xAxis: {
-          type: 'category',
-          data: xAxisData,
-          axisLabel: {
-            rotate: 30,    // 减小旋转角度(如从 45→30),让标签更水平
-            fontSize: 14,
-            interval:0,
-          },
-          axisTick:{
-            shoe:true//显示刻度线
-          },
-          axisLine:{
-            lineStyle:{
-                width:1.5//加粗轴线
-            }
+      return {
+        name: metric,
+        type: 'bar',
+        data: regions.map(region => {
+          if (region === "全市平均") {
+            return cityWideAvg;
           }
           }
+          const group = regionGroups[region][metric];
+          return group.count ? (group.sum / group.count).toFixed(5) : 0;
+        }),
+        itemStyle: { 
+          color: COLORS[idx % COLORS.length],
         },
         },
-        yAxis: { 
-            type: 'value' ,
-            axisLabel:{
-            fontSize:14,
-            },
-            axisLine:{
-                lineStyle:{
-                    width:1.5
-                }
-            },
-            splitLine:{
-                lineStyle:{
-                    opacity:0.3
-                }
-            }
+        label: {
+          show: true,
+          position: 'top',
+          fontSize: 20,
+          color: '#333',
+        }
+      };
+    });
 
 
-        },
-        series: [
-          {
-            name: '平均值',
-            type: 'bar',
-            data: seriesData,
-            barWidth:'60%',
-            label: {
-              show: true, // 显示柱顶数值
-              position: 'top',
-              fontSize:14,
-              fontWeight:'bold'
-            },
-            itemStyle: {
-               // 使用回调函数为每个柱子设置不同颜色
-          color: (params) => {
-            // 定义一组不同的颜色
-            const colors = [
-              '#66CCFF', '#FF9966', '#99CC99', '#CC99CC', 
-              '#FFCC66', '#6699CC', '#FF6666', '#99CC66', 
-              '#CC66CC', '#66CC66', '#FF99CC', '#6666CC', '#CC9999'
-            ];
-            // 根据数据索引选择颜色
-            return colors[params.dataIndex % colors.length];
-          }
-            }
-          }
-        ],
-        tooltip: { // 添加 tooltip 增强交互
+    return { regions, series, totalSamples, metrics };
+  } catch (err) {
+    error.value = '数据处理失败: ' + (err.message || '未知错误');
+    console.error('数据处理错误:', err);
+    return null;
+  }
+};
+
+// 初始化图表
+const initChart = async () => {
+  loading.value = true;
+  error.value = '';
+  
+  try {
+    const processedData = await processData();
+    if (!processedData) return;
+    
+    const { regions, series, totalSamples, metrics } = processedData;
+    
+    if (!chartRef.value) return;
+    if (myChart) myChart.dispose();
+
+    myChart = echarts.init(chartRef.value);
+    
+    // 根据计算方式设置标题
+    const titleText = props.calculationMethod === 'weight' 
+      ? '各区域空气颗粒物重量指标平均值' 
+      : '各区域空气颗粒物体积指标平均值';
+    
+    const option = {
+      title: { 
+        text: titleText,
+        left: 'center',
+        subtext: `数据来源: ${totalSamples}个有效检测样本`,
+        subtextStyle: {
+          fontSize: 18
+        }
+      },
+      tooltip: { 
         trigger: 'axis',
         trigger: 'axis',
-        axisPointer: {
-          type: 'shadow'
-        },
+        formatter: params => {
+          const regionName = params[0].name;
+          const isCityWide = regionName === "全市平均";
+          
+          let content = `${isCityWide ? "全市平均值" : regionName}:`;
+          if (isCityWide) {
+            content += `<br><span style="color: #666;">(基于${totalSamples}个样本计算)</span>`;
+          }
+          
+          return content + params.map(p => `<br>${p.seriesName}: ${p.value}`).join('');
+        }
+      },
+      xAxis: {
+        type: 'category',
+        data: regions,
+        axisLabel: { 
+          rotate: 45,
+          formatter: val => val.replace('韶关市', ''),
+          fontSize: 20
+        }
+      },
+      yAxis: { 
+        type: 'value',
+        axisLabel: { fontSize: 20 },
+        axisLine: { lineStyle: { width: 1.5 } },
+        splitLine: { lineStyle: { opacity: 0.3 } }
+      },
+      dataZoom: [{
+        type: 'inside',
+        start: 0,
+        end: 100
+      }],
+      series,
+      legend: { 
+        data: metrics, 
+        bottom: 10,
         textStyle: {
         textStyle: {
-          fontSize: 14 // 增大 tooltip 字体
+          fontSize: 20
         }
         }
       },
       },
-      grid: { // 调整图表边距
-        left: '5%',
-        right: '5%',
-        bottom: '15%', // 为 x 轴标签留出更多空间
+      grid: { 
+        left: '5%', 
+        right: '5%', 
+        bottom: '15%', 
         top: '10%',
         top: '10%',
-        containLabel: true
+        containLabel: true 
       }
       }
-        }
-      myChart.setOption(option)
-    }
-  } catch (error) {
-    console.error('数据请求或处理失败:', error)
+    };
+
+    myChart.setOption(option);
+  } catch (err) {
+    error.value = '图表初始化失败: ' + (err.message || '未知错误');
+    console.error('图表错误:', err);
+  } finally {
+    loading.value = false;
   }
   }
-})
+};
+
+// 监听计算方式变化,重新渲染图表
+watch(
+  () => props.calculationMethod,
+  () => {
+    initChart();
+  }
+)
+
+// 生命周期钩子
+onMounted(() => {
+  initChart();
+  // 窗口大小变化时重绘图表
+  window.addEventListener('resize', () => {
+    if (myChart) myChart.resize();
+  });
+});
+
+onUnmounted(() => {
+  if (myChart) {
+    myChart.dispose();
+    myChart = null;
+  }
+  window.removeEventListener('resize', () => {
+    if (myChart) myChart.resize();
+  });
+});
 </script>
 </script>
 
 
 <style scoped>
 <style scoped>
 .atmosphere-summary {
 .atmosphere-summary {
+  width: 100%;
+  max-width: 1400px;
+  margin: 0 auto;
   padding: 20px;
   padding: 20px;
+  box-sizing: border-box;
+  position: relative;
+}
+
+.chart-box {
+  width: 100%;
+  height: 600px;
+  min-height: 400px;
+  background-color: white;
+  border-radius: 8px;
+}
+
+.status {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  padding: 15px;
+  background: rgba(255,255,255,0.8);
+  border-radius: 4px;
+  font-size: 16px;
+}
+
+.error { 
+  color: #ff4d4f;
+  font-weight: bold;
 }
 }
 </style>
 </style>

+ 104 - 35
src/views/User/HmOutFlux/atmosDeposition/airsampleLine.vue

@@ -30,11 +30,12 @@
         </div>
         </div>
       </div>
       </div>
       
       
-      <!-- 数据表格 -->
+      <!-- 数据表格(根据计算方式动态显示列) -->
       <div v-else-if="filteredData.length > 0" class="overflow-x-auto">
       <div v-else-if="filteredData.length > 0" class="overflow-x-auto">
         <table class="min-w-full divide-y divide-gray-200">
         <table class="min-w-full divide-y divide-gray-200">
           <thead class="bg-gray-50">
           <thead class="bg-gray-50">
             <tr>
             <tr>
+              <!-- 动态渲染当前计算方式对应的列 -->
               <th 
               <th 
                 v-for="(col, index) in displayColumns" 
                 v-for="(col, index) in displayColumns" 
                 :key="index"
                 :key="index"
@@ -53,6 +54,7 @@
           <tbody class="bg-white divide-y divide-gray-200">
           <tbody class="bg-white divide-y divide-gray-200">
             <tr v-for="(item, rowIndex) in sortedData" :key="rowIndex" 
             <tr v-for="(item, rowIndex) in sortedData" :key="rowIndex" 
                 class="hover:bg-gray-50 transition-colors duration-150">
                 class="hover:bg-gray-50 transition-colors duration-150">
+              <!-- 动态渲染当前计算方式对应的列数据 -->
               <td 
               <td 
                 v-for="(col, colIndex) in displayColumns" 
                 v-for="(col, colIndex) in displayColumns" 
                 :key="colIndex"
                 :key="colIndex"
@@ -60,7 +62,8 @@
               >
               >
                 <div class="flex items-center">
                 <div class="flex items-center">
                   <div class="text-gray-900 font-medium">
                   <div class="text-gray-900 font-medium">
-                    {{ item[col.key] !== null ? item[col.key] : '-' }}
+                    <!-- 体积数据保持原始格式,-->
+                    {{ formatValue(item[col.key], col.isVolume,col.type) }}
                   </div>
                   </div>
                 </div>
                 </div>
               </td>
               </td>
@@ -69,7 +72,7 @@
         </table>
         </table>
       </div>
       </div>
       
       
-      <!-- 空数据状态 -->
+      <!-- 空数据状态(保持不变) -->
       <div v-else class="p-8 text-center">
       <div v-else class="p-8 text-center">
         <div class="flex flex-col items-center justify-center">
         <div class="flex flex-col items-center justify-center">
           <div class="text-gray-400 mb-4">
           <div class="text-gray-400 mb-4">
@@ -80,56 +83,103 @@
         </div>
         </div>
       </div>
       </div>
   
   
-      <!-- 数据表格 + 统计 -->
-  <div class="p-4 bg-gray-50 border-t border-gray-200">
-    <div class="flex flex-col md:flex-row justify-between items-center">
+      <!-- 数据统计 -->
+      <div class="p-4 bg-gray-50 border-t border-gray-200">
+         <div class="flex flex-col md:flex-row justify-between items-center">
       <div class="text-sm text-gray-500 mb-2 md:mb-0">
       <div class="text-sm text-gray-500 mb-2 md:mb-0">
         共 <span class="font-medium text-gray-900">{{ filteredData.length }}</span> 条数据
         共 <span class="font-medium text-gray-900">{{ filteredData.length }}</span> 条数据
       </div>
       </div>
     </div>
     </div>
-</div>
+      </div>
     </div>
     </div>
   </div>
   </div>
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import { ref, computed, onMounted } from 'vue';
+import { ref, computed, onMounted, defineProps } from 'vue';
 import axios from 'axios';
 import axios from 'axios';
 
 
-// 定义固定列配置
-const displayColumns = ref([
-  { key: '采样', label: '采样位置' },
-  { key: '样品名称', label: '样品名称' },
-  { key: 'Cr mg/kg', label: 'Cr mg/kg' },
-  { key: 'As mg/kg', label: 'As mg/kg' },
-  { key: 'Cd mg/kg', label: 'Cd mg/kg' },
-  { key: 'Hg mg/kg', label: 'Hg mg/kg' },
-  { key: 'Pb mg/kg', label: 'Pb mg/kg' },
-  { key: '颗粒物的重量 mg', label: '颗粒物的重量 mg' },
-  { key: '标准体积 m3', label: '标准体积 m3' },
-  { key: '颗粒物浓度ug/m3', label: '颗粒物浓度ug/m3' },
-  { key: 'Cr ug/m3', label: 'Cr ug/m3' },
-  { key: 'As ug/m3', label: 'As ug/m3' },
-  { key: 'Cd ug/m3', label: 'Cd ug/m3' },
-  { key: 'Hg ug/m3', label: 'Hg ug/m3' },
-  { key: 'Pb ug/m3', label: 'Pb ug/m3' },
-  
-]);
+// 1. 接收父组件传递的计算方式(重量/体积)
+const props = defineProps({
+  calculationMethod: {
+    type: String,
+    required: true,
+    default: 'weight' // 默认按重量计算
+  }
+});
+
+// 2. 定义重量/体积对应的列配置(核心:区分两类指标)
+// 公共列(两种方式都显示的基础信息)
+const commonColumns = [
+  { key: '采样', label: '采样位置', isVolume: false,type:'string' }, // 基础信息,不属于任何分类
+  { key: '样品名称', label: '样品名称', isVolume: false ,type:'string'}
+];
+
+// 重量相关列(isVolume标记为false)
+const weightColumns = [
+  { key: 'Cr mg/kg', label: 'Cr mg/kg', isVolume: false,type:'number' },
+  { key: 'As mg/kg', label: 'As mg/kg', isVolume: false ,type:'number'},
+  { key: 'Cd mg/kg', label: 'Cd mg/kg', isVolume: false ,type:'number'},
+  { key: 'Hg mg/kg', label: 'Hg mg/kg', isVolume: false ,type:'number'},
+  { key: 'Pb mg/kg', label: 'Pb mg/kg', isVolume: false ,type:'number'},
+  { key: '颗粒物的重量 mg', label: '颗粒物的重量 mg', isVolume: false ,type:'number'}
+];
+
+// 体积相关列(isVolume标记为true)
+const volumeColumns = [
+  { key: '标准体积 m3', label: '标准体积 m³', isVolume: true ,type:'number'},
+  { key: '颗粒物浓度ug/m3', label: '颗粒物浓度ug/m³', isVolume: true ,type:'number'},
+  { key: 'Cr ug/m3', label: 'Cr ug/m³', isVolume: true ,type:'number'},
+  { key: 'As ug/m3', label: 'As ug/m³', isVolume: true ,type:'number'},
+  { key: 'Cd ug/m3', label: 'Cd ug/m³', isVolume: true ,type:'number'},
+  { key: 'Hg ug/m3', label: 'Hg ug/m³', isVolume: true ,type:'number'},
+  { key: 'Pb ug/m3', label: 'Pb ug/m³', isVolume: true ,type:'number'}
+];
+
+// 3. 根据计算方式动态生成显示列(公共列 + 对应分类列)
+const displayColumns = computed(() => {
+  if (props.calculationMethod === 'volume') {
+    return [...commonColumns, ...volumeColumns]; // 体积模式:公共列 + 体积列
+  } else {
+    return [...commonColumns, ...weightColumns]; // 重量模式:公共列 + 重量列
+  }
+});
 
 
-// 状态管理
+// 4. 状态管理(保持不变)
 const waterData = ref([]);
 const waterData = ref([]);
 const loading = ref(true);
 const loading = ref(true);
 const error = ref(null);
 const error = ref(null);
 const sortKey = ref('');
 const sortKey = ref('');
 const sortOrder = ref('asc');
 const sortOrder = ref('asc');
 
 
-// 获取数据
+// 5. 数值格式化(体积数据保持原始格式)
+const formatValue = (value, isVolume,type) => {
+  if (value === null || value === undefined || value === '') return '-';
+  
+  if(type === 'number'){
+    const numValue = parseFloat(value);
+    if(isNaN(numValue)) 
+      return '-';
+    return isVolume ? numValue.toString():numValue;
+  }else{
+    return value;
+  }
+
+  const numValue = parseFloat(value);
+  if (isNaN(numValue)) return '-';
+  
+  // 体积数据保持原始格式,
+  return isVolume ? numValue.toString() : numValue;
+};
+
+// 6. 获取数据(保持不变)
 const fetchData = async () => {
 const fetchData = async () => {
   try {
   try {
     loading.value = true;
     loading.value = true;
     error.value = null;
     error.value = null;
     const response = await axios.get('http://localhost:3000/table/Atmosphere_summary_data');
     const response = await axios.get('http://localhost:3000/table/Atmosphere_summary_data');
     waterData.value = response.data.data || response.data;
     waterData.value = response.data.data || response.data;
+    console.log('原始数据字段名:', waterData.value[0] || '无数据'); 
   } catch (err) {
   } catch (err) {
     error.value = err.message || '无法连接到服务器,请检查接口是否可用';
     error.value = err.message || '无法连接到服务器,请检查接口是否可用';
   } finally {
   } finally {
@@ -137,22 +187,37 @@ const fetchData = async () => {
   }
   }
 };
 };
 
 
-// 过滤全空行
+// 7. 过滤全空行(保持不变,但基于动态列过滤)
 const filteredData = computed(() => {
 const filteredData = computed(() => {
   return waterData.value.filter(item => {
   return waterData.value.filter(item => {
-    return displayColumns.value.some(col => item[col.key] !== null && item[col.key] !== '-');
+    return displayColumns.value.some(col => {
+      const value = item[col.key];
+      // 字符串字段:只要非空就保留;数值字段:非空且非NaN
+      if (col.type === 'string') {
+        return value !== null && value !== '' && value !== '-';
+      } else {
+        return value !== null && value !== '' && value !== '-' && !isNaN(parseFloat(value));
+      }
+    });
   });
   });
 });
 });
 
 
-// 排序功能
+// 8. 排序功能(保持不变,但只对当前显示的列生效)
 const sortedData = computed(() => {
 const sortedData = computed(() => {
   if (!sortKey.value) return filteredData.value;
   if (!sortKey.value) return filteredData.value;
   
   
   return [...filteredData.value].sort((a, b) => {
   return [...filteredData.value].sort((a, b) => {
     const valA = a[sortKey.value];
     const valA = a[sortKey.value];
     const valB = b[sortKey.value];
     const valB = b[sortKey.value];
-    if (valA < valB) return sortOrder.value === 'asc' ? -1 : 1;
-    if (valA > valB) return sortOrder.value === 'asc' ? 1 : -1;
+    // 处理非数值排序(如采样位置)
+    if (typeof valA === 'string' && typeof valB === 'string') {
+      return sortOrder.value === 'asc' ? valA.localeCompare(valB) : valB.localeCompare(valA);
+    }
+    // 数值排序
+    const numA = parseFloat(valA) || -Infinity;
+    const numB = parseFloat(valB) || -Infinity;
+    if (numA < numB) return sortOrder.value === 'asc' ? -1 : 1;
+    if (numA > numB) return sortOrder.value === 'asc' ? 1 : -1;
     return 0;
     return 0;
   });
   });
 });
 });
@@ -167,12 +232,16 @@ const sortData = (key) => {
   }
   }
 };
 };
 
 
-// 组件挂载
+//  组件挂载
 onMounted(() => {
 onMounted(() => {
   fetchData();
   fetchData();
 });
 });
 </script>
 </script>
 
 
+<style scoped>
+/* 样式保持不变 */
+</style>
+
 <style scoped>
 <style scoped>
 /* 布局 */
 /* 布局 */
 .container {
 .container {

+ 146 - 111
src/views/User/HmOutFlux/atmosDeposition/atmsamplemap.vue

@@ -5,32 +5,102 @@
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import { ref, onMounted } from 'vue';
+import { ref, onMounted, watch } from 'vue'; // 新增watch用于监听计算方式变化
 import L from 'leaflet';
 import L from 'leaflet';
 import 'leaflet/dist/leaflet.css';
 import 'leaflet/dist/leaflet.css';
 
 
+// 1. 接收父组件传递的计算方式(重量/体积)
+const props = defineProps({
+  calculationMethod: {
+    type: String,
+    required: true,
+    default: 'weight' // 默认按重量计算
+  }
+});
+
 const mapContainer = ref(null);
 const mapContainer = ref(null);
+const mapInstance = ref(null); // 保存地图实例
+const markers = ref([]); // 保存所有标记点实例及对应数据,用于后续更新弹窗
+
+// 2. 定义重量/体积对应的指标分类(核心:区分需要展示的字段)
+const metricsMap = {
+  weight: [ // 重量相关指标
+    { label: 'Cr mg/kg', key: 'Cr mg/kg' },
+    { label: 'As mg/kg', key: 'As mg/kg' },
+    { label: 'Cd mg/kg', key: 'Cd mg/kg' },
+    { label: 'Hg mg/kg', key: 'Hg mg/kg' },
+    { label: 'Pb mg/kg', key: 'Pb mg/kg' },
+    { label: '颗粒物重量 mg', key: '颗粒物的重量 mg' }
+  ],
+  volume: [ // 体积相关指标
+    { label: 'Cr ug/m³', key: 'Cr ug/m3' },
+    { label: 'As ug/m³', key: 'As ug/m3' },
+    { label: 'Cd ug/m³', key: 'Cd ug/m3' },
+    { label: 'Hg ug/m³', key: 'Hg ug/m3' },
+    { label: 'Pb ug/m³', key: 'Pb ug/m3' },
+    { label: '标准体积 m³', key: '标准体积 m3' },
+    { label: '颗粒物浓度 ug/m³', key: '颗粒物浓度ug/m3' },
+  ]
+};
 
 
 // 辅助函数:数值格式化
 // 辅助函数:数值格式化
 function formatValue(value) {
 function formatValue(value) {
   if (value === undefined || value === null || value === '') return '未知';
   if (value === undefined || value === null || value === '') return '未知';
-  return parseFloat(value).toFixed(3);
+  return parseFloat(value);
+}
+
+// 辅助函数:位置格式化(处理"广东省韶关市"前缀)
+function formatLocation(fullLocation) {
+  if (!fullLocation) return '未知位置';
+  const processed = fullLocation.replace(/^广东省韶关市/, '').trim().replace(/^韶关市/, '');
+  return processed || '未知位置';
+}
+
+// 3. 生成弹窗内容(根据计算方式动态生成)
+function generatePopupContent(item, method) {
+  const metrics = metricsMap[method]; // 获取当前计算方式对应的指标
+  // 生成指标HTML片段
+  const metricsHtml = metrics.map(metric => `
+    <div class="data-item">
+      <span class="item-label">${metric.label}:</span>
+      <span class="item-value">${formatValue(item[metric.key])}</span>
+    </div>
+  `).join('');
+
+  // 弹窗整体结构(非指标信息保持不变)
+  return `
+    <div class="popup-container">
+      <div class="popup-header">
+        <h3 class="popup-title">${formatLocation(item.采样 || '未知采样点')}</h3>
+      </div>
+      <ul class="popup-info-list">
+        <li>
+          <span class="info-label">采样点ID:</span>
+          <span class="info-value">${item.样品名称 || '未知'}</span>
+        </li>
+      </ul>
+      <div class="grid-container">
+        <div class="grid-item">${metricsHtml}</div>
+      </div>
+    </div>
+  `;
 }
 }
 
 
 onMounted(() => {
 onMounted(() => {
-  // 初始化地图
   if (!mapContainer.value) {
   if (!mapContainer.value) {
     console.error('❌ 地图容器未找到!');
     console.error('❌ 地图容器未找到!');
     return;
     return;
   }
   }
 
 
+  // 初始化地图
   const map = L.map(mapContainer.value, {
   const map = L.map(mapContainer.value, {
     center: [24.7, 114], // 韶关大致中心
     center: [24.7, 114], // 韶关大致中心
     zoom: 8.5,
     zoom: 8.5,
     minZoom: 8.3,
     minZoom: 8.3,
   });
   });
+  mapInstance.value = map;
 
 
-  // 区县颜色映射
+  // 区县颜色映射(保持不变)
   const districtColorMap = {
   const districtColorMap = {
     "武江区": "#FF6B6B",
     "武江区": "#FF6B6B",
     "浈江区": "#4ECDC4",
     "浈江区": "#4ECDC4",
@@ -44,7 +114,7 @@ onMounted(() => {
     "南雄市": "#06D6A0",
     "南雄市": "#06D6A0",
   };
   };
 
 
-  // 加载区县边界
+  // 加载区县边界(保持不变)
   fetch('/data/韶关市各区县边界图.geojson')
   fetch('/data/韶关市各区县边界图.geojson')
     .then(res => {
     .then(res => {
       if (!res.ok) throw new Error(`区县边界加载失败:${res.status}`);
       if (!res.ok) throw new Error(`区县边界加载失败:${res.status}`);
@@ -66,7 +136,32 @@ onMounted(() => {
         },
         },
       }).addTo(map);
       }).addTo(map);
 
 
-      // 加载大气数据
+       fetch('/data/乐昌市.geoJson') // 假设文件路径为 /data/乐昌市.geojson
+        .then(res => {
+          if (!res.ok) throw new Error(`乐昌市边界加载失败:${res.status}`);
+          return res.json();
+        })
+        .then(lechangGeojson => {
+          console.log('✅ 乐昌市边界数据加载完成', lechangGeojson);
+          
+          // 为乐昌市设置更突出的样式(与原有边界区分)
+          L.geoJSON(lechangGeojson, {
+            style: () => {
+              return {
+                fillColor: districtColorMap["乐昌市"], // 复用原有颜色
+                fillOpacity: 0.5, // 透明度略低,避免覆盖原有边界
+                color: '#000000', // 边框颜色加深
+                weight: 4, // 边框加粗,突出显示
+                dashArray: '5, 5', // 可选:添加虚线效果,进一步区分
+              };
+            },
+          }).addTo(map);
+        })
+        .catch(err => {
+          console.warn('⚠️ 乐昌市边界加载失败(不影响主地图):', err);
+        });
+
+      // 加载大气数据并创建标记点
       fetch('http://localhost:3000/table/Atmosphere_summary_data')
       fetch('http://localhost:3000/table/Atmosphere_summary_data')
         .then(res => {
         .then(res => {
           if (!res.ok) throw new Error(`大气数据加载失败:${res.status}`);
           if (!res.ok) throw new Error(`大气数据加载失败:${res.status}`);
@@ -74,11 +169,11 @@ onMounted(() => {
         })
         })
         .then(atmosphereData => {
         .then(atmosphereData => {
           console.log('✅ 大气数据加载完成,记录数:', atmosphereData.length);
           console.log('✅ 大气数据加载完成,记录数:', atmosphereData.length);
+          markers.value = []; // 清空标记点数组
           
           
-          let markerCount = 0;
           atmosphereData.forEach((item, idx) => {
           atmosphereData.forEach((item, idx) => {
             try {
             try {
-              // 提取经纬度
+              // 提取经纬度(保持不变)
               const latField = ['latitude', 'lat', '纬度'].find(key => item[key] !== undefined);
               const latField = ['latitude', 'lat', '纬度'].find(key => item[key] !== undefined);
               const lngField = ['longitude', 'lng', '经度'].find(key => item[key] !== undefined);
               const lngField = ['longitude', 'lng', '经度'].find(key => item[key] !== undefined);
               
               
@@ -87,114 +182,40 @@ onMounted(() => {
                 return;
                 return;
               }
               }
               
               
-              // 清理经纬度数据
+              // 清理经纬度数据(保持不变)
               const cleanLat = String(item[latField]).replace(/[^\d.-]/g, '');
               const cleanLat = String(item[latField]).replace(/[^\d.-]/g, '');
               const cleanLng = String(item[lngField]).replace(/[^\d.-]/g, '');
               const cleanLng = String(item[lngField]).replace(/[^\d.-]/g, '');
               
               
               const lat = parseFloat(parseFloat(cleanLat).toFixed(6));
               const lat = parseFloat(parseFloat(cleanLat).toFixed(6));
               const lng = parseFloat(parseFloat(cleanLng).toFixed(6));
               const lng = parseFloat(parseFloat(cleanLng).toFixed(6));
               
               
-              // 坐标范围校验
+              // 坐标范围校验(保持不变)
               if (isNaN(lat) || isNaN(lng) || lat < 22.7 || lat > 25.5 || lng < 112.7 || lng > 115.3) {
               if (isNaN(lat) || isNaN(lng) || lat < 22.7 || lat > 25.5 || lng < 112.7 || lng > 115.3) {
                 console.warn(`❌ 坐标超出范围(第${idx}条):`, lat, lng);
                 console.warn(`❌ 坐标超出范围(第${idx}条):`, lat, lng);
                 return;
                 return;
               }
               }
               
               
-              // 创建标记点
+              // 创建标记点(保持不变)
               const marker = L.circleMarker([lat, lng], {
               const marker = L.circleMarker([lat, lng], {
-                radius: 3.5, // 略微减小标记点大小
+                radius: 3.5,
                 color: '#FF3333',
                 color: '#FF3333',
                 fillColor: '#FF3333',
                 fillColor: '#FF3333',
                 fillOpacity: 0.9,
                 fillOpacity: 0.9,
-                weight: 1.5, // 减小边框宽度
+                weight: 1.5,
                 zIndexOffset: 1000,
                 zIndexOffset: 1000,
               }).addTo(map);
               }).addTo(map);
 
 
-              // 构建弹窗内容(双列布局)
-              const content = `
-                <div class="popup-container">
-                  <div class="popup-header">
-                    <h3 class="popup-title">${item.采样 || '未知采样点'}</h3>
-                  </div>
-                  <ul class="popup-info-list">
-                    <li>
-                      <span class="info-label">采样点ID:</span>
-                      <span class="info-value">${item.样品名称 || '未知'}</span>
-                    </li>
-                    <li>
-                      <span class="info-label">样品编号:</span>
-                      <span class="info-value">${item.样品编号 || '未知'}</span>
-                    </li>
-                  </ul>
-                  <div class="grid-container" style="grid-template-columns: 1fr 1fr;">
-                    <div class="grid-item">
-                      <div class="data-item">
-                        <span class="item-label">Cr mg/kg:</span>
-                        <span class="item-value">${formatValue(item['Cr mg/kg'])}</span>
-                      </div>
-                      <div class="data-item">
-                        <span class="item-label">Cr ug/m3:</span>
-                        <span class="item-value">${formatValue(item['Cr ug/m3'])}</span>
-                      </div>
-                      <div class="data-item">
-                        <span class="item-label">As mg/kg:</span>
-                        <span class="item-value">${formatValue(item['As mg/kg'])}</span>
-                      </div>
-                      <div class="data-item">
-                        <span class="item-label">As ug/m3:</span>
-                        <span class="item-value">${formatValue(item['As ug/m3'])}</span>
-                      </div>
-                      <div class="data-item">
-                        <span class="item-label">Cd mg/kg:</span>
-                        <span class="item-value">${formatValue(item['Cd mg/kg'])}</span>
-                      </div>
-                      <div class="data-item">
-                        <span class="item-label">Cd ug/m3:</span>
-                        <span class="item-value">${formatValue(item['Cd ug/m3'])}</span>
-                      </div>
-                    </div>
-                    <div class="grid-item">
-                      <div class="data-item">
-                        <span class="item-label">Hg mg/kg:</span>
-                        <span class="item-value">${formatValue(item['Hg mg/kg'])}</span>
-                      </div>
-                      <div class="data-item">
-                        <span class="item-label">Hg ug/m3:</span>
-                        <span class="item-value">${formatValue(item['Hg ug/m3'])}</span>
-                      </div>
-                      <div class="data-item">
-                        <span class="item-label">Pb mg/kg:</span>
-                        <span class="item-value">${formatValue(item['Pb mg/kg'])}</span>
-                      </div>
-                      <div class="data-item">
-                        <span class="item-label">Pb ug/m3:</span>
-                        <span class="item-value">${formatValue(item['Pb ug/m3'])}</span>
-                      </div>
-                      <div class="data-item">
-                        <span class="item-label">颗粒物重量 mg:</span>
-                        <span class="item-value">${formatValue(item['颗粒物的重量 mg'])}</span>
-                      </div>
-                      <div class="data-item">
-                        <span class="item-label">标准体积 m3:</span>
-                        <span class="item-value">${formatValue(item['标准体积 m3'])}</span>
-                      </div>
-                      <div class="data-item">
-                        <span class="item-label">颗粒物浓度 ug/m3:</span>
-                        <span class="item-value">${formatValue(item['颗粒物浓度ug/m3'])}</span>
-                      </div>
-                    </div>
-                  </div>
-                </div>
-              `;
-
-              marker.bindPopup(content);
-              markerCount++;
+              // 绑定初始弹窗内容(根据默认计算方式)
+              marker.bindPopup(generatePopupContent(item, props.calculationMethod));
+              
+              // 保存标记点实例和对应数据,用于后续更新
+              markers.value.push({ marker, item });
             } catch (err) {
             } catch (err) {
               console.error(`❌ 处理大气数据失败(第${idx}条):`, err);
               console.error(`❌ 处理大气数据失败(第${idx}条):`, err);
             }
             }
           });
           });
 
 
-          console.log(`✅ 成功创建 ${markerCount} 个大气数据标记点`);
+          console.log(`✅ 成功创建 ${markers.value.length} 个大气数据标记点`);
         })
         })
         .catch(err => {
         .catch(err => {
           console.error('❌ 大气数据加载失败:', err);
           console.error('❌ 大气数据加载失败:', err);
@@ -206,9 +227,22 @@ onMounted(() => {
       alert('区县边界加载错误:' + err.message);
       alert('区县边界加载错误:' + err.message);
     });
     });
 });
 });
+
+// 4. 监听计算方式变化,更新所有标记点的弹窗内容
+watch(
+  () => props.calculationMethod,
+  (newMethod) => {
+    markers.value.forEach(({ marker, item }) => {
+      // 重新绑定弹窗内容(使用新的计算方式)
+      marker.bindPopup(generatePopupContent(item, newMethod));
+    });
+    console.log(`✅ 已切换为${newMethod === 'weight' ? '重量' : '体积'}计算方式,弹窗内容已更新`);
+  }
+);
 </script>
 </script>
 
 
 <style scoped>
 <style scoped>
+/* 样式保持不变,仅需确保弹窗内容布局适配单组数据 */
 .map-wrapper {
 .map-wrapper {
   width: 100%;
   width: 100%;
   height: 100%;
   height: 100%;
@@ -229,12 +263,12 @@ onMounted(() => {
 ::v-deep .leaflet-popup-content {
 ::v-deep .leaflet-popup-content {
   margin: 0 !important;
   margin: 0 !important;
   width: auto !important;
   width: auto !important;
-  max-width: 400px; /* 减小最大宽度 */
+  max-width: 300px; /* 适当减小最大宽度,适配单列布局 */
 }
 }
 
 
 ::v-deep .popup-container {
 ::v-deep .popup-container {
-  min-width: 280px; /* 减小最小宽度 */
-  padding: 12px; /* 减小内边距 */
+  min-width: 280px;
+  padding: 12px;
   font-family: "Microsoft YaHei", sans-serif;
   font-family: "Microsoft YaHei", sans-serif;
 }
 }
 
 
@@ -244,7 +278,7 @@ onMounted(() => {
 
 
 ::v-deep .popup-title {
 ::v-deep .popup-title {
   text-align: center;
   text-align: center;
-  font-size: 16px; /* 减小标题字体大小 */
+  font-size: 16px;
   font-weight: 700;
   font-weight: 700;
   color: #0066CC;
   color: #0066CC;
   margin: 0 0 5px;
   margin: 0 0 5px;
@@ -258,59 +292,60 @@ onMounted(() => {
   margin: 0 0 10px;
   margin: 0 0 10px;
   display: grid;
   display: grid;
   grid-template-columns: 1fr 1fr;
   grid-template-columns: 1fr 1fr;
-  gap: 6px; /* 减小间隙 */
+  gap: 6px;
 }
 }
 
 
 ::v-deep .popup-info-list li {
 ::v-deep .popup-info-list li {
   display: flex;
   display: flex;
   margin: 0;
   margin: 0;
-  padding: 3px 6px; /* 减小内边距 */
+  padding: 3px 6px;
   background: #f9f9f9;
   background: #f9f9f9;
-  border-radius: 3px; /* 减小圆角 */
+  border-radius: 3px;
 }
 }
 
 
 ::v-deep .info-label {
 ::v-deep .info-label {
-  flex: 0 0 85px; /* 减小标签宽度 */
+  flex: 0 0 85px;
   font-weight: 600;
   font-weight: 600;
   color: #333;
   color: #333;
-  font-size: 13px; /* 减小字体大小 */
+  font-size: 13px;
 }
 }
 
 
 ::v-deep .info-value {
 ::v-deep .info-value {
   flex: 1;
   flex: 1;
   color: #666;
   color: #666;
-  font-size: 13px; /* 减小字体大小 */
+  font-size: 13px;
+   white-space: nowrap;
 }
 }
 
 
 ::v-deep .grid-container {
 ::v-deep .grid-container {
   display: grid;
   display: grid;
-  grid-template-columns: 1fr 1fr;
-  gap: 6px; /* 减小间隙 */
+  grid-template-columns: 1fr; /* 改为单列布局,适配分类后的指标 */
+  gap: 6px;
 }
 }
 
 
 ::v-deep .grid-item {
 ::v-deep .grid-item {
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
-  gap: 6px; /* 减小间隙 */
+  gap: 6px;
 }
 }
 
 
 ::v-deep .data-item {
 ::v-deep .data-item {
   display: flex;
   display: flex;
   justify-content: space-between;
   justify-content: space-between;
-  padding: 6px 8px; /* 减小内边距 */
+  padding: 6px 8px;
   background: #f9f9f9;
   background: #f9f9f9;
-  border-radius: 3px; /* 减小圆角 */
+  border-radius: 3px;
 }
 }
 
 
 ::v-deep .item-label {
 ::v-deep .item-label {
   font-weight: 600;
   font-weight: 600;
   color: #555;
   color: #555;
-  font-size: 13px; /* 减小字体大小 */
+  font-size: 13px;
 }
 }
 
 
 ::v-deep .item-value {
 ::v-deep .item-value {
   color: #000;
   color: #000;
-  font-size: 13px; /* 减小字体大小 */
+  font-size: 13px;
 }
 }
 
 
 /* 隐藏弹窗箭头 */
 /* 隐藏弹窗箭头 */
@@ -328,4 +363,4 @@ onMounted(() => {
   fill-opacity: 1 !important;
   fill-opacity: 1 !important;
   stroke-width: 2.5px !important;
   stroke-width: 2.5px !important;
 }
 }
-</style>    
+</style>

+ 2 - 2
src/views/User/HmOutFlux/irrigationWater/crosssectionmap.vue

@@ -149,11 +149,11 @@ onMounted(() => {
               // 绑定弹窗内容
               // 绑定弹窗内容
               marker.bindPopup(`
               marker.bindPopup(`
                 <div class="popup-container">
                 <div class="popup-container">
-                  <h3 class="popup-title">断面位置:${item.断面位置}</h3>
+                  <h3 class="popup-title">所属河流:</strong> ${item.所属河流}</h3>
                   <div class="popup-divider"></div>
                   <div class="popup-divider"></div>
     
     
                   <p><strong>断面编号:</strong> ${item.断面编号}</p>
                   <p><strong>断面编号:</strong> ${item.断面编号}</p>
-                  <p><strong>所属河流:</strong> ${item.所属河流}</p>
+                  <p><strong>断面位置:${item.断面位置}</p>
                   <p><strong>所属区县:</strong> ${item.所属区县}</p>
                   <p><strong>所属区县:</strong> ${item.所属区县}</p>
                   <p><strong>镉(Cd)含量:</strong> ${formattedCd} 
                   <p><strong>镉(Cd)含量:</strong> ${formattedCd}