|
|
@@ -8,9 +8,13 @@
|
|
|
import { api8000 } from '@/utils/request'
|
|
|
|
|
|
const samplePointsData = ref([])
|
|
|
+ const cdSampleData = ref([]) // 第二个地图的 CD 数据
|
|
|
const mapRef = ref(null)
|
|
|
+ const mapRef2 = ref(null) // 第二个地图
|
|
|
let map = null
|
|
|
+ let map2 = null // 第二个地图实例
|
|
|
let wmsLayer = null
|
|
|
+ let wmsLayer2 = null // 第二个地图的 WMS 图层
|
|
|
let geoJsonLayer = null
|
|
|
|
|
|
|
|
|
@@ -39,12 +43,30 @@
|
|
|
range3: 0,
|
|
|
})
|
|
|
|
|
|
+ // 第二个地图的 CD 含量统计
|
|
|
+ const cdStatistics = ref({
|
|
|
+ totalBlocks: 0,
|
|
|
+ avgCD: 0,
|
|
|
+ safeCount: 0, // 安全区间 (0.0-0.2 mg/kg)
|
|
|
+ warningCount: 0, // 预警区间 (0.2-0.3 mg/kg)
|
|
|
+ exceedCount: 0, // 超标区间 (≥0.3 mg/kg)
|
|
|
+ maxCD: 0,
|
|
|
+ minCD: 0
|
|
|
+ })
|
|
|
+
|
|
|
+ // CD 含量分布个数计算
|
|
|
+ const cdDistribution = ref({
|
|
|
+ safe: 0, // 安全
|
|
|
+ warning: 0, // 预警
|
|
|
+ exceed: 0 // 超标
|
|
|
+ })
|
|
|
+
|
|
|
const selectedPoint = ref(null)
|
|
|
|
|
|
// 地图配置
|
|
|
const CONFIG = {
|
|
|
- center:[25.202903, 113.25383],
|
|
|
- zoom:11,
|
|
|
+ center:[25.222903, 113.25383],
|
|
|
+ zoom:10, // 调小缩放级别,显示更大范围
|
|
|
// 获取所有点的 API 地址
|
|
|
getPoint:'/api/vector/export/all?table_name=le_data_block_map&format=geojson',
|
|
|
geoserver:{
|
|
|
@@ -119,6 +141,24 @@ function getPHComment(avgPH) {
|
|
|
|
|
|
}
|
|
|
|
|
|
+ async function initMap2(){
|
|
|
+ await nextTick()
|
|
|
+
|
|
|
+ if(!mapRef2.value) return
|
|
|
+ map2 = L.map(mapRef2.value).setView(CONFIG.center,CONFIG.zoom)
|
|
|
+
|
|
|
+ // 第二个地图展示 CropCd_block_map_with_boundary 图层
|
|
|
+ wmsLayer2 = L.tileLayer.wms(CONFIG.geoserver.wmsUrl, {
|
|
|
+ layers: `${CONFIG.geoserver.workspace}:CropCd_block_map_with_boundary`,
|
|
|
+ format: 'image/png',
|
|
|
+ transparent: true,
|
|
|
+ version: '1.1.0',
|
|
|
+ srs:'EPSG:4326',
|
|
|
+ attribution: '© GeoServer - Crop CD'
|
|
|
+ }).addTo(map2)
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
async function switchMap(mapType) {
|
|
|
if (!map || !wmsLayer) return
|
|
|
|
|
|
@@ -301,6 +341,104 @@ function getPHComment(avgPH) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// 加载第二个地图的 CD 含量统计数据
|
|
|
+async function loadCDStatistics() {
|
|
|
+ try {
|
|
|
+ // 等待数据加载完成
|
|
|
+ if (cdSampleData.value.length === 0) return;
|
|
|
+
|
|
|
+ let cdCount = 0
|
|
|
+ let avgCD = 0
|
|
|
+ let safeCount = 0 // 0.0-0.2 mg/kg
|
|
|
+ let warningCount = 0 // 0.2-0.3 mg/kg
|
|
|
+ let exceedCount = 0 // ≥0.3 mg/kg
|
|
|
+ let maxCD = -Infinity
|
|
|
+ let minCD = Infinity
|
|
|
+
|
|
|
+ cdSampleData.value.forEach(feature => {
|
|
|
+ // 假设 CD 含量字段为 CropCd_mea
|
|
|
+ const cdValue = feature.properties.CropCd_mea
|
|
|
+ const numericCd = typeof cdValue === 'string' ? parseFloat(cdValue) : cdValue
|
|
|
+
|
|
|
+ if (numericCd !== null && numericCd !== undefined && !isNaN(numericCd) && numericCd >= 0) {
|
|
|
+ cdCount++
|
|
|
+ const delta = numericCd - avgCD;
|
|
|
+ avgCD = avgCD + delta / cdCount;
|
|
|
+ maxCD = Math.max(maxCD, numericCd)
|
|
|
+ minCD = Math.min(minCD, numericCd)
|
|
|
+
|
|
|
+ // 根据规则分类
|
|
|
+ if (numericCd < 0.2) {
|
|
|
+ safeCount++
|
|
|
+ }
|
|
|
+ else if (numericCd < 0.3) {
|
|
|
+ warningCount++
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ exceedCount++
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ cdStatistics.value = {
|
|
|
+ totalBlocks: cdSampleData.value.length,
|
|
|
+ avgCD: cdCount > 0 ? parseFloat(avgCD.toFixed(3)) : 0,
|
|
|
+ safeCount,
|
|
|
+ warningCount,
|
|
|
+ exceedCount,
|
|
|
+ maxCD: maxCD === -Infinity ? 0 : parseFloat(maxCD.toFixed(3)),
|
|
|
+ minCD: minCD === Infinity ? 0 : parseFloat(minCD.toFixed(3))
|
|
|
+ };
|
|
|
+
|
|
|
+ // 计算 CD 含量分布
|
|
|
+ cdDistribution.value = {
|
|
|
+ safe: safeCount,
|
|
|
+ warning: warningCount,
|
|
|
+ exceed: exceedCount
|
|
|
+ };
|
|
|
+
|
|
|
+ // console.log('✅ CD 统计数据加载完成:', cdStatistics.value);
|
|
|
+ } catch (err) {
|
|
|
+ console.error('加载 CD 统计数据失败:', err);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 加载第二个地图的 CD 数据
|
|
|
+async function loadCDData() {
|
|
|
+ try {
|
|
|
+ const url = `${CONFIG.geoserver.url}/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=${CONFIG.geoserver.workspace}:Crop_cd_block_map&outputFormat=application/json`
|
|
|
+
|
|
|
+ const response = await fetch(url);
|
|
|
+
|
|
|
+ if (!response.ok) {
|
|
|
+ const errorText = await response.text();
|
|
|
+ console.error('❌ WFS 响应错误:', errorText);
|
|
|
+ throw new Error(`HTTP error! status: ${response.status}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ const text = await response.text();
|
|
|
+
|
|
|
+ // 尝试解析 JSON
|
|
|
+ let geoJsonData;
|
|
|
+ try {
|
|
|
+ geoJsonData = JSON.parse(text);
|
|
|
+ } catch (parseError) {
|
|
|
+ console.error('❌ JSON 解析失败,响应内容:', text);
|
|
|
+ throw new Error('GeoServer 返回的数据格式不正确,不是有效的 JSON');
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ // 保存 CD 数据
|
|
|
+ cdSampleData.value = geoJsonData.features;
|
|
|
+
|
|
|
+ // 计算 CD 统计信息
|
|
|
+ loadCDStatistics();
|
|
|
+
|
|
|
+ } catch (err) {
|
|
|
+ console.error('❌ 加载 CD 数据失败:', err);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
// 修改为只获取数据用于统计,不渲染到地图
|
|
|
async function fetchDataForStatistics() {
|
|
|
try {
|
|
|
@@ -470,12 +608,22 @@ onUnmounted(()=>{
|
|
|
|
|
|
onMounted(async()=>{
|
|
|
await initMap()
|
|
|
+ await initMap2() // 初始化第二个地图
|
|
|
await fetchDataForStatistics()
|
|
|
await loadStatistics()
|
|
|
+ await loadCDData() // 加载 CD 数据
|
|
|
+ // loadCDStatistics() 会在 loadCDData 完成后自动调用
|
|
|
})
|
|
|
|
|
|
onUnmounted(()=>{
|
|
|
- if(map) map.remove()
|
|
|
+ if(map) {
|
|
|
+ map.remove()
|
|
|
+ map = null
|
|
|
+ }
|
|
|
+ if(map2) {
|
|
|
+ map2.remove()
|
|
|
+ map2 = null
|
|
|
+ }
|
|
|
})
|
|
|
|
|
|
</script>
|
|
|
@@ -483,6 +631,7 @@ onUnmounted(()=>{
|
|
|
<template>
|
|
|
<div class="map-container">
|
|
|
<div class="ph-map" ref="mapRef"></div>
|
|
|
+ <div class="ph-map" ref="mapRef2"></div>
|
|
|
|
|
|
<!-- 计算刷新按钮 -->
|
|
|
<div class="compute">
|
|
|
@@ -553,7 +702,51 @@ onUnmounted(()=>{
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <!-- ph分布 -->
|
|
|
+ <!-- CD 含量统计 -->
|
|
|
+ <div class="statistics-panel" style="top: 550px;">
|
|
|
+ <h4>🌾 作物 Cd 含量统计</h4>
|
|
|
+
|
|
|
+ <div class="stat-row">
|
|
|
+ <span class="stat-label">地块总数:</span>
|
|
|
+ <span class="stat-value">{{ cdStatistics.totalBlocks }} 个</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="stat-row highlight">
|
|
|
+ <span class="stat-label">平均 Cd 含量:</span>
|
|
|
+ <span class="stat-value" :class="cdStatistics.avgCD > 0 ? 'success' : 'danger'">
|
|
|
+ {{ cdStatistics.avgCD > 0 ? cdStatistics.avgCD + ' mg/kg' : '-' }}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="stat-row">
|
|
|
+ <span class="stat-label">安全 (<0.2 mg/kg):</span>
|
|
|
+ <span class="stat-value success">{{ cdStatistics.safeCount }} 个</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="stat-row">
|
|
|
+ <span class="stat-label">预警 (0.2-0.3 mg/kg):</span>
|
|
|
+ <span class="stat-value warning">{{ cdStatistics.warningCount }} 个</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="stat-row">
|
|
|
+ <span class="stat-label">超标 (≥0.3 mg/kg):</span>
|
|
|
+ <span class="stat-value danger">{{ cdStatistics.exceedCount }} 个</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="stat-divider"></div>
|
|
|
+
|
|
|
+ <div class="stat-row small">
|
|
|
+ <span class="stat-label">最高 Cd:</span>
|
|
|
+ <span class="stat-value danger">{{ cdStatistics.maxCD > 0 ? cdStatistics.maxCD + ' mg/kg' : '-' }}</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="stat-row small">
|
|
|
+ <span class="stat-label">最低 Cd:</span>
|
|
|
+ <span class="stat-value success">{{ cdStatistics.minCD > 0 ? cdStatistics.minCD + ' mg/kg' : '-' }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- ph 分布 -->
|
|
|
<div class="distribution-chart">
|
|
|
<h4>📈 pH 分布</h4>
|
|
|
<div class="bar-chart">
|
|
|
@@ -581,6 +774,43 @@ onUnmounted(()=>{
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
+ <!-- CD 含量分布 -->
|
|
|
+ <div class="distribution-chart" style="top: 550px;">
|
|
|
+ <h4>🌾 作物 Cd 含量分布</h4>
|
|
|
+ <div class="bar-chart">
|
|
|
+ <div class="bar-item">
|
|
|
+ <div class="bar-label">安全 (<0.2)</div>
|
|
|
+ <div class="bar-container">
|
|
|
+ <div
|
|
|
+ class="bar-fill success"
|
|
|
+ :style="{ width: `${cdStatistics.totalBlocks > 0 ? (cdDistribution.safe / cdStatistics.totalBlocks) * 100 : 0}%` }"
|
|
|
+ ></div>
|
|
|
+ <span class="bar-value">{{ cdDistribution.safe }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="bar-item">
|
|
|
+ <div class="bar-label">预警 (0.2-0.3)</div>
|
|
|
+ <div class="bar-container">
|
|
|
+ <div
|
|
|
+ class="bar-fill warning"
|
|
|
+ :style="{ width: `${cdStatistics.totalBlocks > 0 ? (cdDistribution.warning / cdStatistics.totalBlocks) * 100 : 0}%` }"
|
|
|
+ ></div>
|
|
|
+ <span class="bar-value">{{ cdDistribution.warning }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="bar-item">
|
|
|
+ <div class="bar-label">超标 (≥0.3)</div>
|
|
|
+ <div class="bar-container">
|
|
|
+ <div
|
|
|
+ class="bar-fill danger"
|
|
|
+ :style="{ width: `${cdStatistics.totalBlocks > 0 ? (cdDistribution.exceed / cdStatistics.totalBlocks) * 100 : 0}%` }"
|
|
|
+ ></div>
|
|
|
+ <span class="bar-value">{{ cdDistribution.exceed }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
<div class="stat-comment" v-if="statistics.avgPH > 0">
|
|
|
<div class="comment-title">📝 综合评估:</div>
|
|
|
<div class="comment-text" :class="getPHLevelClass(statistics.avgPH)">
|
|
|
@@ -609,6 +839,23 @@ onUnmounted(()=>{
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
+ <!-- CD 含量图例 -->
|
|
|
+ <div class="legend" style="top: 580px;">
|
|
|
+ <h4>🌾 Cd 含量图例</h4>
|
|
|
+ <div class="legend-item">
|
|
|
+ <span class="legend-color" style="background: #22c55e;"></span>
|
|
|
+ <span>0.0 - 0.2 mg/kg (安全)</span>
|
|
|
+ </div>
|
|
|
+ <div class="legend-item">
|
|
|
+ <span class="legend-color" style="background: #eab308;"></span>
|
|
|
+ <span>0.2-0.3 mg/kg (预警)</span>
|
|
|
+ </div>
|
|
|
+ <div class="legend-item">
|
|
|
+ <span class="legend-color" style="background: #ef4444;"></span>
|
|
|
+ <span>≥ 0.3 mg/kg (超标)</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
<!-- 采样点详情 -->
|
|
|
<div class="point-detail-modal" v-if="selectedPoint">
|
|
|
<div class="detail-content">
|
|
|
@@ -652,10 +899,15 @@ onUnmounted(()=>{
|
|
|
|
|
|
.ph-map{
|
|
|
width: 600px;
|
|
|
- min-height: 500px;
|
|
|
- border-radius: 16px; /* 圆角大小,可根据需要调整,比如 10px、20px */
|
|
|
- border: 3px solid #1092d8; /* 蓝色边框,宽度和颜色可自定义 */
|
|
|
- overflow: hidden; /* 关键:防止地图内容溢出圆角区域 */
|
|
|
+ height: 500px; /* 固定高度 */
|
|
|
+ border-radius: 16px;
|
|
|
+ border: 3px solid #1092d8;
|
|
|
+ overflow: hidden;
|
|
|
+ margin-bottom: 20px; /* 两个地图之间的间距 */
|
|
|
+}
|
|
|
+
|
|
|
+.ph-map:first-child {
|
|
|
+ margin-bottom: 50px; /* 第一个地图下方留空 */
|
|
|
}
|
|
|
|
|
|
/* ✅ 统计面板样式 */
|
|
|
@@ -971,15 +1223,14 @@ onUnmounted(()=>{
|
|
|
padding: 15px;
|
|
|
border-radius: 8px;
|
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
|
- z-index: 1000;
|
|
|
- min-width: 180px;
|
|
|
+ min-width: 10px;
|
|
|
backdrop-filter: blur(4px);
|
|
|
z-index: 0;
|
|
|
}
|
|
|
|
|
|
.legend h4 {
|
|
|
margin: 0 0 10px 0;
|
|
|
- font-size: 14px;
|
|
|
+ font-size: 10px;
|
|
|
color: #333;
|
|
|
border-bottom: 2px solid #1092d8;
|
|
|
padding-bottom: 6px;
|
|
|
@@ -990,7 +1241,7 @@ onUnmounted(()=>{
|
|
|
align-items: center;
|
|
|
gap: 8px;
|
|
|
margin-bottom: 6px;
|
|
|
- font-size: 12px;
|
|
|
+ font-size: 8px;
|
|
|
color: #555;
|
|
|
}
|
|
|
|
|
|
@@ -999,8 +1250,8 @@ onUnmounted(()=>{
|
|
|
}
|
|
|
|
|
|
.legend-color {
|
|
|
- width: 20px;
|
|
|
- height: 20px;
|
|
|
+ width: 10px;
|
|
|
+ height: 10px;
|
|
|
border-radius: 4px;
|
|
|
border: 1px solid #ddd;
|
|
|
flex-shrink: 0;
|