|
|
@@ -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>
|