yes-yes-yes-k 5 днів тому
батько
коміт
90e562104d

+ 0 - 1
.eslintrc-auto-import.json

@@ -5,7 +5,6 @@
     "ComputedRef": true,
     "DirectiveBinding": true,
     "EffectScope": true,
-    "ElMessage": true,
     "ExtractDefaultPropTypes": true,
     "ExtractPropTypes": true,
     "ExtractPublicPropTypes": true,

+ 6 - 1
src/components/layout/AppLayout.vue

@@ -175,6 +175,10 @@ const backgroundStyle = computed(() => ({
 const isFullScreen = computed(() => route.meta.fullScreen === true);
 const isSelectCity = computed(() => route.path === "/select-city");
 
+const isDataDashboard = computed(() => {
+  return route.path === '/totalIntroduction'
+})
+
 // 当前用户信息
 const userInfo = reactive({
   name: tokenStore.userName,
@@ -309,7 +313,7 @@ const AsideComponent = computed(() => {
 
 const showAside = computed(
   () =>
-    !isFullScreen.value && !["cropRiskAssessment"].includes(activeName.value)
+    !isFullScreen.value && !["cropRiskAssessment"].includes(activeName.value) && !isDataDashboard.value
 );
 
 const mainStyle = computed(() => ({
@@ -317,6 +321,7 @@ const mainStyle = computed(() => ({
     ? "0"
     : "20px",
   overflow: "hidden",
+  width:isDataDashboard.value?'100%':'auto'   //数据看板页占满宽度
 }));
 
 // 登出逻辑 - 支持多语言

+ 841 - 191
src/views/User/introduction/TotalIntroduction.vue

@@ -1,260 +1,910 @@
-<template>
-  <div class="total-introduction">
-    <!-- 背景层 -->
-    <div class="background-layer"></div>
-
-    <!-- 顶部标题 -->
-    <!-- <div class="header-title">
-      <h1>数据看板</h1>
-    </div> -->
-
-    <!-- 主要内容区域 -->
-    <div class="main-content">
-      <!-- 左侧区域 -->
-      <div class="left-section">
-        <CropcdStatictics/>
-      </div>
+<script setup>
+   import { ref, watch, onMounted, onBeforeUnmount, onUnmounted ,computed,nextTick} from 'vue'
+   import L from 'leaflet'
+   import 'leaflet/dist/leaflet.css'
+   import { api8000 } from '@/utils/request'
+
+   const samplePointsData = ref([])
+   const mapRef = ref(null)
+   let map = null
+   let wmsLayer = null
+   let geoJsonLayer = null
+
+
+   const statistics = ref({
+     totalBlocks: 0,
+     avgPH: 0,
+     strongAcidCount: 0,
+     mildAcidCount: 0,
+     normalCount: 0,
+     maxPH: 0,
+     minPH: 0
+   })
+
+  //  
+   const phDistribution = ref({
+     range1: 0,
+     range2: 0,
+     range3: 0,
+   })
+
+   const selectedPoint = ref(null)
+
+   const CONFIG = {
+    center:[25.202903, 113.25383],
+    zoom:11,
+    getPoint:'/api/vector/export/all?table_name=le_soil_data&format=geojson',
+    geoserver:{
+      url:'/geoserver',
+      workspace:'acidmap',
+      layerGroup:'leshujukanbanmap',
+      dataLayer:'le_soil_data',
+      wmsUrl:'/geoserver/acidmap/wms'
+    }
+   }
 
-      <!-- 中间区域 -->
-      <div class="center-section">
-        <!-- 地图区域 -->
-       
-        <div id="map-container" class="map-containter" ref="mapContainer">
-          <div v-if="mapLoading" class="map-loading">
-            <div class="spinner"></div>
-            <p>地图加载中...</p>
-          </div>
-          <div v-if="mapError" class="map-error">
-            <p>地图加载失败,请刷新重试</p>
-            <button @click="reloadMap" class="reload-btn">重新加载</button>
-          </div>
 
-        </div>
+   // 获取 pH 等级对应的 CSS 类
+function getPHLevelClass(ph) {
+    // ✅ 先转换为数字
+    const numericPh = typeof ph === 'string' ? parseFloat(ph) : ph
+    if (!numericPh || numericPh === 0) return ''
+    if (numericPh <= 5.2) return 'danger'
+    if (numericPh < 6.0) return 'warning'
+    return 'success'
+  }
 
-        <FluxcdStatictics/>
 
-      </div>
+// 获取综合评语
+function getPHComment(avgPH) {
+  if (avgPH <= 4.5) return '🔴 土壤严重酸化,需立即治理!'
+  if (avgPH <= 5.2) return '🟠 土壤酸化明显,建议尽快改良'
+  if (avgPH < 6.0) return '🟡 土壤微酸性,注意保持'
+  return '🟢 土壤酸碱度适宜,状态良好'
+}
 
-      <!-- 右侧区域 -->
-      <div class="right-section">
-       <EffcdStatistics/>
-      </div>
-    </div>
-  </div>
-</template>
+  
+  function getPHRangeLabel(range) {
+  const labels = {
+    range1: '< 5.2',
+    range2: '5.2 - 6.0',
+    range3: ' >6.0 ',
+  }
+  return labels[range] || range
+  }
 
-<script setup>
-import { ref, onMounted, nextTick } from 'vue';
-import FluxcdStatictics from "@/components/soilcdStatistics/fluxcdStatictics.vue";
-import { ElMessage } from 'element-plus';
-import { useI18n } from 'vue-i18n';
-import CropcdStatictics from '@/components/soilcdStatistics/cropcdStatictics.vue';
-
-const { t } = useI18n();
-
-// 地图相关
-let L = null;
-const map = ref(null);
-const mapContainer = ref(null);
-const mapLoading = ref(false);
-const mapError = ref(false);
-const initMap = async () => {
-  mapLoading.value = true;
-  mapError.value = false;
+  // 添加分页加载支持
+   const batchSize = 1000;
+   let allFeatures = [];
 
-  try {
-    // 动态导入 Leaflet
-    if (!L) {
-      L = await import('leaflet');
-      await import('leaflet/dist/leaflet.css');
-      
-      delete (L.Icon.Default.prototype)._getIconUrl;
-      L.Icon.Default.mergeOptions({
-        iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
-        iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
-        shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
-      });
-    }
+  async function initMap(){
+    await nextTick()
 
-    // 清除现有地图
-   const container = document.getElementById('map-container');
+    if(!mapRef.value) return 
+    map = L.map(mapRef.value).setView(CONFIG.center,CONFIG.zoom)
 
-    // 创建地图实例
-    map.value = L.map(container, {
-      zoomControl: true,
-      attributionControl: false,
-      center: [25.202903, 113.25383],
-      zoom: 10
+    wmsLayer = L.tileLayer.wms(CONFIG.geoserver.wmsUrl, {
+      layers: `${CONFIG.geoserver.workspace}:${CONFIG.geoserver.layerGroup}`,
+      format: 'image/png',
+      transparent: true,
+      version: '1.1.0',
+      srs:'EPSG:4326',
+      attribution: '© GeoServer - Acidmap'
+    }).addTo(map)
+
+  }
+
+  function createPointIcon(feature) {
+    const ph = feature.properties.ph || feature.properties.value
+    let color = '#22c55e'
+    if (ph <= 5.2) color = '#ef4444'
+    else if (ph < 6.0) color = '#f59e0b'
+
+    return L.divIcon({
+      className: 'custom-marker',
+      html: `<div style="
+        background-color: ${color};
+        width: 12px;
+        height: 12px;
+        border-radius: 50%;
+        border: 2px solid white;
+        box-shadow: 0 2px 4px rgba(0,0,0,0.3);
+      "></div>`,
+      iconSize: [12, 12],
+      iconAnchor: [6, 6]
+    })
+  }
+
+  function parsePHValue(phValue) {
+    if (!phValue && phValue !== 0) return null
+    const numericPh = typeof phValue === 'string' ? parseFloat(phValue) : phValue
+    return !isNaN(numericPh) && numericPh > 0 ? numericPh : null
+  }
+
+
+  async function loadStatistics() {
+  try {
+    // 等待数据加载完成
+    if (samplePointsData.value.length === 0) return;
+
+    let phCount = 0
+    let avgPH = 0
+    let strongAcidCount = 0
+    let mildAcidCount = 0
+    let normalCount = 0
+    let maxPH = -Infinity
+    let minPH = Infinity
+
+    samplePointsData.value.forEach(feature => {
+      const numericPh = parsePHValue(feature.properties.ph || feature.properties.value)
+      
+      if (numericPh && numericPh > 0 && !isNaN(numericPh)) {
+        phCount++
+        const delta = numericPh - avgPH;
+        avgPH = avgPH + delta / phCount;
+        maxPH = Math.max(maxPH, numericPh)
+        minPH = Math.min(minPH, numericPh)
+        
+        if (numericPh <= 5.2) strongAcidCount++
+        else if (numericPh < 6.0) mildAcidCount++
+        else normalCount++
+      }
     });
 
-    // WMS 配置
-    const GEOSERVER_CONFIG = {
-      url: "/geoserver/wms",
-      workspace: "acidmap",
-      layerGroup: "mapwithboundary", 
+    statistics.value = {
+      totalBlocks: samplePointsData.value.length,
+      avgPH:  phCount > 0 ? parseFloat(avgPH.toFixed(2)) : 0,  // ✅ 确保是数字
+      strongAcidCount,
+      mildAcidCount,
+      normalCount,
+      maxPH: maxPH === -Infinity ? 0 : parseFloat(maxPH.toFixed(2)),
+      minPH: minPH === Infinity ? 0 : parseFloat(minPH.toFixed(2))
     };
 
-    // WMS 图层配置
-    const wmsLayer = L.tileLayer.wms(GEOSERVER_CONFIG.url, {
-      layers: `${GEOSERVER_CONFIG.workspace}:${GEOSERVER_CONFIG.layerGroup}`,
-      format: "image/png",
-      transparent: true,
-      version: "1.1.1",
-      crs: L.CRS.EPSG4326,
-      attribution: "Data from GeoServer"
-    });
+    console.log('✅ 统计数据加载完成:', statistics.value);
+  } catch (err) {
+    console.error('加载统计数据失败:', err);
+  }
+}
+
+// 修改为只获取数据用于统计,不渲染到地图
+  async function fetchDataForStatistics() {
+    try {
+      const response = await fetch(
+        `${CONFIG.geoserver.url}/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=${CONFIG.geoserver.workspace}:${CONFIG.geoserver.dataLayer}&outputFormat=application/json`
+      );
+      
+      if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
+      
+      const geoJsonData = await response.json();
+      
+      console.log('✅ 获取到数据:', geoJsonData.features.length, '条记录');
+
+      // 保存数据用于统计和交互
+      allFeatures = geoJsonData.features;
+      samplePointsData.value = geoJsonData.features;
+      
+      // 计算统计信息
+      calculatePHDistribution(allFeatures);
+      loadStatistics();
+      
+      // 添加点击事件监听(不显示标记点)
+      map.on('click', function(e) {
+        const latlng = e.latlng;
+        // 查找最近的采样点
+        findNearestPoint(latlng);
+      });
+      
+    } catch (err) {
+      console.error('❌ 加载统计数据失败:', err);
+    }
+  }
+
+  // 计算 pH 分布
+  function calculatePHDistribution(features) {
+    const distribution = {
+      range1: 0,
+      range2: 0,
+      range3: 0
+    }
 
-    // 添加图层到地图
-    wmsLayer.addTo(map.value);
-    // 等待地图渲染完成后调整大小
-    setTimeout(() => {
-      if (map.value) {
-        map.value.invalidateSize();
+    features.forEach(feature => {
+      const phValue = feature.properties.ph || feature.properties.value
+      const numericPh = typeof phValue === 'string' ? parseFloat(phValue) : phValue
+      if (numericPh && numericPh > 0) {
+        if (numericPh <= 5.2) distribution.range1++
+        else if (numericPh <= 6) distribution.range2++
+        else  distribution.range3++
       }
-    }, 100);
+    })
 
-    mapLoading.value = false;
+    phDistribution.value = distribution
+  }
 
-  } catch (error) {
-    console.error('地图初始化失败:', error);
-    mapError.value = true;
-    mapLoading.value = false;
+  // 查找最近的采样点
+  function findNearestPoint(latlng) {
+    let nearestPoint = null;
+    let minDistance = Infinity;
+    
+    allFeatures.forEach(feature => {
+      const coords = feature.geometry.coordinates;
+      if (coords && coords.length >= 2) {
+        const pointLat = coords[1];
+        const pointLng = coords[0];
+        const distance = Math.sqrt(
+          Math.pow(pointLat - latlng.lat, 2) + 
+          Math.pow(pointLng - latlng.lng, 2)
+        );
+        
+        if (distance < minDistance && distance < 0.01) { // 10 米范围内
+          minDistance = distance;
+          nearestPoint = feature;
+        }
+      }
+    });
     
-    let errorMessage = t('AcidModelMap.mapInitError');
-    if (error instanceof Error) {
-      errorMessage += ': ' + error.message;
+    if (nearestPoint) {
+      const ph = parsePHValue(nearestPoint.properties.ph || nearestPoint.properties.value);
+      
+      selectedPoint.value = {
+        ph: ph,
+        properties: nearestPoint.properties
+      };
     }
-    ElMessage.error(errorMessage);
   }
-};
 
-const reloadMap = () => {
-  initMap();
-};
 
-onMounted(() => {
-  // 等待地图渲染完成后调整大小
-    setTimeout(() => {
-      initMap()
-    }, 200);
-});
+onUnmounted(()=>{
+    if(map) {
+      map.remove()
+      map = null
+    }
+    if(wmsLayer) {
+      wmsLayer = null
+    }
+    if(markersLayer) {
+      markersLayer = null
+    }
+    if(geoJsonLayer) { // 原 markersLayer 改为 geoJsonLayer(代码中实际定义的是 geoJsonLayer)
+      geoJsonLayer = null
+    }
+    samplePointsData.value = []
+    selectedPoint.value = null
+  })
 
+  onMounted(async()=>{
+    await initMap()
+    await fetchDataForStatistics()
+    await loadStatistics()
+  })
 
+  onUnmounted(()=>{
+    if(map) map.remove()
+  })
 
 </script>
 
-<style scoped>
-.total-introduction {
+<template>
+  <div class="map-container">
+    <div class="ph-map" ref="mapRef"></div>
+
+    <!-- 计算刷新按钮 -->
+     <div class="compute">
+      <button class="combtn">实施降酸措施一周期后</button>
+      <button class="combtn">反酸一周期后</button>
+     </div>
+    <!-- pH 统计 -->
+    <div class="statistics-panel">
+      <h4>📊 乐昌县土壤 pH 统计</h4>
+      
+      <div class="stat-row">
+        <span class="stat-label">采样点总数:</span>
+        <span class="stat-value">{{ statistics.totalBlocks }} 个</span>
+      </div>
+      
+      <div class="stat-row highlight">
+        <span class="stat-label">平均 pH 值:</span>
+        <span class="stat-value" :class="getPHLevelClass(parseFloat(statistics.avgPH))">
+          {{ statistics.avgPH || '-' }}
+        </span>
+      </div>
+      
+      <div class="stat-row">
+        <span class="stat-label">强酸性 (pH≤5.2):</span>
+        <span class="stat-value danger">{{ statistics.strongAcidCount }} 个</span>
+      </div>
+      
+      <div class="stat-row">
+        <span class="stat-label">弱酸性 (pH 5.2~6.0):  </span>
+        <span class="stat-value warning">{{ statistics.mildAcidCount }} 个</span>
+      </div>
+      
+      <div class="stat-row">
+        <span class="stat-label">正常 (pH≥6.0):</span>
+        <span class="stat-value success">{{ statistics.normalCount }} 个</span>
+      </div>
+      
+      <div class="stat-divider"></div>
+      
+      <div class="stat-row small">
+        <span class="stat-label">最高 pH:</span>
+        <span class="stat-value success">{{ statistics.maxPH || '-' }}</span>
+      </div>
+      
+      <div class="stat-row small">
+        <span class="stat-label">最低 pH:</span>
+        <span class="stat-value danger">{{ statistics.minPH || '-' }}</span>
+      </div>
+      
+      <div class="stat-comment" v-if="statistics.avgPH > 0">
+        <div class="comment-title">📝 综合评估:</div>
+        <div class="comment-text" :class="getPHLevelClass(statistics.avgPH)">
+          {{ getPHComment(statistics.avgPH) }}
+        </div>
+      </div>
+    </div>
+
+    <!-- ph分布 -->
+    <div class="distribution-chart">
+      <h4>📈 pH 分布</h4>
+      <div class="bar-chart">
+        <div 
+          v-for="(value, key) in phDistribution" 
+          :key="key"
+          class="bar-item"
+        >
+          <div class="bar-label">{{ getPHRangeLabel(key) }}</div>
+          <div class="bar-container">
+            <div 
+              class="bar-fill" 
+              :style="{ width: `${(value / statistics.totalBlocks) * 100}%` }"
+              :class="
+              {
+               danger: key === 'range1',
+               warning: key === 'range2',
+               success: key === 'range3'
+              }
+              "
+            ></div>
+            <span class="bar-value">{{ value }}</span>
+          </div>
+        </div>
+      </div>
+    </div>
+    
+    <!-- 图例 -->
+    <div class="legend">
+      <h4>pH 值图例</h4>
+      <div class="legend-item">
+        <span class="legend-color" style="background: #ef4444;"></span>
+        <span>≤ 5.2 (强酸性)</span>
+      </div>
+      <div class="legend-item">
+        <span class="legend-color" style="background: #eab308;"></span>
+        <span>5.2 - 6.0 (弱酸性)</span>
+      </div>
+      <div class="legend-item">
+        <span class="legend-color" style="background: #22c55e;"></span>
+        <span>≥ 6.0 (中性/碱性)</span>
+      </div>
+      <div class="legend-item">
+        <span class="legend-color" style="background: #cccccc;"></span>
+        <span>无数据</span>
+      </div>
+    </div>
+
+    <!-- 采样点详情 -->
+    <div class="point-detail-modal" v-if="selectedPoint">
+      <div class="detail-content">
+        <div class="detail-header">
+          <h4>📍 采样点详情</h4>
+          <button @click="selectedPoint = null" class="close-btn">×</button>
+        </div>
+        <div class="detail-row">
+          <span class="detail-label">pH 值:</span>
+          <span class="detail-value" :class="getPHLevelClass(selectedPoint.ph)">
+            {{ selectedPoint.ph?.toFixed(2) || '-' }}
+          </span>
+        </div>
+        <div class="detail-row">
+          <span class="detail-label">酸化程度:</span>
+          <span :class="['detail-value', getPHLevelClass(selectedPoint.ph)]">
+            {{ selectedPoint.ph <= 5.2 ? '强酸性' : selectedPoint.ph < 6.0 ? '弱酸性' : '正常' }}
+          </span>
+        </div>
+        <div class="detail-row">
+          <span class="detail-label">建议:</span>
+          <span class="detail-suggestion">
+            {{ selectedPoint.ph <= 5.2 ? '立即治理,施用石灰改良' : selectedPoint.ph < 6.0 ? '注意保持,适量施用有机肥' : '继续保持当前管理措施' }}
+          </span>
+        </div>
+      </div>
+    </div>
+
+   </div>
+</template>
+
+<style  scoped>
+.map-container{
   width: 100%;
-  height: 100%;
-  background-color: #0a0a2a;
-  color: white;
-  font-family: 'Arial', sans-serif;
-  position: relative;
+  height: 100vh;
+  position: absolute;
+  left:0;
+  top:0;
+  z-index: 1000;
 }
 
-.background-layer {
+.ph-map{
+  width: 65%;
+  height: 95%;
+  min-height: 500px;
+  border-radius: 16px; /* 圆角大小,可根据需要调整,比如 10px、20px */
+  border: 3px solid #1092d8; /* 蓝色边框,宽度和颜色可自定义 */
+  overflow: hidden; /* 关键:防止地图内容溢出圆角区域 */
+}
+
+/* ✅ 统计面板样式 */
+.statistics-panel {
   position: absolute;
-  top: 0;
-  left: 0;
-  width: 100%;
-  height: 100%;
-  background-image: url('https://via.placeholder.com/1920x1080/0a0a2a/0a0a2a');
-  background-size: cover;
-  z-index: -1;
+  top: 100px;
+  right: 10px;
+  background: rgba(255, 255, 255, 0.95);
+  padding: 20px;
+  border-radius: 12px;
+  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
+  z-index: 1000;
+  min-width: 350px;
+  backdrop-filter: blur(10px);
+  border: 2px solid rgba(16, 146, 216, 0.2);
+}
+
+.compute {
+  position: absolute;
+  display: flex;
+  gap: 15px;
+  top:5px;
+  right: 20px;
+  padding: 20px;
+  z-index: 1000;
+}
+
+.combtn {
+  padding: 15px 20px;
+  font-size: 14px;
+  font-weight: 600;
+  color: #fff;
+  background: linear-gradient(135deg, #1092d8 0%, #0d7bb8 100%);
+  border: none;
+  border-radius: 8px;
+  cursor: pointer;
+  box-shadow: 0 4px 12px rgba(16, 146, 216, 0.3);
+  transition: all 0.3s ease;
+  white-space: nowrap;
+}
+
+.combtn:hover {
+  background: linear-gradient(135deg, #0d7bb8 0%, #0a6598 100%);
+  box-shadow: 0 6px 16px rgba(16, 146, 216, 0.4);
+  transform: translateY(-2px);
 }
 
-.header-title {
-  text-align: center;
-  padding: 20px 20px 0 20px;
+.combtn:active {
+  transform: translateY(0);
+  box-shadow: 0 2px 8px rgba(16, 146, 216, 0.3);
 }
 
-.header-title h1 {
-  font-size: 32px;
-  color: #00bfff;
-  text-shadow: 0 0 10px rgba(0, 187, 255, 0.8);
-  border-bottom: 2px solid #00bfff;
+
+.statistics-panel h4 {
+  margin: 0 0 15px 0;
+  font-size: 18px;
+  color: #1092d8;
+  border-bottom: 2px solid #1092d8;
   padding-bottom: 10px;
+  font-weight: bold;
 }
 
-.main-content {
+.stat-row {
   display: flex;
-  gap: 20px;
-  padding: 20px;
-  overflow-x: auto;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 12px;
+  font-size: 14px;
+  line-height: 1.4; /* 新增行高,提升可读性 */
 }
 
-.left-section,
+.stat-row.highlight {
+  background: linear-gradient(to right, rgba(16, 146, 216, 0.1), transparent);
+  padding: 8px 12px;
+  border-radius: 6px;
+  margin-bottom: 15px;
+}
 
-.right-section {
-  flex: 1;
-  width: 200px;
+.stat-row.small {
+  font-size: 12px;
+  margin-bottom: 6px;
 }
-.center-section {
-  flex: 2;
-  width: 500px;
+
+.stat-label {
+  color: #666;
+  font-weight: 500;
 }
 
-.map-containter {
-  width: 100%;
-  height: 600px;
-  border: 2px solid #00bfff;
-  border-radius: 10px;
-  overflow: hidden;
-  position: relative;
-  background-color: rgba(10, 10, 42, 0.8);
+.stat-value {
+  font-weight: 600;
+  font-size: 16px;
+  color: #333;
+  letter-spacing: 0.5px; /* 新增字间距 */
+}
+
+.stat-value.danger {
+  color: #ef4444;
 }
 
-.map-loading,
-.map-error {
+.stat-value.warning {
+  color: #f59e0b;
+}
+
+.stat-value.success {
+  color: #22c55e;
+}
+
+.stat-divider {
+  height: 1px;
+  background: linear-gradient(to right, transparent, #ddd, transparent);
+  margin: 15px 0;
+}
+
+/* 综合评语 */
+.stat-comment {
+  margin-top: 15px;
+  padding: 12px;
+  background: rgba(243, 244, 246, 0.8);
+  border-radius: 8px;
+  border-left: 4px solid #1092d8;
+}
+
+.comment-title {
+  font-size: 12px;
+  color: #666;
+  margin-bottom: 6px;
+  font-weight: bold;
+}
+
+.comment-text {
+  font-size: 14px;
+  font-weight: bold;
+  color: #333;
+}
+
+.comment-text.danger {
+  color: #ef4444;
+}
+
+.comment-text.warning {
+  color: #f59e0b;
+}
+
+.comment-text.success {
+  color: #22c55e;
+}
+
+.distribution-chart {
   position: absolute;
-  top: 0;
-  left: 0;
-  width: 100%;
+  top:600px;
+  right: 10px;
+  background: rgba(255, 255, 255, 0.95);
+  padding: 15px;
+  border-radius: 12px;
+  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
+  z-index: 1000;
+  min-width: 350px;
+  backdrop-filter: blur(10px);
+}
+
+.distribution-chart h4 {
+  margin: 0 0 12px 0;
+  font-size: 16px;
+  color: #1092d8;
+  border-bottom: 2px solid #1092d8;
+  padding-bottom: 8px;
+}
+
+.bar-chart {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.bar-item {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.bar-label {
+  font-size: 11px;
+  color: #666;
+  min-width: 60px;
+}
+
+.bar-container {
+  flex: 1;
+  position: relative;
+  height: 20px;
+  background: #f0f0f0;
+  border-radius: 4px;
+  overflow: hidden;
+  border: 1px solid #e5e7eb;
+}
+
+.bar-fill {
   height: 100%;
+  transition: width 0.3s ease-in-out; /* 缓动动画更丝滑 */
+  border-radius: 3px; /* 圆角匹配容器 */
+}
+
+.bar-fill.danger {
+  background: #ef4444;
+}
+
+.bar-fill.warning {
+  background: #f59e0b;
+}
+
+.bar-fill.success {
+  background: #22c55e;
+}
+
+.bar-value {
+  position: absolute;
+  right: 8px;
+  top: 50%;
+  transform: translateY(-50%);
+  font-size: 11px;
+  font-weight: bold;
+  color: black; 
+  text-shadow: 0 1px 2px rgba(0,0,0,0.2); /* 新增文字阴影 */
+}
+
+.alert-panel {
+  position: absolute;
+  top: 20px;
+  left: 10px;
+  background: rgba(255, 255, 255, 0.95);
+  padding: 15px;
+  border-radius: 12px;
+  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
+  z-index: 1000;
+  min-width: 250px;
+  max-height: 300px;
+  overflow-y: auto;
+  backdrop-filter: blur(10px);
+}
+
+.alert-panel h4 {
+  margin: 0 0 12px 0;
+  font-size: 16px;
+  color: #ef4444;
+  border-bottom: 2px solid #ef4444;
+  padding-bottom: 8px;
+}
+
+.alert-list {
   display: flex;
   flex-direction: column;
+  gap: 6px;
+}
+
+.alert-item {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 8px;
+  background: rgba(239, 68, 68, 0.05);
+  border-radius: 6px;
+  cursor: pointer;
+  transition: all 0.3s;
+}
+
+.alert-item:hover {
+  background: rgba(239, 68, 68, 0.15);
+  transform: translateX(4px);
+}
+
+.alert-rank {
+  background: #ef4444;
+  color: white;
+  width: 20px;
+  height: 20px;
+  border-radius: 50%;
+  display: flex;
   align-items: center;
   justify-content: center;
-  background-color: rgba(0, 0, 0, 0.7);
+  font-size: 12px;
+  font-weight: bold;
+  flex-shrink: 0;
+}
+
+.alert-name {
+  flex: 1;
+  font-size: 13px;
+  color: #333;
+}
+
+.alert-ph {
+  font-size: 12px;
+  font-weight: bold;
+}
+
+.alert-ph.danger {
+  color: #ef4444;
+}
+
+/* 图例样式 */
+.legend {
+  position: absolute;
+  top: 30px;
+  left: 30px;
+  background: rgba(255, 255, 255, 0.95);
+  padding: 15px;
+  border-radius: 8px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
   z-index: 1000;
-  color: white;
+  min-width: 180px;
+  backdrop-filter: blur(4px);
+  z-index: 0;
 }
 
-.spinner {
-  width: 40px;
-  height: 40px;
-  border: 4px solid rgba(0, 187, 255, 0.3);
-  border-radius: 50%;
-  border-top-color: #00bfff;
-  animation: spin 1s linear infinite;
-  margin-bottom: 15px;
+.legend h4 {
+  margin: 0 0 10px 0;
+  font-size: 14px;
+  color: #333;
+  border-bottom: 2px solid #1092d8;
+  padding-bottom: 6px;
 }
 
-@keyframes spin {
-  to { transform: rotate(360deg); }
+.legend-item {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  margin-bottom: 6px;
+  font-size: 12px;
+  color: #555;
 }
 
-.map-error p {
-  color: #ff6b6b;
-  font-size: 16px;
+.legend-item:last-child {
+  margin-bottom: 0;
+}
+
+.legend-color {
+  width: 20px;
+  height: 20px;
+  border-radius: 4px;
+  border: 1px solid #ddd;
+  flex-shrink: 0;
+}
+
+.point-detail-modal {
+  position: fixed;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  background: white;
+  padding: 20px;
+  border-radius: 12px;
+  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
+  z-index: 2000;
+  min-width: 300px;
+  border: 1px solid rgba(16, 146, 216, 0.2); /* 新增边框 */
+  animation: fadeIn 0.3s ease; /* 新增淡入动画 */
+}
+
+/* 新增弹窗动画 */
+@keyframes fadeIn {
+  from {
+    opacity: 0;
+    transform: translate(-50%, -45%);
+  }
+  to {
+    opacity: 1;
+    transform: translate(-50%, -50%);
+  }
+}
+
+.detail-content {
+  position: relative;
+}
+
+.detail-content h4 {
+  margin: 0 0 15px 0;
+  color: #1092d8;
+  border-bottom: 2px solid #1092d8;
+  padding-bottom: 10px;
+}
+
+.detail-row {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 12px;
+  padding: 8px;
+  background: #f8f9fa;
+  border-radius: 6px;
+}
+
+.detail-label {
+  font-weight: 500;
+  color: #666;
+}
+
+.detail-value {
+  font-weight: bold;
+  color: #333;
+}
+
+.detail-value.danger {
+  color: #ef4444;
+}
+
+.detail-value.warning {
+  color: #f59e0b;
+}
+
+.detail-value.success {
+  color: #22c55e;
+}
+
+.detail-suggestion {
+  color: #1092d8;
+  font-size: 13px;
+  line-height: 1.5;
+}
+
+/* 自定义 Tooltip 样式 */
+:deep(.custom-tooltip) {
+  background: rgba(255, 255, 255, 0.95);
+  border: 2px solid #1092d8;
+  border-radius: 8px;
+  padding: 8px 12px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+  font-family: 'Microsoft YaHei', sans-serif;
+  backdrop-filter: blur(4px);
+}
+
+.custom-marker {
+  background: transparent !important;
+  border: none !important;
+}
+
+.detail-content {
+  position: relative;
+}
+
+.detail-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
   margin-bottom: 15px;
 }
 
-.reload-btn {
-  padding: 10px 20px;
-  background-color: #00bfff;
-  color: white;
+
+.close-btn {
+  background: none;
   border: none;
-  border-radius: 5px;
+  font-size: 20px;
   cursor: pointer;
-  font-size: 14px;
-  transition: all 0.3s;
+  color: #909399;
+  padding: 0;
+  width: 20px;
+  height: 20px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border-radius: 50%; /* 新增圆形背景 */
+  transition: all 0.2s; /* 新增过渡 */
 }
 
-.reload-btn:hover {
-  background-color: #009acd;
-  transform: translateY(-2px);
+.close-btn:hover {
+  color: #ef4444; /* 改为红色更醒目 */
+  background: #fef2f2; /* 新增背景色 */
 }
-
 </style>