Explorar el Código

灌溉水+大气地图展示修改,数据统计图

yes-yes-yes-k hace 9 meses
padre
commit
aa88d1c525
Se han modificado 100 ficheros con 13827 adiciones y 1928 borrados
  1. 14 0
      .hintrc
  2. 11 0
      components.d.ts
  3. 2 2
      index.html
  4. 663 3
      package-lock.json
  5. 5 0
      package.json
  6. 0 0
      public/data/韶关市各区县边界图.geojson
  7. BIN
      public/reddog.png
  8. BIN
      public/图1.png
  9. BIN
      public/图片2.png
  10. BIN
      public/图片3.png
  11. BIN
      public/图片4.jpg
  12. BIN
      public/图片5.jpg
  13. BIN
      public/图片6.jpg
  14. 2 2
      src/App.vue
  15. BIN
      src/assets/bg/agricultural_input.png
  16. BIN
      src/assets/bg/atmospheric_deposition.png
  17. BIN
      src/assets/bg/irrigation.jpg
  18. BIN
      src/assets/bg/rain_removal.png
  19. BIN
      src/assets/bg/straw-removal.png
  20. BIN
      src/assets/bg/subsurface-leakage.jpg
  21. BIN
      src/assets/bg/surface-runoff.jpg
  22. BIN
      src/assets/city-bg.jpg
  23. BIN
      src/assets/favicon.ico
  24. BIN
      src/assets/header-bg.jpg
  25. BIN
      src/assets/login-bg.png
  26. BIN
      src/assets/背景图.jpg
  27. 110 144
      src/components/layout/AppAside.vue
  28. 56 18
      src/components/layout/AppAsideForTab2.vue
  29. 596 296
      src/components/layout/AppLayout.vue
  30. 170 72
      src/components/layout/menuItems.ts
  31. 230 145
      src/router/index.ts
  32. 0 0
      src/views/Admin/dataManagement/AdminRegionData.vue
  33. 23 0
      src/views/Admin/dataManagement/ClimateInfoData.vue
  34. 0 0
      src/views/Admin/dataManagement/CropHeavyMetalData.vue
  35. 8 0
      src/views/Admin/dataManagement/GeographicEnvInfoData.vue
  36. 1 1
      src/views/Admin/dataManagement/LandUseTypeData.vue
  37. 23 0
      src/views/Admin/dataManagement/SoilAcidificationData.vue
  38. 0 0
      src/views/Admin/dataManagement/SoilAssessmentUnitData.vue
  39. 23 0
      src/views/Admin/dataManagement/SoilHeavyMetalData.vue
  40. 552 0
      src/views/Admin/dataManagement/Visualizatio.vue
  41. 536 0
      src/views/Admin/dataManagement/Visualization.vue
  42. 23 0
      src/views/Admin/modelManagement/AcidReductionModel.vue
  43. 32 0
      src/views/Admin/modelManagement/CadmiumPredictionModel.vue
  44. 23 0
      src/views/Admin/modelManagement/RiceRiskModel.vue
  45. 1 4
      src/views/Admin/modelManagement/VegetableRiskModel.vue
  46. 23 0
      src/views/Admin/modelManagement/WheatRiskModel.vue
  47. 353 0
      src/views/Admin/parameterConfig/ModelSelection.vue
  48. 326 0
      src/views/Admin/parameterConfig/ModelTrain.vue
  49. 148 0
      src/views/Admin/parameterConfig/thres.vue
  50. 283 0
      src/views/User/HmOutFlux/agriInput/prodInputFlux.vue
  51. 25 0
      src/views/User/HmOutFlux/agriInput/samplingDesc2.vue
  52. 23 0
      src/views/User/HmOutFlux/atmosDeposition/airInputFlux.vue
  53. 107 0
      src/views/User/HmOutFlux/atmosDeposition/airSampleData.vue
  54. 559 0
      src/views/User/HmOutFlux/atmosDeposition/airSampleTencentMap.vue
  55. 164 0
      src/views/User/HmOutFlux/atmosDeposition/airsampleChart.vue
  56. 231 0
      src/views/User/HmOutFlux/atmosDeposition/airsampleLine.vue
  57. 479 0
      src/views/User/HmOutFlux/atmosDeposition/atmCompanytencentMap.vue
  58. 218 0
      src/views/User/HmOutFlux/atmosDeposition/atmcompanyline.vue
  59. 239 0
      src/views/User/HmOutFlux/atmosDeposition/atmcompanymap.vue
  60. 331 0
      src/views/User/HmOutFlux/atmosDeposition/atmsamplemap.vue
  61. 112 0
      src/views/User/HmOutFlux/atmosDeposition/heavyMetalEnterprise.vue
  62. 250 0
      src/views/User/HmOutFlux/atmosDeposition/heavyMetalEnterprisechart.vue
  63. 25 0
      src/views/User/HmOutFlux/atmosDeposition/samplingDesc3.vue
  64. 64 0
      src/views/User/HmOutFlux/irrigationWater/crossSection.vue
  65. 139 0
      src/views/User/HmOutFlux/irrigationWater/crossSectionSamplelineData.vue
  66. 217 0
      src/views/User/HmOutFlux/irrigationWater/crossSetionData1.vue
  67. 183 0
      src/views/User/HmOutFlux/irrigationWater/crossSetionData2.vue
  68. 344 0
      src/views/User/HmOutFlux/irrigationWater/crossSetionTencentmap.vue
  69. 272 0
      src/views/User/HmOutFlux/irrigationWater/crosssectionmap.vue
  70. 275 0
      src/views/User/HmOutFlux/irrigationWater/irriWaterInputFlux.vue
  71. 109 0
      src/views/User/HmOutFlux/irrigationWater/irriWaterSampleData.vue
  72. 310 0
      src/views/User/HmOutFlux/irrigationWater/irrwatermap.vue
  73. 87 0
      src/views/User/HmOutFlux/irrigationWater/riverwaterassay.vue
  74. 75 0
      src/views/User/HmOutFlux/irrigationWater/samplingMethodDevice1.vue
  75. 178 208
      src/views/User/HmOutFlux/irrigationWater/tencentMapView.vue
  76. 222 0
      src/views/User/HmOutFlux/irrigationWater/waterassaydata1.vue
  77. 294 0
      src/views/User/HmOutFlux/irrigationWater/waterassaydata2.vue
  78. 197 0
      src/views/User/HmOutFlux/irrigationWater/waterassaydata3.vue
  79. 340 0
      src/views/User/HmOutFlux/irrigationWater/waterassaydata4.vue
  80. 221 0
      src/views/User/HmOutFlux/irrigationWater/waterdataline.vue
  81. 336 41
      src/views/User/cadmiumPrediction/CropCadmiumPrediction.vue
  82. 335 40
      src/views/User/cadmiumPrediction/EffectiveCadmiumPrediction.vue
  83. 142 0
      src/views/User/cadmiumPrediction/currentYearConcentration.vue
  84. 126 0
      src/views/User/cadmiumPrediction/netFlux.vue
  85. 134 0
      src/views/User/cadmiumPrediction/totalInputFlux.vue
  86. 147 0
      src/views/User/cadmiumPrediction/totalOutputFlux.vue
  87. 647 3
      src/views/User/heavyMetalFluxCalculation/inputFluxCalculation/irrigationWater.vue
  88. 0 2
      src/views/User/heavyMetalFluxCalculation/inputFluxCalculation/waterdata/point.vue
  89. 238 0
      src/views/User/hmInFlux/grainRemoval/grainRemovalInputFlux.vue
  90. 25 0
      src/views/User/hmInFlux/grainRemoval/samplingDesc1.vue
  91. 25 0
      src/views/User/hmInFlux/strawRemoval/samplingDesc2.vue
  92. 258 0
      src/views/User/hmInFlux/strawRemoval/strawRemovalInputFlux.vue
  93. 25 0
      src/views/User/hmInFlux/subsurfaceLeakage/samplingDesc3.vue
  94. 118 0
      src/views/User/hmInFlux/subsurfaceLeakage/subsurfaceLeakageInputFlux.vue
  95. 25 0
      src/views/User/hmInFlux/surfaceRunoff/samplingDesc4.vue
  96. 119 0
      src/views/User/hmInFlux/surfaceRunoff/surfaceRunoffInputFlux.vue
  97. 1 1
      src/views/User/introduction/Introduce.vue
  98. 57 571
      src/views/User/mapView/tencentMapView.vue
  99. 282 144
      src/views/User/selectCityAndCounty.vue
  100. 260 231
      src/views/login/loginView.vue

+ 14 - 0
.hintrc

@@ -0,0 +1,14 @@
+{
+  "extends": [
+    "development"
+  ],
+  "hints": {
+    "meta-viewport": "off",
+    "axe/language": [
+      "default",
+      {
+        "html-has-lang": "off"
+      }
+    ]
+  }
+}

+ 11 - 0
components.d.ts

@@ -14,6 +14,7 @@ declare module 'vue' {
     ElAside: typeof import('element-plus/es')['ElAside']
     ElAvatar: typeof import('element-plus/es')['ElAvatar']
     ElButton: typeof import('element-plus/es')['ElButton']
+    ElCard: typeof import('element-plus/es')['ElCard']
     ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
     ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
     ElCol: typeof import('element-plus/es')['ElCol']
@@ -21,14 +22,24 @@ declare module 'vue' {
     ElDropdown: typeof import('element-plus/es')['ElDropdown']
     ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
     ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
+    ElForm: typeof import('element-plus/es')['ElForm']
+    ElFormItem: typeof import('element-plus/es')['ElFormItem']
     ElHeader: typeof import('element-plus/es')['ElHeader']
     ElIcon: typeof import('element-plus/es')['ElIcon']
+    ElImage: typeof import('element-plus/es')['ElImage']
+    ElInput: typeof import('element-plus/es')['ElInput']
+    ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
     ElMain: typeof import('element-plus/es')['ElMain']
     ElMenu: typeof import('element-plus/es')['ElMenu']
     ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
+    ElOption: typeof import('element-plus/es')['ElOption']
     ElRow: typeof import('element-plus/es')['ElRow']
     ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
+    ElSelect: typeof import('element-plus/es')['ElSelect']
     ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
+    ElTable: typeof import('element-plus/es')['ElTable']
+    ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
+    ElTabPane: typeof import('element-plus/es')['ElTabPane']
     ElTabs: typeof import('element-plus/es')['ElTabs']
     HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
     IconCommunity: typeof import('./src/components/icons/IconCommunity.vue')['default']

+ 2 - 2
index.html

@@ -1,5 +1,5 @@
 <!DOCTYPE html>
-<html lang="">
+<html lang="zh">
   <head>
     <meta charset="UTF-8">
     <link rel="icon" href="/logo.ico">
@@ -10,4 +10,4 @@
     <div id="app"></div>
     <script type="module" src="/src/main.ts"></script>
   </body>
-</html>
+</html>

+ 663 - 3
package-lock.json

@@ -9,6 +9,7 @@
       "version": "0.0.0",
       "dependencies": {
         "@element-plus/icons-vue": "^2.3.1",
+        "@mapbox/polyline": "^1.2.1",
         "@turf/turf": "^7.2.0",
         "@types/d3": "^7.4.3",
         "@wangeditor/editor": "^5.1.23",
@@ -22,10 +23,14 @@
         "echarts-gl": "^2.0.9",
         "element-plus": "^2.9.3",
         "file-saver": "^2.0.5",
+        "font-awesome": "^4.7.0",
         "html2canvas": "^1.4.1",
         "leaflet": "^1.9.4",
+        "leaflet-compass": "^1.5.6",
         "leaflet.chinatmsproviders": "^3.0.6",
         "pinia": "^2.3.0",
+        "proj4": "^2.19.10",
+        "proj4leaflet": "^1.0.2",
         "sass": "^1.84.0",
         "vue": "^3.5.13",
         "vue-echarts": "^7.0.3",
@@ -112,7 +117,6 @@
       "version": "7.26.2",
       "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
       "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "@babel/helper-validator-identifier": "^7.25.9",
@@ -1396,6 +1400,17 @@
       "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
       "license": "MIT"
     },
+    "node_modules/@mapbox/polyline": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmmirror.com/@mapbox/polyline/-/polyline-1.2.1.tgz",
+      "integrity": "sha512-sn0V18O3OzW4RCcPoUIVDWvEGQaBNH9a0y5lgqrf5hUycyw1CzrhEoxV5irzrMNXKCkw1xRsZXcaVbsVZggHXA==",
+      "dependencies": {
+        "meow": "^9.0.0"
+      },
+      "bin": {
+        "polyline": "bin/polyline.bin.js"
+      }
+    },
     "node_modules/@nodelib/fs.scandir": {
       "version": "2.1.5",
       "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -5068,6 +5083,12 @@
         "@types/lodash": "*"
       }
     },
+    "node_modules/@types/minimist": {
+      "version": "1.2.5",
+      "resolved": "https://registry.npmmirror.com/@types/minimist/-/minimist-1.2.5.tgz",
+      "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==",
+      "license": "MIT"
+    },
     "node_modules/@types/node": {
       "version": "22.15.31",
       "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.31.tgz",
@@ -5078,6 +5099,12 @@
         "undici-types": "~6.21.0"
       }
     },
+    "node_modules/@types/normalize-package-data": {
+      "version": "2.4.4",
+      "resolved": "https://registry.npmmirror.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz",
+      "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==",
+      "license": "MIT"
+    },
     "node_modules/@types/tough-cookie": {
       "version": "4.0.5",
       "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
@@ -5885,6 +5912,15 @@
         "url": "https://github.com/sponsors/jonschlinkert"
       }
     },
+    "node_modules/arrify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/arrify/-/arrify-1.0.1.tgz",
+      "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/assertion-error": {
       "version": "2.0.1",
       "resolved": "https://registry.npmmirror.com/assertion-error/-/assertion-error-2.0.1.tgz",
@@ -6048,6 +6084,32 @@
         "node": ">=8"
       }
     },
+    "node_modules/camelcase": {
+      "version": "5.3.1",
+      "resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz",
+      "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/camelcase-keys": {
+      "version": "6.2.2",
+      "resolved": "https://registry.npmmirror.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz",
+      "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==",
+      "license": "MIT",
+      "dependencies": {
+        "camelcase": "^5.3.1",
+        "map-obj": "^4.0.0",
+        "quick-lru": "^4.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/caniuse-lite": {
       "version": "1.0.30001695",
       "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001695.tgz",
@@ -6855,6 +6917,40 @@
         }
       }
     },
+    "node_modules/decamelize": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/decamelize/-/decamelize-1.2.0.tgz",
+      "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/decamelize-keys": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/decamelize-keys/-/decamelize-keys-1.1.1.tgz",
+      "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==",
+      "license": "MIT",
+      "dependencies": {
+        "decamelize": "^1.1.0",
+        "map-obj": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/decamelize-keys/node_modules/map-obj": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/map-obj/-/map-obj-1.0.1.tgz",
+      "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/decimal.js": {
       "version": "10.4.3",
       "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz",
@@ -7073,6 +7169,15 @@
         "url": "https://github.com/fb55/entities?sponsor=1"
       }
     },
+    "node_modules/error-ex": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmmirror.com/error-ex/-/error-ex-1.3.2.tgz",
+      "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+      "license": "MIT",
+      "dependencies": {
+        "is-arrayish": "^0.2.1"
+      }
+    },
     "node_modules/error-stack-parser-es": {
       "version": "0.1.5",
       "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-0.1.5.tgz",
@@ -7360,6 +7465,19 @@
         "node": ">=8"
       }
     },
+    "node_modules/find-up": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz",
+      "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+      "license": "MIT",
+      "dependencies": {
+        "locate-path": "^5.0.0",
+        "path-exists": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/follow-redirects": {
       "version": "1.15.9",
       "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
@@ -7380,6 +7498,15 @@
         }
       }
     },
+    "node_modules/font-awesome": {
+      "version": "4.7.0",
+      "resolved": "https://registry.npmmirror.com/font-awesome/-/font-awesome-4.7.0.tgz",
+      "integrity": "sha512-U6kGnykA/6bFmg1M/oT9EkFeIYv7JlX3bozwQJWiiLz6L0w3F5vBVPxHlwyX/vtNq1ckcpRKOB9f2Qal/VtFpg==",
+      "license": "(OFL-1.1 AND MIT)",
+      "engines": {
+        "node": ">=0.10.3"
+      }
+    },
     "node_modules/foreground-child": {
       "version": "3.3.0",
       "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz",
@@ -7450,6 +7577,15 @@
         "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
       }
     },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/gensync": {
       "version": "1.0.0-beta.2",
       "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -7577,6 +7713,27 @@
       "dev": true,
       "license": "ISC"
     },
+    "node_modules/hard-rejection": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/hard-rejection/-/hard-rejection-2.1.0.tgz",
+      "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/he": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
@@ -7594,6 +7751,36 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/hosted-git-info": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmmirror.com/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
+      "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==",
+      "license": "ISC",
+      "dependencies": {
+        "lru-cache": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/hosted-git-info/node_modules/lru-cache": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz",
+      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+      "license": "ISC",
+      "dependencies": {
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/hosted-git-info/node_modules/yallist": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz",
+      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+      "license": "ISC"
+    },
     "node_modules/html-encoding-sniffer": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
@@ -7718,6 +7905,15 @@
       "integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==",
       "license": "MIT"
     },
+    "node_modules/indent-string": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmmirror.com/indent-string/-/indent-string-4.0.0.tgz",
+      "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/ini": {
       "version": "1.3.8",
       "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
@@ -7734,6 +7930,12 @@
         "node": ">=12"
       }
     },
+    "node_modules/is-arrayish": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.2.1.tgz",
+      "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+      "license": "MIT"
+    },
     "node_modules/is-binary-path": {
       "version": "2.1.0",
       "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz",
@@ -7747,6 +7949,21 @@
         "node": ">=8"
       }
     },
+    "node_modules/is-core-module": {
+      "version": "2.16.1",
+      "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz",
+      "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+      "license": "MIT",
+      "dependencies": {
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/is-docker": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
@@ -7980,7 +8197,6 @@
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
       "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/jsdom": {
@@ -8082,6 +8298,15 @@
         "node": ">= 12"
       }
     },
+    "node_modules/kind-of": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmmirror.com/kind-of/-/kind-of-6.0.3.tgz",
+      "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/kolorist": {
       "version": "1.8.0",
       "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz",
@@ -8095,12 +8320,27 @@
       "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
       "license": "BSD-2-Clause"
     },
+    "node_modules/leaflet-compass": {
+      "version": "1.5.6",
+      "resolved": "https://registry.npmmirror.com/leaflet-compass/-/leaflet-compass-1.5.6.tgz",
+      "integrity": "sha512-Ix3p8/8D5tgNzz3Sp7hxQbMi8sG7Od5kb4qN+Encv4jLxht66mcMM3W7Nqo/1R7r/cESX3s0WyxQk4cIjL+w+Q==",
+      "license": "MIT",
+      "dependencies": {
+        "leaflet": "*"
+      }
+    },
     "node_modules/leaflet.chinatmsproviders": {
       "version": "3.0.6",
       "resolved": "https://registry.npmjs.org/leaflet.chinatmsproviders/-/leaflet.chinatmsproviders-3.0.6.tgz",
       "integrity": "sha512-B4UPSn2MT//RkFoyrVjwqQyfKuf4tSmMjJDKQ6nqwCCGgirYKRWHafSH9JmA88WoG5pkuMXBcKQhY32FobxU/g==",
       "license": "MIT"
     },
+    "node_modules/lines-and-columns": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+      "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+      "license": "MIT"
+    },
     "node_modules/local-pkg": {
       "version": "0.5.1",
       "resolved": "https://registry.npmmirror.com/local-pkg/-/local-pkg-0.5.1.tgz",
@@ -8118,6 +8358,18 @@
         "url": "https://github.com/sponsors/antfu"
       }
     },
+    "node_modules/locate-path": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz",
+      "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+      "license": "MIT",
+      "dependencies": {
+        "p-locate": "^4.1.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/lodash": {
       "version": "4.17.21",
       "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
@@ -8207,6 +8459,18 @@
         "@jridgewell/sourcemap-codec": "^1.5.0"
       }
     },
+    "node_modules/map-obj": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmmirror.com/map-obj/-/map-obj-4.3.0.tgz",
+      "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/marchingsquares": {
       "version": "1.3.3",
       "resolved": "https://registry.npmmirror.com/marchingsquares/-/marchingsquares-1.3.3.tgz",
@@ -8228,6 +8492,32 @@
         "node": ">= 0.10.0"
       }
     },
+    "node_modules/meow": {
+      "version": "9.0.0",
+      "resolved": "https://registry.npmmirror.com/meow/-/meow-9.0.0.tgz",
+      "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/minimist": "^1.2.0",
+        "camelcase-keys": "^6.2.2",
+        "decamelize": "^1.2.0",
+        "decamelize-keys": "^1.1.0",
+        "hard-rejection": "^2.1.0",
+        "minimist-options": "4.1.0",
+        "normalize-package-data": "^3.0.0",
+        "read-pkg-up": "^7.0.1",
+        "redent": "^3.0.0",
+        "trim-newlines": "^3.0.0",
+        "type-fest": "^0.18.0",
+        "yargs-parser": "^20.2.3"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/merge2": {
       "version": "1.4.1",
       "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz",
@@ -8238,6 +8528,12 @@
         "node": ">= 8"
       }
     },
+    "node_modules/mgrs": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/mgrs/-/mgrs-1.0.0.tgz",
+      "integrity": "sha512-awNbTOqCxK1DBGjalK3xqWIstBZgN6fxsMSiXLs9/spqWkF2pAhb2rrYCFSsr1/tT7PhcDGjZndG8SWYn0byYA==",
+      "license": "MIT"
+    },
     "node_modules/micromatch": {
       "version": "4.0.8",
       "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz",
@@ -8295,6 +8591,15 @@
         "node": ">= 0.6"
       }
     },
+    "node_modules/min-indent": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/min-indent/-/min-indent-1.0.1.tgz",
+      "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
     "node_modules/minimatch": {
       "version": "9.0.1",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz",
@@ -8311,6 +8616,29 @@
         "url": "https://github.com/sponsors/isaacs"
       }
     },
+    "node_modules/minimist-options": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmmirror.com/minimist-options/-/minimist-options-4.1.0.tgz",
+      "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==",
+      "license": "MIT",
+      "dependencies": {
+        "arrify": "^1.0.1",
+        "is-plain-obj": "^1.1.0",
+        "kind-of": "^6.0.3"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/minimist-options/node_modules/is-plain-obj": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
+      "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/minipass": {
       "version": "7.1.2",
       "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
@@ -8432,6 +8760,21 @@
         "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
       }
     },
+    "node_modules/normalize-package-data": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmmirror.com/normalize-package-data/-/normalize-package-data-3.0.3.tgz",
+      "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==",
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "hosted-git-info": "^4.0.1",
+        "is-core-module": "^2.5.0",
+        "semver": "^7.3.4",
+        "validate-npm-package-license": "^3.0.1"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/normalize-path": {
       "version": "3.0.0",
       "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -8567,6 +8910,42 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/p-limit": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz",
+      "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+      "license": "MIT",
+      "dependencies": {
+        "p-try": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-locate": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz",
+      "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+      "license": "MIT",
+      "dependencies": {
+        "p-limit": "^2.2.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/p-try": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz",
+      "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/package-json-from-dist": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
@@ -8581,6 +8960,30 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/parse-json": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmmirror.com/parse-json/-/parse-json-5.2.0.tgz",
+      "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.0.0",
+        "error-ex": "^1.3.1",
+        "json-parse-even-better-errors": "^2.3.0",
+        "lines-and-columns": "^1.1.6"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/parse-json/node_modules/json-parse-even-better-errors": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmmirror.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+      "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+      "license": "MIT"
+    },
     "node_modules/parse-ms": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz",
@@ -8614,6 +9017,15 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/path-exists": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz",
+      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/path-key": {
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -8624,6 +9036,12 @@
         "node": ">=8"
       }
     },
+    "node_modules/path-parse": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz",
+      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+      "license": "MIT"
+    },
     "node_modules/path-scurry": {
       "version": "1.11.1",
       "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
@@ -8832,6 +9250,28 @@
         "node": ">=6"
       }
     },
+    "node_modules/proj4": {
+      "version": "2.19.10",
+      "resolved": "https://registry.npmmirror.com/proj4/-/proj4-2.19.10.tgz",
+      "integrity": "sha512-uL6/C6kA8+ncJAEDmUeV8PmNJcTlRLDZZa4/87CzRpb8My4p+Ame4LhC4G3H/77z2icVqcu3nNL9h5buSdnY+g==",
+      "license": "MIT",
+      "dependencies": {
+        "mgrs": "1.0.0",
+        "wkt-parser": "^1.5.1"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ahocevar"
+      }
+    },
+    "node_modules/proj4leaflet": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/proj4leaflet/-/proj4leaflet-1.0.2.tgz",
+      "integrity": "sha512-6GdDeUlhX/tHUiMEj80xQhlPjwrXcdfD0D5OBymY8WvxfbmZcdhNqQk7n7nFf53ue6QdP9ls9ZPjsAxnbZDTsw==",
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "proj4": "^2.3.14"
+      }
+    },
     "node_modules/proto-list": {
       "version": "1.2.4",
       "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
@@ -8876,6 +9316,15 @@
       ],
       "license": "MIT"
     },
+    "node_modules/quick-lru": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmmirror.com/quick-lru/-/quick-lru-4.0.1.tgz",
+      "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/quickselect": {
       "version": "2.0.0",
       "resolved": "https://registry.npmmirror.com/quickselect/-/quickselect-2.0.0.tgz",
@@ -8905,6 +9354,83 @@
         "node": "^18.17.0 || >=20.5.0"
       }
     },
+    "node_modules/read-pkg": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmmirror.com/read-pkg/-/read-pkg-5.2.0.tgz",
+      "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/normalize-package-data": "^2.4.0",
+        "normalize-package-data": "^2.5.0",
+        "parse-json": "^5.0.0",
+        "type-fest": "^0.6.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/read-pkg-up": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmmirror.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz",
+      "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==",
+      "license": "MIT",
+      "dependencies": {
+        "find-up": "^4.1.0",
+        "read-pkg": "^5.2.0",
+        "type-fest": "^0.8.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/read-pkg-up/node_modules/type-fest": {
+      "version": "0.8.1",
+      "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.8.1.tgz",
+      "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
+      "license": "(MIT OR CC0-1.0)",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/read-pkg/node_modules/hosted-git-info": {
+      "version": "2.8.9",
+      "resolved": "https://registry.npmmirror.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
+      "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
+      "license": "ISC"
+    },
+    "node_modules/read-pkg/node_modules/normalize-package-data": {
+      "version": "2.5.0",
+      "resolved": "https://registry.npmmirror.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
+      "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "hosted-git-info": "^2.1.4",
+        "resolve": "^1.10.0",
+        "semver": "2 || 3 || 4 || 5",
+        "validate-npm-package-license": "^3.0.1"
+      }
+    },
+    "node_modules/read-pkg/node_modules/semver": {
+      "version": "5.7.2",
+      "resolved": "https://registry.npmmirror.com/semver/-/semver-5.7.2.tgz",
+      "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver"
+      }
+    },
+    "node_modules/read-pkg/node_modules/type-fest": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.6.0.tgz",
+      "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==",
+      "license": "(MIT OR CC0-1.0)",
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/readdirp": {
       "version": "3.6.0",
       "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz",
@@ -8931,12 +9457,45 @@
         "url": "https://github.com/sponsors/jonschlinkert"
       }
     },
+    "node_modules/redent": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/redent/-/redent-3.0.0.tgz",
+      "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
+      "license": "MIT",
+      "dependencies": {
+        "indent-string": "^4.0.0",
+        "strip-indent": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/regenerator-runtime": {
       "version": "0.14.1",
       "resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
       "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
       "license": "MIT"
     },
+    "node_modules/resolve": {
+      "version": "1.22.10",
+      "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.10.tgz",
+      "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
+      "license": "MIT",
+      "dependencies": {
+        "is-core-module": "^2.16.0",
+        "path-parse": "^1.0.7",
+        "supports-preserve-symlinks-flag": "^1.0.0"
+      },
+      "bin": {
+        "resolve": "bin/resolve"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/reusify": {
       "version": "1.0.4",
       "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.0.4.tgz",
@@ -9138,7 +9697,6 @@
       "version": "7.6.3",
       "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
       "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
-      "dev": true,
       "license": "ISC",
       "bin": {
         "semver": "bin/semver.js"
@@ -9265,6 +9823,38 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/spdx-correct": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmmirror.com/spdx-correct/-/spdx-correct-3.2.0.tgz",
+      "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "spdx-expression-parse": "^3.0.0",
+        "spdx-license-ids": "^3.0.0"
+      }
+    },
+    "node_modules/spdx-exceptions": {
+      "version": "2.5.0",
+      "resolved": "https://registry.npmmirror.com/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz",
+      "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==",
+      "license": "CC-BY-3.0"
+    },
+    "node_modules/spdx-expression-parse": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
+      "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
+      "license": "MIT",
+      "dependencies": {
+        "spdx-exceptions": "^2.1.0",
+        "spdx-license-ids": "^3.0.0"
+      }
+    },
+    "node_modules/spdx-license-ids": {
+      "version": "3.0.21",
+      "resolved": "https://registry.npmmirror.com/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz",
+      "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==",
+      "license": "CC0-1.0"
+    },
     "node_modules/speakingurl": {
       "version": "14.0.1",
       "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz",
@@ -9430,6 +10020,18 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/strip-indent": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/strip-indent/-/strip-indent-3.0.0.tgz",
+      "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
+      "license": "MIT",
+      "dependencies": {
+        "min-indent": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/strip-literal": {
       "version": "2.1.1",
       "resolved": "https://registry.npmmirror.com/strip-literal/-/strip-literal-2.1.1.tgz",
@@ -9463,6 +10065,18 @@
         "node": ">=16"
       }
     },
+    "node_modules/supports-preserve-symlinks-flag": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+      "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/svg-tags": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz",
@@ -9674,6 +10288,15 @@
         "node": ">=18"
       }
     },
+    "node_modules/trim-newlines": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/trim-newlines/-/trim-newlines-3.0.1.tgz",
+      "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/tslib": {
       "version": "2.3.0",
       "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
@@ -9686,6 +10309,18 @@
       "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==",
       "license": "ISC"
     },
+    "node_modules/type-fest": {
+      "version": "0.18.1",
+      "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.18.1.tgz",
+      "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==",
+      "license": "(MIT OR CC0-1.0)",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/typescript": {
       "version": "5.6.3",
       "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
@@ -9997,6 +10632,16 @@
         "base64-arraybuffer": "^1.0.2"
       }
     },
+    "node_modules/validate-npm-package-license": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmmirror.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+      "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "spdx-correct": "^3.0.0",
+        "spdx-expression-parse": "^3.0.0"
+      }
+    },
     "node_modules/vite": {
       "version": "6.3.5",
       "resolved": "https://registry.npmmirror.com/vite/-/vite-6.3.5.tgz",
@@ -11536,6 +12181,12 @@
       "integrity": "sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng==",
       "license": "MIT"
     },
+    "node_modules/wkt-parser": {
+      "version": "1.5.2",
+      "resolved": "https://registry.npmmirror.com/wkt-parser/-/wkt-parser-1.5.2.tgz",
+      "integrity": "sha512-1ZUiV1FTwSiSrgWzV9KXJuOF2BVW91KY/mau04BhnmgOdroRQea7Q0s5TVqwGLm0D2tZwObd/tBYXW49sSxp3Q==",
+      "license": "MIT"
+    },
     "node_modules/wmf": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
@@ -11719,6 +12370,15 @@
       "dev": true,
       "license": "ISC"
     },
+    "node_modules/yargs-parser": {
+      "version": "20.2.9",
+      "resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-20.2.9.tgz",
+      "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/yoctocolors": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.1.tgz",

+ 5 - 0
package.json

@@ -13,6 +13,7 @@
   },
   "dependencies": {
     "@element-plus/icons-vue": "^2.3.1",
+    "@mapbox/polyline": "^1.2.1",
     "@turf/turf": "^7.2.0",
     "@types/d3": "^7.4.3",
     "@wangeditor/editor": "^5.1.23",
@@ -26,10 +27,14 @@
     "echarts-gl": "^2.0.9",
     "element-plus": "^2.9.3",
     "file-saver": "^2.0.5",
+    "font-awesome": "^4.7.0",
     "html2canvas": "^1.4.1",
     "leaflet": "^1.9.4",
+    "leaflet-compass": "^1.5.6",
     "leaflet.chinatmsproviders": "^3.0.6",
     "pinia": "^2.3.0",
+    "proj4": "^2.19.10",
+    "proj4leaflet": "^1.0.2",
     "sass": "^1.84.0",
     "vue": "^3.5.13",
     "vue-echarts": "^7.0.3",

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
public/data/韶关市各区县边界图.geojson


BIN
public/reddog.png


BIN
public/图1.png


BIN
public/图片2.png


BIN
public/图片3.png


BIN
public/图片4.jpg


BIN
public/图片5.jpg


BIN
public/图片6.jpg


+ 2 - 2
src/App.vue

@@ -1,7 +1,7 @@
 <script setup lang='ts'>
 import { RouterView } from "vue-router"
 import request from './utils/request';
-import Atmcompanyline from "./views/User/heavyMetalFluxCalculation/inputFluxCalculation/atmospheredata/atmcompanyline.vue";
+import Map from "./views/User/HmOutFlux/irrigationWater/map.vue";
 request({
   url: '/table',
   method: 'get'
@@ -11,7 +11,7 @@ request({
 </script>
 
 <template>
-  <RouterView />
+  <RouterView/>
 </template>
 
 <style scoped></style>

BIN
src/assets/bg/agricultural_input.png


BIN
src/assets/bg/atmospheric_deposition.png


BIN
src/assets/bg/irrigation.jpg


BIN
src/assets/bg/rain_removal.png


BIN
src/assets/bg/straw-removal.png


BIN
src/assets/bg/subsurface-leakage.jpg


BIN
src/assets/bg/surface-runoff.jpg


BIN
src/assets/city-bg.jpg


BIN
src/assets/favicon.ico


BIN
src/assets/header-bg.jpg


BIN
src/assets/login-bg.png


BIN
src/assets/背景图.jpg


+ 110 - 144
src/components/layout/AppAside.vue

@@ -5,58 +5,31 @@
       router
       unique-opened
       :default-active="activeMenuItem.index"
-      :default-openeds="defaultOpeneds"
       class="professional-menu"
     >
       <template v-for="item in filteredMenuItems" :key="item.index">
-        <!-- 一级菜单:有二级子菜单 -->
-        <el-sub-menu 
-          v-if="item.children && item.children.length > 0" 
-          :index="item.index"
-        >
+        <el-sub-menu v-if="item.children" :index="item.index">
           <template #title>
-            <el-icon><component :is="item.icon || Sunny" /></el-icon>
+            <el-icon><component :is="item.icon" /></el-icon>
             <span>{{ item.label }}</span>
           </template>
-          
-          <!-- 遍历二级子菜单 -->
-          <template v-for="child in item.children.filter(child => child.index)" :key="child.index">
-            <!-- 二级菜单:有三级子菜单 -->
-            <el-sub-menu 
-              v-if="child.children && child.children.length > 0" 
-              :index="child.index"
-            >
-              <template #title>
-                <el-icon><component :is="child.icon || item.icon || Sunny" /></el-icon>
-                <span>{{ child.label }}</span>
-              </template>
-              
-              <!-- 遍历三级子菜单 -->
-              <el-menu-item 
-                v-for="grandchild in child.children" 
-                :key="grandchild.index" 
-                :index="grandchild.index" 
-                @click="handleMenuClick(grandchild)"
-              >
-                <el-icon><component :is="grandchild.icon || child.icon || item.icon || Sunny" /></el-icon>
-                <span>{{ grandchild.label }}</span>
-              </el-menu-item>
-            </el-sub-menu>
-            
-            <!-- 二级菜单:无三级子菜单 -->
-            <el-menu-item 
-              v-else 
-              :index="child.index"
-            >
-              <el-icon><component :is="child.icon || item.icon || Sunny" /></el-icon>
-              <span>{{ child.label }}</span>
-            </el-menu-item>
-          </template>
+          <el-menu-item
+            v-for="child in item.children"
+            :key="child.index"
+            :index="child.index"
+            @click="handleMenuClick(child.index)"
+          >
+            <el-icon><component :is="child.icon" /></el-icon>
+            <span>{{ child.label }}</span>
+          </el-menu-item>
         </el-sub-menu>
 
-        <!-- 一级菜单:无二级子菜单 -->
-        <el-menu-item v-else :index="item.index">
-          <el-icon><component :is="item.icon || Sunny" /></el-icon>
+        <el-menu-item
+          v-else
+          :index="item.index"
+          @click="handleMenuClick(item.index)"
+        >
+          <el-icon><component :is="item.icon" /></el-icon>
           <span>{{ item.label }}</span>
         </el-menu-item>
       </template>
@@ -68,8 +41,7 @@
 import { reactive, computed, inject, toRefs, watch } from 'vue';
 import { useRouter } from 'vue-router';
 import { ElMessage } from 'element-plus';
-import { menuItems, type MenuItem } from './menuItems'; // 建议你将 menuItems 独立维护在该文件中
-import { Sunny } from '@element-plus/icons-vue';
+import { menuItems } from './menuItems'; // 建议你将 menuItems 独立维护在该文件中
 
 const props = defineProps({
   activeTab: {
@@ -91,128 +63,122 @@ const userPermissions = inject('userPermissions', [] as string[]);
 const router = useRouter();
 const activeMenuItem = reactive({ index: '' });
 
-// 1. 修复 filteredMenuItems 计算逻辑
-const filteredMenuItems = computed(() => {
-  // 确保包含所有子菜单(包括嵌套)
-  return menuItems.filter(item => {
-    return item.tab === activeTab.value || 
-           // 包含所有子菜单项
-           (item.children?.some(child => child.tab === activeTab.value));
-  });
-});
-
-// 2. 重构 defaultOpeneds 计算逻辑
-const defaultOpeneds = computed(() => {
-  const currentPath = router.currentRoute.value.path;
-  const openedMenus = new Set<string>();
+const filteredMenuItems = computed(() =>
+  menuItems
+    .filter(item => item.tab === activeTab.value)
+    .map(item => {
+      if (!item.children) return item;
+      return {
+        ...item,
+        children: item.children.filter(
+          child => !child.permission || userPermissions.includes(child.permission)
+        )
+      };
+    })
+);
 
-  // 第一级遍历:匹配直接路径
-  menuItems.forEach(item => {
-    if (item.index === currentPath) {
-      openedMenus.add(item.index);
+function handleMenuClick(index: string) {
+  try {
+    if (router.currentRoute.value.path !== index) {
+      router.push(index);
+      activeMenuItem.index = index;
     }
-  });
-
-  // 第二级遍历:匹配嵌套路径
-  menuItems.forEach(item => {
-    item.children?.forEach(child => {
-      // 直接匹配二级路径
-      if (child.index === currentPath) {
-        openedMenus.add(item.index);
-      }
-      
-      // 匹配三级路径
-      child.children?.forEach(grandchild => {
-        if (grandchild.index === currentPath) {
-          openedMenus.add(item.index);
-          openedMenus.add(child.index);
-        }
-      });
-    });
-  });
-
-  console.log("优化后展开菜单", Array.from(openedMenus));
-  return Array.from(openedMenus);
-});
-
-function handleMenuClick(menuItem: MenuItem) {
-  // 直接使用 index 作为目标路径
-  const targetPath = menuItem.index;
-  
-  // 添加调试信息
-  console.log('[导航] 当前路径:', router.currentRoute.value.path);
-  console.log('[导航] 目标路径:', targetPath);
-  
-  // 检查是否已经在目标路径
-  if (router.currentRoute.value.path !== targetPath) {
-    console.log('[导航] 执行导航到:', targetPath);
-    router.push(targetPath).catch(err => {
-      console.error('[导航错误]', err);
-    });
-  } else {
-    console.log('[导航] 已在目标路径,跳过导航');
+  } catch (error) {
+    ElMessage.error('导航失败,请检查网络或联系管理员');
   }
 }
 
-
-// 4. 删除重复的watch监听器(只保留一个)
 watch(
-  () => props.activeTab,
+  activeTab,
   newVal => {
     const current = filteredMenuItems.value?.[0];
     activeMenuItem.index = current?.children?.[0]?.index || current?.index || '';
   },
   { immediate: true }
 );
-watch(filteredMenuItems, (items) => {
-  items.forEach(item => {
-    console.log("父项index:", item.index); // 检查是否有特殊字符
-    item.children?.forEach(child => {
-      console.log("子项index:", child.index);
-    });
-  });
-});
-
 </script>
 
 <style scoped>
-.professional-menu .el-menu {
-  display: block !important;
-  background-color: #f4f6f9;
+:deep(.el-scrollbar) {
+  background: linear-gradient(to bottom, #B7F1FC , #FFF8F0) !important;
+  height: 100%;
+  border-right: none !important;
+  padding-top: 12px;
+}
+
+:deep(.el-scrollbar__wrap),
+:deep(.el-scrollbar__view) {
+  background: transparent;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+
+.professional-menu {
+  background: transparent;
   border-right: none;
-  margin-top: 20px;
-  box-shadow: 2px 0 10px rgba(0, 0, 0, 0.05);
-  font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
-  border-radius: 0 12px 12px 0;
+  padding-top: 12px;
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  min-height: 100%;
 }
 
-.professional-menu .el-menu-item,
-.professional-menu .el-submenu__title {
-  color: #333;
-  font-size: 15px;
-  padding: 14px 20px;
-  transition: background-color 0.3s ease, color 0.3s ease;
-  border-radius: 8px;
+:deep(.el-menu-item),
+:deep(.el-sub-menu__title) {
+  margin-left: 0 !important;
+  margin-right: 0 !important;
+  width: 100%;
+  box-sizing: border-box;
+  padding-left: 40px !important;
+  padding-right: 20px !important;
 }
 
-.professional-menu .el-menu-item:hover,
-.professional-menu .el-submenu__title:hover {
-  background-color: #e6f0ff;
-  color: #007bff;
+:deep(.el-sub-menu .el-menu-item) {
+  background-color: rgba(252, 234, 183, 0.3) !important;
 }
 
-.professional-menu .el-menu-item.is-active {
-  background-color: #007bff;
-  color: #fff;
-  font-weight: bold;
-  box-shadow: inset 0 0 8px rgba(0, 123, 255, 0.4);
+:deep(.el-sub-menu .el-menu-item:not(:last-child)) {
+  margin-bottom: 0;
 }
 
-.professional-menu .el-icon {
-  margin-right: 8px;
+:deep(.el-sub-menu__title) {
+  font-size: 18px;
+  font-weight: 500;
+  color: #000000;
+  border-radius: 6px;
+  padding: 12px 16px !important;
+  transition: all 0.2s ease;
 }
-.professional-menu {
-  min-height: 200px !important; /* 强制设置最小高度 */
-  display: block !important; /* 确保元素显示 */
+
+/* Hover 效果 */
+:deep(.el-menu-item:hover),
+:deep(.el-sub-menu__title:hover) {
+  background-color: rgba(16, 146, 216, 0.1);
+  color: #1092D8;
+}
+
+/* 激活高亮 */
+:deep(.el-menu-item.is-active),
+:deep(.el-sub-menu__title.is-active) {
+  background: linear-gradient(to right, #1092D8, #02C3AD);
+  color: #ffffff !important;
+  border-radius: 8px;
+  font-weight: 600;
+  box-shadow: 0 2px 8px rgba(16, 146, 216, 0.25);
+}
+
+/* 子菜单标题样式 + 图标右移 */
+:deep(.el-sub-menu__title) {
+  display: flex;
+  align-items: center;
+  padding-left: 20px !important;
+}
+
+/* 下拉图标右移 */
+:deep(.el-sub-menu__icon-arrow) {
+  margin-left: auto;
+  margin-right: -160px;
+  transition: transform 0.3s ease;
 }
 </style>

+ 56 - 18
src/components/layout/AppAsideForTab2.vue

@@ -114,29 +114,67 @@ async function handleMenuClick(index: string | RouteLocationAsRelativeGeneric |
 </script>
 
 <style scoped>
-.el-menu {
-  background-color: #ffffff;
-  border-right: none;
-  margin-top: 10px;
+/* 给 el-menu 组件设置背景渐变 */
+:deep(.el-menu) {
+  background: linear-gradient(to bottom, #B7F1FC , #FFF8F0) !important;
+  height: 100%;
+  border-right: none !important;
+  padding-top: 12px;
 }
 
-.el-menu-item {
-  color: #000000;
-  font-size: 14px;
-  padding: 10px 20px;
-  border-radius: 6px;
-  transition: background-color 0.3s ease, color 0.3s ease;
+/* 保持滚动条内容透明 */
+:deep(.el-scrollbar__wrap),
+:deep(.el-scrollbar__view) {
+  background: transparent !important;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
 }
 
-.el-menu-item:hover {
-  background-color: #f3f4f6;
-  color: #2563eb;
+/* 菜单项和子菜单标题 */
+:deep(.el-menu-item),
+:deep(.el-sub-menu__title) {
+  margin-left: 0 !important;
+  margin-right: 0 !important;
+  width: 100%;
+  box-sizing: border-box;
+  padding-left: 40px !important;
+  padding-right: 20px !important;
 }
 
-.el-menu-item.is-active {
-  background-color: #2563eb;
-  color: #ffffff;
-  font-weight: bold;
-  box-shadow: inset 0 0 10px rgba(37, 99, 235, 0.5);
+/* 子菜单中菜单项背景色 */
+:deep(.el-sub-menu .el-menu-item) {
+  background-color: rgba(252, 234, 183, 0.3) !important;
+}
+
+/* Hover 效果 */
+:deep(.el-menu-item:hover),
+:deep(.el-sub-menu__title:hover) {
+  background-color: rgba(16, 146, 216, 0.1) !important;
+  color: #1092D8 !important;
+}
+
+/* 激活高亮 */
+:deep(.el-menu-item.is-active),
+:deep(.el-sub-menu__title.is-active) {
+  background: linear-gradient(to right, #1092D8, #02C3AD) !important;
+  color: #ffffff !important;
+  border-radius: 8px !important;
+  font-weight: 600 !important;
+  box-shadow: 0 2px 8px rgba(16, 146, 216, 0.25) !important;
+}
+
+/* 子菜单标题样式 + 图标右移 */
+:deep(.el-sub-menu__title) {
+  display: flex;
+  align-items: center;
+  padding-left: 20px !important;
+}
+
+/* 下拉图标右移 */
+:deep(.el-sub-menu__icon-arrow) {
+  margin-left: auto;
+  margin-right: -160px;
+  transition: transform 0.3s ease;
 }
 </style>

+ 596 - 296
src/components/layout/AppLayout.vue

@@ -1,217 +1,387 @@
+<template>
+  <div class="layout-wrapper" :class="{ 'full-screen': isFullScreen }">
+    <!-- 背景层 - 只在需要透明的地方显示 -->
+    <div 
+      class="background-layer" 
+      v-if="!isFullScreen && !isSelectCity && !isExcludedRoute"
+    ></div>
+    
+    <!-- Header 部分 -->
+    <el-header 
+      class="layout-header" 
+      v-if="!isFullScreen"
+      :class="{ 'excluded-bg-header': isExcludedRoute }"
+    >
+      <div class="logo-title-row">
+        <img src="@/assets/logo.png" alt="Logo" class="logo" />
+        <div class="title-and-user">
+          <span 
+            class="project-name"
+            :class="{ 'excluded-text': isExcludedRoute }"
+          >
+            区域土壤重金属污染风险评估
+          </span>
+          
+          <!-- 用户信息 - 在select-city页面隐藏 -->
+          <div 
+            class="user-info-row" 
+            v-if="!isSelectCity"
+            :class="{ 'excluded-text': isExcludedRoute }"
+          >
+            <span class="welcome-text">
+              欢迎{{
+                tokenStore.token.loginType === "admin" ? "管理员" : "用户"
+              }}登录成功
+            </span>
+            <el-dropdown>
+              <span class="el-dropdown-link">
+                <el-avatar
+                  :size="40"
+                  :class="{ 'excluded-avatar-border': isExcludedRoute }"
+                  src="https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png"
+                />
+              </span>
+              <template #dropdown>
+                <el-dropdown-menu>
+                  <el-dropdown-item disabled
+                    >用户名:{{ userInfo.name }}</el-dropdown-item
+                  >
+                  <el-dropdown-item divided @click="handleLogout"
+                    >退出登录</el-dropdown-item
+                  >
+                </el-dropdown-menu>
+              </template>
+            </el-dropdown>
+          </div>
+        </div>
+      </div>
+    </el-header>
+
+    <!-- Tab 区域 - 不透明 -->
+    <div class="tabs-row" v-if="!isFullScreen">
+      <el-tabs
+        v-if="showTabs"
+        v-model="activeName"
+        class="demo-tabs"
+        :style="tabStyle"
+        @tab-click="handleClick"
+      >
+        <el-tab-pane v-for="tab in tabs" :key="tab.name" :name="tab.name">
+          <template #label>
+            <i :class="['tab-icon', tab.icon]"></i>
+            <span class="tab-label-text">{{ tab.label }}</span>
+          </template>
+        </el-tab-pane>
+      </el-tabs>
+      <div v-else class="single-tab" @click="handleClick(tabs[0], $event)">
+        <i :class="['tab-icon', tabs[0].icon]"></i>
+        <span class="tab-label-text">{{ tabs[0].label }}</span>
+      </div>
+    </div>
+
+    <!-- 主体区域 -->
+    <el-container class="layout-main-container">
+      <!-- 侧边栏 - 不透明 -->
+      <el-aside v-if="showAside && showTabs" class="layout-aside">
+        <component
+          :is="AsideComponent"
+          :activeTab="activeName"
+          :showTabs="showTabs"
+        />
+      </el-aside>
+
+      <!-- 内容区域 -->
+      <el-main 
+        class="layout-content-wrapper" 
+        :style="mainStyle"
+        :class="{ 'excluded-bg-content': isExcludedRoute }"
+      >
+        <div
+          class="scrollable-content"
+          :style="{ 
+            backgroundImage: currentBgImage,
+          }"
+        >
+          <div :class="{ 'select-city-container': isSelectCity }">
+            <RouterView />
+          </div>
+        </div>
+      </el-main>
+    </el-container>
+  </div>
+</template>
+
 <script setup lang="ts">
 import { ref, reactive, computed, watch, defineAsyncComponent } from "vue";
-import type { Component } from "vue";
 import { useRouter, useRoute } from "vue-router";
 import { useTokenStore } from "@/stores/mytoken";
-import { ElMessageBox, ElMessage,ElTabPane, type Instance, type TabsPaneContext } from "element-plus";
+import { ElMessageBox, ElMessage } from "element-plus";
 import { logout } from "@/API/users";
 
-import { onMounted } from 'vue';
-
-onMounted(() => {
-  router.beforeEach((to, from) => {
-    console.log("[路由导航] 从:", from.path, "到:", to.path);
-    return true;
-  });
-  
-  router.afterEach((to) => {
-    console.log("[路由完成] 当前路径:", to.path);
-  });
-});
-
-// 扩展路由元信息类型
-declare module 'vue-router' {
-  interface RouteMeta {
-    fullScreen?: boolean;
+// 使用更可靠的图片导入方式
+function getImageUrl(name: string) {
+  try {
+    return new URL(`../../assets/bg/${name}`, import.meta.url).href;
+  } catch (error) {
+    console.error("加载背景图失败:", error);
+    return "";
   }
 }
 
-type RouteConfig = 
-  | string  // 基础路由(如 "/agricultureProductInput")
-  | {       // 带标签和子路由的嵌套路由
-      path: string;       // 路由路径
-      label: string;      // 显示名称
-      children?: RouteConfig[]; // 子路由(支持多层嵌套)
-    };
-// 定义Tab项类型
-interface TabItem {
-  name: string;
-  label: string;
-  icon: string;
-  routes: RouteConfig[];
-}
-
-// 定义用户信息类型
-interface UserInfo {
-  name: string;
-}
-
-// 定义Token类型
-interface TokenInfo {
-  loginType: string;
-  name?: string;
-}
-
 const router = useRouter();
 const route = useRoute();
 const tokenStore = useTokenStore();
-
-// 类型断言,确保tokenStore类型正确
-const token = tokenStore.token as TokenInfo;
+const currentBgImage = ref("");
 
 // 是否为全屏页面
 const isFullScreen = computed(() => route.meta.fullScreen === true);
 
+// 判断是否是select-city页面
+const isSelectCity = computed(() => route.path === "/select-city");
+
+// 背景图路径映射 - 使用简单路径
+const routeBackgroundMap: Record<string, string> = {
+  "/samplingMethodDevice1": getImageUrl("irrigation.jpg"),
+  "/irriSampleData": getImageUrl("irrigation.jpg"),
+  "/csSampleData": getImageUrl("irrigation.jpg"),
+  "/irriInputFlux": getImageUrl("irrigation.jpg"),
+  "/prodInputFlux": getImageUrl("agricultural_input.png"),
+  "/airSampleData": getImageUrl("atmospheric_deposition.png"),
+  "/airInputFlux": getImageUrl("atmospheric_deposition.png"),
+  "/heavyMetalEnterprise": getImageUrl("atmospheric_deposition.png"),
+  "/grainRemovalInputFlux": getImageUrl("grain_removal.png"),
+  "/strawRemovalInputFlux": getImageUrl("straw_removal.png"),
+  "/subsurfaceLeakageInputFlux": getImageUrl("subsurface_leakage.jpg"),
+  "/surfaceRunoffInputFlux": getImageUrl("surface_runoff.jpg"),
+};
+
 // Tab 配置
-const tabs = computed<TabItem[]>(() => {
-  if (token.loginType === "admin") {
+const tabs = computed(() => {
+  if (tokenStore.token.loginType === "admin") {
     return [
-      { 
-        name: "dataManagement", 
-        label: "数据管理", 
-        icon: "el-icon-folder", 
-        routes: ["/soilAcidReductionData","/soilAcidificationData", "/AdminRegionData", "/SoilAssessmentUnitData", "/SoilHeavyMetalData", "/CropHeavyMetalData", "/LandUseTypeData", "/ClimateInfoData", "/GeographicEnvInfoData"] 
+      {
+        name: "dataManagement",
+        label: "数据管理",
+        icon: "el-icon-folder",
+        routes: [
+          "/soilAcidReductionData",
+          "/soilAcidificationData",
+          "/AdminRegionData",
+          "/SoilAssessmentUnitData",
+          "/SoilHeavyMetalData",
+          "/CropHeavyMetalData",
+          "/LandUseTypeData",
+          "/ClimateInfoData",
+          "/GeographicEnvInfoData",
+        ],
       },
-      { 
-        name: "infoManagement", 
-        label: "信息管理", 
-        icon: "el-icon-document", 
-        routes: ["/IntroductionUpdate"] 
+      {
+        name: "infoManagement",
+        label: "信息管理",
+        icon: "el-icon-document",
+        routes: ["/IntroductionUpdate"],
       },
-      { 
-        name: "modelManagement", 
-        label: "模型管理及配置", 
-        icon: "el-icon-cpu", 
-        routes: ["/CadmiumPredictionModel", "/EffectiveCadmiumModel", "/Admin/RiceRiskModel", "/AdminModelSelection", "/Admin/thres", "/Admin/ModelTrain", "/Admin/WheatRiskModel", "/Admin/VegetableRiskModel"] 
+      {
+        name: "modelManagement",
+        label: "模型管理及配置",
+        icon: "el-icon-cpu",
+        routes: [
+          "/CadmiumPredictionModel",
+          "/EffectiveCadmiumModel",
+          "/Admin/RiceRiskModel",
+          "/AdminModelSelection",
+          "/Admin/thres",
+          "/Admin/ModelTrain",
+          "/Admin/WheatRiskModel",
+          "/Admin/VegetableRiskModel",
+        ],
       },
-      { 
-        name: "userManagement", 
-        label: "用户管理", 
-        icon: "el-icon-user", 
-        routes: ["/UserManagement", "/UserRegistration"] 
+      {
+        name: "userManagement",
+        label: "用户管理",
+        icon: "el-icon-user",
+        routes: ["/UserManagement", "/UserRegistration"],
       },
     ];
   } else {
     return [
-      { 
-        name: "shuJuKanBan", 
-        label: "数据看板", 
-        icon: "el-icon-data-analysis", 
-        routes: ["/shuJuKanBan"] 
+      {
+        name: "shuJuKanBan",
+        label: "数据看板",
+        icon: "el-icon-data-analysis",
+        routes: ["/shuJuKanBan"],
       },
-      { 
-        name: "introduction", 
-        label: "软件简介", 
-        icon: "el-icon-info-filled", 
-        routes: ["/SoilPro", "/Overview", "/ResearchFindings", "/Unit"] 
+      {
+        name: "introduction",
+        label: "软件简介",
+        icon: "el-icon-info-filled",
+        routes: ["/SoilPro", "/Overview", "/ResearchFindings", "/Unit"],
       },
-      { 
-       name: "heavyMetalFluxCalculation", 
-       label: "重金属输入输出通量", 
-       icon: "el-icon-refresh", 
-       routes: [
-      // 灌溉水:改为对象,包含label和children三级子选项
-        {
-          path: "/irrigationWater", // 父路由路径
-          label: "灌溉水", // 显示的名称(二级选项名称)
-          children: [ // 三级子选项
-            { path: "/irrigationWater/info", label: "信息展示" },
-            { path: "/irrigationWater/map", label: "样本点地图展示" },
-            { path:"/irrigationWater/rivermap", label:"河流地图展示"},
-            { path: "/irrigationWater/data", label: "数据展示" }
-      ]
-        },
-        // 其他二级选项保持不变(字符串路由)
-        "/agricultureProductInput",
-        "/atmosphericDryWetDeposition"
-      ]
+      {
+        name: "HmOutFlux",
+        label: "重金属输入通量",
+        icon: "el-icon-refresh",
+        routes: [
+          "/samplingMethodDevice1",
+          "/irriSampleData",
+          "/csSampleData",
+          "/irriInputFlux",
+          "/samplingDesc2",
+          "/prodInputFlux",
+          "/samplingDesc3",
+          "/airSampleData",
+          "/airInputFlux",
+        ],
       },
-      { 
-        name: "mapView", 
-        label: "地图展示", 
-        icon: "el-icon-map-location", 
-        routes: ["/mapView"] 
+      {
+        name: "hmInFlux",
+        label: "重金属输出通量",
+        icon: "el-icon-refresh",
+        routes: [
+          "/samplingDesc1",
+          "/grainRemovalInputFlux",
+          "/samplingDesc2",
+          "/strawRemovalInputFlux",
+          "/samplingDesc3",
+          "/subsurfaceLeakageInputFlux",
+          "/samplingDesc4",
+          "/surfaceRunoffInputFlux",
+        ],
       },
-      { 
-        name: "cadmiumPrediction", 
-        label: "土壤镉预测", 
-        icon: "el-icon-c-scale-to-original", 
-        routes: ["/TotalCadmiumPrediction", "/EffectiveCadmiumPrediction"] 
+      {
+        name: "mapView",
+        label: "地图展示",
+        icon: "el-icon-map-location",
+        routes: ["/mapView"],
       },
-      { 
-        name: "cropRiskAssessment", 
-        label: "作物风险评估", 
-        icon: "el-icon-warning", 
-        routes: ["/cropRiskAssessment"] 
+      {
+        name: "cadmiumPrediction",
+        label: "土壤污染物含量预测",
+        icon: "el-icon-c-scale-to-original",
+        routes: [
+          "/totalInputFlux",
+          "/TotalCadmiumPrediction",
+          "/totalOutputFlux",
+          "/netFlux",
+          "/currentYearConcentration",
+          "/EffectiveCadmiumPrediction",
+          "CropCadmiumPrediction",
+        ],
       },
-      { 
-        name: "farmlandQualityAssessment", 
-        label: "耕地质量评估", 
-        icon: "el-icon-rank", 
-        routes: ["/farmlandQualityAssessment"] 
+      {
+        name: "cropRiskAssessment",
+        label: "作物风险评估",
+        icon: "el-icon-warning",
+        routes: ["/cropRiskAssessment"],
       },
-      { 
-        name: "soilAcidificationPrediction", 
-        label: "土壤酸化预测", 
-        icon: "el-icon-magic-stick", 
-        routes: ["/Calculation", "/AcidNeutralizationModel"] 
+      {
+        name: "farmlandQualityAssessment",
+        label: "耕地质量评估",
+        icon: "el-icon-rank",
+        routes: ["/farmlandQualityAssessment"],
       },
-      { 
-        name: "scenarioSimulation", 
-        label: "情景模拟", 
-        icon: "el-icon-s-operation", 
-        routes: ["/TraditionalFarmingRisk", "/HeavyMetalCadmiumControl", "/SoilAcidificationControl"] 
+      {
+        name: "soilAcidificationPrediction",
+        label: "土壤酸化预测",
+        icon: "el-icon-magic-stick",
+        routes: ["/Calculation", "/AcidNeutralizationModel"],
       },
-      { 
-        name: "dataStatistics", 
-        label: "数据统计", 
-        icon: "el-icon-pie-chart", 
-        routes: ["/DetectionStatistics", "/FarmlandPollutionStatistics", "/PlantingRiskStatistics"] 
+      {
+        name: "scenarioSimulation",
+        label: "情景模拟",
+        icon: "el-icon-s-operation",
+        routes: [
+          "/TraditionalFarmingRisk",
+          "/HeavyMetalCadmiumControl",
+          "/SoilAcidificationControl",
+        ],
       },
-    ];
+      {
+        name: "dataStatistics",
+        label: "数据统计",
+        icon: "el-icon-pie-chart",
+        routes: [
+          "/DetectionStatistics",
+          "/FarmlandPollutionStatistics",
+          "/PlantingRiskStatistics",
+        ],
+      },
+    ].filter(tab => !["shuJuKanBan", "mapView", "introduction"].includes(tab.name));
   }
 });
 
-// 当前激活 tab
-const activeName = ref<string>(tabs.value[0]?.name || "");
+// 计算:当前激活 tab - 保持不变
+const activeName = ref(tabs.value[0]?.name || "");
 const showTabs = computed(() => tabs.value.length > 1);
-const tabStyle = computed(() => tabs.value.length === 1 ? { width: "100%", justifyContent: "center" } : {});
+const tabStyle = computed(() =>
+  tabs.value.length === 1 ? { width: "100%", justifyContent: "center" } : {}
+);
 let hasNavigated = false;
 
-watch(() => activeName.value, (newTab) => {
-  const tab = tabs.value.find(t => t.name === newTab);
-  let targetPath: string | undefined;
-
-  if (tab) {
-    const firstRoute = tab.routes[0]; // RouteConfig 类型(可能是 string 或对象)
-    if (typeof firstRoute === 'string') {
-      targetPath = firstRoute; // 字符串直接用
-    } else {
-      targetPath = firstRoute.path; // 对象取 path 属性
+watch(
+  () => activeName.value,
+  (newTab) => {
+    const tab = tabs.value.find((t) => t.name === newTab);
+    const targetPath = tab?.routes?.[0];
+    if (!hasNavigated) {
+      hasNavigated = true;
+      return;
     }
-  }
+    if (tab && targetPath && router.currentRoute.value.path !== targetPath) {
+      router.push({ path: targetPath });
+    }
+  },
+  { immediate: true }
+);
+
+// 显式列出所有不需要背景图的路由路径
+const bgExcludedRoutes = computed(() => {
+  return [
+    // 列出所有不需要背景图的路由路径
+    "/shuJuKanBan",
+    "/mapView",
+    "/cropRiskAssessment",
+    "/farmlandQualityAssessment",
+    "/dataStatistics",
+    
+    // 强制"重金属输入通量"和"重金属输出通量"下的所有路由为白色背景
+    ...tabs.value
+      .filter(tab => ["HmOutFlux", "hmInFlux"].includes(tab.name))
+      .flatMap(tab => tab.routes)
+  ];
+});
 
-  if (!hasNavigated) {
-    hasNavigated = true;
-    return;
-  }
-  if (tab && targetPath && router.currentRoute.value.path !== targetPath) {
-    router.push({ path: targetPath }); // 现在 targetPath 一定是字符串
-  }
-}, { immediate: true });
+// 判断当前路由是否在排除列表中
+const isExcludedRoute = computed(() => bgExcludedRoutes.value.includes(route.path));
 
-// 处理Tab点击事件
-const handleClick = (tab: TabsPaneContext) => {
-  activeAsideTab.value = tab.props.name as string;
+// 获取当前页面的背景图
+const getBgImage = (path: string) => {
+  // 若当前路径属于排除项,不显示背景图
+  if (isExcludedRoute.value) return "";
+  
+  return routeBackgroundMap[path] || "";
 };
 
-const handleSingleTabClick = () => {
-  activeAsideTab.value = tabs.value[0].name;
+// 当前背景图
+watch(
+  () => route.path,
+  (newPath) => {
+    currentBgImage.value = getBgImage(newPath) ? `url(${getBgImage(newPath)})` : "";
+  },
+  { immediate: true }
+);
+
+// 点击切换 tab
+const handleClick = (tab: any, event: Event) => {
+  activeAsideTab.value = tab.props.name;
 };
-// 侧边栏组件类型
-const AsideComponent = computed<Component>(() => {
-  const adminTabs = ["parameterConfig", "dataManagement", "infoManagement", "modelManagement", "userManagement"];
-  if (adminTabs.includes(activeName.value)) {
+
+// 动态加载侧边栏组件
+const AsideComponent = computed(() => {
+  if (
+    ["parameterConfig", "dataManagement", "infoManagement", "modelManagement", "userManagement"]
+      .includes(activeName.value)
+  ) {
     return defineAsyncComponent(() => import("./AppAsideForTab2.vue"));
   } else {
     return defineAsyncComponent(() => import("./AppAside.vue"));
@@ -220,22 +390,17 @@ const AsideComponent = computed<Component>(() => {
 
 // 是否显示侧边栏
 const showAside = computed(() => {
-  const hideAsideTabs = ["mapView", "shuJuKanBan", "cropRiskAssessment", "farmlandQualityAssessment"];
-  const result = !isFullScreen.value && !hideAsideTabs.includes(activeName.value) && showTabs.value;
-  console.log("侧边栏是否显示:", result); // 关键打印
-  console.log("当前activeName:", activeName.value); // 查看当前激活的Tab名称
-  console.log("showTabs:", showTabs.value); // 查看tabs长度是否>1
-  return result;
+  return (
+    !isFullScreen.value &&
+    activeName.value !== "mapView" &&
+    activeName.value !== "cropRiskAssessment" &&
+    activeName.value !== "farmlandQualityAssessment"
+  );
 });
 
-const activeAsideTab=ref<string>(activeName.value);
-
-// 用户信息
-const userInfo = reactive<UserInfo>({ 
-  name: token.name || "未登录" 
-});
+const activeAsideTab = ref(activeName.value || "");
+const userInfo = reactive({ name: tokenStore.token.name || "未登录" });
 
-// 退出登录处理
 const handleLogout = async () => {
   try {
     await ElMessageBox.confirm("确定要退出登录吗?", "提示", {
@@ -247,168 +412,238 @@ const handleLogout = async () => {
     tokenStore.clearToken();
     ElMessage.success("退出成功");
     router.push("/login");
-  } catch (error) {
+  } catch {
     ElMessage.info("已取消退出");
   }
 };
 
-// 内容区样式
-const mainPaddingStyle = computed(() => {
+// 内容区样式
+const mainStyle = computed(() => {
   return {
     padding: ["mapView", "infoManagement"].includes(activeName.value) ? "0" : "20px",
+    overflow: "hidden",
   };
 });
-
-// 滚动条样式
-const scrollbarStyle = computed(() => {
-  return isFullScreen.value ? { height: "100vh" } : { height: "calc(100vh - 128px)" };
-});
 </script>
 
-<template>
-  <div class="common-layout" :class="{ 'full-screen': isFullScreen }">
-    <el-container class="layout-container">
-      <!-- 顶部 Header -->
-      <el-header class="layout-header" v-if="!isFullScreen">
-        <div class="logo-title-row">
-          <img src="@/assets/logo.png" alt="Logo" class="logo" />
-          <span class="project-name">区域土壤重金属污染风险评估</span>
-        </div>
-        <el-dropdown>
-            <span class="el-dropdown-link">
-              <el-avatar
-                :size="40"
-                src="https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png"
-              />
-            </span>
-            <template #dropdown>
-              <el-dropdown-menu>
-                <el-dropdown-item disabled>用户名:{{ userInfo.name }}</el-dropdown-item>
-                <el-dropdown-item divided @click="handleLogout">退出登录</el-dropdown-item>
-              </el-dropdown-menu>
-            </template>
-          </el-dropdown>
-      </el-header>
-
-      <!-- Tab 区域 -->
-      <div class="tabs-row" v-if="!isFullScreen">
-        <el-tabs v-if="showTabs" v-model="activeName" class="demo-tabs" :style="tabStyle" @tab-click=handleClick>
-          <el-tab-pane v-for="tab in tabs" :key="tab.name" :name="tab.name">
-            <template #label>
-              <i :class="['tab-icon', tab.icon]"></i>
-              <span class="tab-label-text">{{ tab.label }}</span>
-            </template>
-          </el-tab-pane>
-        </el-tabs>
-        <div v-else class="single-tab" @click="handleSingleTabClick">
-          <i :class="['tab-icon', tabs[0].icon]"></i>
-          <span class="tab-label-text">{{ tabs[0].label }}</span>
-        </div>
-      </div>
-
-      <!-- 主体区域 -->
-      <el-container class="layout-main-container">
-        <el-aside v-if="showAside" class="layout-aside">
-          <component :is="AsideComponent" :activeTab="activeName" :showTabs="showTabs" />
-        </el-aside>
+<style>
+/* 隐藏所有滚动条 */
+*::-webkit-scrollbar {
+  display: none; /* Chrome, Safari, Opera */
+}
 
-        <el-main class="layout-content-wrapper" :style="mainPaddingStyle">
-          <el-scrollbar :style="scrollbarStyle">
-            <RouterView />
-          </el-scrollbar>
-        </el-main>
-      </el-container>
-    </el-container>
-  </div>
-</template>
+* {
+  -ms-overflow-style: none; /* IE and Edge */
+  scrollbar-width: none; /* Firefox */
+}
 
-<style scoped>
-.layout-container {
-  height: 100vh;
+/* 整体布局容器 */
+.layout-wrapper {
   display: flex;
   flex-direction: column;
-  background-color: #f5f7fa;
+  height: 100vh;
+  overflow: hidden;
+  position: relative;
 }
 
+/* 全屏页面特殊处理 */
+.layout-wrapper.full-screen {
+  background: none;
+  min-height: 100vh;
+}
+
+/* 背景层 - 用于透明部分 */
+.background-layer {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: url("@/assets/header-bg.jpg") center center / cover no-repeat;
+  background-attachment: fixed;
+  z-index: -1; /* 确保在内容下方 */
+}
+
+/* 透明 Header */
 .layout-header {
-  height: 160px;
+  height: 150px;
   display: flex;
   align-items: center;
   justify-content: space-between;
-  padding: 0 50px;
-  background: linear-gradient(to bottom, #6c8598cc 0%, #5b7083cc 100%);
-  backdrop-filter: blur(12px);
-  -webkit-backdrop-filter: blur(12px);
+  background-color: transparent !important;
+  backdrop-filter: none !important;
+  -webkit-backdrop-filter: none !important;
   border-bottom: none;
+  box-shadow: none !important;
   color: #f0f3f7;
+  flex-shrink: 0;
+  position: relative; /* 确保在背景层上方 */
+  z-index: 1;
 }
 
-.tabs-row {
+/* 排除背景图页面的Header样式 */
+.excluded-bg-header {
+  background-color: #ffffff !important;
+  border-bottom: 1px solid #eee !important;
+}
+
+.logo-title-row {
   display: flex;
-  justify-content: center;
   align-items: center;
-  padding: 5px 24px;
-  background: linear-gradient(to bottom, #5b7083cc 0%, #4a5e70cc 100%);
-  backdrop-filter: blur(10px);
-  -webkit-backdrop-filter: blur(10px);
-  border-bottom: 1px solid rgba(255, 255, 255, 0.05);
+  gap: 24px;
+  width: 100%;
 }
 
-.logo-title-row {
+.title-and-user {
+  display: flex;
+  flex-direction: column;
+  flex: 1;
+}
+
+/* 用户信息区域 */
+.user-info-row {
   display: flex;
   align-items: center;
+  justify-content: flex-end;
   gap: 24px;
+  background-color: transparent !important;
+  box-shadow: none !important;
+  color: #ffffff;
+  padding-top: 1px;
+  position: static;
+  z-index: 100;
 }
 
-.logo {
-  height: 80px;
+/* 排除背景图页面的文字颜色 */
+.excluded-text {
+  color: #333 !important;
 }
 
-.project-name {
-  font-size: 32px; /* 原72px太大了,调整为更合理的大小 */
-  font-weight: bold;
-  color: #f0f3f7;
+/* 头像边框白色 */
+.el-dropdown-link .el-avatar {
+  border: 2px solid white;
+}
+
+/* 排除背景图页面的头像边框 */
+.excluded-avatar-border {
+  border: 2px solid #333 !important;
+}
+
+.welcome-text {
+  font-size: 28px;
+  font-weight: 500;
+  color: #ffffff !important;
+}
+
+/* 排除背景图页面的欢迎文字颜色 */
+.excluded-text .welcome-text {
+  color: #333 !important;
 }
 
+/* Tab 区域 - 不透明 */
+.tabs-row {
+  height: 48px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  padding: 0 24px;
+  background: linear-gradient(to right, #1092d8, #02c3ad);
+  backdrop-filter: blur(10px);
+  -webkit-backdrop-filter: blur(10px);
+  border-bottom: none !important;
+  box-shadow: none !important;
+  margin-bottom: 0 !important;
+  flex-shrink: 0;
+  position: relative; /* 确保在背景层上方 */
+  z-index: 1;
+}
+
+/* el-tabs 外层容器 */
 .demo-tabs {
-  height: 64px;
+  height: 48px !important;
   display: flex;
   align-items: center;
+  width: 100%;
+  padding: 0 !important;
+  margin: 0 !important;
+  border-bottom: none !important;
+}
+
+/* 清除滑块条和底部线条 */
+.el-tabs__nav-wrap::after,
+.el-tabs__active-bar {
+  display: none !important;
+  height: 0 !important;
+  border: none !important;
+}
+
+.el-tabs__nav-scroll {
+  padding: 0 !important;
+  margin: 0 !important;
 }
 
+/* Tabs 单项样式 */
 .el-tabs__item {
-  font-size: 16px; /* 原40px太大 */
+  height: 48px !important;
+  line-height: 48px !important;
+  display: flex !important;
+  align-items: center;
+  justify-content: center;
+  padding: 0 20px !important;
+  font-size: 20px;
   font-weight: 600;
-  padding: 12px 24px !important;
-  margin: 5px;
   color: #cfd8dc;
-  display: flex;
-  align-items: center;
   border-radius: 10px;
   transition: all 0.2s ease-in-out;
+  background-color: transparent;
+  position: relative;
+  z-index: 1;
 }
 
-.el-tabs__item:hover {
-  background-color: #455a64;
+/* 激活 Tab */
+.el-tabs__item.is-active {
+  background-color: #2a53ba;
   color: #ffffff;
+  font-weight: 700;
+  box-shadow: 0 4px 16px rgba(26, 188, 156, 0.4);
+  z-index: 2;
 }
 
-.el-tabs__item.is-active {
-  background-color: #1abc9c;
+/* 鼠标悬停 */
+.el-tabs__item:hover {
+  background-color: #455a64;
   color: #ffffff;
-  box-shadow: 0 4px 16px rgba(26, 188, 156, 0.4);
-  font-weight: 700;
 }
 
+/* 图标样式 */
 .tab-icon {
-  font-size: 20px;
-  margin-right: 8px;
+  font-size: 24px;
+  margin-right: 4px;
   color: inherit;
 }
 
+/* 文字样式 */
 .tab-label-text {
-  font-size: 14px;
+  font-size: 20px;
   color: inherit;
+  line-height: 1;
+  display: inline-block;
+}
+
+.logo {
+  height: 60px;
+}
+
+.project-name {
+  font-size: 48px;
+  font-weight: bold;
+  margin-top: 30px;
+  color: #f0f3f7;
+}
+
+/* 排除背景图页面的项目名称颜色 */
+.excluded-text.project-name {
+  color: #333 !important;
 }
 
 .layout-main-container {
@@ -416,34 +651,99 @@ const scrollbarStyle = computed(() => {
   display: flex;
   overflow: hidden;
   min-height: 0;
+  position: relative;
+  z-index: 1;
 }
 
+/* 侧边栏 - 不透明 */
 .layout-aside {
-  width: 270px;
-  background-color: #fff;
-  border-right: 1px solid #dcdfe6;
+  width: 360px;
+  background: linear-gradient(to bottom, #B7F1FC, #FFF8F0 );
+  border-right: 1px solid;
   overflow-y: auto;
+  color: #000000;
+  padding-top: 8px;
+  height: 100%;
+  position: relative;
+  z-index: 2; /* 确保在背景层上方 */
+}
+
+/* 隐藏侧边栏滚动条 */
+.layout-aside::-webkit-scrollbar {
+  display: none;
+}
+
+.layout-aside .el-menu-item,
+.layout-aside .el-sub-menu__title {
+  font-size: 18px;
+  font-weight: 500;
+  color: #000000;
+  background-color: transparent;
+  transition: all 0.2s ease;
+  border-radius: 6px;
+  padding: 12px 16px !important;
+}
+
+.layout-aside .el-menu-item:hover,
+.layout-aside .el-sub-menu__title:hover {
+  background-color: rgba(16, 146, 216, 0.1);
+  color: #1092d8;
+}
+
+.layout-aside .el-menu-item.is-active,
+.layout-aside .el-sub-menu__title.is-active {
+  background: linear-gradient(to right, #1092d8, #02c3ad);
+  color: #000000 !important;
+  border-radius: 8px;
+  font-weight: 600;
+  box-shadow: 0 2px 8px rgba(16, 146, 216, 0.25);
 }
 
 .layout-content-wrapper {
   flex: 1;
+  overflow: hidden;
   display: flex;
   flex-direction: column;
-  background-color: #fff;
-  overflow: hidden;
+  position: relative;
 }
 
-.single-tab {
-  display: flex;
-  align-items: center;
-  padding: 12px 24px;
-  cursor: pointer;
-  border-radius: 10px;
-  color: #cfd8dc;
+/* 排除背景图页面的内容区域 */
+.excluded-bg-content {
+  background-color: #ffffff !important;
 }
 
-.single-tab:hover {
-  background-color: #455a64;
-  color: #ffffff;
+/* 可滑动内容区域 */
+.scrollable-content {
+  flex: 1;
+  overflow: auto;
+  padding: 0 20px;
+  box-sizing: border-box;
+}
+
+/* 强制重置 el-tabs header 高度/边距/背景/阴影,避免背景层穿透错位 */
+.el-tabs__header.is-top {
+  height: 48px !important;
+  margin: 0 !important;
+  padding: 0 !important;
+  border: none !important;
+  background: transparent !important;
+  box-shadow: none !important;
+  z-index: 0 !important;
+}
+
+/* 全屏页面特殊处理 */
+.layout-wrapper.full-screen .layout-main-container {
+  height: 100vh;
+}
+
+.scrollable-content {
+  flex: 1;
+  overflow: auto;
+  padding: 0 20px;
+  box-sizing: border-box;
+  background-size: cover;
+  background-repeat: no-repeat;
+  background-position: center;
+  transition: background-image 0.3s ease-in-out;
 }
 </style>

+ 170 - 72
src/components/layout/menuItems.ts

@@ -16,18 +16,13 @@ import {
     Collection,
     MagicStick,
     HelpFilled,
-    Coin,
-    MapLocation,
-    DataAnalysis,
-    Document,
+    Coin
   } from '@element-plus/icons-vue';
-  import type { Component } from 'vue';
   
   export interface MenuItem {
     index: string;
-    path?:string;
     label: string;
-    icon?: Component;
+    icon?: any;
     tab: string;
     permission?: string;
     children?: MenuItem[];
@@ -38,7 +33,7 @@ import {
       index: '/shuJuKanBan',
       label: '数据看板',
       icon: Monitor,
-      tab: 'shuJuKanBan'
+     tab: 'shuJuKanBan'
     },
     {
       index: '/SoilPro',
@@ -65,88 +60,166 @@ import {
       tab: 'introduction'
     },
     {
-      index: '/heavyMetalFluxCalculation',
-      label: '输入通量计算',
+      index: 'irrigationWater',
+      label: '灌溉水',
       icon: Watermelon,
-      tab: 'heavyMetalFluxCalculation',
+      tab: 'HmOutFlux',
       children: [
         {
-          index: '/irrigationWater',
-          label: '灌溉水',
+          index: '/samplingMethodDevice1',
+          label: '采样方法和装置',
           icon: Sunny,
-          tab: 'heavyMetalFluxCalculation',
-          children: [
-          {
-            index: "/irrigationWater/info", 
-            label: "采样过程",
-            tab: 'heavyMetalFluxCalculation',
-            icon:Document,
-          },
-          {
-            index: "/irrigationWater/point", 
-            label: "采样点展示",
-            tab: 'heavyMetalFluxCalculation',
-            icon:MapLocation,
-          },
-          {
-            index:"/irrigationWater/river",
-            label:"断面展示",
-            tab:'heavyMetalFluxCalculation',
-            icon:MapLocation
-          },
-        ]
+          tab: 'HmOutFlux'
         },
         {
-          index: '/agriculturalProductInput',
-          label: '农产品投入',
+          index: '/irriSampleData',
+          label: '灌溉水采样数据',
           icon: Coin,
-          tab: 'heavyMetalFluxCalculation',
+          tab: 'HmOutFlux'
         },
         {
-          index: '/atmosphericDryWetDeposition',
-          label: '大气干湿沉降',
+          index: '/csSampleData',
+          label: '断面采样数据',
           icon: Cloudy,
-          tab: 'heavyMetalFluxCalculation',
-          children:[
-            {
-            index:"/atmosphericDryWetDeposition/atmpulltionmap",
-            label:"大气污染地图展示",
-            tab:'heavyMetalFluxCalculation',
-            icon:MapLocation
-            },
-            {
-            index:"/atmosphericDryWetDeposition/atmpulltioncompanymap",
-            label:"排污企业",
-            tab:'heavyMetalFluxCalculation',
-            icon:MapLocation
-            },
-          ]
+          tab: 'HmOutFlux'
+        },
+        {
+          index: '/irriInputFlux',
+          label: '灌溉水输入通量',
+          icon: Cloudy,
+          tab: 'HmOutFlux'
         }
       ]
     },
     {
-      index: 'outputFlux',
-      label: '输出通量计算',
+      index: 'inputFlux',
+      label: '农产品投入',
+      icon: Watermelon,
+      tab: 'HmOutFlux',
+      children: [
+        {
+          index: '/samplingDesc2',
+          label: '采样说明',
+          icon: Sunny,
+          tab: 'HmOutFlux'
+        },
+        {
+          index: '/prodInputFlux',
+          label: '农产品输入通量',
+          icon: Coin,
+          tab: 'HmOutFlux'
+        },
+      ]
+    },
+    {
+      index: 'atmosDeposition',
+      label: '大气干湿沉降',
+      icon: Watermelon,
+      tab: 'HmOutFlux',
+      children: [
+        {
+          index: '/samplingDesc3',
+          label: '采样说明',
+          icon: Sunny,
+          tab: 'HmOutFlux'
+        },
+        {
+          index: '/heavyMetalEnterprise',
+          label: '涉重企业',
+          icon: Coin,
+          tab: 'HmOutFlux'
+        },
+        {
+          index: '/airSampleData',
+          label: '大气采样数据',
+          icon: Sunny,
+          tab: 'HmOutFlux'
+        },
+        {
+          index: '/airInputFlux',
+          label: '大气输入通量',
+          icon: Coin,
+          tab: 'HmOutFlux'
+        },
+      ]
+    },
+    {
+      index: 'grainRemoval',
+      label: '籽粒移除',
       icon: WindPower,
-      tab: 'heavyMetalFluxCalculation',
+      tab: 'hmInFlux',
       children: [
         {
-          index: '/surfaceRunoff',
-          label: '地表径流',
+          index: '/samplingDesc1',
+          label: '采样说明',
           icon: Watermelon,
-          tab: 'heavyMetalFluxCalculation'
+          tab: 'hmInFlux'
         },
         {
-          index: '/cropRemoval',
-          label: '农作物移除',
+          index: '/grainRemovalInputFlux',
+          label: '籽粒移除输出通量',
           icon: List,
-          tab: 'heavyMetalFluxCalculation'
+          tab: 'hmInFlux'
+        }
+      ]
+    },
+    {
+      index: 'strawRemoval',
+      label: '秸秆移除',
+      icon: WindPower,
+      tab: 'hmInFlux',
+      children: [
+        {
+          index: '/samplingDesc2',
+          label: '采样说明',
+          icon: Watermelon,
+          tab: 'hmInFlux'
         },
         {
-          index: '/subsurfaceFlow',
-          label: '地下渗流',
-          icon: DataLine,
-          tab: 'heavyMetalFluxCalculation'
+          index: '/strawRemovalInputFlux',
+          label: '秸秆移除输出通量',
+          icon: List,
+          tab: 'hmInFlux'
+        }
+      ]
+    },
+    {
+      index: 'subsurfaceLeakage',
+      label: '地下渗漏',
+      icon: WindPower,
+      tab: 'hmInFlux',
+      children: [
+        {
+          index: '/samplingDesc3',
+          label: '采样说明',
+          icon: Watermelon,
+          tab: 'hmInFlux'
+        },
+        {
+          index: '/subsurfaceLeakageInputFlux',
+          label: '地下渗漏输入通量',
+          icon: List,
+          tab: 'hmInFlux'
+        }
+      ]
+    },
+    {
+      index: 'surfaceRunoff',
+      label: '地表径流',
+      icon: WindPower,
+      tab: 'hmInFlux',
+      children: [
+        {
+          index: '/samplingDesc4',
+          label: '采样说明',
+          icon: Watermelon,
+          tab: 'hmInFlux'
+        },
+        {
+          index: '/surfaceRunoffInputFlux',
+          label: '地表径流输入通量',
+          icon: List,
+          tab: 'hmInFlux'
         }
       ]
     },
@@ -156,6 +229,30 @@ import {
       icon: Location,
       tab: 'mapView'
     },
+    {
+      index: '/totalInputFlux',
+      label: '输入总通量',
+      icon: PieChart,
+      tab: 'cadmiumPrediction'
+    },
+    {
+      index: '/totalOutputFlux',
+      label: '输出总通量',
+      icon: PieChart,
+      tab: 'cadmiumPrediction'
+    },
+    {
+      index: '/netFlux',
+      label: '净通量',
+      icon: PieChart,
+      tab: 'cadmiumPrediction'
+    },
+    {
+      index: '/currentYearConcentration',
+      label: '当年浓度',
+      icon: PieChart,
+      tab: 'cadmiumPrediction'
+    },
     {
       index: '/TotalCadmiumPrediction',
       label: '土壤镉的总含量预测',
@@ -196,13 +293,13 @@ import {
           index: '/Calculation',
           label: '土壤反酸预测',
           icon: Sunny,
-          tab: ''
+          tab: 'heavyMetalFluxCalculation'
         },
         {
           index: '/SoilAcidReductionIterativeEvolution',
           label: '反酸模型迭代可视化',
           icon: Coin,
-          tab: ''
+          tab: 'heavyMetalFluxCalculation'
         }
       ]
     },
@@ -216,13 +313,13 @@ import {
           index: '/AcidNeutralizationModel',
           label: '土壤降酸预测',
           icon: Sunny,
-          tab: ''
+          tab: 'heavyMetalFluxCalculation'
         },
         {
           index: '/SoilAcidificationIterativeEvolution',
           label: '土壤降酸预测',
           icon: Coin,
-          tab: ''
+          tab: 'heavyMetalFluxCalculation'
         }
       ]
     },
@@ -262,5 +359,6 @@ import {
       icon: List,
       tab: 'dataStatistics'
     }
-  ];
+  ].filter(({ tab: menuTab }) => !["shuJuKanBan", "mapView", "introduction"].includes(menuTab));
+
   

+ 230 - 145
src/router/index.ts

@@ -14,7 +14,8 @@ const routes = [
     name: "home",
     component: AppLayout,
     meta: { requiresAuth: true, title: "模型" },
-    redirect: { name: "login" }, // 修改默认重定向为 login
+    redirect: { name: "login" }, // 修改默认重定向为 loginView
+
     children: [
       {
         path: "/:catchAll(.*)",
@@ -64,6 +65,168 @@ const routes = [
           import("@/views/User/introduction/IntroductionUpdate.vue"), // 修复路径
         meta: { title: "更新介绍" },
       },
+      // {
+      //   path: "HmOutFlux",
+      //   name: "HmOutFlux",
+      //   component: () => import("@/views/User/HmOutFlux"), 
+      //   meta: { title: "重金属输出通量" },
+      // },
+      // {
+      //   path: "irrigationWater",
+      //   name: "irrigationWater",
+      //   component: () => import("@/views/User/HmOutFlux/irrigationWater"), 
+      //   meta: { title: "灌溉水" },
+      // },
+      {
+        path: "samplingMethodDevice1",
+        name: "samplingMethodDevice1",
+        component: () => import("@/views/User/heavyMetalFluxCalculation/inputFluxCalculation/waterdata/rivermessage.vue"), 
+        meta: { title: "采样方法和装置" },
+      },
+      {
+        path: "irriSampleData",
+        name: "irriSampleData",
+        component: () => import("@/views/User/HmOutFlux/irrigationWater/irriWaterSampleData.vue"), 
+        meta: { title: "灌溉水采样数据" },
+      },
+      {
+        path: "csSampleData",
+        name: "csSampleData",
+        component: () => import("@/views/User/HmOutFlux/irrigationWater/crossSection.vue"), 
+        meta: { title: "断面采样数据" },
+      },
+      {
+        path: "irriInputFlux",
+        name: "irriInputFlux",
+        component: () => import("@/views/User/HmOutFlux/irrigationWater/irriWaterInputFlux.vue"), 
+        meta: { title: "灌溉水输入通量" },
+      },
+      // {
+      //   path: "agriInput",
+      //   name: "agriInput",
+      //   component: () => import("@/views/User/HmOutFlux/agriInput"), 
+      //   meta: { title: "农产品投入" },
+      // },
+      {
+        path: "samplingDesc2",
+        name: "samplingDesc2",
+        component: () => import("@/views/User/HmOutFlux/agriInput/samplingDesc2.vue"),  
+        meta: { title: "采样说明" },
+      },
+      {
+        path: "prodInputFlux",
+        name: "prodInputFlux",
+        component: () => import("@/views/User/HmOutFlux/agriInput/prodInputFlux.vue"),  
+        meta: { title: "农产品输入通量" },
+      },
+      // {
+      //   path: "atmosDeposition",
+      //   name: "atmosDeposition",
+      //   component: () => import("@/views/User/HmOutFlux/atmosDeposition"), 
+      //   meta: { title: "大气干湿沉降" },
+      // },
+      {
+        path: "samplingDesc3",
+        name: "samplingDesc3",
+        component: () => import("@/views/User/HmOutFlux/atmosDeposition/samplingDesc3.vue"), 
+        meta: { title: "采样说明" },
+      },
+      {
+        path: "heavyMetalEnterprise",
+        name: "heavyMetalEnterprise",
+        component: () => import("@/views/User/HmOutFlux/atmosDeposition/heavyMetalEnterprise.vue"), 
+        meta: { title: "涉重企业" },
+      },
+      {
+        path: "airSampleData",
+        name: "airSampleData",
+        component: () => import("@/views/User/HmOutFlux/atmosDeposition/airSampleData.vue"), 
+        meta: { title: "大气采样数据" },
+      },
+      {
+        path: "airInputFlux",
+        name: "airInputFlux",
+        component: () => import("@/views/User/HmOutFlux/atmosDeposition/airInputFlux.vue"), 
+        meta: { title: "大气输入通量" },
+      },
+      // {
+      //   path: "hmInFlux",
+      //   name: "hmInFlux",
+      //   component: () => import("@/views/User/hmInFlux"), 
+      //   meta: { title: "重金属输入通量" },
+      // },
+      // {
+      //   path: "grainRemoval",
+      //   name: "grainRemoval",
+      //   component: () => import("@/views/User/hmInFlux/grainRemoval"), 
+      //   meta: { title: "籽粒移除" },
+      // },
+      {
+        path: "samplingDesc1",
+        name: "samplingDesc1",
+        component: () => import("@/views/User/hmInFlux/grainRemoval/samplingDesc1.vue"), 
+        meta: { title: "采样说明" },
+      },
+      {
+        path: "grainRemovalInputFlux",
+        name: "grainRemovalInputFlux",
+        component: () => import("@/views/User/hmInFlux/grainRemoval/grainRemovalInputFlux.vue"), 
+        meta: { title: "籽粒移除输入通量" },
+      },
+      // {
+      //   path: "strawRemoval",
+      //   name: "strawRemoval",
+      //   component: () => import("@/views/User/hmInFlux/strawRemoval"), 
+      //   meta: { title: "秸秆移除" },
+      // },
+       {
+        path: "samplingDesc2",
+        name: "samplingDesc2",
+        component: () => import("@/views/User/hmInFlux/strawRemoval/samplingDesc2.vue"), 
+        meta: { title: "采样说明" },
+       },
+       {
+        path: "strawRemovalInputFlux",
+        name: "strawRemovalInputFlux",
+        component: () => import("@/views/User/hmInFlux/strawRemoval/strawRemovalInputFlux.vue"), 
+        meta: { title: "秸秆移除输入通量" },
+       },
+      // {
+      //   path: "subsurfaceLeakage",
+      //   name: "subsurfaceLeakage",
+      //   component: () => import("@/views/User/hmInFlux/subsurfaceLeakage"), 
+      //   meta: { title: "地下渗漏" },
+      // },
+      {
+         path: "samplingDesc3",
+         name: "samplingDesc3",
+         component: () => import("@/views/User/hmInFlux/subsurfaceLeakage/samplingDesc3.vue"), 
+         meta: { title: "采样说明" },
+       },
+       {
+         path: "subsurfaceLeakageInputFlux",
+         name: "subsurfaceLeakageInputFlux",
+         component: () => import("@/views/User/hmInFlux/subsurfaceLeakage/subsurfaceLeakageInputFlux.vue"), 
+         meta: { title: "地下渗漏输入通量" },
+       },
+      // {
+      //   path: "surfaceRunoff",
+      //   name: "surfaceRunoff",
+      //   component: () => import("@/views/User/hmInFlux/surfaceRunoff"), 
+      //   meta: { title: "地表径流" },
+      // },
+       {
+         path: "samplingDesc4",
+         name: "samplingDesc4",
+         component: () => import("@/views/User/hmInFlux/surfaceRunoff/samplingDesc4.vue"), 
+         meta: { title: "采样说明" },
+       },
+       {
+         path: "surfaceRunoffInputFlux",
+         name: "surfaceRunoffInputFlux",
+         component: () => import("@/views/User/hmInFlux/surfaceRunoff/surfaceRunoffInputFlux.vue"), 
+         meta: { title: "地表径流输入通量" },
+       },
       {
         path: "Calculation",
         name: "Calculation",
@@ -100,9 +263,37 @@ const routes = [
       {
         path: "mapView",
         name: "mapView",
-        component: () => import("@/views/User/heavyMetalFluxCalculation/inputFluxCalculation/waterdata/tencentMapView.vue"), // 修复路径
+        component: () => import("@/views/User/mapView/leafletMapView.vue"), // 修复路径
         meta: { title: "地图展示" },
       },
+      {
+        path: "totalInputFlux",
+        name: "totalInputFlux",
+        component: () =>
+          import("@/views/User/cadmiumPrediction/totalInputFlux.vue"), 
+        meta: { title: "输入总通量" },
+      },
+      {
+        path: "totalOutputFlux",
+        name: "totalOutputFlux",
+        component: () =>
+          import("@/views/User/cadmiumPrediction/totalOutputFlux.vue"), // 修复路径
+        meta: { title: "输出总通量" },
+      },
+      {
+        path: "netFlux",
+        name: "netFlux",
+        component: () =>
+          import("@/views/User/cadmiumPrediction/netFlux.vue"), // 修复路径
+        meta: { title: "净通量" },
+      },
+      {
+        path: "currentYearConcentration",
+        name: "currentYearConcentration",
+        component: () =>
+          import("@/views/User/cadmiumPrediction/currentYearConcentration.vue"), // 修复路径
+        meta: { title: "当年浓度" },
+      },
       {
         path: "TotalCadmiumPrediction",
         name: "TotalCadmiumPrediction",
@@ -211,92 +402,92 @@ const routes = [
         meta: { title: "种植风险信息统计" },
       },
       {
-        path: "soilAcidReductionData",
-        name: "soilAcidReductionData",
+        path: "Visualizatio",
+        name: "Visualizatio",
         component: () =>
-          import("@/views/Admin/dataManagement/Soil Acidification and Acid Reduction Data Management/soilAcidReductionData.vue"), // 修复路径
-        meta: { title: "降酸数据" },
+          import("@/views/Admin/dataManagement/Visualizatio.vue"), // 修复路径
+        meta: { title: "降酸数据管理" },
       },
       {
-        path: "soilAcidificationData",
-        name: "soilAcidificationData",
+        path: "Visualization",
+        name: "Visualization",
         component: () =>
-          import("@/views/Admin/dataManagement/Soil Acidification and Acid Reduction Data Management/soilAcidificationData.vue"), // 修复路径
-        meta: { title: "反酸数据" },
+          import("@/views/Admin/dataManagement/Visualization.vue"), // 修复路径
+        meta: { title: "反酸数据管理" },
       },
       {
         path: "AdminRegionData",
         name: "AdminRegionData",
         component: () =>
-          import("@/views/Admin/dataManagement/Administrative Area Data Management/AdminRegionData.vue"), // 修复路径
+          import("@/views/Admin/dataManagement/AdminRegionData.vue"), // 修复路径
         meta: { title: "行政区域数据" },
       },
       {
         path: "SoilAssessmentUnitData",
         name: "SoilAssessmentUnitData",
         component: () =>
-          import("@/views/Admin/dataManagement/SoilAssessmentUnitData/SoilAssessmentUnitData.vue"),
+          import("@/views/Admin/dataManagement/SoilAssessmentUnitData.vue"),
         meta: { title: "土壤评估单元格数据" },
       },
       {
         path: "SoilHeavyMetalData",
         name: "SoilHeavyMetalData",
         component: () =>
-          import("@/views/Admin/dataManagement/Soil Heavy Metal Sampling Data Management/SoilHeavyMetalData.vue"),
+          import("@/views/Admin/dataManagement/SoilHeavyMetalData.vue"),
         meta: { title: "土壤重金属采集数据" },
       },
       {
         path: "CropHeavyMetalData",
         name: "CropHeavyMetalData",
         component: () =>
-          import("@/views/Admin/dataManagement/Crop Heavy Metal Sampling Data Management/CropHeavyMetalData.vue"),
+          import("@/views/Admin/dataManagement/CropHeavyMetalData.vue"),
         meta: { title: "农作物重金属采集样数据" },
       },
       {
         path: "LandUseTypeData",
         name: "LandUseTypeData",
         component: () =>
-          import("@/views/Admin/dataManagement/Land Use Type Data Management/LandUseTypeData.vue"),
+          import("@/views/Admin/dataManagement/LandUseTypeData.vue"),
         meta: { title: "用地类型数据" },
       },
       {
         path: "SoilAcidificationData",
         name: "SoilAcidificationData",
         component: () =>
-          import("@/views/Admin/dataManagement/Soil Acidification Sampling Data Management/SoilAcidificationData.vue"),
+          import("@/views/Admin/dataManagement/SoilAcidificationData.vue"),
         meta: { title: "土壤酸化采样数据" },
       },
       {
         path: "ClimateInfoData",
         name: "ClimateInfoData",
         component: () =>
-          import("@/views/Admin/dataManagement/Climate Information Data Management/ClimateInfoData.vue"),
+          import("@/views/Admin/dataManagement/ClimateInfoData.vue"),
         meta: { title: "气候信息数据" },
       },
       {
         path: "GeographicEnvInfoData",
         name: "GeographicEnvInfoData",
         component: () =>
-          import("@/views/Admin/dataManagement/Geographic Environmental Information Management/GeographicEnvInfoData.vue"),
+          import("@/views/Admin/dataManagement/GeographicEnvInfoData.vue"),
         meta: { title: "地理环境信息" },
       },
       {
         path: "ModelSelection",
         name: "ModelSelection",
         component: () =>
-          import("@/views/Admin/modelManagement/AcidReductionModel/ModelSelection.vue"),
+          import("@/views/Admin/parameterConfig/ModelSelection.vue"),
         meta: { title: "模型选择" },
       },
       {
         path: "thres",
         name: "thres",
-        component: () => import("@/views/Admin/modelManagement/AcidReductionModel/thres.vue"),
+        component: () => import("@/views/Admin/parameterConfig/thres.vue"),
         meta: { title: "阈值选择" },
       },
       {
         path: "ModelTrain",
         name: "ModelTrain",
-        component: () => import("@/views/Admin/modelManagement/AcidReductionModel/ModelTrain.vue"),
+        component: () => import("@/views/Admin/parameterConfig/ModelTrain.vue"),
         meta: { title: "模型训练" },
       },
       {
@@ -317,7 +508,7 @@ const routes = [
         path: "CadmiumPredictionModel",
         name: "CadmiumPredictionModel",
         component: () =>
-          import("@/views/Admin/modelManagement/Soil Cadmium Content Prediction Model Management/CadmiumPredictionModel.vue"),
+          import("@/views/Admin/modelManagement/CadmiumPredictionModel.vue"),
         meta: { title: "土壤镉含量预测模型" },
       },
       {
@@ -331,111 +522,30 @@ const routes = [
         path: "RiceRiskModel",
         name: "RiceRiskModel",
         component: () =>
-          import("@/views/Admin/modelManagement/Rice Cadmium Pollution Risk Model Management/RiceRiskModel.vue"),
+          import("@/views/Admin/modelManagement/RiceRiskModel.vue"),
         meta: { title: "水稻镉污染风险模型" },
       },
+      {
+        path: "AcidReductionModel",
+        name: "AcidReductionModel",
+        component: () =>
+          import("@/views/Admin/modelManagement/AcidReductionModel.vue"),
+        meta: { title: "反酸及降酸模型" },
+      },
       {
         path: "WheatRiskModel",
         name: "WheatRiskModel",
         component: () =>
-          import("@/views/Admin/Wheat Cadmium Pollution Risk Model Management/WheatRiskModel.vue"),
+          import("@/views/Admin/modelManagement/WheatRiskModel.vue"),
         meta: { title: "小麦镉污染风险模型" },
       },
       {
         path: "VegetableRiskModel",
         name: "VegetableRiskModel",
         component: () =>
-          import("@/views/Admin/modelManagement/Vegetable Cadmium Pollution Risk Model Management/VegetableRiskModel.vue"),
+          import("@/views/Admin/modelManagement/VegetableRiskModel.vue"),
         meta: { title: "蔬菜镉污染风险模型" },
       },
-      {
-        path: "/irrigationWater",
-        name: "irrigationWater",
-        component: () =>
-          import(
-            "@/views/User/heavyMetalFluxCalculation/inputFluxCalculation/irrigationWater.vue"
-          ),
-        meta: { title: "灌溉水" },
-        children: [ 
-          {
-             path: "info",
-             name: "irrigationWaterInfo",
-             component: () => import("@/views/User/heavyMetalFluxCalculation/inputFluxCalculation/waterdata/rivermessage.vue"),
-             meta: { title: "采样过程" }
-          },
-          {
-             path: "point", 
-             name: "irrigationWaterPoint",
-             component: () => import("@/views/User/heavyMetalFluxCalculation/inputFluxCalculation/waterdata/point.vue"),
-             meta: { title: "采样点展示" }
-          },
-          {
-             path:"river",
-             name:"irrigationWaterRiver",
-             component: () =>import("@/views/User/heavyMetalFluxCalculation/inputFluxCalculation/waterdata/rivertencentMapView.vue"),
-             meta:{title:"断面展示"}
-          },
-          ]
-          },
-      {
-        path: "agriculturalProductInput",
-        name: "agriculturalProductInput",
-        component: () =>
-          import(
-            "@/views/User/heavyMetalFluxCalculation/inputFluxCalculation/agriculturalProductInput.vue"
-          ),
-        meta: { title: "农产品投入" },
-      },
-      {
-        path: "atmosphericDryWetDeposition",
-        name: "atmosphericDryWetDeposition",
-        component: () =>
-          import(
-            "@/views/User/heavyMetalFluxCalculation/inputFluxCalculation/atmosphericDryWetDeposition.vue"
-          ),
-        meta: { title: "大气干湿沉降" },
-        children: [ 
-          {
-             path: "atmpulltionmap", 
-             name: "atmosphericDryWetDepositionAtmpulltionmap",
-             component: () => import("@/views/User/heavyMetalFluxCalculation/inputFluxCalculation/atmospheredata/atmtencentMap.vue"),
-             meta: { title: "大气污染地图展示" }
-          },
-          {
-             path:"atmpulltioncompanymap",
-             name:"atmosphericDryWetDepositionAtmosphericDryWetDeposition",
-             component: () =>import("@/views/User/heavyMetalFluxCalculation/inputFluxCalculation/atmospheredata/atmcompany.vue"),
-             meta:{title:"排污企业"}
-          },
-          ]
-      },
-      {
-        path: "surfaceRunoff",
-        name: "surfaceRunoff",
-        component: () =>
-          import(
-            "@/views/User/heavyMetalFluxCalculation/outputFluxCalculation/surfaceRunoff.vue"
-          ),
-        meta: { title: "地表径流" },
-      },
-      {
-        path: "cropRemoval",
-        name: "cropRemoval",
-        component: () =>
-          import(
-            "@/views/User/heavyMetalFluxCalculation/outputFluxCalculation/cropRemoval.vue"
-          ),
-        meta: { title: "农作物移除" },
-      },
-      {
-        path: "subsurfaceFlow",
-        name: "subsurfaceFlow",
-        component: () =>
-          import(
-            "@/views/User/heavyMetalFluxCalculation/outputFluxCalculation/subsurfaceFlow.vue"
-          ),
-        meta: { title: "地下渗流" },
-      },
       {
         path: "about",
         name: "about",
@@ -455,40 +565,15 @@ const router = createRouter({
   routes,
 });
 
-router.beforeEach(async (to, from, next) => {
+router.beforeEach((to, from, next) => {
   const tokenStore = useTokenStore();
-  
-  console.log('[路由导航] 从:', from.path, '到:', to.path, '是否需要认证:', to.matched.some(r => r.meta.requiresAuth));
-
-  try {
-    // 情况1: 用户已登录但访问登录页,重定向到主页
-    if (to.name === "login" && tokenStore.token.userid) {
-      console.log('[路由导航] 用户已登录,重定向到主页');
-      next({ name: "selectCityAndCounty" });
-      return;
-    }
-    // 情况2: 访问需要认证的路由
-    if (to.matched.some(r => r.meta.requiresAuth)) {
-      // 检查用户是否已登录
-      if (!tokenStore.token.userid) {
-        console.log('[路由导航] 未登录,重定向到登录页');
-        next({ name: "login" });
-        return;
-      }
-      console.log('[路由导航] 已登录,允许访问需要认证的路由');
-      next();
-      return;
-    }
-
-    // 情况3: 不需要认证的路由直接放行
-    console.log('[路由导航] 不需要认证,直接放行');
+  if (to.name === "login" && tokenStore.token.userid) {
+    next({ name: "selectCityAndCounty" });
+  } else if (to.matched.some((r) => r.meta.requiresAuth)) {
+    tokenStore.token.userid ? next() : next({ name: "login" });
+  } else {
     next();
-  } catch (error) {
-    console.error('[路由导航错误]', error);
-    next({ name: "login" }); // 发生错误时重定向到登录页
   }
 });
-router.afterEach((to) => {
-  console.log('[路由完成] 当前路径:', to.path)
-})
+
 export default router;

+ 0 - 0
src/views/User/heavyMetalFluxCalculation/outputFluxCalculation/cropRemoval.vue → src/views/Admin/dataManagement/AdminRegionData.vue


+ 23 - 0
src/views/Admin/dataManagement/ClimateInfoData.vue

@@ -0,0 +1,23 @@
+<template>
+  <div class="">
+    
+  </div>
+</template>
+
+<script>
+export default {
+  name: '',
+  data() {
+    return {
+      
+    };
+  },
+  methods: {
+    
+  }
+};
+</script>
+
+<style scoped>
+
+</style>

+ 0 - 0
src/views/User/heavyMetalFluxCalculation/outputFluxCalculation/subsurfaceFlow.vue → src/views/Admin/dataManagement/CropHeavyMetalData.vue


+ 8 - 0
src/views/Admin/dataManagement/GeographicEnvInfoData.vue

@@ -0,0 +1,8 @@
+<script setup lang='ts' name=''>
+</script>
+
+<template>
+  <div class=""></div>
+</template>
+
+<style scoped></style>

+ 1 - 1
src/views/User/heavyMetalFluxCalculation/inputFluxCalculation/agriculturalProductInput.vue → src/views/Admin/dataManagement/LandUseTypeData.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="">
-    1
+    
   </div>
 </template>
 

+ 23 - 0
src/views/Admin/dataManagement/SoilAcidificationData.vue

@@ -0,0 +1,23 @@
+<template>
+  <div class="">
+    
+  </div>
+</template>
+
+<script>
+export default {
+  name: '',
+  data() {
+    return {
+      
+    };
+  },
+  methods: {
+    
+  }
+};
+</script>
+
+<style scoped>
+  
+</style>

+ 0 - 0
src/views/User/heavyMetalFluxCalculation/outputFluxCalculation/surfaceRunoff.vue → src/views/Admin/dataManagement/SoilAssessmentUnitData.vue


+ 23 - 0
src/views/Admin/dataManagement/SoilHeavyMetalData.vue

@@ -0,0 +1,23 @@
+<template>
+  <div class="">
+    
+  </div>
+</template>
+
+<script>
+export default {
+  name: '',
+  data() {
+    return {
+      
+    };
+  },
+  methods: {
+    
+  }
+};
+</script>
+
+<style scoped>
+  
+</style>

+ 552 - 0
src/views/Admin/dataManagement/Visualizatio.vue

@@ -0,0 +1,552 @@
+<template>
+  <div class="app-container">
+    <!-- 操作按钮区 -->
+    <div class="button-group">
+      <el-button
+        :icon="Plus"
+        type="primary"
+        @click="openDialog('add')"
+        class="custom-button add-button"
+        >新增记录</el-button
+      >
+      <el-button
+        :icon="Download"
+        type="primary"
+        @click="downloadTemplateAction"
+        class="custom-button download-button"
+        >下载模板</el-button
+      >
+      <el-button
+        :icon="Download"
+        type="primary"
+        @click="exportDataAction"
+        class="custom-button export-button"
+        >导出数据</el-button
+      >
+      <el-upload :before-upload="importDataAction" accept=".xlsx, .csv">
+        <el-button
+          :icon="Upload"
+          type="primary"
+          class="custom-button import-button"
+          >导入数据</el-button
+        >
+      </el-upload>
+    </div>
+
+    <!-- 数据表格 -->
+    <el-table
+      :data="pagedTableDataWithIndex"
+      fit
+      style="width: 100%"
+      @row-click="handleRowClick"
+      highlight-current-row
+      class="custom-table"
+      v-loading="loading"
+      table-layout="auto"
+    >
+      <el-table-column
+        key="displayIndex"
+        prop="displayIndex"
+        label="序号"
+        width="80"
+        align="center"
+      ></el-table-column>
+      <el-table-column
+        v-for="col in columns.filter((col) => col.key !== 'id')"
+        :key="col.key"
+        :prop="col.dataKey"
+        :label="col.title"
+        :min-width="col.title.length * 20 + 40"
+        :formatter="formatNumber"
+        align="center"
+        header-align="center"
+      ></el-table-column>
+      <el-table-column label="操作" width="120" align="center">
+        <template #default="scope">
+          <span class="action-buttons">
+            <el-tooltip
+              class="item"
+              effect="dark"
+              content="编辑"
+              placement="top"
+            >
+              <el-button
+                circle
+                :icon="EditPen"
+                @click.stop="openDialog('edit', scope.row)"
+                class="action-button edit-button"
+              ></el-button>
+            </el-tooltip>
+            <el-tooltip
+              class="item"
+              effect="dark"
+              content="删除"
+              placement="top"
+            >
+              <el-button
+                circle
+                :icon="DeleteFilled"
+                @click.stop="deleteItem(scope.row)"
+                class="action-button delete-button"
+              ></el-button>
+            </el-tooltip>
+          </span>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 分页控制 -->
+    <PaginationComponent
+      :total="tableData.length"
+      :currentPage="currentPage4"
+      :pageSize="pageSize4"
+      @update:currentPage="currentPage4 = $event"
+      @update:pageSize="pageSize4 = $event"
+      @size-change="handleSizeChange"
+      @current-change="handleCurrentChange"
+      class="pagination-container"
+    />
+
+    <!-- 新增/编辑对话框 -->
+    <el-dialog :title="dialogTitle" v-model="dialogVisible" width="50%">
+      <el-form :model="formData" label-width="150px">
+        <el-form-item
+          v-for="col in editableColumns"
+          :key="col.key"
+          :label="col.title"
+        >
+          <el-input
+            v-model="formData[col.dataKey]"
+            :type="col.inputType || 'text'"
+            class="custom-input"
+          ></el-input>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="dialogVisible = false" class="custom-cancel-button"
+          >取消</el-button
+        >
+        <el-button
+          type="primary"
+          @click="submitForm"
+          class="custom-submit-button"
+          >{{ dialogSubmitButtonText }}</el-button
+        >
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive, computed, onMounted } from "vue";
+import {
+  DeleteFilled,
+  Download,
+  Upload,
+  Plus,
+  EditPen,
+} from "@element-plus/icons-vue";
+import { ElMessage } from "element-plus";
+import {
+  table,
+  updateItem,
+  addItem,
+  deleteItemApi,
+  downloadTemplate,
+  exportData,
+  importData,
+} from "@/API/menus";
+import PaginationComponent from "@/components/PaginationComponent.vue";
+
+interface Column {
+  key: string;
+  dataKey: string;
+  title: string;
+  width: number;
+  inputType?: string;
+}
+
+const columns: Column[] = [
+  { key: "id", dataKey: "id", title: "ID", width: 100 },
+  { key: "OM", dataKey: "OM", title: "有机质含量", width: 150 },
+  { key: "CL", dataKey: "CL", title: "土壤粘粒", width: 150 },
+  { key: "CEC", dataKey: "CEC", title: "阳离子交换量", width: 150 },
+  { key: "H_plus", dataKey: "H_plus", title: "交换性氢", width: 150 },
+  { key: "N", dataKey: "N", title: "水解氮", width: 150 },
+  { key: "Al3_plus", dataKey: "Al3_plus", title: "交换性铝", width: 150 },
+  { key: "Delta_pH", dataKey: "Delta_pH", title: "ΔpH", width: 170 },
+];
+
+const editableColumns = columns.filter((col) => col.key !== "id");
+
+const tableData = ref<any[]>([]);
+const selectedRow = ref<any | null>(null);
+const dialogVisible = ref(false);
+const formData = reactive<any>({});
+const dialogMode = ref<"add" | "edit">("add"); // 新增还是编辑模式
+
+const currentPage4 = ref(1);
+const pageSize4 = ref(10);
+
+const pagedTableData = computed(() => {
+  const start = (currentPage4.value - 1) * pageSize4.value;
+  const end = start + pageSize4.value;
+  return tableData.value.slice(start, end);
+});
+
+const pagedTableDataWithIndex = computed(() => {
+  return pagedTableData.value.map((item, index) => ({
+    ...item,
+    displayIndex: (currentPage4.value - 1) * pageSize4.value + index + 1,
+  }));
+});
+
+const loading = ref(false);
+const currentTableName = "current_reflux";
+
+const fetchTable = async () => {
+  console.log("正在获取表格数据...");
+  try {
+    loading.value = true;
+    const response = await table({ table: currentTableName });
+    console.log("获取到的数据:", response);
+    tableData.value = response.data.rows;
+  } catch (error) {
+    console.error("获取数据时出错:", error);
+    ElMessage.error("获取数据失败,请检查网络连接或服务器状态");
+  } finally {
+    loading.value = false;
+  }
+};
+
+onMounted(() => {
+  fetchTable();
+});
+
+const handleRowClick = (row: any) => {
+  selectedRow.value = row;
+  Object.assign(formData, row);
+};
+
+const openDialog = (mode: "add" | "edit", row?: any) => {
+  console.log(`${mode === "add" ? "打开新增记录" : "打开编辑记录"}对话框`);
+  if (mode === "add") {
+    selectedRow.value = null;
+    editableColumns.forEach((col) => {
+      formData[col.dataKey] = "";
+    });
+  } else if (row) {
+    console.log("编辑记录:", row);
+    selectedRow.value = row;
+    Object.assign(formData, row);
+  }
+  dialogMode.value = mode;
+  dialogVisible.value = true;
+};
+
+function prepareFormData(
+  formData: { [x: string]: any },
+  excludeKeys = ["displayIndex"]
+): { [x: string]: any } {
+  const result: { [x: string]: any } = {};
+  for (const key in formData) {
+    if (!excludeKeys.includes(key)) {
+      let value = formData[key];
+      if (typeof value === "string" && value.startsWith("displayIndex-")) {
+        value = value.replace("displayIndex-", "");
+      }
+      result[key] = value;
+    }
+  }
+  return result;
+}
+
+const submitForm = async () => {
+  console.log("开始提交表单...");
+  try {
+    const isValid = validateFormData(formData);
+    if (!isValid) {
+      console.error("表单验证失败,请检查输入的数据");
+      alert("请检查输入的数据");
+      return;
+    }
+    const dataToSubmit = prepareFormData(formData);
+    if (!dataToSubmit.id && dialogMode.value !== "add") {
+      console.error("无法找到记录ID,请联系管理员");
+      alert("无法找到记录ID,请联系管理员");
+      return;
+    }
+    let response;
+    if (dialogMode.value === "add") {
+      console.log("正在添加新记录...");
+      response = await addItem({ table: currentTableName, item: dataToSubmit });
+    } else {
+      console.log("正在更新现有记录...");
+      response = await updateItem({
+        table: currentTableName,
+        item: dataToSubmit,
+      });
+    }
+    console.log(
+      dialogMode.value === "add" ? "添加响应:" : "更新响应:",
+      response
+    );
+    dialogVisible.value = false;
+    fetchTable();
+    alert(dialogMode.value === "add" ? "添加成功" : "修改成功");
+  } catch (error) {
+    console.error("提交表单时发生错误:", error);
+    let errorMessage = "未知错误";
+    if (error && typeof error === "object" && "response" in error) {
+      const response = (error as { response?: { data?: { message?: string } } })
+        .response;
+      if (
+        response &&
+        response.data &&
+        typeof response.data.message === "string"
+      ) {
+        errorMessage = response.data.message;
+      }
+    }
+    console.error(`提交失败原因: ${errorMessage}`);
+    alert(`提交失败: ${errorMessage}`);
+  }
+};
+
+function validateFormData(data: { [x: string]: undefined }) {
+  for (let key in data) {
+    if (data[key] === "" || data[key] === undefined) {
+      return false;
+    }
+  }
+  return true;
+}
+
+const deleteItem = async (row: any) => {
+  console.log("准备删除记录:", row);
+  if (!row) {
+    ElMessage.warning("请先选择一行记录");
+    return;
+  }
+  try {
+    const condition = { id: row.id };
+    await deleteItemApi({ table: "current_reflux", condition });
+    const index = tableData.value.findIndex((item) => item.id === row.id);
+    if (index > -1) {
+      tableData.value.splice(index, 1);
+    }
+    fetchTable();
+    console.log("记录删除成功");
+  } catch (error) {
+    console.error("删除记录时发生错误:", error);
+  }
+};
+
+const downloadTemplateAction = async () => {
+  try {
+    await downloadTemplate("current_reflux");
+  } catch (error) {
+    console.error("下载模板时发生错误:", error);
+  }
+};
+
+const exportDataAction = async () => {
+  try {
+    await exportData("current_reflux");
+  } catch (error) {
+    console.error("导出数据时发生错误:", error);
+  }
+};
+
+const importDataAction = async (file: File) => {
+  try {
+    const response = await importData("reduce", file); // 传递 dataset_type 的值(如 'reduce')
+    if (response && response.data) {
+      const { total_data, new_data, duplicate_data, message } = response.data;
+      ElMessage({
+        message: `导入结果: ${message} 新增总数:${total_data}, 成功新增:${new_data}, 数据重复:${duplicate_data}`,
+        type: "success",
+      });
+      fetchTable(); // 假设存在 fetchTable 方法刷新表格
+    }
+  } catch (error) {
+    let errorMessage = "数据导入失败";
+    if (error && typeof error === "object" && "response" in error) {
+      const response = (error as { response?: { data?: { message?: string } } })
+        .response;
+      if (response && response.data && typeof response.data.message === "string") {
+        errorMessage += `: ${response.data.message}`;
+      }
+    }
+    ElMessage.error(errorMessage);
+  }
+};
+
+const handleFileChange = (event: Event) => {
+  const target = event.target as HTMLInputElement;
+  if (target.files && target.files.length > 0) {
+    importDataAction(target.files[0]);
+  }
+};
+
+const handleSizeChange = (val: number) => {};
+const handleCurrentChange = (val: number) => {};
+
+const formatNumber = (row: any, column: any, cellValue: any) => {
+  if (typeof cellValue === "number") {
+    return cellValue.toFixed(3);
+  }
+  return cellValue;
+};
+
+const dialogTitle = computed(() => {
+  return dialogMode.value === "add" ? "新增记录" : "编辑记录";
+});
+
+const dialogSubmitButtonText = computed(() => {
+  return dialogMode.value === "add" ? "添加" : "保存";
+});
+</script>
+
+<style scoped>
+.app-container {
+  padding: 20px 40px;
+  min-height: 100vh;
+  background-color: #f0f2f5;
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+.button-group {
+  display: flex;
+  gap: 12px;
+  justify-content: flex-end;
+  width: 100%;
+  margin-bottom: 20px;
+}
+.custom-button {
+  color: #fff;
+  border: none;
+  border-radius: 8px;
+  font-size: 14px;
+  padding: 10px 18px;
+  transition: transform 0.3s ease, background-color 0.3s ease;
+  min-width: 130px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.download-button,
+.export-button,
+.add-button,
+.import-button {
+  background-color: #67c23a;
+}
+.download-button:hover,
+.export-button:hover,
+.import-button:hover,
+.add-button:hover {
+  background-color: #85ce61;
+  transform: scale(1.05);
+}
+.custom-table {
+  width: 100%;
+  border-radius: 8px;
+  overflow: hidden;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+  background-color: #fff;
+  margin-top: 10px;
+}
+:deep( .el-table th) {
+  background: linear-gradient(180deg, #61e054, #4db944);
+  color: #fff;
+  font-weight: bold;
+  text-align: center;
+  padding: 14px 0;
+  font-size: 14px;
+}
+:deep( .el-table__row:nth-child(odd)) {
+  background-color: #E4FBE5;
+}
+:deep( .el-table__row:nth-child(even)) {
+  background-color: #ffffff;
+}
+:deep( .el-table td) {
+  padding: 12px 10px;
+  text-align: center;
+  border-bottom: 1px solid #ebeef5;
+}
+.action-button {
+  font-size: 16px;
+  margin-right: 5px;
+  border-radius: 50%;
+  transition: transform 0.3s ease, background-color 0.3s ease;
+}
+.edit-button {
+  background-color: #409eff;
+  color: #fff;
+}
+.edit-button:hover {
+  background-color: #66b1ff;
+  transform: scale(1.1);
+}
+.delete-button {
+  background-color: #f56c6c;
+  color: #fff;
+}
+.delete-button:hover {
+  background-color: #f78989;
+  transform: scale(1.1);
+}
+.el-form-item__label {
+  width: 150px;
+  font-weight: bold;
+}
+.el-form-item__content {
+  margin-left: 150px;
+}
+.custom-input .el-input__inner {
+  border-radius: 6px;
+  border: 1px solid #dcdcdc;
+  transition: border-color 0.3s ease;
+  padding: 10px;
+  font-size: 14px;
+}
+.custom-input .el-input__inner:focus {
+  border-color: #409eff;
+}
+.custom-cancel-button,
+.custom-submit-button {
+  border: none;
+  border-radius: 6px;
+  font-size: 14px;
+  padding: 10px 20px;
+  transition: transform 0.3s ease, background-color 0.3s ease;
+  min-width: 100px;
+}
+.custom-cancel-button {
+  background-color: #f4f4f5;
+  color: #606266;
+}
+.custom-cancel-button:hover {
+  background-color: #e4e7ed;
+  transform: scale(1.05);
+}
+.custom-submit-button {
+  background-color: #409eff;
+  color: #fff;
+}
+.custom-submit-button:hover {
+  background-color: #66b1ff;
+  transform: scale(1.05);
+}
+.pagination-container {
+  margin-top: 30px;
+  text-align: center;
+}
+.action-buttons {
+  display: flex;
+  justify-content: center;
+}
+</style>

+ 536 - 0
src/views/Admin/dataManagement/Visualization.vue

@@ -0,0 +1,536 @@
+<template>
+  <div class="app-container">
+    <!-- 操作按钮区 -->
+    <div class="button-group">
+      <el-button
+        :icon="Plus"
+        type="primary"
+        @click="openDialog('add')"
+        class="custom-button add-button"
+        >新增记录</el-button
+      >
+      <el-button
+        :icon="Download"
+        type="primary"
+        @click="downloadTemplateAction"
+        class="custom-button download-button"
+        >下载模板</el-button
+      >
+      <el-button
+        :icon="Download"
+        type="primary"
+        @click="exportDataAction"
+        class="custom-button export-button"
+        >导出数据</el-button
+      >
+      <el-upload :before-upload="importDataAction" accept=".xlsx, .csv">
+        <el-button
+          :icon="Upload"
+          type="primary"
+          class="custom-button import-button"
+          >导入数据</el-button
+        >
+      </el-upload>
+    </div>
+
+    <!-- 数据表格 -->
+    <el-table
+      :data="pagedTableDataWithIndex"
+      fit
+      style="width: 100%"
+      @row-click="handleRowClick"
+      highlight-current-row
+      class="custom-table"
+      v-loading="loading"
+      table-layout="auto"
+    >
+      <el-table-column
+        key="displayIndex"
+        prop="displayIndex"
+        label="序号"
+        width="80"
+        align="center"
+      ></el-table-column>
+      <el-table-column
+        v-for="col in columns.filter((col) => col.key !== 'id')"
+        :key="col.key"
+        :prop="col.dataKey"
+        :label="col.title"
+        :min-width="col.title.length * 20 + 40"
+        :formatter="formatNumber"
+        align="center"
+        header-align="center"
+      ></el-table-column>
+      <el-table-column label="操作" width="120" align="center">
+        <template #default="scope">
+          <span class="action-buttons">
+            <el-tooltip
+              class="item"
+              effect="dark"
+              content="编辑"
+              placement="top"
+            >
+              <el-button
+                circle
+                :icon="EditPen"
+                @click.stop="openDialog('edit', scope.row)"
+                class="action-button edit-button"
+              ></el-button>
+            </el-tooltip>
+            <el-tooltip
+              class="item"
+              effect="dark"
+              content="删除"
+              placement="top"
+            >
+              <el-button
+                circle
+                :icon="DeleteFilled"
+                @click.stop="deleteItem(scope.row)"
+                class="action-button delete-button"
+              ></el-button>
+            </el-tooltip>
+          </span>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 分页控制 -->
+    <PaginationComponent
+      :total="tableData.length"
+      :currentPage="currentPage4"
+      :pageSize="pageSize4"
+      @update:currentPage="currentPage4 = $event"
+      @update:pageSize="pageSize4 = $event"
+      @size-change="handleSizeChange"
+      @current-change="handleCurrentChange"
+      class="pagination-container"
+    />
+
+    <!-- 新增/编辑对话框 -->
+    <el-dialog :title="dialogTitle" v-model="dialogVisible" width="50%">
+      <el-form :model="formData" label-width="120px">
+        <el-form-item
+          v-for="col in editableColumns"
+          :key="col.key"
+          :label="col.title"
+        >
+          <el-input
+            v-model="formData[col.dataKey]"
+            :type="col.inputType || 'text'"
+            class="custom-input"
+          ></el-input>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="dialogVisible = false" class="custom-cancel-button">
+          取消
+        </el-button>
+        <el-button
+          type="primary"
+          @click="submitForm"
+          class="custom-submit-button"
+        >
+          {{ dialogSubmitButtonText }}
+        </el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive, computed, onMounted } from "vue";
+import {
+  DeleteFilled,
+  Download,
+  Upload,
+  Plus,
+  EditPen,
+} from "@element-plus/icons-vue";
+import { ElMessage } from "element-plus";
+import {
+  table,
+  updateItem,
+  addItem,
+  deleteItemApi,
+  downloadTemplate,
+  exportData,
+  importData,
+} from "@/API/menus";
+import PaginationComponent from "@/components/PaginationComponent.vue";
+
+interface Column {
+  key: string;
+  dataKey: string;
+  title: string;
+  width: number;
+  inputType?: string;
+}
+
+const columns: Column[] = [
+  { key: "id", dataKey: "id", title: "序号", width: 100 },
+  { key: "Q_over_b", dataKey: "Q_over_b", title: "Q/ΔpH", width: 180 },
+  { key: "pH", dataKey: "pH", title: "初始pH", width: 180 },
+  { key: "OM", dataKey: "OM", title: "有机质含量", width: 180 },
+  { key: "CL", dataKey: "CL", title: "土壤粘粒", width: 180 },
+  { key: "H", dataKey: "H", title: "交换性氢", width: 180 },
+  { key: "Al", dataKey: "Al", title: "交换性铝", width: 180 },
+];
+
+const editableColumns = columns.filter((col) => col.key !== "id");
+
+const tableData = ref<any[]>([]);
+const selectedRow = ref<any | null>(null);
+const dialogVisible = ref(false);
+const formData = reactive<any>({});
+const dialogMode = ref<"add" | "edit">("add"); // 新增还是编辑模式
+
+const currentPage4 = ref(1);
+const pageSize4 = ref(10);
+
+const pagedTableData = computed(() => {
+  const start = (currentPage4.value - 1) * pageSize4.value;
+  const end = start + pageSize4.value;
+  return tableData.value.slice(start, end);
+});
+
+const pagedTableDataWithIndex = computed(() => {
+  return pagedTableData.value.map((item, index) => ({
+    ...item,
+    displayIndex: (currentPage4.value - 1) * pageSize4.value + index + 1,
+  }));
+});
+
+const loading = ref(false);
+const currentTableName = "current_reduce";
+
+const fetchTable = async () => {
+  try {
+    loading.value = true;
+    const response = await table({ table: currentTableName });
+    tableData.value = response.data.rows;
+  } catch (error) {
+    console.error("获取数据时出错:", error);
+    ElMessage.error("获取数据失败,请检查网络连接或服务器状态");
+  } finally {
+    loading.value = false;
+  }
+};
+
+onMounted(() => {
+  fetchTable();
+});
+
+const handleRowClick = (row: any) => {
+  selectedRow.value = row;
+  Object.assign(formData, row);
+};
+
+const openDialog = (mode: "add" | "edit", row?: any) => {
+  if (mode === "add") {
+    selectedRow.value = null;
+    editableColumns.forEach((col) => {
+      formData[col.dataKey] = "";
+    });
+  } else if (row) {
+    selectedRow.value = row;
+    Object.assign(formData, row);
+  }
+  dialogMode.value = mode;
+  dialogVisible.value = true;
+};
+
+function prepareFormData(
+  formData: { [x: string]: any },
+  excludeKeys = ["displayIndex"]
+): { [x: string]: any } {
+  const result: { [x: string]: any } = {};
+  for (const key in formData) {
+    if (!excludeKeys.includes(key)) {
+      let value = formData[key];
+      if (typeof value === "string" && value.startsWith("displayIndex-")) {
+        value = value.replace("displayIndex-", "");
+      }
+      result[key] = value;
+    }
+  }
+  return result;
+}
+
+const submitForm = async () => {
+  try {
+    const isValid = validateFormData(formData);
+    if (!isValid) {
+      alert("请检查输入的数据");
+      return;
+    }
+    const dataToSubmit = prepareFormData(formData);
+    if (!dataToSubmit.id && dialogMode.value !== "add") {
+      alert("无法找到记录ID,请联系管理员");
+      return;
+    }
+    let response;
+    if (dialogMode.value === "add") {
+      response = await addItem({ table: currentTableName, item: dataToSubmit });
+    } else {
+      response = await updateItem({
+        table: currentTableName,
+        item: dataToSubmit,
+      });
+    }
+    dialogVisible.value = false;
+    fetchTable();
+    alert(dialogMode.value === "add" ? "添加成功" : "修改成功");
+  } catch (error) {
+    let errorMessage = "未知错误";
+    if (error && typeof error === "object" && "response" in error) {
+      const response = (error as { response?: { data?: { message?: string } } })
+        .response;
+      if (
+        response &&
+        response.data &&
+        typeof response.data.message === "string"
+      ) {
+        errorMessage = response.data.message;
+      }
+    }
+    alert(`提交失败: ${errorMessage}`);
+  }
+};
+
+function validateFormData(data: { [x: string]: undefined }) {
+  for (let key in data) {
+    if (data[key] === "" || data[key] === undefined) {
+      return false;
+    }
+  }
+  return true;
+}
+
+const deleteItem = async (row: any) => {
+  if (!row) {
+    ElMessage.warning("请先选择一行记录");
+    return;
+  }
+  try {
+    const condition = { id: row.id };
+    await deleteItemApi({ table: "current_reduce", condition });
+    const index = tableData.value.findIndex((item) => item.id === row.id);
+    if (index > -1) {
+      tableData.value.splice(index, 1);
+    }
+    fetchTable();
+  } catch (error) {
+    console.error("删除记录时发生错误:", error);
+  }
+};
+
+const downloadTemplateAction = async () => {
+  try {
+    await downloadTemplate("current_reduce");
+  } catch (error) {
+    console.error("下载模板时发生错误:", error);
+  }
+};
+
+const exportDataAction = async () => {
+  try {
+    await exportData("current_reduce");
+  } catch (error) {
+    console.error("导出数据时发生错误:", error);
+  }
+};
+
+// 导入
+const importDataAction = async (file: File) => {
+  try {
+    const response = await importData("reduce", file); // 传递 dataset_type 的值(如 'reduce')
+    if (response && response.data) {
+      const { total_data, new_data, duplicate_data, message } = response.data;
+      ElMessage({
+        message: `导入结果: ${message} 新增总数:${total_data}, 成功新增:${new_data}, 数据重复:${duplicate_data}`,
+        type: "success",
+      });
+      fetchTable(); // 假设存在 fetchTable 方法刷新表格
+    }
+  } catch (error) {
+    let errorMessage = "数据导入失败";
+    if (error && typeof error === "object" && "response" in error) {
+      const response = (error as { response?: { data?: { message?: string } } })
+        .response;
+      if (response && response.data && typeof response.data.message === "string") {
+        errorMessage += `: ${response.data.message}`;
+      }
+    }
+    ElMessage.error(errorMessage);
+  }
+};
+
+const handleFileChange = (event: Event) => {
+  const target = event.target as HTMLInputElement;
+  if (target.files && target.files.length > 0) {
+    importDataAction(target.files[0]);
+  }
+};
+
+
+const handleSizeChange = (val: number) => {};
+const handleCurrentChange = (val: number) => {};
+
+const formatNumber = (row: any, column: any, cellValue: any) => {
+  if (typeof cellValue === "number") {
+    return cellValue.toFixed(3);
+  }
+  return cellValue;
+};
+
+const dialogTitle = computed(() => {
+  return dialogMode.value === "add" ? "新增记录" : "编辑记录";
+});
+
+const dialogSubmitButtonText = computed(() => {
+  return dialogMode.value === "add" ? "添加" : "保存";
+});
+</script>
+
+<style scoped>
+.app-container {
+  padding: 15px 40px;
+  min-height: 100vh;
+  background-color: #f0f2f5;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+.button-group {
+  display: flex;
+  gap: 12px;
+  justify-content: flex-end;
+  width: 100%;
+  margin-bottom: 20px;
+}
+.custom-button {
+  color: #fff;
+  border: none;
+  border-radius: 8px;
+  font-size: 14px;
+  padding: 10px 18px;
+  transition: transform 0.3s ease, background-color 0.3s ease;
+  min-width: 130px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.download-button,
+.export-button,
+.add-button,
+.import-button {
+  background-color: #67c23a;
+}
+.download-button:hover,
+.export-button:hover,
+.import-button:hover,
+.add-button:hover {
+  background-color: #85ce61;
+  transform: scale(1.05);
+}
+.custom-table {
+  width: 100%;
+  border-radius: 8px;
+  overflow: hidden;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+  background-color: #fff;
+  margin-top: 10px;
+}
+:deep( .el-table th) {
+  background: linear-gradient(180deg, #61e054, #4db944);
+  color: #fff;
+  font-weight: bold;
+  text-align: center;
+  padding: 14px 0;
+  font-size: 14px;
+}
+:deep( .el-table__row:nth-child(odd)) {
+  background-color: #E4FBE5;
+}
+:deep( .el-table__row:nth-child(even)) {
+  background-color: #ffffff;
+}
+:deep( .el-table td) {
+  padding: 12px 10px;
+  text-align: center;
+  border-bottom: 1px solid #ebeef5;
+}
+.action-button {
+  font-size: 16px;
+  margin-right: 5px;
+  border-radius: 50%;
+  transition: transform 0.3s ease, background-color 0.3s ease;
+}
+.edit-button {
+  background-color: #409eff;
+  color: #fff;
+}
+.edit-button:hover {
+  background-color: #66b1ff;
+  transform: scale(1.1);
+}
+.delete-button {
+  background-color: #f56c6c;
+  color: #fff;
+}
+.delete-button:hover {
+  background-color: #f78989;
+  transform: scale(1.1);
+}
+.el-form-item__label {
+  width: 150px;
+  font-weight: bold;
+}
+.el-form-item__content {
+  margin-left: 150px;
+}
+.custom-input .el-input__inner {
+  border-radius: 6px;
+  border: 1px solid #dcdcdc;
+  transition: border-color 0.3s ease;
+  padding: 10px;
+  font-size: 14px;
+}
+.custom-input .el-input__inner:focus {
+  border-color: #409eff;
+}
+.custom-cancel-button,
+.custom-submit-button {
+  border: none;
+  border-radius: 6px;
+  font-size: 14px;
+  padding: 10px 20px;
+  transition: transform 0.3s ease, background-color 0.3s ease;
+  min-width: 100px;
+}
+.custom-cancel-button {
+  background-color: #f4f4f5;
+  color: #606266;
+}
+.custom-cancel-button:hover {
+  background-color: #e4e7ed;
+  transform: scale(1.05);
+}
+.custom-submit-button {
+  background-color: #409eff;
+  color: #fff;
+}
+.custom-submit-button:hover {
+  background-color: #66b1ff;
+  transform: scale(1.05);
+}
+.pagination-container {
+  margin-top: 30px;
+  text-align: center;
+}
+.action-buttons {
+  display: flex;
+  justify-content: center;
+}
+</style>

+ 23 - 0
src/views/Admin/modelManagement/AcidReductionModel.vue

@@ -0,0 +1,23 @@
+<template>
+  <div class="">
+    
+  </div>
+</template>
+
+<script>
+export default {
+  name: '',
+  data() {
+    return {
+      
+    };
+  },
+  methods: {
+    
+  }
+};
+</script>
+
+<style scoped>
+  
+</style>

+ 32 - 0
src/views/Admin/modelManagement/CadmiumPredictionModel.vue

@@ -0,0 +1,32 @@
+<template>
+  <div>
+    <el-tabs v-model="activeTab" @tab-click="handleTabClick">
+      <el-tab-pane label="韶关" name="shaoguan"></el-tab-pane>
+      <el-tab-pane label="河池" name="hechi"></el-tab-pane>
+      <el-tab-pane label="腾冲" name="tengchong"></el-tab-pane>
+    </el-tabs>
+    <div v-if="activeTab === 'shaoguan'">韶关1</div>
+    <div v-if="activeTab === 'hechi'">河池2</div>
+    <div v-if="activeTab === 'tengchong'">腾冲3</div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'CadmiumPredictionModel',
+  data() {
+    return {
+      activeTab: 'shaoguan', // 默认选中韶关
+    };
+  },
+  methods: {
+    handleTabClick(tab) {
+      console.log(`当前选中标签: ${tab.name}`);
+    },
+  },
+};
+</script>
+
+<style scoped>
+ 
+</style>

+ 23 - 0
src/views/Admin/modelManagement/RiceRiskModel.vue

@@ -0,0 +1,23 @@
+<template>
+  <div class="">
+    
+  </div>
+</template>
+
+<script>
+export default {
+  name: '',
+  data() {
+    return {
+      
+    };
+  },
+  methods: {
+    
+  }
+};
+</script>
+
+<style scoped>
+  
+</style>

+ 1 - 4
src/views/User/heavyMetalFluxCalculation/inputFluxCalculation/atmosphericDryWetDeposition.vue → src/views/Admin/modelManagement/VegetableRiskModel.vue

@@ -1,13 +1,10 @@
 <template>
   <div class="">
-    <router-view></router-view>
+    
   </div>
 </template>
 
 <script>
-import AtmtencentMap from './atmospheredata/atmtencentMap.vue';
-
-
 export default {
   name: '',
   data() {

+ 23 - 0
src/views/Admin/modelManagement/WheatRiskModel.vue

@@ -0,0 +1,23 @@
+<template>
+  <div class="">
+    
+  </div>
+</template>
+
+<script>
+export default {
+  name: '',
+  data() {
+    return {
+      
+    };
+  },
+  methods: {
+    
+  }
+};
+</script>
+
+<style scoped>
+  
+</style>

+ 353 - 0
src/views/Admin/parameterConfig/ModelSelection.vue

@@ -0,0 +1,353 @@
+<template>
+  <div class="model-container">
+    <!-- 模型选择下拉菜单 -->
+    <el-dropdown @command="handleModelTypeChange" trigger="click" :hide-on-click="true" style="margin-bottom: 5px">
+      <span class="el-dropdown-link">
+        {{ selectedModelTypeLabel }}<el-icon class="el-icon--right"><ArrowDown /></el-icon>
+      </span>
+      <template #dropdown>
+        <el-dropdown-menu>
+          <el-dropdown-item
+            v-for="type in modelTypes"
+            :key="type.value"
+            :command="type.value"
+            :disabled="selectedModelType === type.value"
+          >
+            {{ type.label }}
+          </el-dropdown-item>
+        </el-dropdown-menu>
+      </template>
+    </el-dropdown>
+
+    <!-- 加载失败提示 -->
+    <el-alert
+      v-if="errorOccurred"
+      :title="`加载数据失败: ${errorMessage}`"
+      type="error"
+      show-icon
+      style="margin-bottom: 5px"
+    />
+
+    <!-- 动态加载的模型信息按表格显示 -->
+    <el-table
+      v-if="selectedModelType && filteredModels.length > 0 && !loading"
+      :data="pagedFilteredModels"
+      style="width: 100%"
+      border
+      height="calc(100vh - 300px)" 
+    >
+      <el-table-column prop="ModelID" label="序号" min-width="80" />
+      <el-table-column prop="Model_name" label="模型名称" min-width="180" />
+      <el-table-column prop="Data_type" label="数据类型" min-width="120" />
+      <el-table-column prop="Performance_score" label="性能评分" min-width="120" />
+      <el-table-column prop="Created_at" label="创建时间" min-width="180" />
+      <el-table-column label="操作" min-width="100">
+        <template #default="scope">
+          <el-button size="small" @click="selectModel(scope.row)">选择</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 分页控制 -->
+    <div
+      class="demo-pagination-block"
+      v-if="selectedModelType && filteredModels.length > 0"
+      style="text-align: center; margin-top: 20px;"
+    >
+      <el-pagination
+        v-model:current-page="currentPage"
+        v-model:page-size="pageSize"
+        :page-sizes="[10, 20, 30, 40]"
+        layout="total, sizes, prev, pager, next, jumper"
+        :total="filteredModels.length"
+        @size-change="handleSizeChange"
+        @current-change="handleCurrentChange"
+      />
+    </div>
+
+    <!-- 切换模型按钮 -->
+    <div
+      v-if="selectedModel && !loading"
+      style="text-align: center; margin-top: 5px"
+    >
+      <el-button type="success" size="small" @click="switchModel">切换模型</el-button>
+    </div>
+
+    <!-- 当没有选择任何模型类型时显示提示信息 -->
+    <div
+      v-if="!selectedModelType && !loading && !errorOccurred"
+      style="text-align: center; padding-top: 5px"
+    >
+      请选择一个模型类型以查看详细信息。
+    </div>
+
+    <!-- 当筛选结果为空时显示提示信息 -->
+    <div
+      v-if="selectedModelType && filteredModels.length === 0 && !loading && !errorOccurred"
+      style="text-align: center; padding-top: 5px"
+    >
+      没有找到与所选模型类型匹配的数据。
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, onMounted, computed } from 'vue';
+import axios from 'axios';
+import { ElMessage } from 'element-plus';
+import { ArrowDown } from '@element-plus/icons-vue';
+import { table } from '@/API/menus';
+
+interface ModelData {
+  ModelID: number;
+  Model_name: string;
+  Model_type: string;
+  Created_at: string;
+  Description: string;
+  DatasetID: string;
+  ModelFilePath: string;
+  Data_type: string;
+  Performance_score: number;
+  MAE: number;
+  RMSE: number;
+  CV_score: string;
+}
+
+interface Dataset {
+  id: any;
+  name: string;
+  description: string;
+  type: string;
+  count: number;
+  status: string;
+  uploadTime: string;
+  selected: boolean;
+}
+
+const loading = ref(true);
+const allModels = ref<ModelData[]>([]);
+const rows = ref<Dataset[]>([]);
+const selectedModelType = ref<string>("");
+const selectedModelTypeLabel = ref<string>("请选择模型类型");
+const selectedModel = ref<ModelData | null>(null);
+const modelTypes = [
+  { value: "reduce", label: "降酸模型(Reduce Model)" },
+  { value: "reflux", label: "反酸模型(Reflux Model)" },
+];
+
+// 分页相关的变量
+const currentPage = ref(1);
+const pageSize = ref(10);
+
+// 获取所有模型数据
+const fetchAllModels = async () => {
+  try {
+    const requestBody = { table: "Models" };
+    const response = await table(requestBody);
+    allModels.value = response.data.rows.map((row: any) => ({
+      ...row,
+      selected: false,
+    }));
+  } catch (error) {
+    let errorMessageVal = "未知错误";
+    if (error && typeof error === 'object' && 'message' in error) {
+      errorMessageVal = (error as { message?: string }).message || errorMessageVal;
+    }
+    console.error("Error fetching data:", errorMessageVal);
+    ElMessage.error("数据加载失败:" + errorMessageVal);
+    errorOccurred.value = true;
+    errorMessage.value = errorMessageVal;
+  } finally {
+    loading.value = false;
+  }
+};
+
+// 获取所有数据集数据
+const fetchData = async () => {
+  try {
+    const response = await table({ table: 'Datasets' });
+    return response.data.rows;
+  } catch (error) {
+    let errorMessageVal = '未知错误';
+    if (error && typeof error === 'object' && 'message' in error) {
+      errorMessageVal = (error as { message?: string }).message || errorMessageVal;
+    }
+    console.error('Error fetching data:', errorMessageVal);
+    throw new Error(errorMessageVal); // 重新抛出带有详细信息的新错误
+  }
+};
+
+// 加载数据集数据
+const LoadData = async () => {
+  try {
+    rows.value = (await fetchData()).map(
+      (row: {
+        Dataset_ID: any;
+        Dataset_name: any;
+        Dataset_description: any;
+        Dataset_type: any;
+        Row_count: any;
+        Status: any;
+        Uploaded_at: any;
+      }) => ({
+        id: row.Dataset_ID,
+        name: row.Dataset_name,
+        description: row.Dataset_description,
+        type: row.Dataset_type,
+        count: row.Row_count,
+        status: row.Status,
+        uploadTime: row.Uploaded_at,
+        selected: false,
+      })
+    );
+  } catch (error) {
+    let errorMessageVal = '未知错误';
+    if (error && typeof error === 'object' && 'message' in error) {
+      errorMessageVal = (error as { message?: string }).message || errorMessageVal;
+    }
+    console.error('Error fetching data:', errorMessageVal);
+    ElMessage.error('数据加载失败:' + errorMessageVal);
+    errorOccurred.value = true;
+    errorMessage.value = errorMessageVal;
+  } finally {
+    loading.value = false;
+  }
+};
+
+// 根据选择的模型类型计算要展示的数据
+const filteredModels = computed(() => {
+  if (!selectedModelType.value) return [];
+
+  // 根据选定的模型类型(降酸或反酸)进行筛选
+  return allModels.value.filter(m => m.Data_type === selectedModelType.value);
+});
+
+// 分页后的过滤数据
+const pagedFilteredModels = computed(() => {
+  const start = (currentPage.value - 1) * pageSize.value;
+  const end = start + pageSize.value;
+  return filteredModels.value.slice(start, end);
+});
+
+// 过滤后的数据集数据
+const filteredRows = computed(() =>
+  selectedModelType.value ? rows.value.filter((row) => row.type === selectedModelType.value) : []
+);
+
+// 分页后的过滤数据集数据
+const pagedFilteredRows = computed(() => {
+  const start = (currentPage.value - 1) * pageSize.value;
+  const end = start + pageSize.value;
+  return filteredRows.value.slice(start, end);
+});
+
+const handleModelTypeChange = (value: string) => {
+  selectedModelType.value = value;
+  const selectedType = modelTypes.find(type => type.value === value);
+  if (selectedType) {
+    selectedModelTypeLabel.value = selectedType.label;
+    ElMessage.success(`已切换到 ${selectedType.label}`);
+  }
+  selectedModel.value = null; // 清空已选中的模型
+  currentPage.value = 1; // 当模型类型改变时重置当前页码
+};
+
+const selectModel = (model: ModelData) => {
+  selectedModel.value = model;
+};
+
+// 分页事件处理
+const handleSizeChange = (val: number) => {
+  pageSize.value = val;
+};
+const handleCurrentChange = (val: number) => {
+  currentPage.value = val;
+};
+
+// 切换模型状态
+const switchModel = async () => {
+  if (!selectedModel.value) {
+    ElMessage.warning("请先选择一个模型");
+    return;
+  }
+
+  try {
+    await axios.post("https://127.0.0.1:5000/switch-model", {
+      model_id: selectedModel.value.ModelID,
+      model_name: selectedModel.value.Model_name,
+    });
+    ElMessage.success(`模型 ${selectedModel.value.Model_name} 切换成功`);
+    selectedModel.value = null; // 清空已选中的模型
+  } catch (error) {
+    let message = "未知错误";
+    if (error && typeof error === "object" && "message" in error) {
+      message = (error as { message?: string }).message || message;
+    }
+    console.error("Failed to switch model:", message);
+    ElMessage.error("模型切换失败:" + message);
+  }
+};
+
+const errorOccurred = ref(false);
+const errorMessage = ref('');
+const selectedRows = ref<Dataset[]>([]);
+
+onMounted(() => {
+  fetchAllModels();
+  LoadData();
+});
+</script>
+
+<style scoped>
+.model-container {
+  display: grid;
+  gap: 10px;
+  padding: 10px;
+  background-color: #f9fafb;
+  border-radius: 8px;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+  height: 100%; /* 让容器高度占满父元素 */
+}
+
+.example-showcase .el-dropdown-link {
+  cursor: pointer;
+  color: var(--el-color-primary);
+  display: flex;
+  align-items: center;
+}
+
+/* 移除下拉菜单的边框 */
+.el-popper.is-light {
+  border: none !important;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+}
+
+.el-table {
+  width: 100%;
+  margin-top: 10px;
+  border-radius: 8px; /* 添加圆角 */
+}
+
+:deep( .el-table th) {
+  background-color: #61e054 !important;
+  color: #fff;
+}
+
+:deep( .el-table td),
+:deep( .el-table th) {
+  padding: 8px 0;
+}
+
+:deep( .el-table__row:nth-child(odd)) {
+  background-color: #e4fbe5 !important;
+}
+
+/* 禁用项样式 */
+:deep( .el-dropdown-menu__item.is-disabled) {
+  color: #c0c4cc !important;
+  cursor: not-allowed !important;
+}
+</style>
+
+
+

+ 326 - 0
src/views/Admin/parameterConfig/ModelTrain.vue

@@ -0,0 +1,326 @@
+<template>
+  <div class="model-container">
+    <!-- 加载失败提示 -->
+    <el-alert
+      v-if="errorOccurred"
+      :title="`加载数据失败: ${errorMessage}`"
+      type="error"
+      show-icon
+      style="margin-bottom: 5px"
+    />
+
+    <!-- 下拉菜单 -->
+    <el-dropdown
+      @command="handleCommand"
+      trigger="click"
+      :hide-on-click="true"
+      style="margin-bottom: 5px"
+    >
+      <span class="el-dropdown-link">
+        {{ selectedModelTypeLabel
+        }}<el-icon class="el-icon--right"><ArrowDown /></el-icon>
+      </span>
+      <template #dropdown>
+        <el-dropdown-menu>
+          <el-dropdown-item
+            v-for="type in modelTypes"
+            :key="type.value"
+            :command="type.value"
+            :disabled="selectedModelType === type.value"
+          >
+            {{ type.label }}
+          </el-dropdown-item>
+        </el-dropdown-menu>
+      </template>
+    </el-dropdown>
+
+    <el-table
+      :data="pagedFilteredRows"
+      v-loading="loading"
+      style="width: 100%"
+      empty-text="暂无相关数据"
+      v-if="selectedModelType"
+      border
+      height="calc(100vh - 300px)"
+    >
+      <el-table-column prop="id" label="序号" min-width="80" />
+      <el-table-column prop="name" label="数据集名称" min-width="200" />
+      <el-table-column prop="count" label="数据条数" min-width="100" />
+      <el-table-column prop="uploadTime" label="更新时间" min-width="180" />
+      <el-table-column label="操作" min-width="100">
+        <template #default="scope">
+          <el-button size="small" @click="selectRow(scope.row)">选择</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 分页控制 -->
+    <div
+      class="demo-pagination-block"
+      v-if="selectedModelType && filteredRows.length > 0"
+      style="text-align: center; margin-top: 20px"
+    >
+      <el-pagination
+        v-model:current-page="currentPage"
+        v-model:page-size="pageSize"
+        :page-sizes="[10, 20, 30, 40]"
+        layout="total, sizes, prev, pager, next, jumper"
+        :total="filteredRows.length"
+        @size-change="handleSizeChange"
+        @current-change="handleCurrentChange"
+      />
+    </div>
+
+    <!-- 训练模型按钮 -->
+    <div
+      v-if="
+        selectedModelType &&
+        pagedFilteredRows.length > 0 &&
+        selectedRows.length > 0
+      "
+      style="text-align: center; margin-top: 5px"
+    >
+      <el-button type="success" size="small" @click="trainModel"
+        >训练模型</el-button
+      >
+    </div>
+
+    <!-- 当没有选择任何模型类型时显示提示信息 -->
+    <div
+      v-if="!selectedModelType && !loading && !errorOccurred"
+      style="text-align: center; padding-top: 5px"
+    >
+      请选择一个模型类型以查看详细信息。
+    </div>
+
+    <!-- 当筛选结果为空时显示提示信息 -->
+    <div
+      v-if="
+        selectedModelType &&
+        filteredRows.length === 0 &&
+        !loading &&
+        !errorOccurred
+      "
+      style="text-align: center; padding-top: 5px"
+    >
+      没有找到与所选模型类型匹配的数据。
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, onMounted, computed } from "vue";
+import axios from "axios";
+import { ElMessage } from "element-plus";
+import { ArrowDown } from "@element-plus/icons-vue";
+import { table } from "@/API/menus";
+
+interface Dataset {
+  id: any;
+  name: string;
+  description: string;
+  type: string;
+  count: number;
+  status: string;
+  uploadTime: string;
+}
+
+const loading = ref(true);
+const rows = ref<Dataset[]>([]);
+const errorOccurred = ref(false);
+const errorMessage = ref("");
+const selectedRows = ref<Dataset[]>([]);
+const currentPage = ref(1);
+const pageSize = ref(10);
+
+const modelTypes = [
+  { value: "reduce", label: "降酸模型" },
+  { value: "reflux", label: "反酸模型" },
+];
+const selectedModelType = ref();
+
+// 数据过滤后的结果
+const filteredRows = computed(() =>
+  selectedModelType.value
+    ? rows.value.filter((row) => row.type === selectedModelType.value)
+    : []
+);
+
+// 分页后的过滤数据
+const pagedFilteredRows = computed(() => {
+  const start = (currentPage.value - 1) * pageSize.value;
+  const end = start + pageSize.value;
+  return filteredRows.value.slice(start, end);
+});
+
+// 定义 fetchData 方法
+const fetchData = async () => {
+  try {
+    const response = await table({ table: "Datasets" });
+    return response.data.rows;
+  } catch (error) {
+    let errorMessage = "未知错误";
+    if (error && typeof error === "object" && "message" in error) {
+      errorMessage = (error as { message?: string }).message || errorMessage;
+    }
+    console.error("Error fetching data:", errorMessage);
+    throw new Error(errorMessage); // 重新抛出带有详细信息的新错误
+  }
+};
+
+// 定义 LoadData 方法
+const LoadData = async () => {
+  try {
+    rows.value = (await fetchData()).map(
+      (row: {
+        Dataset_ID: any;
+        Dataset_name: any;
+        Dataset_description: any;
+        Dataset_type: any;
+        Row_count: any;
+        Status: any;
+        Uploaded_at: any;
+      }) => ({
+        id: row.Dataset_ID,
+        name: row.Dataset_name,
+        description: row.Dataset_description,
+        type: row.Dataset_type,
+        count: row.Row_count,
+        status: row.Status,
+        uploadTime: row.Uploaded_at,
+      })
+    );
+  } catch (error) {
+    let errorMessageText = "未知错误";
+    if (error && typeof error === "object" && "message" in error) {
+      errorMessageText = (error as { message?: string }).message || errorMessageText;
+    }
+    console.error("Error fetching data:", errorMessageText);
+    ElMessage.error("数据加载失败:" + errorMessageText);
+    errorOccurred.value = true;
+    errorMessage.value = errorMessageText; // 修复赋值
+  } finally {
+    loading.value = false;
+  }
+};
+
+// 定义 handleCommand 方法
+const handleCommand = (newType: string) => {
+  selectedModelType.value = newType;
+  ElMessage.success(
+    `已切换到 ${modelTypes.find((type) => type.value === newType)?.label}`
+  );
+};
+
+const selectRow = (row: Dataset) => {
+  selectedRows.value.push(row);
+};
+
+const showMessageDialog = (
+  message: string,
+  type: "success" | "warning" | "info" | "error"
+) => {
+  ElMessage({
+    message: "提示信息",
+    type: "success",
+    showClose: false,
+  });
+};
+
+const trainModel = async () => {
+  if (!selectedRows.value.length) {
+    showMessageDialog("请先选择一行或多行数据", "warning");
+    return;
+  }
+
+  const firstSelectedRow = selectedRows.value[0];
+  const trainData = {
+    model_type: "RandomForest",
+    model_name: "ForestModel1",
+    model_description: "A random forest model trained on current data.",
+    data_type: firstSelectedRow.type,
+    dataset_id: firstSelectedRow.id,
+  };
+
+  try {
+    await axios.post("https://127.0.0.1:5000/train-and-save-model", trainData);
+    showMessageDialog("模型训练完成", "success");
+    selectedRows.value = [];
+  } catch (error) {
+    console.error("模型训练失败:", error);
+    showMessageDialog("模型训练失败", "error");
+  }
+};
+
+// 定义 handleCurrentChange 方法
+const handleCurrentChange = (newPage: number) => {
+  currentPage.value = newPage;
+};
+
+// 定义 handleSizeChange 方法
+const handleSizeChange = (newSize: number) => {
+  pageSize.value = newSize;
+};
+
+onMounted(() => {
+  LoadData();
+});
+
+const selectedModelTypeLabel = computed(() => {
+  const type = modelTypes.find((t) => t.value === selectedModelType.value);
+  return type ? type.label : "请选择数据集进行计算";
+});
+</script>
+
+<style scoped>
+.model-container {
+  display: grid;
+  gap: 10px; /* 增加元素之间的间隔 */
+  padding: 10px; /* 增加整体容器的内边距 */
+  background-color: #f9fafb; /* 背景颜色 */
+  border-radius: 8px; /* 圆角 */
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); /* 阴影效果 */
+}
+
+.example-showcase .el-dropdown-link {
+  cursor: pointer;
+  color: var(--el-color-primary);
+  display: flex;
+  align-items: center;
+}
+
+/* 移除下拉菜单的边框 */
+.el-popper.is-light {
+  border: none !important;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); /* 下拉菜单阴影效果 */
+}
+
+.el-table {
+  width: 100%;
+  margin-top: 10px; /* 调整表格与上方元素的距离 */
+  border-radius: 8px; /* 添加圆角 */
+}
+
+/* 设置表头背景色 */
+:deep( .el-table th) {
+  background-color: #61e054 !important; /* 设置你想要的颜色 */
+  color: #fff; /* 表头文字颜色 */
+}
+
+/* 减少表格单元格的内边距 */
+:deep( .el-table td),
+:deep( .el-table th) {
+  padding: 8px 0;
+}
+
+/* 设置奇数行背景色 */
+:deep( .el-table__row:nth-child(odd)) {
+  background-color: #e4fbe5 !important;
+}
+
+/* 禁用项样式 */
+:deep( .el-dropdown-menu__item.is-disabled) {
+  color: #c0c4cc !important; /* 禁用项文字颜色 */
+  cursor: not-allowed !important; /* 禁用项鼠标样式 */
+}
+</style>

+ 148 - 0
src/views/Admin/parameterConfig/thres.vue

@@ -0,0 +1,148 @@
+<template>
+  <div class="container">
+    <!-- 降酸阈值 -->
+    <el-card class="threshold-card">
+      <h3>降酸阈值</h3>
+      <p><strong>当前阈值:</strong> {{ currentThresholdReduce }}</p>
+
+      <el-input 
+        v-model="newThresholdReduce" 
+        placeholder="请输入新的降酸阈值" 
+        class="input"
+      ></el-input>
+
+      <el-button 
+        type="success" 
+        @click="updateThreshold('reduce')" 
+        class="button"
+      >
+        更新降酸阈值
+      </el-button>
+    </el-card>
+
+    <!-- 反酸阈值 -->
+    <el-card class="threshold-card">
+      <h3>反酸阈值</h3>
+      <p><strong>当前阈值:</strong> {{ currentThresholdReflux }}</p>
+
+      <el-input 
+        v-model="newThresholdReflux" 
+        placeholder="请输入新的反酸阈值" 
+        class="input"
+      ></el-input>
+
+      <el-button 
+        type="success" 
+        @click="updateThreshold('reflux')" 
+        class="button"
+      >
+        更新反酸阈值
+      </el-button>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import axios from 'axios';
+
+export default {
+  data() {
+    return {
+      // 当前阈值
+      currentThresholdReduce: null,
+      currentThresholdReflux: null,
+      // 新增阈值输入框绑定值
+      newThresholdReduce: '',
+      newThresholdReflux: ''
+    };
+  },
+  methods: {
+    /**
+     * 获取当前阈值信息
+     */
+    fetchThresholds() {
+      axios.get('https://127.0.0.1:5000/get-threshold')
+        .then(response => {
+          const { reduce, reflux } = response.data;
+          this.currentThresholdReduce = reduce.current_threshold;
+          this.currentThresholdReflux = reflux.current_threshold;
+        })
+        .catch(error => {
+          console.error("获取阈值失败:", error);
+          this.$message.error('无法加载阈值,请稍后再试');
+        });
+    },
+
+    /**
+     * 更新阈值
+     * @param {string} dataType - 数据类型 ('reduce' 或 'reflux')
+     */
+    updateThreshold(dataType) {
+      let thresholdValue, targetVariable;
+
+      if (dataType === 'reduce') {
+        thresholdValue = parseFloat(this.newThresholdReduce);
+        targetVariable = 'currentThresholdReduce';
+      } else if (dataType === 'reflux') {
+        thresholdValue = parseFloat(this.newThresholdReflux);
+        targetVariable = 'currentThresholdReflux';
+      }
+
+      if (isNaN(thresholdValue) || thresholdValue <= 0) {
+        this.$message.error('请输入有效的正数');
+        return;
+      }
+
+      axios.post('https://127.0.0.1:5000/update-threshold', {
+        threshold: thresholdValue,
+        data_type: dataType
+      })
+        .then(response => {
+          if (response.data.success) {
+            this[targetVariable] = thresholdValue; // 动态更新当前阈值
+            this.$message.success(response.data.message);
+          } else {
+            this.$message.error(response.data.error);
+          }
+        })
+        .catch(error => {
+          console.error("更新阈值失败:", error);
+          this.$message.error('更新阈值时发生错误');
+        });
+    }
+  },
+  mounted() {
+    // 页面加载时获取当前阈值
+    this.fetchThresholds();
+  }
+};
+</script>
+
+<style scoped>
+/* 容器样式 */
+.container {
+  display: flex;
+  justify-content: space-around;
+  gap: 20px;
+}
+
+/* 卡片样式 */
+.threshold-card {
+  flex: 1;
+  max-width: 400px;
+  min-width: 300px;
+  margin: 0 auto;
+  padding: 16px;
+}
+
+/* 输入框和按钮样式 */
+.input {
+  width: 100%;
+  margin-top: 10px;
+}
+
+.button {
+  width: 100%;
+  margin-top: 10px;
+}
+</style>

+ 283 - 0
src/views/User/HmOutFlux/agriInput/prodInputFlux.vue

@@ -0,0 +1,283 @@
+<template>
+  <div class="fertilizer-input-form">
+    <el-card shadow="always" class="form-card">
+      <div class="card-content">
+        <div class="input-section">
+          <el-form label-width="250px" label-position="top">
+            <div class="form-section">
+              <div class="input-group">
+                <el-form-item label="氮肥镉含量平均值 (mg/kg)" class="form-item">
+                  <el-input v-model="nitrogenCdContent" placeholder="0.05"></el-input>
+                </el-form-item>
+                <el-form-item label="氮肥单位面积使用量 (t/ha/a)" class="form-item">
+                  <el-input v-model="nitrogenUsage" placeholder="0.05"></el-input>
+                </el-form-item>
+              </div>
+              <div class="input-group">
+                <el-form-item label="磷肥镉含量平均值 (mg/kg)" class="form-item">
+                  <el-input v-model="phosphorusCdContent" placeholder="0.158"></el-input>
+                </el-form-item>
+                <el-form-item label="磷肥单位面积使用量 (t/ha/a)" class="form-item">
+                  <el-input v-model="phosphorusUsage" placeholder="0.158"></el-input>
+                </el-form-item>
+              </div>
+              <div class="input-group">
+                <el-form-item label="钾肥镉含量平均值 (mg/kg)" class="form-item">
+                  <el-input v-model="potassiumCdContent" placeholder="0.06"></el-input>
+                </el-form-item>
+                <el-form-item label="钾肥单位面积使用量 (t/ha/a)" class="form-item">
+                  <el-input v-model="potassiumUsage" placeholder="0.06"></el-input>
+                </el-form-item>
+              </div>
+              <div class="input-group">
+                <el-form-item label="复合肥镉含量平均值 (mg/kg)" class="form-item">
+                  <el-input v-model="compoundFertilizerCdContent" placeholder="0.065"></el-input>
+                </el-form-item>
+                <el-form-item label="复合肥单位面积使用量 (t/ha/a)" class="form-item">
+                  <el-input v-model="compoundFertilizerUsage" placeholder="0.065"></el-input>
+                </el-form-item>
+              </div>
+              <div class="input-group">
+                <el-form-item label="有机肥镉含量平均值 (mg/kg)" class="form-item">
+                  <el-input v-model="organicFertilizerCdContent" placeholder="0.6"></el-input>
+                </el-form-item>
+                <el-form-item label="有机肥单位面积使用量 (t/ha/a)" class="form-item">
+                  <el-input v-model="organicFertilizerUsage" placeholder="0.6"></el-input>
+                </el-form-item>
+              </div>
+              <div class="input-group">
+                <el-form-item label="农药镉含量 (mg/kg)" class="form-item">
+                  <el-input v-model="pesticideCdContent" placeholder="0.25"></el-input>
+                </el-form-item>
+                <el-form-item label="农药单位面积使用量 (t/ha/a)" class="form-item">
+                  <el-input v-model="pesticideUsage" placeholder="0.25"></el-input>
+                </el-form-item>
+              </div>
+              <div class="input-group">
+                <el-form-item label="农家肥镉含量 (mg/kg)" class="form-item">
+                  <el-input v-model="farmYardManureCdContent" placeholder="0.35"></el-input>
+                </el-form-item>
+                <el-form-item label="农家肥单位面积使用量 (t/ha/a)" class="form-item">
+                  <el-input v-model="farmYardManureUsage" placeholder="0.35"></el-input>
+                </el-form-item>
+              </div>
+              <div class="input-group">
+                <el-form-item label="农膜镉含量 (mg/kg)" class="form-item">
+                  <el-input v-model="agriFilmCdContent" placeholder="0.25"></el-input>
+                </el-form-item>
+                <el-form-item label="农膜(存留)单位面积使用量 (t/ha/a)" class="form-item">
+                  <el-input v-model="agriFilmResidueUsage" placeholder="0.6"></el-input>
+                </el-form-item>
+              </div>
+            </div>
+          </el-form>
+        </div>
+        
+        <div class="button-section">
+          <!-- 按钮区域背景图 -->
+          <div class="button-bg"></div>
+          <!-- 底部半透明层 -->
+          <div class="bottom-overlay"></div>
+          <el-button class="calculate-btn">
+            <span class="btn-text">农产品输入通量计算</span>
+          </el-button>
+        </div>
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      nitrogenCdContent: '0.05',
+      nitrogenUsage: '0.05',
+      phosphorusCdContent: '0.158',
+      phosphorusUsage: '0.158',
+      potassiumCdContent: '0.06',
+      potassiumUsage: '0.06',
+      compoundFertilizerCdContent: '0.065',
+      compoundFertilizerUsage: '0.065',
+      organicFertilizerCdContent: '0.6',
+      organicFertilizerUsage: '0.6',
+      pesticideCdContent: '0.25',
+      pesticideUsage: '0.25',
+      farmYardManureCdContent: '0.35',
+      farmYardManureUsage: '0.35',
+      agriFilmCdContent: '0.25',
+      agriFilmResidueUsage: '0.6'
+    };
+  }
+};
+</script>
+
+<style scoped>
+.fertilizer-input-form {
+  padding: 20px;
+  display: flex;
+  justify-content: center;
+  background-color: rgba(255, 255, 255, 0.8); /* 半透明背景 */
+}
+
+.form-card {
+  width: 90%;
+  max-width: 1200px;
+  margin: 0 auto;
+  background: linear-gradient(135deg, #FAFDFF, #FFFAA2);
+  border: 1px solid #e6e6e6;
+  border-radius: 12px;
+  overflow: hidden;
+  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
+}
+
+.card-content {
+  display: flex;
+  min-height: 600px;
+}
+
+.input-section {
+  width: 60%;
+  padding: 30px;
+  border-right: 1px dashed #c0c4cc;
+}
+
+.button-section {
+  width: 40%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 30px;
+  position: relative;
+  overflow: hidden;
+}
+
+/* 按钮区域背景图 - 使用本地图片 */
+.button-bg {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  /* 请替换为您的本地图片路径 */
+  background: url('@/assets/images/fertilizer-bg.jpg') no-repeat center center;
+  background-size: cover;
+  z-index: 0;
+}
+
+/* 底部半透明层 */
+.bottom-overlay {
+  position: absolute;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  height: 40%;
+  background: linear-gradient(to top, rgba(255, 255, 255, 0.5), transparent);
+  z-index: 1;
+}
+
+.form-section {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+}
+
+.input-group {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 20px;
+  gap: 20px;
+}
+
+.form-item {
+  flex: 1;
+  margin-bottom: 0;
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+}
+
+.el-form-item__label {
+  font-size: 16px;
+  text-align: left;
+  margin-bottom: 8px;
+  padding: 0 !important;
+  font-weight: 600;
+  color: #333;
+}
+
+.el-input {
+  width: 100%;
+}
+
+/* 使用 :deep() 替代已弃用的 >>> */
+:deep(.el-input) .el-input__inner {
+  width: 100% !important;
+  padding: 12px 0;
+  border: none;
+  border-radius: 0;
+  background: transparent;
+  /* 底部边框效果 */
+  border-bottom: 1px solid #dcdfe6;
+  /* 底部阴影效果 */
+  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.05);
+  transition: all 0.3s ease;
+}
+
+:deep(.el-input) .el-input__inner:focus {
+  border-bottom: 2px solid #409EFF;
+  /* 聚焦时底部阴影加强 */
+  box-shadow: 0 2px 0 rgba(64, 158, 255, 0.2);
+  background: rgba(64, 158, 255, 0.03);
+}
+
+/* 占位符样式 */
+:deep(.el-input) .el-input__inner::placeholder {
+  color: #a0a0a0;
+  font-style: italic;
+}
+
+.calculate-btn {
+  width: 100%;
+  max-width: 300px;
+  height: 200px;
+  border: none;
+  border-radius: 25px !important;
+  font-size: 24px;
+  font-weight: bold;
+  transition: all 0.4s ease;
+  position: relative;
+  z-index: 2; /* 确保按钮在覆盖层之上 */
+  
+  /* 渐变背景色 */
+  background: linear-gradient(to right, #8DF9F0, #26B046);
+  /* 按钮整体阴影 */
+  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15),
+              0 4px 10px rgba(38, 176, 70, 0.3) inset;
+}
+
+.calculate-btn:hover {
+  transform: scale(1.03);
+  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2),
+              0 4px 12px rgba(38, 176, 70, 0.4) inset;
+  background: linear-gradient(to right, #7de8df, #20a03d);
+}
+
+.calculate-btn:active {
+  transform: scale(0.98);
+  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15),
+              0 2px 8px rgba(38, 176, 70, 0.4) inset;
+}
+
+.btn-text {
+  position: relative;
+  color: white;
+  text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
+  font-size: 26px;
+  letter-spacing: 1px;
+  z-index: 1;
+  padding: 20px;
+  text-align: center;
+  line-height: 1.4;
+}
+</style>

+ 25 - 0
src/views/User/HmOutFlux/agriInput/samplingDesc2.vue

@@ -0,0 +1,25 @@
+<template>
+  <div class="">
+    
+  </div>
+</template>
+
+<script>
+export default {
+  name: '',
+  data() {
+    return {
+      
+    };
+  },
+  methods: {
+    
+  }
+};
+</script>
+
+<style scoped>
+  . {
+    
+  }
+</style>

+ 23 - 0
src/views/User/HmOutFlux/atmosDeposition/airInputFlux.vue

@@ -0,0 +1,23 @@
+<template>
+  <div class="">
+    
+  </div>
+</template>
+
+<script>
+export default {
+  name: '',
+  data() {
+    return {
+      
+    };
+  },
+  methods: {
+    
+  }
+};
+</script>
+
+<style scoped>
+
+</style>

+ 107 - 0
src/views/User/HmOutFlux/atmosDeposition/airSampleData.vue

@@ -0,0 +1,107 @@
+<template>
+  <div class="page-container">
+    
+   <div class="point-map">
+    <div class="component-title">采样点地图展示</div>
+    <div class="map-holder">
+      <atmsamplemap/>
+     </div>
+   </div>
+
+  
+   <div class="point-line">
+    <div class="component-title">采样点数据列表展示</div>
+    <AirsampleLine/>
+   </div>
+
+   <div>
+    <div class="component-title">各区县企业平均大气颗粒物排放(t/a)</div>
+    <AirsampleChart/>
+   </div>
+  </div>
+</template>
+<!--污染企业-->
+
+<script setup>
+import AirsampleLine from './airsampleLine.vue';
+import atmsamplemap from './atmsamplemap.vue';
+import AirsampleChart from './airsampleChart.vue';
+
+
+
+
+</script>
+
+<style scoped>
+.page-container {
+  display: flex;
+  flex-direction: column;
+  height: 100vh; /* 整屏高度 */
+  padding: 0;
+  box-sizing: border-box;
+  background-color: #f5f7fa;
+  gap: 20px;
+  margin: 0;
+}
+
+
+.map-holder {
+  position: relative;
+  height: 600px;
+  z-index: 100; /* 保持地图在中间层级 */
+  overflow: visible; /* 允许内容溢出 */
+}
+.point-map {
+    flex: 0 0 70%;
+    margin-bottom: 20px;
+    background-color: white;
+    border-radius: 8px;
+    box-shadow: 0 2px 4px rgba(0,0,0,1);
+    overflow: visible;
+}
+
+.point-line {
+    background-color: white;
+    border-radius: 12px;/*圆角 */
+    box-shadow: 0 2px 8px rgba(0, 0,0, 0.08);
+    padding: 16px;/*内部间距 */
+    box-sizing: border-box;
+}
+.charts-line{
+  background-color: white;
+  border-radius: 12px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+  padding: 16px;
+  box-sizing: border-box;
+  max-width: 1800px;
+  width: 100%;
+  margin: 20px auto;
+}
+.component-title {
+  /* 基础布局:左对齐 + 紧凑间距 */
+  text-align: left;        /* 强制左对齐,告别居中 */
+  margin: 12px 0;          /* 上下间距缩小,更紧凑(原16px→12px) */
+  padding-left: 24px;      /* 给蓝色方块留空间 */
+  position: relative;      /* 为伪元素定位做准备 */
+
+  /* 文字样式:简约但醒目 */
+  font-size: 1.7rem;      /* 稍小一号,更克制(原1.5rem→1.25rem) */
+  font-weight: 600;        /* 适度加粗,比正文突出但不夸张 */
+  color: #1e88e5;          /* 统一蓝色,和方块颜色呼应 */
+  line-height: 1.2;        /* 紧凑行高,避免臃肿 */
+}
+
+/* 蓝色小方块:用伪元素实现,无额外HTML */
+.component-title::before {
+  content: "";
+  position: absolute;
+  left: 0;                /* 靠最左侧 */
+  top: 50%;              /* 垂直居中 */
+  transform: translateY(-50%);
+  width: 12px;           /* 方块大小,适中即可 */
+  height: 12px;
+  background-color: #1e88e5; /* 和文字同色,统一感 */
+  border-radius: 2px;    /* 轻微圆角,比直角更柔和 */
+}
+
+</style>

+ 559 - 0
src/views/User/HmOutFlux/atmosDeposition/airSampleTencentMap.vue

@@ -0,0 +1,559 @@
+<template>
+  <div class="map-page">
+    <div ref="mapContainer" class="map-container"></div>
+    <!-- 错误提示 -->
+    <div v-if="error" class="error-message">{{ error }}</div>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
+import axios from 'axios'
+const isMapReady = ref(false)
+const mapContainer = ref(null)
+const error = ref(null)
+const TMap = ref(null);
+let activeTempMarker = ref(null)
+let infoWindow = ref(null)
+let map = null
+let markersLayer = null
+let soilTypeVectorLayer = null; // 土壤类型多边形图层
+let overlay = null
+const state = reactive({
+  showOverlay: false,
+  showSoilTypes: true,
+  showSurveyData: true,
+  shoeWaterSystem: true,
+  excelData: [], // 用于存储从接口获取的数据
+  lastTapTime: 0
+})
+
+const tMapConfig = reactive({
+  key: import.meta.env.VITE_TMAP_KEY, // 请替换为你的开发者密钥
+  geocoderURL: 'https://apis.map.qq.com/ws/geocoder/v1/'
+})
+
+// 加载SDK的代码保持不变...
+const loadSDK = () => {
+  return new Promise((resolve, reject) => {
+    if (window.TMap?.service?.Geocoder) {
+      console.log('SDK已缓存,直接使用');
+      TMap.value = window.TMap
+      return resolve(window.TMap)
+    }
+
+    const script = document.createElement('script')
+    script.src = `https://map.qq.com/api/gljs?v=2.exp&libraries=basic,service,vector&key=${tMapConfig.key}&callback=initTMap`
+    window.initTMap = () => {
+      if (!window.TMap?.service?.Geocoder) {
+        console.error('SDK加载后仍无效');
+        reject(new Error('地图SDK加载失败'))
+        return
+      }
+      console.log('SDK动态加载完毕');
+      TMap.value = window.TMap
+      resolve(window.TMap)
+    }
+
+    script.onerror = (err) => {
+      console.error('SDK加载报错', err);
+      reject(`地图资源加载失败: ${err.message}`)
+      document.head.removeChild(script)
+    }
+
+    document.head.appendChild(script)
+  })
+}
+
+// 初始化地图 - 保持大部分不变,增加数据加载
+const initMap = async () => {
+  try {
+    await loadSDK()
+    console.log('开始创建地图实例');
+    
+    map = new TMap.value.Map(mapContainer.value, {
+      center: new TMap.value.LatLng(24.9, 113.9),//前大往下,后大往左
+      zoom: 10,
+      minZoom: 9.25,
+      maxZoom: 11,
+      renderOptions: {
+        antialias: true
+      },
+    })
+    console.log('地图实例创建成功');
+    
+    // 创建标记点向量图层
+    markersLayer = new TMap.value.MultiMarker({
+      map: map,
+      zIndex: 1000,
+      collision:false,
+      styles: {
+        default: new TMap.value.MarkerStyle({
+          width: 15, // 图标宽度
+          height: 15, // 图标高度
+          anchor: { x: 12.5, y: 12.5 }, // 居中定位
+          src: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMCIgaGVpZ2h0PSIzMCI+PGNpcmNsZSBjeD0iMTUiIGN5PSIxNSIgcj0iMTAiIGZpbGw9InJlZCIvPjwvc3ZnPg=='
+        })
+      }
+    });
+    
+    // 绑定标记点击事件
+    markersLayer.on('click', handleMarkerClick);
+    
+    // 创建土壤类型多边形图层
+    soilTypeVectorLayer = new TMap.value.MultiPolygon({
+      map: map,
+      styles: {
+        default: new TMap.value.PolygonStyle({
+          fillColor: '#cccccc',
+          fillOpacity: 0.4,
+          strokeColor: '#333',
+          strokeWidth: 1
+        })
+      }    
+    });  
+   
+    // 先加载数据,再更新标记
+    await fetchData(); // 新增:获取数据
+    if(state.excelData.length > 0) {
+    const first = state.excelData[0];
+    console.log('第一条数据坐标:', 
+      first.纬度 || first.latitude, 
+      first.经度 || first.longitude
+    );
+  }
+    updateMarkers();   // 更新标记点
+    
+    // 标记地图就绪
+    isMapReady.value = true;
+    console.log('地图初始化完成');
+
+  } catch (err) {
+    isMapReady.value = true;
+    console.error('initMap执行异常:', err);
+    error.value = err.message
+  }
+}
+
+// 新增:从接口获取数据
+const fetchData = async () => {
+  try {
+    const response = await axios.get('http://localhost:3000/table/Atmosphere_summary_data', {
+      timeout: 100000
+    });
+
+    state.excelData = response.data.filter(item => {
+      // 检查数据完整性
+      if(!item['样品编码'] || !item.纬度 || !item.经度) {
+        console.warn(`数据不完整,已跳过: ${item.样品编码 || '未知编码'}`);
+        return false;
+      }
+      
+      const lat = Number(item.纬度);
+      const lng = Number(item.经度);
+      
+      // 验证数值范围
+      const isValid = !isNaN(lat) && !isNaN(lng) && 
+                     lat >= -90 && lat <= 90 && 
+                     lng >= -180 && lng <= 180;
+      
+      if(!isValid) {
+        console.error(`无效经纬度: ${item.样品编码} (${item.纬度}, ${item.经度})`);
+      }
+      return isValid;
+    });
+    
+    console.log('有效数据记录:', state.excelData.length);
+  } catch (err) {
+    console.error('数据请求失败详情:', err.response?.data || err.message);
+    error.value = `数据加载失败: ${err.message}`;
+    
+  }
+}
+
+// 更新标记点 - 保持不变
+const updateMarkers = () => {
+  const coordCount = new Map();
+  const geometries = state.excelData.map(item => {
+    console.log(`ID: ${item.样品编码}, 坐标: (${item.纬度}, ${item.经度})`); // 替换字段名
+    if (!item.样品编码 || !item.纬度 || !item.经度) {
+      console.error(`无效数据项: ${JSON.stringify(item)}`);
+      return null;
+    }
+    const lat = Number(item.纬度);
+    const lng = Number(item.经度);
+
+    if (isNaN(lat) || isNaN(lng)) {
+      console.error(`坐标值非数字: ${item.样品编码} (${item.纬度}, ${item.经度})`);
+      return null;
+    }
+
+    const coordKey = `${lat}_${lng}`;
+    const count = coordCount.get(coordKey) || 0;
+    coordCount.set(coordKey, count + 1);
+
+    let finalLat = lat;
+    let finalLng = lng;
+    
+    // 重复坐标添加偏移
+    if (count > 0) {
+      const latOffset = count * 0.01;  // 南北方向偏移(约11米)
+      const lngOffset = count * 0.02;
+      finalLat = lat + latOffset;
+      finalLng = lng + lngOffset;
+      
+      console.log(`偏移点 ${item.样品编码}: ${lat},${lng} → ${finalLat},${finalLng}`);
+    }
+
+    const position = new TMap.value.LatLng(finalLat, finalLng);
+
+    return {
+      id: item.样品名称,
+      styleId: 'default',
+      position:position, // 替换字段名
+      properties: {
+        title: item.采样 || `采样点 ${item.样品名称}`, 
+        sampler_id: item.样品编码,
+        originalPosition: { lat, lng }
+      }
+    };
+  })
+  
+  markersLayer.setGeometries(geometries);
+  console.log('成功添加标记点数量:', geometries.length);
+};
+
+// Marker点击事件处理 - 保持不变
+const handleMarkerClick = async (e) => {
+  console.log('点击标记点');
+  
+  const marker = e.geometry;
+  if (!marker) {
+    console.error('未获取到标记点对象');
+    return;
+  }
+
+  // 关闭之前的信息窗口
+  if (infoWindow.value) {
+    infoWindow.value.close();
+    infoWindow.value = null;
+  }
+  
+  // 显示加载中
+  infoWindow.value = new TMap.value.InfoWindow({
+    map: map,
+    position: marker.position,
+    content: '<div style="padding:12px;text-align:center">加载数据中...</div>',
+    //offset: { x: 0, y: -32 }
+  });
+  infoWindow.value.open();
+
+  try {
+    const markerId = marker.id.trim();
+    console.log('点击标记点样品名称:', markerId);
+    
+    // 直接从本地数据查找,无需二次请求
+    const matchedData = state.excelData.find(item => 
+      item.样品名称.trim() === markerId
+    );
+
+    if (!matchedData) {
+      console.error("无法匹配的数据列表:", state.excelData.map(i => i.样品名称));
+      throw new Error(`未找到样品名称为 ${markerId} 的监测数据`);
+    }
+
+    // 创建信息窗口内容
+    const content = `
+      <div class="water-info-window">
+        <h3 class="info-title">${matchedData.采样}</h3>
+        <div class="info-row">
+          <span class="info-label">采样点ID:</span>
+          <span class="info-value">${matchedData.样品名称}</span>
+        </div>
+
+        <div class="info-row">
+          <span class="info-label">样品编号:</span>
+          <span class="info-value">${matchedData.样品编号}</span>
+        </div>
+  
+        <div class="contaminant-grid" style="grid-template-columns: repeat(2, 1fr); gap: 8px;">
+
+          <div class="contaminant-item">
+            <span class="contaminant-name">Cr mg/kg:</span>
+            <span class="contaminant-value">${matchedData['Cr mg/kg']}</span>
+          </div>
+
+          <div class="contaminant-item">
+            <span class="contaminant-name">Cr ug/m3:</span>
+            <span class="contaminant-value">${matchedData['Cr ug/m3']}</span>
+          </div>
+
+          <div class="contaminant-item">
+            <span class="contaminant-name">As mg/kg:</span>
+            <span class="contaminant-value">${matchedData['As mg/kg']}</span>
+          </div>
+
+          <div class="contaminant-item">
+            <span class="contaminant-name">As ug/m3:</span>
+            <span class="contaminant-value">${matchedData['As ug/m3']}</span>
+          </div>
+          
+          <div class="contaminant-item">
+            <span class="contaminant-name">Cd mg/kg:</span>
+            <span class="contaminant-value">${matchedData['Cd mg/kg']}</span>
+          </div>
+
+          <div class="contaminant-item">
+            <span class="contaminant-name">Cd ug/m3:</span>
+            <span class="contaminant-value">${matchedData['Cd ug/m3']}</span>
+          </div>
+
+          <div class="contaminant-item">
+            <span class="contaminant-name">Hg mg/kg:</span>
+            <span class="contaminant-value">${matchedData['Hg mg/kg']}</span>
+          </div>
+
+          <div class="contaminant-item">
+            <span class="contaminant-name">Hg ug/m3:</span>
+            <span class="contaminant-value">${matchedData['Hg ug/m3']}</span>
+          </div>
+
+          <div class="contaminant-item">
+            <span class="contaminant-name">Pb mg/kg:</span>
+            <span class="contaminant-value">${matchedData['Pb mg/kg']}</span>
+          </div>
+
+          <div class="contaminant-item">
+            <span class="contaminant-name">Pb ug/m3:</span>
+            <span class="contaminant-value">${matchedData['Pb ug/m3']}</span>
+          </div>
+          
+          <div class="contaminant-item">
+            <span class="contaminant-name">颗粒物的重量 mg:</span>
+            <span class="contaminant-value">${matchedData['颗粒物的重量 mg']}</span>
+          </div>
+
+          <div class="contaminant-item">
+            <span class="contaminant-name">标准体积 m3:</span>
+            <span class="contaminant-value">${matchedData['标准体积 m3']}</span>
+          </div>
+
+          <div class="contaminant-item">
+            <span class="contaminant-name">颗粒物浓度ug/m3:</span>
+            <span class="contaminant-value">${matchedData['颗粒物浓度ug/m3']}</span>
+          </div>
+
+        </div>
+      </div>
+    `;
+    
+    // 更新信息窗口
+    infoWindow.value.setContent(content);
+    
+  } catch (error) {
+    console.error('API请求失败:', error);
+    
+    // 显示错误信息
+    const errorContent = `
+      <div style="padding:12px;color:red">
+        <h3>${marker.properties.title}</h3>
+        <p>获取数据失败: ${error.message}</p>
+      </div>
+    `;
+    
+    infoWindow.value.setContent(errorContent);
+  }
+}
+
+// 其余函数保持不变...
+const manageTempMarker = {
+  add: (lat, lng, phValue) => {
+    if (activeTempMarker.value) {
+      markersLayer.remove("-999")
+    }
+    
+    // 确保已添加临时样式
+    if (!markersLayer.getStyles().temp) {
+      markersLayer.setStyles({
+        temp: new TMap.value.MarkerStyle({
+          width: 30,
+          height: 30,
+          anchor: { x: 12.5, y: 12.5 },
+          src: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTIgMkg2Yy0xLjEgMC0yIC45LTIgMnYxNmMwIDEuMS45IDIgMiAyaDEyYzEuMSAwIDIgLS45IDItMnYtNGMwLTEuMS0uOS0yLTItMmgtMnY0aC00di00SDEyVjJ6bTAgMTZINnYtOEgxOFYxOHoiIGZpbGw9IiNGRjAwMDAiLz48L3N2Zz4='
+        })
+      });
+    }
+    
+    const tempMarker = markersLayer.add({
+      id: "-999",
+      position: new TMap.value.LatLng(lat, lng),
+      styleId: 'temp',
+      properties: {
+        title: '克里金插值',
+        phValue: parseFloat(phValue).toFixed(2),
+        isTemp: true
+      }
+    })
+    activeTempMarker.value = tempMarker
+  },
+  remove: () => {
+    if (activeTempMarker.value) {
+      markersLayer.remove("-999")
+      activeTempMarker.value = null
+    }
+  }
+}
+
+onMounted(async () => {
+  console.log('开始执行 onMounted');
+  
+  try {
+    await initMap()
+    console.log('地图初始化完成');
+  } catch (err) {
+    console.error('onMounted执行异常', err);
+    error.value = err.message
+  }
+})
+
+onBeforeUnmount(() => {
+  if (activeTempMarker.value) {
+    manageTempMarker.remove()
+  }
+  if (markersLayer) markersLayer.setMap(null)
+  if (overlay) overlay.setMap(null)
+  if (infoWindow.value) {
+    infoWindow.value.close()
+    infoWindow.value = null
+  }
+  if (soilTypeVectorLayer) soilTypeVectorLayer.setMap(null)
+})
+</script>
+
+<style>
+/* 原有样式保持不变,修改以下部分 */
+.error-message {
+  position: fixed;
+  top: 20px;
+  left: 50%;
+  transform: translateX(-50%);
+  padding: 12px 20px;
+  background-color: #ff4444;
+  color: white;
+  border-radius: 4px;
+  z-index: 9999;
+  box-shadow: 0 2px 8px rgba(0,0,0,0.2);
+  animation: fadein 0.5s, fadeout 0.5s 4.5s;
+}
+
+@keyframes fadein {
+  from { top: 0; opacity: 0; }
+  to { top: 20px; opacity: 1; }
+}
+
+@keyframes fadeout {
+  from { top: 20px; opacity: 1; }
+  to { top: 0; opacity: 0; }
+}
+
+.map-page {
+  position: relative;
+  width: 100vw;
+  height: 100vh;
+}
+
+.map-container {
+  width: 100%;
+  height: 100vh ;
+  min-height: 600px;
+  pointer-events: all;
+}
+
+.contaminants {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  gap: 2px;
+}
+
+/* 窗口容器:精准控制尺寸 */
+.water-info-window {
+  max-width: 340px !important;
+  width: 100%;
+  height: auto;
+  padding: 8px;
+  box-sizing: border-box;
+  background: #FFFFFF;
+  border-radius: 8px;
+  box-shadow: 0 3px 12px rgba(0, 32, 71, 0.1);
+  border: 1px solid #e5e7eb;
+  overflow: hidden !important;
+}
+
+/* 标题区样式 */
+.info-title {
+  font-size: 0.9rem;
+  padding: 6px 8px;
+  letter-spacing: 0.2px;
+  border-bottom: 1px solid #f1f2f6;
+  margin: 0 0 8px 0;
+}
+
+/* 基础数据行 */
+.info-row {
+  display: flex;
+  align-items: center;
+  margin: 4px 0;
+  padding-left: 8px;
+}
+
+.info-label {
+  font-size: 0.8rem;
+  padding-right: 6px;
+  flex: 0 0 80px;
+}
+
+.info-value {
+  font-size: 0.8rem;
+  padding: 2px 6px;
+  border-left: 2px solid #e5e7eb;
+}
+
+/* 污染物网格:优化布局使名称和数值在同一行并放大字体 */
+.contaminant-grid {
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  gap: 4px; /* 减小间距以补偿字体增大 */
+  margin: 4px 0;
+}
+
+/* 污染物项:确保名称和数值在同一行 */
+.contaminant-item {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 3px 5px; /* 减小内边距 */
+  background: #f9fafb;
+  border-radius: 4px;
+}
+
+/* 增大字体大小,保持在同一行 */
+.contaminant-name {
+  font-size: 0.8rem; /* 增大字体 */
+  color: #6b7280;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  flex: 1;
+  margin-right: 5px;
+}
+
+.contaminant-value {
+  font-size: 0.8rem; /* 增大字体 */
+  background: #e5e7eb;
+  padding: 2px 6px;
+  border-radius: 3px;
+  min-width: 40px;
+  text-align: center;
+  flex-shrink: 0;
+}
+</style>

+ 164 - 0
src/views/User/HmOutFlux/atmosDeposition/airsampleChart.vue

@@ -0,0 +1,164 @@
+<template>
+  <div class="atmosphere-summary">
+    <!-- ECharts 容器,需设置宽高 -->
+    <div id="chart" style="width: 1000px; height: 500px;"></div>
+  </div>
+</template>
+
+<script setup>
+import { onMounted } from 'vue'
+import axios from 'axios'
+import * as echarts from 'echarts'
+
+onMounted(async () => {
+  try {
+    // 1. 请求接口数据
+    const response = await axios.get('http://localhost:3000/table/Atmosphere_summary_data')
+    const samples = response.data 
+
+    if (!Array.isArray(samples) || samples.length === 0) {
+      console.warn('无有效样本数据')
+      return
+    }
+
+    // 2. 定义需计算平均值的指标(根据实际数据字段调整)
+    const metrics = [
+      'Cr mg/kg',
+      'As mg/kg',
+      'Cd mg/kg',
+      'Hg mg/kg',
+      'Pb mg/kg',
+      '颗粒物的重量 mg',
+      '标准体积 m3',
+      '颗粒物浓度ug/m3',
+      'Cr ug/m3',
+      'As ug/m3',
+      'Cd ug/m3',
+      'Hg ug/m3',
+      'Pb ug/m3'
+    ]
+
+    // 3. 计算总和与平均值
+    const sumMap = metrics.reduce((acc, key) => {
+      acc[key] = 0
+      return acc
+    }, {})
+
+    samples.forEach(sample => {
+      metrics.forEach(key => {
+        const value = parseFloat(sample[key])
+        if (!isNaN(value)) { // 过滤无效值
+          sumMap[key] += value
+        }
+      })
+    })
+
+    const averageMap = {}
+    const sampleCount = samples.length
+    metrics.forEach(key => {
+      averageMap[key] = sampleCount > 0 
+      ?Number((sumMap[key] / sampleCount).toFixed(5)):0; 
+      
+    })
+
+    // 4. 处理 ECharts 数据格式
+    const xAxisData = Object.keys(averageMap)
+    const seriesData = Object.values(averageMap)
+
+    // 5. 初始化 ECharts 并渲染
+    const chartDom = document.getElementById('chart')
+    if (chartDom) {
+      const myChart = echarts.init(chartDom)
+      const option = {
+        title: { text: '空气颗粒物样本数据平均值' },
+        xAxis: {
+          type: 'category',
+          data: xAxisData,
+          axisLabel: {
+            rotate: 30,    // 减小旋转角度(如从 45→30),让标签更水平
+            fontSize: 14,
+            interval:0,
+          },
+          axisTick:{
+            shoe:true//显示刻度线
+          },
+          axisLine:{
+            lineStyle:{
+                width:1.5//加粗轴线
+            }
+          }
+        },
+        yAxis: { 
+            type: 'value' ,
+            axisLabel:{
+            fontSize:14,
+            },
+            axisLine:{
+                lineStyle:{
+                    width:1.5
+                }
+            },
+            splitLine:{
+                lineStyle:{
+                    opacity:0.3
+                }
+            }
+
+        },
+        series: [
+          {
+            name: '平均值',
+            type: 'bar',
+            data: seriesData,
+            barWidth:'60%',
+            label: {
+              show: true, // 显示柱顶数值
+              position: 'top',
+              fontSize:14,
+              fontWeight:'bold'
+            },
+            itemStyle: {
+               // 使用回调函数为每个柱子设置不同颜色
+          color: (params) => {
+            // 定义一组不同的颜色
+            const colors = [
+              '#66CCFF', '#FF9966', '#99CC99', '#CC99CC', 
+              '#FFCC66', '#6699CC', '#FF6666', '#99CC66', 
+              '#CC66CC', '#66CC66', '#FF99CC', '#6666CC', '#CC9999'
+            ];
+            // 根据数据索引选择颜色
+            return colors[params.dataIndex % colors.length];
+          }
+            }
+          }
+        ],
+        tooltip: { // 添加 tooltip 增强交互
+        trigger: 'axis',
+        axisPointer: {
+          type: 'shadow'
+        },
+        textStyle: {
+          fontSize: 14 // 增大 tooltip 字体
+        }
+      },
+      grid: { // 调整图表边距
+        left: '5%',
+        right: '5%',
+        bottom: '15%', // 为 x 轴标签留出更多空间
+        top: '10%',
+        containLabel: true
+      }
+        }
+      myChart.setOption(option)
+    }
+  } catch (error) {
+    console.error('数据请求或处理失败:', error)
+  }
+})
+</script>
+
+<style scoped>
+.atmosphere-summary {
+  padding: 20px;
+}
+</style>

+ 231 - 0
src/views/User/HmOutFlux/atmosDeposition/airsampleLine.vue

@@ -0,0 +1,231 @@
+<template>
+  <div class="container mx-auto px-4 py-8">
+    <div class="bg-white rounded-xl shadow-lg overflow-hidden">
+      <div class="p-6 border-b border-gray-200">
+        <h1 class="text-[clamp(1rem,3vw,2.5rem)] font-bold text-gray-800 text-center">大气污染采样点列表</h1>
+      </div>
+      
+      <!-- 加载状态 -->
+      <div v-if="loading" class="py-20 flex justify-center items-center">
+        <div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
+      </div>
+      
+      <!-- 错误状态 -->
+      <div v-else-if="error" class="p-8 bg-red-50 border-l-4 border-red-400 text-red-700">
+        <div class="flex">
+          <div class="flex-shrink-0">
+            <i class="fa fa-exclamation-triangle text-red-500 text-xl"></i>
+          </div>
+          <div class="ml-3">
+            <h3 class="text-sm font-medium text-red-800">加载失败</h3>
+            <div class="mt-2 text-sm text-red-700">
+              <p>{{ error }}</p>
+            </div>
+            <div class="mt-4">
+              <button @click="fetchData" class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition duration-150 ease-in-out">
+                <i class="fa fa-refresh mr-2"></i>重试
+              </button>
+            </div>
+          </div>
+        </div>
+      </div>
+      
+      <!-- 数据表格 -->
+      <div v-else-if="filteredData.length > 0" class="overflow-x-auto">
+        <table class="min-w-full divide-y divide-gray-200">
+          <thead class="bg-gray-50">
+            <tr>
+              <th 
+                v-for="(col, index) in displayColumns" 
+                :key="index"
+                class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 transition-colors"
+                @click="sortData(col.key)"
+              >
+                <div class="flex items-center justify-between">
+                  {{ col.label }}
+                  <span v-if="sortKey === col.key" class="ml-1 text-gray-400">
+                    {{ sortOrder === 'asc' ? '↑' : '↓' }}
+                  </span>
+                </div>
+              </th>
+            </tr>
+          </thead>
+          <tbody class="bg-white divide-y divide-gray-200">
+            <tr v-for="(item, rowIndex) in sortedData" :key="rowIndex" 
+                class="hover:bg-gray-50 transition-colors duration-150">
+              <td 
+                v-for="(col, colIndex) in displayColumns" 
+                :key="colIndex"
+                class="px-6 py-4 whitespace-nowrap text-sm"
+              >
+                <div class="flex items-center">
+                  <div class="text-gray-900 font-medium">
+                    {{ item[col.key] !== null ? item[col.key] : '-' }}
+                  </div>
+                </div>
+              </td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+      
+      <!-- 空数据状态 -->
+      <div v-else class="p-8 text-center">
+        <div class="flex flex-col items-center justify-center">
+          <div class="text-gray-400 mb-4">
+            <i class="fa fa-database text-5xl"></i>
+          </div>
+          <h3 class="text-lg font-medium text-gray-900 mb-1">暂无有效数据</h3>
+          <p class="text-gray-500">已过滤全空行</p>
+        </div>
+      </div>
+  
+      <!-- 数据表格 + 统计 -->
+  <div class="p-4 bg-gray-50 border-t border-gray-200">
+    <div class="flex flex-col md:flex-row justify-between items-center">
+      <div class="text-sm text-gray-500 mb-2 md:mb-0">
+        共 <span class="font-medium text-gray-900">{{ filteredData.length }}</span> 条数据
+      </div>
+    </div>
+</div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, onMounted } from 'vue';
+import axios from 'axios';
+
+// 定义固定列配置
+const displayColumns = ref([
+  { key: '采样', label: '采样位置' },
+  { key: '样品名称', label: '样品名称' },
+  { key: 'Cr mg/kg', label: 'Cr mg/kg' },
+  { key: 'As mg/kg', label: 'As mg/kg' },
+  { key: 'Cd mg/kg', label: 'Cd mg/kg' },
+  { key: 'Hg mg/kg', label: 'Hg mg/kg' },
+  { key: 'Pb mg/kg', label: 'Pb mg/kg' },
+  { key: '颗粒物的重量 mg', label: '颗粒物的重量 mg' },
+  { key: '标准体积 m3', label: '标准体积 m3' },
+  { key: '颗粒物浓度ug/m3', label: '颗粒物浓度ug/m3' },
+  { key: 'Cr ug/m3', label: 'Cr ug/m3' },
+  { key: 'As ug/m3', label: 'As ug/m3' },
+  { key: 'Cd ug/m3', label: 'Cd ug/m3' },
+  { key: 'Hg ug/m3', label: 'Hg ug/m3' },
+  { key: 'Pb ug/m3', label: 'Pb ug/m3' },
+  
+]);
+
+// 状态管理
+const waterData = ref([]);
+const loading = ref(true);
+const error = ref(null);
+const sortKey = ref('');
+const sortOrder = ref('asc');
+
+// 获取数据
+const fetchData = async () => {
+  try {
+    loading.value = true;
+    error.value = null;
+    const response = await axios.get('http://localhost:3000/table/Atmosphere_summary_data');
+    waterData.value = response.data.data || response.data;
+  } catch (err) {
+    error.value = err.message || '无法连接到服务器,请检查接口是否可用';
+  } finally {
+    loading.value = false;
+  }
+};
+
+// 过滤全空行
+const filteredData = computed(() => {
+  return waterData.value.filter(item => {
+    return displayColumns.value.some(col => item[col.key] !== null && item[col.key] !== '-');
+  });
+});
+
+// 排序功能
+const sortedData = computed(() => {
+  if (!sortKey.value) return filteredData.value;
+  
+  return [...filteredData.value].sort((a, b) => {
+    const valA = a[sortKey.value];
+    const valB = b[sortKey.value];
+    if (valA < valB) return sortOrder.value === 'asc' ? -1 : 1;
+    if (valA > valB) return sortOrder.value === 'asc' ? 1 : -1;
+    return 0;
+  });
+});
+
+// 切换排序
+const sortData = (key) => {
+  if (sortKey.value === key) {
+    sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc';
+  } else {
+    sortKey.value = key;
+    sortOrder.value = 'asc';
+  }
+};
+
+// 组件挂载
+onMounted(() => {
+  fetchData();
+});
+</script>
+
+<style scoped>
+/* 布局 */
+.container {
+  width: 100%;
+  margin: 0 auto;
+  padding: 32px 16px;
+}
+.overflow-x-auto { overflow-x: auto; }
+
+/* 卡片 */
+.bg-white { background-color: #fff; }
+.rounded-xl { border-radius: 1rem; }
+.shadow-lg { 
+  box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1), 
+             0 4px 6px -4px rgba(0,0,0,0.1); 
+}
+
+/* 文字 */
+.text-center { text-align: center; }
+.text-lg { font-size: 1.125rem; }
+.font-bold { font-weight: 700; }
+.text-gray-800 { color: #111827; }
+
+/* 动画 */
+.animate-spin {
+  animation: spin 1s linear infinite;
+}
+@keyframes spin {
+  from { transform: rotate(0deg); }
+  to { transform: rotate(360deg); }
+}
+
+/* 表格 */
+table { width: 100%; }
+.px-6 { padding: 0 1.5rem; }
+.py-4 { padding: 1rem 0; }
+.hover\:bg-gray-50:hover { background-color: #f9fafb; }
+
+/* 响应式 */
+@media (max-width: 640px) {
+  .container {
+     padding: 32px 8px; 
+     max-width: 1600px;
+    }
+  .px-6 { padding: 0 0.75rem; }
+}
+table {
+  border-collapse: collapse; /* 合并边框线 */
+}
+th, td {
+  border: 1px solid #d1d5db; /* 灰色边框 */
+  text-align: center; /* 内容居中 */
+  padding: 12px 8px; /* 内边距优化 */
+  min-width: 120px;
+}
+</style>

+ 479 - 0
src/views/User/HmOutFlux/atmosDeposition/atmCompanytencentMap.vue

@@ -0,0 +1,479 @@
+<template>
+  <div class="map-page">
+    <div ref="mapContainer" class="map-container"></div>
+    <!-- 错误提示 -->
+    <div v-if="error" class="error-message">{{ error }}</div>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
+import axios from 'axios'
+const isMapReady = ref(false)
+const mapContainer = ref(null)
+const error = ref(null)
+const TMap = ref(null);
+let activeTempMarker = ref(null)
+let infoWindow = ref(null)
+let map = null
+let markersLayer = null
+let soilTypeVectorLayer = null; // 土壤类型多边形图层
+let overlay = null
+const state = reactive({
+  showOverlay: false,
+  showSoilTypes: true,
+  showSurveyData: true,
+  shoeWaterSystem: true,
+  excelData: [], // 用于存储从接口获取的数据
+  lastTapTime: 0
+})
+let soilTypeLayer = null
+let currentInfoWindow = null;
+let surveyDataLayer = ref(null);
+let selectedPolygon = ref(null); // 补充定义
+
+const tMapConfig = reactive({
+  key: import.meta.env.VITE_TMAP_KEY, // 请替换为你的开发者密钥
+  geocoderURL: 'https://apis.map.qq.com/ws/geocoder/v1/'
+})
+
+// 加载SDK的代码保持不变...
+const loadSDK = () => {
+  return new Promise((resolve, reject) => {
+    if (window.TMap?.service?.Geocoder) {
+      //console.log('SDK已缓存,直接使用');
+      TMap.value = window.TMap
+      return resolve(window.TMap)
+    }
+
+    const script = document.createElement('script')
+    script.src = `https://map.qq.com/api/gljs?v=2.exp&libraries=basic,service,vector&key=${tMapConfig.key}&callback=initTMap`
+    window.initTMap = () => {
+      if (!window.TMap?.service?.Geocoder) {
+        console.error('SDK加载后仍无效');
+        reject(new Error('地图SDK加载失败'))
+        return
+      }
+      //console.log('SDK动态加载完毕');
+      TMap.value = window.TMap
+      resolve(window.TMap)
+    }
+
+    script.onerror = (err) => {
+      console.error('SDK加载报错', err);
+      reject(`地图资源加载失败: ${err.message}`)
+      document.head.removeChild(script)
+    }
+
+    document.head.appendChild(script)
+  })
+}
+
+// 初始化地图 - 保持大部分不变,增加数据加载
+const initMap = async () => {
+  try {
+    await loadSDK()
+    //console.log('开始创建地图实例');
+    
+    map = new TMap.value.Map(mapContainer.value, {
+      center: new TMap.value.LatLng(24.39, 114),//前大往下,后大往左
+      zoom: 9.25,
+      minZoom: 8,
+      //maxZoom: 11,
+      renderOptions: {
+        antialias: true
+      },
+    })
+    //console.log('地图实例创建成功');
+    
+    // 创建标记点向量图层
+    markersLayer = new TMap.value.MultiMarker({
+      map: map,
+      zIndex: 1000,
+      collision:false,
+      styles: {
+        default: new TMap.value.MarkerStyle({
+          width: 30, // 图标宽度
+          height: 30, // 图标高度
+          anchor: { x: 12.5, y: 12.5 }, // 居中定位
+          src: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cG9seWdvbiBwb2ludHM9IjEyLDIgMiwyMCAyMiwyMCIgZmlsbD0iIzFFODhFNSIvPjwvc3ZnPg=='
+        })
+      }
+    });
+    
+    // 绑定标记点击事件
+    markersLayer.on('click', handleMarkerClick);
+    
+    // 创建土壤类型多边形图层
+    soilTypeVectorLayer = new TMap.value.MultiPolygon({
+      map: map,
+      styles: {
+        default: new TMap.value.PolygonStyle({
+          fillColor: '#cccccc',
+          fillOpacity: 0.4,
+          strokeColor: '#333',
+          strokeWidth: 1
+        })
+      }    
+    });  
+   
+    // 先加载数据,再更新标记
+    await fetchData(); // 新增:获取数据
+    if(state.excelData.length > 0) {
+    const first = state.excelData[0];
+   // console.log('第一条数据坐标:', 
+     // first.纬度 || first.latitude, 
+      //first.经度 || first.longitude
+    //);
+  }
+    updateMarkers();   // 更新标记点
+    
+    // 标记地图就绪
+    isMapReady.value = true;
+    //console.log('地图初始化完成');
+
+    // 创建样式标签并注入样式
+    const style = document.createElement('style');
+    style.textContent = `
+      .water-info-window {
+        max-width: 80vw !important;
+        width: auto !important;
+        overflow: visible !important;
+      }
+      
+      .info-value {
+        white-space: normal !important;
+        word-wrap: break-word !important;
+        max-width: none !important;
+      }
+    `;
+    document.head.appendChild(style);
+
+  } catch (err) {
+    isMapReady.value = true;
+    console.error('initMap执行异常:', err);
+    error.value = err.message
+  }
+}
+
+// 新增:从接口获取数据
+const fetchData = async () => {
+  try {
+    const response = await axios.get('http://localhost:3000/table/Atmosphere_company_data', {
+      timeout: 100000
+    });
+
+    state.excelData = response.data.filter(item => {
+      if(!item['污染源序号'] || !item.纬度 || !item.经度) { // 替换为新字段
+        console.warn(`数据不完整,已跳过: ${item.污染源序号 || '未知序号'}`);
+        return false;
+      }  
+      const lat = Number(item.纬度);
+      const lng = Number(item.经度);  
+      const isValid = !isNaN(lat) && !isNaN(lng) && lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180;  
+      if(!isValid) console.error(`无效经纬度: ${item.污染源序号} (${item.纬度}, ${item.经度})`);  
+      return isValid;
+    });
+    
+    //console.log('有效数据记录:', state.excelData.length);
+  } catch (err) {
+    console.error('数据请求失败详情:', err.response?.data || err.message);
+    error.value = `数据加载失败: ${err.message}`;
+    
+  }
+}
+
+// 更新标记点 - 保持不变
+const updateMarkers = () => {
+  const geometries = state.excelData.map(item => {  
+  //console.log(`ID: ${item.污染源序号}, 坐标: (${item.纬度}, ${item.经度})`); // 替换为新字段  
+  if (!item.污染源序号 || !item.纬度 || !item.经度) return null;  
+  const lat = Number(item.纬度);
+  const lng = Number(item.经度);  
+  if (isNaN(lat) || isNaN(lng)) return null;  
+
+  return {
+    id: item.污染源序号, // 标记 ID 设为「污染源序号」
+    styleId: 'default',
+    position: new TMap.value.LatLng(lat, lng), 
+    properties: {
+      title: item.公司 || `污染源 ${item.污染源序号}`, // 标题用「公司」名称
+      sampler_id: item.污染源序号,
+    }
+  };
+  });
+  
+  markersLayer.setGeometries(geometries);
+  //console.log('成功添加标记点数量:', geometries.length);
+};
+
+// Marker点击事件处理 - 保持不变
+const handleMarkerClick = async (e) => {
+  //console.log('点击标记点');
+  
+  const marker = e.geometry;
+  if (!marker) {
+    console.error('未获取到标记点对象');
+    return;
+  }
+
+  // 关闭之前的信息窗口
+  if (infoWindow.value) {
+    infoWindow.value.close();
+    infoWindow.value = null;
+  }
+  
+  // 显示加载中
+  infoWindow.value = new TMap.value.InfoWindow({
+    map: map,
+    position: marker.position,
+    content: '<div style="padding:12px;text-align:center">加载数据中...</div>',
+    //offset: { x: 0, y: -220 },
+    
+  });
+  infoWindow.value.open();
+
+  try {
+    const markerId = marker.id.trim();
+    //console.log('点击标记点样品名称:', markerId);
+    
+    // 直接从本地数据查找,无需二次请求
+    const matchedData = state.excelData.find(item => 
+      item.污染源序号.trim() === markerId
+    );
+
+    if (!matchedData) {
+      console.error("无法匹配的数据列表:", state.excelData.map(i => i.样品名称));
+      throw new Error(`未找到样品名称为 ${markerId} 的监测数据`);
+    }
+
+    // 创建信息窗口内容
+    const content = `
+  <div class="water-info-window">
+    <h3 class="info-title">${matchedData.公司}</h3>
+
+    <div class="info-row">
+      <span class="info-label">污染源序号:</span>
+      <span class="info-value">${matchedData.污染源序号}</span>
+    </div>
+
+    <div class="info-row">
+      <span class="info-label">所属区县:</span>
+      <span class="info-value">${matchedData.所属区县}</span>
+    </div>
+
+    <div class="info-row">
+      <span class="info-label">类型:</span>
+      <span class="info-value">${matchedData.类型}</span>
+    </div>
+
+    <div class="info-row">
+      <span class="info-label">大气颗粒物排放(t/a):</span>
+      <span class="info-value">${matchedData['大气颗粒物排放(t/a)']}</span>
+    </div>
+
+  </div>
+`;
+    
+    // 更新信息窗口
+    infoWindow.value.setContent(content);
+    
+  } catch (error) {
+    console.error('API请求失败:', error);
+    
+    // 显示错误信息
+    const errorContent = `
+      <div style="padding:12px;color:red">
+        <h3>${marker.properties.title}</h3>
+        <p>获取数据失败: ${error.message}</p>
+      </div>
+    `;
+    
+    infoWindow.value.setContent(errorContent);
+  }
+}
+
+// 其余函数保持不变...
+const manageTempMarker = {
+  add: (lat, lng, phValue) => {
+    if (activeTempMarker.value) {
+      markersLayer.remove("-999")
+    }
+    
+    // 确保已添加临时样式
+    if (!markersLayer.getStyles().temp) {
+      markersLayer.setStyles({
+        temp: new TMap.value.MarkerStyle({
+          width: 30,
+          height: 30,
+          anchor: { x: 12.5, y: 12.5 },
+          src: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cG9seWdvbiBwb2ludHM9IjEyLDIgMiwyMCAyMiwyMCIgZmlsbD0iIzFFODhFNSIvPjwvc3ZnPg=='
+        })
+      });
+    }
+    
+    const tempMarker = markersLayer.add({
+      id: "-999",
+      position: new TMap.value.LatLng(lat, lng),
+      styleId: 'temp',
+      properties: {
+        title: '克里金插值',
+        phValue: parseFloat(phValue).toFixed(2),
+        isTemp: true
+      }
+    })
+    activeTempMarker.value = tempMarker
+  },
+  remove: () => {
+    if (activeTempMarker.value) {
+      markersLayer.remove("-999")
+      activeTempMarker.value = null
+    }
+  }
+}
+
+onMounted(async () => {
+  //console.log('开始执行 onMounted');
+  
+  try {
+    await initMap()
+    //console.log('地图初始化完成');
+  } catch (err) {
+    console.error('onMounted执行异常', err);
+    error.value = err.message
+  }
+})
+
+onBeforeUnmount(() => {
+  if (activeTempMarker.value) {
+    manageTempMarker.remove()
+  }
+  if (markersLayer) markersLayer.setMap(null)
+  if (overlay) overlay.setMap(null)
+  if (infoWindow.value) {
+    infoWindow.value.close()
+    infoWindow.value = null
+  }
+  if (soilTypeVectorLayer) soilTypeVectorLayer.setMap(null)
+})
+</script>
+
+<style>
+/* 原有样式保持不变,新增错误提示样式 */
+.error-message {
+  position: fixed;
+  top: 20px;
+  left: 50%;
+  transform: translateX(-50%);
+  padding: 12px 20px;
+  background-color: #ff4444;
+  color: white;
+  border-radius: 4px;
+  z-index: 9999;
+  box-shadow: 0 2px 8px rgba(0,0,0,0.2);
+  animation: fadein 0.5s, fadeout 0.5s 4.5s;
+}
+
+@keyframes fadein {
+  from { top: 0; opacity: 0; }
+  to { top: 20px; opacity: 1; }
+}
+
+@keyframes fadeout {
+  from { top: 20px; opacity: 1; }
+  to { top: 0; opacity: 0; }
+}
+
+/* 其余样式保持不变 */
+.map-page {
+  position: relative;
+  width: 100vw;
+  height: 100vh;
+}
+
+.map-container {
+  width: 100%;
+  height: 100vh ;
+  min-height: 600px;
+  pointer-events: all;
+}
+
+
+
+.water-info-window {
+  max-width: 80vw; /* 相对视口最大值,更加灵活 */
+  width: auto;
+  padding: 16px;
+  border-radius: 12px;
+  box-shadow: 0 6px 18px rgba(0,0,0,0.15);
+  background: #fff;
+  border: 1px solid #e5e7eb;
+  overflow: visible; /* 配合自动平移,确保内容不被裁剪 */
+}
+
+/* 标题区:增加渐变装饰线 */
+.info-title {
+  font-size: 18px;
+  font-weight: 600;
+  color: #1e3a8a;
+  text-align: center;
+  margin-bottom: 14px;
+  padding-bottom: 12px;
+  border-bottom: 1px solid #e5e7eb;
+  position: relative; /* 为伪元素准备 */
+}
+.info-title::after {
+  content: "";
+  position: absolute;
+  bottom: 0;
+  left: 20px;
+  right: 20px;
+  height: 1px;
+  background: linear-gradient(to right, 
+    rgba(30, 58, 138, 0.1), 
+    rgba(30, 58, 138, 0.2), 
+    rgba(30, 58, 138, 0.1)
+  );
+}
+
+/* 数据行:Grid 布局确保对齐 */
+.info-row {
+  display: grid;
+  grid-template-columns: 180px 1fr; /* 标签180px,值自适应 */
+  gap: 12px; /* 列间距 */
+  align-items: flex-start;
+  margin: 10px 0;
+  margin: 10px 0;/**增加行间距,提升可读性 */
+}
+
+/* 标签:右对齐 + 深灰配色 */
+.info-label {
+  text-align: right;
+  color: #6b7280;
+  font-size: 14px;
+}
+
+/* 数据值:浅灰背景 + 智能换行 */
+.info-value {
+  padding: 6px 10px;
+  background: #f3f4f6;
+  border-radius: 6px;
+  font-size: 14px;
+  white-space: normal;   /* 长文本强制换行 */
+  word-wrap: break-word;
+  overflow: visible;/**不隐藏溢出内容 */
+  text-overflow: clip;
+  min-height: 24px;        /* 确保最小高度,避免内容塌陷 */
+}
+
+/* 响应式适配(小窗口自动压缩) */
+@media (max-width: 480px) {
+  .water-info-window {
+    max-width: 90vw; /* 占满视口宽度 */
+    padding: 10px;
+  }
+  .info-row {
+    grid-template-columns: 100px 1fr; /* 缩小标签宽度 */
+  }
+}
+</style>

+ 218 - 0
src/views/User/HmOutFlux/atmosDeposition/atmcompanyline.vue

@@ -0,0 +1,218 @@
+<template>
+  <div class="container mx-auto px-4 py-8">
+    <div class="bg-white rounded-xl shadow-lg overflow-hidden">
+      <div class="p-6 border-b border-gray-200">
+        <h1 class="text-[clamp(1rem,3vw,2.5rem)] font-bold text-gray-800 text-center">大气污染公司列表</h1>
+      </div>
+      
+      <!-- 加载状态 -->
+      <div v-if="loading" class="py-20 flex justify-center items-center">
+        <div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
+      </div>
+      
+      <!-- 错误状态 -->
+      <div v-else-if="error" class="p-8 bg-red-50 border-l-4 border-red-400 text-red-700">
+        <div class="flex">
+          <div class="flex-shrink-0">
+            <i class="fa fa-exclamation-triangle text-red-500 text-xl"></i>
+          </div>
+          <div class="ml-3">
+            <h3 class="text-sm font-medium text-red-800">加载失败</h3>
+            <div class="mt-2 text-sm text-red-700">
+              <p>{{ error }}</p>
+            </div>
+            <div class="mt-4">
+              <button @click="fetchData" class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition duration-150 ease-in-out">
+                <i class="fa fa-refresh mr-2"></i>重试
+              </button>
+            </div>
+          </div>
+        </div>
+      </div>
+      
+      <!-- 数据表格 -->
+      <div v-else-if="filteredData.length > 0" class="overflow-x-auto">
+        <table class="min-w-full divide-y divide-gray-200">
+          <thead class="bg-gray-50">
+            <tr>
+              <th 
+                v-for="(col, index) in displayColumns" 
+                :key="index"
+                class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 transition-colors"
+                @click="sortData(col.key)"
+              >
+                <div class="flex items-center justify-between">
+                  {{ col.label }}
+                  <span v-if="sortKey === col.key" class="ml-1 text-gray-400">
+                    {{ sortOrder === 'asc' ? '↑' : '↓' }}
+                  </span>
+                </div>
+              </th>
+            </tr>
+          </thead>
+          <tbody class="bg-white divide-y divide-gray-200">
+            <tr v-for="(item, rowIndex) in sortedData" :key="rowIndex" 
+                class="hover:bg-gray-50 transition-colors duration-150">
+              <td 
+                v-for="(col, colIndex) in displayColumns" 
+                :key="colIndex"
+                class="px-6 py-4 whitespace-nowrap text-sm"
+              >
+                <div class="flex items-center">
+                  <div class="text-gray-900 font-medium">
+                    {{ item[col.key] !== null ? item[col.key] : '-' }}
+                  </div>
+                </div>
+              </td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+      
+      <!-- 空数据状态 -->
+      <div v-else class="p-8 text-center">
+        <div class="flex flex-col items-center justify-center">
+          <div class="text-gray-400 mb-4">
+            <i class="fa fa-database text-5xl"></i>
+          </div>
+          <h3 class="text-lg font-medium text-gray-900 mb-1">暂无有效数据</h3>
+          <p class="text-gray-500">已过滤全空行</p>
+        </div>
+      </div>
+  
+      <!-- 数据表格 + 统计 -->
+  <div class="p-4 bg-gray-50 border-t border-gray-200">
+    <div class="flex flex-col md:flex-row justify-between items-center">
+      <div class="text-sm text-gray-500 mb-2 md:mb-0">
+        共 <span class="font-medium text-gray-900">{{ filteredData.length }}</span> 条数据
+      </div>
+    </div>
+</div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, onMounted } from 'vue';
+import axios from 'axios';
+
+// 定义固定列配置
+const displayColumns = ref([
+  { key: '污染源序号', label: '污染源序号' },
+  { key: '公司', label: '公司' },
+  { key: '类型', label: '类型' },
+  { key: '所属区县', label: '所属区县' },
+  { key: '大气颗粒物排放(t/a)', label: '大气颗粒物排放(t/a)' },
+  { key: '经度', label: '经度' },
+  { key: '纬度', label: '纬度' },
+]);
+
+// 状态管理
+const waterData = ref([]);
+const loading = ref(true);
+const error = ref(null);
+const sortKey = ref('');
+const sortOrder = ref('asc');
+
+// 获取数据
+const fetchData = async () => {
+  try {
+    loading.value = true;
+    error.value = null;
+    const response = await axios.get('http://localhost:3000/table/Atmosphere_company_data');
+    waterData.value = response.data.data || response.data;
+  } catch (err) {
+    error.value = err.message || '无法连接到服务器,请检查接口是否可用';
+  } finally {
+    loading.value = false;
+  }
+};
+
+// 过滤全空行
+const filteredData = computed(() => {
+  return waterData.value.filter(item => {
+    return displayColumns.value.some(col => item[col.key] !== null && item[col.key] !== '-');
+  });
+});
+
+// 排序功能
+const sortedData = computed(() => {
+  if (!sortKey.value) return filteredData.value;
+  
+  return [...filteredData.value].sort((a, b) => {
+    const valA = a[sortKey.value];
+    const valB = b[sortKey.value];
+    if (valA < valB) return sortOrder.value === 'asc' ? -1 : 1;
+    if (valA > valB) return sortOrder.value === 'asc' ? 1 : -1;
+    return 0;
+  });
+});
+
+// 切换排序
+const sortData = (key) => {
+  if (sortKey.value === key) {
+    sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc';
+  } else {
+    sortKey.value = key;
+    sortOrder.value = 'asc';
+  }
+};
+
+// 组件挂载
+onMounted(() => {
+  fetchData();
+});
+</script>
+
+<style scoped>
+/* 布局 */
+.container {
+  max-width: 1280px;
+  margin: 0 auto;
+  padding: 32px 16px;
+}
+.overflow-x-auto { overflow-x: auto; }
+
+/* 卡片 */
+.bg-white { background-color: #fff; }
+.rounded-xl { border-radius: 1rem; }
+.shadow-lg { 
+  box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1), 
+             0 4px 6px -4px rgba(0,0,0,0.1); 
+}
+
+/* 文字 */
+.text-center { text-align: center; }
+.text-lg { font-size: 1.125rem; }
+.font-bold { font-weight: 700; }
+.text-gray-800 { color: #111827; }
+
+/* 动画 */
+.animate-spin {
+  animation: spin 1s linear infinite;
+}
+@keyframes spin {
+  from { transform: rotate(0deg); }
+  to { transform: rotate(360deg); }
+}
+
+/* 表格 */
+table { width: 100%; }
+.px-6 { padding: 0 1.5rem; }
+.py-4 { padding: 1rem 0; }
+.hover\:bg-gray-50:hover { background-color: #f9fafb; }
+
+/* 响应式 */
+@media (max-width: 640px) {
+  .container { padding: 32px 8px; }
+  .px-6 { padding: 0 0.75rem; }
+}
+table {
+  border-collapse: collapse; /* 合并边框线 */
+}
+th, td {
+  border: 1px solid #d1d5db; /* 灰色边框 */
+  text-align: center; /* 内容居中 */
+  padding: 12px 8px; /* 内边距优化 */
+}
+</style>

+ 239 - 0
src/views/User/HmOutFlux/atmosDeposition/atmcompanymap.vue

@@ -0,0 +1,239 @@
+<template>
+  <div class="map-wrapper">
+    <div ref="mapContainer" class="map-container"></div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue';
+import L from 'leaflet';
+import 'leaflet/dist/leaflet.css';
+
+const mapContainer = ref(null);
+
+// 定义蓝色三角形标记(保持不变)
+const blueTriangle = L.divIcon({
+  className: 'custom-div-icon',
+  html: `<svg width="24" height="24" viewBox="0 0 24 24">
+          <path d="M12 2L2 22h20L12 2z" fill="#0066CC" stroke="#003366" stroke-width="2"/>
+        </svg>`,
+  iconSize: [24, 24],
+  iconAnchor: [12, 24]
+});
+
+onMounted(() => {
+  if (!mapContainer.value) {
+    console.error('❌ 地图容器未找到!');
+    return;
+  }
+
+  const map = L.map(mapContainer.value, {
+    center: [24.7, 114], // 韶关大致中心
+    zoom: 8.5,
+    minZoom: 8.3,
+  });
+
+  // 区县颜色映射(保持不变)
+  const districtColorMap = {
+    "武江区": "#FF6B6B",
+    "浈江区": "#4ECDC4",
+    "曲江区": "#FFD166",
+    "始兴县": "#A0DAA9",
+    "仁化县": "#6A0572",
+    "翁源县": "#1A535C",
+    "乳源瑶族自治县": "#FF9F1C",
+    "新丰县": "#87CEEB",
+    "乐昌市": "#118AB2",
+    "南雄市": "#06D6A0",
+    "韶关市": "#cccccc",
+  };
+
+  // 区县颜色匹配函数(保持不变)
+  function getDistrictColor(name) {
+    if (districtColorMap[name]) return districtColorMap[name];
+    const normalizedName = name.replace(/市|县|区|自治县/g, '');
+    for (const key in districtColorMap) {
+      if (key.includes(normalizedName) || normalizedName.includes(key.replace(/市|县|区|自治县/g, ''))) {
+        return districtColorMap[key];
+      }
+    }
+    return '#cccccc';
+  }
+
+  // 加载区县边界(保持不变)
+  fetch('/data/韶关市各区县边界图.geojson')
+    .then(res => {
+      if (!res.ok) throw new Error(`区县边界加载失败:${res.status}`);
+      return res.json();
+    })
+    .then(geojson => {
+      L.geoJSON(geojson, {
+        style: (feature) => ({
+          fillColor: getDistrictColor(feature.properties.name || ''),
+          fillOpacity: 0.7,
+          color: '#333333',
+          weight: 2,
+        }),
+      }).addTo(map);
+
+
+    // 从接口加载大气污染源数据(核心修改:适配新接口)
+      fetch('http://localhost:3000/table/Atmosphere_company_data')
+             .then(res => {
+            if (!res.ok) throw new Error(`接口请求失败:${res.status}`);
+            return res.json();
+            })
+            .then(apiData => {
+              console.log('✅ 接口数据加载完成,共', apiData.length, '条记录');
+              
+              let markerCount = 0;
+              apiData.forEach((item, idx) => {
+                try {
+                  // 解析经纬度(注意:接口里经纬度是字符串,需转数字)
+                  const lng = parseFloat(item.经度);
+                  const lat = parseFloat(item.纬度);
+                  if (isNaN(lat) || isNaN(lng) || lat < 22.7 || lat > 25.5 || lng < 112.7 || lng > 115.3) {
+                    console.warn(`❌ 无效坐标(第${idx}条):`, lat, lng);
+                    return;
+                  }
+
+                  // 解析大气颗粒物排放量
+                  const emission = parseFloat(item["大气颗粒物排放(t/a)"]);
+                  const formattedEmission = isNaN(emission) 
+                    ? '未知' 
+                    : `${emission.toFixed(3)} t/a`;
+
+                  // 创建标记(保持不变)
+                  const marker = L.marker([lat, lng], {
+                    icon: blueTriangle,
+                    zIndexOffset: 1000,
+                  }).addTo(map);
+
+                  // 弹窗内容修改:适配新字段
+                  marker.bindPopup(`
+                    <div class="popup-container">
+                      <h3 class="popup-title">${item.公司 || '未知'}</h3>
+                      <div class="popup-divider"></div>
+                      <p><strong>污染源序号:</strong> ${item["污染源序号"] || '未知'}</p>
+                      <p><strong>企业类型:</strong> ${item.类型 || '未知'}</p>
+                      <p><strong>所属区县:</strong> ${item["所属区县"] || '未知'}</p>
+                      <p><strong>大气颗粒物排放:</strong> ${formattedEmission}</p>
+                    </div>
+                  `);
+
+                  markerCount++;
+                } catch (err) {
+                  console.error(`❌ 处理第${idx}条数据失败:`, err);
+                }
+              });
+
+              console.log(`✅ 成功创建 ${markerCount} 个有效标记`);
+            })
+            .catch(err => {
+              console.error('❌ 接口数据加载失败:', err);
+              alert('数据加载错误:' + err.message);
+            });
+        })
+    .catch(err => {
+      console.error('❌ 区县边界加载失败:', err);
+      alert('区县边界加载错误:' + err.message);
+    });
+
+    map.on('load', () => {
+    // 延迟执行,确保 DOM 已完全渲染
+    setTimeout(() => {
+      map.invalidateSize(); // 强制 Leaflet 重新计算地图尺寸
+      console.log('✅ 地图尺寸已重新计算');
+    }, 300);
+  });
+
+  // 监听窗口大小变化,确保响应式布局中地图尺寸正确
+  window.addEventListener('resize', () => {
+    map.invalidateSize();
+  });
+});
+</script>
+
+<style scoped>
+.map-wrapper {
+  width: 100%;
+  height: 100%;
+  position: relative;
+}
+.map-container {
+  width: 100% !important;
+  height: 100% !important;
+}
+
+/* 弹窗样式 */
+::v-deep .popup-title {
+  text-align: center;
+  font-size: 18px;
+  font-weight: 700;
+  color: #0066CC;
+  margin: 0 0 6px;
+  border-bottom: none;
+  padding-bottom: 8px;
+}
+
+::v-deep .popup-divider {
+  height: 1px;
+  background: #0066CC;
+  margin: 8px 0;
+}
+
+::v-deep .popup-container {
+  min-width: 240px;
+  max-width: 300px;
+  padding: 16px;
+  font-family: "Microsoft YaHei", sans-serif;
+}
+
+::v-deep .popup-container p {
+  margin: 6px 0;
+  font-size: 15px;
+  color: #666;
+  line-height: 1.6;
+}
+
+::v-deep .popup-container strong {
+  color: #0066CC;
+  font-weight: 600;
+}
+
+::v-deep .exceeding {
+  color: #FF3333;
+  font-weight: bold;
+}
+
+/* 美化弹窗 */
+::v-deep .leaflet-popup-content-wrapper {
+  padding: 0 !important;
+  border-radius: 12px !important;
+  box-shadow: 0 6px 16px rgba(0,0,0,0.2) !important;
+}
+
+::v-deep .leaflet-popup-content {
+  margin: 0 !important;
+  width: auto !important;
+}
+
+::v-deep .leaflet-popup-tip {
+  display: none;
+}
+
+/* 图例样式 */
+::v-deep .info {
+  padding: 6px 8px;
+  background: white;
+  background: rgba(255,255,255,0.9);
+  box-shadow: 0 0 15px rgba(0,0,0,0.2);
+  border-radius: 5px;
+}
+
+/* 自定义标记样式 */
+::v-deep .custom-div-icon svg {
+  transition: transform 0.2s;
+  display: block;
+}
+</style>

+ 331 - 0
src/views/User/HmOutFlux/atmosDeposition/atmsamplemap.vue

@@ -0,0 +1,331 @@
+<template>
+  <div class="map-wrapper">
+    <div ref="mapContainer" class="map-container"></div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue';
+import L from 'leaflet';
+import 'leaflet/dist/leaflet.css';
+
+const mapContainer = ref(null);
+
+// 辅助函数:数值格式化
+function formatValue(value) {
+  if (value === undefined || value === null || value === '') return '未知';
+  return parseFloat(value).toFixed(3);
+}
+
+onMounted(() => {
+  // 初始化地图
+  if (!mapContainer.value) {
+    console.error('❌ 地图容器未找到!');
+    return;
+  }
+
+  const map = L.map(mapContainer.value, {
+    center: [24.7, 114], // 韶关大致中心
+    zoom: 8.5,
+    minZoom: 8.3,
+  });
+
+  // 区县颜色映射
+  const districtColorMap = {
+    "武江区": "#FF6B6B",
+    "浈江区": "#4ECDC4",
+    "曲江区": "#FFD166",
+    "始兴县": "#A0DAA9",
+    "仁化县": "#6A0572",
+    "翁源县": "#1A535C",
+    "乳源瑶族自治县": "#FF9F1C",
+    "新丰县": "#87CEEB",
+    "乐昌市": "#118AB2",
+    "南雄市": "#06D6A0",
+  };
+
+  // 加载区县边界
+  fetch('/data/韶关市各区县边界图.geojson')
+    .then(res => {
+      if (!res.ok) throw new Error(`区县边界加载失败:${res.status}`);
+      return res.json();
+    })
+    .then(geojson => {
+      console.log('✅ 区县边界数据加载完成,要素数:', geojson.features.length);
+      
+      L.geoJSON(geojson, {
+        style: (feature) => {
+          const districtName = feature.properties.name;
+          const color = districtColorMap[districtName] || '#cccccc';
+          return {
+            fillColor: color,
+            fillOpacity: 0.7,
+            color: '#333333',
+            weight: 2,
+          };
+        },
+      }).addTo(map);
+
+      // 加载大气数据
+      fetch('http://localhost:3000/table/Atmosphere_summary_data')
+        .then(res => {
+          if (!res.ok) throw new Error(`大气数据加载失败:${res.status}`);
+          return res.json();
+        })
+        .then(atmosphereData => {
+          console.log('✅ 大气数据加载完成,记录数:', atmosphereData.length);
+          
+          let markerCount = 0;
+          atmosphereData.forEach((item, idx) => {
+            try {
+              // 提取经纬度
+              const latField = ['latitude', 'lat', '纬度'].find(key => item[key] !== undefined);
+              const lngField = ['longitude', 'lng', '经度'].find(key => item[key] !== undefined);
+              
+              if (!latField || !lngField) {
+                console.error(`❌ 未找到经纬度字段(第${idx}条)`);
+                return;
+              }
+              
+              // 清理经纬度数据
+              const cleanLat = String(item[latField]).replace(/[^\d.-]/g, '');
+              const cleanLng = String(item[lngField]).replace(/[^\d.-]/g, '');
+              
+              const lat = parseFloat(parseFloat(cleanLat).toFixed(6));
+              const lng = parseFloat(parseFloat(cleanLng).toFixed(6));
+              
+              // 坐标范围校验
+              if (isNaN(lat) || isNaN(lng) || lat < 22.7 || lat > 25.5 || lng < 112.7 || lng > 115.3) {
+                console.warn(`❌ 坐标超出范围(第${idx}条):`, lat, lng);
+                return;
+              }
+              
+              // 创建标记点
+              const marker = L.circleMarker([lat, lng], {
+                radius: 3.5, // 略微减小标记点大小
+                color: '#FF3333',
+                fillColor: '#FF3333',
+                fillOpacity: 0.9,
+                weight: 1.5, // 减小边框宽度
+                zIndexOffset: 1000,
+              }).addTo(map);
+
+              // 构建弹窗内容(双列布局)
+              const content = `
+                <div class="popup-container">
+                  <div class="popup-header">
+                    <h3 class="popup-title">${item.采样 || '未知采样点'}</h3>
+                  </div>
+                  <ul class="popup-info-list">
+                    <li>
+                      <span class="info-label">采样点ID:</span>
+                      <span class="info-value">${item.样品名称 || '未知'}</span>
+                    </li>
+                    <li>
+                      <span class="info-label">样品编号:</span>
+                      <span class="info-value">${item.样品编号 || '未知'}</span>
+                    </li>
+                  </ul>
+                  <div class="grid-container" style="grid-template-columns: 1fr 1fr;">
+                    <div class="grid-item">
+                      <div class="data-item">
+                        <span class="item-label">Cr mg/kg:</span>
+                        <span class="item-value">${formatValue(item['Cr mg/kg'])}</span>
+                      </div>
+                      <div class="data-item">
+                        <span class="item-label">Cr ug/m3:</span>
+                        <span class="item-value">${formatValue(item['Cr ug/m3'])}</span>
+                      </div>
+                      <div class="data-item">
+                        <span class="item-label">As mg/kg:</span>
+                        <span class="item-value">${formatValue(item['As mg/kg'])}</span>
+                      </div>
+                      <div class="data-item">
+                        <span class="item-label">As ug/m3:</span>
+                        <span class="item-value">${formatValue(item['As ug/m3'])}</span>
+                      </div>
+                      <div class="data-item">
+                        <span class="item-label">Cd mg/kg:</span>
+                        <span class="item-value">${formatValue(item['Cd mg/kg'])}</span>
+                      </div>
+                      <div class="data-item">
+                        <span class="item-label">Cd ug/m3:</span>
+                        <span class="item-value">${formatValue(item['Cd ug/m3'])}</span>
+                      </div>
+                    </div>
+                    <div class="grid-item">
+                      <div class="data-item">
+                        <span class="item-label">Hg mg/kg:</span>
+                        <span class="item-value">${formatValue(item['Hg mg/kg'])}</span>
+                      </div>
+                      <div class="data-item">
+                        <span class="item-label">Hg ug/m3:</span>
+                        <span class="item-value">${formatValue(item['Hg ug/m3'])}</span>
+                      </div>
+                      <div class="data-item">
+                        <span class="item-label">Pb mg/kg:</span>
+                        <span class="item-value">${formatValue(item['Pb mg/kg'])}</span>
+                      </div>
+                      <div class="data-item">
+                        <span class="item-label">Pb ug/m3:</span>
+                        <span class="item-value">${formatValue(item['Pb ug/m3'])}</span>
+                      </div>
+                      <div class="data-item">
+                        <span class="item-label">颗粒物重量 mg:</span>
+                        <span class="item-value">${formatValue(item['颗粒物的重量 mg'])}</span>
+                      </div>
+                      <div class="data-item">
+                        <span class="item-label">标准体积 m3:</span>
+                        <span class="item-value">${formatValue(item['标准体积 m3'])}</span>
+                      </div>
+                      <div class="data-item">
+                        <span class="item-label">颗粒物浓度 ug/m3:</span>
+                        <span class="item-value">${formatValue(item['颗粒物浓度ug/m3'])}</span>
+                      </div>
+                    </div>
+                  </div>
+                </div>
+              `;
+
+              marker.bindPopup(content);
+              markerCount++;
+            } catch (err) {
+              console.error(`❌ 处理大气数据失败(第${idx}条):`, err);
+            }
+          });
+
+          console.log(`✅ 成功创建 ${markerCount} 个大气数据标记点`);
+        })
+        .catch(err => {
+          console.error('❌ 大气数据加载失败:', err);
+          alert('大气数据接口错误:' + err.message);
+        });
+    })
+    .catch(err => {
+      console.error('❌ 区县边界加载失败:', err);
+      alert('区县边界加载错误:' + err.message);
+    });
+});
+</script>
+
+<style scoped>
+.map-wrapper {
+  width: 100%;
+  height: 100%;
+  position: relative;
+}
+.map-container {
+  width: 100% !important;
+  height: 100% !important;
+}
+
+/* 弹窗样式 */
+::v-deep .leaflet-popup-content-wrapper {
+  padding: 0 !important;
+  border-radius: 10px !important;
+  box-shadow: 0 4px 12px rgba(0,0,0,0.15) !important;
+}
+
+::v-deep .leaflet-popup-content {
+  margin: 0 !important;
+  width: auto !important;
+  max-width: 400px; /* 减小最大宽度 */
+}
+
+::v-deep .popup-container {
+  min-width: 280px; /* 减小最小宽度 */
+  padding: 12px; /* 减小内边距 */
+  font-family: "Microsoft YaHei", sans-serif;
+}
+
+::v-deep .popup-header {
+  margin-bottom: 10px;
+}
+
+::v-deep .popup-title {
+  text-align: center;
+  font-size: 16px; /* 减小标题字体大小 */
+  font-weight: 700;
+  color: #0066CC;
+  margin: 0 0 5px;
+  padding-bottom: 6px;
+  border-bottom: 1.5px solid #0066CC;
+}
+
+::v-deep .popup-info-list {
+  list-style: none;
+  padding: 0;
+  margin: 0 0 10px;
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 6px; /* 减小间隙 */
+}
+
+::v-deep .popup-info-list li {
+  display: flex;
+  margin: 0;
+  padding: 3px 6px; /* 减小内边距 */
+  background: #f9f9f9;
+  border-radius: 3px; /* 减小圆角 */
+}
+
+::v-deep .info-label {
+  flex: 0 0 85px; /* 减小标签宽度 */
+  font-weight: 600;
+  color: #333;
+  font-size: 13px; /* 减小字体大小 */
+}
+
+::v-deep .info-value {
+  flex: 1;
+  color: #666;
+  font-size: 13px; /* 减小字体大小 */
+}
+
+::v-deep .grid-container {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 6px; /* 减小间隙 */
+}
+
+::v-deep .grid-item {
+  display: flex;
+  flex-direction: column;
+  gap: 6px; /* 减小间隙 */
+}
+
+::v-deep .data-item {
+  display: flex;
+  justify-content: space-between;
+  padding: 6px 8px; /* 减小内边距 */
+  background: #f9f9f9;
+  border-radius: 3px; /* 减小圆角 */
+}
+
+::v-deep .item-label {
+  font-weight: 600;
+  color: #555;
+  font-size: 13px; /* 减小字体大小 */
+}
+
+::v-deep .item-value {
+  color: #000;
+  font-size: 13px; /* 减小字体大小 */
+}
+
+/* 隐藏弹窗箭头 */
+::v-deep .leaflet-popup-tip {
+  display: none;
+}
+
+/* 标记点样式 */
+::v-deep .leaflet-circle-marker {
+  stroke-width: 1.5px !important;
+}
+
+/* 大气数据标记点的悬停效果 */
+::v-deep .leaflet-marker-pane .leaflet-circle-marker[fill="#118AB2"]:hover {
+  fill-opacity: 1 !important;
+  stroke-width: 2.5px !important;
+}
+</style>    

+ 112 - 0
src/views/User/HmOutFlux/atmosDeposition/heavyMetalEnterprise.vue

@@ -0,0 +1,112 @@
+<template>
+  <div class="page-container">
+    
+   <div class="point-map">
+    <div class="component-title">涉重企业地图展示</div>
+    <div class="map-holder">
+      <atmcompanymap/>
+    </div>
+   </div>
+
+  
+   <div class="point-line">
+    <div class="component-title">涉重企业数据列表展示</div>
+    <atmcompanyline/>
+   </div>
+
+   <div>
+    <div class="component-title">各区县企业平均大气颗粒物排放(t/a)</div>
+    <HeavyMetalEnterprisechart/>
+   </div>
+  </div>
+</template>
+<!--污染企业-->
+
+<script setup>
+import { ref, onMounted, watch, computed } from 'vue';
+import * as echarts from 'echarts';
+import atmcompanyline from './atmcompanyline.vue';
+import atmcompanymap from './atmcompanymap.vue';
+import AtmCompanytencentMap from './atmCompanytencentMap.vue';
+import HeavyMetalEnterprisechart from './heavyMetalEnterprisechart.vue';
+
+
+
+
+</script>
+
+<style scoped>
+.page-container {
+  display: flex;
+  flex-direction: column;
+  height: 100vh; /* 整屏高度 */
+  padding: 0;
+  box-sizing: border-box;
+  background-color: #f5f7fa;
+  gap: 20px;
+  margin: 0;
+  position: relative !important; /* 防止被 Leaflet 修改 */
+  overflow: visible !important; /* 确保标题不被裁剪 */
+}
+
+.map-holder {
+  position: relative ;
+  height: 600px;
+  z-index: 100; /* 保持地图在中间层级 */
+  overflow: visible; /* 允许内容溢出 */
+}
+
+.point-map {
+  flex: 0 0 70%;
+    margin-bottom: 20px;
+    background-color: white;
+    border-radius: 8px;
+    box-shadow: 0 2px 4px rgba(0,0,0,1);
+    overflow: visible !important;
+}
+
+.point-line {
+    background-color: white;
+    border-radius: 12px;/*圆角 */
+    box-shadow: 0 2px 8px rgba(0, 0,0, 0.08);
+    padding: 16px;/*内部间距 */
+    box-sizing: border-box;
+}
+.charts-line{
+  background-color: white;
+  border-radius: 12px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+  padding: 16px;
+  box-sizing: border-box;
+  max-width: 1800px;
+  width: 100%;
+  margin: 20px auto;
+}
+.component-title {
+  /* 基础布局:左对齐 + 紧凑间距 */
+  text-align: left;        /* 强制左对齐,告别居中 */
+  margin: 12px 0;          /* 上下间距缩小,更紧凑(原16px→12px) */
+  padding-left: 24px;      /* 给蓝色方块留空间 */
+  position: relative;      /* 为伪元素定位做准备 */
+
+  /* 文字样式:简约但醒目 */
+  font-size: 1.7rem;      /* 稍小一号,更克制(原1.5rem→1.25rem) */
+  font-weight: 600;        /* 适度加粗,比正文突出但不夸张 */
+  color: #1e88e5;          /* 统一蓝色,和方块颜色呼应 */
+  line-height: 1.2;        /* 紧凑行高,避免臃肿 */
+}
+
+/* 蓝色小方块:用伪元素实现,无额外HTML */
+.component-title::before {
+  content: "";
+  position: absolute;
+  left: 0;                /* 靠最左侧 */
+  top: 50%;              /* 垂直居中 */
+  transform: translateY(-50%);
+  width: 12px;           /* 方块大小,适中即可 */
+  height: 12px;
+  background-color: #1e88e5; /* 和文字同色,统一感 */
+  border-radius: 2px;    /* 轻微圆角,比直角更柔和 */
+}
+
+</style>

+ 250 - 0
src/views/User/HmOutFlux/atmosDeposition/heavyMetalEnterprisechart.vue

@@ -0,0 +1,250 @@
+<template>
+  <div class="region-average-chart">
+    <!-- 图表容器 -->
+    <div ref="chartRef" class="chart-box"></div>
+    <!-- 加载/错误提示 -->
+    <div v-if="loading" class="status">数据加载中...</div>
+    <div v-else-if="error" class="status error">{{ error }}</div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onUnmounted } from 'vue';
+import * as echarts from 'echarts';
+import axios from 'axios';
+import { icon } from 'leaflet';
+
+// ========== 1. 配置项(根据实际需求修改) ==========
+const POLLUTANT_API = 'http://localhost:3000/table/Atmosphere_company_data'; 
+
+// 韶关市区县白名单(确保与接口返回的“所属区县”完全一致)
+const SG_REGIONS = [
+  '浈江区', '武江区', '曲江区', '乐昌市', 
+  '南雄市', '始兴县', '仁化县', '翁源县', 
+  '新丰县', '乳源县'
+];
+
+// 排除的非污染物字段(根据接口结构调整)
+const EXCLUDE_FIELDS = [
+  '污染源序号', '公司', '类型', '经度', '纬度' // 这些字段不是污染物,需排除
+];
+
+// ========== 2. 响应式数据 & 变量 ==========
+const chartRef = ref(null);   // ECharts容器引用
+const loading = ref(true);    // 加载状态
+const error = ref('');        // 错误信息
+let myChart = null;          // ECharts实例
+
+// ========== 3. 数据处理核心逻辑 ==========
+const processData = (apiData) => {
+  // 3.1 空数据保护
+  if (!apiData || apiData.length === 0) {
+    console.error('接口返回空数据');
+    return { regions: [], series: [], totalSamples: 0 };
+  }
+
+  // 3.2 提取**污染物字段**(自动过滤非数值字段)
+  const pollutantFields = Object.keys(apiData[0])
+    .filter(key => 
+      !EXCLUDE_FIELDS.includes(key) &&  // 排除非污染物字段
+      !isNaN(parseFloat(apiData[0][key])) // 只保留数值字段
+    );
+
+  // 3.3 无有效污染物字段的保护
+  if (pollutantFields.length === 0) {
+    console.error('未识别到有效污染物字段,请检查 EXCLUDE_FIELDS 配置');
+    return { regions: [], series: [], totalSamples: 0 };
+  }
+
+  // 3.4 按区县分组统计(总和 + 计数)
+  const regionGroups = {}; // 结构:{ 区县: { 污染物: { sum, count } } }
+  const globalStats={};//全局统计对象
+  let totalSamples = 0;    // 总样本数
+
+  pollutantFields.forEach(field=>{
+    globalStats[field] = {sum:0,count:0};
+  })
+
+  apiData.forEach(item => {
+    const region = item['所属区县'] || '未知区县'; // 提取区县
+    totalSamples++;
+
+    // 初始化区县分组(每个污染物字段都要初始化)
+    if (!regionGroups[region]) {
+      regionGroups[region] = {};
+      pollutantFields.forEach(field => {
+        regionGroups[region][field] = { sum: 0, count: 0 };
+      });
+    }
+
+    // 累加每个污染物的浓度
+    pollutantFields.forEach(field => {
+      const value = parseFloat(item[field]);
+      if (!isNaN(value)) { // 过滤无效值(如空字符串、null)
+        regionGroups[region][field].sum += value;
+        regionGroups[region][field].count++;
+
+        globalStats[field].sum+=value;
+        globalStats[field].count++;
+      }
+    });
+  });
+
+  // 3.5 筛选**有效区县**(仅韶关市区县白名单内的区域)
+  const validRegions = SG_REGIONS.filter(region => 
+    regionGroups[region] !== undefined
+  );
+  validRegions.push('全部样本平均');//全局平均的分类
+
+  // 3.6 构建ECharts所需的series数据
+  const series = pollutantFields.map((field, index) => ({
+    name: field, // 污染物名称作为系列名
+    type: 'bar',
+    data: validRegions.map(region => {
+        if(region === '全部样本平均'){
+            const stats = globalStats[field];
+            return stats.count>0
+             ?(stats.sum/stats.count).toFixed(2)
+             :0;
+        }
+      const group = regionGroups[region][field];
+      return group.count > 0 
+        ? (group.sum / group.count).toFixed(2) // 计算平均值,保留2位小数
+        : 0; // 无数据时显示0
+    }),
+    itemStyle: { color: (params)=>{
+      return params.name  ==='全部样本平均'?'#ff0000' : '#1890ff'
+    } }, 
+    label: {
+      show: true,
+      position: 'top',
+      fontSize: 18,
+      color: '#333'
+    }
+  }));
+
+  return { regions: validRegions, series, totalSamples };
+};
+
+// ========== 4. ECharts 初始化 & 更新 ==========
+const initChart = ({ regions, series, totalSamples }) => {
+  if (!chartRef.value) return;
+
+  // 销毁旧实例(避免重复初始化)
+  if (myChart && !myChart.isDisposed()) {
+    myChart.dispose();
+  }
+
+  myChart = echarts.init(chartRef.value);
+  const option = {
+    title: {
+      left: 'center',
+      subtext: `(基于 ${totalSamples} 个有效样本计算)`,
+      subtextStyle: { fontSize: 14, color: '#666' }
+    },
+    tooltip: {
+      trigger: 'axis',
+      formatter: (params) => {
+        const region = params[0].name;
+        let content = `${region}:<br>`;
+        params.forEach(p => {
+          content += `${p.seriesName}: ${p.value} t/a<br>`;
+        });
+        return content;
+      },
+      textStyle: { fontSize: 16 }
+    },
+    xAxis: {
+      type: 'category',
+      data: regions,
+      axisLabel: {
+        formatter: val => val.replace('韶关市', ''), // 简化显示(可选)
+        fontSize: 16,
+        interval:0
+      }
+    },
+    yAxis: {
+      type: 'value',
+      name: '浓度 (t/a)',
+      nameTextStyle: { fontSize: 16 },
+      axisLabel: { fontSize: 16 }
+    },
+    legend: {
+      data: series.map(s => s.name),
+      bottom: 10,
+      textStyle: { fontSize: 14 },
+      icon:'none'
+    },
+    series:series,
+    grid: { left: '5%', right: '5%', bottom: '15%', containLabel: true }
+  };
+
+  myChart.setOption(option);
+};
+
+// ========== 5. 生命周期 & 响应式 ==========
+onMounted(async () => {
+  loading.value = true;
+  try {
+    // 1. 请求接口数据
+    const res = await axios.get(POLLUTANT_API, { timeout: 10000 });
+    
+    // 2. 处理数据
+    const processedData = processData(res.data);
+    
+    // 3. 渲染图表
+    initChart(processedData);
+
+  } catch (err) {
+    // 细化错误提示(网络/服务器/超时等)
+    if (err.response) {
+      error.value = `数据加载失败(${err.response.status})`;
+    } else if (err.request) {
+      error.value = '网络错误,无法连接服务器';
+    } else {
+      error.value = `加载失败:${err.message}`;
+    }
+    console.error('接口请求错误:', err);
+  } finally {
+    loading.value = false;
+  }
+});
+
+// 窗口resize时自动调整图表
+const resizeHandler = () => {
+  if (myChart) myChart.resize();
+};
+onMounted(() => window.addEventListener('resize', resizeHandler));
+onUnmounted(() => window.removeEventListener('resize', resizeHandler));
+</script>
+
+<style scoped>
+.region-average-chart {
+  width: 100%;
+  max-width: 1200px;
+  margin: 20px auto;
+  position: relative;
+}
+.chart-box {
+  width: 100%;
+  height: 600px;
+  min-height: 400px;
+  background: white;
+  border-radius: 8px;
+  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+.status {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  padding: 12px 20px;
+  background: rgba(255,255,255,0.9);
+  border-radius: 6px;
+  font-size: 16px;
+}
+.error { 
+  color: #ff4d4f;
+  font-weight: bold;
+}
+</style>

+ 25 - 0
src/views/User/HmOutFlux/atmosDeposition/samplingDesc3.vue

@@ -0,0 +1,25 @@
+<template>
+  <div class="">
+    
+  </div>
+</template>
+
+<script>
+export default {
+  name: '',
+  data() {
+    return {
+      
+    };
+  },
+  methods: {
+    
+  }
+};
+</script>
+
+<style scoped>
+  . {
+    
+  }
+</style>

+ 64 - 0
src/views/User/HmOutFlux/irrigationWater/crossSection.vue

@@ -0,0 +1,64 @@
+<template>
+  <div class="cross-container">
+    <h3 class="table-title">断面数据地图展示</h3>
+    <div class="map-holder"><crosssectionmap/></div>
+
+     <h3 class="table-title">断面数据详情</h3>
+     <div><CrossSectionSamplelineData/></div>
+
+     <h3 class="table-title"> 各河流Cd的平均浓度柱状图</h3>
+     <div><CrossSetionData1/></div>
+
+     <h3 class="table-title">各区县的Cd平均浓度柱状图</h3>
+     <div><CrossSetionData2/></div>
+  </div>
+
+</template>
+
+<script setup lang="ts">
+import CrossSectionSamplelineData from './crossSectionSamplelineData.vue';
+import CrossSetionData1 from './crossSetionData1.vue';
+import CrossSetionData2 from './crossSetionData2.vue';
+import crosssectionmap from './crosssectionmap.vue';
+
+</script>
+
+<style scoped>
+.map-holder {
+  position: relative;
+  height: 600px; /* 减去标题高度,避免覆盖 */
+  overflow: visible;
+  z-index: 100;
+}
+.cross-container {
+  position: relative !important; /* 防止被 Leaflet 修改 */
+    overflow: visible !important; /* 确保标题不被裁剪 */
+}
+.table-title {
+  /* 基础布局:左对齐 + 紧凑间距 */
+  text-align: left;        /* 强制左对齐,告别居中 */
+  margin: 12px 0;          /* 上下间距缩小,更紧凑(原16px→12px) */
+  padding-left: 24px;      /* 给蓝色方块留空间 */
+  position: relative;      /* 为伪元素定位做准备 */
+  z-index: 200;
+
+  /* 文字样式:简约但醒目 */
+  font-size: 1.7rem;      /* 稍小一号,更克制(原1.5rem→1.25rem) */
+  font-weight: 600;        /* 适度加粗,比正文突出但不夸张 */
+  color: #1e88e5;          /* 统一蓝色,和方块颜色呼应 */
+  line-height: 1.2;        /* 紧凑行高,避免臃肿 */
+}
+
+/* 蓝色小方块:用伪元素实现,无额外HTML */
+.table-title::before {
+  content: "";
+  position: absolute;
+  left: 0;                /* 靠最左侧 */
+  top: 50%;              /* 垂直居中 */
+  transform: translateY(-50%);
+  width: 12px;           /* 方块大小,适中即可 */
+  height: 12px;
+  background-color: #1e88e5; /* 和文字同色,统一感 */
+  border-radius: 2px;    /* 轻微圆角,比直角更柔和 */
+}
+</style>

+ 139 - 0
src/views/User/HmOutFlux/irrigationWater/crossSectionSamplelineData.vue

@@ -0,0 +1,139 @@
+<template>
+  <div class="map-page">
+    <!-- 数据表格容器 -->
+    <div class="table-container">
+      <table class="data-table">
+        <!-- 表头 -->
+        <thead>
+          <tr>
+            <th>断面编号</th>
+            <th>所属河流</th>
+            <th>断面位置</th>
+            <th>所属区县</th>
+            <th>Cd含量(ug/L)</th>
+            <th>经度</th>
+            <th>纬度</th>
+          </tr>
+        </thead>
+        <!-- 表体(遍历数据) -->
+        <tbody>
+          <tr v-for="item in state.excelData" :key="item.id">
+            <td>{{ item.id }}</td>
+            <td>{{ item.river }}</td>
+            <td>{{ item.location }}</td>
+            <td>{{ item.district }}</td>
+            <td>{{ item.cdValue }}</td>
+            <td>{{ item.longitude.toFixed(6) }}</td> <!-- 保留6位小数 -->
+            <td>{{ item.latitude.toFixed(6) }}</td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
+import { wgs84togcj02 } from 'coordtransform';
+
+// 状态管理
+const error = ref(null)
+const state = reactive({
+  excelData: [], // 存储解析后的断面数据
+  riverAvgData:[],//存储按河流分组后的平均数据
+})
+
+// 初始化断面数据(直接嵌入你的Excel数据)
+const initData = () => {
+  const rawData = [
+    { "断面编号": 0, "所属河流": "浈江", "断面位置": "小古录", "所属区县": "始兴县", "经度": 114.208543, "纬度": 25.059851, "Cd(ug/L)": 0.11 },
+    { "断面编号": 1, "所属河流": "浈江", "断面位置": "长坝", "所属区县": "仁化县", "经度": 113.692874, "纬度": 24.874845, "Cd(ug/L)": 1.116 },
+    { "断面编号": 2, "所属河流": "浈江", "断面位置": "东河桥", "所属区县": "浈江区", "经度": 113.601631, "纬度": 24.80784, "Cd(ug/L)": 3.46 },
+    { "断面编号": 3, "所属河流": "武江", "断面位置": "坪石", "所属区县": "乐昌市", "经度": 113.066281, "纬度": 25.274421, "Cd(ug/L)": 0.98 },
+    { "断面编号": 4, "所属河流": "武江", "断面位置": "乐昌", "所属区县": "乐昌市", "经度": 113.338782, "纬度": 25.129212, "Cd(ug/L)": 0.11 },
+    { "断面编号": 5, "所属河流": "武江", "断面位置": "武江桥", "所属区县": "乐昌市", "经度": 113.349815, "纬度": 25.120278, "Cd(ug/L)": 0.15 },
+    { "断面编号": 6, "所属河流": "北江", "断面位置": "九公里", "所属区县": "浈江区", "经度": 113.580758, "纬度": 24.761299, "Cd(ug/L)": 7.83 },
+    { "断面编号": 7, "所属河流": "北江", "断面位置": "白土", "所属区县": "曲江区", "经度": 113.531284, "纬度": 24.679958, "Cd(ug/L)": 5.94 },
+    { "断面编号": 8, "所属河流": "浈江", "断面位置": "昆仑水站", "所属区县": "南雄市", "经度": 114.3629285, "纬度": 25.10053746, "Cd(ug/L)": 0.517 },
+    { "断面编号": 9, "所属河流": "北江", "断面位置": "白沙", "所属区县": "曲江", "经度": 113.5707136, "纬度": 24.58139261, "Cd(ug/L)": 1.54 },
+    { "断面编号": 10, "所属河流": "浈江", "断面位置": "周田水站", "所属区县": "仁化县", "经度": 113.8293461, "纬度": 24.97851516, "Cd(ug/L)": 0.182 },
+    { "断面编号": 11, "所属河流": "武江", "断面位置": "坪石水站", "所属区县": "乐昌市", "经度": 113.0467854, "纬度": 25.28883459, "Cd(ug/L)": 1.071 }
+  ];
+
+  // 处理坐标(WGS84转GCJ02,腾讯地图用GCJ02)
+  state.excelData = rawData.map(item => {
+    const lng = Number(item.经度);
+    const lat = Number(item.纬度);
+    if (isNaN(lat) || isNaN(lng)) {
+      console.error('无效经纬度:', item);
+      return null;
+    }
+    const [gcjLng, gcjLat] = wgs84togcj02(lng, lat); // 坐标转换
+    return {
+      id: item.断面编号,
+      river: item.所属河流,
+      location: item.断面位置,
+      district: item.所属区县,
+      cdValue: item["Cd(ug/L)"],
+      latitude: gcjLat,
+      longitude: gcjLng,
+    };
+  }).filter(item => item !== null);
+}
+
+
+
+// 生命周期
+onMounted(async () => {
+  try {
+    initData();
+  } catch (err) {
+    error.value = err.message;
+  }
+})
+
+</script>
+
+<style scoped>
+.map-page, .chart-page {
+  width: 100%;
+  margin: 0 auto 24px; /* 水平居中,底部间距24px */
+  background-color: white;
+  border-radius: 12px;
+  padding: 20px;
+  box-sizing: border-box;
+}
+
+
+.data-table {
+  width: 100%;
+  border-collapse: collapse;/*合并边框 */
+  min-width: 800px;
+}
+
+.data-table th,.data-table td {
+  padding: 12px 15px;
+  text-align: center;
+  border: 1px solid #e5e7eb;
+}
+
+.data-table th {
+  background-color: #f3f4f6;
+  font-weight: bold;
+  color: #1f2937;
+}
+
+.data-table tr:nth-child(even){
+  background-color: #f9fafb;
+}
+
+.data-table tr:hover{
+  background-color: #f3f4f6;
+}
+/* 响应式调整 */
+@media (max-width: 768px) {
+  .map-page {
+    width: 96%; /* 小屏幕稍窄,避免边缘拥挤 */
+  }
+}
+</style>

+ 217 - 0
src/views/User/HmOutFlux/irrigationWater/crossSetionData1.vue

@@ -0,0 +1,217 @@
+<template>
+    <!--柱状图容器-->
+  <div class="chart-page">
+    <div ref="chartContainer" class="chart-container"></div>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
+import { wgs84togcj02 } from 'coordtransform';
+import * as echarts from 'echarts'
+
+// 状态管理
+const error = ref(null)
+const chartContainer=ref(null)
+let chart = null
+const state = reactive({
+  excelData: [], // 存储解析后的断面数据
+  riverAvgData:[],//存储按河流分组后的平均数据
+})
+
+// 初始化断面数据(直接嵌入你的Excel数据)
+const initData = () => {
+  const rawData = [
+    { "断面编号": 0, "所属河流": "浈江", "断面位置": "小古录", "所属区县": "始兴县", "经度": 114.208543, "纬度": 25.059851, "Cd(ug/L)": 0.11 },
+    { "断面编号": 1, "所属河流": "浈江", "断面位置": "长坝", "所属区县": "仁化县", "经度": 113.692874, "纬度": 24.874845, "Cd(ug/L)": 1.116 },
+    { "断面编号": 2, "所属河流": "浈江", "断面位置": "东河桥", "所属区县": "浈江区", "经度": 113.601631, "纬度": 24.80784, "Cd(ug/L)": 3.46 },
+    { "断面编号": 3, "所属河流": "武江", "断面位置": "坪石", "所属区县": "乐昌市", "经度": 113.066281, "纬度": 25.274421, "Cd(ug/L)": 0.98 },
+    { "断面编号": 4, "所属河流": "武江", "断面位置": "乐昌", "所属区县": "乐昌市", "经度": 113.338782, "纬度": 25.129212, "Cd(ug/L)": 0.11 },
+    { "断面编号": 5, "所属河流": "武江", "断面位置": "武江桥", "所属区县": "乐昌市", "经度": 113.349815, "纬度": 25.120278, "Cd(ug/L)": 0.15 },
+    { "断面编号": 6, "所属河流": "北江", "断面位置": "九公里", "所属区县": "浈江区", "经度": 113.580758, "纬度": 24.761299, "Cd(ug/L)": 7.83 },
+    { "断面编号": 7, "所属河流": "北江", "断面位置": "白土", "所属区县": "曲江区", "经度": 113.531284, "纬度": 24.679958, "Cd(ug/L)": 5.94 },
+    { "断面编号": 8, "所属河流": "浈江", "断面位置": "昆仑水站", "所属区县": "南雄市", "经度": 114.3629285, "纬度": 25.10053746, "Cd(ug/L)": 0.517 },
+    { "断面编号": 9, "所属河流": "北江", "断面位置": "白沙", "所属区县": "曲江", "经度": 113.5707136, "纬度": 24.58139261, "Cd(ug/L)": 1.54 },
+    { "断面编号": 10, "所属河流": "浈江", "断面位置": "周田水站", "所属区县": "仁化县", "经度": 113.8293461, "纬度": 24.97851516, "Cd(ug/L)": 0.182 },
+    { "断面编号": 11, "所属河流": "武江", "断面位置": "坪石水站", "所属区县": "乐昌市", "经度": 113.0467854, "纬度": 25.28883459, "Cd(ug/L)": 1.071 }
+  ];
+
+  // 处理坐标(WGS84转GCJ02,腾讯地图用GCJ02)
+  state.excelData = rawData.map(item => {
+    const lng = Number(item.经度);
+    const lat = Number(item.纬度);
+    if (isNaN(lat) || isNaN(lng)) {
+      console.error('无效经纬度:', item);
+      return null;
+    }
+    const [gcjLng, gcjLat] = wgs84togcj02(lng, lat); // 坐标转换
+    return {
+      id: item.断面编号,
+      river: item.所属河流,
+      location: item.断面位置,
+      district: item.所属区县,
+      cdValue: item["Cd(ug/L)"],
+      latitude: gcjLat,
+      longitude: gcjLng,
+    };
+  }).filter(item => item !== null);
+  calculateRiverAvg();
+}
+
+const calculateRiverAvg = () => {
+  // 按河流分组
+  const riverGroups = {};
+  
+  // 分组并累加浓度值
+  state.excelData.forEach(item => {
+    if (!riverGroups[item.river]) {
+      riverGroups[item.river] = {
+        total: 0,
+        count: 0,
+        avg: 0
+      };
+    }
+    riverGroups[item.river].total += item.cdValue;
+    riverGroups[item.river].count += 1;
+  });
+  
+  // 计算每组平均值
+  const riverAvg = [];
+  let totalAll = 0;
+  let countAll = 0;
+  
+  for (const river in riverGroups) {
+    const avg = riverGroups[river].total / riverGroups[river].count;
+    riverAvg.push({
+      river,
+      avg: parseFloat(avg.toFixed(3)) // 保留3位小数
+    });
+    totalAll += riverGroups[river].total;
+    countAll += riverGroups[river].count;
+  }
+  
+  // 计算总平均值并添加到数组
+  const totalAvg = {
+    river: '总河流平均',
+    avg: parseFloat((totalAll / countAll).toFixed(3))
+  };
+  
+  riverAvg.push(totalAvg);
+  state.riverAvgData = riverAvg;
+  
+  // 更新图表
+  updateChart();
+}
+
+// 新增:初始化图表
+const initChart = () => {
+  if (chartContainer.value) {
+    chart = echarts.init(chartContainer.value);
+    updateChart();
+  }
+}
+
+// 新增:更新图表数据
+const updateChart = () => {
+  if (!chart || state.riverAvgData.length === 0) return;
+  
+  // 准备图表数据
+  const rivers = state.riverAvgData.map(item => item.river);
+  const avgs = state.riverAvgData.map(item => item.avg);
+  
+  // 配置图表选项
+  const option = {
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow'
+      },
+      formatter: '{a} <br/>{b}: {c} ug/L'
+    },
+    grid: {
+      //left: '3%',
+      right: '4%',
+      bottom: '3%',
+      containLabel: true
+    },
+    xAxis: {
+      type: 'category',
+      data: rivers,
+      axisLabel: {
+        interval: 0,
+        fontSize:18
+      }
+    },
+    yAxis: {
+      type: 'value',
+      name: 'Cd浓度 (ug/L)',
+      min: 0,
+      nameTextStyle:{
+        fontSize:18,
+      },
+      axisLabel: {
+        formatter: '{value}',
+        fontSize:18
+      }
+    },
+    series: [
+      {
+        name: '平均镉浓度',
+        type: 'bar',
+        data: avgs,
+        itemStyle: {
+          // 为总平均值设置不同颜色
+          color: function(params) {
+            return params.dataIndex === rivers.length - 1 ? '#FF4500' : '#1E88E5';
+          }
+        },
+        label: {
+          show: true,
+          position: 'top',
+          formatter: '{c}',
+          fontSize:18
+        },
+        emphasis: {
+          focus: 'series'
+        }
+      }
+    ]
+  };
+  
+  // 设置图表选项
+  chart.setOption(option);
+}
+
+// 生命周期
+onMounted(async () => {
+  try {
+    initData();
+    initChart();
+    //监听窗口大小变化,调整图表
+    window.addEventListener('resize',()=>{
+      if(chart){
+        chart.resize();
+      }
+    })
+  } catch (err) {
+    error.value = err.message;
+  }
+})
+
+onBeforeUnmount(() => {
+  if(chart){
+    chart.dispose();
+  }
+})
+</script>
+
+<style>
+
+/* 图表容器样式 */
+.chart-container {
+  width: 100%; /* 占满图表容器宽度 */
+  height: 400px;
+  margin: 0 auto;
+  border-radius: 12px;
+}
+</style>

+ 183 - 0
src/views/User/HmOutFlux/irrigationWater/crossSetionData2.vue

@@ -0,0 +1,183 @@
+<template>
+  <!-- 柱状图容器 -->
+  <div class="chart-page">
+    <div ref="chartContainer" class="chart-container"></div>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
+import * as echarts from 'echarts'
+
+// 状态管理
+const chartContainer = ref(null)
+let chart = null
+const state = reactive({
+  excelData: [], // 存储解析后的断面数据
+  districtAvgData: [] // 存储按区县分组后的平均数据
+})
+
+// 初始化数据(移除坐标转换,只保留必要字段)
+const initData = () => {
+  const rawData = [
+    { "断面编号": 0, "所属河流": "浈江", "断面位置": "小古录", "所属区县": "始兴县", "Cd(ug/L)": 0.11 },
+    { "断面编号": 1, "所属河流": "浈江", "断面位置": "长坝", "所属区县": "仁化县", "Cd(ug/L)": 1.116 },
+    { "断面编号": 2, "所属河流": "浈江", "断面位置": "东河桥", "所属区县": "浈江区", "Cd(ug/L)": 3.46 },
+    { "断面编号": 3, "所属河流": "武江", "断面位置": "坪石", "所属区县": "乐昌市", "Cd(ug/L)": 0.98 },
+    { "断面编号": 4, "所属河流": "武江", "断面位置": "乐昌", "所属区县": "乐昌市", "Cd(ug/L)": 0.11 },
+    { "断面编号": 5, "所属河流": "武江", "断面位置": "武江桥", "所属区县": "乐昌市", "Cd(ug/L)": 0.15 },
+    { "断面编号": 6, "所属河流": "北江", "断面位置": "九公里", "所属区县": "浈江区", "Cd(ug/L)": 7.83 },
+    { "断面编号": 7, "所属河流": "北江", "断面位置": "白土", "所属区县": "曲江区", "Cd(ug/L)": 5.94 },
+    { "断面编号": 8, "所属河流": "浈江", "断面位置": "昆仑水站", "所属区县": "南雄市", "Cd(ug/L)": 0.517 },
+    { "断面编号": 9, "所属河流": "北江", "断面位置": "白沙", "所属区县": "曲江区", "Cd(ug/L)": 1.54 },
+    { "断面编号": 10, "所属河流": "浈江", "断面位置": "周田水站", "所属区县": "仁化县", "Cd(ug/L)": 0.182 },
+    { "断面编号": 11, "所属河流": "武江", "断面位置": "坪石水站", "所属区县": "乐昌市", "Cd(ug/L)": 1.071 }
+  ];
+
+  // 处理数据:只保留柱状图需要的字段(移除经纬度和坐标转换)
+  state.excelData = rawData.map(item => ({
+    id: item.断面编号,
+    river: item.所属河流,
+    district: item.所属区县, // 区县字段(核心)
+    cdValue: item["Cd(ug/L)"] // 浓度值(核心)
+  }));
+
+  calculateDistrictAvg(); // 计算区县平均值(替换原河流分组逻辑)
+}
+
+// 按区县计算平均浓度(核心逻辑)
+const calculateDistrictAvg = () => {
+  // 1. 按区县分组:累加每个区县的浓度总和和断面数量
+  const districtGroups = {};
+  state.excelData.forEach(item => {
+    const district = item.district;
+    if (!districtGroups[district]) {
+      districtGroups[district] = {
+        total: 0, // 浓度总和
+        count: 0  // 断面数量
+      };
+    }
+    districtGroups[district].total += item.cdValue;
+    districtGroups[district].count += 1;
+  });
+
+  // 2. 计算每个区县的平均值
+  const districtAvg = [];
+  let totalAll = 0; // 所有区县总浓度
+  let countAll = 0; // 所有区县总断面数
+
+  for (const district in districtGroups) {
+    const avg = districtGroups[district].total / districtGroups[district].count;
+    districtAvg.push({
+      district, // 区县名称
+      avg: parseFloat(avg.toFixed(3)) // 保留3位小数
+    });
+    totalAll += districtGroups[district].total;
+    countAll += districtGroups[district].count;
+  }
+
+  // 3. 计算总平均值(可选,用于对比)
+  const totalAvg = {
+    district: '总平均',
+    avg: parseFloat((totalAll / countAll).toFixed(3))
+  };
+  districtAvg.push(totalAvg);
+
+  state.districtAvgData = districtAvg;
+  updateChart(); // 更新图表
+}
+
+// 初始化图表
+const initChart = () => {
+  if (chartContainer.value) {
+    chart = echarts.init(chartContainer.value);
+    updateChart();
+  }
+}
+
+// 更新图表数据
+const updateChart = () => {
+  if (!chart || state.districtAvgData.length === 0) return;
+
+  // 准备图表数据
+  const districts = state.districtAvgData.map(item => item.district); // x轴:区县名
+  const avgs = state.districtAvgData.map(item => item.avg); // y轴:平均浓度
+
+  // 图表配置
+  const option = {
+    tooltip: {
+      trigger: 'axis',
+      formatter: '{b}: {c} μg/L' // 悬停提示:区县名 + 浓度值
+    },
+    grid: {
+      left: '5%',
+      right: '5%',
+      bottom: '15%', // 底部留空间,防止区县名重叠
+      containLabel: true
+    },
+    xAxis: {
+      type: 'category',
+      data: districts,
+      axisLabel: {
+        interval: 0,
+        fontSize: 18
+      }
+    },
+    yAxis: {
+      type: 'value',
+      name: 'Cd浓度 (μg/L)',
+      min: 0, // 从0开始更直观
+      nameTextStyle:{
+        fontSize:18,
+      },
+      axisLabel: {
+        formatter: '{value}',
+        fontSize: 18
+      }
+    },
+    series: [
+      {
+        name: '平均浓度',
+        type: 'bar',
+        data: avgs,
+        itemStyle: {
+          // 为"总平均"设置不同颜色(最后一项)
+          color: (params) => params.dataIndex === districts.length - 1 ? '#FF4500' : '#1E88E5'
+        },
+        label: {
+          show: true, // 显示数值标签
+          position: 'top',
+          formatter: '{c}',
+          fontSize: 18
+        },
+        barWidth: '60%'
+      }
+    ]
+  };
+
+  chart.setOption(option);
+}
+
+// 生命周期管理
+onMounted(() => {
+  initData();
+  initChart();
+  // 监听窗口 resize,自动调整图表大小
+  window.addEventListener('resize', () => chart && chart.resize());
+})
+
+onBeforeUnmount(() => {
+  // 组件销毁时释放图表资源
+  if (chart) chart.dispose();
+})
+</script>
+
+<style>
+.chart-page {
+  padding: 20px;
+}
+.chart-container {
+  width: 100%;
+  height: 500px; /* 确保图表有足够高度 */
+}
+</style>

+ 344 - 0
src/views/User/HmOutFlux/irrigationWater/crossSetionTencentmap.vue

@@ -0,0 +1,344 @@
+<template>
+    <div class="map-page">
+    <div ref="mapContainer" class="map-container"></div>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, onBeforeUnmount } from 'vue';
+import { wgs84togcj02 } from 'coordtransform';
+const isMapReady = ref(false)
+const TMap = ref(null); // 存储腾讯地图SDK实例
+const mapContainer = ref(null); // 地图容器DOM
+const state = reactive({ excelData: [] }); // 存储处理后的数据
+const infoWindow = ref(null); // 信息窗口实例
+let map = null; // 地图实例
+let markersLayer = null; // 标记图层实例
+
+// 腾讯地图配置
+const tMapConfig = reactive({
+  key: import.meta.env.VITE_TMAP_KEY, // 必须配置环境变量(腾讯地图开发者密钥)
+})
+
+// 加载腾讯地图SDK
+const loadSDK = () => {
+  return new Promise((resolve, reject) => {
+    if (window.TMap) {
+      TMap.value = window.TMap
+      return resolve(window.TMap)
+    }
+    const script = document.createElement('script')
+    script.src = `https://map.qq.com/api/gljs?v=2.exp&libraries=basic&key=${tMapConfig.key}&callback=initTMap`
+    window.initTMap = () => {
+      TMap.value = window.TMap
+      resolve(window.TMap)
+    }
+    script.onerror = (err) => {
+      reject(`地图加载失败: ${err.message}`)
+      document.head.removeChild(script)
+    }
+    document.head.appendChild(script)
+  })
+}
+
+// 初始化断面数据(直接嵌入你的Excel数据)
+const initData = () => {
+  const rawData = [
+    { "断面编号": 0, "所属河流": "浈江", "断面位置": "小古录", "所属区县": "始兴县", "经度": 114.208543, "纬度": 25.059851, "Cd(ug/L)": 0.11 },
+    { "断面编号": 1, "所属河流": "浈江", "断面位置": "长坝", "所属区县": "仁化县", "经度": 113.692874, "纬度": 24.874845, "Cd(ug/L)": 1.116 },
+    { "断面编号": 2, "所属河流": "浈江", "断面位置": "东河桥", "所属区县": "浈江区", "经度": 113.601631, "纬度": 24.80784, "Cd(ug/L)": 3.46 },
+    { "断面编号": 3, "所属河流": "武江", "断面位置": "坪石", "所属区县": "乐昌市", "经度": 113.066281, "纬度": 25.274421, "Cd(ug/L)": 0.98 },
+    { "断面编号": 4, "所属河流": "武江", "断面位置": "乐昌", "所属区县": "乐昌市", "经度": 113.338782, "纬度": 25.129212, "Cd(ug/L)": 0.11 },
+    { "断面编号": 5, "所属河流": "武江", "断面位置": "武江桥", "所属区县": "乐昌市", "经度": 113.349815, "纬度": 25.120278, "Cd(ug/L)": 0.15 },
+    { "断面编号": 6, "所属河流": "北江", "断面位置": "九公里", "所属区县": "浈江区", "经度": 113.580758, "纬度": 24.761299, "Cd(ug/L)": 7.83 },
+    { "断面编号": 7, "所属河流": "北江", "断面位置": "白土", "所属区县": "曲江区", "经度": 113.531284, "纬度": 24.679958, "Cd(ug/L)": 5.94 },
+    { "断面编号": 8, "所属河流": "浈江", "断面位置": "昆仑水站", "所属区县": "南雄市", "经度": 114.3629285, "纬度": 25.10053746, "Cd(ug/L)": 0.517 },
+    { "断面编号": 9, "所属河流": "北江", "断面位置": "白沙", "所属区县": "曲江", "经度": 113.5707136, "纬度": 24.58139261, "Cd(ug/L)": 1.54 },
+    { "断面编号": 10, "所属河流": "浈江", "断面位置": "周田水站", "所属区县": "仁化县", "经度": 113.8293461, "纬度": 24.97851516, "Cd(ug/L)": 0.182 },
+    { "断面编号": 11, "所属河流": "武江", "断面位置": "坪石水站", "所属区县": "乐昌市", "经度": 113.0467854, "纬度": 25.28883459, "Cd(ug/L)": 1.071 }
+  ];
+
+  // 处理坐标(WGS84转GCJ02,腾讯地图用GCJ02)
+  state.excelData = rawData.map(item => {
+    const lng = Number(item.经度);
+    const lat = Number(item.纬度);
+    if (isNaN(lat) || isNaN(lng)) {
+      console.error('无效经纬度:', item);
+      return null;
+    }
+    const [gcjLng, gcjLat] = wgs84togcj02(lng, lat); // 坐标转换
+    return {
+      id: item.断面编号,
+      river: item.所属河流,
+      location: item.断面位置,
+      district: item.所属区县,
+      cdValue: item["Cd(ug/L)"],
+      latitude: gcjLat,
+      longitude: gcjLng,
+    };
+  }).filter(item => item !== null);
+}
+
+// 初始化地图
+const initMap = async () => {
+  try {
+    await loadSDK()
+    // 创建地图实例(中心设为数据区域:粤北)
+    map = new TMap.value.Map(mapContainer.value, {
+      center: new TMap.value.LatLng(25.2, 114), //前大往下,后大往左
+      zoom: 9.8,
+      minZoom: 9.8,
+      maxZoom: 14,
+    })
+    // 创建标记图层
+    markersLayer = new TMap.value.MultiMarker({
+      map: map,
+      styles: {
+        default: new TMap.value.MarkerStyle({
+          width: 30,
+          height: 30,
+          src: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPjxwYXRoIGQ9Ik0xMiAyTDIgMjJoMjBMMTIgMnoiIGZpbGw9IiMxRTg4RTUiLz48L3N2Zz4='        
+        })
+      }
+    });
+    // 绑定标记点击事件
+    markersLayer.on('click', handleMarkerClick);
+    // 加载数据并渲染标记
+    initData();
+    updateMarkers();
+    isMapReady.value = true;
+  } catch (err) {
+    console.error('地图初始化失败:', err);
+    error.value = err.message;
+  }
+}
+
+// 更新标记点
+const updateMarkers = () => {
+  const geometries = state.excelData.map(item => ({
+    id: String(item.id), // 统一转字符串,避免类型错误
+    styleId: 'default',
+    position: new TMap.value.LatLng(item.latitude, item.longitude),
+    properties: {
+      title: item.location, // 断面位置作为标题
+    }
+  }));
+  markersLayer.setGeometries(geometries);
+}
+
+// 标记点击事件(直接用本地数据)
+const handleMarkerClick = (e) => {
+  const marker = e.geometry;
+  if (!marker) return;
+  // 查找本地数据
+  const markerId = String(marker.id);
+  const matchedData = state.excelData.find(item => String(item.id) === markerId);
+  if (!matchedData) {
+    console.error('未找到数据:', markerId);
+    return;
+  }
+  // 构建信息窗口内容
+  const content = `
+    <div class="water-info-window">
+      <h3 class="info-title">断面编号:${matchedData.id}</h3>
+      <div class="info-content">
+        <div class="info-row">
+          <span class="info-label">所属河流:</span>
+          <span class="info-value">${matchedData.river}</span>
+        </div>
+
+        <div class="info-row">
+          <span class="info-label">断面位置:</span>
+          <span class="info-value">${matchedData.location}</span> 
+        </div>
+
+        <div class="info-row">
+          <span class="info-label">所属区县:</span>
+          <span class="info-value">${matchedData.district}</span>
+        </div>
+        
+        <div class="info-row">
+          <span class="info-label">Cd含量:</span>
+          <span class="info-value">${matchedData.cdValue} ug/L</span>
+        </div>
+      
+      </div>
+    </div>
+  `;
+  // 关闭之前的信息窗口
+  if (infoWindow.value) {
+    infoWindow.value.close();
+  }
+  // 打开新信息窗口
+  infoWindow.value = new TMap.value.InfoWindow({
+    map: map,
+    position: marker.position,
+    content,
+    offset: { x: 0, y: -32 } // 向上偏移,避免遮挡标记
+  });
+  infoWindow.value.open();
+}
+
+// 生命周期
+onMounted(async () => {
+  try {
+    await loadSDK();
+    await initMap();
+q    //监听窗口大小变化,调整图表
+    window.addEventListener('resize',()=>{
+      if(chart){
+        chart.resize();
+      }
+    })
+  } catch (err) {
+    error.value = err.message;
+  }
+})
+
+onBeforeUnmount(() => {
+  if (markersLayer) markersLayer.setMap(null); // 销毁标记图层
+  if (infoWindow.value) infoWindow.value.close(); // 关闭信息窗口
+})
+</script>
+
+<style scoped>
+.map-page {
+  width: 100%;
+  margin: 0 auto 24px; /* 水平居中,底部间距24px */
+  background-color: white;
+  border-radius: 12px;
+  padding: 20px;
+  box-sizing: border-box;
+}
+
+/* 地图容器样式 */
+.map-container {
+  width: 100%;
+  height: 550px; /* 固定高度,确保地图显示完整 */
+  margin: 1rem auto;
+  border-radius: 12px;
+}
+
+/* 信息窗口核心调整:暴力放大 + 宽高适配 */
+:v-deep(.tmap-infowindow) {
+  padding: 20px !important;  /* 超大内边距 */
+  min-width: 320px !important; /* 强制加宽 */
+  font-size: 1.25rem !important; /* 基准字体爆炸大 */
+  box-shadow: 0 4px 12px rgba(0,0,0,0.2) !important; /* 阴影增强 */
+}
+
+.water-info-window {
+  background-color: #FFFFFF;
+  border-radius: 8px;
+  box-shadow: 0 6px 20px rgba(0, 32, 71, 0.15);
+  font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
+  transition: transform 0.3s ease;
+  border: 1px solid #e0e7ef;
+}
+
+.info-title {
+  background: linear-gradient(135deg, #1e88e5, #1565c0);
+  color: white;
+  font-size: 1.15rem;
+  font-weight: 600;
+  padding: 16px 20px;
+  margin: 0;
+  position: relative;
+  letter-spacing: 0.5px;
+  border-bottom: 1px solid #e0e7ef;
+}
+
+.info-title:after {
+  content: "";
+  position: absolute;
+  bottom: 0;
+  left: 20px;
+  right: 20px;
+  height: 1px;
+  background: linear-gradient(to right, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0.35), rgba(255, 255, 255, 0.15));
+}
+
+.info-row {
+  display: flex;
+  margin-bottom: 15px;
+  align-items: center;
+  position: relative;
+}
+
+.info-row:last-child {
+  margin-bottom: 0;
+}
+
+.info-row:before {
+  content: "";
+  position: absolute;
+  left: 0;
+  top: 50%;
+  transform: translateY(-50%);
+  width: 14px;
+  height: 14px;
+  border-radius: 50%;
+  background-color: #e3f2fd;
+  border: 2px solid #90caf9;
+}
+
+.info-row:nth-child(4):before {
+  background-color: #ffebee;
+  border-color: #ffcdd2;
+}
+
+.info-label {
+  flex: 0 0 100px;
+  color: #546e7a;
+  font-size: 0.95rem;
+  font-weight: 500;
+  text-align: right;
+  padding-right: 15px;
+  position: relative;
+}
+
+.info-label:after {
+  content: ":";
+  position: absolute;
+  right: 5px;
+}
+
+.info-value {
+  flex: 1;
+  color: #263238;
+  font-size: 1rem;
+  background: #f8f9fa;
+  padding: 10px 15px;
+  border-radius: 6px;
+  border-left: 3px solid #64b5f6;
+  font-weight: 500;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.03);
+  transition: all 0.3s ease;
+}
+
+.info-row:nth-child(4) .info-value {
+  color: #e53935;
+  border-left-color: #ef9a9a;
+  font-weight: 600;
+  position: relative;
+}
+
+.info-row:nth-child(4) .info-value:after {
+  content: "mg/L";
+  position: absolute;
+  right: 15px;
+  font-size: 0.85rem;
+  font-weight: normal;
+  color: #78909c;
+}
+
+/* 响应式调整 */
+@media (max-width: 768px) {
+  .map-page {
+    width: 96%; /* 小屏幕稍窄,避免边缘拥挤 */
+  }
+  .map-container {
+    height: 300px; /* 小屏幕缩短高度 */
+  }
+}
+</style>

+ 272 - 0
src/views/User/HmOutFlux/irrigationWater/crosssectionmap.vue

@@ -0,0 +1,272 @@
+<template>
+  <div class="map-wrapper">
+    <div ref="mapContainer" class="map-container"></div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue';
+import L from 'leaflet';
+import 'leaflet/dist/leaflet.css';
+
+const mapContainer = ref(null);
+
+// 本地数据定义
+const rawData = [
+  { "断面编号": 0, "所属河流": "浈江", "断面位置": "小古录", "所属区县": "始兴县", "经度": 114.208543, "纬度": 25.059851, "Cd(ug/L)": 0.11 },
+  { "断面编号": 1, "所属河流": "浈江", "断面位置": "长坝", "所属区县": "仁化县", "经度": 113.692874, "纬度": 24.874845, "Cd(ug/L)": 1.116 },
+  { "断面编号": 2, "所属河流": "浈江", "断面位置": "东河桥", "所属区县": "浈江区", "经度": 113.601631, "纬度": 24.80784, "Cd(ug/L)": 3.46 },
+  { "断面编号": 3, "所属河流": "武江", "断面位置": "坪石", "所属区县": "乐昌市", "经度": 113.066281, "纬度": 25.274421, "Cd(ug/L)": 0.98 },
+  { "断面编号": 4, "所属河流": "武江", "断面位置": "乐昌", "所属区县": "乐昌市", "经度": 113.338782, "纬度": 25.129212, "Cd(ug/L)": 0.11 },
+  { "断面编号": 5, "所属河流": "武江", "断面位置": "武江桥", "所属区县": "乐昌市", "经度": 113.349815, "纬度": 25.120278, "Cd(ug/L)": 0.15 },
+  { "断面编号": 6, "所属河流": "北江", "断面位置": "九公里", "所属区县": "浈江区", "经度": 113.580758, "纬度": 24.761299, "Cd(ug/L)": 7.83 },
+  { "断面编号": 7, "所属河流": "北江", "断面位置": "白土", "所属区县": "曲江区", "经度": 113.531284, "纬度": 24.679958, "Cd(ug/L)": 5.94 },
+  { "断面编号": 8, "所属河流": "浈江", "断面位置": "昆仑水站", "所属区县": "南雄市", "经度": 114.3629285, "纬度": 25.10053746, "Cd(ug/L)": 0.517 },
+  { "断面编号": 9, "所属河流": "北江", "断面位置": "白沙", "所属区县": "曲江", "经度": 113.5707136, "纬度": 24.58139261, "Cd(ug/L)": 1.54 },
+  { "断面编号": 10, "所属河流": "浈江", "断面位置": "周田水站", "所属区县": "仁化县", "经度": 113.8293461, "纬度": 24.97851516, "Cd(ug/L)": 0.182 },
+  { "断面编号": 11, "所属河流": "武江", "断面位置": "坪石水站", "所属区县": "乐昌市", "经度": 113.0467854, "纬度": 25.28883459, "Cd(ug/L)": 1.071 }
+];
+
+// 定义蓝色三角形标记
+const blueTriangle = L.divIcon({
+  className: 'custom-div-icon',
+  html: `<svg width="24" height="24" viewBox="0 0 24 24">
+          <path d="M12 2L2 22h20L12 2z" fill="#0066CC" stroke="#003366" stroke-width="2"/>
+        </svg>`,
+  iconSize: [24, 24],
+  iconAnchor: [12, 24]
+});
+
+onMounted(() => {
+  // 初始化地图
+  if (!mapContainer.value) {
+    console.error('❌ 地图容器未找到!');
+    return;
+  }
+
+  const map = L.map(mapContainer.value, {
+    center: [24.75, 114], // 韶关大致中心  前大往下,后大往左
+    zoom: 8.5,
+    minZoom:8.3,
+  });
+
+  // 区县颜色映射(增强匹配)
+  const districtColorMap = {
+    "武江区": "#FF6B6B",
+    "浈江区": "#4ECDC4",
+    "曲江区": "#FFD166",
+    "始兴县": "#A0DAA9",
+    "仁化县": "#6A0572",
+    "翁源县": "#1A535C",
+    "乳源瑶族自治县": "#FF9F1C",
+    "新丰县": "#87CEEB",
+    "乐昌市": "#118AB2",
+    "南雄市": "#06D6A0",
+    "韶关市": "#cccccc",
+  };
+
+  // 增强匹配函数
+  function getDistrictColor(name) {
+    // 精确匹配
+    if (districtColorMap[name]) return districtColorMap[name];
+    
+    // 模糊匹配(尝试添加/移除"市"/"县"/"区"/"自治县")
+    const normalizedName = name.replace(/市|县|区|自治县/g, '');
+    for (const key in districtColorMap) {
+      if (key.includes(normalizedName) || normalizedName.includes(key.replace(/市|县|区|自治县/g, ''))) {
+        return districtColorMap[key];
+      }
+    }
+    
+    return '#cccccc'; // 默认颜色
+  }
+
+  //  加载区县边界
+  fetch('/data/韶关市各区县边界图.geojson')
+    .then(res => {
+      if (!res.ok) throw new Error(`区县边界加载失败:${res.status}`);
+      return res.json();
+    })
+    .then(geojson => {
+      console.log('✅ 区县边界数据加载完成,要素数:', geojson.features.length);
+      
+      L.geoJSON(geojson, {
+        style: (feature) => {
+          const districtName = feature.properties.name || '';
+          const color = getDistrictColor(districtName);
+          return {
+            fillColor: color,
+            fillOpacity: 0.7,
+            color: '#333333',
+            weight: 2,
+          };
+        },
+      }).addTo(map);
+
+      // 加载水系图
+      fetch('/data/韶关市河流水系图.geojson')
+        .then(res => {
+          if (!res.ok) throw new Error(`水系图加载失败:${res.status}`);
+          return res.json();
+        })
+        .then(waterGeojson => {
+          console.log('✅ 水系图数据加载完成,要素数:', waterGeojson.features.length);
+          
+          L.geoJSON(waterGeojson, {
+            style: {
+              color: '#0066CC',
+              weight: 2,
+              opacity: 0.8,
+            },
+          }).addTo(map);
+
+          //  从本地rawData加载采样点数据
+          console.log('✅ 从本地数据加载采样点,数量:', rawData.length);
+          
+          let markerCount = 0;
+          rawData.forEach((item, idx) => {
+            try {
+              console.log(`🔍 处理采样点 #${idx + 1}:`, item);
+              
+              const lng = parseFloat(item.经度);
+              const lat = parseFloat(item.纬度);
+              
+              if (isNaN(lat) || isNaN(lng) || lat < 22.7 || lat > 25.5 || lng < 112.7 || lng > 115.3) {
+                console.warn(`❌ 坐标超出合理范围(第${idx}条):`, lat, lng, item);
+                return;
+              }
+              
+              // 创建蓝色三角形标记
+              const marker = L.marker([lat, lng], {
+                icon: blueTriangle,
+                zIndexOffset: 1000,
+              }).addTo(map);
+
+              // 获取镉含量并格式化
+              const cdValue = parseFloat(item["Cd(ug/L)"]);
+              const formattedCd = isNaN(cdValue) ? '未知' : cdValue.toFixed(2) + ' μg/L';
+              
+              // 绑定弹窗内容
+              marker.bindPopup(`
+                <div class="popup-container">
+                  <h3 class="popup-title">断面位置:${item.断面位置}</h3>
+                  <div class="popup-divider"></div>
+    
+                  <p><strong>断面编号:</strong> ${item.断面编号}</p>
+                  <p><strong>所属河流:</strong> ${item.所属河流}</p>
+                  <p><strong>所属区县:</strong> ${item.所属区县}</p>
+                  <p><strong>镉(Cd)含量:</strong> ${formattedCd} 
+                     
+                </div>
+              `);
+              
+              // 添加鼠标交互效果
+              marker.on('mouseover', () => {
+                marker.getElement().querySelector('svg').style.transform = 'scale(1.2)';
+              }).on('mouseout', () => {
+                marker.getElement().querySelector('svg').style.transform = 'scale(1)';
+              });
+              
+              markerCount++;
+            } catch (err) {
+              console.error(`❌ 处理采样点失败(第${idx}条):`, err);
+            }
+          });
+
+          console.log(`✅ 成功创建 ${markerCount} 个标记点`);
+        })
+        .catch(err => {
+          console.error('❌ 水系图加载失败:', err);
+          alert('水系图加载错误:' + err.message);
+        });
+    })
+    .catch(err => {
+      console.error('❌ 区县边界加载失败:', err);
+      alert('区县边界加载错误:' + err.message);
+    });
+});
+</script>
+
+<style scoped>
+.map-wrapper {
+  width: 100%;
+  height: 100%;
+  position: relative;
+}
+.map-container {
+  width: 100% !important;
+  height: 100% !important;
+}
+
+/* 弹窗样式 */
+::v-deep .popup-title {
+  text-align: center;
+  font-size: 18px;
+  font-weight: 700;
+  color: #0066CC;
+  margin: 0 0 6px;
+  border-bottom: none;
+  padding-bottom: 8px;
+}
+
+::v-deep .popup-divider {
+  height: 1px;
+  background: #0066CC;
+  margin: 8px 0;
+}
+
+::v-deep .popup-container {
+  min-width: 240px;
+  max-width: 300px;
+  padding: 16px;
+  font-family: "Microsoft YaHei", sans-serif;
+}
+
+::v-deep .popup-container p {
+  margin: 6px 0;
+  font-size: 15px;
+  color: #666;
+  line-height: 1.6;
+}
+
+::v-deep .popup-container strong {
+  color: #0066CC;
+  font-weight: 600;
+}
+
+::v-deep .exceeding {
+  color: #FF3333;
+  font-weight: bold;
+}
+
+/* 美化弹窗 */
+::v-deep .leaflet-popup-content-wrapper {
+  padding: 0 !important;
+  border-radius: 12px !important;
+  box-shadow: 0 6px 16px rgba(0,0,0,0.2) !important;
+}
+
+::v-deep .leaflet-popup-content {
+  margin: 0 !important;
+  width: auto !important;
+}
+
+::v-deep .leaflet-popup-tip {
+  display: none;
+}
+
+/* 图例样式 */
+::v-deep .info {
+  padding: 6px 8px;
+  background: white;
+  background: rgba(255,255,255,0.9);
+  box-shadow: 0 0 15px rgba(0,0,0,0.2);
+  border-radius: 5px;
+}
+
+/* 自定义标记样式 */
+::v-deep .custom-div-icon svg {
+  transition: transform 0.2s;
+  display: block;
+}
+</style>

+ 275 - 0
src/views/User/HmOutFlux/irrigationWater/irriWaterInputFlux.vue

@@ -0,0 +1,275 @@
+<template>
+  <div class="irrigation-management">
+    <el-card shadow="always" class="gradient-card">
+      <el-row :gutter="20" style="margin-bottom: 10px;">
+        <el-col :span="6">
+          <el-checkbox v-model="waterLand" label="水地" />
+        </el-col>
+        <el-col :span="9">
+          <el-input
+            v-model="irrigationWaterUsage"
+            placeholder="请输入灌溉水用量"
+            :disabled="!waterLand"
+            style="margin-top: 10px;"
+          />
+        </el-col>
+        <el-col :span="9">
+          <el-input
+            v-model="irrigationEfficiency"
+            placeholder="请输入灌溉水有效利用率"
+            :disabled="!waterLand"
+            style="margin-top: 10px;"
+          />
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="20" style="margin-bottom: 10px;">
+        <el-col :span="6">
+          <el-checkbox v-model="irrigatedLand" label="水浇地" />
+        </el-col>
+        <el-col :span="9">
+          <el-input
+            v-model="irrigatedWaterUsage"
+            placeholder="请输入灌溉水用量"
+            :disabled="!irrigatedLand"
+            style="margin-top: 10px;"
+          />
+        </el-col>
+        <el-col :span="9">
+          <el-input
+            v-model="irrigatedEfficiency"
+            placeholder="请输入灌溉水有效利用率"
+            :disabled="!irrigatedLand"
+            style="margin-top: 10px;"
+          />
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="20" style="margin-bottom: 10px;">
+        <el-col :span="6">
+          <el-checkbox v-model="dryLand" label="旱地" />
+        </el-col>
+        <el-col :span="9">
+          <el-input
+            v-model="dryWaterUsage"
+            placeholder="请输入灌溉水用量"
+            :disabled="!dryLand"
+            style="margin-top: 10px;"
+          />
+        </el-col>
+        <el-col :span="9">
+          <el-input
+            v-model="dryEfficiency"
+            placeholder="请输入灌溉水有效利用率"
+            :disabled="!dryLand"
+            style="margin-top: 10px;"
+          />
+        </el-col>
+      </el-row>
+
+      <el-row justify="center" style="margin-top: 20px;">
+        <el-button
+          class="calculate-btn"
+          @click="calculateFlux"
+        >
+          计算灌溉水输入通量
+        </el-button>
+      </el-row>
+
+      <div v-if="fluxResult !== null" style="margin-top: 20px; text-align: center; font-weight: bold;">
+        灌溉水输入通量为: {{ fluxResult.toFixed(2) }} m³
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import { ref } from 'vue';
+import { ElCheckbox, ElInput, ElButton, ElMessage, ElCard, ElRow, ElCol } from 'element-plus';
+
+export default {
+  components: {
+    ElCheckbox,
+    ElInput,
+    ElButton,
+    ElMessage,
+    ElCard,
+    ElRow,
+    ElCol
+  },
+  setup() {
+    const waterLand = ref(false);
+    const irrigatedLand = ref(false);
+    const dryLand = ref(false);
+
+    const irrigationWaterUsage = ref('');
+    const irrigationEfficiency = ref('');
+
+    const irrigatedWaterUsage = ref('');
+    const irrigatedEfficiency = ref('');
+
+    const dryWaterUsage = ref('');
+    const dryEfficiency = ref('');
+
+    const fluxResult = ref(null);
+
+    const calculateFlux = () => {
+      let totalFlux = 0;
+      let valid = true;
+
+      if (waterLand.value) {
+        if (!irrigationWaterUsage.value || !irrigationEfficiency.value) {
+          ElMessage.warning('请输入水地的灌溉水用量和灌溉水有效利用率');
+          valid = false;
+        } else {
+          const usage = parseFloat(irrigationWaterUsage.value);
+          const efficiency = parseFloat(irrigationEfficiency.value);
+
+          if (isNaN(usage) || isNaN(efficiency)) {
+            ElMessage.error('请输入有效的数字');
+            valid = false;
+          } else {
+            totalFlux += usage * efficiency;
+          }
+        }
+      }
+
+      if (irrigatedLand.value) {
+        if (!irrigatedWaterUsage.value || !irrigatedEfficiency.value) {
+          ElMessage.warning('请输入水浇地的灌溉水用量和灌溉水有效利用率');
+          valid = false;
+        } else {
+          const usage = parseFloat(irrigatedWaterUsage.value);
+          const efficiency = parseFloat(irrigatedEfficiency.value);
+
+          if (isNaN(usage) || isNaN(efficiency)) {
+            ElMessage.error('请输入有效的数字');
+            valid = false;
+          } else {
+            totalFlux += usage * efficiency;
+          }
+        }
+      }
+
+      if (dryLand.value) {
+        if (!dryWaterUsage.value || !dryEfficiency.value) {
+          ElMessage.warning('请输入旱地的灌溉水用量和灌溉水有效利用率');
+          valid = false;
+        } else {
+          const usage = parseFloat(dryWaterUsage.value);
+          const efficiency = parseFloat(dryEfficiency.value);
+
+          if (isNaN(usage) || isNaN(efficiency)) {
+            ElMessage.error('请输入有效的数字');
+            valid = false;
+          } else {
+            totalFlux += usage * efficiency;
+          }
+        }
+      }
+
+      if (valid) {
+        fluxResult.value = totalFlux;
+        ElMessage.success(`灌溉水输入通量为: ${totalFlux.toFixed(2)} m³`);
+      }
+    };
+
+    return {
+      waterLand,
+      irrigatedLand,
+      dryLand,
+
+      irrigationWaterUsage,
+      irrigationEfficiency,
+
+      irrigatedWaterUsage,
+      irrigatedEfficiency,
+
+      dryWaterUsage,
+      dryEfficiency,
+
+      calculateFlux,
+      fluxResult
+    };
+  }
+};
+</script>
+
+<style scoped>
+.irrigation-management {
+  padding: 20px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.gradient-card {
+  /* 半透明渐变背景 */
+  background: linear-gradient(
+    135deg, 
+    rgba(250, 253, 255, 0.8), 
+    rgba(137, 223, 252, 0.8)
+  );
+  width: 80%;
+  max-width: 600px;
+  padding: 25px;
+  box-sizing: border-box;
+  border-radius: 12px;
+  border: none;
+  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
+  backdrop-filter: blur(5px); /* 添加模糊效果增强半透明感 */
+}
+
+.el-checkbox {
+  margin-bottom: 10px;
+  font-weight: 500;
+}
+
+.el-input {
+  width: 100%;
+}
+
+/* 使用 Vue 3 推荐的 :deep() 选择器 */
+:deep(.el-input) .el-input__inner {
+  border-radius: 6px;
+  border: 1px solid #dcdfe6;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
+  background: rgba(255, 255, 255, 0.03); /* 输入框半透明白色背景 */
+}
+
+:deep(.el-input) .el-input__inner:focus {
+  border-color: #409EFF;
+  box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
+}
+
+.calculate-btn {
+  width: 100%;
+  max-width: 300px;
+  height: 50px;
+  border: none;
+  border-radius: 25px !important;
+  font-size: 18px;
+  font-weight: bold;
+  transition: all 0.4s ease;
+  
+  /* 渐变背景色 */
+  background: linear-gradient(to right, #8DF9F0, #26B046);
+  color: white !important;
+  /* 按钮整体阴影 */
+  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15),
+              0 2px 6px rgba(38, 176, 70, 0.3) inset;
+}
+
+.calculate-btn:hover {
+  transform: scale(1.03);
+  box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2),
+              0 2px 8px rgba(38, 176, 70, 0.4) inset;
+  background: linear-gradient(to right, #7de8df, #20a03d);
+}
+
+.calculate-btn:active {
+  transform: scale(0.98);
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1),
+              0 1px 6px rgba(38, 176, 70, 0.4) inset;
+}
+</style>

+ 109 - 0
src/views/User/HmOutFlux/irrigationWater/irriWaterSampleData.vue

@@ -0,0 +1,109 @@
+<template>
+  <div class="page-container">
+    
+   <div class="point-map">
+    <div class="component-title">采样点地图展示</div>
+    <div class="map-holder">
+        <irrwatermap/>
+    </div>
+   </div>
+
+  
+   <div class="point-line">
+    <div class="component-title">采样点数据列表展示</div>
+    <Waterdataline/>
+   </div>
+
+   <div class="charts-line">
+    <div class="component-title">韶关市各区县重金属平均浓度</div>
+    <Waterassaydata2/>
+   </div>
+  </div>
+</template>
+
+<script setup>
+import Waterdataline from './waterdataline.vue';
+import Waterassaydata2 from './waterassaydata2.vue';
+import irrwatermap from './irrwatermap.vue';
+
+
+
+
+</script>
+
+<style scoped>
+.map-holder {
+  position: relative;
+  height: calc(100% - 40px); /* 减去标题高度,避免覆盖 */
+  z-index: 100;
+}
+.page-container {
+  display: flex;
+  flex-direction: column;
+  height: 100vh; /* 整屏高度 */
+  padding: 0;
+  box-sizing: border-box;
+  background-color: #f5f7fa;
+  gap: 20px;
+  margin: 0;
+  position: relative !important; /* 防止被 Leaflet 修改 */
+  overflow: visible !important; /* 确保标题不被裁剪 */
+}
+
+.point-map {
+    flex: 2; /* 自适应剩余空间 */
+  min-height: 600px; /* 最小高度保证显示 */
+  max-height: 70vh; /* 最大高度限制,避免溢出 */
+  margin-bottom: 20px;
+  background-color: white;
+  border-radius: 8px;
+  box-shadow: 0 2px 4px rgba(0,0,0,0.1); /* 弱化阴影,避免视觉冲突 */
+  position: relative;
+  overflow: hidden; /* 隐藏超出容器的内容 */
+}
+
+.point-line {
+    background-color: white;
+    border-radius: 12px;/*圆角 */
+    box-shadow: 0 2px 8px rgba(0, 0,0, 0.08);
+    padding: 16px;/*内部间距 */
+    box-sizing: border-box;
+}
+.charts-line{
+  background-color: white;
+  border-radius: 12px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+  padding: 16px;
+  box-sizing: border-box;
+  max-width: 1800px;
+  width: 100%;
+  margin: 20px auto;
+}
+.component-title {
+  /* 基础布局:左对齐 + 紧凑间距 */
+  text-align: left;        /* 强制左对齐,告别居中 */
+  margin: 12px 0 5px;          /* 上下间距缩小,更紧凑(原16px→12px) */
+  padding-left: 24px;      /* 给蓝色方块留空间 */
+  position: relative;      /* 为伪元素定位做准备 */
+  z-index: 10000;
+  /* 文字样式:简约但醒目 */
+  font-size: 1.7rem;      /* 稍小一号,更克制(原1.5rem→1.25rem) */
+  font-weight: 600;        /* 适度加粗,比正文突出但不夸张 */
+  color: #1e88e5;          /* 统一蓝色,和方块颜色呼应 */
+  line-height: 1.2;        /* 紧凑行高,避免臃肿 */
+}
+
+/* 蓝色小方块:用伪元素实现,无额外HTML */
+.component-title::before {
+  content: "";
+  position: absolute;
+  left: 0;                /* 靠最左侧 */
+  top: 50%;              /* 垂直居中 */
+  transform: translateY(-50%);
+  width: 12px;           /* 方块大小,适中即可 */
+  height: 12px;
+  background-color: #1e88e5; /* 和文字同色,统一感 */
+  border-radius: 2px;    /* 轻微圆角,比直角更柔和 */
+}
+
+</style>

+ 310 - 0
src/views/User/HmOutFlux/irrigationWater/irrwatermap.vue

@@ -0,0 +1,310 @@
+<template>
+  <div class="map-wrapper" @click.stop>
+    <div ref="mapContainer" class="map-container"></div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue';
+import L from 'leaflet';
+import 'leaflet/dist/leaflet.css';
+
+const mapContainer = ref(null);
+
+
+
+onMounted(() => {
+  // 初始化地图(强制确保容器可用)
+  if (!mapContainer.value) {
+    console.error('❌ 地图容器未找到!');
+    return;
+  }
+
+  // 定义位置格式化函数(处理"广东省韶关市"前缀)
+  const formatLocation = (fullLocation) => {
+    if (!fullLocation) return '未知位置'; // 处理空值
+    // 移除前缀并清理空格
+    const processed = fullLocation.replace(/^广东省韶关市/, '').trim();
+    // 处理移除后为空的情况
+    return processed || '未知位置';
+  };
+
+  const map = L.map(mapContainer.value, {
+    center: [25, 114], // 韶关大致中心  前大往下,后大往左
+    zoom: 8.5,
+    minZoom:8.3,
+  });
+
+  // 区县颜色映射(与GeoJSON的properties.name严格匹配)
+  const districtColorMap = {
+    "武江区": "#FF6B6B",
+    "浈江区": "#4ECDC4",
+    "曲江区": "#FFD166",
+    "始兴县": "#A0DAA9",
+    "仁化县": "#6A0572",
+    "翁源县": "#1A535C",
+    "乳源瑶族自治县": "#FF9F1C",
+    "新丰县": "#87CEEB",
+    "乐昌市": "#118AB2",
+    "南雄市": "#06D6A0",
+  };
+
+  // 加载区县边界(带完整错误处理)
+  fetch('/data/韶关市各区县边界图.geojson')
+    .then(res => {
+      if (!res.ok) throw new Error(`区县边界加载失败:${res.status}`);
+      return res.json();
+    })
+    .then(geojson => {
+      console.log('✅ 区县边界数据加载完成,要素数:', geojson.features.length);
+      
+      L.geoJSON(geojson, {
+        style: (feature) => {
+          const districtName = feature.properties.name; // 匹配GeoJSON的name字段
+          const color = districtColorMap[districtName] || '#cccccc';
+          return {
+            fillColor: color,
+            fillOpacity: 0.7,
+            color: '#333333', // 边界颜色
+            weight: 2,        // 边界宽度
+          };
+        },
+      }).addTo(map);
+
+      // 加载水系图(新增,带样式和错误处理)
+      fetch('/data/韶关市河流水系图.geojson')
+        .then(res => {
+          if (!res.ok) throw new Error(`水系图加载失败:${res.status}`);
+          return res.json();
+        })
+        .then(waterGeojson => {
+          console.log('✅ 水系图数据加载完成,要素数:', waterGeojson.features.length);
+          
+          L.geoJSON(waterGeojson, {
+            style: {
+              color: '#0066CC', // 水系颜色
+              weight: 2,       // 线条宽度
+              opacity: 0.8,    // 透明度
+            },
+          }).addTo(map);
+
+          // 加载采样点和检测数据(并行请求)
+          Promise.all([
+            fetch('http://localhost:3000/table/Water_sampling_data').then(res => res.json()),
+            fetch('http://localhost:3000/table/Water_assay_data').then(res => res.json())
+          ])
+          .then(([samplingData, assayData]) => {
+            console.log('✅ 采样点数据:', samplingData.length, '条;检测数据:', assayData.length, '条');
+            
+            // 构建检测数据映射
+            const assayMap = {};
+            assayData.forEach(item => {
+              assayMap[item.water_sample_ID] = item;
+            });
+
+            let markerCount = 0;
+            samplingData.forEach((sample, idx) => {
+              try {
+                // 打印原始数据(用于后续分析)
+               // console.log(`🔍 处理采样点 #${idx + 1}:`, sample);
+                
+                // 智能提取经纬度字段(支持多种可能的字段名)
+                const latField = ['latitude', 'lat', 'Latitude', 'Lat'].find(key => sample[key] !== undefined);
+                const lngField = ['longitude', 'lng', 'Longitude', 'Lng'].find(key => sample[key] !== undefined);
+                
+                if (!latField || !lngField) {
+                  console.error(`❌ 未找到经纬度字段(第${idx}条):`, sample);
+                  return;
+                }
+                
+                //  清理并转换经纬度(处理特殊字符和逗号)
+                const cleanLat = String(sample[latField]).replace(/[^\d.-]/g, '');
+                const cleanLng = String(sample[lngField]).replace(/[^\d.-]/g, '');
+                
+                // 强制四舍五入到6位小数(避免精度问题)
+                const lat = parseFloat(parseFloat(cleanLat).toFixed(6));
+                const lng = parseFloat(parseFloat(cleanLng).toFixed(6));
+                
+                // 范围校验(扩大范围10%,兼容边界值)
+                if (isNaN(lat) || isNaN(lng) || lat < 22.7 || lat > 25.5 || lng < 112.7 || lng > 115.3) {
+                  console.warn(`❌ 坐标超出合理范围(第${idx}条):`, lat, lng, sample);
+                  return;
+                }
+                
+                // 增强型检测数据匹配(忽略大小写 + 部分匹配)
+                const sampleId = String(sample.water_sample_ID || '').trim().toLowerCase();
+                let data = assayMap[sampleId];
+                
+                // 若未找到,尝试模糊匹配
+                if (!data && sampleId) {
+                  data = Object.values(assayMap).find(
+                    item => item.water_sample_ID?.toLowerCase().includes(sampleId)
+                  );
+                  
+                  if (data) {
+                    console.log(`⚠️ 部分匹配检测数据:${sampleId} → ${data.water_sample_ID}`);
+                  }
+                }
+                
+                if (!data) {
+                  console.warn(`❌ 无匹配检测数据(ID: ${sampleId},第${idx}条)`);
+                  return;
+                }
+                
+                // 创建标记点(使用 L.circleMarker 而非 L.marker)
+                const marker = L.circleMarker([lat, lng], {
+                  radius: 4,                  // 增大圆点半径,确保可见
+                  color: '#FF3333',           // 边框颜色(红色)
+                  fillColor: '#FF3333',       // 填充颜色(红色)
+                  fillOpacity: 0.9,           // 填充透明度(接近不透明)
+                  weight: 2,                  // 边框宽度(加粗)
+                  zIndexOffset: 1000,         // 提高层级,确保在所有图层之上
+                }).addTo(map);
+
+                
+                
+               marker.bindPopup(`
+                 <div class="popup-container">
+                   <!-- 1. 顶部标题(假设地区信息来自 sample.location,若无则固定) -->
+                   <h3 class="popup-title">${formatLocation(sample.sampling_location) }</h3>
+                   <div class="popup-divider"></div> <!-- 分隔线 -->
+    
+                   <!-- 3. 检测数据表格 -->
+                   <table class="popup-table">
+                     <thead>
+                       <tr>
+                         <th>检测项</th>
+                         <th>数值</th>
+                       </tr>
+                     </thead>
+                     <tbody>
+                       <tr><td>Ph</td><td>${data.pH || '未知'}</td></tr>
+                       <tr><td>铬(Cr)</td><td>${data.Cr || '未知'}</td></tr>
+                       <tr><td>砷(As)</td><td>${data.As || '未知'}</td></tr>
+                       <tr><td>镉(Cd)</td><td>${data.Cd || '未知'}</td></tr>
+                       <tr><td>汞(Hg)</td><td>${data.Hg || '未知'}</td></tr>
+                       <tr><td>铅(Pb)</td><td>${data.Pb || '未知'}</td></tr>
+                     </tbody>
+                   </table>
+                 </div>
+               `);
+                
+                markerCount++;
+              } catch (err) {
+                console.error(`❌ 处理采样点失败(第${idx}条):`, err);
+              }
+            });
+
+            console.log(`✅ 成功创建 ${markerCount} 个标记点`);
+          })
+          .catch(err => {
+            console.error('❌ 采样/检测数据加载失败:', err);
+            alert('数据接口错误:' + err.message);
+          });
+        })
+        .catch(err => {
+          console.error('❌ 水系图加载失败:', err);
+          alert('水系图加载错误:' + err.message);
+        });
+    })
+    .catch(err => {
+      console.error('❌ 区县边界加载失败:', err);
+      alert('区县边界加载错误:' + err.message);
+    });
+});
+</script>
+
+<style scoped>
+.map-wrapper {
+  width:100%;
+  height: 100%;
+  position: relative;
+  z-index: 100;
+}
+.map-container {
+  width: 100% !important;
+  height: 100% !important;
+}
+
+/*  标题和分隔线 */
+::v-deep .popup-title {
+  text-align: center;       /* 居中 */
+  font-size: 16px;          /* 减小字号 */
+  font-weight: 700;         /* 加粗 */
+  color: #0066CC;           /* 蓝色,匹配设计 */
+  margin: 0 0 4px;          /* 间距调整 */
+  border-bottom: 2px solid #0066CC; /* 底部横线 */
+  padding-bottom: 4px;      /* 横线与文字间距 */
+}
+
+::v-deep .popup-divider {
+  height: 1px;              /* 横线高度 */
+  background: #0066CC;      /* 横线颜色 */
+  margin: 6px 0;            /* 上下间距 */
+}
+
+/*  表格样式 */
+::v-deep .popup-table {
+  width: 100%;              /* 占满容器 */
+  border-collapse: collapse;/* 合并边框 */
+  margin-top: 12px;         /* 与段落间距 */
+}
+
+::v-deep .popup-table th,
+::v-deep .popup-table td {
+  border: 1px solid #CCCCCC;/* 单元格边框 */
+  padding: 4px 6px;         /* 内边距 */
+  text-align: center;       /* 内容居中 */
+  font-size: 12px;          /* 字号调整 */
+}
+
+::v-deep .popup-table th {
+  background: #F5F5F5;      /* 表头背景色 */
+  font-weight: 600;         /* 表头加粗 */
+}
+
+/* 美化弹窗(完整层级穿透) */
+::v-deep .leaflet-popup-content-wrapper {
+  padding: 0 !important;
+  border-radius: 12px !important;
+  box-shadow: 0 4px 12px rgba(0,0,0,0.15) !important;
+}
+
+::v-deep .leaflet-popup-content {
+  margin: 0 !important;
+  width: auto !important;
+  max-width: 220px !important;
+}
+
+::v-deep .popup-container {
+  min-width: 180px;
+  max-width: 220px;
+  padding: 10px;/**内边距 */
+  font-family: "Microsoft YaHei", sans-serif;
+}
+
+::v-deep .popup-content p {
+  margin: 6px 0;
+  font-size: 15px;
+  color: #666;
+  line-height: 1.6;
+}
+
+::v-deep .popup-content strong {
+  color: #FF3333; /* 与标记点颜色呼应 */
+  font-weight: 600;
+}
+
+/* 可选:隐藏弹窗箭头,更像卡片 */
+::v-deep .leaflet-popup-tip {
+  display: none;
+}
+
+/* 临时调试:确保标记点可见 */
+::v-deep .leaflet-marker-icon {
+  display: none !important; /* 隐藏默认标记图标 */
+}
+::v-deep .leaflet-circle-marker {
+  stroke-width: 2px !important;
+}
+</style>

+ 87 - 0
src/views/User/HmOutFlux/irrigationWater/riverwaterassay.vue

@@ -0,0 +1,87 @@
+<template>
+  <div class="map-container">
+    <div id="water-system-map"></div>
+  </div>
+</template>
+
+<script setup>
+//水系图的转换
+import 'leaflet/dist/leaflet.css';
+import { onMounted, onUnmounted } from 'vue';
+import L from 'leaflet';
+
+let map; // 声明为全局变量,避免被Vue垃圾回收
+
+onMounted(() => {
+  // 初始化地图容器尺寸
+  const mapContainer = document.getElementById('water-system-map');
+  mapContainer.style.width = '100%';
+  mapContainer.style.height = '600px';
+
+  // 初始化地图(经纬度、缩放级别可根据GeoJSON数据调整)
+  const map = L.map('water-system-map').setView([24.88, 113.62], 9);
+
+
+  // 加载GeoJSON
+  fetch('/data/韶关市河流水系图.geojson')
+    .then(res => {
+      if (!res.ok) {
+        throw new Error('GeoJSON加载失败');
+      }
+      return res.json();
+    })
+    .then(geojson => {
+      // 添加水系样式(可自定义颜色、宽度)
+      L.geoJSON(geojson, {
+        style: {
+          color: '#0066cc',    // 蓝色线条
+          weight: 2,           // 线条宽度
+          opacity: 0.8,        // 透明度
+          lineJoin: 'round'    // 拐角圆润
+        },
+        // 可选:添加鼠标悬停效果
+        onEachFeature(feature, layer) {
+          layer.on('mouseover', function() {
+            this.setStyle({ color: '#ff3300', weight: 3 }); // 悬停变红加粗
+          });
+          layer.on('mouseout', function() {
+            this.setStyle({ color: '#0066cc', weight: 2 }); // 离开恢复
+          });
+        }
+      }).addTo(map);
+    })
+    .catch(err => {
+      console.error('加载GeoJSON失败:', err);
+      alert('水系图加载失败,请检查文件路径');
+    });
+
+  // 监听窗口Resize,适配地图尺寸
+  window.addEventListener('resize', handleResize);
+});
+
+onUnmounted(() => {
+  // 组件销毁时移除事件监听,避免内存泄漏
+  window.removeEventListener('resize', handleResize);
+  if (map) {
+    map.remove();
+    map = null;
+  }
+});
+
+// 窗口Resize处理函数
+function handleResize() {
+  if (map) {
+    map.invalidateSize();
+  }
+}
+</script>
+
+<style scoped>
+.map-container {
+  width: 100%;
+  height: 600px; /* 确保父容器有高度 */
+}
+.leaflet-default-icon-path {
+  background-image: url('https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png');
+}
+</style>

+ 75 - 0
src/views/User/HmOutFlux/irrigationWater/samplingMethodDevice1.vue

@@ -0,0 +1,75 @@
+<template>
+  <div class="sampling-process">
+    <el-card shadow="always" style="margin-bottom: 20px;">
+      <h2>1.采样容器与过程</h2>
+      <p>采样容器均为500mL的白色聚乙烯瓶,采样体积均为500mL,采样过程在不同天气条件下进行,主要天气状况包括多云、阴天和小雨,采样点周边环境主要为河流,只有少数样品采集于水渠或瀑布区域。</p>
+      <div class="image-row">
+        <el-image :src="image1" alt="图1" style="width: 30%; margin-right: 2%"></el-image>
+        <el-image :src="image2" alt="图2" style="width: 30%; margin-right: 2%"></el-image>
+        <el-image :src="image3" alt="图3" style="width: 30%;"></el-image>
+      </div>
+      <p class="caption">图 3 灌溉水采样设备</p>
+
+      <h2>2.样品保存与现场情况</h2>
+      <p>绝大多数样品状态为无色、无沉淀、无味、无悬浮物,只有少量样品稍显浑浊并含有沉淀物,为了保证样品的完整性和数据的准确性,采样后的保存方式包括了冷藏、避光、确保标签完好以及采取有效的减震措施,以避免运输过程中的振动和损坏。</p>
+      <div class="image-row">
+        <el-image :src="fieldImage1" alt="图4-1" style="width: 30%; margin-right: 2%"></el-image>
+        <el-image :src="fieldImage2" alt="图4-2" style="width: 30%; margin-right: 2%"></el-image>
+        <el-image :src="fieldImage3" alt="图4-3" style="width: 30%;"></el-image>
+      </div>
+      <p class="caption">图 4 工作人员采样现场</p>
+    </el-card>
+  </div>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      image1: '/图1.png', 
+      image2: '/图片2.png',
+      image3: '/图片3.png',
+      fieldImage1: '/图片4.jpg',
+      fieldImage2: '/图片5.jpg',
+      fieldImage3: '/图片6.jpg'
+    };
+  }
+};
+</script>
+
+<style scoped>
+.sampling-process {
+  padding: 20px;
+}
+.el-card {
+  background-color: rgba(255, 255, 255, 0.3); 
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+  border-radius: 8px;
+}
+.el-card h2 {
+  font-size: 24px;
+  margin-bottom: 10px;
+}
+
+P {
+  text-indent: 2em;
+}
+.el-card p {
+  line-height: 1.6;
+}
+.image-row {
+  display: flex;
+  justify-content: space-between;
+  margin-top: 10px;
+}
+.el-image {
+  border: 1px solid #ebeef5;
+  border-radius: 4px;
+}
+.caption {
+  text-align: center;
+  font-size: 14px;
+  color: #000; /* 确保图注为黑色 */
+  margin-top: 10px;
+}
+</style>

+ 178 - 208
src/views/User/heavyMetalFluxCalculation/inputFluxCalculation/waterdata/tencentMapView.vue → src/views/User/HmOutFlux/irrigationWater/tencentMapView.vue

@@ -10,12 +10,12 @@
 import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
 import axios from 'axios'
 import {wgs84togcj02} from 'coordtransform';
-
 const farmlandLayer = ref(null);
 const isMapReady = ref(false)
 const mapContainer = ref(null)
 const error = ref(null)
 const TMap = ref(null);
+let districtBoundaryLayer = null;
 let activeTempMarker = ref(null)
 let infoWindow = ref(null)
 let map = null
@@ -26,8 +26,8 @@ const state = reactive({
   showOverlay: false,
   showSoilTypes: true,
   showSurveyData: true,
-  shoeWaterSystem:true,
-  excelData: [],
+  showWaterSystem:true,
+  excelData: [],//标记点数据
   lastTapTime: 0
 })
 
@@ -104,7 +104,7 @@ const initData =async ()=>{
         longitude:lng,
       };
     }).filter(item=>item !==null)
-    console.log('成功加载${state.excelData.length}条有效数据');
+    console.log(`成功加载${state.excelData.length}条有效数据`);
   }catch(err){
     console.error('数据初始化失败:',err);
     error.value = err.message;
@@ -123,21 +123,16 @@ const initMap = async () => {
     //console.log('开始创建地图实例');
     
     map = new TMap.value.Map(mapContainer.value, {
-      center: new TMap.value.LatLng(24.55,114.2),//前大往下,后大往左
-      zoom: 9,
-      minZoom:8.5,
-      maxZoom:12,
+      center: new TMap.value.LatLng(24.25,114.5),//前大往下,后大往左
+      zoom:9,
+      zoomControl:true,
       renderOptions: {
         preserveDrawingBuffer: true, // 必须开启以支持截图
         antialias: true
       },
-      restrictBounds: new TMap.value.LatLngBounds(
-    new TMap.value.LatLng(24.8, 113.7), // 西南角(最南最西)
-    new TMap.value.LatLng(25.2, 114.0)  // 东北角(最北最东)
-  )
     })
     //console.log('地图实例创建成功,开始创建markersLayer');
-    
+    //console.log('当前地图样式ID:', map.getMapStyleId());
      if (markersLayer) {
     markersLayer.setMap(null);
     markersLayer = null;
@@ -148,14 +143,14 @@ const initMap = async () => {
   zIndex:1000,
   styles: {
     default: new TMap.value.MarkerStyle({
-      width: 30, // 图标宽度
-      height: 30, // 图标高度
+      width: 15, // 图标宽度
+      height: 15, // 图标高度
       anchor: { x: 12.5, y: 12.5 }, // 居中定位
-      src: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTIgMTcuMjdsNi4xOCAzLjYzLTEuNjQtNy4wMyA1LjM0LTQuNjMtNy4xOS0uNjFMMTIgM2wtMy4xOSA2LjYzLTcuMTkuNjFMMTAuNDYgMTMuODkgOC44MiAyMC45IDE4IDE3LjI3eiIgZmlsbD0iI0ZGMDAwMCIvPjwvc3ZnPg=='
+      src: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMCIgaGVpZ2h0PSIzMCI+PGNpcmNsZSBjeD0iMTUiIGN5PSIxNSIgcj0iMTAiIGZpbGw9InJlZCIvPjwvc3ZnPg=='
       })
     }
   });
-    console.log('markersLayer是否绑定地图',markersLayer.getMap() === map);
+    console.log('markersLayer是否绑定地图:',markersLayer.getMap() === map);
     
 
     // 创建土壤类型多边形图层
@@ -183,8 +178,10 @@ const initMap = async () => {
     
 
      // 6. 绑定事件
-    map.on('click', handleMapClick);    
+     
     //console.log('地图实例创建完成,开始加载水系图');
+    //加载区县边界
+    await loadDistrictBoundaries();
     await loadWaterSystemGeoJSON(); // 等待水系图加载完成
 
     // 标记地图就绪
@@ -198,7 +195,6 @@ const initMap = async () => {
       //console.log('地图初始化完成');
       //console.log('标记点图层初始化:',markersLayer.value);
     })
-
   } catch (err) {
     isMapReady.value = true;
     console.error('initMap执行异常:',err);
@@ -209,90 +205,176 @@ const initMap = async () => {
 // 加载水系 GeoJSON 并添加到地图
 const loadWaterSystemGeoJSON = async () => {
   try {
-    // 1. 请求 GeoJSON 文件(路径根据实际存放位置修改,如 public 目录下的 water_system.geojson)
-    const response = await fetch('/data/韶关市河流水系图.geojson');
+    const url = `${window.location.origin}/data/韶关市河流水系图.geojson`;
+    console.log('加载水系:', url);
+    
+    const response = await fetch(url);
+    
+    // 检查响应状态
+    if (!response.ok) {
+      const errorText = await response.text();
+      throw new Error(`HTTP错误 ${response.status}: ${errorText.substring(0, 100)}`);
+    }
+    
     const geojson = await response.json();
-    //console.log('水系 GeoJSON 加载成功,要素数量:', geojson.features.length);
+    console.log('成功加载水系GeoJSON:', geojson.features.length, '个要素');
+    
+    // 创建腾讯地图可用的坐标转换函数
+    const processCoordinates = (coords) => {
+      const [gcjLng, gcjLat] = wgs84togcj02(coords[0], coords[1]);
+      return new TMap.value.LatLng(gcjLat, gcjLng);
+    };
 
-    // 2. 销毁旧图层(避免重复加载)
+    // 销毁旧图层
     if (waterSystemLayer) {
       waterSystemLayer.setMap(null);
       waterSystemLayer = null;
     }
 
-    // 3. 根据 GeoJSON 类型创建图层(水系通常是 LineString,用 MultiPolyline)
+    // 创建水系图层
     waterSystemLayer = new TMap.value.MultiPolyline({
-      map: map, // 绑定到地图实例
+      map: map,
       styles: {
         default: new TMap.value.PolylineStyle({
-          color: '#0066cc', // 水系线条颜色(蓝色)
-          width: 2,         // 线条宽度
-          opacity: 0.8,     // 透明度
-          lineCap: 'round', // 线条端点圆润
-          lineJoin: 'round' // 线条拐角圆润
+          color: '#0066cc',
+          width: 2,
+          opacity: 0.8,
+          lineCap: 'round',
+          lineJoin: 'round'
         })
       },
       geometries: geojson.features
-        .filter(feature =>{
-    const type = feature.geometry.type;
-    //console.log('要素类型:', type); // 调试:打印每个要素的类型
-    return type === 'LineString' || type === 'MultiLineString';
-     }) // 筛选线要素
+        .filter(feature => {
+          return feature.geometry.type === 'LineString' || 
+                 feature.geometry.type === 'MultiLineString';
+        })
         .map(feature => {
           let paths = [];
+          
           if (feature.geometry.type === 'LineString') {
-          paths = feature.geometry.coordinates.map(coord => {
-           const [gcjLng, gcjLat] = wgs84togcj02(coord[0], coord[1]); // WGS84 → GCJ02
-            return new TMap.value.LatLng(gcjLat, gcjLng);
-          });
-           } else if (feature.geometry.type === 'MultiLineString') {
-           paths = feature.geometry.coordinates.map(line => 
-           line.map(coord => {
-           const [gcjLng, gcjLat] = wgs84togcj02(coord[0], coord[1]);
-           return new TMap.value.LatLng(gcjLat, gcjLng);
-    })
-  );
-}
-    //console.log('转换后的路径长度:', paths.length); // 调试:确保有坐标
-    return {
-      id: feature.id || `water_${Date.now()}`,
-      styleId: 'default',
-      paths: paths,
-      properties: feature.properties
-    };
+            paths = feature.geometry.coordinates.map(processCoordinates);
+          } else {
+            paths = feature.geometry.coordinates.map(line => 
+              line.map(processCoordinates)
+            );
+          }
+          
+          return {
+            id: feature.id || `water_${Date.now()}`,
+            styleId: 'default',
+            paths: paths,
+            properties: feature.properties
+          };
         })
     });
+    console.log('水系图层加载完成');
+
+  } catch (err) {
+    console.error('水系加载失败:', err.message);
+    error.value = `水系图加载失败: ${err.message}`;
+  }
+};
 
-   // console.log('水系图层加载完成');
 
-    // 4. 修正:遍历几何要素,合并边界
-    if (waterSystemLayer) {
-      const geometries = waterSystemLayer.getGeometries(); // 获取所有几何要素
-      if (geometries.length === 0) {
-        console.warn('水系图层无有效几何要素');
-        return;
-      }
+// 加载区县边界数据
+const loadDistrictBoundaries = async () => {
+  try {
+    const url = '/data/韶关市各区县边界图.geojson';
+    console.log('加载区县边界:', url);
+    
+    const response = await fetch(url);
+    
+    // 打印响应状态和头信息
+    console.log('HTTP状态码:', response.status);
+    console.log('内容类型:', response.headers.get('content-type'));
+    console.log('内容长度:', response.headers.get('content-length'));
+    
+    // 检查响应状态
+    if (!response.ok) {
+      const errorText = await response.text();
+      throw new Error(`HTTP错误 ${response.status}: ${errorText.substring(0, 100)}`);
+    }
+    
+    const geojson = await response.json();
+    console.log('成功加载区县GeoJSON:', geojson.features.length, '个要素');
+
+    // 1. 定义颜色数组(顺序与 geojson.features 中的区县顺序一致)
+    const districtColorMap = {
+      '武江区': '#FF6B6B',
+      '浈江区': '#4ECDC4',
+      '曲江区': '#FFD166',
+      '始兴县': '#A0DAA9',
+      '仁化县': '#6A0572',
+      '翁源县': '#1A535C',
+      '乳源瑶族自治县': '#FF9F1C', // 修正:匹配 GeoJSON 的“乳源瑶族自治县”
+      '新丰县': '#87CEEB', // 新增:为“新丰县”配置颜色(可自定义)
+      '乐昌市': '#118AB2',
+      '南雄市': '#06D6A0'
+    };
 
-      // 初始化边界为第一个要素的边界
-      let bounds = geometries[0].getBounds(); 
-      // 合并剩余要素的边界
-      for (let i = 1; i < geometries.length; i++) {
-        bounds.extend(geometries[i].getBounds()); 
+    // 2. 处理几何数据:为每个区县分配 styleId(用索引,与颜色数组对应)
+    const geometries = geojson.features.map(feature => {
+      const districtName = feature.properties.name;
+      console.log('GEOJSON中的区县名称',districtName);
+      
+      const color = districtColorMap[districtName] ||'#ccc';
+      // 坐标转换(WGS84 → GCJ02,确保边界在正确位置)
+      let paths = [];
+      if (feature.geometry.type === 'Polygon') {
+        paths = feature.geometry.coordinates.map(ring => 
+          ring.map(coord => {
+            const [gcjLng, gcjLat] = wgs84togcj02(coord[0], coord[1]);
+            return new TMap.value.LatLng(gcjLat, gcjLng);
+          })
+        );
+      } else if (feature.geometry.type === 'MultiPolygon') {
+        paths = feature.geometry.coordinates.map(polygon => 
+          polygon.map(ring => 
+            ring.map(coord => {
+              const [gcjLng, gcjLat] = wgs84togcj02(coord[0], coord[1]);
+              return new TMap.value.LatLng(gcjLat, gcjLng);
+            })
+          )
+        );
       }
 
-      // 适配地图视野
-      map.fitBounds(bounds, { padding: [50, 50] }); 
+      // 关键:styleId 设为索引(与颜色数组索引对应)
+      return {
+        id: `district-${districtName}`,
+        styleId: `style-${districtName}`,
+        paths:paths
+      };
+    });
+
+    districtColorMap['武江区'] = '#FF0000'; // 强制武江区为红色
+    // 3. 构建样式对象(key 与 styleId 一致)
+    const styles = {};
+    for(const name in districtColorMap){
+      styles[`style-${name}`]=new TMap.value.PolygonStyle({
+        fillColor:districtColorMap[name],
+        fillOpacity:0.7,
+        strokeColor:'#333',
+        strokeWidth:2
+      })
+       console.log(`区县${name}的样式颜色:`, styles[`style-${name}`].getFillColor());
     }
 
+    // 4. 创建图层并应用样式
+    districtBoundaryLayer = new TMap.value.MultiPolygon({
+      map: map,
+      geometries: geometries,
+      styles:styles
+    });
+    console.log('区县样式对象:', styles);
+    districtBoundaryLayer.setStyles(styles); // 样式生效
+    
   } catch (err) {
-    console.error('水系 GeoJSON 加载失败:', err);
-    error.value = `水系图加载失败:${err.message}`;
+    console.error('加载区县边界失败:', err.message);
+    error.value = `区县边界加载失败: ${err.message}`;
   }
 };
 
 
-
-
 // 更新标记点,添加Label显示
 const updateMarkers = () => {
   // 正确的标记点创建方式
@@ -397,7 +479,7 @@ const handleMarkerClick = async(e) => {
     <div class="divider"></div>
     
     <!-- 重金属区 -->
-    <h4 class="contaminant-title">重金属含量 (mg/L)</h4>
+    <h4 class="contaminant-title">重金属含量 (ug/L)</h4>
     <div class="contaminant-grid">
       <div class="contaminant-item">
         <span class="contaminant-name">Cr:</span>
@@ -441,139 +523,6 @@ const handleMarkerClick = async(e) => {
     infoWindow.value.setContent(errorContent);
   }
 }
-  
-
-
- const manageTempMarker = {
-  add: (lat, lng, phValue) => {
-    if (activeTempMarker.value) {
-      markersLayer.remove("-999")
-    }
-    
-    const tempMarker = markersLayer.add({
-      id: "-999",
-      position: new TMap.value.LatLng(lat, lng),
-      styleId: 'temp',
-      properties: {
-        title: '克里金插值',
-        phValue: parseFloat(phValue).toFixed(2),
-        isTemp: true
-      }
-    })
-    activeTempMarker.value = tempMarker
-  },
-  remove: () => {
-    if (activeTempMarker.value) {
-      markersLayer.remove("-999")
-      activeTempMarker.value = null
-    }
-  }
-}
-
- const handleMapClick = async (e) => {
-  if (selectedPolygon.value) {
-    resetPolygonStyle();
-     infoWindow.value?.close();
-   }
-   const now = Date.now()
-  
-   if (now - state.lastTapTime < 1000) return
-   state.lastTapTime = now
-
-   try {
-     const latLng = e?.latLng
-     if (!latLng) throw new Error("地图点击事件缺少坐标信息")
-
-    const lat = Number(latLng.lat)
-     const lng = Number(latLng.lng)
-
-     if (!isValidCoordinate(lat, lng)) throw new Error(`非法坐标值 (${lat}, ${lng})`)
-
-     //console.log('有效坐标:', lat, lng)
-
-     const result = await reverseGeocode(lat, lng)
-     if (!validateLocation(result)) throw new Error('非有效陆地区域')
-     const phValue = await getPhValue(lng, lat)
-
-    // 使用封装方法添加临时标记
-    manageTempMarker.add(lat, lng, phValue)
-
-     if (infoWindow.value) {
-       infoWindow.value.close()
-     }
-    infoWindow.value = new TMap.value.InfoWindow({
-       map: map,
-       position: new TMap.value.LatLng(lat,lng),
-       content: `
-         <div style="padding:12px">
-           <h3>临时采样点</h3>
-           <p>位置:${result.address}</p>
-          <p>PH值:${phValue}</p>
-         </div>
-       `
-     })
-     infoWindow.value.open()
-   } catch (error) {
-     console.error('操作失败详情:', error)
-    error.value = error.message.includes('非法坐标') 
-       ? '请点击有效地图区域' 
-       : '服务暂时不可用,请稍后重试'
-     setTimeout(() => error.value = null, 3000)
-   }
- }
-
-
-
-// // 验证坐标有效性
- const isValidCoordinate = (lat, lng) => {
-   return !isNaN(lat) && !isNaN(lng) && 
-            lat >= -90 && lat <= 90 && 
-          lng >= -180 && lng <= 180
- }
-
-// // 逆地理编码
- const reverseGeocode = (lat, lng) => {
-   return new Promise((resolve, reject) => {
-     const callbackName = `tmap_callback_${Date.now()}`
-     window[callbackName] = (response) => {
-       delete window[callbackName]
-       document.body.removeChild(script)
-       if (response.status !== 0) reject(response.message)
-       else resolve(response.result)
-     }
-
-     const script = document.createElement('script')
-     script.src = `https://apis.map.qq.com/ws/geocoder/v1/?location=${lat},${lng}&key=${tMapConfig.key}&output=jsonp&callback=${callbackName}`
-     script.onerror = reject
-     document.body.appendChild(script)
-   })
- }
-
-// // 验证地理位置
- const validateLocation = (result) => {
-   if (!result || !result.address_component) {
-     return false;
-   }
-   return result.address_component.nation === '中国' &&
-          !['香港特别行政区', '澳门特别行政区', '台湾省'].includes(
-            result.address_component.province
-          )
- }
-
-// // 获取PH值
- const getPhValue = async (lng, lat) => {
-   try {
-     const { data } = await axios.post('https://soilgd.com:5000/kriging_interpolation', {
-       file_name: 'emissions.xlsx',
-       emission_column: 'dust_emissions',
-       points: [[lng, lat]]
-     })
-     return parseFloat(data.interpolated_concentrations[0]).toFixed(2)
-   } catch (error) {
-     console.error('获取PH值失败:', error)
-     throw error 
- }
- }
 
 
 
@@ -636,7 +585,7 @@ onUpdated(() => {
 
 </script>
 
-<style>
+<style scoped>
 .map-page {
   position: relative;
   width: 100vw;
@@ -1042,4 +991,25 @@ onUpdated(() => {
   100% { transform: scale(1); opacity: 0.8; }
 }
 
+/* 区县边界样式 */
+.district-boundary {
+  stroke: #333;
+  stroke-width: 1px;
+  fill-opacity: 0.6;
+  transition: fill-opacity 0.3s;
+}
+
+.district-boundary:hover {
+  fill-opacity: 0.8;
+  stroke-width: 2px;
+}
+
+.district-label {
+  font-size: 14px;
+  font-weight: bold;
+  text-anchor: middle;
+  pointer-events: none;
+  fill: #333;
+  text-shadow: 0 0 3px white, 0 0 3px white, 0 0 3px white;
+}
 </style>

+ 222 - 0
src/views/User/HmOutFlux/irrigationWater/waterassaydata1.vue

@@ -0,0 +1,222 @@
+<template>
+  <div class="boxplot-container">
+    <div ref="chartRef" style="width: 100%; height: 500px;"></div>
+  </div>
+</template>
+<!--各种重金属的箱图-->
+<script setup lang="ts">
+import * as echarts from 'echarts';
+import { ref, onMounted, onUnmounted } from 'vue';
+import axios from 'axios';
+
+// 明确定义数据类型
+interface HeavyMetalData {
+  sampleId: string;
+  Cr: number | null;
+  As: number | null;
+  Cd: number | null;
+  Hg: number | null;
+  Pb: number | null;
+}
+
+const METALS = ['Cr', 'As', 'Cd', 'Hg', 'Pb'] as const;
+type MetalType = typeof METALS[number];
+
+const METAL_LABELS: Record<MetalType, string> = {
+  Cr: '铬(Cr)',
+  As: '砷(As)',
+  Cd: '镉(Cd)',
+  Hg: '汞(Hg)',
+  Pb: '铅(Pb)'
+};
+
+// 图表变量
+const chartRef = ref<HTMLElement | null>(null);
+const chartInstance = ref<echarts.ECharts | null>(null);
+const metalData = ref<HeavyMetalData[]>([]);
+let resizeHandler: (() => void) | null = null; // 用于存储resize处理函数
+
+// 数据清洗函数
+const cleanData = (rawValue: any): number | null => {
+  if (typeof rawValue === 'string') {
+    const num = parseFloat(rawValue);
+    return isNaN(num) || num < 0 ? null : num;
+  }
+  return typeof rawValue === 'number' && rawValue >= 0 ? rawValue : null;
+};
+
+// 修复后的四分位数计算算法
+const calculateBoxplotStats = (values: number[]): [number, number, number, number, number] | null => {
+  if (values.length < 5) return null; // 至少需要5个数据点才能生成有效的箱线图
+  
+  // 升序排序
+  const sorted = [...values].sort((a, b) => a - b);
+  const n = sorted.length;
+
+  // 正确的分位位置计算
+  const quantile = (p: number) => {
+    const pos = (n + 1) * p;
+    const lowerIndex = Math.max(0, Math.min(n - 1, Math.floor(pos) - 1));
+    const fraction = pos - Math.floor(pos);
+    
+    if (lowerIndex >= n - 1) return sorted[n - 1];
+    return sorted[lowerIndex] + fraction * (sorted[lowerIndex + 1] - sorted[lowerIndex]);
+  };
+
+  return [
+    sorted[0],               // 最小值
+    quantile(0.25),          // Q1
+    quantile(0.5),           // 中位数
+    quantile(0.75),          // Q3
+    sorted[n - 1]            // 最大值
+  ];
+};
+
+// 渲染图表
+const renderBoxplot = () => {
+  if (!chartRef.value || metalData.value.length === 0) return;
+  
+  // 移除旧的resize监听器
+  if (resizeHandler) {
+    window.removeEventListener('resize', resizeHandler);
+  }
+
+  // 分组收集每种金属的有效数值
+  const metalValues = Object.fromEntries(
+    METALS.map(metal => [
+      metal, 
+      metalData.value
+        .map(item => item[metal])
+        .filter((val): val is number => val !== null)
+    ])
+  ) as Record<MetalType, number[]>;
+
+  // 准备箱线图数据
+  const validBoxplotData: ([number, number, number, number, number] | null)[] = 
+    METALS.map(metal => calculateBoxplotStats(metalValues[metal]));
+
+  // ECharts配置
+  const option: echarts.EChartsOption = {
+    backgroundColor: '#FFFFFF',
+    title: {
+      text: '重金属浓度分布箱线图',
+      left: 'center',
+      textStyle: { color: '#333', fontSize: 16 }
+    },
+    tooltip: {
+      trigger: 'item',
+      formatter: (params: any) => {
+        const metalIndex = params.dataIndex;
+        const metal = METALS[metalIndex];
+        const stats = validBoxplotData[metalIndex];
+        
+        // 处理空数据情况(修复图片中的null错误)
+        if (stats === null || stats[0] === null) {
+          return `<span style="color:#ff0000">${METAL_LABELS[metal]}数据不足,无法生成统计值</span>`;
+        }
+        
+        // 类型安全解构(确保所有值都是number类型)
+        const [min, q1, median, q3, max] = stats;
+        
+        return `
+          <b>${METAL_LABELS[metal]}</b><br/>
+          最小值: ${min.toFixed(4)} mg/L<br/>
+          下四分位: ${q1.toFixed(4)} mg/L<br/>
+          中位数: ${median.toFixed(4)} mg/L<br/>
+          上四分位: ${q3.toFixed(4)} mg/L<br/>
+          最大值: ${max.toFixed(4)} mg/L
+        `;
+      }
+    },
+    xAxis: {
+      type: 'category',
+      data: METALS.map(metal => METAL_LABELS[metal]),
+      axisLabel: { color: '#333', interval: 0 }
+    },
+    yAxis: {
+      type: 'value',
+      name: '浓度(mg/L)',
+      nameTextStyle: { color: '#333' },
+      axisLabel: { 
+        color: '#333',
+        formatter: (value: number) => value.toFixed(4)
+      }
+    },
+    series: [{
+      type: 'boxplot',
+      // 过滤无效数据(解决ts 2322错误)
+      data: validBoxplotData.filter(arr => arr !== null) as [number, number, number, number, number][],
+      itemStyle: {
+        color: '#4285F4',
+        borderWidth: 1.5
+      },
+      emphasis: {
+        itemStyle: {
+          borderColor: '#333',
+          borderWidth: 2
+        }
+      }
+    }]
+  };
+
+  // 初始化图表
+  if (chartInstance.value) {
+    chartInstance.value.dispose();
+  }
+  chartInstance.value = echarts.init(chartRef.value);
+  chartInstance.value.setOption(option);
+  
+  // 响应式处理
+  resizeHandler = () => chartInstance.value?.resize();
+  window.addEventListener('resize', resizeHandler);
+};
+
+// 数据加载
+const loadData = async () => {
+  try {
+    const response = await axios.get<any[]>(
+      'http://localhost:3000/table/Water_assay_data',
+      { timeout: 5000 }
+    );
+    
+    // 数据转换与过滤
+    metalData.value = response.data
+      .map(item => ({
+        sampleId: String(item.sampleId),
+        Cr: cleanData(item.Cr),
+        As: cleanData(item.As),
+        Cd: cleanData(item.Cd),
+        Hg: cleanData(item.Hg),
+        Pb: cleanData(item.Pb)
+      }))
+      // 修复:允许部分有效数据
+      .filter(item => METALS.some(metal => item[metal] !== null));
+    
+    renderBoxplot();
+  } catch (error) {
+    console.error('数据加载失败:', error);
+    alert('数据加载错误,请查看控制台日志');
+  }
+};
+
+onMounted(() => loadData());
+onUnmounted(() => {
+  // 清理资源
+  if (resizeHandler) {
+    window.removeEventListener('resize', resizeHandler);
+  }
+  chartInstance.value?.dispose();
+});
+</script>
+
+<style scoped>
+.boxplot-container {
+  width: 100%;
+  max-width: 1000px;
+  margin: 20px auto;
+  padding: 20px;
+  background: white;
+  border-radius: 8px;
+  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
+}
+</style>

+ 294 - 0
src/views/User/HmOutFlux/irrigationWater/waterassaydata2.vue

@@ -0,0 +1,294 @@
+<template>
+  <div class="region-average-chart">
+    <div ref="chartRef" class="chart-box"></div>
+    <div v-if="loading" class="status">数据加载中...</div>
+    <div v-else-if="error" class="status error">{{ error }}</div>
+  </div>
+</template>
+<!--各地区的重金属平均值得柱状图-->
+<script setup>
+import { ref, onMounted, onUnmounted } from 'vue';
+import * as echarts from 'echarts';
+import axios from 'axios';
+
+// ========== 接口配置 ==========
+const SAMPLING_API = 'http://localhost:3000/table/Water_sampling_data';
+const ASSAY_API = 'http://localhost:3000/table/Water_assay_data';
+
+// ========== 配置项 ==========
+const EXCLUDE_FIELDS = [
+  'water_assay_ID', 'sample_code', 'assayer_ID', 'assay_time', 
+  'assay_instrument_model', 'water_sample_ID', 'pH'
+];
+const COLORS = ['#ff4d4f99', '#1890ff', '#ffd700',  '#52c41a88', '#722ed199' ];
+
+// 韶关市下属行政区划白名单 [关键修改点]
+const SG_REGIONS = [
+  '浈江区', '武江区', '曲江区', '乐昌市', 
+  '南雄市', '始兴县', '仁化县', '翁源县', 
+  '新丰县', '乳源瑶族自治县'
+];
+
+// ========== 响应式数据 ==========
+const chartRef = ref(null);
+const loading = ref(true);
+const error = ref('');
+let myChart = null;
+
+// ========== 地区提取函数(韶关市专属版)[核心修改] ==========
+const extractRegion = (location) => {
+  if (!location || typeof location !== 'string') return null;
+
+  // 1. 精确匹配官方区县名称
+  const officialMatch = SG_REGIONS.find(region => 
+    location.includes(region)
+  );
+  if (officialMatch) return officialMatch;
+
+  // 2. 处理嵌套格式(如"韶关市-浈江区")
+  const nestedMatch = location.match(/(韶关市)([^市]+?[区市县])/);
+  if (nestedMatch && nestedMatch[2]) {
+    const region = nestedMatch[2].replace("韶关市", "").trim();
+    // 验证是否为合法区县
+    const validRegion = SG_REGIONS.find(r => r.includes(region));
+    if (validRegion) return validRegion;
+  }
+
+  // 3. 特殊格式处理(如"韶关市浈江区")
+  const shortMatch = location.match(/韶关市([区市县][^市]{2,5})/);
+  if (shortMatch && shortMatch[1]) return shortMatch[1];
+
+  // 4. 修正常见拼写错误
+  if (location.includes('乐昌')) return '乐昌市';
+  if (location.includes('乳源')) return '乳源瑶族自治县';
+
+  console.warn(`⚠️ 未识别地区: ${location}`);
+  return '未知区县';
+};
+
+// ========== 数据处理流程 ==========
+const processMergedData = (samplingData, assayData) => {
+  // 1. 构建采样点ID到区县的映射
+  const regionMap = new Map();
+  samplingData.forEach(item => {
+    const region = extractRegion(item.sampling_location || '');
+    if (region && region !== '未知区县') {
+      regionMap.set(item.water_sample_ID, region);
+    }
+  });
+
+  // 2. 关联重金属数据与区县
+  const mergedData = assayData.map(item => ({
+    ...item,
+    region: regionMap.get(item.water_sample_ID) || '未知区县'
+  }));
+
+  // 3. 识别重金属字段
+  const metals = Object.keys(mergedData[0] || {})
+    .filter(key => !EXCLUDE_FIELDS.includes(key) && !isNaN(parseFloat(mergedData[0][key])));
+
+  // 4. 按区县分组统计
+  const regionGroups = {};
+  const cityWideAverages = {}; // 新增:全市平均值
+  const uniqueSampleIds = new Set();
+  
+  // 初始化全市平均值计数器
+  metals.forEach(metal => {
+    cityWideAverages[metal] = { sum: 0, count: 0 };
+  });
+  
+  mergedData.forEach(item => {
+    const region = item.region;
+
+    if(item.water_sample_ID){
+      uniqueSampleIds.add(item.water_sample_ID);
+    }
+
+    // 区县统计
+    if (!regionGroups[region]) {
+      regionGroups[region] = {};
+      metals.forEach(metal => {
+        regionGroups[region][metal] = { sum: 0, count: 0 };
+      });
+    }
+
+    metals.forEach(metal => {
+      const val = parseFloat(item[metal]);
+      if (!isNaN(val)) {
+        // 更新区县统计
+        regionGroups[region][metal].sum += val;
+        regionGroups[region][metal].count++;
+        
+        // 更新全市统计
+        cityWideAverages[metal].sum += val;
+        cityWideAverages[metal].count++;
+      }
+    });
+  });
+  
+  const totalSamples = uniqueSampleIds.size;
+
+  // 5. 按官方顺序排序区县
+  const regions = SG_REGIONS.filter(region => regionGroups[region]);
+  
+  // 6. 添加"全市平均"作为最后一个类别
+  regions.push("全市平均");
+
+  // 7. 构建ECharts数据(包括全市平均值)
+  const series = metals.map((metal, idx) => {
+    // 计算全市平均值
+    const cityWideAvg = cityWideAverages[metal].count 
+      ? (cityWideAverages[metal].sum / cityWideAverages[metal].count).toFixed(2) 
+      : 0;
+    
+    return {
+      name: metal,
+      type: 'bar',
+      data: regions.map(region => {
+        if (region === "全市平均") {
+          return cityWideAvg;
+        }
+        const group = regionGroups[region][metal];
+        return group.count ? (group.sum / group.count).toFixed(2) : 0;
+      }),
+      itemStyle: { 
+        color: COLORS[idx % COLORS.length],
+      },
+      label: {
+        show: true,
+        position: 'top',
+        fontSize:18,
+        color:'#333',
+      }
+    };
+  });
+
+  return { regions, series, totalSamples };
+};
+
+// ========== ECharts 初始化 ==========
+const initChart = ({ regions, series, totalSamples }) => {
+  if (!chartRef.value) return;
+  if (myChart) myChart.dispose();
+
+  myChart = echarts.init(chartRef.value);
+  const option = {
+    title: { 
+      left: 'center',
+      subtext: `数据来源: ${totalSamples}个有效检测样本`,
+      subtextStyle:{
+        fontSize:18  //提示框字体大小
+      }
+    },
+    tooltip: { 
+      trigger: 'axis',
+      formatter: params => {
+        const regionName = params[0].name;
+        const isCityWide = regionName === "全市平均";
+        
+        let content = `${isCityWide ? "全市平均值" : regionName}:`;
+        if (isCityWide) {
+          content += `<br><span style="color: #666;">(基于${totalSamples}个样本计算)</span>`;
+        }
+        
+        return content + params.map(p => `<br>${p.seriesName}: ${p.value} ug/L`).join('');
+      },
+      textSize:{
+        fontSize:20
+      }
+    },
+    xAxis: {
+      type: 'category',
+      data: regions,
+      axisLabel: { 
+        rotate: 45,
+        formatter: val => val.replace('韶关市', ''),
+        fontSize:20
+      }
+    },
+    yAxis: { 
+      type: 'value', 
+      name: '浓度(ug/L)' ,
+      nameTextStyle:{
+        fontSize:20,
+      },
+      axisLabel:{
+        fontSize:20,
+      }
+    },
+    dataZoom: [{
+      type: 'inside',
+      start: 0,
+      end: 100
+    }],
+    series,
+    legend: { 
+      data: series.map(s => s.name), 
+      bottom: 10,
+      textStyle:{
+        fontSize:18
+      }
+    },
+    grid: { 
+      left: '3%', 
+      right: '3%', 
+      bottom: '15%', 
+      containLabel: true 
+    },
+  };
+
+  myChart.setOption(option);
+};
+
+// ========== 生命周期钩子 ==========
+onMounted(async () => {
+  try {
+    const [samplingRes, assayRes] = await Promise.all([
+      axios.get(SAMPLING_API, { timeout: 10000 }),
+      axios.get(ASSAY_API, { timeout: 10000 })
+    ]);
+    
+    initChart(processMergedData(samplingRes.data, assayRes.data));
+  } catch (err) {
+    error.value = '数据加载失败: ' + (err.message || '未知错误');
+    console.error('接口错误:', err);
+  } finally {
+    loading.value = false;
+  }
+});
+
+// 响应式布局
+const resizeHandler = () => myChart && myChart.resize();
+onMounted(() => window.addEventListener('resize', resizeHandler));
+onUnmounted(() => window.removeEventListener('resize', resizeHandler));
+</script>
+
+<style scoped>
+.region-average-chart {
+  width: 100%;
+  max-width: 1200px;
+  margin: 0 auto;
+  position: relative;
+}
+.chart-box {
+  width: 100%;
+  height: 600px;
+  min-height: 400px;
+  background-color: white;
+  border-radius: 8px;
+}
+.status {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  padding: 15px;
+  background: rgba(255,255,255,0.8);
+  border-radius: 4px;
+  font-size: 16px;
+}
+.error { 
+  color: #ff4d4f;
+  font-weight: bold;
+}
+</style>

+ 197 - 0
src/views/User/HmOutFlux/irrigationWater/waterassaydata3.vue

@@ -0,0 +1,197 @@
+<template>
+  <div class="heavy-metal-radar">
+    <h2 class="chart-title">重金属指标雷达图分析</h2>
+    <canvas ref="chartRef" class="chart-box"></canvas>
+    <div v-if="loading" class="status">数据加载中...</div>
+    <div v-else-if="error" class="status error">{{ error }}</div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onUnmounted } from 'vue';
+import Chart from 'chart.js/auto';
+import axios from 'axios';
+
+// ========== 接口配置(和柱状图对齐) ==========
+const ASSAY_API = 'http://localhost:3000/table/Water_assay_data'; // 复用柱状图的接口
+
+// ========== 配置项(模仿柱状图) ==========
+const EXCLUDE_FIELDS = [
+  'water_assay_ID', 'sample_code', 'assayer_ID', 'assay_time', 
+  'assay_instrument_model', 'water_sample_ID', 'pH'
+];
+const COLORS = ['#165DFF', '#36CFC9', '#722ED1']; // 雷达图三色
+
+// ========== 响应式数据 ==========
+const chartRef = ref(null);
+const loading = ref(true);
+const error = ref('');
+let radarChart = null;
+
+// ========== 数据处理:提取重金属指标 + 统计计算 ==========
+const processRadarData = (assayData) => {
+  // 1. 提取重金属字段(排除指定字段,且为数值类型)
+  const metals = Object.keys(assayData[0] || {})
+    .filter(key => !EXCLUDE_FIELDS.includes(key) && !isNaN(parseFloat(assayData[0][key])));
+
+  // 2. 计算每个重金属的统计值(均值、中位数、标准差)
+  const stats = metals.map(metal => {
+    const values = assayData.map(item => parseFloat(item[metal])).filter(v => !isNaN(v));
+    return {
+      mean: calculateMean(values),
+      median: calculateMedian(values),
+      std: calculateStdDev(values)
+    };
+  });
+
+  return { metals, stats };
+};
+
+// ========== 统计工具函数 ==========
+const calculateMean = (values) => {
+  if (values.length === 0) return 0;
+  return values.reduce((sum, val) => sum + val, 0) / values.length;
+};
+
+const calculateMedian = (values) => {
+  if (values.length === 0) return 0;
+  const sorted = [...values].sort((a, b) => a - b);
+  const mid = Math.floor(sorted.length / 2);
+  return sorted.length % 2 === 0 
+    ? (sorted[mid - 1] + sorted[mid]) / 2 
+    : sorted[mid];
+};
+
+const calculateStdDev = (values) => {
+  if (values.length <= 1) return 0;
+  const mean = calculateMean(values);
+  const variance = values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / values.length;
+  return Math.sqrt(variance);
+};
+
+// ========== 初始化雷达图(Chart.js) ==========
+const initRadarChart = ({ metals, stats }) => {
+  if (!chartRef.value) return;
+  if (radarChart) radarChart.destroy();
+
+  const ctx = chartRef.value.getContext('2d');
+  radarChart = new Chart(ctx, {
+    type: 'radar',
+    data: {
+      labels: metals,
+      datasets: [
+        {
+          label: '均值',
+          data: stats.map(s => s.mean.toFixed(2)),
+          borderColor: COLORS[0],
+          backgroundColor: 'rgba(22, 93, 255, 0.1)',
+          pointRadius: 4,
+          borderWidth: 2
+        },
+        {
+          label: '中位数',
+          data: stats.map(s => s.median.toFixed(2)),
+          borderColor: COLORS[1],
+          backgroundColor: 'rgba(54, 207, 201, 0.1)',
+          pointRadius: 4,
+          borderWidth: 2
+        },
+        {
+          label: '标准差',
+          data: stats.map(s => s.std.toFixed(2)),
+          borderColor: COLORS[2],
+          backgroundColor: 'rgba(114, 46, 209, 0.1)',
+          pointRadius: 4,
+          borderWidth: 2
+        }
+      ]
+    },
+    options: {
+      responsive: true,
+      maintainAspectRatio: false,
+      scales: {
+        r: {
+          beginAtZero: true,
+          ticks: { display: false },
+          pointLabels: { font: { size: 12, weight: 'bold' } },
+          grid: { color: 'rgba(0,0,0,0.05)' },
+          angleLines: { color: 'rgba(0,0,0,0.1)' }
+        }
+      },
+      plugins: {
+        legend: { position: 'bottom' },
+        tooltip: {
+          callbacks: {
+            label: (ctx) => `${ctx.dataset.label}: ${ctx.raw} mg/L`
+          }
+        }
+      }
+    }
+  });
+};
+
+// ========== 生命周期钩子(和柱状图对齐) ==========
+onMounted(async () => {
+  try {
+    // 【关键】和柱状图一样,axios 请求 **不携带凭证**(withCredentials: false,默认就是false)
+    const assayRes = await axios.get(ASSAY_API, { timeout: 10000, withCredentials:false });
+    const processed = processRadarData(assayRes.data);
+    
+    if (processed.metals.length === 0) {
+      throw new Error('未检测到有效重金属指标');
+    }
+    
+    initRadarChart(processed);
+  } catch (err) {
+    error.value = '数据加载失败: ' + (err.message || '未知错误');
+    console.error('接口错误:', err);
+  } finally {
+    loading.value = false;
+  }
+});
+
+// 响应式resize(模仿柱状图)
+const resizeHandler = () => radarChart && radarChart.resize();
+onMounted(() => window.addEventListener('resize', resizeHandler));
+onUnmounted(() => window.removeEventListener('resize', resizeHandler));
+</script>
+
+<style scoped>
+.heavy-metal-radar {
+  width: 100%;
+  max-width: 800px;
+  margin: 20px auto;
+  position: relative;
+  padding-top: 0;
+  background-color: white;
+  border-radius: 8px;
+}
+.chart-box {
+  width: 100%;
+  min-height: 350px;
+  max-height: 600px;
+  height: auto;
+  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
+}
+.chart-title {
+  text-align: center;   /* 水平居中 */
+  font-size: 18px;      /* 字体大小 */
+  font-weight: 600;     /* 加粗 */
+  color: #333;          /* 字体颜色 */
+  margin: 10px 0;     /* 底部间距,避免和图表贴紧 */
+}
+.status {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  padding: 15px;
+  background: rgba(255,255,255,0.8);
+  border-radius: 4px;
+  z-index: 10;
+}
+.error { 
+  color: #ff4d4f;
+  font-weight: bold;
+}
+</style>

+ 340 - 0
src/views/User/HmOutFlux/irrigationWater/waterassaydata4.vue

@@ -0,0 +1,340 @@
+<template>
+  <div class="region-average-chart">
+    <!-- 重金属选择器 -->
+    <div class="metal-selector" v-if="!loading && !error && metals.length > 0">
+      <label for="metal-select">选择重金属:</label>
+      <select id="metal-select" v-model="selectedMetal">
+        <option v-for="metal in metals" :key="metal" :value="metal">{{ metal }}</option>
+      </select>
+    </div>
+    
+    <!-- 图表容器 -->
+    <div ref="chartRef" class="chart-box"></div>
+    
+    <!-- 状态信息 -->
+    <div v-if="loading" class="status">数据加载中...</div>
+    <div v-else-if="error" class="status error">{{ error }}</div>
+    <div v-else-if="metals.length === 0" class="status">没有有效的重金属数据</div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onUnmounted, watch } from 'vue';
+import * as echarts from 'echarts';
+import axios from 'axios';
+
+// ========== 接口配置 ==========
+const SAMPLING_API = 'http://localhost:3000/table/Water_sampling_data';
+const ASSAY_API = 'http://localhost:3000/table/Water_assay_data';
+
+// ========== 配置项 ==========
+const EXCLUDE_FIELDS = [
+  'water_assay_ID', 'sample_code', 'assayer_ID', 'assay_time', 
+  'assay_instrument_model', 'water_sample_ID', 'pH'
+];
+const COLORS = [
+  '#FF6B6B', '#4ECDC4', '#FFD166', '#6A4C93', '#1982C4',
+  '#FF9F1C', '#2EC4B6', '#E71D36', '#3A86FF', '#FF006E'
+];
+
+// 韶关市下属行政区划
+const SG_REGIONS = [
+  '浈江区', '武江区', '曲江区', '乐昌市', 
+  '南雄市', '始兴县', '仁化县', '翁源县', 
+  '新丰县', '乳源瑶族自治县'
+];
+
+// ========== 响应式数据 ==========
+const chartRef = ref(null);
+const loading = ref(true);
+const error = ref('');
+// 修复 selectedMetal 未定义问题:提前声明变量
+const selectedMetal = ref(''); 
+const metals = ref([]);
+const chartData = ref(null);
+let myChart = null;
+
+// ========== 地区提取函数 ==========
+const extractRegion = (location) => {
+  if (!location || typeof location !== 'string') return null;
+
+  // 1. 精确匹配官方区县名称
+  const officialMatch = SG_REGIONS.find(region => 
+    location.includes(region)
+  );
+  if (officialMatch) return officialMatch;
+
+  // 2. 处理嵌套格式(如"韶关市-浈江区")
+  const nestedMatch = location.match(/(韶关市)([^市]+?[区市县])/);
+  if (nestedMatch && nestedMatch[2]) {
+    const region = nestedMatch[2].replace("韶关市", "").trim();
+    // 验证是否为合法区县
+    const validRegion = SG_REGIONS.find(r => r.includes(region));
+    if (validRegion) return validRegion;
+  }
+
+  // 3. 特殊格式处理(如"韶关市浈江区")
+  const shortMatch = location.match(/韶关市([区市县][^市]{2,5})/);
+  if (shortMatch && shortMatch[1]) return shortMatch[1];
+
+  // 4. 修正常见拼写错误
+  if (location.includes('乐昌')) return '乐昌市';
+  if (location.includes('乳源')) return '乳源瑶族自治县';
+
+  console.warn(`⚠️ 未识别地区: ${location}`);
+  return '未知区县';
+};
+
+// ========== 数据处理流程 ==========
+const processMergedData = (samplingData, assayData) => {
+  // 1. 构建采样点ID到区县的映射
+  const regionMap = new Map();
+  const uniqueSampleIds = new Set();
+  
+  samplingData.forEach(item => {
+    const region = extractRegion(item.sampling_location || '');
+    if (region && region !== '未知区县') {
+      regionMap.set(item.water_sample_ID, region);
+    }
+  });
+
+  // 2. 关联重金属数据与区县
+  const mergedData = assayData.map(item => ({
+    ...item,
+    region: regionMap.get(item.water_sample_ID) || '未知区县'
+  }));
+
+  // 3. 识别重金属字段
+  const detectedMetals = Object.keys(mergedData[0] || {})
+    .filter(key => !EXCLUDE_FIELDS.includes(key) && !isNaN(parseFloat(mergedData[0][key])));
+  
+  metals.value = detectedMetals;
+  if (detectedMetals.length > 0 && !selectedMetal.value) {
+    selectedMetal.value = detectedMetals[0];
+  }
+
+  // 4. 按区县分组统计
+  const regionGroups = {};
+  
+  mergedData.forEach(item => {
+    const region = item.region;
+    const sampleId = item.water_sample_ID;
+    
+    if (sampleId) uniqueSampleIds.add(sampleId);
+    
+    // 初始化区县数据
+    if (!regionGroups[region]) {
+      regionGroups[region] = {};
+      detectedMetals.forEach(metal => {
+        regionGroups[region][metal] = { sum: 0, count: 0 };
+      });
+    }
+
+    // 统计重金属数据
+    detectedMetals.forEach(metal => {
+      const val = parseFloat(item[metal]);
+      if (!isNaN(val)) {
+        regionGroups[region][metal].sum += val;
+        regionGroups[region][metal].count++;
+      }
+    });
+  });
+
+  // 5. 构建扇形图数据
+  const pieSeriesData = detectedMetals.map(metal => {
+    // 该重金属在各区县的平均浓度总和
+    let totalAverage = 0;
+    
+    // 收集各区县该重金属的平均值
+    const regionAverages = SG_REGIONS.map(region => {
+      if (!regionGroups[region]) return null;
+      
+      const group = regionGroups[region][metal];
+      const avg = group.count ? group.sum / group.count : 0;
+      totalAverage += avg;
+      
+      return { 
+        name: region,
+        value: avg
+      };
+    }).filter(Boolean);
+
+    // 计算占比
+    const seriesData = regionAverages.map(item => ({
+      name: item.name,
+      value: totalAverage > 0 ? (item.value / totalAverage) * 100 : 0,
+      rawValue: item.value // 保留原始浓度值用于显示
+    }));
+
+    return {
+      metal,
+      seriesData
+    };
+  });
+
+  return {
+    regions: SG_REGIONS,
+    pieSeriesData,
+    totalSamples: uniqueSampleIds.size
+  };
+};
+
+// ========== 初始化/更新图表 ==========
+const initChart = () => {
+  if (!chartRef.value || !selectedMetal.value || !chartData.value) return;
+  
+  if (myChart) myChart.dispose();
+  myChart = echarts.init(chartRef.value);
+
+  // 获取当前重金属的数据
+  const currentMetalData = chartData.value.pieSeriesData.find(
+    item => item.metal === selectedMetal.value
+  );
+
+  if (!currentMetalData) return;
+
+  const option = {
+    title: { 
+      text: `韶关市${selectedMetal.value}平均浓度区域占比`, 
+      left: 'center',
+      subtext: `数据来源: ${chartData.value.totalSamples}个有效检测样本`
+    },
+    tooltip: {
+      trigger: 'item',
+      formatter: function(params) {
+        return `${params.name}<br/>
+                ${selectedMetal.value}: ${params.data.rawValue.toFixed(4)} mg/L<br/>
+                占比: ${params.percent}%`;
+      }
+    },
+    legend: {
+      orient: 'vertical',
+      right: 10,
+      top: 'center',
+      data: currentMetalData.seriesData.map(item => item.name)
+    },
+    series: [
+      {
+        name: selectedMetal.value,
+        type: 'pie',
+        radius: ['35%', '65%'],
+        center: ['45%', '50%'],
+        avoidLabelOverlap: false,
+        itemStyle: {
+          borderRadius: 10,
+          borderColor: '#fff',
+          borderWidth: 2
+        },
+        label: {
+          show: false,
+          position: 'center'
+        },
+        emphasis: {
+          label: {
+            show: true,
+            fontSize: '16',
+            fontWeight: 'bold',
+            formatter: '{b}\n{c}%'
+          }
+        },
+        labelLine: {
+          show: false
+        },
+        data: currentMetalData.seriesData
+      }
+    ],
+    color: COLORS
+  };
+
+  myChart.setOption(option);
+};
+
+// ========== 生命周期钩子 ==========
+onMounted(async () => {
+  try {
+    // 修复请求超时问题:将超时时间延长至10秒
+    const [samplingRes, assayRes] = await Promise.all([
+      axios.get(SAMPLING_API, { timeout: 10000 }), // 10秒超时
+      axios.get(ASSAY_API, { timeout: 10000 })
+    ]);
+    
+    chartData.value = processMergedData(samplingRes.data, assayRes.data);
+    initChart();
+  } catch (err) {
+    // 处理超时错误
+    if (err.code === 'ECONNABORTED') {
+      error.value = '请求超时:服务器响应时间超过10秒';
+    } else {
+      error.value = '数据加载失败: ' + (err.message || '未知错误');
+    }
+    console.error('接口错误:', err);
+  } finally {
+    loading.value = false;
+  }
+});
+
+// 监听重金属选择变化
+watch(selectedMetal, (newVal) => {
+  if (newVal && myChart && chartData.value) {
+    initChart();
+  }
+});
+
+// 响应式布局
+const resizeHandler = () => myChart && myChart.resize();
+onMounted(() => window.addEventListener('resize', resizeHandler));
+onUnmounted(() => window.removeEventListener('resize', resizeHandler));
+</script>
+
+<style scoped>
+.region-average-chart {
+  width: 100%;
+  max-width: 1200px;
+  margin: 20px auto;
+  position: relative;
+}
+.chart-box {
+  width: 100%;
+  height: 600px;
+  min-height: 400px;
+  background-color: white;
+  border-radius: 8px;
+  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
+}
+.status {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  padding: 15px;
+  background: rgba(255,255,255,0.8);
+  border-radius: 4px;
+  text-align: center;
+}
+.error { 
+  color: #ff4d4f;
+  font-weight: bold;
+}
+.metal-selector {
+  margin-bottom: 15px;
+  text-align: center;
+  padding: 10px;
+}
+.metal-selector label {
+  margin-right: 10px;
+  font-weight: bold;
+}
+.metal-selector select {
+  padding: 8px 15px;
+  border-radius: 4px;
+  border: 1px solid #ddd;
+  background-color: #f8f8f8;
+  font-size: 14px;
+  min-width: 150px;
+  cursor: pointer;
+  transition: border 0.3s;
+}
+.metal-selector select:hover {
+  border-color: #1890ff;
+}
+</style>

+ 221 - 0
src/views/User/HmOutFlux/irrigationWater/waterdataline.vue

@@ -0,0 +1,221 @@
+<template>
+  <div class="container mx-auto px-4 py-8">
+    <div class="bg-white rounded-xl shadow-lg overflow-hidden">
+      <div class="p-6 border-b border-gray-200">
+        <h1 class="text-[clamp(1.5rem,3vw,2.5rem)] font-bold text-gray-800 text-center">水质检测数据列表</h1>
+        <p class="text-gray-500 text-center mt-2">实时监测与分析水质指标</p>
+      </div>
+      
+      <!-- 加载状态 -->
+      <div v-if="loading" class="py-20 flex justify-center items-center">
+        <div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
+      </div>
+      
+      <!-- 错误状态 -->
+      <div v-else-if="error" class="p-8 bg-red-50 border-l-4 border-red-400 text-red-700">
+        <div class="flex">
+          <div class="flex-shrink-0">
+            <i class="fa fa-exclamation-triangle text-red-500 text-xl"></i>
+          </div>
+          <div class="ml-3">
+            <h3 class="text-sm font-medium text-red-800">加载失败</h3>
+            <div class="mt-2 text-sm text-red-700">
+              <p>{{ error }}</p>
+            </div>
+            <div class="mt-4">
+              <button @click="fetchData" class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition duration-150 ease-in-out">
+                <i class="fa fa-refresh mr-2"></i>重试
+              </button>
+            </div>
+          </div>
+        </div>
+      </div>
+      
+      <!-- 数据表格 -->
+      <div v-else-if="filteredData.length > 0" class="overflow-x-auto">
+        <table class="min-w-full divide-y divide-gray-200">
+          <thead class="bg-gray-50">
+            <tr>
+              <th 
+                v-for="(col, index) in displayColumns" 
+                :key="index"
+                class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 transition-colors"
+                @click="sortData(col.key)"
+              >
+                <div class="flex items-center justify-between">
+                  {{ col.label }}
+                  <span v-if="sortKey === col.key" class="ml-1 text-gray-400">
+                    {{ sortOrder === 'asc' ? '↑' : '↓' }}
+                  </span>
+                </div>
+              </th>
+            </tr>
+          </thead>
+          <tbody class="bg-white divide-y divide-gray-200">
+            <tr v-for="(item, rowIndex) in sortedData" :key="rowIndex" 
+                class="hover:bg-gray-50 transition-colors duration-150">
+              <td 
+                v-for="(col, colIndex) in displayColumns" 
+                :key="colIndex"
+                class="px-6 py-4 whitespace-nowrap text-sm"
+              >
+                <div class="flex items-center">
+                  <div class="text-gray-900 font-medium">
+                    {{ item[col.key] !== null ? item[col.key] : '-' }}
+                  </div>
+                </div>
+              </td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+      
+      <!-- 空数据状态 -->
+      <div v-else class="p-8 text-center">
+        <div class="flex flex-col items-center justify-center">
+          <div class="text-gray-400 mb-4">
+            <i class="fa fa-database text-5xl"></i>
+          </div>
+          <h3 class="text-lg font-medium text-gray-900 mb-1">暂无有效数据</h3>
+          <p class="text-gray-500">已过滤全空行</p>
+        </div>
+      </div>
+  
+      <!-- 数据表格 + 统计 -->
+  <div class="p-4 bg-gray-50 border-t border-gray-200">
+    <div class="flex flex-col md:flex-row justify-between items-center">
+      <div class="text-sm text-gray-500 mb-2 md:mb-0">
+        共 <span class="font-medium text-gray-900">{{ filteredData.length }}</span> 条数据
+      </div>
+    </div>
+</div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, onMounted } from 'vue';
+import axios from 'axios';
+
+// 定义固定列配置
+const displayColumns = ref([
+  { key: 'water_assay_ID', label: '检测ID' },
+  { key: 'pH', label: 'PH值' },
+  { key: 'Cr', label: '铬含量(ug/L)' },
+  { key: 'As', label: '砷含量(ug/L)' },
+  { key: 'Cd', label: '镉含量(ug/L)' },
+  { key: 'Hg', label: '汞含量(ug/L)' },
+  { key: 'Pb', label: '铅含量(ug/L)' },
+  { key: 'sample_code', label: '样本编码' },
+  { key: 'water_sample_ID', label: '水样ID' },
+]);
+
+// 状态管理
+const waterData = ref([]);
+const loading = ref(true);
+const error = ref(null);
+const sortKey = ref('');
+const sortOrder = ref('asc');
+
+// 获取数据
+const fetchData = async () => {
+  try {
+    loading.value = true;
+    error.value = null;
+    const response = await axios.get('http://localhost:3000/table/Water_assay_data');
+    waterData.value = response.data.data || response.data;
+  } catch (err) {
+    error.value = err.message || '无法连接到服务器,请检查接口是否可用';
+  } finally {
+    loading.value = false;
+  }
+};
+
+// 过滤全空行
+const filteredData = computed(() => {
+  return waterData.value.filter(item => {
+    return displayColumns.value.some(col => item[col.key] !== null && item[col.key] !== '-');
+  });
+});
+
+// 排序功能
+const sortedData = computed(() => {
+  if (!sortKey.value) return filteredData.value;
+  
+  return [...filteredData.value].sort((a, b) => {
+    const valA = a[sortKey.value];
+    const valB = b[sortKey.value];
+    if (valA < valB) return sortOrder.value === 'asc' ? -1 : 1;
+    if (valA > valB) return sortOrder.value === 'asc' ? 1 : -1;
+    return 0;
+  });
+});
+
+// 切换排序
+const sortData = (key) => {
+  if (sortKey.value === key) {
+    sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc';
+  } else {
+    sortKey.value = key;
+    sortOrder.value = 'asc';
+  }
+};
+
+// 组件挂载
+onMounted(() => {
+  fetchData();
+});
+</script>
+
+<style scoped>
+/* 布局 */
+.container {
+  max-width: 1280px;
+  margin: 0 auto;
+  padding: 32px 16px;
+}
+.overflow-x-auto { overflow-x: auto; }
+
+/* 卡片 */
+.bg-white { background-color: #fff; }
+.rounded-xl { border-radius: 1rem; }
+.shadow-lg { 
+  box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1), 
+             0 4px 6px -4px rgba(0,0,0,0.1); 
+}
+
+/* 文字 */
+.text-center { text-align: center; }
+.text-lg { font-size: 1.125rem; }
+.font-bold { font-weight: 700; }
+.text-gray-800 { color: #111827; }
+
+/* 动画 */
+.animate-spin {
+  animation: spin 1s linear infinite;
+}
+@keyframes spin {
+  from { transform: rotate(0deg); }
+  to { transform: rotate(360deg); }
+}
+
+/* 表格 */
+table { width: 100%; }
+.px-6 { padding: 0 1.5rem; }
+.py-4 { padding: 1rem 0; }
+.hover\:bg-gray-50:hover { background-color: #f9fafb; }
+
+/* 响应式 */
+@media (max-width: 640px) {
+  .container { padding: 32px 8px; }
+  .px-6 { padding: 0 0.75rem; }
+}
+table {
+  border-collapse: collapse; /* 合并边框线 */
+}
+th, td {
+  border: 1px solid #d1d5db; /* 灰色边框 */
+  text-align: center; /* 内容居中 */
+  padding: 12px 8px; /* 内边距优化 */
+}
+</style>

+ 336 - 41
src/views/User/cadmiumPrediction/CropCadmiumPrediction.vue

@@ -28,7 +28,7 @@
         <el-button class="custom-button" :disabled="!histogramBlob" @click="exportHistogram">
           <el-icon class="upload-icon"><Download /></el-icon>
           导出直方图</el-button>
-        <el-button class="custom-button" :disabled="!tableData.length" @click="exportData">
+        <el-button class="custom-button" :disabled="!statisticsData.length" @click="exportData">
           <el-icon class="upload-icon"><Download /></el-icon>
           导出数据</el-button>
       </div>
@@ -67,15 +67,50 @@
         </div>
       </div>
 
-      <!-- 表格区域 -->
-      <div class="table-area">
-        <h3>表格数据</h3>
-        <el-table :data="tableData" style="width: 100%;">
-          <el-table-column prop="name" label="名称" width="180" />
-          <el-table-column prop="value" label="值" width="100" />
-          <el-table-column prop="unit" label="单位" width="100" />
-          <el-table-column prop="description" label="描述" />
-        </el-table>
+      <!-- 统计图表区域 -->
+      <div class="stats-area">
+        <h3>{{countyName}} - 作物Cd预测统计信息</h3>
+        <div class="model-info">
+          <el-tag type="info">{{currentStats?.['模型类型'] || '作物Cd模型'}}</el-tag>
+          <span class="update-time">
+            最后更新: {{currentStats?.['数据更新时间'] ? new Date(currentStats['数据更新时间']).toLocaleString() : '未知'}}
+          </span>
+        </div>
+        
+        <div v-if="loadingStats" class="loading-container">
+          <el-icon class="loading-icon"><Loading /></el-icon>
+          <span>统计数据加载中...</span>
+        </div>
+        
+        <div v-if="!loadingStats && statisticsData.length" class="stats-container">
+          <!-- 统计表格 -->
+         <el-table 
+            :data="statisticsData" 
+            style="width: 100%; margin-bottom: 20px;"
+            border
+            stripe
+          >
+            <el-table-column prop="name" label="统计项" min-width="180" />
+            <el-table-column prop="value" label="值" min-width="150" />
+            <el-table-column prop="unit" label="单位" min-width="100" />
+            <el-table-column prop="description" label="描述" min-width="200" />
+          </el-table>
+          
+          <!-- 统计图表 -->
+          <div class="charts-container">
+            <div class="chart-item">
+              <div ref="distributionChart" style="width: 100%; height: 400px;"></div>
+            </div>
+            <div class="chart-item">
+              <div ref="exceedanceChart" style="width: 100%; height: 400px;"></div>
+            </div>
+          </div>
+        </div>
+        
+        <div v-if="!loadingStats && !statisticsData.length" class="no-data">
+          <el-icon><DataAnalysis /></el-icon>
+          <p>暂无统计数据</p>
+        </div>
       </div>
     </div>
   </div>
@@ -85,28 +120,45 @@
 import * as XLSX from 'xlsx';
 import { saveAs } from 'file-saver';
 import axios from 'axios';
-import { Loading, Upload, Picture, Histogram, Download, Document } from '@element-plus/icons-vue';
+import * as echarts from 'echarts';
+import { 
+  Loading, Upload, Picture, Histogram, Download, Document, DataAnalysis 
+} from '@element-plus/icons-vue';
 
 export default {
   name: 'CropCadmiumPrediction',
-  components: { Loading, Upload, Picture, Histogram, Download, Document },
+  components: { 
+    Loading, Upload, Picture, Histogram, Download, Document, DataAnalysis 
+  },
   data() {
     return {
       isCalculating: false,
       loadingMap: false,
       loadingHistogram: false,
-      tableData: [],
+      loadingStats: false,
+      statisticsData: [],
       mapImageUrl: null,
       histogramImageUrl: null,
       mapBlob: null,
       histogramBlob: null,
       selectedFile: null,
-      countyName: '乐昌市' // 默认县市名称
+      countyName: '乐昌市', // 默认县市名称
+      distributionChart: null,
+      exceedanceChart: null
     };
   },
+
   mounted() {
     // 组件挂载时获取最新数据
     this.fetchLatestResults();
+    this.fetchStatistics();
+  },
+
+  beforeDestroy() {
+    if (this.mapImageUrl) URL.revokeObjectURL(this.mapImageUrl);
+    if (this.histogramImageUrl) URL.revokeObjectURL(this.histogramImageUrl);
+    if (this.distributionChart) this.distributionChart.dispose();
+    if (this.exceedanceChart) this.exceedanceChart.dispose();
   },
   methods: {
     // 触发文件选择
@@ -177,6 +229,197 @@ export default {
       }
     },
     
+    // 格式化统计数据
+    formatStatisticsData(stats) {
+      return [
+        { name: '数据点总数', value: stats['基础统计']['数据点总数'], unit: '个', description: '总样本数量' },
+        { name: '平均值', value: stats['基础统计']['均值'].toFixed(4), unit: '(mg/kg)', description: '所有样本的平均Cd含量' },
+        { name: '中位数', value: stats['基础统计']['中位数'].toFixed(4), unit: '(mg/kg)', description: '样本的中位Cd含量' },
+        { name: '标准差', value: stats['基础统计']['标准差'].toFixed(4), unit: '(mg/kg)', description: 'Cd含量的标准差' },
+        { name: '最小值', value: stats['基础统计']['最小值'].toFixed(4), unit: '(mg/kg)', description: '样本中的最小Cd含量' },
+        { name: '最大值', value: stats['基础统计']['最大值'].toFixed(4), unit: '(mg/kg)', description: '样本中的最大Cd含量' },
+        { name: '偏度', value: stats['基础统计']['偏度'].toFixed(4), unit: '', description: '数据分布偏斜程度' },
+        { name: '峰度', value: stats['基础统计']['峰度'].toFixed(4), unit: '', description: '数据分布峰态' },
+        { 
+          name: '经度范围', 
+          value: `${stats['空间统计']['经度范围']['最小值'].toFixed(6)} - ${stats['空间统计']['经度范围']['最大值'].toFixed(6)}`, 
+          unit: '度', 
+          description: `跨度: ${stats['空间统计']['经度范围']['跨度'].toFixed(6)}度` 
+        },
+        { 
+          name: '纬度范围', 
+          value: `${stats['空间统计']['纬度范围']['最小值'].toFixed(6)} - ${stats['空间统计']['纬度范围']['最大值'].toFixed(6)}`, 
+          unit: '度', 
+          description: `跨度: ${stats['空间统计']['纬度范围']['跨度'].toFixed(6)}度` 
+        }
+      ];
+    },
+
+    // 初始化图表 - 根据实际数据更新
+    initCharts() {
+      if (!this.statisticsData.length || !this.currentStats) return;
+      
+      // 销毁旧图表
+      if (this.distributionChart) this.distributionChart.dispose();
+      if (this.exceedanceChart) this.exceedanceChart.dispose();
+      
+      const histData = this.currentStats['分布直方图'];
+      
+      // 1. 分布直方图
+      this.distributionChart = echarts.init(this.$refs.distributionChart);
+      this.distributionChart.setOption({
+        title: {
+          text: 'Cd含量分布直方图',
+          left: 'center'
+        },
+        tooltip: {
+          trigger: 'item',
+          formatter: params => {
+            const index = params.dataIndex;
+            const lowerBound = histData['区间边界'][index].toFixed(4);
+            const upperBound = histData['区间边界'][index + 1].toFixed(4);
+            return `区间: ${lowerBound} ~ ${upperBound}<br/>频次: ${params.value}`;
+          }
+        },
+        xAxis: {
+          type: 'category',
+          data: histData['区间中心'].map(v => v.toFixed(4)),
+          name: 'Cd含量',
+          axisLabel: {
+            rotate: 45
+          }
+        },
+        yAxis: {
+          type: 'value',
+          name: '频次'
+        },
+        series: [{
+          name: '样本分布',
+          type: 'bar',
+          data: histData['频次'],
+          itemStyle: {
+            color: '#47C3B9'
+          },
+          barWidth: '80%'
+        }],
+        grid: {
+          bottom: '20%'
+        }
+      });
+      
+      // 2. 箱线图/统计图表
+      this.exceedanceChart = echarts.init(this.$refs.exceedanceChart);
+      
+      // 准备箱线图数据
+      const boxData = [
+        [
+          this.currentStats['基础统计']['最小值'],
+          this.currentStats['基础统计']['25%分位数'],
+          this.currentStats['基础统计']['中位数'],
+          this.currentStats['基础统计']['75%分位数'],
+          this.currentStats['基础统计']['最大值'],
+          // 还可以添加离群点数据(如果有)
+        ]
+      ];
+      
+      this.exceedanceChart.setOption({
+        title: {
+          text: 'Cd含量统计指标',
+          left: 'center'
+        },
+        tooltip: {
+          trigger: 'item',
+          axisPointer: {
+            type: 'shadow'
+          },
+          formatter: params => {
+            const data = boxData[0];
+            return [
+              '最大值: ' + data[4].toFixed(4),
+              '75%分位数: ' + data[3].toFixed(4),
+              '中位数: ' + data[2].toFixed(4),
+              '25%分位数: ' + data[1].toFixed(4),
+              '最小值: ' + data[0].toFixed(4)
+            ].join('<br/>');
+          }
+        },
+        xAxis: {
+          type: 'category',
+          data: ['Cd含量统计'],
+          axisLabel: {
+            rotate: 45
+          }
+        },
+        yAxis: {
+          type: 'value',
+          name: '(Cd含量)'
+        },
+        series: [{
+          name: '统计值',
+          type: 'boxplot',
+          data: boxData,
+          itemStyle: {
+            color: '#47C3B9',
+            borderColor: '#2F4554'
+          },
+          emphasis: {
+            itemStyle: {
+              color: '#FF6B6B',
+              borderColor: '#C23531'
+            }
+          },
+          tooltip: {
+            formatter: param => {
+              const data = boxData[0];
+              return [
+                '最大值: ' + data[4].toFixed(4),
+                '75%分位数: ' + data[3].toFixed(4),
+                '中位数: ' + data[2].toFixed(4),
+                '25%分位数: ' + data[1].toFixed(4),
+                '最小值: ' + data[0].toFixed(4)
+              ].join('<br/>');
+            }
+          }
+        }],
+        grid: {
+          bottom: '15%'
+        }
+      });
+      
+      // 响应式调整
+      window.addEventListener('resize', this.handleResize);
+    },
+
+    // 修改fetchStatistics方法
+    async fetchStatistics() {
+      try {
+        this.loadingStats = true;
+        
+        const response = await axios.get(
+          `https://soilgd.com:8000/api/cd-prediction/crop-cd/statistics/${this.countyName}`
+        );
+        
+        if (response.data.success && response.data.data) {
+          this.currentStats = response.data.data; // 保存原始统计数据
+          this.statisticsData = this.formatStatisticsData(response.data.data);
+          this.$nextTick(() => {
+            this.initCharts();
+          });
+        }
+      } catch (error) {
+        console.error('获取统计信息失败:', error);
+        this.$message.warning('获取统计信息失败');
+      } finally {
+        this.loadingStats = false;
+      }
+    },
+    
+    // 处理窗口大小变化
+    handleResize() {
+      if (this.distributionChart) this.distributionChart.resize();
+      if (this.exceedanceChart) this.exceedanceChart.resize();
+    },
+    
     // 上传并计算
     async calculate() {
       if (!this.selectedFile) {
@@ -188,13 +431,14 @@ export default {
         this.isCalculating = true;
         this.loadingMap = true;
         this.loadingHistogram = true;
+        this.loadingStats = true;
         
         // 创建FormData
         const formData = new FormData();
         formData.append('county_name', this.countyName);
         formData.append('data_file', this.selectedFile);
         
-        // 调用有效态Cd地图接口
+        // 调用作物Cd地图接口
         const mapResponse = await axios.post(
           'https://soilgd.com:8000/api/cd-prediction/crop-cd/generate-and-get-map',
           formData,
@@ -210,14 +454,9 @@ export default {
         this.mapBlob = mapResponse.data;
         this.mapImageUrl = URL.createObjectURL(this.mapBlob);
         
-        // 更新后重新获取直方图(因为生成新数据后直方图也会更新)
+        // 更新后重新获取直方图和统计数据
         await this.fetchLatestHistogram();
-        
-        // 更新表格数据(示例)
-        this.tableData = [
-          { name: '样本1', value: 10, unit: 'mg/L', description: '描述1' },
-          { name: '样本2', value: 20, unit: 'mg/L', description: '描述2' }
-        ];
+        await this.fetchStatistics();
         
         this.$message.success('计算完成!');
         
@@ -226,7 +465,6 @@ export default {
         let errorMessage = '计算失败,请重试';
         
         if (error.response) {
-          // 处理不同错误状态码
           if (error.response.status === 400) {
             errorMessage = '文件格式错误:' + (error.response.data.detail || '请上传正确的CSV文件');
           } else if (error.response.status === 404) {
@@ -241,6 +479,7 @@ export default {
         this.isCalculating = false;
         this.loadingMap = false;
         this.loadingHistogram = false;
+        this.loadingStats = false;
       }
     },
     
@@ -272,29 +511,41 @@ export default {
       URL.revokeObjectURL(link.href);
     },
     
-    // 导出数据
-    exportData() {
-      if (!this.tableData.length) {
-        this.$message.warning('暂无数据可导出');
-        return;
+    // 导出数据 - 修改为获取作物Cd的CSV文件
+    async exportData() {
+      try {
+        this.$message.info('正在获取作物Cd预测数据...');
+        
+        const response = await axios.get(
+          `https://soilgd.com:8000/api/cd-prediction/download-final-crop-cd-csv`,
+          { responseType: 'blob' }
+        );
+        
+        const blob = new Blob([response.data], { type: 'text/csv' });
+        const link = document.createElement('a');
+        link.href = URL.createObjectURL(blob);
+        link.download = `${this.countyName}_作物Cd预测数据.csv`;
+        link.click();
+        URL.revokeObjectURL(link.href);
+        
+        this.$message.success('数据导出成功');
+      } catch (error) {
+        console.error('导出数据失败:', error);
+        this.$message.error('导出数据失败: ' + (error.response?.data?.detail || '请稍后重试'));
       }
-      
-      const workbook = XLSX.utils.book_new();
-      const worksheet = XLSX.utils.json_to_sheet(this.tableData);
-      XLSX.utils.book_append_sheet(workbook, worksheet, '作物态Cd数据');
-      const excelBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
-      const excelData = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
-      saveAs(excelData, `${this.countyName}_作物态Cd数据.xlsx`);
     }
-  },
-  beforeDestroy() {
-    if (this.mapImageUrl) URL.revokeObjectURL(this.mapImageUrl);
-    if (this.histogramImageUrl) URL.revokeObjectURL(this.histogramImageUrl);
   }
 };
 </script>
 
 <style scoped>
+::v-deep .el-table th.el-table__cell {
+  text-align: center;
+  background-color: #f5f7fa !important;
+}
+::v-deep .el-table td.el-table__cell {
+  text-align: center;
+}
 .container {
   padding: 20px;
   background-color: #f5f7fa;
@@ -367,7 +618,7 @@ export default {
 
 .map-section, .histogram-section {
   flex: 1;
-  min-width: 300px; /* 最小宽度,确保在小屏幕上也能正常显示 */
+  min-width: 300px;
   background-color: white;
   border-radius: 8px;
   padding: 15px;
@@ -384,15 +635,34 @@ export default {
   border-radius: 4px;
 }
 
-.table-area {
+.stats-area {
   width: 100%;
   background-color: white;
   border-radius: 8px;
   padding: 15px;
   box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+}
+
+.stats-container {
   margin-top: 20px;
 }
 
+.charts-container {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 20px;
+  margin-top: 30px;
+}
+
+.chart-item {
+  flex: 1;
+  min-width: 300px;
+  background: #f9f9f9;
+  padding: 15px;
+  border-radius: 8px;
+  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
+}
+
 .loading-container {
   display: flex;
   flex-direction: column;
@@ -443,4 +713,29 @@ export default {
     flex: none;
   }
 }
+.model-info {
+  display: flex;
+  align-items: center;
+  gap: 15px;
+  margin: 10px 0;
+  color: #666;
+}
+
+.update-time {
+  font-size: 14px;
+}
+
+.chart-item {
+  flex: 1;
+  min-width: 400px;
+  background: white;
+  padding: 15px;
+  border-radius: 8px;
+  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
+  margin-bottom: 20px;
+}
+
+.el-table {
+  margin-top: 20px;
+}
 </style>

+ 335 - 40
src/views/User/cadmiumPrediction/EffectiveCadmiumPrediction.vue

@@ -28,7 +28,7 @@
         <el-button class="custom-button" :disabled="!histogramBlob" @click="exportHistogram">
           <el-icon class="upload-icon"><Download /></el-icon>
           导出直方图</el-button>
-        <el-button class="custom-button" :disabled="!tableData.length" @click="exportData">
+        <el-button class="custom-button" :disabled="!statisticsData.length" @click="exportData">
           <el-icon class="upload-icon"><Download /></el-icon>
           导出数据</el-button>
       </div>
@@ -67,15 +67,50 @@
         </div>
       </div>
 
-      <!-- 表格区域 -->
-      <div class="table-area">
-        <h3>表格数据</h3>
-        <el-table :data="tableData" style="width: 100%;">
-          <el-table-column prop="name" label="名称" width="180" />
-          <el-table-column prop="value" label="值" width="100" />
-          <el-table-column prop="unit" label="单位" width="100" />
-          <el-table-column prop="description" label="描述" />
-        </el-table>
+      <!-- 统计图表区域 -->
+      <div class="stats-area">
+        <h3>{{countyName}} - 有效Cd预测统计信息</h3>
+        <div class="model-info">
+          <el-tag type="info">{{currentStats?.['模型类型'] || '有效Cd模型'}}</el-tag>
+          <span class="update-time">
+            最后更新: {{currentStats?.['数据更新时间'] ? new Date(currentStats['数据更新时间']).toLocaleString() : '未知'}}
+          </span>
+        </div>
+        
+        <div v-if="loadingStats" class="loading-container">
+          <el-icon class="loading-icon"><Loading /></el-icon>
+          <span>统计数据加载中...</span>
+        </div>
+        
+        <div v-if="!loadingStats && statisticsData.length" class="stats-container">
+          <!-- 统计表格 -->
+           <el-table 
+              :data="statisticsData" 
+              style="width: 100%; margin-bottom: 20px;"
+              border
+              stripe
+            >
+              <el-table-column prop="name" label="统计项" min-width="180" />
+              <el-table-column prop="value" label="值" min-width="150" />
+              <el-table-column prop="unit" label="单位" min-width="100" />
+              <el-table-column prop="description" label="描述" min-width="200" />
+            </el-table>
+          
+          <!-- 统计图表 -->
+          <div class="charts-container">
+            <div class="chart-item">
+              <div ref="distributionChart" style="width: 100%; height: 400px;"></div>
+            </div>
+            <div class="chart-item">
+              <div ref="exceedanceChart" style="width: 100%; height: 400px;"></div>
+            </div>
+          </div>
+        </div>
+        
+        <div v-if="!loadingStats && !statisticsData.length" class="no-data">
+          <el-icon><DataAnalysis /></el-icon>
+          <p>暂无统计数据</p>
+        </div>
       </div>
     </div>
   </div>
@@ -85,28 +120,45 @@
 import * as XLSX from 'xlsx';
 import { saveAs } from 'file-saver';
 import axios from 'axios';
-import { Loading, Upload, Picture, Histogram, Download, Document } from '@element-plus/icons-vue';
+import * as echarts from 'echarts';
+import { 
+  Loading, Upload, Picture, Histogram, Download, Document, DataAnalysis 
+} from '@element-plus/icons-vue';
 
 export default {
-  name: 'EffectiveCadmiumPrediction',
-  components: { Loading, Upload, Picture, Histogram, Download, Document },
+  name: 'CropCadmiumPrediction',
+  components: { 
+    Loading, Upload, Picture, Histogram, Download, Document, DataAnalysis 
+  },
   data() {
     return {
       isCalculating: false,
       loadingMap: false,
       loadingHistogram: false,
-      tableData: [],
+      loadingStats: false,
+      statisticsData: [],
       mapImageUrl: null,
       histogramImageUrl: null,
       mapBlob: null,
       histogramBlob: null,
       selectedFile: null,
-      countyName: '乐昌市' // 默认县市名称
+      countyName: '乐昌市', // 默认县市名称
+      distributionChart: null,
+      exceedanceChart: null
     };
   },
+
   mounted() {
     // 组件挂载时获取最新数据
     this.fetchLatestResults();
+    this.fetchStatistics();
+  },
+
+  beforeDestroy() {
+    if (this.mapImageUrl) URL.revokeObjectURL(this.mapImageUrl);
+    if (this.histogramImageUrl) URL.revokeObjectURL(this.histogramImageUrl);
+    if (this.distributionChart) this.distributionChart.dispose();
+    if (this.exceedanceChart) this.exceedanceChart.dispose();
   },
   methods: {
     // 触发文件选择
@@ -177,6 +229,197 @@ export default {
       }
     },
     
+    // 格式化统计数据
+    formatStatisticsData(stats) {
+      return [
+        { name: '数据点总数', value: stats['基础统计']['数据点总数'], unit: '个', description: '总样本数量' },
+        { name: '平均值', value: stats['基础统计']['均值'].toFixed(4), unit: '(mg/kg)', description: '所有样本的平均Cd含量' },
+        { name: '中位数', value: stats['基础统计']['中位数'].toFixed(4), unit: '(mg/kg)', description: '样本的中位Cd含量' },
+        { name: '标准差', value: stats['基础统计']['标准差'].toFixed(4), unit: '(mg/kg)', description: 'Cd含量的标准差' },
+        { name: '最小值', value: stats['基础统计']['最小值'].toFixed(4), unit: '(mg/kg)', description: '样本中的最小Cd含量' },
+        { name: '最大值', value: stats['基础统计']['最大值'].toFixed(4), unit: '(mg/kg)', description: '样本中的最大Cd含量' },
+        { name: '偏度', value: stats['基础统计']['偏度'].toFixed(4), unit: '', description: '数据分布偏斜程度' },
+        { name: '峰度', value: stats['基础统计']['峰度'].toFixed(4), unit: '', description: '数据分布峰态' },
+        { 
+          name: '经度范围', 
+          value: `${stats['空间统计']['经度范围']['最小值'].toFixed(6)} - ${stats['空间统计']['经度范围']['最大值'].toFixed(6)}`, 
+          unit: '度', 
+          description: `跨度: ${stats['空间统计']['经度范围']['跨度'].toFixed(6)}度` 
+        },
+        { 
+          name: '纬度范围', 
+          value: `${stats['空间统计']['纬度范围']['最小值'].toFixed(6)} - ${stats['空间统计']['纬度范围']['最大值'].toFixed(6)}`, 
+          unit: '度', 
+          description: `跨度: ${stats['空间统计']['纬度范围']['跨度'].toFixed(6)}度` 
+        }
+      ];
+    },
+
+    // 初始化图表 - 根据实际数据更新
+    initCharts() {
+      if (!this.statisticsData.length || !this.currentStats) return;
+      
+      // 销毁旧图表
+      if (this.distributionChart) this.distributionChart.dispose();
+      if (this.exceedanceChart) this.exceedanceChart.dispose();
+      
+      const histData = this.currentStats['分布直方图'];
+      
+      // 1. 分布直方图
+      this.distributionChart = echarts.init(this.$refs.distributionChart);
+      this.distributionChart.setOption({
+        title: {
+          text: 'Cd含量分布直方图',
+          left: 'center'
+        },
+        tooltip: {
+          trigger: 'item',
+          formatter: params => {
+            const index = params.dataIndex;
+            const lowerBound = histData['区间边界'][index].toFixed(4);
+            const upperBound = histData['区间边界'][index + 1].toFixed(4);
+            return `区间: ${lowerBound} ~ ${upperBound}<br/>频次: ${params.value}`;
+          }
+        },
+        xAxis: {
+          type: 'category',
+          data: histData['区间中心'].map(v => v.toFixed(4)),
+          name: 'Cd含量',
+          axisLabel: {
+            rotate: 45
+          }
+        },
+        yAxis: {
+          type: 'value',
+          name: '频次'
+        },
+        series: [{
+          name: '样本分布',
+          type: 'bar',
+          data: histData['频次'],
+          itemStyle: {
+            color: '#47C3B9'
+          },
+          barWidth: '80%'
+        }],
+        grid: {
+          bottom: '20%'
+        }
+      });
+      
+      // 2. 箱线图/统计图表
+      this.exceedanceChart = echarts.init(this.$refs.exceedanceChart);
+      
+      // 准备箱线图数据
+      const boxData = [
+        [
+          this.currentStats['基础统计']['最小值'],
+          this.currentStats['基础统计']['25%分位数'],
+          this.currentStats['基础统计']['中位数'],
+          this.currentStats['基础统计']['75%分位数'],
+          this.currentStats['基础统计']['最大值'],
+          // 还可以添加离群点数据(如果有)
+        ]
+      ];
+      
+      this.exceedanceChart.setOption({
+        title: {
+          text: 'Cd含量统计指标',
+          left: 'center'
+        },
+        tooltip: {
+          trigger: 'item',
+          axisPointer: {
+            type: 'shadow'
+          },
+          formatter: params => {
+            const data = boxData[0];
+            return [
+              '最大值: ' + data[4].toFixed(4),
+              '75%分位数: ' + data[3].toFixed(4),
+              '中位数: ' + data[2].toFixed(4),
+              '25%分位数: ' + data[1].toFixed(4),
+              '最小值: ' + data[0].toFixed(4)
+            ].join('<br/>');
+          }
+        },
+        xAxis: {
+          type: 'category',
+          data: ['Cd含量统计'],
+          axisLabel: {
+            rotate: 45
+          }
+        },
+        yAxis: {
+          type: 'value',
+          name: 'Cd含量'
+        },
+        series: [{
+          name: '统计值',
+          type: 'boxplot',
+          data: boxData,
+          itemStyle: {
+            color: '#47C3B9',
+            borderColor: '#2F4554'
+          },
+          emphasis: {
+            itemStyle: {
+              color: '#FF6B6B',
+              borderColor: '#C23531'
+            }
+          },
+          tooltip: {
+            formatter: param => {
+              const data = boxData[0];
+              return [
+                '最大值: ' + data[4].toFixed(4),
+                '75%分位数: ' + data[3].toFixed(4),
+                '中位数: ' + data[2].toFixed(4),
+                '25%分位数: ' + data[1].toFixed(4),
+                '最小值: ' + data[0].toFixed(4)
+              ].join('<br/>');
+            }
+          }
+        }],
+        grid: {
+          bottom: '15%'
+        }
+      });
+      
+      // 响应式调整
+      window.addEventListener('resize', this.handleResize);
+    },
+
+    // 修改fetchStatistics方法
+    async fetchStatistics() {
+      try {
+        this.loadingStats = true;
+        
+        const response = await axios.get(
+          `https://soilgd.com:8000/api/cd-prediction/effective-cd/statistics/${this.countyName}`
+        );
+        
+        if (response.data.success && response.data.data) {
+          this.currentStats = response.data.data; // 保存原始统计数据
+          this.statisticsData = this.formatStatisticsData(response.data.data);
+          this.$nextTick(() => {
+            this.initCharts();
+          });
+        }
+      } catch (error) {
+        console.error('获取统计信息失败:', error);
+        this.$message.warning('获取统计信息失败');
+      } finally {
+        this.loadingStats = false;
+      }
+    },
+    
+    // 处理窗口大小变化
+    handleResize() {
+      if (this.distributionChart) this.distributionChart.resize();
+      if (this.exceedanceChart) this.exceedanceChart.resize();
+    },
+    
     // 上传并计算
     async calculate() {
       if (!this.selectedFile) {
@@ -188,6 +431,7 @@ export default {
         this.isCalculating = true;
         this.loadingMap = true;
         this.loadingHistogram = true;
+        this.loadingStats = true;
         
         // 创建FormData
         const formData = new FormData();
@@ -210,14 +454,9 @@ export default {
         this.mapBlob = mapResponse.data;
         this.mapImageUrl = URL.createObjectURL(this.mapBlob);
         
-        // 更新后重新获取直方图(因为生成新数据后直方图也会更新)
+        // 更新后重新获取直方图和统计数据
         await this.fetchLatestHistogram();
-        
-        // 更新表格数据(示例)
-        this.tableData = [
-          { name: '样本1', value: 10, unit: 'mg/L', description: '描述1' },
-          { name: '样本2', value: 20, unit: 'mg/L', description: '描述2' }
-        ];
+        await this.fetchStatistics();
         
         this.$message.success('计算完成!');
         
@@ -226,7 +465,6 @@ export default {
         let errorMessage = '计算失败,请重试';
         
         if (error.response) {
-          // 处理不同错误状态码
           if (error.response.status === 400) {
             errorMessage = '文件格式错误:' + (error.response.data.detail || '请上传正确的CSV文件');
           } else if (error.response.status === 404) {
@@ -241,6 +479,7 @@ export default {
         this.isCalculating = false;
         this.loadingMap = false;
         this.loadingHistogram = false;
+        this.loadingStats = false;
       }
     },
     
@@ -273,28 +512,40 @@ export default {
     },
     
     // 导出数据
-    exportData() {
-      if (!this.tableData.length) {
-        this.$message.warning('暂无数据可导出');
-        return;
+    async exportData() {
+      try {
+        this.$message.info('正在获取有效Cd预测数据...');
+        
+        const response = await axios.get(
+          `https://soilgd.com:8000/api/cd-prediction/download-final-effective-cd-csv`,
+          { responseType: 'blob' }
+        );
+        
+        const blob = new Blob([response.data], { type: 'text/csv' });
+        const link = document.createElement('a');
+        link.href = URL.createObjectURL(blob);
+        link.download = `${this.countyName}_有效Cd预测数据.csv`;
+        link.click();
+        URL.revokeObjectURL(link.href);
+        
+        this.$message.success('数据导出成功');
+      } catch (error) {
+        console.error('导出数据失败:', error);
+        this.$message.error('导出数据失败: ' + (error.response?.data?.detail || '请稍后重试'));
       }
-      
-      const workbook = XLSX.utils.book_new();
-      const worksheet = XLSX.utils.json_to_sheet(this.tableData);
-      XLSX.utils.book_append_sheet(workbook, worksheet, '有效态Cd数据');
-      const excelBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
-      const excelData = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
-      saveAs(excelData, `${this.countyName}_有效态Cd数据.xlsx`);
     }
-  },
-  beforeDestroy() {
-    if (this.mapImageUrl) URL.revokeObjectURL(this.mapImageUrl);
-    if (this.histogramImageUrl) URL.revokeObjectURL(this.histogramImageUrl);
   }
 };
 </script>
 
 <style scoped>
+::v-deep .el-table th.el-table__cell {
+  text-align: center;
+  background-color: #f5f7fa !important;
+}
+::v-deep .el-table td.el-table__cell {
+  text-align: center;
+}
 .container {
   padding: 20px;
   background-color: #f5f7fa;
@@ -367,7 +618,7 @@ export default {
 
 .map-section, .histogram-section {
   flex: 1;
-  min-width: 300px; /* 最小宽度,确保在小屏幕上也能正常显示 */
+  min-width: 300px;
   background-color: white;
   border-radius: 8px;
   padding: 15px;
@@ -384,15 +635,34 @@ export default {
   border-radius: 4px;
 }
 
-.table-area {
+.stats-area {
   width: 100%;
   background-color: white;
   border-radius: 8px;
   padding: 15px;
   box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+}
+
+.stats-container {
   margin-top: 20px;
 }
 
+.charts-container {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 20px;
+  margin-top: 30px;
+}
+
+.chart-item {
+  flex: 1;
+  min-width: 300px;
+  background: #f9f9f9;
+  padding: 15px;
+  border-radius: 8px;
+  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
+}
+
 .loading-container {
   display: flex;
   flex-direction: column;
@@ -443,4 +713,29 @@ export default {
     flex: none;
   }
 }
+.model-info {
+  display: flex;
+  align-items: center;
+  gap: 15px;
+  margin: 10px 0;
+  color: #666;
+}
+
+.update-time {
+  font-size: 14px;
+}
+
+.chart-item {
+  flex: 1;
+  min-width: 400px;
+  background: white;
+  padding: 15px;
+  border-radius: 8px;
+  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
+  margin-bottom: 20px;
+}
+
+.el-table {
+  margin-top: 20px;
+}
 </style>

+ 142 - 0
src/views/User/cadmiumPrediction/currentYearConcentration.vue

@@ -0,0 +1,142 @@
+<template>
+  <div class="cd-concentration-flux-container">
+    <el-card class="gradient-card" shadow="hover">
+      <h2 class="card-title">标题</h2>
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <p class="label">初始 CD 格浓度 (mg/kg)</p>
+          <el-input
+            v-model="initialCdConcentration"
+            placeholder="请输入内容"
+            class="custom-input"
+          />
+        </el-col>
+        <el-col :span="12">
+          <p class="label">净通量 (g/ha/a)</p>
+          <el-input
+            v-model="netFlux"
+            placeholder="请输入内容"
+            class="custom-input"
+          />
+        </el-col>
+         <el-col :span="12">
+          <p class="label">标题</p>
+          <el-input
+            v-model="netFlux"
+            placeholder="2000"
+            class="custom-input"
+          />
+        </el-col>
+        <el-col :span="24" style="margin-top: 20px;">
+          <el-button class="calculate-btn" @click="onCalculate">计算</el-button>
+        </el-col>
+      </el-row>
+    </el-card>
+  </div>
+</template>
+
+<script setup>
+import { ref } from 'vue';
+import { ElCard, ElRow, ElCol, ElInput, ElButton } from 'element-plus';
+
+const initialCdConcentration = ref('');
+const netFlux = ref('');
+
+const onCalculate = () => {
+  // 暂无计算逻辑,仅作展示
+  alert('计算按钮已点击');
+};
+</script>
+
+<style scoped>
+.cd-concentration-flux-container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  padding: 20px;
+}
+
+.gradient-card {
+  /* 半透明渐变背景 */
+  background: linear-gradient(
+    135deg, 
+    rgba(250, 253, 255, 0.8), 
+    rgba(137, 223, 252, 0.8)
+  );
+  border-radius: 12px;
+  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
+  padding: 30px;
+  text-align: left; /* 改为左对齐 */
+  width: 600px;
+  backdrop-filter: blur(5px); /* 添加模糊效果增强半透明感 */
+  border: none;
+}
+
+.card-title {
+  font-weight: bold;
+  font-size: 24px;
+  margin-bottom: 20px;
+  color: #333;
+  text-align: center;
+}
+
+.label {
+  font-weight: bold;
+  font-size: 18px;
+  margin-bottom: 10px; /* 减少底部外边距 */
+  color: #333;
+}
+
+.custom-input {
+  width: 100%;
+  max-width: 200px;
+  margin-left: 0; /* 确保输入框靠左对齐 */
+}
+
+/* 自定义输入框样式 */
+:deep(.custom-input .el-input__inner) {
+  background: rgba(255, 255, 255, 0.7);
+  border-radius: 8px;
+  border: 1px solid #dcdfe6;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
+  padding: 10px 15px;
+  font-size: 16px;
+  color: #333;
+}
+
+:deep(.custom-input .el-input__inner:focus) {
+  border-color: #409EFF;
+  box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
+}
+
+.calculate-btn {
+  width: 100%;
+  max-width: 200px;
+  height: 50px;
+  border: none;
+  border-radius: 25px !important;
+  font-size: 18px;
+  font-weight: bold;
+  transition: all 0.4s ease;
+  
+  /* 渐变背景色 */
+  background: linear-gradient(to right, #8DF9F0, #26B046);
+  color: white !important;
+  /* 按钮整体阴影 */
+  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15),
+              0 2px 6px rgba(38, 176, 70, 0.3) inset;
+}
+
+.calculate-btn:hover {
+  transform: scale(1.03);
+  box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2),
+              0 2px 8px rgba(38, 176, 70, 0.4) inset;
+  background: linear-gradient(to right, #7de8df, #20a03d);
+}
+
+.calculate-btn:active {
+  transform: scale(0.98);
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1),
+              0 1px 6px rgba(38, 176, 70, 0.4) inset;
+}
+</style>

+ 126 - 0
src/views/User/cadmiumPrediction/netFlux.vue

@@ -0,0 +1,126 @@
+<template>
+  <div class="total-input-output-container">
+    <el-card class="gradient-card" shadow="hover">
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <p class="label">输入总量 (g/ha/a)</p>
+          <el-input
+            v-model="inputTotal"
+            placeholder="请输入内容"
+            class="custom-input"
+          />
+        </el-col>
+        <el-col :span="12">
+          <p class="label">输出总量 (g/ha/a)</p>
+          <el-input
+            v-model="outputTotal"
+            placeholder="请输入内容"
+            class="custom-input"
+            readonly
+          />
+        </el-col>
+        <el-col :span="24" style="margin-top: 20px;">
+          <el-button class="calculate-btn" @click="onCalculate">计算</el-button>
+        </el-col>
+      </el-row>
+    </el-card>
+  </div>
+</template>
+
+<script setup>
+import { ref } from 'vue';
+import { ElCard, ElRow, ElCol, ElInput, ElButton } from 'element-plus';
+
+const inputTotal = ref('');
+const outputTotal = ref('');
+
+const onCalculate = () => {
+  // 暂无计算逻辑,仅作展示
+  alert('计算按钮已点击');
+};
+</script>
+
+<style scoped>
+.total-input-output-container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  padding: 20px;
+}
+
+.gradient-card {
+  /* 半透明渐变背景 */
+  background: linear-gradient(
+    135deg, 
+    rgba(250, 253, 255, 0.8), 
+    rgba(137, 223, 252, 0.8)
+  );
+  border-radius: 12px;
+  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
+  padding: 30px;
+  text-align: left; /* 改为左对齐 */
+  width: 600px;
+  backdrop-filter: blur(5px); /* 添加模糊效果增强半透明感 */
+  border: none;
+}
+
+.label {
+  font-weight: bold;
+  font-size: 18px;
+  margin-bottom: 10px; /* 减少底部外边距 */
+  color: #333;
+}
+
+.custom-input {
+  width: 100%;
+  max-width: 200px;
+  margin-left: 0; /* 确保输入框靠左对齐 */
+}
+
+/* 自定义输入框样式 */
+:deep(.custom-input .el-input__inner) {
+  background: rgba(255, 255, 255, 0.7);
+  border-radius: 8px;
+  border: 1px solid #dcdfe6;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
+  padding: 10px 15px;
+  font-size: 16px;
+  color: #333;
+}
+
+:deep(.custom-input .el-input__inner:focus) {
+  border-color: #409EFF;
+  box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
+}
+
+.calculate-btn {
+  width: 100%;
+  max-width: 200px;
+  height: 50px;
+  border: none;
+  border-radius: 25px !important;
+  font-size: 18px;
+  font-weight: bold;
+  transition: all 0.4s ease;
+  
+  /* 渐变背景色 */
+  background: linear-gradient(to right, #8DF9F0, #26B046);
+  color: white !important;
+  /* 按钮整体阴影 */
+  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15),
+              0 2px 6px rgba(38, 176, 70, 0.3) inset;
+}
+
+.calculate-btn:hover {
+  transform: scale(1.03);
+  box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2),
+              0 2px 8px rgba(38, 176, 70, 0.4) inset;
+  background: linear-gradient(to right, #7de8df, #20a03d);
+}
+
+.calculate-btn:active {
+  transform: scale(0.98);
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1),
+              0 1px 6px rgba(38, 176, 70, 0.4) inset;
+}
+</style>

+ 134 - 0
src/views/User/cadmiumPrediction/totalInputFlux.vue

@@ -0,0 +1,134 @@
+<template>
+  <div class="cd-input-container">
+    <el-card class="gradient-card" shadow="hover">
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <p class="label">大气沉降输入 Cd (g/ha/a)</p>
+          <el-input
+            v-model="atmosphericCd"
+            placeholder="请输入内容"
+            class="custom-input"
+          />
+        </el-col>
+        <el-col :span="12">
+          <p class="label">灌溉水输入 Cd (g/ha/a)</p>
+          <el-input
+            v-model="irrigationCd"
+            placeholder="请输入内容"
+            class="custom-input"
+          />
+        </el-col>
+        <el-col :span="24" style="margin-top: 20px;">
+          <p class="label">农业投入输入 Cd (g/ha/a)</p>
+          <el-input
+            v-model="agriculturalCd"
+            placeholder="请输入内容"
+            class="custom-input"
+          />
+        </el-col>
+        <el-col :span="24" style="margin-top: 20px;">
+          <el-button class="calculate-btn" @click="onCalculate">计算</el-button>
+        </el-col>
+      </el-row>
+    </el-card>
+  </div>
+</template>
+
+<script setup>
+import { ref } from 'vue';
+import { ElCard, ElRow, ElCol, ElInput, ElButton } from 'element-plus';
+
+const atmosphericCd = ref('');
+const irrigationCd = ref('');
+const agriculturalCd = ref('');
+
+const onCalculate = () => {
+  // 暂无计算逻辑,仅作展示
+  alert('计算按钮已点击');
+};
+</script>
+
+<style scoped>
+.cd-input-container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  padding: 20px;
+}
+
+.gradient-card {
+  /* 半透明渐变背景 */
+  background: linear-gradient(
+    135deg, 
+    rgba(250, 253, 255, 0.8), 
+    rgba(137, 223, 252, 0.8)
+  );
+  border-radius: 12px;
+  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
+  padding: 30px;
+  text-align: left; /* 改为左对齐 */
+  width: 600px;
+  backdrop-filter: blur(5px); /* 添加模糊效果增强半透明感 */
+  border: none;
+}
+
+.label {
+  font-weight: bold;
+  font-size: 18px;
+  margin-bottom: 10px; /* 减少底部外边距 */
+  color: #333;
+}
+
+.custom-input {
+  width: 100%;
+  max-width: 200px;
+  margin-left: 0; /* 确保输入框靠左对齐 */
+}
+
+/* 自定义输入框样式 */
+:deep(.custom-input .el-input__inner) {
+  background: rgba(255, 255, 255, 0.7);
+  border-radius: 8px;
+  border: 1px solid #dcdfe6;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
+  padding: 10px 15px;
+  font-size: 16px;
+  color: #333;
+}
+
+:deep(.custom-input .el-input__inner:focus) {
+  border-color: #409EFF;
+  box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
+}
+
+.calculate-btn {
+  width: 100%;
+  max-width: 200px;
+  height: 50px;
+  border: none;
+  border-radius: 25px !important;
+  font-size: 18px;
+  font-weight: bold;
+  transition: all 0.4s ease;
+  
+  /* 渐变背景色 */
+  background: linear-gradient(to right, #8DF9F0, #26B046);
+  color: white !important;
+  /* 按钮整体阴影 */
+  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15),
+              0 2px 6px rgba(38, 176, 70, 0.3) inset;
+}
+
+.calculate-btn:hover {
+  transform: scale(1.03);
+  box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2),
+              0 2px 8px rgba(38, 176, 70, 0.4) inset;
+  background: linear-gradient(to right, #7de8df, #20a03d);
+}
+
+.calculate-btn:active {
+  transform: scale(0.98);
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1),
+              0 1px 6px rgba(38, 176, 70, 0.4) inset;
+}
+</style>

+ 147 - 0
src/views/User/cadmiumPrediction/totalOutputFlux.vue

@@ -0,0 +1,147 @@
+<template>
+  <div class="cd-input-output-container">
+    <el-card class="gradient-card" shadow="hover">
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <p class="label">地表径流 (g/ha/a) 输出</p>
+          <el-input
+            v-model="runoffOutput"
+            placeholder="请输入内容"
+            class="custom-input"
+            readonly
+          />
+        </el-col>
+        <el-col :span="12">
+          <p class="label">籽粒移除 (g/ha/a) 输出</p>
+          <el-input
+            v-model="grainRemovalOutput"
+            placeholder="请输入内容"
+            class="custom-input"
+            readonly
+          />
+        </el-col>
+        <el-col :span="12" style="margin-top: 20px;">
+          <p class="label">地下渗漏 (g/ha/a) 输出</p>
+          <el-input
+            v-model="leakageOutput"
+            placeholder="请输入内容"
+            class="custom-input"
+            readonly
+          />
+        </el-col>
+        <el-col :span="12" style="margin-top: 20px;">
+          <p class="label">籽粒移除 (g/ha/a) 输出</p>
+          <el-input
+            v-model="anotherGrainRemovalOutput"
+            placeholder="请输入内容"
+            class="custom-input"
+            readonly
+          />
+        </el-col>
+        <el-col :span="24" style="margin-top: 20px;">
+          <el-button class="calculate-btn" @click="onCalculate">计算</el-button>
+        </el-col>
+      </el-row>
+    </el-card>
+  </div>
+</template>
+
+<script setup>
+import { ref } from 'vue';
+import { ElCard, ElRow, ElCol, ElInput, ElButton } from 'element-plus';
+
+const runoffOutput = ref('');
+const grainRemovalOutput = ref('');
+const leakageOutput = ref('');
+const anotherGrainRemovalOutput = ref('');
+
+const onCalculate = () => {
+  // 暂无计算逻辑,仅作展示
+  alert('计算按钮已点击');
+};
+</script>
+
+<style scoped>
+.cd-input-output-container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  padding: 20px;
+}
+
+.gradient-card {
+  /* 半透明渐变背景 */
+  background: linear-gradient(
+    135deg, 
+    rgba(250, 253, 255, 0.8), 
+    rgba(137, 223, 252, 0.8)
+  );
+  border-radius: 12px;
+  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
+  padding: 30px;
+  text-align: left; /* 改为左对齐 */
+  width: 600px;
+  backdrop-filter: blur(5px); /* 添加模糊效果增强半透明感 */
+  border: none;
+}
+
+.label {
+  font-weight: bold;
+  font-size: 18px;
+  margin-bottom: 10px; /* 减少底部外边距 */
+  color: #333;
+}
+
+.custom-input {
+  width: 100%;
+  max-width: 200px;
+  margin-left: 0; /* 确保输入框靠左对齐 */
+}
+
+/* 自定义输入框样式 */
+:deep(.custom-input .el-input__inner) {
+  background: rgba(255, 255, 255, 0.7);
+  border-radius: 8px;
+  border: 1px solid #dcdfe6;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
+  padding: 10px 15px;
+  font-size: 16px;
+  color: #333;
+}
+
+:deep(.custom-input .el-input__inner:focus) {
+  border-color: #409EFF;
+  box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
+}
+
+.calculate-btn {
+  width: 100%;
+  max-width: 200px;
+  height: 50px;
+  border: none;
+  border-radius: 25px !important;
+  font-size: 18px;
+  font-weight: bold;
+  transition: all 0.4s ease;
+  
+  /* 渐变背景色 */
+  background: linear-gradient(to right, #8DF9F0, #26B046);
+  color: white !important;
+  /* 按钮整体阴影 */
+  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15),
+              0 2px 6px rgba(38, 176, 70, 0.3) inset;
+}
+
+.calculate-btn:hover {
+  transform: scale(1.03);
+  box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2),
+              0 2px 8px rgba(38, 176, 70, 0.4) inset;
+  background: linear-gradient(to right, #7de8df, #20a03d);
+}
+
+.calculate-btn:active {
+  transform: scale(0.98);
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1),
+              0 1px 6px rgba(38, 176, 70, 0.4) inset;
+}
+</style>

+ 647 - 3
src/views/User/heavyMetalFluxCalculation/inputFluxCalculation/irrigationWater.vue

@@ -1,29 +1,673 @@
 <template>
   <div class="page-container">
+<<<<<<< HEAD
    <div>
     <router-view></router-view> <!-- 关键:子路由渲染位置 -->
   </div>
 
+=======
+    <!-- 上半部分:地图 + 柱状图 -->
+    <div class="top-content">
+      <!-- 灌溉水输入通量计算模块 -->
+      <div class="irrigation-form">
+        <div class="form-header">
+          <h3>灌溉水输入通量计算</h3>
+        </div>
+        
+        <div class="form-body">
+          <div class="land-form-row">
+            <!-- 水田 -->
+            <div class="land-form">
+              <h4>水田</h4>
+              <el-form :model="irrigationData.paddy" label-position="top">
+                <el-form-item label="灌溉水用量 (吨/公顷)">
+                  <el-input-number 
+                    v-model="irrigationData.paddy.usage" 
+                    :min="0" 
+                    :step="100"
+                  ></el-input-number>
+                </el-form-item>
+                <el-form-item label="灌溉水有效利用率 (%)">
+                  <el-input-number 
+                    v-model="irrigationData.paddy.efficiency" 
+                    :min="0" 
+                    :max="100" 
+                    :step="5"
+                  ></el-input-number>
+                </el-form-item>
+                <el-form-item label="计算通量(吨/公顷·年)">
+                  <el-input 
+                    v-model="irrigationData.paddy.flux" 
+                    readonly
+                    class="flux-input"
+                  >
+                  </el-input>
+                </el-form-item>
+                <el-button 
+                  type="primary" 
+                  @click="calculateFlux('paddy')"
+                  class="calculate-btn"
+                >计算水田通量</el-button>
+              </el-form>
+            </div>
+            
+            <!-- 水浇地 -->
+            <div class="land-form">
+              <h4>水浇地</h4>
+              <el-form :model="irrigationData.irrigated" label-position="top">
+                <el-form-item label="灌溉水用量 (吨/公顷)">
+                  <el-input-number 
+                    v-model="irrigationData.irrigated.usage" 
+                    :min="0" 
+                    :step="100"
+                  ></el-input-number>
+                </el-form-item>
+                <el-form-item label="灌溉水有效利用率 (%)">
+                  <el-input-number 
+                    v-model="irrigationData.irrigated.efficiency" 
+                    :min="0" 
+                    :max="100" 
+                    :step="5"
+                  ></el-input-number>
+                </el-form-item>
+                <el-form-item label="计算通量(吨/公顷·年)">
+                  <el-input 
+                    v-model="irrigationData.irrigated.flux" 
+                    readonly
+                    class="flux-input"
+                  >
+                  </el-input>
+                </el-form-item>
+                <el-button 
+                  type="primary" 
+                  @click="calculateFlux('irrigated')"
+                  class="calculate-btn"
+                >计算水浇地通量</el-button>
+              </el-form>
+            </div>
+            
+            <!-- 旱地 -->
+            <div class="land-form">
+              <h4>旱地</h4>
+              <el-form :model="irrigationData.dryland" label-position="top">
+                <el-form-item label="灌溉水用量 (吨/公顷)">
+                  <el-input-number 
+                    v-model="irrigationData.dryland.usage" 
+                    :min="0" 
+                    :step="100"
+                  ></el-input-number>
+                </el-form-item>
+                <el-form-item label="灌溉水有效利用率 (%)">
+                  <el-input-number 
+                    v-model="irrigationData.dryland.efficiency" 
+                    :min="0" 
+                    :max="100" 
+                    :step="5"
+                  ></el-input-number>
+                </el-form-item>
+                <el-form-item label="计算通量(吨/公顷·年)">
+                  <el-input 
+                    v-model="irrigationData.dryland.flux" 
+                    readonly
+                    class="flux-input"
+                  >
+                  </el-input>
+                </el-form-item>
+                <el-button 
+                  type="primary" 
+                  @click="calculateFlux('dryland')"
+                  class="calculate-btn"
+                >计算旱地通量</el-button>
+              </el-form>
+            </div>
+          </div>
+        </div>
+      </div>
+      
+      <!-- 右侧图表区域 -->
+      <div class="graphics-container">
+        <!-- 下拉选择栏 -->
+        <div class="land-type-selector">
+          <el-select v-model="selectedLandType" placeholder="选择土地类型" @change="handleLandTypeChange">
+            <el-option label="水田" value="paddy"></el-option>
+            <el-option label="水浇地" value="irrigated"></el-option>
+            <el-option label="旱地" value="dryland"></el-option>
+          </el-select>
+        </div>
+        
+        <!-- 图表容器 -->
+        <div class="graphics-row">
+          <!-- 地图模块 -->
+          <div class="map-module">
+            <div v-if="rasterMapImage" class="map-box">
+              <img :src="rasterMapImage" alt="土地类型Cd分布图" class="raster-image">
+            </div>
+            <div v-else-if="mapLoading" class="map-box">地图加载中...</div>
+            <div v-else class="map-box error-message">
+              地图加载失败: {{ mapError }}
+              <el-button type="primary" size="small" @click="retryMap">重试</el-button>
+            </div>
+          </div>
+          
+          <!-- 图表模块 -->
+          <div class="chart-module">
+            <div v-if="histogramImage" class="chart-box">
+              <img :src="histogramImage" alt="数据分布直方图" class="histogram-image">
+            </div>
+            <div v-else-if="histogramLoading" class="chart-box">直方图加载中...</div>
+            <div v-else class="chart-box error-message">
+              直方图加载失败: {{ histogramError }}
+              <el-button type="primary" size="small" @click="retryHistogram">重试</el-button>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 中间间距 -->
+    <div class="middle-gap"></div>
+
+    <!-- 下半部分:表格 -->
+    <div class="table-module">
+      <div class="table-header">
+        <h3>计算结果汇总</h3>
+        <el-button type="primary" @click="exportData">导出数据</el-button>
+      </div>
+      
+      <el-table
+        :data="tableData"
+        style="width: 100%"
+        border
+        stripe
+        class="compact-table"
+      >
+        <el-table-column prop="name" label="土地类型" width="120" />
+        <el-table-column prop="area" label="面积 (公顷)" width="100" />
+        <el-table-column prop="quality" label="质量等级" width="100" />
+        <el-table-column prop="productivity" label="生产力指数" width="120" />
+        <el-table-column prop="waterUsage" label="灌溉水用量" width="120" />
+        <el-table-column prop="waterEfficiency" label="利用率" width="100" />
+        <el-table-column prop="waterFlux" label="通量" width="120" />
+        <el-table-column label="操作" width="80">
+          <template #default="{ row }">
+            <el-button type="primary" size="small" @click="editItem(row)">修改</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+>>>>>>> ding
   </div>
 </template>
 
 <script setup>
-import { ref, onMounted, watch, computed } from 'vue';
-import * as echarts from 'echarts';
+import { ref, onMounted, reactive, watch } from 'vue';
+import axios from 'axios';
+
+// 配置API基础URL
+const BASE_URL = 'http://localhost:8000/api/water';
+
+// 创建自定义axios实例
+const http = axios.create({
+  timeout: 60000, // 60秒超时
+});
+
+// 土地类型数据映射
+const landTypeData = {
+  paddy: {
+    shp_base_name: 'lechang',
+    tif_type: '水田',
+    properties: {
+      area: 1200,
+      quality: '优',
+      productivity: 850
+    }
+  },
+  dryland: {
+    shp_base_name: 'lechang',
+    tif_type: '旱地',
+    properties: {
+      area: 800,
+      quality: '良',
+      productivity: 600
+    }
+  },
+  irrigated: {
+    shp_base_name: 'lechang',
+    tif_type: '水浇地',
+    properties: {
+      area: 950,
+      quality: '中',
+      productivity: 720
+    }
+  }
+};
+
+// 灌溉水计算数据
+const irrigationData = reactive({
+  paddy: {
+    usage: 8000,
+    efficiency: 60,
+    flux: 4800
+  },
+  irrigated: {
+    usage: 6000,
+    efficiency: 70,
+    flux: 4200
+  },
+  dryland: {
+    usage: 4000,
+    efficiency: 50,
+    flux: 2000
+  }
+});
+
+// 响应式数据
+const selectedLandType = ref('paddy');
+const rasterMapImage = ref(null);
+const histogramImage = ref(null);
+const tableData = ref([]);
+
+// 加载状态和错误信息
+const mapLoading = ref(false);
+const histogramLoading = ref(false);
+const mapError = ref('');
+const histogramError = ref('');
+
+// 初始化数据和表格
+const initTableData = () => {
+  tableData.value = [
+    {
+      id: 1,
+      name: '水田',
+      ...landTypeData.paddy.properties,
+      waterUsage: irrigationData.paddy.usage,
+      waterEfficiency: irrigationData.paddy.efficiency + '%',
+      waterFlux: irrigationData.paddy.flux
+    },
+    {
+      id: 2,
+      name: '水浇地',
+      ...landTypeData.irrigated.properties,
+      waterUsage: irrigationData.irrigated.usage,
+      waterEfficiency: irrigationData.irrigated.efficiency + '%',
+      waterFlux: irrigationData.irrigated.flux
+    },
+    {
+      id: 3,
+      name: '旱地',
+      ...landTypeData.dryland.properties,
+      waterUsage: irrigationData.dryland.usage,
+      waterEfficiency: irrigationData.dryland.efficiency + '%',
+      waterFlux: irrigationData.dryland.flux
+    }
+  ];
+};
+
+// 计算灌溉水通量
+const calculateFlux = (landType) => {
+  const data = irrigationData[landType];
+  data.flux = Math.round(data.usage * data.efficiency / 100);
+  
+  // 更新表格数据
+  const row = tableData.value.find(item => 
+    landType === 'paddy' ? item.name === '水田' : 
+    landType === 'irrigated' ? item.name === '水浇地' : 
+    item.name === '旱地'
+  );
+  
+  if (row) {
+    row.waterUsage = data.usage;
+    row.waterEfficiency = data.efficiency + '%';
+    row.waterFlux = data.flux;
+  }
+};
 
+// 导出数据功能
+const exportData = () => {
+  alert('导出数据功能已触发,数据已准备好下载');
+  // 实际应用中这里会生成并下载CSV文件
+};
 
+// 修改条目功能
+const editItem = (row) => {
+  alert(`正在修改 ${row.name} 的数据`);
+  // 实际应用中这里会打开编辑模态框
+};
 
+onMounted(() => {
+  initTableData();
+  handleLandTypeChange();
+});
 
+// 监听灌溉数据变化更新表格
+watch(irrigationData, (newVal) => {
+  tableData.value = tableData.value.map(item => {
+    if (item.name === '水田') {
+      return {...item, 
+        waterUsage: newVal.paddy.usage, 
+        waterEfficiency: newVal.paddy.efficiency + '%', 
+        waterFlux: newVal.paddy.flux};
+    } else if (item.name === '水浇地') {
+      return {...item, 
+        waterUsage: newVal.irrigated.usage, 
+        waterEfficiency: newVal.irrigated.efficiency + '%', 
+        waterFlux: newVal.irrigated.flux};
+    } else {
+      return {...item, 
+        waterUsage: newVal.dryland.usage, 
+        waterEfficiency: newVal.dryland.efficiency + '%', 
+        waterFlux: newVal.dryland.flux};
+    }
+  });
+}, { deep: true });
+
+const handleLandTypeChange = async () => {
+  const landType = selectedLandType.value;
+  const data = landTypeData[landType];
+  
+  // 重置状态
+  rasterMapImage.value = null;
+  histogramImage.value = null;
+  mapError.value = '';
+  histogramError.value = '';
+  
+  try {
+    // 设置加载状态
+    mapLoading.value = true;
+    histogramLoading.value = true;
+    
+    // 顺序请求,避免同时处理大文件导致问题
+    await generateRasterMap(data.shp_base_name, data.tif_type);
+    await generateHistogram(data.tif_type);
+  } catch (error) {
+    console.error('数据加载失败:', error);
+  } finally {
+    // 重置加载状态
+    mapLoading.value = false;
+    histogramLoading.value = false;
+  }
+};
+
+const generateRasterMap = async (shp_base_name, tif_type) => {
+  try {
+    const formData = new FormData();
+    formData.append('shp_base_name', shp_base_name);
+    formData.append('tif_type', tif_type);
+    formData.append('color_map_name', 'colormap6');
+    formData.append('title_name', `${getLandTypeName(selectedLandType.value)}Cd分布图`);
+    formData.append('output_size', '8'); // 减小输出尺寸
+    
+    const response = await http.post(`${BASE_URL}/generate-raster-map`, formData, {
+      headers: { 'Content-Type': 'multipart/form-data' },
+      responseType: 'blob'
+    });
+    
+    if (response.data.size === 0) {
+      throw new Error('后端返回空响应');
+    }
+    
+    const blob = new Blob([response.data], { type: 'image/jpeg' });
+    rasterMapImage.value = URL.createObjectURL(blob);
+    mapError.value = '';
+  } catch (error) {
+    console.error('栅格地图生成失败:', error);
+    mapError.value = error.response?.data?.message || error.message || '未知错误';
+    throw error;
+  }
+};
+
+const generateHistogram = async (tif_type) => {
+  try {
+    const formData = new FormData();
+    formData.append('tif_type', tif_type);
+    formData.append('figsize_width', '8'); // 减小宽度
+    formData.append('figsize_height', '6'); // 减小高度
+    formData.append('xlabel', '像元值');
+    formData.append('ylabel', '频率密度');
+    formData.append('title', `${getLandTypeName(selectedLandType.value)}数据分布`);
+    
+    const response = await http.post(`${BASE_URL}/generate-tif-histogram`, formData, {
+      headers: { 'Content-Type': 'multipart/form-data' },
+      responseType: 'blob'
+    });
+    
+    if (response.data.size === 0) {
+      throw new Error('后端返回空响应');
+    }
+    
+    const blob = new Blob([response.data], { type: 'image/jpeg' });
+    histogramImage.value = URL.createObjectURL(blob);
+    histogramError.value = '';
+  } catch (error) {
+    console.error('直方图生成失败:', error);
+    histogramError.value = error.response?.data?.message || error.message || '未知错误';
+    throw error;
+  }
+};
+
+const getLandTypeName = (value) => {
+  switch (value) {
+    case 'paddy': return '水田';
+    case 'dryland': return '旱地';
+    case 'irrigated': return '水浇地';
+    default: return '';
+  }
+};
+
+// 重试功能
+const retryMap = async () => {
+  const landType = selectedLandType.value;
+  const data = landTypeData[landType];
+  
+  mapLoading.value = true;
+  mapError.value = '';
+  
+  try {
+    await generateRasterMap(data.shp_base_name, data.tif_type);
+  } catch (error) {
+    console.error('重试栅格地图失败:', error);
+  } finally {
+    mapLoading.value = false;
+  }
+};
+
+const retryHistogram = async () => {
+  const landType = selectedLandType.value;
+  const data = landTypeData[landType];
+  
+  histogramLoading.value = true;
+  histogramError.value = '';
+  
+  try {
+    await generateHistogram(data.tif_type);
+  } catch (error) {
+    console.error('重试直方图失败:', error);
+  } finally {
+    histogramLoading.value = false;
+  }
+};
 </script>
 
 <style scoped>
 .page-container {
   display: flex;
   flex-direction: column;
-  height: 100vh; /* 整屏高度 */
+  height: 100vh;
   padding: 20px;
   box-sizing: border-box;
   background-color: #f5f7fa;
 }
 
+.top-content {
+  display: flex;
+  height: 45%;
+  gap: 20px; /* 增加间距 */
+}
+
+.irrigation-form {
+  flex: 0 0 45%;
+  background: white;
+  border-radius: 12px;
+  box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
+  display: flex;
+  flex-direction: column;
+}
+
+.form-header {
+  background-color: #2c3e50;
+  color: white;
+  padding: 10px 15px;
+  text-align: center;
+  font-size: 16px;
+  font-weight: bold;
+}
+
+.form-body {
+  padding: 15px;
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+}
+
+.land-form-row {
+  display: flex;
+  gap: 20px; /* 增加间距 */
+  height: 100%;
+}
+
+.land-form {
+  flex: 1;
+  border: 1px solid #e1e4e8;
+  border-radius: 8px;
+  padding: 15px; /* 增加内边距 */
+  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
+}
+
+.land-form h4 {
+  margin-top: 0;
+  margin-bottom: 15px; /* 增加下边距 */
+  color: #1a73e8;
+  border-bottom: 1px solid #e1e4e8;
+  padding-bottom: 10px; /* 增加下边距 */
+  font-size: 14px;
+}
+
+/* 修复通量数字显示不全 */
+.flux-input {
+  width: 100%; /* 确保输入框宽度填满 */
+}
+
+.flux-input >>> .el-input__inner {
+  text-align: left; /* 数字右对齐 */
+  padding-right: 10px; /* 增加右边距 */
+  font-weight: bold; /* 加粗显示 */
+}
+
+.calculate-btn {
+  margin-top: 15px; /* 增加按钮上边距 */
+}
+
+.graphics-container {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  gap: 15px; /* 增加间距 */
+}
+
+.land-type-selector {
+  margin-bottom: 15px; /* 增加下边距 */
+}
+
+.graphics-row {
+  display: flex;
+  gap: 15px; /* 增加间距 */
+  height: calc(100% - 45px); /* 减去选择器高度 */
+}
+
+.map-module, .chart-module {
+  flex: 1;
+  background: white;
+  border-radius: 8px;
+  box-shadow: 0 0 8px rgba(0, 0, 0, 0.1);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  overflow: hidden;
+  height: 100%; /* 固定高度 */
+}
+
+.map-box, .chart-box {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  position: relative;
+  padding: 10px; /* 增加内边距 */
+}
+
+.raster-image, .histogram-image {
+  width: 100%; /* 宽度固定为容器宽度 */
+  height: 100%; /* 高度固定为容器高度 */
+  object-fit: contain; /* 保持比例 */
+}
+
+.error-message {
+  color: #f56c6c;
+  font-size: 13px;
+  padding: 10px; /* 增加内边距 */
+  text-align: center;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 10px; /* 增加间距 */
+}
+
+.middle-gap {
+  height: 20px; /* 增加中间间距高度 */
+}
+
+.table-module {
+  height: 50%;
+  background: white;
+  border-radius: 12px;
+  box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
+  display: flex;
+  flex-direction: column;
+  margin-top: 50px; /* 增加上边距 */
+}
+
+.table-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 15px 20px; /* 增加内边距 */
+  background-color: #f8f9fa;
+  border-bottom: 1px solid #e1e4e8;
+}
+
+.table-header h3 {
+  margin: 0;
+  font-size: 16px; /* 增加字体大小 */
+  color: #2c3e50;
+}
+
+.compact-table {
+  width: 100%;
+  height: 100%;
+  font-size: 13px;
+}
+
+.compact-table >>> .el-table__cell {
+  padding: 10px 0; /* 增加单元格内边距 */
+}
+
+/* 修复表格中数字显示 */
+.compact-table >>> .el-table__cell .cell {
+  padding: 0 10px; /* 增加单元格内边距 */
+  text-align: center; /* 居中显示 */
+  white-space: nowrap; /* 防止换行 */
+  overflow: hidden; /* 隐藏溢出 */
+  text-overflow: ellipsis; /* 显示省略号 */
+}
 </style>

+ 0 - 2
src/views/User/heavyMetalFluxCalculation/inputFluxCalculation/waterdata/point.vue

@@ -3,7 +3,6 @@
     
    <div class="point-map">
     <div class="component-title">采样点地图展示</div>
-   <TencentMapView/>
    </div>
 
   
@@ -22,7 +21,6 @@
 <script setup>
 import { ref, onMounted, watch, computed } from 'vue';
 import * as echarts from 'echarts';
-import TencentMapView from './tencentMapView.vue';
 import Waterdataline from './waterdataline.vue';
 import Waterassaydata2 from './waterassaydata2.vue';
 

+ 238 - 0
src/views/User/hmInFlux/grainRemoval/grainRemovalInputFlux.vue

@@ -0,0 +1,238 @@
+<template>
+  <div class="crop-cd-model-container">
+    <el-card shadow="always" class="gradient-card">
+      <h2>作物镉模型计算</h2>
+      
+      <el-button class="enable-btn" @click="enableInputs">作物镉模型计算</el-button>
+      
+      <el-form label-width="250px" label-position="top" class="form-container">
+        <div class="form-section">
+          <!-- 作物亩产量 (单独一行) -->
+          <div class="input-row single-input">
+            <el-form-item label="作物亩产量 (斤)" class="input-item">
+              <el-input 
+                v-model="yieldPerMu" 
+                placeholder="800" 
+                :disabled="!inputsEnabled"
+                class="custom-input"
+              ></el-input>
+            </el-form-item>
+          </div>
+          
+          <div class="input-row">
+            <el-form-item label="标题" class="input-item">
+              <el-input 
+                v-model="soilCdContent" 
+                placeholder="15" 
+                :disabled="!inputsEnabled"
+                class="custom-input"
+              ></el-input>
+            </el-form-item>
+            <el-form-item label="标题" class="input-item">
+              <el-input 
+                v-model="safetyThreshold" 
+                placeholder="15/1000" 
+                :disabled="!inputsEnabled"
+                class="custom-input"
+              ></el-input>
+            </el-form-item>
+          </div>
+        </div>
+        
+        <div class="button-container">
+          <el-button 
+            type="primary" 
+            class="calculate-btn" 
+            :disabled="!inputsEnabled"
+          >
+            计算
+          </el-button>
+        </div>
+      </el-form>
+    </el-card>
+  </div>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      inputsEnabled: false,
+      yieldPerMu: '800',
+      soilCdContent: '0.76',
+      absorptionFactor: '0.0034',
+      soilPH: '0.5',
+      safetyThreshold: '15/1000'
+    };
+  },
+  methods: {
+    enableInputs() {
+      this.inputsEnabled = true;
+    }
+  }
+};
+</script>
+
+<style scoped>
+.crop-cd-model-container {
+  padding: 20px;
+  display: flex;
+  justify-content: center;
+}
+
+.gradient-card {
+  /* 半透明渐变背景 */
+  background: linear-gradient(
+    135deg, 
+    rgba(250, 253, 255, 0.8), 
+    rgba(137, 223, 252, 0.8)
+  );
+  backdrop-filter: blur(5px); /* 添加模糊效果增强半透明感 */
+  border-radius: 12px;
+  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
+  padding: 30px;
+  width: 90%;
+  max-width: 800px;
+  border: none;
+}
+
+h2 {
+  font-size: 24px;
+  margin-bottom: 20px;
+  text-align: center;
+  color: #333;
+  font-weight: 600;
+  padding-bottom: 15px;
+  border-bottom: 1px solid rgba(0, 0, 0, 0.1);
+}
+
+.form-container {
+  margin-top: 25px;
+}
+
+.form-section {
+  display: flex;
+  flex-direction: column;
+}
+
+.input-row {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 20px;
+  flex-wrap: wrap;
+}
+
+.single-input {
+  justify-content: center;
+}
+
+.input-item {
+  flex: 0 0 calc(50% - 15px); /* 两个输入框各占50%,减去间距 */
+  margin-bottom: 0;
+}
+
+.single-input .input-item {
+  flex: 0 0 100%; /* 单行输入框占满宽度 */
+  max-width: 500px; /* 限制最大宽度 */
+}
+
+:deep(.el-form-item__label) {
+  font-size: 16px;
+  font-weight: 500;
+  color: #333;
+  margin-bottom: 8px;
+  display: block;
+}
+
+.custom-input {
+  width: 100%;
+}
+
+:deep(.custom-input .el-input__inner) {
+  background: rgba(255, 255, 255, 0.7);
+  border-radius: 8px;
+  border: 1px solid #dcdfe6;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
+  padding: 12px 15px;
+  font-size: 16px;
+  color: #333;
+  transition: all 0.3s ease;
+}
+
+:deep(.custom-input .el-input__inner:focus) {
+  border-color: #409EFF;
+  box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
+}
+
+:deep(.custom-input .el-input__inner:disabled) {
+  background: rgba(245, 247, 250, 0.5);
+  color: #a8abb2;
+}
+
+.enable-btn, .calculate-btn {
+  width: 100%;
+  max-width: 300px;
+  height: 50px;
+  border: none;
+  border-radius: 25px !important;
+  font-size: 18px;
+  font-weight: bold;
+  transition: all 0.4s ease;
+  display: block;
+  margin: 0 auto;
+  
+  /* 渐变背景色 */
+  background: linear-gradient(to right, #8DF9F0, #26B046);
+  color: white !important;
+  /* 按钮整体阴影 */
+  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15),
+              0 2px 6px rgba(38, 176, 70, 0.3) inset;
+}
+
+.enable-btn {
+  margin-top: 10px;
+  margin-bottom: 20px;
+}
+
+.calculate-btn {
+  margin-top: 20px;
+}
+
+.enable-btn:hover, .calculate-btn:hover {
+  transform: scale(1.03);
+  box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2),
+              0 2px 8px rgba(38, 176, 70, 0.4) inset;
+  background: linear-gradient(to right, #7de8df, #20a03d);
+}
+
+.enable-btn:active, .calculate-btn:active {
+  transform: scale(0.98);
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1),
+              0 1px 6px rgba(38, 176, 70, 0.4) inset;
+}
+
+.button-container {
+  display: flex;
+  justify-content: center;
+}
+
+/* 响应式调整 */
+@media (max-width: 768px) {
+  .input-row {
+    flex-direction: column;
+  }
+  
+  .input-item {
+    flex: 0 0 100%;
+    margin-bottom: 20px;
+  }
+  
+  .gradient-card {
+    padding: 20px;
+  }
+  
+  .enable-btn, .calculate-btn {
+    width: 100%;
+  }
+}
+</style>

+ 25 - 0
src/views/User/hmInFlux/grainRemoval/samplingDesc1.vue

@@ -0,0 +1,25 @@
+<template>
+  <div class="">
+    
+  </div>
+</template>
+
+<script>
+export default {
+  name: '',
+  data() {
+    return {
+      
+    };
+  },
+  methods: {
+    
+  }
+};
+</script>
+
+<style scoped>
+  . {
+    
+  }
+</style>

+ 25 - 0
src/views/User/hmInFlux/strawRemoval/samplingDesc2.vue

@@ -0,0 +1,25 @@
+<template>
+  <div class="">
+    
+  </div>
+</template>
+
+<script>
+export default {
+  name: '',
+  data() {
+    return {
+      
+    };
+  },
+  methods: {
+    
+  }
+};
+</script>
+
+<style scoped>
+  . {
+    
+  }
+</style>

+ 258 - 0
src/views/User/hmInFlux/strawRemoval/strawRemovalInputFlux.vue

@@ -0,0 +1,258 @@
+<template>
+  <div class="crop-cd-model-container">
+    <el-card shadow="always" class="gradient-card">
+      <h2>作物镉模型计算</h2>
+      
+      <el-button class="enable-btn" @click="enableInputs">作物镉模型计算</el-button>
+      
+      <el-form label-width="250px" label-position="top" class="form-container">
+        <div class="form-section">
+          <div class="input-row">
+            <el-form-item label="作物亩产量 (斤)" class="input-item">
+              <el-input 
+                v-model="yieldPerMu" 
+                placeholder="800" 
+                :disabled="!inputsEnabled"
+                class="custom-input"
+              ></el-input>
+            </el-form-item>
+            
+            <el-form-item label="标题" class="input-item">
+              <el-input 
+                v-model="soilCdContent" 
+                placeholder="0.76" 
+                :disabled="!inputsEnabled"
+                class="custom-input"
+              ></el-input>
+            </el-form-item>
+          </div>
+          
+          <div class="input-row">
+            <el-form-item label="标题" class="input-item">
+              <el-input 
+                v-model="absorptionFactor" 
+                placeholder="0.0034" 
+                :disabled="!inputsEnabled"
+                class="custom-input"
+              ></el-input>
+            </el-form-item>
+            
+            <el-form-item label="标题" class="input-item">
+              <el-input 
+                v-model="soilPH" 
+                placeholder="0.5" 
+                :disabled="!inputsEnabled"
+                class="custom-input"
+              ></el-input>
+            </el-form-item>
+          </div>
+          
+          <div class="input-row single-input">
+            <el-form-item label="标题" class="input-item">
+              <el-input 
+                v-model="safetyThreshold" 
+                placeholder="15/1000" 
+                :disabled="!inputsEnabled"
+                class="custom-input"
+              ></el-input>
+            </el-form-item>
+          </div>
+        </div>
+        
+        <div class="button-container">
+          <el-button 
+            type="primary" 
+            class="calculate-btn" 
+            :disabled="!inputsEnabled"
+          >
+            计算
+          </el-button>
+        </div>
+      </el-form>
+    </el-card>
+  </div>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      inputsEnabled: false,
+      yieldPerMu: '800',
+      soilCdContent: '0.76',
+      absorptionFactor: '0.0034',
+      soilPH: '0.5',
+      safetyThreshold: '15/1000'
+    };
+  },
+  methods: {
+    enableInputs() {
+      this.inputsEnabled = true;
+    }
+  }
+};
+</script>
+
+<style scoped>
+.crop-cd-model-container {
+  padding: 20px;
+  display: flex;
+  justify-content: center;
+}
+
+.gradient-card {
+  /* 半透明渐变背景 */
+  background: linear-gradient(
+    135deg, 
+    rgba(250, 253, 255, 0.8), 
+    rgba(137, 223, 252, 0.8)
+  );
+  backdrop-filter: blur(5px); /* 添加模糊效果增强半透明感 */
+  border-radius: 12px;
+  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
+  padding: 30px;
+  width: 90%;
+  max-width: 800px; /* 增加最大宽度以容纳两列 */
+  border: none;
+}
+
+h2 {
+  font-size: 24px;
+  margin-bottom: 20px;
+  text-align: center;
+  color: #333;
+  font-weight: 600;
+  padding-bottom: 15px;
+  border-bottom: 1px solid rgba(0, 0, 0, 0.1);
+}
+
+.form-container {
+  margin-top: 25px;
+}
+
+.form-section {
+  display: flex;
+  flex-direction: column;
+}
+
+.input-row {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 20px;
+  flex-wrap: wrap;
+}
+
+.input-item {
+  flex: 0 0 calc(50% - 15px); /* 两个输入框各占50%,减去间距 */
+  margin-bottom: 0;
+}
+
+.single-input {
+  justify-content: center;
+}
+
+.single-input .input-item {
+  flex: 0 0 100%; /* 单行输入框占满宽度 */
+  max-width: 500px; /* 限制最大宽度 */
+}
+
+:deep(.el-form-item__label) {
+  font-size: 16px;
+  font-weight: 500;
+  color: #333;
+  margin-bottom: 8px;
+  display: block;
+}
+
+.custom-input {
+  width: 100%;
+}
+
+:deep(.custom-input .el-input__inner) {
+  background: rgba(255, 255, 255, 0.7);
+  border-radius: 8px;
+  border: 1px solid #dcdfe6;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
+  padding: 12px 15px;
+  font-size: 16px;
+  color: #333;
+  transition: all 0.3s ease;
+}
+
+:deep(.custom-input .el-input__inner:focus) {
+  border-color: #409EFF;
+  box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
+}
+
+:deep(.custom-input .el-input__inner:disabled) {
+  background: rgba(245, 247, 250, 0.5);
+  color: #a8abb2;
+}
+
+.enable-btn, .calculate-btn {
+  width: 100%;
+  max-width: 300px;
+  height: 50px;
+  border: none;
+  border-radius: 25px !important;
+  font-size: 18px;
+  font-weight: bold;
+  transition: all 0.4s ease;
+  display: block;
+  margin: 0 auto;
+  
+  /* 渐变背景色 */
+  background: linear-gradient(to right, #8DF9F0, #26B046);
+  color: white !important;
+  /* 按钮整体阴影 */
+  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15),
+              0 2px 6px rgba(38, 176, 70, 0.3) inset;
+}
+
+.enable-btn {
+  margin-top: 10px;
+  margin-bottom: 20px;
+}
+
+.calculate-btn {
+  margin-top: 20px;
+}
+
+.enable-btn:hover, .calculate-btn:hover {
+  transform: scale(1.03);
+  box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2),
+              0 2px 8px rgba(38, 176, 70, 0.4) inset;
+  background: linear-gradient(to right, #7de8df, #20a03d);
+}
+
+.enable-btn:active, .calculate-btn:active {
+  transform: scale(0.98);
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1),
+              0 1px 6px rgba(38, 176, 70, 0.4) inset;
+}
+
+.button-container {
+  display: flex;
+  justify-content: center;
+}
+
+/* 响应式调整 */
+@media (max-width: 768px) {
+  .input-row {
+    flex-direction: column;
+  }
+  
+  .input-item {
+    flex: 0 0 100%;
+    margin-bottom: 20px;
+  }
+  
+  .gradient-card {
+    padding: 20px;
+  }
+  
+  .enable-btn, .calculate-btn {
+    width: 100%;
+  }
+}
+</style>

+ 25 - 0
src/views/User/hmInFlux/subsurfaceLeakage/samplingDesc3.vue

@@ -0,0 +1,25 @@
+<template>
+  <div class="">
+    
+  </div>
+</template>
+
+<script>
+export default {
+  name: '',
+  data() {
+    return {
+      
+    };
+  },
+  methods: {
+    
+  }
+};
+</script>
+
+<style scoped>
+  . {
+    
+  }
+</style>

+ 118 - 0
src/views/User/hmInFlux/subsurfaceLeakage/subsurfaceLeakageInputFlux.vue

@@ -0,0 +1,118 @@
+<template>
+  <div class="leakage-container">
+    <el-card class="gradient-card" shadow="hover">
+      <el-row :gutter="20">
+        <el-col :span="24">
+          <p class="label">地下渗漏(g/ha/a)</p>
+        </el-col>
+        <el-col :span="24">
+          <el-input
+            v-model="leakage"
+            placeholder="请输入"
+            class="custom-input"
+          />
+        </el-col>
+        <el-col :span="24" style="margin-top: 20px;">
+          <el-button class="calculate-btn" @click="onCalculate">计算</el-button>
+        </el-col>
+      </el-row>
+    </el-card>
+  </div>
+</template>
+
+<script setup>
+import { ref } from 'vue';
+import { ElCard, ElRow, ElCol, ElInput, ElButton } from 'element-plus';
+
+const leakage = ref('0.023');
+
+const onCalculate = () => {
+  // 暂无计算逻辑,仅作展示
+  alert('计算按钮已点击');
+};
+</script>
+
+<style scoped>
+.leakage-container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  padding: 20px;
+}
+
+.gradient-card {
+  /* 半透明渐变背景 */
+  background: linear-gradient(
+    135deg, 
+    rgba(250, 253, 255, 0.8), 
+    rgba(137, 223, 252, 0.8)
+  );
+  border-radius: 12px;
+  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
+  padding: 30px;
+  text-align: left; /* 改为左对齐 */
+  width: 300px;
+  backdrop-filter: blur(5px); /* 添加模糊效果增强半透明感 */
+  border: none;
+}
+
+.label {
+  font-weight: bold;
+  font-size: 18px;
+  margin-bottom: 10px; /* 减少底部外边距 */
+  color: #333;
+}
+
+.custom-input {
+  width: 100%;
+  max-width: 200px;
+  margin-left: 0; /* 确保输入框靠左对齐 */
+}
+
+/* 自定义输入框样式 */
+:deep(.custom-input .el-input__inner) {
+  background: rgba(255, 255, 255, 0.7);
+  border-radius: 8px;
+  border: 1px solid #dcdfe6;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
+  padding: 10px 15px;
+  font-size: 16px;
+  color: #333;
+}
+
+:deep(.custom-input .el-input__inner:focus) {
+  border-color: #409EFF;
+  box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
+}
+
+.calculate-btn {
+  width: 100%;
+  max-width: 200px;
+  height: 50px;
+  border: none;
+  border-radius: 25px !important;
+  font-size: 18px;
+  font-weight: bold;
+  transition: all 0.4s ease;
+  
+  /* 渐变背景色 */
+  background: linear-gradient(to right, #8DF9F0, #26B046);
+  color: white !important;
+  /* 按钮整体阴影 */
+  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15),
+              0 2px 6px rgba(38, 176, 70, 0.3) inset;
+}
+
+.calculate-btn:hover {
+  transform: scale(1.03);
+  box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2),
+              0 2px 8px rgba(38, 176, 70, 0.4) inset;
+  background: linear-gradient(to right, #7de8df, #20a03d);
+}
+
+.calculate-btn:active {
+  transform: scale(0.98);
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1),
+              0 1px 6px rgba(38, 176, 70, 0.4) inset;
+}
+</style>

+ 25 - 0
src/views/User/hmInFlux/surfaceRunoff/samplingDesc4.vue

@@ -0,0 +1,25 @@
+<template>
+  <div class="">
+    
+  </div>
+</template>
+
+<script>
+export default {
+  name: '',
+  data() {
+    return {
+      
+    };
+  },
+  methods: {
+    
+  }
+};
+</script>
+
+<style scoped>
+  . {
+    
+  }
+</style>

+ 119 - 0
src/views/User/hmInFlux/surfaceRunoff/surfaceRunoffInputFlux.vue

@@ -0,0 +1,119 @@
+<template>
+  <div class="runoff-container">
+    <el-card class="gradient-card" shadow="hover">
+      <el-row :gutter="20">
+        <el-col :span="24">
+          <p class="label">地表径流(g/ha/a)</p>
+        </el-col>
+        <el-col :span="24">
+          <el-input
+            v-model="runoff"
+            placeholder="请输入"
+            class="custom-input"
+          />
+        </el-col>
+        <el-col :span="24" style="margin-top: 20px;">
+          <el-button class="calculate-btn" @click="onCalculate">计算</el-button>
+        </el-col>
+      </el-row>
+    </el-card>
+  </div>
+</template>
+
+<script setup>
+import { ref } from 'vue';
+import { ElCard, ElRow, ElCol, ElInput, ElButton } from 'element-plus';
+
+const runoff = ref('0.368');
+
+const onCalculate = () => {
+  // 暂无计算逻辑,仅作展示
+  alert('计算按钮已点击');
+};
+</script>
+
+<style scoped>
+.runoff-container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  padding: 20px;
+  margin-left: 10px; /* 确保输入框靠左对齐 */
+}
+
+.gradient-card {
+  /* 半透明渐变背景 */
+  background: linear-gradient(
+    135deg, 
+    rgba(250, 253, 255, 0.8), 
+    rgba(137, 223, 252, 0.8)
+  );
+  border-radius: 12px;
+  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
+  padding: 30px;
+  text-align: left; /* 改为左对齐 */
+  width: 300px;
+  backdrop-filter: blur(5px); /* 添加模糊效果增强半透明感 */
+  border: none;
+}
+
+.label {
+  font-weight: bold;
+  font-size: 18px;
+  margin-bottom: 10px; /* 减少底部外边距 */
+  color: #333;
+}
+
+.custom-input {
+  width: 100%;
+  max-width: 200px;
+  margin-left: 10px; /* 确保输入框靠左对齐 */
+}
+
+/* 自定义输入框样式 */
+:deep(.custom-input .el-input__inner) {
+  background: rgba(255, 255, 255, 0.7);
+  border-radius: 8px;
+  border: 1px solid #dcdfe6;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
+  padding: 10px 15px;
+  font-size: 16px;
+}
+
+:deep(.custom-input .el-input__inner:focus) {
+  border-color: #409EFF;
+  box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
+}
+
+.calculate-btn {
+  width: 100%;
+  max-width: 200px;
+  height: 50px;
+  border: none;
+  border-radius: 25px !important;
+  font-size: 18px;
+  font-weight: bold;
+  transition: all 0.4s ease;
+  
+  /* 渐变背景色 */
+  background: linear-gradient(to right, #8DF9F0, #26B046);
+  color: white !important;
+  /* 按钮整体阴影 */
+  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15),
+              0 2px 6px rgba(38, 176, 70, 0.3) inset;
+}
+
+.calculate-btn:hover {
+  transform: scale(1.03);
+  box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2),
+              0 2px 8px rgba(38, 176, 70, 0.4) inset;
+  background: linear-gradient(to right, #7de8df, #20a03d);
+}
+
+.calculate-btn:active {
+  transform: scale(0.98);
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1),
+              0 1px 6px rgba(38, 176, 70, 0.4) inset;
+}
+
+</style>

+ 1 - 1
src/views/User/introduction/Introduce.vue

@@ -52,7 +52,7 @@ const processParagraph = (paragraph: string) => {
 onMounted(async () => {
   try {
     // 从后端获取介绍数据
-    const response = await axios.get(`https://soilgd.com:5000/software-intro/${props.targetId}`);
+    const response = await axios.get(`https://127.0.0.1:5000/software-intro/${props.targetId}`);
     const { title, intro } = response.data;
     // 保留 \r\n 换行符
     const processedIntro = intro.replace(/<p>/g, '').replace(/<\/p>/g, '\r\n').replace(/<br>/g, '');

+ 57 - 571
src/views/menu/tencentMapView.vue → src/views/User/mapView/tencentMapView.vue

@@ -1,28 +1,17 @@
 <template>
   <div class="map-page">
-    <!-- 新增加载提示 -->
-    <div v-if="isLoading" class="loading-overlay">
-      <div class="loading-spinner"></div>
-      <p>地图数据加载中...</p>
-    </div>
-    <!-- 新增工具栏容器 -->
-    <div class="map-toolbar">
-      <RegionSelector 
-        class="compact-region-selector"
-        ref="regionSelector"
-        @region-change="handleRegionChange" />
-        
-    </div>
     <div ref="mapContainer" 
-    class="map-container"></div>
+    class="map-container"
+    ></div>
     <div v-if="error" class="error">{{ error }}</div>
-    
+    <!-- 覆盖层控制 -->
+    <!-- <div class="control-panel">
+      <label>
+        <input type="checkbox" v-model="state.showOverlay" @change="toggleOverlay" />
+        显示土壤类型覆盖
+      </label>
+    </div> -->
     <div class="control-panel">
-      <div class="basemap-toggle">
-        <button @click="toggleBaseLayer" :class="{ active: isBaseLayer }">
-          {{ isBaseLayer ? '纯净地图' : '腾讯地图' }}
-        </button>
-      </div>
       <label>
         <input type="checkbox" v-model="state.showSoilTypes" @change="toggleSoilTypeLayer" />
         显示韶关市评估单元
@@ -38,48 +27,14 @@
         </button>
       </div>
     </div>
-     <div class="map-legend" :class="{ active: isShowLegend }">
-        <div class="legend-controls">
-          <button @click="switchLegendType('cdRisk')">Cd风险</button>
-          <button @click="switchLegendType('safetyQ')">安全指数Q</button>
-        </div>
-      <div class="legend-header">
-        <h1>图例</h1>
-      </div>
-      <div class="legend-header">
-        <h4>{{ currentLegendTitle }}</h4>
-      </div>
-      
-      <!-- Cd风险等级图例 -->
-      <div v-if="currentLegend === 'cdRisk'" class="legend-section">
-        <div class="legend-scale">
-          <div class="scale-item" v-for="(item, index) in cdRiskLegend" :key="index">
-            <div class="color-box" :style="{ backgroundColor: item.color }"></div>
-            <span>{{ item.label }}</span>
-          </div>
-        </div>
-      </div>
-
-      <!-- 安全指数Q图例 -->
-      <div v-if="currentLegend === 'safetyQ'" class="legend-section">
-        <div class="legend-scale">
-          <div class="scale-item" v-for="(item, index) in safetyQLegend" :key="index">
-            <div class="color-box" :style="{ backgroundColor: item.color }"></div>
-            <span>{{ item.label }}</span>
-          </div>
-        </div>
-        <div class="legend-source">
-          <p>注:Q = 阈值/污染物含量</p>
-        </div>
-      </div>
-    </div>
   </div>
 </template>
 
 <script setup>
 import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
+import axios from 'axios'
+import markerIcon from '@/assets/dot.png' 
 import html2canvas from 'html2canvas'
-import RegionSelector from '@/components/RegionSelector.vue'
 
 const isExporting = ref(false)
 const isMapReady = ref(false)
@@ -106,160 +61,20 @@ const state = reactive({
 let soilTypeLayer = null
 let geoJSONLayer; 
 let currentInfoWindow = null;
-const surveyDataLayer = ref(null); // 保持响应式引用
-let multiPolygon;
-const districtLayers = ref(new Map()) // 存储区县图层
-const combinedSurveyFeatures = ref([]); // 存储原始数据
-const currentSurveyFilter = ref([]); // 当前选中区域
-const isLoading = ref(false)
+let surveyDataLayer = ref(null);
+let multiPolygon; 
 
 const categoryColors = { // 分类颜色配置
   '优先保护类': '#00C853', // 绿色
   '安全利用类': '#FFD600', // 黄色
-  '严格管控类': '#D50000', // 红色
-  '其他': '#CCCCCC', // 灰色
-  '农产品样品': '#4CAF50',    // 绿色
-  '土壤样品': '#2196F3'      // 蓝色
+  '严格管控类': '#D50000' // 红色
 };
 
-const isShowLegend = ref(true)
-const currentLegend = ref('cdRisk') // 默认显示Cd风险图例
-
-/// 图例配置数据
-const cdRiskLegend = reactive([
-  { label: '无风险', color: '#00C853' },
-  { label: '中低风险', color: '#FFD600' },
-  { label: '高风险', color: '#D50000' }
-])
-
-const safetyQLegend = reactive([
-  { label: 'Q < 1', color: '#00C853' },
-  { label: '1 < Q < 5', color: '#FFD600' },
-  { label: 'Q > 5', color: '#D50000' }
-])
-
-// 图例标题映射
-const legendTitles = {
-  cdRisk: 'Cd污染风险等级',
-  safetyQ: '安全生产指数Q'
-}
-
-const currentLegendTitle = computed(() => legendTitles[currentLegend.value])
-
-// 切换图例显示
-const toggleLegend = () => {
-  isShowLegend.value = !isShowLegend.value
-}
-
-// 切换图例类型
-const switchLegendType = (type) => {
-  currentLegend.value = type
-}
-
 const tMapConfig = reactive({
   key: import.meta.env.VITE_TMAP_KEY, // 请替换为你的开发者密钥
   geocoderURL: 'https://apis.map.qq.com/ws/geocoder/v1/'
 })
 
-const isBaseLayer = ref(false)
-const layerVisibility = reactive({
-  province: false,
-  city: false,
-  county: false
-})
-const currentZoom = ref(12)
-
-// 预加载所有GeoJSON图层
-const geoLayers = reactive({
-  province: null,
-  city: null,
-  county: null
-})
-
-const initBaseLayers = async () => {
-  try {
-    // 按层级加载GeoJSON(需替换实际路径)
-    isLoading.value = true;
-    geoLayers.province = await loadAndCreateLayer('/data/省.geojson', 'province')
-    geoLayers.city = await loadAndCreateLayer('/data/市.geojson', 'city')
-    geoLayers.county = await loadAndCreateLayer('/data/县.geojson', 'county')
-    
-    // 初始化默认状态
-    updateLayerVisibility()
-  } catch (error) {
-    console.error('加载地理数据失败:', error)
-    error.value = '地理数据加载失败'
-  } finally {
-    isLoading.value = false;
-  }
-}
-
-// 创建带样式的图层
-const loadAndCreateLayer = async (url, type) => {
-  const geoData = await loadGeoJSON(url)
-  return new TMap.value.vector.GeoJSONLayer({
-    map: map,
-    data: geoData,
-    zIndex: 5,
-    polygonStyle: new TMap.value.PolygonStyle({
-      color: 'rgba(242, 241, 237, 1)',
-      borderColor: '#000000',
-      borderWidth: 1
-    })
-  })
-}
-
-// 智能切换核心方法
-const toggleBaseLayer = () => {
-  isBaseLayer.value = !isBaseLayer.value
-  // 新增地图样式切换逻辑
-  if (map) {
-    map.setMapStyleId(isBaseLayer.value ? '1' : '0')
-  }
-  
-  if (isBaseLayer.value) {
-    map.on('zoom', handleZoomChange)
-    updateLayerVisibility()
-  } else {
-    map.off('zoom', handleZoomChange)
-    hideAllLayers()
-  }
-}
-
-// 缩放事件处理
-const handleZoomChange = () => {
-  currentZoom.value = map.getZoom()
-  updateLayerVisibility()
-}
-
-// 图层可见性逻辑
-const updateLayerVisibility = () => {
-  const zoom = currentZoom.value
-  const rules = [
-    { min: 0, max: 5, types: ['province'] },
-    { min: 5, max: 10, types: ['city'] },
-    { min: 10, max: 20, types: ['county'] }
-  ]
-
-  rules.forEach(rule => {
-    const isActive = zoom >= rule.min && zoom <= rule.max
-    rule.types.forEach(type => {
-      layerVisibility[type] = isActive && isBaseLayer.value
-      geoLayers[type]?.setVisible(isActive && isBaseLayer.value)
-    })
-  })
-}
-
-
-
-// 清理方法
-const hideAllLayers = () => {
-  Object.values(geoLayers).forEach(layer => {
-    if (layer) layer.setVisible(false)
-  })
-}
-
-
 
 const loadSDK = () => {
   return new Promise((resolve, reject) => {
@@ -367,7 +182,6 @@ const initData = () => {
 // 初始化地图
 const initMap = async () => {
   try {
-   isLoading.value = true
     await loadSDK()
     
     map = new TMap.value.Map(mapContainer.value, {
@@ -390,10 +204,9 @@ const initMap = async () => {
     //   map: map,
     //   styles: { default: defaultStyle }
     // })
-    const geojsonData = await loadGeoJSON('https://soilgd.com:8000/api/vector/export/all?table_name=unit_ceil');
+    const geojsonData = await loadGeoJSON('/data/单元格.geojson');
     initMapWithGeoJSON(geojsonData, map);
     await initSurveyDataLayer(map);
-    filterSurveyDataLayer(currentSurveyFilter.value)
     // 绑定点击事件
     // map.on('click', handleMapClick)
     // markersLayer.on('click', handleMarkerClick)
@@ -406,8 +219,6 @@ const initMap = async () => {
     updateMarkers()
   } catch (err) {
     error.value = err.message
-  } finally {
-    isLoading.value = false
   }
 }
 
@@ -678,129 +489,6 @@ async function loadGeoJSON(url) {
   return await response.json();
 }
 
-const handleRegionChange = async (districtNames) => {
-  isLoading.value = true;
-  console.log('收到区域变更:', districtNames)
-  currentSurveyFilter.value = districtNames;
-  
-  
-  // // 删除已取消选择的图层
-  // Array.from(districtLayers.value.keys()).forEach(name => {
-  //   if (!districtNames.includes(name)) {
-  //     const layer = districtLayers.value.get(name)
-  //     layer.setMap(null) // 正确销毁图层
-  //     districtLayers.value.delete(name)
-  //   }
-  // })
-
-  // // 添加新选择的图层
-  // await Promise.all(districtNames.map(async name => {
-  //   if (!districtLayers.value.has(name)) {
-  //     try {
-  //       const geoData = await loadGeoJSON(`/data/${name}.geojson`)
-        
-  //       // 创建独立图层实例
-  //       const layer = new TMap.value.vector.GeoJSONLayer({
-  //         map: map, // 确保传入当前地图实例
-  //         data: geoData,
-  //         zIndex: 3,
-  //         styles: {
-  //           // 按腾讯地图规范定义样式
-  //           polygonStyle: new TMap.value.PolygonStyle({
-  //             color: randomRGBA(0.3),
-  //             borderColor: '#FF0000',
-  //             borderWidth: 2
-  //           })
-  //         }
-  //       })
-
-  //       districtLayers.value.set(name, layer)
-  //     } catch (error) {
-  //       console.error(`加载【${name}】边界失败:`, error)
-  //     }
-  //   }
-  // }))
-  filterSurveyDataLayer(districtNames);
-  isLoading.value = false;
-}
-
-const filterSurveyDataLayer = (selectedRegions) => {
-     // ===== 1. 销毁旧图层 ===== [1,3](@ref)
-     if (surveyDataLayer.value) {
-      surveyDataLayer.value.setMap(null);  // 从地图解除关联
-      surveyDataLayer.value.destroy();     // 释放内存资源
-      surveyDataLayer.value = null;       // 清除引用
-    }
-
-    const mergedCategoryColors = {
-      ...categoryColors,
-    };
-
-    // 创建样式(包含默认分类)
-    const pointStyles = Object.keys(mergedCategoryColors).map(category => ({
-      id: category,
-      style: new TMap.value.MarkerStyle({
-        width: 12,
-        height: 12,
-        anchor: { x: 6, y: 6 },
-        src: createColoredCircle(mergedCategoryColors[category])
-      })
-    }));
-    const layerId = `survey-layer-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
-    surveyDataLayer.value = new TMap.value.MultiMarker({
-      id: layerId,
-      map: map,
-      styles: Object.assign({}, ...pointStyles.map(s => ({ [s.id]: s.style }))),
-      geometries: [] // 初始空数据
-    });
-  if (!surveyDataLayer?.value) {
-      throw new Error("调查数据图层未初始化")
-    }
-    if (!combinedSurveyFeatures?.value) {
-      throw new Error("调查数据未加载")
-    }
-  console.groupCollapsed("[区域过滤] 调试信息");
-  console.log("🔄 收到过滤请求,当前选中区域:", selectedRegions);
-  console.log("📦 原始数据总量:", combinedSurveyFeatures.value.length);
-  console.log(combinedSurveyFeatures.value);
-
-  const filtered = selectedRegions.length === 0 
-    ? combinedSurveyFeatures.value 
-    : combinedSurveyFeatures.value.filter(feature => {
-        const xmc = feature.properties.XMC || '';
-        return selectedRegions.some(region => xmc.includes(region));
-      });
-      console.log("✅ 过滤后数据量:", filtered.length);
-      console.log("🔍 示例过滤后数据:", filtered.slice(0,3).map(f => ({
-        id: f.properties.ID || f.properties.OBJECTID,
-        XMC: f.properties.XMC,
-        CMC: f.properties.CMC,
-        H_XTFX: f.properties.H_XTFX,
-      })));
-      console.groupEnd();
-
-      try {
-      surveyDataLayer.value.setGeometries(filtered.map(feature => ({
-        id: feature.properties.ID || feature.properties.OBJECTID,
-        styleId: feature.properties.H_XTFX || feature.properties.h_xtfx || '其他',
-        position: new TMap.value.LatLng(
-          feature.geometry.coordinates[1],
-          feature.geometry.coordinates[0]
-        ),
-        properties: {
-          ...feature.properties,
-          H_XTFX: feature.properties.H_XTFX || '其他'
-        }
-        
-      })));
-      console.log("🗺️ 图层更新成功");
-    } catch (e) {
-      console.error("[图层操作异常]", e);
-      error.value = `地图更新失败: ${e.message}`;
-      setTimeout(() => error.value = null, 5000);
-    }
-};
-
 function initMapWithGeoJSON(geojsonData, map) {
   // 销毁旧图层
   if (geoJSONLayer) {
@@ -811,7 +499,7 @@ function initMapWithGeoJSON(geojsonData, map) {
   geoJSONLayer = new TMap.value.vector.GeoJSONLayer({
     map: map,
     data: geojsonData,
-    zIndex: 10,
+    zIndex: 1,
     polygonStyle: new TMap.value.PolygonStyle({ // 必须用 PolygonStyle 类实例
       color: 'rgba(255, 0, 0, 0.25)', 
       showBorder: true,
@@ -826,7 +514,7 @@ function initMapWithGeoJSON(geojsonData, map) {
   // 高亮选中图层
   const highlightLayer = new TMap.value.MultiPolygon({
         map,
-        zIndex: 20,
+        zIndex: 2,
         styles: {
           highlight: new TMap.value.PolygonStyle({ // 注意要改为 PolygonStyle
             color: 'rgba(0, 123, 255, 0.5)',      // 半透明蓝色填充
@@ -859,56 +547,40 @@ function initMapWithGeoJSON(geojsonData, map) {
 // 加载调查数据并初始化图层
 const initSurveyDataLayer = async (map) => {
   try {
-    isLoading.value = true
-    const geoJsonFiles = [
-      'https://soilgd.com:8000/api/vector/export/all?table_name=surveydata',
-      '/data/河池土壤样品.geojson',
-      '/data/河池农产品样品.geojson',
-    ];
-
-    const surveyDataArray = await Promise.all(geoJsonFiles.map(loadGeoJSON));
-    const features = surveyDataArray.flatMap(geoData => geoData.features);
+    // 加载GeoJSON数据
+    const surveyData = await loadGeoJSON('/data/调查数据.geojson');
     
-    // 保存原始数据用于过滤
-    combinedSurveyFeatures.value = features;
-
-    // 合并颜色配置(添加默认分类)
-    const mergedCategoryColors = {
-      ...categoryColors,
-    };
-
-    // 创建样式(包含默认分类)
-    const pointStyles = Object.keys(mergedCategoryColors).map(category => ({
+    // 创建分类样式
+    const pointStyles = Object.keys(categoryColors).map(category => ({
       id: category,
       style: new TMap.value.MarkerStyle({
         width: 12,
         height: 12,
         anchor: { x: 6, y: 6 },
-        src: createColoredCircle(mergedCategoryColors[category])
+        src: createColoredCircle(categoryColors[category]) // 生成圆形图标
       })
     }));
 
-    // 初始化图层(处理缺失属性)
-    surveyDataLayer.value = new TMap.value.MultiMarker({
+    // 初始化图层
+    surveyDataLayer = new TMap.value.MultiMarker({
       map: map,
       styles: Object.assign({}, ...pointStyles.map(s => ({ [s.id]: s.style }))),
-      geometries: combinedSurveyFeatures.value.map(feature => ({
-        id: feature.properties.ID || feature.properties.OBJECTID,
-        styleId: feature.properties.H_XTFX || feature.properties.h_xtfx || '其他', // 设置默认值
+      geometries: surveyData.features.map(feature => ({
+        id: feature.properties.ID,
+        styleId: feature.properties.H_XTFX,
         position: new TMap.value.LatLng(
-          feature.geometry.coordinates[1],
+          feature.geometry.coordinates[1], 
           feature.geometry.coordinates[0]
         ),
         properties: {
           ...feature.properties,
-          // 强制添加H_XTFX字段保证数据一致性
-          H_XTFX: feature.properties.H_XTFX|| '其他' 
+          
         }
       }))
     });
 
     // 添加点击事件
-    surveyDataLayer.value.on('click', (event) => {
+    surveyDataLayer.on('click', (event) => {
       const prop = event.geometry.properties;
       if (currentInfoWindow) currentInfoWindow.close();
       currentInfoWindow = new TMap.value.InfoWindow({
@@ -923,14 +595,7 @@ const initSurveyDataLayer = async (map) => {
       });
     });
   } catch (error) {
-    console.error("调查数据加载失败:", error);
-    // 添加详细错误日志
-    console.groupCollapsed("[错误详情]");
-    console.error("错误对象:", error);
-    console.trace("调用堆栈");
-    console.groupEnd();
-  } finally {
-    isLoading.value = false
+    console.error('调查数据加载失败:', error);
   }
 };
 
@@ -971,7 +636,7 @@ const createColoredCircle = (color) => {
       return;
     }
     if (surveyDataLayer) {
-      surveyDataLayer.value.setVisible(state.showSurveyData);
+      surveyDataLayer.setVisible(state.showSurveyData);
     }
   };
 
@@ -999,7 +664,6 @@ onMounted(async () => {
     await loadSDK()
     initData()
     await initMap()
-    await initBaseLayers()
   } catch (err) {
     error.value = err.message
   }
@@ -1015,96 +679,41 @@ onBeforeUnmount(() => {
     infoWindow.value.close()
     infoWindow.value = null
   }
+  if (farmlandLayer) {
+    farmlandLayer.destroy(); 
+    farmlandLayer = null;
+  }
+  if (bboxLayer) {
+    bboxLayer.destroy(); 
+    bboxLayer = null;
+  }
   if (soilTypeLayer) {
     soilTypeLayer.destroy();
     soilTypeLayer = null;
   }
-  if (surveyDataLayer.value) {
-    surveyDataLayer.value.setMap(null);
-    surveyDataLayer.value.destroy();
+  if (surveyDataLayer) {
+    surveyDataLayer.destroy();
+    surveyDataLayer = null;
   }
-  map.off('zoom', handleZoomChange)
 })
 </script>
 
 <style scoped>
-.basemap-toggle {
-  margin-top: 8px;
-}
-
-.basemap-toggle button {
-  padding: 8px 16px;
-  background: #3876ff;
-  color: white;
-  border: none;
-  border-radius: 20px;
-  cursor: pointer;
-  transition: all 0.3s ease;
-}
-
-.basemap-toggle button:hover {
-  background: #2b5dc5;
-}
-
-
-
-/* 图层过渡动画 */
-.tmap-geojson-layer {
-  transition: opacity 0.3s ease, visibility 0.3s ease;
-}
-
-.map-toolbar {
-  position: relative; /* 确保层级上下文 */
-  z-index: 1000;     /* 低于子组件下拉菜单的z-index */
-  padding: 12px;
-  background: rgba(255, 255, 255, 0.9);
-  backdrop-filter: blur(5px);
-}
-
-.compact-region-selector {
-  width: 800px;
-  max-width: 100%;
-  
-  /* 重置可能影响子组件的样式 */
-  .selection-container {
-    gap: 8px;
-  }
-  
-  .select-group {
-    min-width: 180px;
-  }
-  
-  /* 移动端适配 */
-  @media (max-width: 768px) {
-    width: 100%;
-    
-    .selection-container {
-      flex-wrap: wrap;
-    }
-    
-    .select-group {
-      flex: 1 1 30%;
-    }
-  }
-}
-
 .map-page {
   position: relative;
   width: 100vw;
   height: 100vh;
-  display: flex;
-  flex-direction: column;
 }
 
 .map-container {
-  flex: 1;
-  height: calc(100vh - 48px); /* 对应新的工具栏高度 */
-  position: relative;
-  background: #f5f5f7;
+  width: 100%;
+  height: 100vh !important;
+  min-height: 600px;
 }
+
 .control-panel {
   position: fixed;
-  top: 80px;  /* 下移避开工具栏 */
+  top: 24px;
   right: 24px;
   background: rgba(255, 255, 255, 0.95);
   padding: 16px;
@@ -1185,34 +794,19 @@ onBeforeUnmount(() => {
   box-shadow: 0 4px 12px rgba(56, 118, 255, 0.3);
 }
 
-/* 新增加载提示样式 */
-.loading-overlay {
-  position: fixed;
-  top: 0;
-  left: 0;
-  width: 100%;
-  height: 100%;
-  background: rgba(255, 255, 255, 0.8);
-  display: flex;
-  flex-direction: column;
-  justify-content: center;
-  align-items: center;
-  z-index: 9999;
+/* 新增加载动画 */
+@keyframes spin {
+  0% { transform: rotate(0deg); }
+  100% { transform: rotate(360deg); }
 }
 
 .loading-spinner {
-  width: 50px;
-  height: 50px;
-  border: 5px solid #f3f3f3;
-  border-top: 5px solid #3876ff;
+  width: 18px;
+  height: 18px;
+  border: 2px solid rgba(255, 255, 255, 0.3);
+  border-top-color: white;
   border-radius: 50%;
-  animation: spin 1s linear infinite;
-  margin-bottom: 16px;
-}
-
-@keyframes spin {
-  0% { transform: rotate(0deg); }
-  100% { transform: rotate(360deg); }
+  animation: spin 0.8s linear infinite;
 }
 
 /* 响应式调整 */
@@ -1316,118 +910,10 @@ onBeforeUnmount(() => {
 .point-info h3[data-category="优先保护类"] { --category-color: #00C853; }
 .point-info h3[data-category="安全利用类"] { --category-color: #FFD600; }
 .point-info h3[data-category="严格管控类"] { --category-color: #D50000; }
-.point-info h3[data-category="其他"] { --category-color: #CCCCCC; }
-.point-info h3[data-category="土壤样品"] { --category-color: #2196F3; }
-.point-info h3[data-category="农产品样品"] { --category-color: #4CAF50; }
 .highlight-status {
   padding: 8px;
   background: rgba(0, 255, 0, 0.1);
   border-left: 3px solid #00FF00;
   margin-top: 12px;
 }
-.map-legend {
-  position: fixed;
-  left: 20px;
-  bottom: 80px;
-  z-index: 1000;
-  background: rgba(255, 255, 255, 0.95);
-  border-radius: 8px;
-  padding: 15px;
-  backdrop-filter: blur(8px);
-  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
-  transition: all 0.3s ease;
-  width: 220px;
-}
-
-.map-legend.active {
-  bottom: 20px;
-}
-
-.legend-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 12px;
-}
-
-.close-btn {
-  background: none;
-  border: none;
-  font-size: 20px;
-  cursor: pointer;
-  color: #666;
-}
-
-.legend-section {
-  max-height: 180px;
-  overflow-y: auto;
-}
-
-.legend-scale {
-  display: flex;
-  flex-direction: column; /* 改为纵向排列 */
-  gap: 10px;
-}
-
-.scale-item {
-  display: flex;
-  align-items: flex-start;
-  gap: 6px;
-}
-
-.color-box {
-  width: 20px;
-  height: 20px;
-  border-radius: 3px;
-  border: 1px solid #ccc;
-}
-
-.legend-source {
-  font-size: 12px;
-  color: #666;
-  line-height: 1.4;
-}
-
-/* 响应式调整 */
-@media (max-width: 768px) {
-  .map-legend {
-    width: 180px;
-    bottom: 10px;
-  }
-  
-  .scale-item {
-    flex-direction: column;
-    text-align: center;
-  }
-}
-.legend-controls {
-  display: flex;
-  gap: 12px;
-  margin-bottom: 16px;
-  position: relative;
-  z-index: 1001; /* 确保在图例面板之上 */
-}
-
-.legend-controls button {
-  padding: 8px 16px;
-  background: #3876ff;
-  color: white;
-  border: none;
-  border-radius: 20px;
-  cursor: pointer;
-  transition: all 0.3s ease;
-}
-
-.legend-controls button:hover{
-  background: #2b5dc5;
-}
-
-/* 响应式调整 */
-@media (max-width: 768px) {
-  .legend-btn {
-    padding: 8px 16px;
-    font-size: 13px;
-    border-radius: 20px;
-  }
-}
 </style>

+ 282 - 144
src/views/User/selectCityAndCounty.vue

@@ -1,57 +1,64 @@
 <template>
   <div class="city-selection full-page">
-    <h1 class="title">选择市及县区</h1>
-
-    <div class="city-buttons-wrapper">
-      <el-row :gutter="20" class="city-buttons">
-        <el-col :span="8" v-for="city in cities" :key="city.name">
-          <el-button
-            class="city-button"
-            :class="{ selected: selectedCity === city.name }"
-            @click="selectCity(city.name)"
-          >
-            {{ city.name }}
-          </el-button>
-        </el-col>
-      </el-row>
+    <!-- 左侧背景图容器 -->
+    <div class="background-container">
+      <h1 class="title">选择市县及地区</h1>
+      <!-- 城市按钮容器放在背景图内 -->
+      <div class="city-buttons-container">
+        <el-button
+          v-for="city in cities"
+          :key="city.name"
+          class="city-button"
+          :class="{ selected: selectedCity === city.name }"
+          @click="selectCity(city.name)"
+        >
+          {{ city.name }}
+        </el-button>
+      </div>
     </div>
-
-    <h2 class="subtitle" v-if="selectedCity">请选择县或区</h2>
-
-   <!-- 1. 使用 el-checkbox-group 进行多选绑定 -->
-   <el-checkbox-group
-      v-if="selectedCity"
-      v-model="selectedDistricts"
-      class="districts"
-    >
-      <!-- 2. 每个 el-checkbox 包裹整个 卡片 -->
-      <el-checkbox
-        v-for="district in districts[selectedCity]"
-        :key="district.name"
-        :value="district.name"
-        class="district-checkbox"
-      >
-        <div class="district-card">
-          <img :src="district.image" class="district-image" />
-          <div class="district-content">
-            <span class="district-name">{{ district.name }}</span>
-            <p class="district-desc">{{ district.description }}</p>
+    
+    <!-- 右侧内容区域 -->
+    <div class="content-area">
+      <h2 v-if="selectedCity" class="subtitle">请选择县或区</h2>
+      
+      <div class="districts-container">
+        <div class="districts-group">
+          <div class="districts">
+            <div 
+              v-for="district in districts[selectedCity]"
+              :key="district.name"
+              class="district-item"
+            >
+              <div 
+                class="district-card"
+                :class="{ selected: isDistrictSelected(district.name) }"
+                @click="toggleDistrict(district.name)"
+              >
+                <img :src="district.image" class="district-image" />
+                <div class="district-content">
+                  <span class="district-name">{{ district.name }}</span>
+                  <p class="district-desc">{{ district.description }}</p>
+                </div>
+                <el-icon class="arrow-icon">
+                  <arrow-right />
+                </el-icon>
+              </div>
+            </div>
           </div>
-          <el-icon class="arrow-icon">
-            <arrow-right />
-          </el-icon>
         </div>
-      </el-checkbox>
-    </el-checkbox-group>
-
-    <el-button
-      type="primary"
-      class="go-button"
-      :disabled="selectedDistricts.length === 0"
-      @click="goToKanBan"
-    >
-      跳转到数据看板
-    </el-button>
+      </div>
+
+      <div class="button-container">
+        <el-button
+          type="primary"
+          class="go-button"
+          :disabled="selectedDistricts.length === 0"
+          @click="goToKanBan"
+        >
+          跳转到重金属输出通量
+        </el-button>
+      </div>
+    </div>
   </div>
 </template>
 
@@ -73,39 +80,39 @@ export default {
 
     const districts = {
       韶关: [
-        { name: "武江区", description: "游武江山水,品韶关古韵", image: "/images/武江区.jpg" },
-        { name: "浈江区", description: "浈江水秀,韶关人杰地灵", image: "/images/浈江区.jpg" },
-        { name: "曲江区", description: "曲江风光,韶关人文荟萃", image: "/images/曲江区.jpg" },
-        { name: "始兴县", description: "始兴古韵,韶关文化底蕴深厚", image: "/images/始兴县.jpg" },
-        { name: "仁化县", description: "仁化山水,韶关自然风光迷人", image: "/images/仁化县.jpg" },
-        { name: "翁源县", description: "翁源美食,韶关美味之乡", image: "/images/翁源县.jpg" },
-        { name: "乳源瑶族自治县", description: "乳源瑶族风情,韶关民族文化多姿多彩", image: "/images/乳源瑶族自治县.jpg" },
-        { name: "新丰县", description: "新丰美景,韶关自然资源丰富", image: "/images/新丰县.jpg" },
-        { name: "乐昌市", description: "乐昌风光,韶关旅游胜地", image: "/images/乐昌市.jpg" },
-        { name: "南雄市", description: "南雄古城,韶关历史文化名城", image: "/images/南雄市.jpg" }
+        { name: "武江区", image: "/images/武江区.jpg" },
+        { name: "浈江区", image: "/images/浈江区.jpg" },
+        { name: "曲江区", image: "/images/曲江区.jpg" },
+        { name: "始兴县", image: "/images/始兴县.jpg" },
+        { name: "仁化县", deimage: "/images/仁化县.jpg" },
+        { name: "翁源县", image: "/images/翁源县.jpg" },
+        { name: "乳源瑶族自治县", image: "/images/乳源瑶族自治县.jpg" },
+        { name: "新丰县", image: "/images/新丰县.jpg" },
+        { name: "乐昌市", image: "/images/乐昌市.jpg" },
+        { name: "南雄市", image: "/images/南雄市.jpg" }
       ],
       河池: [
-        { name: "金城江区", description: "金城江风光,河池文化中心", image: "/images/金城江区.jpg" },
-        { name: "宜州区", description: "宜州美景,河池历史名城", image: "/images/宜州区.jpg" },
-        { name: "南丹县", description: "南丹瑶族风情,河池自然奇观", image: "/images/南丹县.jpg" },
-        { name: "天峨县", description: "天峨山水,河池生态天堂", image: "/images/天峨县.jpg" },
-        { name: "凤山县", description: "凤山溶洞,河池地质奇观", image: "/images/凤山县.jpg" },
-        { name: "东兰县", description: "东兰红色文化,河池革命老区", image: "/images/东兰县.jpg" },
-        { name: "罗城仫佬族自治县", description: "罗城仫佬族风情,河池民族文化", image: "/images/罗城仫佬族自治县.jpg" },
-        { name: "环江毛南族自治县", description: "环江毛南族风情,河池自然美景", image: "/images/环江毛南族自治县.jpg" },
-        { name: "巴马瑶族自治县", description: "巴马长寿之乡,河池健康之地", image: "/images/巴马瑶族自治县.png" },
-        { name: "都安瑶族自治县", description: "都安瑶族风情,河池山水画卷", image: "/images/都安瑶族自治县.jpg" },
-        { name: "大化瑶族自治县", description: "大化瑶族文化,河池民族风采", image: "/images/大化瑶族自治县.jpg" }
+        { name: "金城江区", image: "/images/金城江区.jpg" },
+        { name: "宜州区", image: "/images/宜州区.jpg" },
+        { name: "南丹县", image: "/images/南丹县.jpg" },
+        { name: "天峨县", image: "/images/天峨县.jpg" },
+        { name: "凤山县", image: "/images/凤山县.jpg" },
+        { name: "东兰县", image: "/images/东兰县.jpg" },
+        { name: "罗城仫佬族自治县", image: "/images/罗城仫佬族自治县.jpg" },
+        { name: "环江毛南族自治县", image: "/images/环江毛南族自治县.jpg" },
+        { name: "巴马瑶族自治县", image: "/images/巴马瑶族自治县.png" },
+        { name: "都安瑶族自治县", image: "/images/都安瑶族自治县.jpg" },
+        { name: "大化瑶族自治县", image: "/images/大化瑶族自治县.jpg" }
       ],
       腾冲: [
-        { name: "腾冲市区", description: "腾冲火山热海,云南旅游胜地", image: "/images/腾冲市区.jpg" },
-        { name: "和顺古镇", description: "和顺古镇,腾冲文化名片", image: "/images/和顺古镇.jpg" },
-        { name: "固东镇", description: "固东银杏村,腾冲自然美景", image: "/images/固东镇.jpg" },
-        { name: "界头镇", description: "界头田园风光,腾冲农业基地", image: "/images/界头镇.jpg" },
-        { name: "曲石镇", description: "曲石温泉,腾冲休闲之地", image: "/images/曲石镇.jpg" },
-        { name: "明光镇", description: "明光古村,腾冲历史遗迹", image: "/images/明光镇.jpg" },
-        { name: "清水乡", description: "清水河畔,腾冲自然风光", image: "/images/清水乡.jpg" },
-        { name: "猴桥镇", description: "猴桥边境风情,腾冲异域风光", image: "/images/猴桥镇.jpg" }
+        { name: "腾冲市区", image: "/images/腾冲市区.jpg" },
+        { name: "和顺古镇", image: "/images/和顺古镇.jpg" },
+        { name: "固东镇", image: "/images/固东镇.jpg" },
+        { name: "界头镇", image: "/images/界头镇.jpg" },
+        { name: "曲石镇", image: "/images/曲石镇.jpg" },
+        { name: "明光镇", image: "/images/明光镇.jpg" },
+        { name: "清水乡", image: "/images/清水乡.jpg" },
+        { name: "猴桥镇", image: "/images/猴桥镇.jpg" }
       ],
     };
 
@@ -117,6 +124,23 @@ export default {
       selectedCity.value = cityName;
       selectedDistricts.value = []; // 切换城市时清空所选项
     };
+    
+    // 切换地区选择状态
+    const toggleDistrict = (districtName) => {
+      const index = selectedDistricts.value.indexOf(districtName);
+      if (index >= 0) {
+        // 如果已选中,则移除
+        selectedDistricts.value.splice(index, 1);
+      } else {
+        // 如果未选中,则添加
+        selectedDistricts.value.push(districtName);
+      }
+    };
+    
+    // 检查地区是否被选中
+    const isDistrictSelected = (districtName) => {
+      return selectedDistricts.value.includes(districtName);
+    };
 
     const goToKanBan = () => {
       localStorage.setItem("selectedDistricts", JSON.stringify(selectedDistricts.value));
@@ -125,7 +149,7 @@ export default {
         router.push({ name: "parameterConfig" }); // 管理员跳转到 parameterConfig
       } else {
         router.push({
-          name: "shuJuKanBan", // 普通用户跳转到 shuJuKanBan
+          name: "samplingMethodDevice1", // 普通用户跳转到 samplingMethodDevice1
           query: { districts: selectedDistricts.value.join(",") },
         });
       }
@@ -137,6 +161,8 @@ export default {
       districts,
       selectedDistricts,
       selectCity,
+      toggleDistrict,
+      isDistrictSelected,
       goToKanBan,
     };
   },
@@ -144,139 +170,251 @@ export default {
 </script>
 
 <style scoped>
-.city-selection {
-  text-align: center;
-  background: linear-gradient(180deg, #a581ff, #b3e5fc);
-  padding: 20px;
+/* 左侧背景图容器 */
+.background-container {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 300px;
+  height: 100vh;
+  background-image: url('@/assets/city-bg.jpg');
+  background-size: cover;
+  background-position: center;
+  z-index: 0;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding-top: 40px;
 }
 
-.city-selection.full-page {
+.city-selection {
+  display: flex;
   min-height: 100vh;
+  position: relative;
+}
+
+.content-area {
+  margin-left: 300px;
+  flex: 1;
+  padding: 20px;
   display: flex;
   flex-direction: column;
-  align-items: center;
-  justify-content: flex-start;
-  padding-top: 100px;
+  height: 100vh;
+  box-sizing: border-box;
 }
 
 .title {
-  color: #FEFDFB;
+  color: #fff;
   font-weight: 700;
-  font-size: 64px;
+  font-size: 36px;
   margin-bottom: 60px;
   text-align: center;
+  text-shadow: 0 2px 4px rgba(0,0,0,0.5);
+  padding: 0 20px;
 }
 
-.city-buttons-wrapper {
+/* 城市按钮容器 */
+.city-buttons-container {
   display: flex;
-  justify-content: center;
-  width: 100%;
-}
-
-.city-buttons {
-  width: 750px;
+  flex-direction: column;
+  gap: 35px;
+  width: 80%;
+  padding: 90px 70px 0 30px;
 }
 
 .city-button {
-  width: 159px;
-  height: 159px;
-  font-size: 36px;
+  width: 100%;
+  height: 80px;
+  font-size: 28px;
   border: none;
-  border-radius: 35px 35px 60px 35px;
-  background-color: #E1E2F8;
-  color: #B5C1F5;
+  border-radius: 15px;
+  background-color: rgba(255, 255, 255, 0.3);
+  color: #fff;
   transition: all 0.3s ease;
   cursor: pointer;
+  box-shadow: 0 4px 8px rgba(0,0,0,0.2);
+  backdrop-filter: blur(5px);
+}
+
+.city-button:hover {
+  background-color: rgba(255, 255, 255, 0.5);
 }
 
 .city-button.selected {
-  background-color: #1266FC;
+  background-color: rgba(18, 102, 252, 0.8);
   color: #E9FEFF;
+  box-shadow: 0 6px 12px rgba(0,0,0,0.3);
 }
 
+/* 县/区标题 */
 .subtitle {
-  margin-top: 85px;
-  margin-left: 224px;
   font-size: 24px;
   font-weight: bold;
   color: #333;
-  width: 100%;
+  margin: 20px 0 20px 30px;
   text-align: left;
 }
 
-/* 将 el-checkbox-group 当作 flex 容器,使卡片并排排列 */
+/* 卡片容器布局 */
+.districts-container {
+  width: 100%;
+  padding: 0 20px;
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+}
+
+.districts-group {
+  flex: 1;
+  overflow-y: auto;
+  padding: 10px 0;
+}
+
 .districts {
-  margin-top: 40px;
   display: flex;
   flex-wrap: wrap;
-  justify-content: center;
-  gap: 150px;
-  width: 100%;
-  padding: 0 20px;
+  justify-content: flex-start;
+  gap: 30px;
+  padding-bottom: 20px;
 }
 
-/* 让每个 el-checkbox 占用固定宽度,并且把卡片当成 label 显示 */
-.district-checkbox {
-  display: block;
-  width: 374px;
+/* 每个卡片容器 */
+.district-item {
+  width: calc(50% - 15px);
+  min-width: 400px;
+  max-width: 500px;
+  margin-bottom: 20px;
 }
 
+/* 卡片样式 */
 .district-card {
-  width: 374px;
-  height: 125px;
+  width: 95%;
+  height: 150px;
   background-color: #B5DBF0;
   border-radius: 12px;
   display: flex;
   align-items: center;
-  justify-content: space-between;
-  padding: 16px;
-  box-shadow: 0 4px 8px rgba(0,0,0,0.1);
+  padding: 20px;
+  box-shadow: 0 4px 15px rgba(0,0,0,0.1);
   cursor: pointer;
-  transition: background-color 0.2s ease;
+  transition: all 0.3s ease;
+  position: relative;
+  border: 2px solid transparent; /* 默认透明边框 */
+}
+
+/* 选中状态的卡片样式 */
+.district-card.selected {
+  border-color: #1266FC;
+  background-color: #a1c3e6;
+  box-shadow: 0 6px 20px rgba(18, 102, 252, 0.3);
 }
 
 .district-card:hover {
   background-color: #a1c3e6;
+  transform: translateY(-5px);
+  box-shadow: 0 6px 20px rgba(0,0,0,0.15);
 }
 
 .district-image {
-  width: 150px;
-  height: 100px;
-  object-fit: contain;
-  margin-right: 16px;
+  width: 170px;
+  height: 110px;
+  object-fit: cover;
+  border-radius: 8px;
+  margin-right: 20px;
+  flex-shrink: 0;
 }
 
 .district-content {
   flex: 1;
-  text-align: left;
-  margin-right: 16px;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
 }
 
 .district-name {
-  font-size: 20px;
-  font-weight: 600;
+  font-size: 22px;
+  font-weight: 700;
   color: #222;
+  margin-bottom: 10px;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
 }
 
 .district-desc {
-  margin-top: 8px;
-  font-size: 14px;
+  font-size: 16px;
   color: #444;
+  line-height: 1.4;
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  flex-grow: 1;
+}
+
+.arrow-icon {
+  font-size: 24px;
+  color: #1266FC;
+  margin-left: 15px;
+  flex-shrink: 0;
+}
+
+/* 按钮容器 - 固定在底部 */
+.button-container {
+  padding: 20px 0;
+  text-align: center;
+  flex-shrink: 0;
+  position: sticky;
+  bottom: 20px;
+  z-index: 10;
+  border-radius: 10px;
+  margin-top: auto; /* 确保按钮在底部 */
 }
 
 .go-button {
-  margin-top: 180px;
-  width: 240px;
-  height: 50px;
-  font-size: 18px;
-  border-radius: 25px;
+  width: 200px;
+  height: 60px;
+  font-size: 20px;
+  border-radius: 30px;
+  margin-right: -1130px;
+  background: linear-gradient(to right, #1266FC, #4e8cff);
+  border: none;
+  box-shadow: 0 6px 15px rgba(18, 102, 252, 0.4);
+  transition: all 0.3s ease;
+}
+
+.go-button:hover {
+  transform: scale(1.05);
+  box-shadow: 0 8px 20px rgba(18, 102, 252, 0.6);
+}
+
+.go-button:disabled {
+  opacity: 0.6;
+  cursor: not-allowed;
 }
 
-/* 全局样式去除白边 */
-:global(html), :global(body) {
-  height: 100%;
-  margin: 0;
-  padding: 0;
-  background: linear-gradient(180deg, #a581ff, #b3e5fc);
+/* 滚动条样式 */
+.districts-group::-webkit-scrollbar {
+  width: 8px;
 }
-</style>
+
+.districts-group::-webkit-scrollbar-track {
+  background: #f1f1f1;
+  border-radius: 4px;
+}
+
+.districts-group::-webkit-scrollbar-thumb {
+  background: #888;
+  border-radius: 4px;
+}
+
+.districts-group::-webkit-scrollbar-thumb:hover {
+  background: #555;
+}
+
+.el-button, .el-button.is-round {
+    margin-left: 15px ;
+}
+
+</style>

+ 260 - 231
src/views/login/loginView.vue

@@ -1,134 +1,186 @@
 <template>
-  <div class="auth">
-    <!-- 登录表单 -->
-    <el-form
-      v-if="isLogin"
-      ref="formRef"
-      :model="form"
-      :rules="rules"
-      label-width="100px"
-      class="login-form"
-    >
-      <div class="form-header">
-        <!-- 切换语言按钮 -->
-        <el-button class="language-toggle" @click="toggleLanguage">{{
-          currentLanguageName
-        }}</el-button>
-        <!-- 切换用户类型按钮 -->
-        <el-button class="user-type-toggle" @click="toggleUserType">{{
-          currentUserTypeName
-        }}</el-button>
-      </div>
-      <h2 class="form-title">
-        {{
-          userType === "user" ? $t("login.userTitle") : $t("login.adminTitle")
-        }}
-      </h2>
-      <el-form-item :label="$t('login.username')" prop="name">
-        <el-input v-model="form.name"></el-input>
-      </el-form-item>
-      <el-form-item :label="$t('login.password')" prop="password">
-        <el-input type="password" v-model="form.password"></el-input>
-      </el-form-item>
-      <el-form-item>
-        <div class="button-group">
-          <el-button type="primary" @click="onSubmit" :loading="loading">
-            {{ $t("login.loginButton") }}
+  <div class="auth-wrapper">
+    <!-- 左侧背景图部分 -->
+    <div class="auth-left"></div>
+
+    <!-- 登录/注册表单部分 -->
+    <div class="auth-form-container">
+      <!-- 登录表单 -->
+      <el-form
+        v-if="isLogin"
+        ref="formRef"
+        :model="form"
+        :rules="rules"
+        label-width="100px"
+        class="login-form"
+      >
+        <div class="form-header">
+          <!-- 根据用户类型显示不同的标题 -->
+          <h2 class="form-title">
+            {{
+              userType === "user"
+                ? $t("login.userTitle")
+                : $t("login.adminTitle")
+            }}
+          </h2>
+          <!-- 切换用户类型的按钮 -->
+          <el-button class="user-type-toggle" @click="toggleUserType" link>
+            <el-icon><User /></el-icon>
+            <span>{{ currentUserTypeName }}</span>
           </el-button>
         </div>
-        <div class="register-link" style="margin-top: 20px; text-align: center">
-          <a @click="toggleForm">{{ $t("login.registerLink") }}</a>
+
+        <!-- 用户名输入框 -->
+        <el-form-item :label="$t('login.username')" prop="name">
+          <el-input v-model="form.name"></el-input>
+        </el-form-item>
+
+        <!-- 密码输入框 -->
+        <el-form-item :label="$t('login.password')" prop="password">
+          <el-input type="password" v-model="form.password"></el-input>
+        </el-form-item>
+
+        <!-- 语言切换按钮 -->
+        <div class="language-toggle-wrapper">
+          <span class="text-toggle" @click="toggleLanguage">{{
+            currentLanguageName
+          }}</span>
         </div>
-      </el-form-item>
-    </el-form>
-
-    <!-- 注册表单 -->
-    <el-form
-      v-else
-      ref="registerFormRef"
-      :model="registerForm"
-      :rules="registerRules"
-      label-width="100px"
-      class="login-form"
-    >
-      <div class="form-header">
-        <!-- 切换语言按钮 -->
-        <el-button class="language-toggle" @click="toggleLanguage">{{
-          currentLanguageName
-        }}</el-button>
-        <!-- 切换用户类型按钮 -->
-        <el-button class="user-type-toggle" @click="toggleUserType">{{
-          currentUserTypeName
-        }}</el-button>
-      </div>
-      <h2 class="form-title">{{ $t("register.title") }}</h2>
-      <el-form-item :label="$t('register.username')" prop="name">
-        <el-input v-model="registerForm.name"></el-input>
-      </el-form-item>
-      <el-form-item :label="$t('register.password')" prop="password">
-        <el-input type="password" v-model="registerForm.password"></el-input>
-      </el-form-item>
-      <el-form-item
-        :label="$t('register.confirmPassword')"
-        prop="confirmPassword"
+
+        <!-- 提交登录按钮和注册链接 -->
+        <el-form-item>
+          <div class="button-group">
+            <el-button
+              type="primary"
+              @click="onSubmit"
+              :loading="loading"
+              class="login-button"
+            >
+              {{ $t("login.loginButton") }}
+            </el-button>
+          </div>
+          <div class="text-link-wrapper">
+            <span class="text-toggle" @click="toggleForm">{{
+              $t("login.registerLink")
+            }}</span>
+          </div>
+        </el-form-item>
+      </el-form>
+
+      <!-- 注册表单 -->
+      <el-form
+        v-else
+        ref="registerFormRef"
+        :model="registerForm"
+        :rules="registerRules"
+        label-width="100px"
+        class="login-form"
       >
-        <el-input
-          type="password"
-          v-model="registerForm.confirmPassword"
-        ></el-input>
-      </el-form-item>
-      <el-form-item>
-        <div class="button-group">
-          <!-- 注册按钮 -->
-          <el-button type="primary" @click="onRegister" :loading="loading">
-            {{ $t("register.registerButton") }}
+        <div class="form-header">
+          <!-- 注册表单标题 -->
+          <h2 class="form-title">{{ $t("register.title") }}</h2>
+          <!-- 切换用户类型的按钮 -->
+          <el-button class="user-type-toggle" @click="toggleUserType" link>
+            <el-icon><User /></el-icon>
+            <span>{{ currentUserTypeName }}</span>
           </el-button>
         </div>
-        <div class="button-group">
-          <!-- 返回登录按钮 -->
-          <el-button @click="toggleForm">
-            {{ $t("register.backToLoginButton") }}
-          </el-button>
+
+        <!-- 用户名输入框 -->
+        <el-form-item :label="$t('register.username')" prop="name">
+          <el-input v-model="registerForm.name"></el-input>
+        </el-form-item>
+
+        <!-- 密码输入框 -->
+        <el-form-item :label="$t('register.password')" prop="password">
+          <el-input type="password" v-model="registerForm.password"></el-input>
+        </el-form-item>
+
+        <!-- 确认密码输入框 -->
+        <el-form-item
+          :label="$t('register.confirmPassword')"
+          prop="confirmPassword"
+        >
+          <el-input
+            type="password"
+            v-model="registerForm.confirmPassword"
+          ></el-input>
+        </el-form-item>
+
+        <!-- 语言切换按钮 -->
+        <div class="language-toggle-wrapper">
+          <span class="text-toggle" @click="toggleLanguage">{{
+            currentLanguageName
+          }}</span>
         </div>
-      </el-form-item>
-    </el-form>
+        <!-- 提交注册按钮和返回登录链接 -->
+        <el-form-item>
+          <div class="button-group">
+            <el-button
+              type="primary"
+              @click="onRegister"
+              :loading="loading"
+              class="login-button"
+            >
+              {{ $t("register.registerButton") }}
+            </el-button>
+          </div>
+          <div class="button-group">
+            <span type="primary" @click="toggleForm" :loading="loading" class="register-button">
+              {{ $t("register.backToLoginButton") }}
+            </span>
+          </div>
+        </el-form-item>
+      </el-form>
+    </div>
   </div>
 </template>
 
 <script lang="ts" setup>
 import { reactive, ref, computed } from "vue";
 import { ElMessage, ElForm } from "element-plus";
+import { User } from "@element-plus/icons-vue";
 import axios from "axios";
-import { login, register } from "@/API/users"; // 假设已封装好的接口
+import { login, register } from "@/API/users";
 import { useRouter, useRoute } from "vue-router";
 import { useTokenStore } from "@/stores/mytoken";
 import { useI18n } from "vue-i18n";
 import i18n from "@/i18n";
 
+// 获取状态管理实例
 const store = useTokenStore();
 const router = useRouter();
 const route = useRoute();
 
-const isLogin = ref(true); // 确保默认值为 true,显示登录表单
-const userType = ref("user"); // 当前用户类型,默认为普通用户
+// 控制是否显示登录表单
+const isLogin = ref(true);
+// 当前用户类型(用户或管理员)
+const userType = ref("user");
 
+// 登录表单数据模型
 const form = reactive({
   name: "",
   password: "",
 });
-console.log("form 的类型:", typeof form);
 
+// 注册表单数据模型
 const registerForm = reactive({
   name: "",
   password: "",
   confirmPassword: "",
 });
+
+// 表单引用
 const formRef = ref<InstanceType<typeof ElForm> | null>(null);
 const registerFormRef = ref<InstanceType<typeof ElForm> | null>(null);
+
+// 加载状态控制
 const loading = ref(false);
-const { t, locale } = useI18n(); // 使用 Composition API 模式
 
-// 获取当前语言名称
+// 国际化相关
+const { t, locale } = useI18n();
+
+// 当前语言名称
 const currentLanguageName = computed(() => {
   return locale.value === "zh" ? "English" : "中文";
 });
@@ -136,10 +188,10 @@ const currentLanguageName = computed(() => {
 // 切换语言
 const toggleLanguage = () => {
   locale.value = locale.value === "zh" ? "en" : "zh";
-  localStorage.setItem("lang", locale.value); // 更新 localStorage
+  localStorage.setItem("lang", locale.value);
 };
 
-// 获取当前用户类型名称
+// 当前用户类型名称
 const currentUserTypeName = computed(() => {
   return userType.value === "user"
     ? t("login.switchToAdmin")
@@ -156,7 +208,7 @@ const toggleForm = () => {
   isLogin.value = !isLogin.value;
 };
 
-// 密码登录提交
+// 提交登录请求
 const onSubmit = async () => {
   if (!formRef.value) return;
   formRef.value.validate(async (valid: boolean) => {
@@ -169,44 +221,30 @@ const onSubmit = async () => {
         userType: userType.value,
       });
       if (res.data.success) {
-        console.log("登录成功返回的信息:", res.data);
         const userInfo = {
           userId: parseInt(res.data.userId),
           name: res.data.name,
-          loginType: userType.value === "admin" ? "admin" : "user", // 根据用户类型设置 loginType
+          loginType: userType.value === "admin" ? "admin" : "user",
         };
         store.saveToken(userInfo);
-        console.log("保存的Token信息:", store.token);
         ElMessage.success(i18n.global.t("login.loginSuccess"));
-        if (res.data.success) {
-          let redirect = "/";
-          if (userType.value === "user") {
-            console.log("普通用户登录成功,准备跳转到 selectCityAndCounty");
-            router.push({ name: "selectCityAndCounty" }).then(() => {
-              console.log("loginView.vue:185 成功跳转到 selectCityAndCounty");
-            }).catch((err) => {
-              console.error("跳转到 selectCityAndCounty 失败:", err);
-            });
-          } else {
-            redirect =
-              typeof route.query.redirect === "string"
-                ? route.query.redirect
-                : "/select-city";
-            router.push(redirect);
-          }
-          console.log("用户类型:", userType.value, "跳转地址:", redirect);
+        // 根据用户类型跳转到不同页面
+        if (userType.value === "user") {
+          router.push({ name: "selectCityAndCounty" });
+        } else {
+          const redirect =
+            typeof route.query.redirect === "string"
+              ? route.query.redirect
+              : "/select-city";
+          router.push(redirect);
         }
       } else {
         ElMessage.error(res.data.message || i18n.global.t("login.loginFailed"));
       }
     } catch (error) {
       if (axios.isAxiosError(error)) {
-        console.error(
-          `HTTP Error: ${error.message}, status code: ${error.response?.status}`
-        );
         ElMessage.error(`HTTP Error: ${error.response?.statusText}`);
       } else {
-        console.error(error);
         ElMessage.error(i18n.global.t("login.loginFailed"));
       }
     } finally {
@@ -215,12 +253,13 @@ const onSubmit = async () => {
   });
 };
 
-// 注册提交
+// 提交注册请求
 const onRegister = async () => {
   if (!registerFormRef.value) return;
   registerFormRef.value.validate(async (valid: boolean) => {
     if (!valid) return;
 
+    // 检查两次输入的密码是否一致
     if (registerForm.password !== registerForm.confirmPassword) {
       ElMessage.error(i18n.global.t("register.passwordMismatch"));
       return;
@@ -233,7 +272,7 @@ const onRegister = async () => {
       });
       if (res.data.success) {
         ElMessage.success(i18n.global.t("register.registerSuccess"));
-        toggleForm(); // 切换回登录表单
+        toggleForm(); // 注册成功后切换回登录表单
       } else {
         ElMessage.error(
           res.data.message || i18n.global.t("register.registerFailed")
@@ -241,12 +280,8 @@ const onRegister = async () => {
       }
     } catch (error) {
       if (axios.isAxiosError(error)) {
-        console.error(
-          `HTTP Error: ${error.message}, status code: ${error.response?.status}`
-        );
         ElMessage.error(`HTTP Error: ${error.response?.statusText}`);
       } else {
-        console.error(error);
         ElMessage.error(i18n.global.t("register.registerFailed"));
       }
     } finally {
@@ -255,7 +290,7 @@ const onRegister = async () => {
   });
 };
 
-// 验证规则
+// 登录表单验证规则
 const rules = reactive({
   name: [
     {
@@ -279,6 +314,7 @@ const rules = reactive({
   ],
 });
 
+// 注册表单验证规则
 const registerRules = reactive({
   name: [
     {
@@ -325,152 +361,145 @@ const registerRules = reactive({
 </script>
 
 <style scoped>
-.auth {
-  background: linear-gradient(135deg, #74ebd5, #acb6e5); /* 渐变背景 */
+.auth-wrapper {
+  display: flex;
   height: 100vh;
+  background-color: #f6f6f6;
+}
+
+.auth-left {
+  width: 45%;
+  background: url("@/assets/login-bg.png") no-repeat center center;
+  background-size: cover;
+}
+
+.auth-form-container {
+  width: 50%;
+  padding: 0 40px 0 90px;
   display: flex;
   justify-content: center;
-  align-items: center;
-  font-family: "Arial", sans-serif; /* 更现代的字体 */
+  align-items: flex-start;
 }
 
 .login-form {
-  width: 400px; /* 更宽的表单 */
-  padding: 50px;
-  background: rgba(255, 255, 255, 0.9); /* 半透明背景 */
-  border-radius: 20px; /* 更大的圆角 */
-  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2); /* 更深的阴影 */
-  position: relative;
-  animation: fadeIn 0.5s ease-in-out; /* 淡入动画 */
-}
-
-@keyframes fadeIn {
-  from {
-    opacity: 0;
-    transform: translateY(-20px);
-  }
-  to {
-    opacity: 1;
-    transform: translateY(0);
-  }
+  width: 100%;
+  padding: 40px 0 60px 60px;
+  margin-top: 70px;
 }
 
+/* 表单头部 */
 .form-header {
   display: flex;
-  justify-content: space-between; /* 两端对齐 */
-  margin-bottom: 30x; /* 更大的间距 */
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 50px;
+  margin-top: 40px;
 }
 
-.language-toggle,
-.user-type-toggle {
-  padding: 12px 24px; /* 增加内边距 */
-  font-size: 16px; /* 更大的字体 */
-  color: #ffffff; /* 白色文字 */
-  border: none; /* 移除边框 */
-  border-radius: 25px; /* 圆角按钮 */
-  background: linear-gradient(135deg, #ff9a9e, #fad0c4); /* 柔和渐变背景 */
-  cursor: pointer;
-  transition: all 0.3s ease;
-  box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15); /* 更深的阴影 */
-  width: 150px; /* 统一宽度 */
-  text-align: center; /* 居中文本 */
+.form-title {
+  font-size: 32px;
+  font-weight: 600;
+  color: #333;
 }
 
-.language-toggle:hover,
-.user-type-toggle:hover {
-  background: linear-gradient(135deg, #fad0c4, #ff9a9e); /* 悬停时渐变反转 */
-  transform: translateY(-3px); /* 悬停时轻微上移 */
-  box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); /* 增强阴影 */
+.user-type-toggle {
+  position: relative;
+  top: -50px;
+  right: 20px;
+  margin-left: 10px;
+  font-size: 36px;
+  color: #333;
 }
 
-.form-title {
-  text-align: center;
-  font-size: 24px; /* 更大的字体 */
-  font-weight: bold;
-  margin: 20px 20px; /* 更大的外边距 */
+.user-type-toggle span {
+  margin-left: 6px;
+  color: #333;
+  font-size: 36px;
 }
 
-.el-form-item__label {
-  font-weight: bold;
-  color: #555; /* 更柔和的颜色 */
+/* 语言切换按钮 */
+.language-toggle-wrapper {
+  text-align: right;
+  margin: 5px 0 1px;
+  transform: translateX(-210px);
 }
 
-.el-input__inner {
-  border: 1px solid #ccc;
-  border-radius: 8px; /* 更大的圆角 */
-  padding: 12px; /* 更大的内边距 */
-  transition: border-color 0.3s ease, box-shadow 0.3s ease;
+/* 表单项样式 */
+:deep(.el-form-item__label) {
+  float: none !important;
+  display: block;
+  text-align: left;
+  font-size: 24px;
+  padding-bottom: 4px;
 }
 
-.el-input__inner:focus {
-  border-color: #409eff;
-  box-shadow: 0 0 5px rgba(64, 158, 255, 0.5); /* 聚焦时的阴影 */
+:deep(.el-form-item) {
+  display: flex;
+  flex-direction: column;
+  margin-bottom: 25px;
 }
 
-.el-button {
-  width: 150px; /* 统一按钮宽度 */
-  height: 45px; /* 调整按钮高度 */
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  font-size: 16px; /* 调整字体大小 */
-  border-radius: 25px; /* 圆角按钮 */
-  background: linear-gradient(135deg, #89f7fe, #66a6ff); /* 新的渐变背景 */
-  color: #ffffff; /* 白色文字 */
-  border: none; /* 移除边框 */
-  box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15); /* 添加阴影 */
-  transition: all 0.3s ease;
-  margin: 0 auto; /* 按钮居中 */
-  padding: 12px 85px; /* 增加内边距 */
+/* 登录按钮 */
+.login-button {
+  background: linear-gradient(to right, #8df9f0, #26b046);
+  width: 393px;
+  height: 56px;
+  color: white;
+  border: none;
+  border-radius: 20px;
+  font-size: 24px;
+  cursor: pointer;
+  margin-top: -30px; /* 向上移动20px */
 }
 
-.el-button:hover {
-  background: linear-gradient(135deg, #66a6ff, #89f7fe); /* 悬停时渐变反转 */
-  transform: translateY(-3px); /* 悬停时轻微上移 */
-  box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); /* 增强阴影 */
+.login-button:hover {
+  opacity: 0.9;
 }
 
-.el-button--primary {
-  background: linear-gradient(135deg, #43cea2, #185a9d); /* 主按钮渐变背景 */
+.register-button {
+  text-align: right;
+  margin: 90px 0 1px;
+  color: #478bf0;
+  font-size: 14px;
+  cursor: pointer;
+  transform: translateX(-240px);
 }
 
-.el-button--primary:hover {
-  background: linear-gradient(135deg, #185a9d, #43cea2); /* 悬停时渐变反转 */
+.register-button:hover {
+  color: #357ae8;
+  text-decoration: underline;
 }
 
+/* 按钮容器 */
 .button-group {
   display: flex;
-  justify-content: center; /* 按钮居中 */
-  margin-top: 30px; /* 更大的间距 */
+  justify-content: center;
 }
 
-.register-link {
-  text-align: center;
-  margin: 10px 80px;
+.text-toggle {
+  color: #478bf0;
   font-size: 14px;
-  color: #409eff;
   cursor: pointer;
 }
-.register-link a:hover {
+
+.text-toggle:hover {
+  color: #357ae8;
   text-decoration: underline;
 }
 
-@media (max-width: 480px) {
-  .login-form {
-    width: 90%; /* 小屏幕自适应 */
-    padding: 30px;
-  }
-
-  .el-button {
-    width: 150px; /* 小屏幕按钮宽度仍为 150px */
-    height: 45px; /* 小屏幕按钮高度仍为 45px */
-    margin-bottom: 10px; /* 按钮间距 */
-    padding: 12px 24px; /* 增加内边距 */
-  }
-
-  .button-group {
-    flex-direction: column; /* 垂直排列 */
-    align-items: center;
-  }
+.language-toggle-wrapper {
+  text-align: right;
+  margin: 10px 0 40px;
+}
+
+.text-link-wrapper {
+  text-align: center;
+  margin-left: -295px;
+  margin-top: 120px;
+}
+
+:deep(.el-input) {
+  max-width: 560px;
+  width: 100%;
 }
 </style>

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio