|
|
@@ -1,5 +1,5 @@
|
|
|
<script setup>
|
|
|
- import { ref, watch, onMounted, onBeforeUnmount, onUnmounted } from 'vue'
|
|
|
+ import { ref, watch, onMounted, onBeforeUnmount, onUnmounted ,computed,nextTick} from 'vue'
|
|
|
import L from 'leaflet'
|
|
|
import 'leaflet/dist/leaflet.css'
|
|
|
import { api8000 } from '@/utils/request'
|
|
|
@@ -8,6 +8,8 @@
|
|
|
const mapRef = ref(null)
|
|
|
let map = null
|
|
|
let wmsLayer = null
|
|
|
+ let geoJsonLayer = null
|
|
|
+
|
|
|
|
|
|
const statistics = ref({
|
|
|
totalBlocks: 0,
|
|
|
@@ -19,9 +21,18 @@
|
|
|
minPH: 0
|
|
|
})
|
|
|
|
|
|
+ //
|
|
|
+ const phDistribution = ref({
|
|
|
+ range1: 0,
|
|
|
+ range2: 0,
|
|
|
+ range3: 0,
|
|
|
+ })
|
|
|
+
|
|
|
+ const selectedPoint = ref(null)
|
|
|
+
|
|
|
const CONFIG = {
|
|
|
center:[25.202903, 113.25383],
|
|
|
- zoom:9.5,
|
|
|
+ zoom:11,
|
|
|
getPoint:'/api/vector/export/all?table_name=le_soil_data&format=geojson',
|
|
|
geoserver:{
|
|
|
url:'/geoserver',
|
|
|
@@ -35,11 +46,13 @@
|
|
|
|
|
|
// 获取 pH 等级对应的 CSS 类
|
|
|
function getPHLevelClass(ph) {
|
|
|
- if (!ph || ph === 0) return ''
|
|
|
- if (ph <= 5.2) return 'danger'
|
|
|
- if (ph < 6.0) return 'warning'
|
|
|
- return 'success'
|
|
|
-}
|
|
|
+ // ✅ 先转换为数字
|
|
|
+ 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'
|
|
|
+ }
|
|
|
|
|
|
|
|
|
// 获取综合评语
|
|
|
@@ -50,6 +63,20 @@ function getPHComment(avgPH) {
|
|
|
return '🟢 土壤酸碱度适宜,状态良好'
|
|
|
}
|
|
|
|
|
|
+
|
|
|
+ function getPHRangeLabel(range) {
|
|
|
+ const labels = {
|
|
|
+ range1: '< 5.2',
|
|
|
+ range2: '5.2 - 6.0',
|
|
|
+ range3: ' >6.0 ',
|
|
|
+ }
|
|
|
+ return labels[range] || range
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加分页加载支持
|
|
|
+ const batchSize = 1000;
|
|
|
+ let allFeatures = [];
|
|
|
+
|
|
|
async function initMap(){
|
|
|
await nextTick()
|
|
|
|
|
|
@@ -67,90 +94,187 @@ function getPHComment(avgPH) {
|
|
|
|
|
|
}
|
|
|
|
|
|
- async function loadLeSoilData() {
|
|
|
+ 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 {
|
|
|
- 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('✅ le_soil_data 数据:', geoJsonData.features);
|
|
|
-
|
|
|
- if (geoJsonData.features && geoJsonData.features.length > 0) {
|
|
|
- // 处理数据...
|
|
|
- geoJsonData.features.forEach(feature => {
|
|
|
- const ph = feature.properties.ph || feature.properties.value;
|
|
|
- if (ph && ph > 0) {
|
|
|
- // console.log('📌 采样点:', feature.properties.TXZQMC, 'pH:', ph);
|
|
|
- }
|
|
|
- });
|
|
|
- } else {
|
|
|
- console.warn('⚠️ 没有找到 le_soil_data 数据');
|
|
|
- }
|
|
|
+ // 等待数据加载完成
|
|
|
+ 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++
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ 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))
|
|
|
+ };
|
|
|
+
|
|
|
+ console.log('✅ 统计数据加载完成:', statistics.value);
|
|
|
} catch (err) {
|
|
|
- console.error('❌ 加载 le_soil_data 失败:', err);
|
|
|
+ console.error('加载统计数据失败:', err);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- async function loadStatistics() {
|
|
|
+// 修改为只获取数据用于统计,不渲染到地图
|
|
|
+ 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, '条记录');
|
|
|
|
|
|
- if (geoJsonData.features && geoJsonData.features.length > 0) {
|
|
|
- let phCount = 0
|
|
|
- let avgPH = 0
|
|
|
- let strongAcidCount = 0
|
|
|
- let mildAcidCount = 0
|
|
|
- let normalCount = 0
|
|
|
- let maxPH = -Infinity
|
|
|
- let minPH = Infinity
|
|
|
+ // 保存数据用于统计和交互
|
|
|
+ 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
|
|
|
+ }
|
|
|
+
|
|
|
+ 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++
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ phDistribution.value = distribution
|
|
|
+ }
|
|
|
+
|
|
|
+ // 查找最近的采样点
|
|
|
+ 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)
|
|
|
+ );
|
|
|
|
|
|
- geoJsonData.features.forEach(feature => {
|
|
|
- const ph = feature.properties.ph || feature.properties.value
|
|
|
- if (ph && ph > 0) {
|
|
|
- phCount++
|
|
|
-
|
|
|
- // ✅ 使用在线算法计算平均值,避免大数累加
|
|
|
- const delta = ph - avgPH;
|
|
|
- avgPH = avgPH + delta / phCount;
|
|
|
-
|
|
|
-
|
|
|
- maxPH = Math.max(maxPH, ph)
|
|
|
- minPH = Math.min(minPH, ph)
|
|
|
-
|
|
|
- if (ph <= 5.2) strongAcidCount++
|
|
|
- else if (ph < 6.0) mildAcidCount++
|
|
|
- else normalCount++
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- statistics.value = {
|
|
|
- totalBlocks: geoJsonData.features.length,
|
|
|
- avgPH:avgPH.toFixed(2),
|
|
|
- strongAcidCount,
|
|
|
- mildAcidCount,
|
|
|
- normalCount,
|
|
|
- maxPH: maxPH === -Infinity ? 0 : parseFloat(maxPH.toFixed(2)),
|
|
|
- minPH: minPH === Infinity ? 0 : parseFloat(minPH.toFixed(2))
|
|
|
- };
|
|
|
-
|
|
|
- console.log('✅ 统计数据加载完成:', statistics.value);
|
|
|
+ if (distance < minDistance && distance < 0.01) { // 10 米范围内
|
|
|
+ minDistance = distance;
|
|
|
+ nearestPoint = feature;
|
|
|
+ }
|
|
|
}
|
|
|
- } catch (err) {
|
|
|
- console.error('加载统计数据失败:', err);
|
|
|
+ });
|
|
|
+
|
|
|
+ if (nearestPoint) {
|
|
|
+ const ph = parsePHValue(nearestPoint.properties.ph || nearestPoint.properties.value);
|
|
|
+
|
|
|
+ selectedPoint.value = {
|
|
|
+ ph: ph,
|
|
|
+ properties: nearestPoint.properties
|
|
|
+ };
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+
|
|
|
+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 loadLeSoilData()
|
|
|
+ await fetchDataForStatistics()
|
|
|
await loadStatistics()
|
|
|
})
|
|
|
|
|
|
@@ -164,6 +288,12 @@ function getPHComment(avgPH) {
|
|
|
<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>
|
|
|
|
|
|
@@ -174,7 +304,7 @@ function getPHComment(avgPH) {
|
|
|
|
|
|
<div class="stat-row highlight">
|
|
|
<span class="stat-label">平均 pH 值:</span>
|
|
|
- <span class="stat-value" :class="getPHLevelClass(statistics.avgPH)">
|
|
|
+ <span class="stat-value" :class="getPHLevelClass(parseFloat(statistics.avgPH))">
|
|
|
{{ statistics.avgPH || '-' }}
|
|
|
</span>
|
|
|
</div>
|
|
|
@@ -214,8 +344,35 @@ function getPHComment(avgPH) {
|
|
|
</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">
|
|
|
@@ -235,6 +392,35 @@ function getPHComment(avgPH) {
|
|
|
<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>
|
|
|
|
|
|
@@ -249,26 +435,65 @@ function getPHComment(avgPH) {
|
|
|
}
|
|
|
|
|
|
.ph-map{
|
|
|
- width: 60%;
|
|
|
+ width: 65%;
|
|
|
height: 95%;
|
|
|
min-height: 500px;
|
|
|
+ border-radius: 16px; /* 圆角大小,可根据需要调整,比如 10px、20px */
|
|
|
+ border: 3px solid #1092d8; /* 蓝色边框,宽度和颜色可自定义 */
|
|
|
+ overflow: hidden; /* 关键:防止地图内容溢出圆角区域 */
|
|
|
}
|
|
|
|
|
|
/* ✅ 统计面板样式 */
|
|
|
.statistics-panel {
|
|
|
position: absolute;
|
|
|
- top: 20px;
|
|
|
+ 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: 280px;
|
|
|
+ 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);
|
|
|
+}
|
|
|
+
|
|
|
+.combtn:active {
|
|
|
+ transform: translateY(0);
|
|
|
+ box-shadow: 0 2px 8px rgba(16, 146, 216, 0.3);
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
.statistics-panel h4 {
|
|
|
margin: 0 0 15px 0;
|
|
|
font-size: 18px;
|
|
|
@@ -284,6 +509,7 @@ function getPHComment(avgPH) {
|
|
|
align-items: center;
|
|
|
margin-bottom: 12px;
|
|
|
font-size: 14px;
|
|
|
+ line-height: 1.4; /* 新增行高,提升可读性 */
|
|
|
}
|
|
|
|
|
|
.stat-row.highlight {
|
|
|
@@ -304,9 +530,10 @@ function getPHComment(avgPH) {
|
|
|
}
|
|
|
|
|
|
.stat-value {
|
|
|
- font-weight: bold;
|
|
|
+ font-weight: 600;
|
|
|
font-size: 16px;
|
|
|
color: #333;
|
|
|
+ letter-spacing: 0.5px; /* 新增字间距 */
|
|
|
}
|
|
|
|
|
|
.stat-value.danger {
|
|
|
@@ -361,6 +588,158 @@ function getPHComment(avgPH) {
|
|
|
color: #22c55e;
|
|
|
}
|
|
|
|
|
|
+.distribution-chart {
|
|
|
+ position: absolute;
|
|
|
+ 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;
|
|
|
+ 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;
|
|
|
@@ -405,6 +784,81 @@ function getPHComment(avgPH) {
|
|
|
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);
|
|
|
@@ -415,4 +869,42 @@ function getPHComment(avgPH) {
|
|
|
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;
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+.close-btn {
|
|
|
+ background: none;
|
|
|
+ border: none;
|
|
|
+ font-size: 20px;
|
|
|
+ cursor: pointer;
|
|
|
+ color: #909399;
|
|
|
+ padding: 0;
|
|
|
+ width: 20px;
|
|
|
+ height: 20px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ border-radius: 50%; /* 新增圆形背景 */
|
|
|
+ transition: all 0.2s; /* 新增过渡 */
|
|
|
+}
|
|
|
+
|
|
|
+.close-btn:hover {
|
|
|
+ color: #ef4444; /* 改为红色更醒目 */
|
|
|
+ background: #fef2f2; /* 新增背景色 */
|
|
|
+}
|
|
|
</style>
|