TotalIntroduction.vue 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497
  1. <script setup>
  2. // 安全区间 (0.0-0.2 mg/kg)
  3. // 预警区间 (0.2-0.3 mg/kg)
  4. // 超标区间 (≥0.3 mg/kg)
  5. import { ref, watch, onMounted, onBeforeUnmount, onUnmounted ,computed,nextTick} from 'vue'
  6. import L from 'leaflet'
  7. import 'leaflet/dist/leaflet.css'
  8. import { api8000 } from '@/utils/request'
  9. const samplePointsData = ref([])
  10. const cdSampleData = ref([]) // 第二个地图的 CD 数据
  11. const mapRef = ref(null)
  12. const mapRef2 = ref(null) // 第二个地图
  13. let map = null
  14. let map2 = null // 第二个地图实例
  15. let wmsLayer = null
  16. let wmsLayer2 = null // 第二个地图的 WMS 图层
  17. let geoJsonLayer = null
  18. // 统计信息
  19. const statistics = ref({
  20. totalBlocks: 0,
  21. avgPH: 0,
  22. strongAcidCount: 0,
  23. mildAcidCount: 0,
  24. normalCount: 0,
  25. maxPH: 0,
  26. minPH: 0,
  27. strongAcidArea: 0,
  28. mildAcidArea: 0,
  29. normalArea: 0,
  30. totalArea: 0,
  31. strongAcidPercent: 0,
  32. mildAcidPercent: 0,
  33. normalPercent: 0
  34. })
  35. // pH 分布个数计算
  36. const phDistribution = ref({
  37. range1: 0,
  38. range2: 0,
  39. range3: 0,
  40. })
  41. // 第二个地图的 CD 含量统计
  42. const cdStatistics = ref({
  43. totalBlocks: 0,
  44. avgCD: 0,
  45. safeCount: 0, // 安全区间 (0.0-0.2 mg/kg)
  46. warningCount: 0, // 预警区间 (0.2-0.3 mg/kg)
  47. exceedCount: 0, // 超标区间 (≥0.3 mg/kg)
  48. maxCD: 0,
  49. minCD: 0,
  50. safeArea: 0,
  51. warningArea: 0,
  52. exceedArea: 0,
  53. totalArea: 0,
  54. safePercent: 0,
  55. warningPercent: 0,
  56. exceedPercent: 0
  57. })
  58. // CD 含量分布个数计算
  59. const cdDistribution = ref({
  60. safe: 0, // 安全
  61. warning: 0, // 预警
  62. exceed: 0 // 超标
  63. })
  64. const selectedPoint = ref(null)
  65. // 地图配置
  66. const CONFIG = {
  67. center:[25.222903, 113.25383],
  68. zoom:10, // 调小缩放级别,显示更大范围
  69. // 获取所有点的 API 地址
  70. getPoint:'/api/vector/export/all?table_name=le_origin_ph_map&format=geojson',
  71. geoserver:{
  72. url:'/geoserver',
  73. workspace:'acidmap',
  74. layerGroup:'leshujukanbanmap',
  75. dataLayer:'le_origin_ph_map',
  76. wmsUrl:'/geoserver/acidmap/wms',
  77. phField: 'ph_mean'
  78. },
  79. reboundGeoserver:{
  80. url:'/geoserver',
  81. workspace:'acidmap',
  82. layerGroup:'le_reflux_map',
  83. dataLayer:'le_data_reflux_result',
  84. wmsUrl:'/geoserver/acidmap/wms',
  85. phField: 'le_data__4'
  86. },
  87. cdGeoserver:{
  88. url:'/geoserver',
  89. workspace:'acidmap',
  90. layerGroup:'CropCd_block_map_with_boundary',
  91. dataLayer:'Crop_cd_block_map',
  92. wmsUrl:'/geoserver/acidmap/wms',
  93. cdField: 'CropCd_mea'
  94. },
  95. reboundCdGeoserver:{
  96. url:'/geoserver',
  97. workspace:'acidmap',
  98. layerGroup:'PrediCropCd_block_with_boundary',
  99. dataLayer:'Predi_Cropcd_block',
  100. wmsUrl:'/geoserver/acidmap/wms',
  101. cdField: 'sceCropCd_'
  102. }
  103. }
  104. // 图层配置已验证:正常地图使用 le_data_block_map,反酸地图使用 le_data_reflux_result
  105. const currentMapType = ref('normal')
  106. // 获取 pH 等级对应的 CSS 类
  107. function getPHLevelClass(ph) {
  108. // ✅ 先转换为数字
  109. const numericPh = typeof ph === 'string' ? parseFloat(ph) : ph
  110. if (!numericPh || numericPh === 0) return ''
  111. if (numericPh <= 5.2) return 'danger'
  112. if (numericPh < 6.0) return 'warning'
  113. return 'success'
  114. }
  115. // 获取综合评语
  116. function getPHComment(avgPH) {
  117. if (avgPH <= 4.5) return '🔴 土壤严重酸化,需立即治理!'
  118. if (avgPH <= 5.2) return '🟠 土壤酸化明显,建议尽快改良'
  119. if (avgPH < 6.0) return '🟡 土壤微酸性,注意保持'
  120. return '🟢 土壤酸碱度适宜,状态良好'
  121. }
  122. function getPHRangeLabel(range) {
  123. const labels = {
  124. range1: '< 5.2',
  125. range2: '5.2 - 6.0',
  126. range3: ' >6.0 ',
  127. }
  128. return labels[range] || range
  129. }
  130. // 添加分页加载支持
  131. const batchSize = 1000;
  132. let allFeatures = [];
  133. async function initMap(){
  134. await nextTick()
  135. if(!mapRef.value) return
  136. map = L.map(mapRef.value).setView(CONFIG.center,CONFIG.zoom)
  137. wmsLayer = L.tileLayer.wms(CONFIG.geoserver.wmsUrl, {
  138. layers: `${CONFIG.geoserver.workspace}:${CONFIG.geoserver.layerGroup}`,
  139. format: 'image/png',
  140. transparent: true,
  141. version: '1.1.0',
  142. srs:'EPSG:4326',
  143. attribution: '© GeoServer - Acidmap'
  144. }).addTo(map)
  145. }
  146. async function initMap2(){
  147. await nextTick()
  148. if(!mapRef2.value) return
  149. map2 = L.map(mapRef2.value).setView(CONFIG.center,CONFIG.zoom)
  150. // 第二个地图展示 CD 图层
  151. wmsLayer2 = L.tileLayer.wms(CONFIG.cdGeoserver.wmsUrl, {
  152. layers: `${CONFIG.cdGeoserver.workspace}:${CONFIG.cdGeoserver.layerGroup}`,
  153. format: 'image/png',
  154. transparent: true,
  155. version: '1.1.0',
  156. srs:'EPSG:4326',
  157. attribution: '© GeoServer - Crop CD'
  158. }).addTo(map2)
  159. }
  160. async function switchMap(mapType) {
  161. if (!map || !wmsLayer) return
  162. if (!map2 || !wmsLayer2) return
  163. console.log('🔄 开始切换地图,目标类型:', mapType)
  164. console.log('️ map2 存在:', !!map2, 'wmsLayer2 存在:', !!wmsLayer2)
  165. currentMapType.value = mapType
  166. // 切换 pH 地图
  167. const config = mapType === 'rebound' ? CONFIG.reboundGeoserver : CONFIG.geoserver
  168. console.log('📊 pH 配置:', config.layerGroup, config.dataLayer)
  169. map.removeLayer(wmsLayer)
  170. wmsLayer = L.tileLayer.wms(config.wmsUrl, {
  171. layers: `${config.workspace}:${config.layerGroup}`,
  172. format: 'image/png',
  173. transparent: true,
  174. version: '1.1.0',
  175. srs:'EPSG:4326',
  176. attribution: '© GeoServer - Acidmap'
  177. }).addTo(map)
  178. console.log('✅ pH 地图图层已切换')
  179. // 切换 CD 地图
  180. const cdConfig = mapType === 'rebound' ? CONFIG.reboundCdGeoserver : CONFIG.cdGeoserver
  181. console.log(' CD 配置:', cdConfig.layerGroup, cdConfig.dataLayer, cdConfig.cdField)
  182. console.log('🔗 CD WMS 完整图层名:', `${cdConfig.workspace}:${cdConfig.layerGroup}`)
  183. // 移除旧图层(和 pH 地图使用相同的方式)
  184. map2.removeLayer(wmsLayer2)
  185. // 创建新图层并添加到地图
  186. wmsLayer2 = L.tileLayer.wms(cdConfig.wmsUrl, {
  187. layers: `${cdConfig.workspace}:${cdConfig.layerGroup}`,
  188. format: 'image/png',
  189. transparent: true,
  190. version: '1.1.0',
  191. srs:'EPSG:4326',
  192. attribution: '© GeoServer - Crop CD'
  193. }).addTo(map2)
  194. // 监听瓦片加载事件
  195. wmsLayer2.on('tileload', function(e) {
  196. console.log(' CD 瓦片加载成功:', e.tile.src)
  197. })
  198. wmsLayer2.on('tileerror', function(e) {
  199. console.error(' CD 瓦片加载失败! 完整URL:', e.tile.src, '错误信息:', e.error)
  200. })
  201. console.log('✅ CD 地图图层已切换,新图层:', wmsLayer2.options.layers)
  202. // 刷新地图渲染
  203. await nextTick()
  204. map2.invalidateSize()
  205. wmsLayer2.redraw()
  206. // 重新加载 pH 地图的数据和统计
  207. await loadMapData()
  208. // 重新加载 CD 地图的数据和统计
  209. await loadCDData()
  210. console.log('🎉 地图切换完成')
  211. }
  212. // 加载当前地图类型的数据
  213. async function loadMapData() {
  214. try {
  215. const config = currentMapType.value === 'rebound' ? CONFIG.reboundGeoserver : CONFIG.geoserver
  216. const url = `${config.url}/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=${config.workspace}:${config.dataLayer}&outputFormat=application/json`
  217. // console.log('🔍 请求 WFS 数据:', url);
  218. const response = await fetch(url);
  219. if (!response.ok) {
  220. const errorText = await response.text();
  221. console.error('❌ WFS 响应错误:', errorText);
  222. throw new Error(`HTTP error! status: ${response.status}`);
  223. }
  224. const text = await response.text();
  225. // console.log('📦 原始响应:', text.substring(0, 200));
  226. // 尝试解析 JSON
  227. let geoJsonData;
  228. try {
  229. geoJsonData = JSON.parse(text);
  230. } catch (parseError) {
  231. console.error('❌ JSON 解析失败,响应内容:', text);
  232. throw new Error('GeoServer 返回的数据格式不正确,不是有效的 JSON');
  233. }
  234. // console.log('✅ 获取到地图数据:', geoJsonData.features?.length || 0, '条记录');
  235. // 保存数据用于统计和交互
  236. allFeatures = geoJsonData.features;
  237. samplePointsData.value = geoJsonData.features;
  238. // 计算统计信息
  239. calculatePHDistribution(allFeatures);
  240. loadStatistics();
  241. } catch (err) {
  242. console.error('❌ 加载数据失败:', err);
  243. }
  244. }
  245. function createPointIcon(feature) {
  246. const ph = feature.properties.ph || feature.properties.value
  247. let color = '#22c55e'
  248. if (ph <= 5.2) color = '#ef4444'
  249. else if (ph < 6.0) color = '#f59e0b'
  250. return L.divIcon({
  251. className: 'custom-marker',
  252. html: `<div style="
  253. background-color: ${color};
  254. width: 12px;
  255. height: 12px;
  256. border-radius: 50%;
  257. border: 2px solid white;
  258. box-shadow: 0 2px 4px rgba(0,0,0,0.3);
  259. "></div>`,
  260. iconSize: [12, 12],
  261. iconAnchor: [6, 6]
  262. })
  263. }
  264. function parsePHValue(phValue) {
  265. if (!phValue && phValue !== 0) return null
  266. const numericPh = typeof phValue === 'string' ? parseFloat(phValue) : phValue
  267. return !isNaN(numericPh) && numericPh > 0 ? numericPh : null
  268. }
  269. async function loadStatistics() {
  270. try {
  271. // 等待数据加载完成
  272. if (samplePointsData.value.length === 0) return;
  273. // 根据当前地图类型选择 pH 字段
  274. const config = currentMapType.value === 'rebound' ? CONFIG.reboundGeoserver : CONFIG.geoserver
  275. let phCount = 0
  276. let avgPH = 0
  277. let strongAcidCount = 0
  278. let mildAcidCount = 0
  279. let normalCount = 0
  280. let maxPH = -Infinity
  281. let minPH = Infinity
  282. let strongAcidArea = 0
  283. let mildAcidArea = 0
  284. let normalArea = 0
  285. let totalArea = 0
  286. samplePointsData.value.forEach(feature => {
  287. const numericPh = parsePHValue(feature.properties[config.phField])
  288. const area = parseFloat(feature.properties.area) || 0
  289. // 累加总面积(所有地块)
  290. if (area > 0) {
  291. totalArea += area
  292. }
  293. // 过滤无效 pH 值,只统计有数据的地块
  294. if (numericPh && numericPh > 0 && !isNaN(numericPh)) {
  295. phCount++
  296. const delta = numericPh - avgPH;
  297. avgPH = avgPH + delta / phCount;
  298. maxPH = Math.max(maxPH, numericPh)
  299. minPH = Math.min(minPH, numericPh)
  300. if (numericPh <= 5.2) {
  301. strongAcidCount++
  302. strongAcidArea += area
  303. }
  304. else if (numericPh < 6.0) {
  305. mildAcidCount++
  306. mildAcidArea += area
  307. }
  308. else {
  309. normalCount++
  310. normalArea += area
  311. }
  312. }
  313. });
  314. // 计算有 pH 数据的地块总面积(用于占比计算)
  315. const totalPHArea = strongAcidArea + mildAcidArea + normalArea;
  316. // 计算面积占比(基于有 pH 数据的地块)
  317. const strongAcidPercent = totalPHArea > 0 ? parseFloat(((strongAcidArea / totalPHArea) * 100).toFixed(2)) : 0
  318. const mildAcidPercent = totalPHArea > 0 ? parseFloat(((mildAcidArea / totalPHArea) * 100).toFixed(2)) : 0
  319. const normalPercent = totalPHArea > 0 ? parseFloat(((normalArea / totalPHArea) * 100).toFixed(2)) : 0
  320. statistics.value = {
  321. totalBlocks: samplePointsData.value.length,
  322. avgPH: phCount > 0 ? parseFloat(avgPH.toFixed(2)) : 0, // ✅ 确保是数字
  323. strongAcidCount,
  324. mildAcidCount,
  325. normalCount,
  326. maxPH: maxPH === -Infinity ? 0 : parseFloat(maxPH.toFixed(2)),
  327. minPH: minPH === Infinity ? 0 : parseFloat(minPH.toFixed(2)),
  328. strongAcidArea: parseFloat(strongAcidArea.toFixed(2)),
  329. mildAcidArea: parseFloat(mildAcidArea.toFixed(2)),
  330. normalArea: parseFloat(normalArea.toFixed(2)),
  331. totalArea: parseFloat(totalArea.toFixed(2)),
  332. strongAcidPercent,
  333. mildAcidPercent,
  334. normalPercent
  335. };
  336. // console.log('✅ 统计数据加载完成:', statistics.value);
  337. } catch (err) {
  338. console.error('加载统计数据失败:', err);
  339. }
  340. }
  341. // 加载第二个地图的 CD 含量统计数据
  342. async function loadCDStatistics() {
  343. try {
  344. // 等待数据加载完成
  345. if (cdSampleData.value.length === 0) return;
  346. const cdConfig = currentMapType.value === 'rebound' ? CONFIG.reboundCdGeoserver : CONFIG.cdGeoserver
  347. let cdCount = 0
  348. let avgCD = 0
  349. let safeCount = 0 // 0.0-0.2 mg/kg
  350. let warningCount = 0 // 0.2-0.3 mg/kg
  351. let exceedCount = 0 // ≥0.3 mg/kg
  352. let maxCD = -Infinity
  353. let minCD = Infinity
  354. let safeArea = 0
  355. let warningArea = 0
  356. let exceedArea = 0
  357. let totalArea = 0
  358. cdSampleData.value.forEach(feature => {
  359. // 获取 CD 含量和面积
  360. const cdValue = feature.properties[cdConfig.cdField]
  361. const numericCd = typeof cdValue === 'string' ? parseFloat(cdValue) : cdValue
  362. const area = parseFloat(feature.properties.area) || 0
  363. // 累加总面积
  364. if (area > 0) {
  365. totalArea += area
  366. }
  367. if (numericCd !== null && numericCd !== undefined && !isNaN(numericCd) && numericCd >= 0) {
  368. cdCount++
  369. const delta = numericCd - avgCD;
  370. avgCD = avgCD + delta / cdCount;
  371. maxCD = Math.max(maxCD, numericCd)
  372. minCD = Math.min(minCD, numericCd)
  373. // 根据规则分类并累加面积
  374. if (numericCd < 0.2) {
  375. safeCount++
  376. safeArea += area
  377. }
  378. else if (numericCd < 0.3) {
  379. warningCount++
  380. warningArea += area
  381. }
  382. else {
  383. exceedCount++
  384. exceedArea += area
  385. }
  386. }
  387. });
  388. // 计算有 CD 数据的地块总面积(用于占比计算)
  389. const totalCDArea = safeArea + warningArea + exceedArea;
  390. // 计算面积占比(基于有 CD 数据的地块)
  391. const safePercent = totalCDArea > 0 ? parseFloat(((safeArea / totalCDArea) * 100).toFixed(2)) : 0
  392. const warningPercent = totalCDArea > 0 ? parseFloat(((warningArea / totalCDArea) * 100).toFixed(2)) : 0
  393. const exceedPercent = totalCDArea > 0 ? parseFloat(((exceedArea / totalCDArea) * 100).toFixed(2)) : 0
  394. cdStatistics.value = {
  395. totalBlocks: cdSampleData.value.length,
  396. avgCD: cdCount > 0 ? parseFloat(avgCD.toFixed(3)) : 0,
  397. safeCount,
  398. warningCount,
  399. exceedCount,
  400. maxCD: maxCD === -Infinity ? 0 : parseFloat(maxCD.toFixed(3)),
  401. minCD: minCD === Infinity ? 0 : parseFloat(minCD.toFixed(3)),
  402. safeArea: parseFloat(safeArea.toFixed(2)),
  403. warningArea: parseFloat(warningArea.toFixed(2)),
  404. exceedArea: parseFloat(exceedArea.toFixed(2)),
  405. totalArea: parseFloat(totalArea.toFixed(2)),
  406. safePercent,
  407. warningPercent,
  408. exceedPercent
  409. };
  410. // 计算 CD 含量分布
  411. cdDistribution.value = {
  412. safe: safeCount,
  413. warning: warningCount,
  414. exceed: exceedCount
  415. };
  416. // console.log('✅ CD 统计数据加载完成:', cdStatistics.value);
  417. } catch (err) {
  418. console.error('加载 CD 统计数据失败:', err);
  419. }
  420. }
  421. // 加载第二个地图的 CD 数据
  422. async function loadCDData() {
  423. try {
  424. const cdConfig = currentMapType.value === 'rebound' ? CONFIG.reboundCdGeoserver : CONFIG.cdGeoserver
  425. const url = `${cdConfig.url}/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=${cdConfig.workspace}:${cdConfig.dataLayer}&outputFormat=application/json`
  426. const response = await fetch(url);
  427. if (!response.ok) {
  428. const errorText = await response.text();
  429. console.error('❌ WFS 响应错误:', errorText);
  430. throw new Error(`HTTP error! status: ${response.status}`);
  431. }
  432. const text = await response.text();
  433. // 尝试解析 JSON
  434. let geoJsonData;
  435. try {
  436. geoJsonData = JSON.parse(text);
  437. } catch (parseError) {
  438. console.error('❌ JSON 解析失败,响应内容:', text);
  439. throw new Error('GeoServer 返回的数据格式不正确,不是有效的 JSON');
  440. }
  441. // 保存 CD 数据
  442. cdSampleData.value = geoJsonData.features;
  443. // 计算 CD 统计信息
  444. loadCDStatistics();
  445. } catch (err) {
  446. console.error('❌ 加载 CD 数据失败:', err);
  447. }
  448. }
  449. // 修改为只获取数据用于统计,不渲染到地图
  450. async function fetchDataForStatistics() {
  451. try {
  452. const config = currentMapType.value === 'rebound' ? CONFIG.reboundGeoserver : CONFIG.geoserver
  453. const url = `${config.url}/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=${config.workspace}:${config.dataLayer}&outputFormat=application/json`
  454. // console.log('🔍 请求 WFS 数据:', url);
  455. const response = await fetch(url);
  456. if (!response.ok) {
  457. const errorText = await response.text();
  458. console.error('❌ WFS 响应错误:', errorText);
  459. throw new Error(`HTTP error! status: ${response.status}`);
  460. }
  461. const text = await response.text();
  462. // console.log('📦 原始响应:', text.substring(0, 200));
  463. // 尝试解析 JSON
  464. let geoJsonData;
  465. try {
  466. geoJsonData = JSON.parse(text);
  467. } catch (parseError) {
  468. console.error('❌ JSON 解析失败,响应内容:', text);
  469. throw new Error('GeoServer 返回的数据格式不正确,不是有效的 JSON');
  470. }
  471. // console.log('✅ 获取到数据:', geoJsonData.features?.length || 0, '条记录');
  472. // 调试:打印第一个要素的 properties
  473. // if (geoJsonData.features && geoJsonData.features.length > 0) {
  474. // console.log('📋 第一个要素的 properties:', geoJsonData.features[0].properties);
  475. // console.log('📋 pH 字段值 (ph_mean):', geoJsonData.features[0].properties.ph_mean);
  476. // console.log('📋 pH 字段值 (le_data__4):', geoJsonData.features[0].properties.le_data__4);
  477. // }
  478. // 保存数据用于统计和交互
  479. allFeatures = geoJsonData.features;
  480. samplePointsData.value = geoJsonData.features;
  481. // 计算统计信息
  482. calculatePHDistribution(allFeatures);
  483. loadStatistics();
  484. // 添加点击事件监听
  485. map.on('click', handleMapClick);
  486. } catch (err) {
  487. console.error('❌ 加载统计数据失败:', err);
  488. }
  489. }
  490. // 计算 pH 分布
  491. function calculatePHDistribution(features) {
  492. const config = currentMapType.value === 'rebound' ? CONFIG.reboundGeoserver : CONFIG.geoserver
  493. const distribution = {
  494. range1: 0,
  495. range2: 0,
  496. range3: 0
  497. }
  498. features.forEach(feature => {
  499. const phValue = feature.properties[config.phField]
  500. const numericPh = typeof phValue === 'string' ? parseFloat(phValue) : phValue
  501. if (numericPh && numericPh > 0) {
  502. if (numericPh <= 5.2) distribution.range1++
  503. else if (numericPh <= 6) distribution.range2++
  504. else distribution.range3++
  505. }
  506. })
  507. phDistribution.value = distribution
  508. }
  509. // 处理地图点击,查询地块属性
  510. async function handleMapClick(e) {
  511. const latlng = e.latlng;
  512. // console.log('🔍 地图点击:', latlng);
  513. try {
  514. // 根据当前地图类型选择配置
  515. const config = currentMapType.value === 'rebound' ? CONFIG.reboundGeoserver : CONFIG.geoserver
  516. // console.log('📋 当前地图类型:', currentMapType.value);
  517. // console.log('📋 查询的数据层:', config.dataLayer);
  518. // 构建 WMS GetFeatureInfo 请求
  519. const size = map.getSize();
  520. const point = map.latLngToContainerPoint(latlng);
  521. const params = new URLSearchParams({
  522. SERVICE: 'WMS',
  523. VERSION: '1.1.1',
  524. REQUEST: 'GetFeatureInfo',
  525. LAYERS: `${config.workspace}:${config.dataLayer}`,
  526. QUERY_LAYERS: `${config.workspace}:${config.dataLayer}`,
  527. STYLES: '',
  528. SRS: 'EPSG:4326',
  529. BBOX: map.getBounds().toBBoxString(),
  530. WIDTH: size.x,
  531. HEIGHT: size.y,
  532. X: Math.round(point.x),
  533. Y: Math.round(point.y),
  534. INFO_FORMAT: 'application/json',
  535. FEATURE_COUNT: 1
  536. });
  537. const url = `${config.wmsUrl}?${params.toString()}`;
  538. // console.log('🔍 查询 URL:', url);
  539. const response = await fetch(url);
  540. if (!response.ok) {
  541. throw new Error(`HTTP error! status: ${response.status}`);
  542. }
  543. const data = await response.json();
  544. // console.log('� 查询结果:', data);
  545. if (data.features && data.features.length > 0) {
  546. const feature = data.features[0];
  547. const ph = parsePHValue(feature.properties[config.phField]);
  548. // console.log('📋 pH 字段:', config.phField);
  549. // console.log('📋 pH 值:', ph);
  550. // console.log('📋 完整属性:', feature.properties);
  551. selectedPoint.value = {
  552. ph: ph,
  553. properties: feature.properties
  554. };
  555. // console.log('✅ 选中地块:', selectedPoint.value);
  556. } else {
  557. // console.log('⚠️ 该位置没有地块数据');
  558. selectedPoint.value = null;
  559. }
  560. } catch (err) {
  561. console.error('❌ 查询地块信息失败:', err);
  562. alert('查询地块信息失败,请重试');
  563. }
  564. }
  565. onUnmounted(()=>{
  566. if(map) {
  567. map.off('click', handleMapClick) // 移除点击事件监听
  568. map.remove()
  569. map = null
  570. }
  571. if(wmsLayer) {
  572. wmsLayer = null
  573. }
  574. if(markersLayer) {
  575. markersLayer = null
  576. }
  577. if(geoJsonLayer) { // 原 markersLayer 改为 geoJsonLayer(代码中实际定义的是 geoJsonLayer)
  578. geoJsonLayer = null
  579. }
  580. samplePointsData.value = []
  581. selectedPoint.value = null
  582. })
  583. onMounted(async()=>{
  584. await initMap()
  585. await initMap2() // 初始化第二个地图
  586. await fetchDataForStatistics()
  587. await loadStatistics()
  588. await loadCDData() // 加载 CD 数据
  589. // loadCDStatistics() 会在 loadCDData 完成后自动调用
  590. })
  591. onUnmounted(()=>{
  592. if(map) {
  593. map.remove()
  594. map = null
  595. }
  596. if(map2) {
  597. map2.remove()
  598. map2 = null
  599. }
  600. })
  601. </script>
  602. <template>
  603. <div class="map-container">
  604. <div class="ph-map" ref="mapRef"></div>
  605. <div class="ph-map" ref="mapRef2"></div>
  606. <!-- 计算刷新按钮 -->
  607. <div class="compute">
  608. <button class="combtn" @click="switchMap('normal')">实施降酸措施一周期后</button>
  609. <button class="combtn" @click="switchMap('rebound')">反酸一周期后</button>
  610. </div>
  611. <!-- pH 统计 -->
  612. <div class="statistics-panel">
  613. <h4>📊 韶关土壤 pH 统计</h4>
  614. <div class="stat-row">
  615. <span class="stat-label">地块总数:</span>
  616. <span class="stat-value">{{ statistics.totalBlocks }} 个</span>
  617. </div>
  618. <div class="stat-row highlight">
  619. <span class="stat-label">平均 pH 值:</span>
  620. <span class="stat-value" :class="getPHLevelClass(parseFloat(statistics.avgPH))">
  621. {{ statistics.avgPH || '-' }}
  622. </span>
  623. </div>
  624. <div class="stat-row">
  625. <span class="stat-label">强酸性 (pH≤5.2):</span>
  626. <span class="stat-value danger">{{ statistics.strongAcidCount }} 个</span>
  627. </div>
  628. <div class="stat-row">
  629. <span class="stat-label">强酸性面积占比:</span>
  630. <span class="stat-value danger">{{ statistics.strongAcidPercent }}%</span>
  631. </div>
  632. <div class="stat-row">
  633. <span class="stat-label">弱酸性 (pH 5.2~6.0): </span>
  634. <span class="stat-value warning">{{ statistics.mildAcidCount }} 个</span>
  635. </div>
  636. <div class="stat-row">
  637. <span class="stat-label">弱酸性面积占比:</span>
  638. <span class="stat-value warning">{{ statistics.mildAcidPercent }}%</span>
  639. </div>
  640. <div class="stat-row">
  641. <span class="stat-label">正常 (pH≥6.0):</span>
  642. <span class="stat-value success">{{ statistics.normalCount }} 个</span>
  643. </div>
  644. <div class="stat-row">
  645. <span class="stat-label">正常面积占比:</span>
  646. <span class="stat-value success">{{ statistics.normalPercent }}%</span>
  647. </div>
  648. <div class="stat-divider"></div>
  649. <div class="stat-row small">
  650. <span class="stat-label">总面积:</span>
  651. <span class="stat-value">{{ statistics.totalArea }} 亩</span>
  652. </div>
  653. <div class="stat-row small">
  654. <span class="stat-label">最高 pH:</span>
  655. <span class="stat-value success">{{ statistics.maxPH || '-' }}</span>
  656. </div>
  657. <div class="stat-row small">
  658. <span class="stat-label">最低 pH:</span>
  659. <span class="stat-value danger">{{ statistics.minPH || '-' }}</span>
  660. </div>
  661. </div>
  662. <!-- CD 含量统计 -->
  663. <div class="statistics-panel" style="top: 550px;">
  664. <h4>🌾 作物 Cd 含量统计</h4>
  665. <div class="stat-row">
  666. <span class="stat-label">地块总数:</span>
  667. <span class="stat-value">{{ cdStatistics.totalBlocks }} 个</span>
  668. </div>
  669. <div class="stat-row highlight">
  670. <span class="stat-label">平均 Cd 含量:</span>
  671. <span class="stat-value" :class="cdStatistics.avgCD > 0 ? 'success' : 'danger'">
  672. {{ cdStatistics.avgCD > 0 ? cdStatistics.avgCD + ' mg/kg' : '-' }}
  673. </span>
  674. </div>
  675. <div class="stat-row">
  676. <span class="stat-label">安全 (<0.2 mg/kg):</span>
  677. <span class="stat-value success">{{ cdStatistics.safeCount }} 个</span>
  678. </div>
  679. <div class="stat-row">
  680. <span class="stat-label">安全面积占比:</span>
  681. <span class="stat-value success">{{ cdStatistics.safePercent }}%</span>
  682. </div>
  683. <div class="stat-row">
  684. <span class="stat-label">预警 (0.2-0.3 mg/kg):</span>
  685. <span class="stat-value warning">{{ cdStatistics.warningCount }} 个</span>
  686. </div>
  687. <div class="stat-row">
  688. <span class="stat-label">预警面积占比:</span>
  689. <span class="stat-value warning">{{ cdStatistics.warningPercent }}%</span>
  690. </div>
  691. <div class="stat-row">
  692. <span class="stat-label">超标 (≥0.3 mg/kg):</span>
  693. <span class="stat-value danger">{{ cdStatistics.exceedCount }} 个</span>
  694. </div>
  695. <div class="stat-row">
  696. <span class="stat-label">超标面积占比:</span>
  697. <span class="stat-value danger">{{ cdStatistics.exceedPercent }}%</span>
  698. </div>
  699. <div class="stat-divider"></div>
  700. <div class="stat-row small">
  701. <span class="stat-label">总面积:</span>
  702. <span class="stat-value">{{ cdStatistics.totalArea }} 亩</span>
  703. </div>
  704. <div class="stat-row small">
  705. <span class="stat-label">最高 Cd:</span>
  706. <span class="stat-value danger">{{ cdStatistics.maxCD > 0 ? cdStatistics.maxCD + ' mg/kg' : '-' }}</span>
  707. </div>
  708. <div class="stat-row small">
  709. <span class="stat-label">最低 Cd:</span>
  710. <span class="stat-value success">{{ cdStatistics.minCD > 0 ? cdStatistics.minCD + ' mg/kg' : '-' }}</span>
  711. </div>
  712. </div>
  713. <!-- ph 分布 -->
  714. <div class="distribution-chart">
  715. <h4>📈 pH 分布</h4>
  716. <div class="bar-chart">
  717. <div
  718. v-for="(value, key) in phDistribution"
  719. :key="key"
  720. class="bar-item"
  721. >
  722. <div class="bar-label">{{ getPHRangeLabel(key) }}</div>
  723. <div class="bar-container">
  724. <div
  725. class="bar-fill"
  726. :style="{ width: `${(value / statistics.totalBlocks) * 100}%` }"
  727. :class="
  728. {
  729. danger: key === 'range1',
  730. warning: key === 'range2',
  731. success: key === 'range3'
  732. }
  733. "
  734. ></div>
  735. <span class="bar-value">{{ value }}</span>
  736. </div>
  737. </div>
  738. </div>
  739. </div>
  740. <!-- CD 含量分布 -->
  741. <div class="distribution-chart" style="top: 550px;">
  742. <h4>🌾 作物 Cd 含量分布</h4>
  743. <div class="bar-chart">
  744. <div class="bar-item">
  745. <div class="bar-label">安全 (<0.2)</div>
  746. <div class="bar-container">
  747. <div
  748. class="bar-fill success"
  749. :style="{ width: `${cdStatistics.totalBlocks > 0 ? (cdDistribution.safe / cdStatistics.totalBlocks) * 100 : 0}%` }"
  750. ></div>
  751. <span class="bar-value">{{ cdDistribution.safe }}</span>
  752. </div>
  753. </div>
  754. <div class="bar-item">
  755. <div class="bar-label">预警 (0.2-0.3)</div>
  756. <div class="bar-container">
  757. <div
  758. class="bar-fill warning"
  759. :style="{ width: `${cdStatistics.totalBlocks > 0 ? (cdDistribution.warning / cdStatistics.totalBlocks) * 100 : 0}%` }"
  760. ></div>
  761. <span class="bar-value">{{ cdDistribution.warning }}</span>
  762. </div>
  763. </div>
  764. <div class="bar-item">
  765. <div class="bar-label">超标 (≥0.3)</div>
  766. <div class="bar-container">
  767. <div
  768. class="bar-fill danger"
  769. :style="{ width: `${cdStatistics.totalBlocks > 0 ? (cdDistribution.exceed / cdStatistics.totalBlocks) * 100 : 0}%` }"
  770. ></div>
  771. <span class="bar-value">{{ cdDistribution.exceed }}</span>
  772. </div>
  773. </div>
  774. </div>
  775. </div>
  776. <div class="stat-comment" v-if="statistics.avgPH > 0">
  777. <div class="comment-title">📝 综合评估:</div>
  778. <div class="comment-text" :class="getPHLevelClass(statistics.avgPH)">
  779. {{ getPHComment(statistics.avgPH) }}
  780. </div>
  781. </div>
  782. <!-- 图例 -->
  783. <div class="legend">
  784. <h4>pH 值图例</h4>
  785. <div class="legend-item">
  786. <span class="legend-color" style="background: #ef4444;"></span>
  787. <span>≤ 5.2 (强酸性)</span>
  788. </div>
  789. <div class="legend-item">
  790. <span class="legend-color" style="background: #eab308;"></span>
  791. <span>5.2 - 6.0 (弱酸性)</span>
  792. </div>
  793. <div class="legend-item">
  794. <span class="legend-color" style="background: #22c55e;"></span>
  795. <span>≥ 6.0 (中性/碱性)</span>
  796. </div>
  797. <div class="legend-item">
  798. <span class="legend-color" style="background: #cccccc;"></span>
  799. <span>无数据</span>
  800. </div>
  801. </div>
  802. <!-- CD 含量图例 -->
  803. <div class="legend" style="top: 580px;">
  804. <h4>🌾 Cd 含量图例</h4>
  805. <div class="legend-item">
  806. <span class="legend-color" style="background: #22c55e;"></span>
  807. <span>0.0 - 0.2 mg/kg (安全)</span>
  808. </div>
  809. <div class="legend-item">
  810. <span class="legend-color" style="background: #eab308;"></span>
  811. <span>0.2-0.3 mg/kg (预警)</span>
  812. </div>
  813. <div class="legend-item">
  814. <span class="legend-color" style="background: #ef4444;"></span>
  815. <span>≥ 0.3 mg/kg (超标)</span>
  816. </div>
  817. </div>
  818. <!-- 采样点详情 -->
  819. <div class="point-detail-modal" v-if="selectedPoint">
  820. <div class="detail-content">
  821. <div class="detail-header">
  822. <h4>📍 采样点详情</h4>
  823. <button @click="selectedPoint = null" class="close-btn">×</button>
  824. </div>
  825. <div class="detail-row">
  826. <span class="detail-label">pH 值:</span>
  827. <span class="detail-value" :class="getPHLevelClass(selectedPoint.ph)">
  828. {{ selectedPoint.ph?.toFixed(2) || '-' }}
  829. </span>
  830. </div>
  831. <div class="detail-row">
  832. <span class="detail-label">酸化程度:</span>
  833. <span :class="['detail-value', getPHLevelClass(selectedPoint.ph)]">
  834. {{ !selectedPoint.ph || selectedPoint.ph <= 0 ? '无数据' : selectedPoint.ph <= 5.2 ? '强酸性' : selectedPoint.ph < 6.0 ? '弱酸性' : '正常' }}
  835. </span>
  836. </div>
  837. <div class="detail-row">
  838. <span class="detail-label">建议:</span>
  839. <span class="detail-suggestion">
  840. {{ !selectedPoint.ph || selectedPoint.ph <= 0 ? '无数据' :selectedPoint.ph <= 5.2 ? '立即治理,施用石灰改良' : selectedPoint.ph < 6.0 ? '注意保持,适量施用有机肥' : '继续保持当前管理措施' }}
  841. </span>
  842. </div>
  843. </div>
  844. </div>
  845. </div>
  846. </template>
  847. <style scoped>
  848. .map-container{
  849. width: 100%;
  850. height: 100vh;
  851. position: absolute;
  852. left:0;
  853. top:0;
  854. z-index: 1000;
  855. }
  856. .ph-map{
  857. width: 600px;
  858. height: 500px; /* 固定高度 */
  859. border-radius: 16px;
  860. border: 3px solid #1092d8;
  861. overflow: hidden;
  862. margin-bottom: 20px; /* 两个地图之间的间距 */
  863. }
  864. .ph-map:first-child {
  865. margin-bottom: 50px; /* 第一个地图下方留空 */
  866. }
  867. /* ✅ 统计面板样式 */
  868. .statistics-panel {
  869. position: absolute;
  870. top: 0px;
  871. left: 650px;
  872. background: rgba(255, 255, 255, 0.95);
  873. padding: 20px;
  874. border-radius: 12px;
  875. box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
  876. z-index: 1000;
  877. min-width: 350px;
  878. height: 450px;
  879. backdrop-filter: blur(10px);
  880. border: 2px solid rgba(16, 146, 216, 0.2);
  881. }
  882. .compute {
  883. position: absolute;
  884. display: flex;
  885. gap: 15px;
  886. top:5px;
  887. right: 20px;
  888. padding: 20px;
  889. z-index: 1000;
  890. }
  891. .combtn {
  892. padding: 15px 20px;
  893. font-size: 14px;
  894. font-weight: 600;
  895. color: #fff;
  896. background: linear-gradient(135deg, #1092d8 0%, #0d7bb8 100%);
  897. border: none;
  898. border-radius: 8px;
  899. cursor: pointer;
  900. box-shadow: 0 4px 12px rgba(16, 146, 216, 0.3);
  901. transition: all 0.3s ease;
  902. white-space: nowrap;
  903. }
  904. .combtn:hover {
  905. background: linear-gradient(135deg, #0d7bb8 0%, #0a6598 100%);
  906. box-shadow: 0 6px 16px rgba(16, 146, 216, 0.4);
  907. transform: translateY(-2px);
  908. }
  909. .combtn:active {
  910. transform: translateY(0);
  911. box-shadow: 0 2px 8px rgba(16, 146, 216, 0.3);
  912. }
  913. .statistics-panel h4 {
  914. margin: 0 0 15px 0;
  915. font-size: 18px;
  916. color: #1092d8;
  917. border-bottom: 2px solid #1092d8;
  918. padding-bottom: 10px;
  919. font-weight: bold;
  920. }
  921. .stat-row {
  922. display: flex;
  923. justify-content: space-between;
  924. align-items: center;
  925. margin-bottom: 12px;
  926. font-size: 14px;
  927. line-height: 1.4; /* 新增行高,提升可读性 */
  928. }
  929. .stat-row.highlight {
  930. background: linear-gradient(to right, rgba(16, 146, 216, 0.1), transparent);
  931. padding: 8px 12px;
  932. border-radius: 6px;
  933. margin-bottom: 15px;
  934. }
  935. .stat-row.small {
  936. font-size: 12px;
  937. margin-bottom: 6px;
  938. }
  939. .stat-label {
  940. color: #666;
  941. font-weight: 500;
  942. }
  943. .stat-value {
  944. font-weight: 600;
  945. font-size: 16px;
  946. color: #333;
  947. letter-spacing: 0.5px; /* 新增字间距 */
  948. }
  949. .stat-value.danger {
  950. color: #ef4444;
  951. }
  952. .stat-value.warning {
  953. color: #f59e0b;
  954. }
  955. .stat-value.success {
  956. color: #22c55e;
  957. }
  958. .stat-divider {
  959. height: 1px;
  960. background: linear-gradient(to right, transparent, #ddd, transparent);
  961. margin: 15px 0;
  962. }
  963. /* 综合评语 */
  964. .stat-comment {
  965. position: absolute;
  966. top: 300px; /* 放在 pH 分布统计下面 */
  967. right: 10px;
  968. padding: 15px;
  969. width: 350px; /* 和 pH 分布统计差不多的宽度 */
  970. background: rgba(255, 255, 255, 0.95);
  971. border-radius: 12px;
  972. box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
  973. z-index: 1000;
  974. backdrop-filter: blur(10px);
  975. border-left: 4px solid #1092d8;
  976. }
  977. .comment-title {
  978. font-size: 15px;
  979. color: #666;
  980. margin-bottom: 6px;
  981. font-weight: bold;
  982. }
  983. .comment-text {
  984. font-size: 14px;
  985. font-weight: bold;
  986. color: #333;
  987. }
  988. .comment-text.danger {
  989. color: #ef4444;
  990. }
  991. .comment-text.warning {
  992. color: #f59e0b;
  993. }
  994. .comment-text.success {
  995. color: #22c55e;
  996. }
  997. .distribution-chart {
  998. position: absolute;
  999. top:100px;
  1000. right: 10px;
  1001. background: rgba(255, 255, 255, 0.95);
  1002. padding: 15px;
  1003. border-radius: 12px;
  1004. box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
  1005. z-index: 1000;
  1006. min-width: 350px;
  1007. backdrop-filter: blur(10px);
  1008. }
  1009. .distribution-chart h4 {
  1010. margin: 0 0 12px 0;
  1011. font-size: 16px;
  1012. color: #1092d8;
  1013. border-bottom: 2px solid #1092d8;
  1014. padding-bottom: 8px;
  1015. }
  1016. .bar-chart {
  1017. display: flex;
  1018. flex-direction: column;
  1019. gap: 8px;
  1020. }
  1021. .bar-item {
  1022. display: flex;
  1023. align-items: center;
  1024. gap: 8px;
  1025. }
  1026. .bar-label {
  1027. font-size: 11px;
  1028. color: #666;
  1029. min-width: 60px;
  1030. }
  1031. .bar-container {
  1032. flex: 1;
  1033. position: relative;
  1034. height: 20px;
  1035. background: #f0f0f0;
  1036. border-radius: 4px;
  1037. overflow: hidden;
  1038. border: 1px solid #e5e7eb;
  1039. }
  1040. .bar-fill {
  1041. height: 100%;
  1042. transition: width 0.3s ease-in-out; /* 缓动动画更丝滑 */
  1043. border-radius: 3px; /* 圆角匹配容器 */
  1044. }
  1045. .bar-fill.danger {
  1046. background: #ef4444;
  1047. }
  1048. .bar-fill.warning {
  1049. background: #f59e0b;
  1050. }
  1051. .bar-fill.success {
  1052. background: #22c55e;
  1053. }
  1054. .bar-value {
  1055. position: absolute;
  1056. right: 8px;
  1057. top: 50%;
  1058. transform: translateY(-50%);
  1059. font-size: 11px;
  1060. font-weight: bold;
  1061. color: black;
  1062. text-shadow: 0 1px 2px rgba(0,0,0,0.2); /* 新增文字阴影 */
  1063. }
  1064. .alert-panel {
  1065. position: absolute;
  1066. top: 20px;
  1067. left: 10px;
  1068. background: rgba(255, 255, 255, 0.95);
  1069. padding: 15px;
  1070. border-radius: 12px;
  1071. box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
  1072. z-index: 1000;
  1073. min-width: 250px;
  1074. max-height: 300px;
  1075. overflow-y: auto;
  1076. backdrop-filter: blur(10px);
  1077. }
  1078. .alert-panel h4 {
  1079. margin: 0 0 12px 0;
  1080. font-size: 16px;
  1081. color: #ef4444;
  1082. border-bottom: 2px solid #ef4444;
  1083. padding-bottom: 8px;
  1084. }
  1085. .alert-list {
  1086. display: flex;
  1087. flex-direction: column;
  1088. gap: 6px;
  1089. }
  1090. .alert-item {
  1091. display: flex;
  1092. align-items: center;
  1093. gap: 8px;
  1094. padding: 8px;
  1095. background: rgba(239, 68, 68, 0.05);
  1096. border-radius: 6px;
  1097. cursor: pointer;
  1098. transition: all 0.3s;
  1099. }
  1100. .alert-item:hover {
  1101. background: rgba(239, 68, 68, 0.15);
  1102. transform: translateX(4px);
  1103. }
  1104. .alert-rank {
  1105. background: #ef4444;
  1106. color: white;
  1107. width: 20px;
  1108. height: 20px;
  1109. border-radius: 50%;
  1110. display: flex;
  1111. align-items: center;
  1112. justify-content: center;
  1113. font-size: 12px;
  1114. font-weight: bold;
  1115. flex-shrink: 0;
  1116. }
  1117. .alert-name {
  1118. flex: 1;
  1119. font-size: 13px;
  1120. color: #333;
  1121. }
  1122. .alert-ph {
  1123. font-size: 12px;
  1124. font-weight: bold;
  1125. }
  1126. .alert-ph.danger {
  1127. color: #ef4444;
  1128. }
  1129. /* 图例样式 */
  1130. .legend {
  1131. position: absolute;
  1132. top: 30px;
  1133. left: 30px;
  1134. background: rgba(255, 255, 255, 0.95);
  1135. padding: 15px;
  1136. border-radius: 8px;
  1137. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  1138. min-width: 10px;
  1139. backdrop-filter: blur(4px);
  1140. z-index: 0;
  1141. }
  1142. .legend h4 {
  1143. margin: 0 0 10px 0;
  1144. font-size: 10px;
  1145. color: #333;
  1146. border-bottom: 2px solid #1092d8;
  1147. padding-bottom: 6px;
  1148. }
  1149. .legend-item {
  1150. display: flex;
  1151. align-items: center;
  1152. gap: 8px;
  1153. margin-bottom: 6px;
  1154. font-size: 8px;
  1155. color: #555;
  1156. }
  1157. .legend-item:last-child {
  1158. margin-bottom: 0;
  1159. }
  1160. .legend-color {
  1161. width: 10px;
  1162. height: 10px;
  1163. border-radius: 4px;
  1164. border: 1px solid #ddd;
  1165. flex-shrink: 0;
  1166. }
  1167. .point-detail-modal {
  1168. position: fixed;
  1169. top: 50%;
  1170. left: 50%;
  1171. transform: translate(-50%, -50%);
  1172. background: white;
  1173. padding: 20px;
  1174. border-radius: 12px;
  1175. box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
  1176. z-index: 2000;
  1177. min-width: 300px;
  1178. border: 1px solid rgba(16, 146, 216, 0.2); /* 新增边框 */
  1179. animation: fadeIn 0.3s ease; /* 新增淡入动画 */
  1180. }
  1181. /* 新增弹窗动画 */
  1182. @keyframes fadeIn {
  1183. from {
  1184. opacity: 0;
  1185. transform: translate(-50%, -45%);
  1186. }
  1187. to {
  1188. opacity: 1;
  1189. transform: translate(-50%, -50%);
  1190. }
  1191. }
  1192. .detail-content {
  1193. position: relative;
  1194. }
  1195. .detail-content h4 {
  1196. margin: 0 0 15px 0;
  1197. color: #1092d8;
  1198. border-bottom: 2px solid #1092d8;
  1199. padding-bottom: 10px;
  1200. }
  1201. .detail-row {
  1202. display: flex;
  1203. justify-content: space-between;
  1204. margin-bottom: 12px;
  1205. padding: 8px;
  1206. background: #f8f9fa;
  1207. border-radius: 6px;
  1208. }
  1209. .detail-label {
  1210. font-weight: 500;
  1211. color: #666;
  1212. }
  1213. .detail-value {
  1214. font-weight: bold;
  1215. color: #333;
  1216. }
  1217. .detail-value.danger {
  1218. color: #ef4444;
  1219. }
  1220. .detail-value.warning {
  1221. color: #f59e0b;
  1222. }
  1223. .detail-value.success {
  1224. color: #22c55e;
  1225. }
  1226. .detail-suggestion {
  1227. color: #1092d8;
  1228. font-size: 13px;
  1229. line-height: 1.5;
  1230. }
  1231. /* 自定义 Tooltip 样式 */
  1232. :deep(.custom-tooltip) {
  1233. background: rgba(255, 255, 255, 0.95);
  1234. border: 2px solid #1092d8;
  1235. border-radius: 8px;
  1236. padding: 8px 12px;
  1237. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  1238. font-family: 'Microsoft YaHei', sans-serif;
  1239. backdrop-filter: blur(4px);
  1240. }
  1241. .custom-marker {
  1242. background: transparent !important;
  1243. border: none !important;
  1244. }
  1245. .detail-content {
  1246. position: relative;
  1247. }
  1248. .detail-header {
  1249. display: flex;
  1250. justify-content: space-between;
  1251. align-items: center;
  1252. margin-bottom: 15px;
  1253. }
  1254. .close-btn {
  1255. background: none;
  1256. border: none;
  1257. font-size: 20px;
  1258. cursor: pointer;
  1259. color: #909399;
  1260. padding: 0;
  1261. width: 20px;
  1262. height: 20px;
  1263. display: flex;
  1264. align-items: center;
  1265. justify-content: center;
  1266. border-radius: 50%; /* 新增圆形背景 */
  1267. transition: all 0.2s; /* 新增过渡 */
  1268. }
  1269. .close-btn:hover {
  1270. color: #ef4444; /* 改为红色更醒目 */
  1271. background: #fef2f2; /* 新增背景色 */
  1272. }
  1273. </style>