Эх сурвалжийг харах

新增资源文件、重构菜单结构、优化镉污染预测模块、添加新页面

qw 1 долоо хоног өмнө
parent
commit
2f6ac1634b
77 өөрчлөгдсөн 14794 нэмэгдсэн , 2269 устгасан
  1. 5 12
      components.d.ts
  2. 6 6
      index.html
  3. 1 1
      myenv/Lib/site-packages/click/_compat.py
  4. 1 1
      myenv/Lib/site-packages/pip/_vendor/urllib3/poolmanager.py
  5. 3779 843
      package-lock.json
  6. 7 3
      package.json
  7. 12 0
      public/data/韶关市河流水系图.geojson
  8. BIN
      public/favicon.ico
  9. BIN
      public/图1.png
  10. BIN
      public/图片2.png
  11. BIN
      public/图片3.png
  12. BIN
      public/图片4.jpg
  13. BIN
      public/图片5.jpg
  14. BIN
      public/图片6.jpg
  15. 1 0
      src/App.vue
  16. BIN
      src/assets/bg/agricultural_input.png
  17. BIN
      src/assets/bg/atmospheric_deposition.png
  18. BIN
      src/assets/bg/irrigation.jpg
  19. BIN
      src/assets/bg/rain_removal.png
  20. BIN
      src/assets/bg/straw-removal.png
  21. BIN
      src/assets/bg/subsurface-leakage.jpg
  22. BIN
      src/assets/bg/surface-runoff.jpg
  23. BIN
      src/assets/city-bg.jpg
  24. BIN
      src/assets/favicon.ico
  25. BIN
      src/assets/header-bg.jpg
  26. BIN
      src/assets/login-bg.png
  27. 5 1
      src/assets/logo.svg
  28. BIN
      src/assets/背景图.jpg
  29. 120 230
      src/components/layout/AppAside.vue
  30. 131 103
      src/components/layout/AppAsideForTab2.vue
  31. 621 213
      src/components/layout/AppLayout.vue
  32. 363 0
      src/components/layout/menuItems.ts
  33. 179 0
      src/components/layout/menuItems2.ts
  34. 205 61
      src/router/index.ts
  35. 0 0
      src/views/Admin/selectCityAndCounty.vue
  36. 283 0
      src/views/User/HmOutFlux/agriInput/prodInputFlux.vue
  37. 3 1
      src/views/User/HmOutFlux/agriInput/samplingDesc2.vue
  38. 3 1
      src/views/User/HmOutFlux/atmosDeposition/airInputFlux.vue
  39. 559 0
      src/views/User/HmOutFlux/atmosDeposition/airSampleData.vue
  40. 479 0
      src/views/User/HmOutFlux/atmosDeposition/atmCompanytencentMap.vue
  41. 218 0
      src/views/User/HmOutFlux/atmosDeposition/atmcompanyline.vue
  42. 94 0
      src/views/User/HmOutFlux/atmosDeposition/heavyMetalEnterprise.vue
  43. 3 1
      src/views/User/HmOutFlux/atmosDeposition/samplingDesc3.vue
  44. 425 0
      src/views/User/HmOutFlux/irrigationWater/crossSectionSampleData.vue
  45. 275 0
      src/views/User/HmOutFlux/irrigationWater/irriWaterInputFlux.vue
  46. 99 0
      src/views/User/HmOutFlux/irrigationWater/irriWaterSampleData.vue
  47. 87 0
      src/views/User/HmOutFlux/irrigationWater/riverwaterassay.vue
  48. 75 0
      src/views/User/HmOutFlux/irrigationWater/samplingMethodDevice1.vue
  49. 1045 0
      src/views/User/HmOutFlux/irrigationWater/tencentMapView.vue
  50. 222 0
      src/views/User/HmOutFlux/irrigationWater/waterassaydata1.vue
  51. 290 0
      src/views/User/HmOutFlux/irrigationWater/waterassaydata2.vue
  52. 197 0
      src/views/User/HmOutFlux/irrigationWater/waterassaydata3.vue
  53. 340 0
      src/views/User/HmOutFlux/irrigationWater/waterassaydata4.vue
  54. 221 0
      src/views/User/HmOutFlux/irrigationWater/waterdataline.vue
  55. 741 0
      src/views/User/cadmiumPrediction/CropCadmiumPrediction.vue
  56. 665 85
      src/views/User/cadmiumPrediction/EffectiveCadmiumPrediction.vue
  57. 363 78
      src/views/User/cadmiumPrediction/TotalCadmiumPrediction.vue
  58. 142 0
      src/views/User/cadmiumPrediction/currentYearConcentration.vue
  59. 126 0
      src/views/User/cadmiumPrediction/netFlux.vue
  60. 134 0
      src/views/User/cadmiumPrediction/totalInputFlux.vue
  61. 147 0
      src/views/User/cadmiumPrediction/totalOutputFlux.vue
  62. 0 23
      src/views/User/heavyMetalFluxCalculation/inputFluxCalculation/agriculturalProductInput.vue
  63. 0 197
      src/views/User/heavyMetalFluxCalculation/inputFluxCalculation/irrigationWater.vue
  64. 238 0
      src/views/User/hmInFlux/grainRemoval/grainRemovalInputFlux.vue
  65. 4 2
      src/views/User/hmInFlux/grainRemoval/samplingDesc1.vue
  66. 25 0
      src/views/User/hmInFlux/strawRemoval/samplingDesc2.vue
  67. 258 0
      src/views/User/hmInFlux/strawRemoval/strawRemovalInputFlux.vue
  68. 25 0
      src/views/User/hmInFlux/subsurfaceLeakage/samplingDesc3.vue
  69. 118 0
      src/views/User/hmInFlux/subsurfaceLeakage/subsurfaceLeakageInputFlux.vue
  70. 25 0
      src/views/User/hmInFlux/surfaceRunoff/samplingDesc4.vue
  71. 119 0
      src/views/User/hmInFlux/surfaceRunoff/surfaceRunoffInputFlux.vue
  72. 295 123
      src/views/User/selectCityAndCounty.vue
  73. 325 227
      src/views/login/loginView.vue
  74. 571 57
      src/views/menu/tencentMapView.vue
  75. 46 0
      vitest.config.ts.timestamp-1753017273303-815a308c407d1.mjs
  76. 46 0
      vitest.config.ts.timestamp-1753017288268-2bb6d1b228918.mjs
  77. 27 0
      韶关市河流水系图.qmd

+ 5 - 12
components.d.ts

@@ -11,11 +11,12 @@ declare module 'vue' {
     AppAsideForTab2: typeof import('./src/components/layout/AppAsideForTab2.vue')['default']
     AppHeader: typeof import('./src/components/layout/AppHeader.vue')['default']
     AppLayout: typeof import('./src/components/layout/AppLayout.vue')['default']
-    ElAlert: typeof import('element-plus/es')['ElAlert']
     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']
     ElContainer: typeof import('element-plus/es')['ElContainer']
     ElDialog: typeof import('element-plus/es')['ElDialog']
@@ -26,26 +27,22 @@ declare module 'vue' {
     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']
-    ElMenuItemGroup: typeof import('element-plus/es')['ElMenuItemGroup']
     ElOption: typeof import('element-plus/es')['ElOption']
-    ElPagination: typeof import('element-plus/es')['ElPagination']
-    ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
-    ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
     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']
     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']
-    ElTooltip: typeof import('element-plus/es')['ElTooltip']
-    ElUpload: typeof import('element-plus/es')['ElUpload']
+    ElTag: typeof import('element-plus/es')['ElTag']
     HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
     IconCommunity: typeof import('./src/components/icons/IconCommunity.vue')['default']
     IconDocumentation: typeof import('./src/components/icons/IconDocumentation.vue')['default']
@@ -55,11 +52,7 @@ declare module 'vue' {
     PaginationComponent: typeof import('./src/components/PaginationComponent.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
-    TabsComponent: typeof import('./src/components/layout/TabsComponent.vue')['default']
     TheWelcome: typeof import('./src/components/TheWelcome.vue')['default']
     WelcomeItem: typeof import('./src/components/WelcomeItem.vue')['default']
   }
-  export interface ComponentCustomProperties {
-    vLoading: typeof import('element-plus/es')['ElLoadingDirective']
-  }
 }

+ 6 - 6
index.html

@@ -1,13 +1,13 @@
 <!DOCTYPE html>
-<html lang="">
+<html lang="zh">
   <head>
-    <meta charset="UTF-8">
-    <link rel="icon" href="/favicon.ico">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>Vite App</title>
+    <meta charset="UTF-8" />
+    <link rel="icon" href="/favicon.ico" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>区域土壤重金属评估</title>
   </head>
   <body>
     <div id="app"></div>
     <script type="module" src="/src/main.ts"></script>
   </body>
-</html>
+</html>

+ 1 - 1
myenv/Lib/site-packages/click/_compat.py

@@ -530,7 +530,7 @@ if sys.platform.startswith("win") and WIN:
         if cached is not None:
             return cached
 
-        import colorama
+        import colorama # type: ignore
 
         strip = should_strip_ansi(stream, color)
         ansi_wrapper = colorama.AnsiToWin32(stream, strip=strip)

+ 1 - 1
myenv/Lib/site-packages/pip/_vendor/urllib3/poolmanager.py

@@ -14,7 +14,7 @@ from .exceptions import (
     URLSchemeUnknown,
 )
 from .packages import six
-from .packages.six.moves.urllib.parse import urljoin
+from .packages.six.moves.urllib.parse import urljoin # type: ignore
 from .request import RequestMethods
 from .util.proxy import connection_requires_http_tunnel
 from .util.retry import Retry

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 3779 - 843
package-lock.json


+ 7 - 3
package.json

@@ -13,10 +13,14 @@
   },
   "dependencies": {
     "@element-plus/icons-vue": "^2.3.1",
+    "@turf/turf": "^7.2.0",
+    "@types/d3": "^7.4.3",
     "@wangeditor/editor": "^5.1.23",
     "@wangeditor/editor-for-vue": "^5.1.12",
     "axios": "^1.7.9",
     "chart.js": "^4.4.9",
+    "coordtransform": "^2.1.2",
+    "d3": "^7.9.0",
     "dom-to-image": "^2.6.0",
     "echarts": "^5.6.0",
     "echarts-gl": "^2.0.9",
@@ -36,10 +40,10 @@
   },
   "devDependencies": {
     "@iconify-json/ep": "^1.2.2",
-    "@tsconfig/node22": "^22.0.0",
+    "@tsconfig/node22": "^22.0.2",
     "@types/jsdom": "^21.1.7",
     "@types/leaflet": "^1.9.16",
-    "@types/node": "^22.13.8",
+    "@types/node": "^22.15.31",
     "@types/vue": "^2.0.0",
     "@vitejs/plugin-vue": "^5.2.1",
     "@vue/test-utils": "^2.4.6",
@@ -50,7 +54,7 @@
     "unplugin-auto-import": "^19.0.0",
     "unplugin-icons": "^22.0.0",
     "unplugin-vue-components": "^28.0.0",
-    "vite": "^6.0.5",
+    "vite": "^6.3.5",
     "vite-plugin-vue-devtools": "^7.6.8",
     "vitest": "^2.1.9",
     "vue-tsc": "^2.1.10"

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 12 - 0
public/data/韶关市河流水系图.geojson


BIN
public/favicon.ico


BIN
public/图1.png


BIN
public/图片2.png


BIN
public/图片3.png


BIN
public/图片4.jpg


BIN
public/图片5.jpg


BIN
public/图片6.jpg


+ 1 - 0
src/App.vue

@@ -1,6 +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";
 request({
   url: '/table',
   method: 'get'

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


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 5 - 1
src/assets/logo.svg


BIN
src/assets/背景图.jpg


+ 120 - 230
src/components/layout/AppAside.vue

@@ -5,290 +5,180 @@
       router
       unique-opened
       :default-active="activeMenuItem.index"
+      class="professional-menu"
     >
-      <!-- 根据 activeTab 动态渲染菜单项 -->
-      <template v-if="activeTab === 'shuJuKanBan'">
-        <el-menu-item
-          index="/shuJuKanBan"
-          @click="handleMenuClick('/shuJuKanBan')"
-          >数据看板</el-menu-item
-        >
-      </template>
-
-      <template v-else-if="activeTab === 'introduction'">
-        <el-menu-item index="/SoilPro" @click="handleMenuClick('/SoilPro')"
-          >软件简介</el-menu-item
-        >
-        <el-menu-item index="/Overview" @click="handleMenuClick('/Overview')"
-          >项目简介</el-menu-item
-        >
-        <el-menu-item
-          index="/ResearchFindings"
-          @click="handleMenuClick('/ResearchFindings')"
-          >研究成果</el-menu-item
-        >
-        <el-menu-item index="/Unit" @click="handleMenuClick('/Unit')"
-          >团队信息</el-menu-item
-        >
-      </template>
-
-      <template v-else-if="activeTab === 'heavyMetalFluxCalculation'">
-        <el-sub-menu index="1">
+      <template v-for="item in filteredMenuItems" :key="item.index">
+        <el-sub-menu v-if="item.children" :index="item.index">
           <template #title>
-            <span>输入通量计算</span>
+            <el-icon><component :is="item.icon" /></el-icon>
+            <span>{{ item.label }}</span>
           </template>
           <el-menu-item
-            index="/irrigationWater"
-            @click="handleMenuClick('/irrigationWater')"
-            >灌溉水</el-menu-item
-          >
-          <el-menu-item
-            index="/agriculturalProductInput"
-            @click="handleMenuClick('/agriculturalProductInput')"
-            >农产品投入</el-menu-item
-          >
-          <el-menu-item
-            index="/atmosphericDryWetDeposition"
-            @click="handleMenuClick('/atmosphericDryWetDeposition')"
-            >大气干湿沉降</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-sub-menu index="2">
-          <template #title>
-            <span>输出通量计算</span>
-          </template>
-          <el-menu-item
-            index="/surfaceRunoff"
-            @click="handleMenuClick('/surfaceRunoff')"
-            >地表径流</el-menu-item
-          >
-          <el-menu-item
-            index="/cropRemoval"
-            @click="handleMenuClick('/cropRemoval')"
-            >农作物移除</el-menu-item
-          >
-          <el-menu-item
-            index="/subsurfaceFlow"
-            @click="handleMenuClick('/subsurfaceFlow')"
-            >地下渗流</el-menu-item
-          >
-        </el-sub-menu>
-      </template>
-
-      <template v-else-if="activeTab === 'mapView'">
-        <el-menu-item index="/mapView" @click="handleMenuClick('/mapView')"
-          >地图展示</el-menu-item
-        >
-      </template>
-
-      <template v-else-if="activeTab === 'cadmiumPrediction'">
-        <el-menu-item
-          index="/TotalCadmiumPrediction"
-          @click="handleMenuClick('/TotalCadmiumPrediction')"
-          >土壤镉的总含量预测</el-menu-item
-        >
-        <el-menu-item
-          index="/EffectiveCadmiumPrediction"
-          @click="handleMenuClick('/EffectiveCadmiumPrediction')"
-          >土壤镉有效态含量预测</el-menu-item
-        >
-      </template>
-
-      <template v-else-if="activeTab === 'cropRiskAssessment'">
-        <el-menu-item
-          index="/cropRiskAssessment"
-          @click="handleMenuClick('/cropRiskAssessment')"
-          >水稻镉污染风险</el-menu-item
-        >
-      </template>
-
-      <template v-else-if="activeTab === 'farmlandQualityAssessment'">
-        <el-menu-item
-          index="/farmlandQualityAssessment"
-          @click="handleMenuClick('/farmlandQualityAssessment')"
-          >韶关</el-menu-item
-        >
-      </template>
-
-      <template v-else-if="activeTab === 'soilAcidificationPrediction'">
-        <el-menu-item
-          index="/Calculation"
-          @click="handleMenuClick('/Calculation')"
-          >土壤反酸预测</el-menu-item
-        >
-        <el-menu-item
-          index="/AcidNeutralizationModel"
-          @click="handleMenuClick('/AcidNeutralizationModel')"
-          >土壤降酸预测</el-menu-item
-        >
-      </template>
-
-      <template v-else-if="activeTab === 'scenarioSimulation'">
-        <el-menu-item
-          index="/TraditionalFarmingRisk"
-          @click="handleMenuClick('/TraditionalFarmingRisk')"
-          >传统耕种习惯风险趋势</el-menu-item
-        >
-        <el-menu-item
-          index="/HeavyMetalCadmiumControl"
-          @click="handleMenuClick('/HeavyMetalCadmiumControl')"
-          >重金属镉污染治理</el-menu-item
-        >
-        <el-menu-item
-          index="/SoilAcidificationControl"
-          @click="handleMenuClick('/SoilAcidificationControl')"
-          >土壤酸化治理</el-menu-item
-        >
-      </template>
-
-      <template v-else-if="activeTab === 'dataStatistics'">
-        <el-menu-item
-          index="/DetectionStatistics"
-          @click="handleMenuClick('/DetectionStatistics')"
-          >检测信息统计</el-menu-item
-        >
         <el-menu-item
-          index="/FarmlandPollutionStatistics"
-          @click="handleMenuClick('/FarmlandPollutionStatistics')"
-          >耕地污染信息统计</el-menu-item
+          v-else
+          :index="item.index"
+          @click="handleMenuClick(item.index)"
         >
-        <el-menu-item
-          index="/PlantingRiskStatistics"
-          @click="handleMenuClick('/PlantingRiskStatistics')"
-          >种植风险信息统计</el-menu-item
-        >
-      </template>
-
-      <template v-else>
-        <el-menu-item disabled>未知的 Tab:{{ activeTab }}</el-menu-item>
+          <el-icon><component :is="item.icon" /></el-icon>
+          <span>{{ item.label }}</span>
+        </el-menu-item>
       </template>
     </el-menu>
   </el-scrollbar>
 </template>
 
 <script setup lang="ts">
-import { inject, reactive, watch, toRefs } from "vue";
-import { ElMessage } from "element-plus";
-import { useRouter } from "vue-router";
+import { reactive, computed, inject, toRefs, watch } from 'vue';
+import { useRouter } from 'vue-router';
+import { ElMessage } from 'element-plus';
+import { menuItems } from './menuItems'; // 建议你将 menuItems 独立维护在该文件中
 
 const props = defineProps({
   activeTab: {
     type: String,
     required: true,
-    default: "introduction",
+    default: 'introduction'
   },
   showTabs: {
     type: Boolean,
     required: false,
-    default: true,
-  },
+    default: true
+  }
 });
 
 const { activeTab } = toRefs(props);
-const isCollapse = inject("isCollapse", false);
-
-const activeMenuItem = reactive({ index: "" });
+const isCollapse = inject('isCollapse', false);
+const userPermissions = inject('userPermissions', [] as string[]);
 
 const router = useRouter();
+const activeMenuItem = reactive({ index: '' });
+
+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)
+        )
+      };
+    })
+);
 
-function logAction(action: string) {
-  console.log(`[AppAside] ${action}`);
-}
-
-function showError(message: string) {
-  ElMessage.error(message);
-}
-
-async function handleMenuClick(index: string) {
+function handleMenuClick(index: string) {
   try {
-    logAction(`Menu item clicked: ${index}`);
     if (router.currentRoute.value.path !== index) {
-      // 避免重复导航
-      await router.push(index);
+      router.push(index);
       activeMenuItem.index = index;
     }
   } catch (error) {
-    console.error(`[AppAside] Error navigating to ${index}:`, error);
-    if ((error as any).code === "ERR_NETWORK") {
-      showError("网络错误,请检查您的网络连接。");
-    } else {
-      showError("导航失败,请检查您的网络连接或联系管理员。");
-    }
-  }
-}
-
-// 统一设置默认激活菜单项的函数
-function getDefaultMenuIndex(tab: string) {
-  switch (tab) {
-    case "shuJuKanBan":
-      return "/shuJuKanBan";
-    case "introduction":
-      return "/SoilPro";
-    case "heavyMetalFluxCalculation":
-      return "/inputFluxCalculation/irrigationWater";
-    case "mapView":
-      return "/mapView";
-    case "cadmiumPrediction":
-      return "/TotalCadmiumPrediction";
-    case "cropRiskAssessment":
-      return "/cropRiskAssessment";
-    case "farmlandQualityAssessment":
-      return "/farmlandQualityAssessment";
-    case "soilAcidificationPrediction":
-      return "/Calculation";
-    case "scenarioSimulation":
-      return "/TraditionalFarmingRisk";
-    case "dataStatistics":
-      return "/DetectionStatistics";
-    case "selectCityAndCounty":
-      return "";
-    default:
-      return "";
+    ElMessage.error('导航失败,请检查网络或联系管理员');
   }
 }
 
 watch(
   activeTab,
-  (newVal) => {
-    console.log(`[AppAside] activeTab changed to: ${newVal}`);
-    activeMenuItem.index = getDefaultMenuIndex(newVal);
-  },
-  { immediate: true }
-);
-
-watch(
-  () => isCollapse,
-  (newVal) => {
-    console.log(`[AppAside] isCollapse changed to: ${newVal}`);
+  newVal => {
+    const current = filteredMenuItems.value?.[0];
+    activeMenuItem.index = current?.children?.[0]?.index || current?.index || '';
   },
   { immediate: true }
 );
 </script>
 
 <style scoped>
-.el-menu {
-  background-color: #ffffff;
+: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;
+  padding-top: 12px;
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  min-height: 100%;
+}
+
+: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 {
+:deep(.el-sub-menu .el-menu-item) {
+  background-color: rgba(252, 234, 183, 0.3) !important;
+}
+
+:deep(.el-sub-menu .el-menu-item:not(:last-child)) {
+  margin-bottom: 0;
+}
+
+:deep(.el-sub-menu__title) {
+  font-size: 18px;
+  font-weight: 500;
   color: #000000;
-  font-size: 14px;
-  padding: 10px 20px;
   border-radius: 6px;
-  transition: background-color 0.3s ease, color 0.3s ease;
+  padding: 12px 16px !important;
+  transition: all 0.2s ease;
+}
+
+/* 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);
 }
 
-.el-menu-item:hover {
-  background-color: #f3f4f6;
-  color: #2563eb;
+/* 子菜单标题样式 + 图标右移 */
+:deep(.el-sub-menu__title) {
+  display: flex;
+  align-items: center;
+  padding-left: 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__icon-arrow) {
+  margin-left: auto;
+  margin-right: -160px;
+  transition: transform 0.3s ease;
 }
 </style>

+ 131 - 103
src/components/layout/AppAsideForTab2.vue

@@ -1,39 +1,34 @@
 <template>
   <el-scrollbar v-if="showTabs">
-    <!-- 菜单 -->
-    <el-menu :collapse="isCollapse" router unique-opened :default-active="activeMenuItem.index">
-      <!-- 根据 activeTab 动态渲染菜单项 -->
-      <template v-if="activeTab === 'parameterConfig'">
-        <el-menu-item index="/ModelSelection" @click="handleMenuClick('/ModelSelection')">模型选择</el-menu-item>
-        <el-menu-item index="/thres" @click="handleMenuClick('/thres')">阈值选择</el-menu-item>
-        <el-menu-item index="/ModelTrain" @click="handleMenuClick('/ModelTrain')">模型训练</el-menu-item>
-      </template>
-      <template v-else-if="activeTab === 'dataManagement'">
-        <el-menu-item index="/Visualizatio" @click="handleMenuClick('/Visualizatio')">降酸数据管理</el-menu-item>
-        <el-menu-item index="/Visualization" @click="handleMenuClick('/Visualization')">反酸数据管理</el-menu-item>
-        <el-menu-item index="/AdminRegionData" @click="handleMenuClick('/AdminRegionData')">行政区域数据</el-menu-item>
-        <el-menu-item index="/SoilAssessmentUnitData" @click="handleMenuClick('/SoilAssessmentUnitData')">土壤评估单元格数据</el-menu-item>
-        <el-menu-item index="/SoilHeavyMetalData" @click="handleMenuClick('/SoilHeavyMetalData')">土壤重金属采集数据</el-menu-item>
-        <el-menu-item index="/CropHeavyMetalData" @click="handleMenuClick('/CropHeavyMetalData')">农作物重金属采集样数据</el-menu-item>
-        <el-menu-item index="/LandUseTypeData" @click="handleMenuClick('/LandUseTypeData')">用地类型数据</el-menu-item>
-        <el-menu-item index="/SoilAcidificationData" @click="handleMenuClick('/SoilAcidificationData')">土壤酸化采样数据</el-menu-item>
-        <el-menu-item index="/ClimateInfoData" @click="handleMenuClick('/ClimateInfoData')">气候信息数据</el-menu-item>
-        <el-menu-item index="/GeographicEnvInfoData" @click="handleMenuClick('/GeographicEnvInfoData')">地理环境信息</el-menu-item>
-      </template>
-      <template v-else-if="activeTab === 'infoManagement'">
-        <el-menu-item index="/IntroductionUpdate" @click="handleMenuClick('/IntroductionUpdate')">介绍信息管理</el-menu-item>
-      </template>
-      <template v-else-if="activeTab === 'modelManagement'">
-        <el-menu-item index="/CadmiumPredictionModel" @click="handleMenuClick('/CadmiumPredictionModel')">土壤镉含量预测模型</el-menu-item>
-        <el-menu-item index="/EffectiveCadmiumModel" @click="handleMenuClick('/EffectiveCadmiumModel')">土壤有效态镉预测模型</el-menu-item>
-        <el-menu-item index="/RiceRiskModel" @click="handleMenuClick('/RiceRiskModel')">水稻镉污染风险模型</el-menu-item>
-        <el-menu-item index="/AcidReductionModel" @click="handleMenuClick('/AcidReductionModel')">反酸及降酸模型</el-menu-item>
-        <el-menu-item index="/WheatRiskModel" @click="handleMenuClick('/WheatRiskModel')">小麦镉污染风险模型</el-menu-item>
-        <el-menu-item index="/VegetableRiskModel" @click="handleMenuClick('/VegetableRiskModel')">蔬菜镉污染风险模型</el-menu-item>
-      </template>
-      <template v-else-if="activeTab === 'userManagement'">
-        <el-menu-item index="/UserRegistration" @click="handleMenuClick('/UserRegistration')">普通用户</el-menu-item>
-        <el-menu-item index="/UserManagement" @click="handleMenuClick('/UserManagement')">用户信息</el-menu-item>
+    <el-menu
+      :collapse="isCollapse"
+      router
+      unique-opened
+      :default-active="activeMenuItem.index"
+      :default-openeds="openKeys"
+      @select="handleMenuClick"
+    >
+      <template v-if="menuList.length">
+        <template v-for="item in menuList" :key="item.index">
+          <el-sub-menu v-if="item.children && item.children.length" :index="item.index">
+            <template #title>
+              <el-icon v-if="item.icon"><component :is="item.icon" /></el-icon>
+              <span>{{ item.label }}</span>
+            </template>
+            <el-menu-item
+              v-for="child in item.children"
+              :key="child.index"
+              :index="child.index"
+            >
+              <el-icon v-if="child.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 v-if="item.icon"><component :is="item.icon" /></el-icon>
+            <span>{{ item.label }}</span>
+          </el-menu-item>
+        </template>
       </template>
       <template v-else>
         <el-menu-item disabled>未知的 Tab:{{ activeTab }}</el-menu-item>
@@ -43,110 +38,143 @@
 </template>
 
 <script setup lang="ts">
-import { inject, reactive, watch, toRefs } from "vue";
+import { inject, reactive, watch, toRefs, computed } from "vue";
+import { useRouter, useRoute, type RouteLocationAsPathGeneric, type RouteLocationAsRelativeGeneric } from "vue-router";
 import { ElMessage } from "element-plus";
-import { useRouter } from "vue-router";
+import { tabMenuMap } from "./menuItems2";
 
-// 接收来自父组件的 activeTab 和 showTabs 属性
 const props = defineProps({
   activeTab: {
-    type: String, // 确保 activeTab 是字符串
+    type: String,
     required: true,
-    default: "introduction", // 默认值为 "introduction"
+    default: "introduction",
   },
   showTabs: {
-    type: Boolean, // 确保 showTabs 是布尔值
+    type: Boolean,
     required: true,
-    default: true, // 默认值为 true
+    default: true,
   },
 });
 
-// 将 activeTab 转为响应式引用
 const { activeTab } = toRefs(props);
+const isCollapse = inject("isCollapse", false);
+const router = useRouter();
+const route = useRoute();
 
-// 注入 isCollapse 值,用于控制侧边栏的缩放
-const isCollapse = inject("isCollapse", false); // 默认值为 false,确保注入失败时有默认值
-
-// 新增用于高亮的状态
-const activeMenuItem = reactive({ index: "" });
-
-// 日志记录函数
-function logAction(action: string) {
-  console.log(`[AppAsideForTab2] ${action}`);
-}
-
-// 错误提示函数
-function showError(message: string) {
-  ElMessage.error(message);
-}
+const activeMenuItem = reactive({ index: route.path });
+const openKeys = reactive<string[]>([]);
 
-const router = useRouter();
+const menuList = computed(() => {
+  return tabMenuMap[activeTab.value] || [];
+});
 
-// 在菜单项点击时记录日志并导航
-async function handleMenuClick(index: string) {
-  try {
-    logAction(`Menu item clicked: ${index}`);
-    await router.push(index); // 确保导航到对应的路由
-    activeMenuItem.index = index; // 更新高亮状态
-  } catch (error) {
-    console.error(`[AppAsideForTab2] Error navigating to ${index}:`, error);
-    showError("导航失败,请检查您的网络连接或联系管理员。");
+// 查找当前路由对应的父级菜单index,用于默认展开子菜单
+function findOpenKeys(menuItems: any[], currentPath: string, parents: string[] = []): string[] {
+  for (const item of menuItems) {
+    if (item.index === currentPath) {
+      return parents;
+    } else if (item.children) {
+      const found = findOpenKeys(item.children, currentPath, [...parents, item.index]);
+      if (found.length) return found;
+    }
   }
+  return [];
 }
 
-// 调试日志:记录 activeTab 和 isCollapse 的值
 watch(
-  activeTab, // 使用响应式引用
-  (newVal) => {
-    console.log(`[AppAside] activeTab changed to: ${newVal}`);
-    // 默认高亮第一个菜单项
-    if (newVal === "parameterConfig") {
-      activeMenuItem.index = "/ModelSelection";
-    } else if (newVal === "dataManagement") {
-      activeMenuItem.index = "/Visualizatio";
-    } else if (newVal === "infoManagement") {
-      activeMenuItem.index = "/IntroductionUpdate";
-    } else if (newVal === "modelManagement") {
-      activeMenuItem.index = "/CadmiumPredictionModel";
-    } else if (newVal === "userManagement") {
-      activeMenuItem.index = "/UserRegistration";
-    }
+  () => route.path,
+  (newPath) => {
+    activeMenuItem.index = newPath;
+    openKeys.splice(0, openKeys.length, ...findOpenKeys(menuList.value, newPath));
   },
   { immediate: true }
 );
 
 watch(
-  () => isCollapse,
+  activeTab,
   (newVal) => {
-    console.log(`[AppAside] isCollapse changed to: ${newVal}`);
+    const first = tabMenuMap[newVal]?.[0];
+    if (first) activeMenuItem.index = first.index;
+    else activeMenuItem.index = "";
+    openKeys.splice(0, openKeys.length, ...findOpenKeys(menuList.value, activeMenuItem.index));
   },
   { immediate: true }
 );
+
+async function handleMenuClick(index: string | RouteLocationAsRelativeGeneric | RouteLocationAsPathGeneric) {
+  try {
+    if (index === route.path) return;
+    await router.push(index);
+    activeMenuItem.index = index as string;
+  } catch (error) {
+    console.error(`[AppAsideForTab2] 路由跳转失败: ${index}`, error);
+    ElMessage.error("导航失败,请检查网络连接或联系管理员。");
+  }
+}
 </script>
 
 <style scoped>
-.el-menu {
-  background-color: #1f2937; /* 深色背景 */
-  border-right: none; /* 移除边框 */
+/* 给 el-menu 组件设置背景渐变 */
+:deep(.el-menu) {
+  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 !important;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+
+/* 菜单项和子菜单标题 */
+: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;
+}
+
+/* 子菜单中菜单项背景色 */
+: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;
 }
 
-.el-menu-item {
-  color: #d1d5db; /* 浅灰色字体 */
-  font-size: 14px; /* 调整字体大小 */
-  padding: 10px 20px; /* 增加内边距 */
-  border-radius: 6px; /* 添加圆角 */
-  transition: background-color 0.3s ease, color 0.3s ease; /* 平滑过渡 */
+/* 激活高亮 */
+: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;
 }
 
-.el-menu-item:hover {
-  background-color: #374151; /* 悬停时背景色 */
-  color: #60a5fa; /* 悬停时字体颜色 */
+/* 子菜单标题样式 + 图标右移 */
+:deep(.el-sub-menu__title) {
+  display: flex;
+  align-items: center;
+  padding-left: 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__icon-arrow) {
+  margin-left: auto;
+  margin-right: -160px;
+  transition: transform 0.3s ease;
 }
-</style>
+</style>

+ 621 - 213
src/components/layout/AppLayout.vue

@@ -1,3 +1,116 @@
+<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 { useRouter, useRoute } from "vue-router";
@@ -5,102 +118,289 @@ import { useTokenStore } from "@/stores/mytoken";
 import { ElMessageBox, ElMessage } from "element-plus";
 import { logout } from "@/API/users";
 
+// 使用更可靠的图片导入方式
+function getImageUrl(name: string) {
+  try {
+    return new URL(`../../assets/bg/${name}`, import.meta.url).href;
+  } catch (error) {
+    console.error("加载背景图失败:", error);
+    return "";
+  }
+}
+
 const router = useRouter();
 const route = useRoute();
 const tokenStore = useTokenStore();
+const currentBgImage = ref("");
 
-// 新增:判断当前路由是否全屏模式(无头部和侧边栏)
+// 是否为全屏页面
 const isFullScreen = computed(() => route.meta.fullScreen === true);
 
-// 根据用户类型动态定义 Tab 数据
+// 判断是否是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(() => {
   if (tokenStore.token.loginType === "admin") {
     return [
-      { name: "parameterConfig", label: "参数配置", routes: ["/ModelSelection", "/thres", "/ModelTrain"] },
-      { name: "dataManagement", label: "数据管理", routes: ["/Visualization", "/Visualizatio", "/AdminRegionData", "/SoilAssessmentUnitData", "/SoilHeavyMetalData", "/CropHeavyMetalData", "/LandUseTypeData", "/SoilAcidificationData", "/ClimateInfoData", "/GeographicEnvInfoData"] },
-      { name: "infoManagement", label: "信息管理", routes: ["/IntroductionUpdate"] },
-      { name: "modelManagement", label: "模型管理及配置", routes: ["/Admin/CadmiumPredictionModel", "/Admin/EffectiveCadmiumModel", "/Admin/RiceRiskModel", "/Admin/AcidReductionModel", "/Admin/WheatRiskModel", "/Admin/VegetableRiskModel"] },
-      { name: "userManagement", label: "用户管理", routes: ["/Admin/UserRegistration", "/Admin/UserManagement"] },
+      {
+        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: "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"],
+      },
     ];
   } else {
     return [
-      { name: "shuJuKanBan", label: "数据看板", routes: ["/shuJuKanBan"] },
-      { name: "introduction", label: "软件简介", routes: ["/SoilPro", "/Overview", "/ResearchFindings", "/Unit"] },
-      { name: "heavyMetalFluxCalculation", label: "重金属输入输出通量计算", routes: ["/irrigationWater", "/agriculturalProductInput", "/atmosphericDryWetDeposition", "/surfaceRunoff", "/cropRemoval", "/subsurfaceFlow"] },
-      { name: "mapView", label: "地图展示", routes: ["/mapView"] },
-      { name: "cadmiumPrediction", label: "土壤镉含量预测", routes: ["/TotalCadmiumPrediction", "/EffectiveCadmiumPrediction"] },
-      { name: "cropRiskAssessment", label: "作物安全生产风险阈值评估", routes: ["/cropRiskAssessment"] },
-      { name: "farmlandQualityAssessment", label: "耕地质量评估", routes: ["/farmlandQualityAssessment"] },
-      { name: "soilAcidificationPrediction", label: "土壤酸化预测", routes: ["/Calculation", "/AcidNeutralizationModel"] },
-      { name: "scenarioSimulation", label: "情景模拟", routes: ["/TraditionalFarmingRisk", "/HeavyMetalCadmiumControl", "/SoilAcidificationControl"] },
-      { name: "dataStatistics", label: "数据统计报表", routes: ["/DetectionStatistics", "/FarmlandPollutionStatistics", "/PlantingRiskStatistics"] },
-    ];
+      {
+        name: "shuJuKanBan",
+        label: "数据看板",
+        icon: "el-icon-data-analysis",
+        routes: ["/shuJuKanBan"],
+      },
+      {
+        name: "introduction",
+        label: "软件简介",
+        icon: "el-icon-info-filled",
+        routes: ["/SoilPro", "/Overview", "/ResearchFindings", "/Unit"],
+      },
+      {
+        name: "HmOutFlux",
+        label: "重金属输入通量",
+        icon: "el-icon-refresh",
+        routes: [
+          "/samplingMethodDevice1",
+          "/irriSampleData",
+          "/csSampleData",
+          "/irriInputFlux",
+          "/samplingDesc2",
+          "/prodInputFlux",
+          "/samplingDesc3",
+          "/airSampleData",
+          "/airInputFlux",
+        ],
+      },
+      {
+        name: "hmInFlux",
+        label: "重金属输出通量",
+        icon: "el-icon-refresh",
+        routes: [
+          "/samplingDesc1",
+          "/grainRemovalInputFlux",
+          "/samplingDesc2",
+          "/strawRemovalInputFlux",
+          "/samplingDesc3",
+          "/subsurfaceLeakageInputFlux",
+          "/samplingDesc4",
+          "/surfaceRunoffInputFlux",
+        ],
+      },
+      {
+        name: "mapView",
+        label: "地图展示",
+        icon: "el-icon-map-location",
+        routes: ["/mapView"],
+      },
+      {
+        name: "cadmiumPrediction",
+        label: "土壤污染物含量预测",
+        icon: "el-icon-c-scale-to-original",
+        routes: [
+          "/totalInputFlux",
+          "/TotalCadmiumPrediction",
+          "/totalOutputFlux",
+          "/netFlux",
+          "/currentYearConcentration",
+          "/EffectiveCadmiumPrediction",
+          "CropCadmiumPrediction",
+        ],
+      },
+      {
+        name: "cropRiskAssessment",
+        label: "作物风险评估",
+        icon: "el-icon-warning",
+        routes: ["/cropRiskAssessment"],
+      },
+      {
+        name: "farmlandQualityAssessment",
+        label: "耕地质量评估",
+        icon: "el-icon-rank",
+        routes: ["/farmlandQualityAssessment"],
+      },
+      {
+        name: "soilAcidificationPrediction",
+        label: "土壤酸化预测",
+        icon: "el-icon-magic-stick",
+        routes: ["/Calculation", "/AcidNeutralizationModel"],
+      },
+      {
+        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(tabs.value[0]?.name || ""); // 确保默认激活的是第一个 Tab
-
-// 控制是否显示 Tabs
+// 计算:当前激活 tab - 保持不变
+const activeName = ref(tabs.value[0]?.name || "");
 const showTabs = computed(() => tabs.value.length > 1);
-
-// 控制 Tabs 样式
-const tabStyle = computed(() => {
-  return tabs.value.length === 1 ? { width: "100%", justifyContent: "center" } : {};
-});
-
-// 用于控制首次是否跳转
+const tabStyle = computed(() =>
+  tabs.value.length === 1 ? { width: "100%", justifyContent: "center" } : {}
+);
 let hasNavigated = false;
 
-// 监听 activeName 的变化,加载对应数据,且首次不自动跳转
 watch(
   () => activeName.value,
   (newTab) => {
-    const tab = tabs.value.find(t => t.name === 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) {
-      console.log("跳转目标:", targetPath);
       router.push({ path: targetPath });
     }
   },
   { immediate: true }
 );
 
-// Tab 点击事件
+// 显式列出所有不需要背景图的路由路径
+const bgExcludedRoutes = computed(() => {
+  return [
+    // 列出所有不需要背景图的路由路径
+    "/shuJuKanBan",
+    "/mapView",
+    "/cropRiskAssessment",
+    "/farmlandQualityAssessment",
+    "/dataStatistics",
+    
+    // 强制"重金属输入通量"和"重金属输出通量"下的所有路由为白色背景
+    ...tabs.value
+      .filter(tab => ["HmOutFlux", "hmInFlux"].includes(tab.name))
+      .flatMap(tab => tab.routes)
+  ];
+});
+
+// 判断当前路由是否在排除列表中
+const isExcludedRoute = computed(() => bgExcludedRoutes.value.includes(route.path));
+
+// 获取当前页面的背景图
+const getBgImage = (path: string) => {
+  // 若当前路径属于排除项,不显示背景图
+  if (isExcludedRoute.value) return "";
+  
+  return routeBackgroundMap[path] || "";
+};
+
+// 当前背景图
+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(() => {
-  if (["parameterConfig", "dataManagement", "infoManagement", "modelManagement", "userManagement"].includes(activeName.value)) {
+  if (
+    ["parameterConfig", "dataManagement", "infoManagement", "modelManagement", "userManagement"]
+      .includes(activeName.value)
+  ) {
     return defineAsyncComponent(() => import("./AppAsideForTab2.vue"));
-  } else if (["introduction", "acidModel", "neutralizationModel", "mapView", "cadmiumPrediction", "cropRiskAssessment", "farmlandQualityAssessment", "soilAcidificationPrediction", "scenarioSimulation", "dataStatistics", "heavyMetalFluxCalculation"].includes(activeName.value)) {
+  } else {
     return defineAsyncComponent(() => import("./AppAside.vue"));
   }
-  return null;
 });
 
-// 控制侧边栏显示(全屏时隐藏)
+// 是否显示侧边栏
 const showAside = computed(() => {
-  return !isFullScreen.value && activeName.value !== "mapView" && activeName.value !== "shuJuKanBan" && activeName.value !== "cropRiskAssessment" && activeName.value !== "farmlandQualityAssessment"; // 数据看板不显示侧边栏
+  return (
+    !isFullScreen.value &&
+    activeName.value !== "mapView" &&
+    activeName.value !== "cropRiskAssessment" &&
+    activeName.value !== "farmlandQualityAssessment"
+  );
 });
 
-// 固定选中侧边栏选项
-const activeAsideTab = ref(activeName.value || "shuJuKanBan");
-
-// 用户信息
-const userInfo = reactive({
-  name: tokenStore.token.name || "未登录",
-});
+const activeAsideTab = ref(activeName.value || "");
+const userInfo = reactive({ name: tokenStore.token.name || "未登录" });
 
-// 处理退出逻辑
 const handleLogout = async () => {
   try {
     await ElMessageBox.confirm("确定要退出登录吗?", "提示", {
@@ -112,230 +412,338 @@ const handleLogout = async () => {
     tokenStore.clearToken();
     ElMessage.success("退出成功");
     router.push("/login");
-  } catch (error) {
+  } catch {
     ElMessage.info("已取消退出");
   }
 };
 
-// 添加全局错误处理逻辑(如果需要)
-watch(
-  () => route,
-  (newRoute) => {
-    console.log(`[AppLayout] Route changed to: ${newRoute.path}`);
-  },
-  { immediate: true }
-);
+// 内容区样式
+const mainStyle = computed(() => {
+  return {
+    padding: ["mapView", "infoManagement"].includes(activeName.value) ? "0" : "20px",
+    overflow: "hidden",
+  };
+});
 </script>
 
-<template>
-  <div class="common-layout" :class="{ 'full-screen': isFullScreen }">
-    <el-container style="height: 100vh">
-      <!-- 只在非全屏模式显示顶部导航栏 -->
-      <el-header v-if="!isFullScreen" class="header">
-        <div class="header-content">
-          <!-- 添加 logo 图片 -->
-          <div class="logo-container">
-            <img src="@/assets/logo.png" alt="Logo 1" class="logo" />
-          </div>
-          <span class="project-name">酸性土壤精准治酸智能专家系统</span>
-          <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"
-              :label="tab.label"
-              :name="tab.name"
-            />
-          </el-tabs>
-          <div v-else class="single-tab" @click="handleClick(tabs[0], $event)">
-            {{ tabs[0]?.label }}
-          </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>
-        </div>
-      </el-header>
-
-      <el-container>
-        <!-- 非全屏且满足条件时显示侧边栏 -->
-        <el-aside
-          v-if="!isFullScreen && showAside && showTabs"
-          :width="'200px'"
-          class="aside"
-        >
-          <component
-            :is="AsideComponent"
-            :activeTab="activeName"
-            :showTabs="showTabs"
-          />
-        </el-aside>
-
-        <el-container class="header-and-main" :style="isFullScreen ? { height: '100vh' } : {}">
-          <el-main
-            :style="isFullScreen ? { padding: 0 } : { padding: activeName === 'mapView' || activeName === 'infoManagement' ? '0' : '20px', backgroundColor: '#ecf0f1' }"
-          >
-            <el-scrollbar :style="isFullScreen ? { height: '100vh' } : { height: 'auto' }">
-              <RouterView />
-            </el-scrollbar>
-          </el-main>
-        </el-container>
-      </el-container>
-    </el-container>
-  </div>
-</template>
+<style>
+/* 隐藏所有滚动条 */
+*::-webkit-scrollbar {
+  display: none; /* Chrome, Safari, Opera */
+}
 
-<style scoped>
-.common-layout {
-  display: flex;
-  height: 100%;
-  font-family: 'Roboto', sans-serif;
-  background: linear-gradient(135deg, #f3f4f6, #e5e7eb);
+* {
+  -ms-overflow-style: none; /* IE and Edge */
+  scrollbar-width: none; /* Firefox */
 }
 
-/* 新增:全屏布局,移除间距,充满视图 */
-.full-screen {
+/* 整体布局容器 */
+.layout-wrapper {
+  display: flex;
+  flex-direction: column;
   height: 100vh;
   overflow: hidden;
+  position: relative;
 }
 
-.full-screen .el-main,
-.full-screen .el-scrollbar {
-  height: 100vh !important;
-  padding: 0 !important;
-  background-color: transparent !important;
+/* 全屏页面特殊处理 */
+.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 {
+/* 透明 Header */
+.layout-header {
+  height: 150px;
   display: flex;
-  justify-content: space-between;
   align-items: center;
-  padding: 15px 30px;
-  background-color: #ffffff;
-  border-bottom: 1px solid #e5e7eb;
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
-  border-radius: 10px;
+  justify-content: space-between;
+  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;
+}
+
+/* 排除背景图页面的Header样式 */
+.excluded-bg-header {
+  background-color: #ffffff !important;
+  border-bottom: 1px solid #eee !important;
 }
 
-.header-content {
+.logo-title-row {
   display: flex;
   align-items: center;
-  justify-content: space-between;
+  gap: 24px;
   width: 100%;
 }
 
-.project-name {
-  font-size: 1.2rem;
-  font-weight: bold;
-  color: #1f2937;
-  margin-right: 20px;
+.title-and-user {
+  display: flex;
+  flex-direction: column;
+  flex: 1;
 }
 
-.demo-tabs {
-  flex-grow: 1;
-  margin-right: 20px;
+/* 用户信息区域 */
+.user-info-row {
   display: flex;
   align-items: center;
-  background-color: #f9fafb;
-  border-radius: 8px;
-  padding: 5px;
-  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+  justify-content: flex-end;
+  gap: 24px;
+  background-color: transparent !important;
+  box-shadow: none !important;
+  color: #ffffff;
+  padding-top: 1px;
+  position: static;
+  z-index: 100;
 }
 
-.demo-tabs > .el-tabs__header {
-  border-bottom: none;
+/* 排除背景图页面的文字颜色 */
+.excluded-text {
+  color: #333 !important;
 }
 
-.demo-tabs > .el-tabs__item {
-  color: #374151;
-  font-size: 14px;
+/* 头像边框白色 */
+.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;
-  padding: 8px 16px;
-  border-radius: 6px;
-  transition: background-color 0.3s ease, color 0.3s ease;
+  color: #ffffff !important;
 }
 
-.demo-tabs > .el-tabs__item:hover {
-  background-color: #e5e7eb;
-  color: #2563eb;
+/* 排除背景图页面的欢迎文字颜色 */
+.excluded-text .welcome-text {
+  color: #333 !important;
 }
 
-.demo-tabs > .el-tabs__item.is-active {
-  background-color: #2563eb;
-  color: #ffffff;
-  font-weight: bold;
-  box-shadow: inset 0 0 6px rgba(37, 99, 235, 0.5);
+/* 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;
 }
 
-.single-tab {
+/* el-tabs 外层容器 */
+.demo-tabs {
+  height: 48px !important;
   display: flex;
   align-items: center;
-  justify-content: center;
   width: 100%;
-  height: 40px;
-  background-color: #2563eb;
+  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 {
+  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;
+  color: #cfd8dc;
+  border-radius: 10px;
+  transition: all 0.2s ease-in-out;
+  background-color: transparent;
+  position: relative;
+  z-index: 1;
+}
+
+/* 激活 Tab */
+.el-tabs__item.is-active {
+  background-color: #2a53ba;
   color: #ffffff;
-  font-size: 16px;
+  font-weight: 700;
+  box-shadow: 0 4px 16px rgba(26, 188, 156, 0.4);
+  z-index: 2;
+}
+
+/* 鼠标悬停 */
+.el-tabs__item:hover {
+  background-color: #455a64;
+  color: #ffffff;
+}
+
+/* 图标样式 */
+.tab-icon {
+  font-size: 24px;
+  margin-right: 4px;
+  color: inherit;
+}
+
+/* 文字样式 */
+.tab-label-text {
+  font-size: 20px;
+  color: inherit;
+  line-height: 1;
+  display: inline-block;
+}
+
+.logo {
+  height: 60px;
+}
+
+.project-name {
+  font-size: 48px;
   font-weight: bold;
-  border-radius: 8px;
-  cursor: pointer;
-  transition: background-color 0.3s ease;
-  user-select: none;
+  margin-top: 30px;
+  color: #f0f3f7;
 }
 
-.single-tab:hover {
-  background-color: #1d4ed8;
+/* 排除背景图页面的项目名称颜色 */
+.excluded-text.project-name {
+  color: #333 !important;
 }
 
-.aside {
-  padding: 0;
-  background-color: #f9fafb;
-  border-radius: 10px;
-  margin-left: 10px;
-  box-shadow: 0 0 10px rgb(32 33 36 / 8%);
+.layout-main-container {
+  flex: 1;
+  display: flex;
+  overflow: hidden;
+  min-height: 0;
+  position: relative;
+  z-index: 1;
 }
 
-.header-and-main {
-  border-radius: 10px;
-  background-color: #ffffff;
-  margin-left: 10px;
+/* 侧边栏 - 不透明 */
+.layout-aside {
+  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;
+  position: relative;
 }
 
-.el-main {
-  overflow-y: auto;
+/* 排除背景图页面的内容区域 */
+.excluded-bg-content {
+  background-color: #ffffff !important;
 }
 
-.el-scrollbar {
-  height: 100%;
+/* 可滑动内容区域 */
+.scrollable-content {
+  flex: 1;
+  overflow: auto;
+  padding: 0 20px;
+  box-sizing: border-box;
 }
 
-.logo-container {
-  display: flex;
-  align-items: center;
-  gap: 10px;
+/* 强制重置 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;
 }
 
-.logo {
-  height: 40px;
-  width: auto;
+/* 全屏页面特殊处理 */
+.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>
+</style>

+ 363 - 0
src/components/layout/menuItems.ts

@@ -0,0 +1,363 @@
+// src/config/menuItems.ts
+import {
+    Menu as MenuIcon,
+    Monitor,
+    InfoFilled,
+    DataLine,
+    Histogram,
+    Cloudy,
+    Watermelon,
+    WindPower,
+    Sunny,
+    List,
+    Location,
+    PieChart,
+    Compass,
+    Collection,
+    MagicStick,
+    HelpFilled,
+    Coin
+  } from '@element-plus/icons-vue';
+  
+  export interface MenuItem {
+    index: string;
+    label: string;
+    icon?: any;
+    tab: string;
+    permission?: string;
+    children?: MenuItem[];
+  }
+  
+  export const menuItems: MenuItem[] = [
+    {
+      index: '/shuJuKanBan',
+      label: '数据看板',
+      icon: Monitor,
+     tab: 'shuJuKanBan'
+    },
+    {
+      index: '/SoilPro',
+      label: '软件简介',
+      icon: InfoFilled,
+      tab: 'introduction'
+    },
+    {
+      index: '/Overview',
+      label: '项目简介',
+      icon: Collection,
+      tab: 'introduction'
+    },
+    {
+      index: '/ResearchFindings',
+      label: '研究成果',
+      icon: Histogram,
+      tab: 'introduction'
+    },
+    {
+      index: '/Unit',
+      label: '团队信息',
+      icon: HelpFilled,
+      tab: 'introduction'
+    },
+    {
+      index: 'irrigationWater',
+      label: '灌溉水',
+      icon: Watermelon,
+      tab: 'HmOutFlux',
+      children: [
+        {
+          index: '/samplingMethodDevice1',
+          label: '采样方法和装置',
+          icon: Sunny,
+          tab: 'HmOutFlux'
+        },
+        {
+          index: '/irriSampleData',
+          label: '灌溉水采样数据',
+          icon: Coin,
+          tab: 'HmOutFlux'
+        },
+        {
+          index: '/csSampleData',
+          label: '断面采样数据',
+          icon: Cloudy,
+          tab: 'HmOutFlux'
+        },
+        {
+          index: '/irriInputFlux',
+          label: '灌溉水输入通量',
+          icon: Cloudy,
+          tab: 'HmOutFlux'
+        }
+      ]
+    },
+    {
+      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: 'hmInFlux',
+      children: [
+        {
+          index: '/samplingDesc1',
+          label: '采样说明',
+          icon: Watermelon,
+          tab: 'hmInFlux'
+        },
+        {
+          index: '/grainRemovalInputFlux',
+          label: '籽粒移除输出通量',
+          icon: List,
+          tab: 'hmInFlux'
+        }
+      ]
+    },
+    {
+      index: 'strawRemoval',
+      label: '秸秆移除',
+      icon: WindPower,
+      tab: 'hmInFlux',
+      children: [
+        {
+          index: '/samplingDesc2',
+          label: '采样说明',
+          icon: Watermelon,
+          tab: 'hmInFlux'
+        },
+        {
+          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'
+        }
+      ]
+    },
+    {
+      index: '/mapView',
+      label: '地图展示',
+      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: '土壤镉的总含量预测',
+      icon: PieChart,
+      tab: 'cadmiumPrediction'
+    },
+    {
+      index: '/EffectiveCadmiumPrediction',
+      label: '土壤镉有效态含量预测',
+      icon: PieChart,
+      tab: 'cadmiumPrediction'
+    },
+    {
+      index: '/CropCadmiumPrediction',
+      label: '土壤镉作物态含量预测',
+      icon: PieChart,
+      tab: 'cadmiumPrediction'
+    },
+    {
+      index: '/cropRiskAssessment',
+      label: '水稻镉污染风险',
+      icon: Compass,
+      tab: 'cropRiskAssessment'
+    },
+    {
+      index: '/farmlandQualityAssessment',
+      label: '韶关',
+      icon: DataLine,
+      tab: 'farmlandQualityAssessment'
+    },
+    {
+      index: '/acidModel',
+      label: '土壤反酸',
+      icon: MagicStick,
+      tab: 'soilAcidificationPrediction',
+      children: [
+        {
+          index: '/Calculation',
+          label: '土壤反酸预测',
+          icon: Sunny,
+          tab: 'heavyMetalFluxCalculation'
+        },
+        {
+          index: '/SoilAcidReductionIterativeEvolution',
+          label: '反酸模型迭代可视化',
+          icon: Coin,
+          tab: 'heavyMetalFluxCalculation'
+        }
+      ]
+    },
+    {
+      index: '/neutralizationModel',
+      label: '土壤降酸',
+      icon: MagicStick,
+      tab: 'soilAcidificationPrediction',
+      children: [
+        {
+          index: '/AcidNeutralizationModel',
+          label: '土壤降酸预测',
+          icon: Sunny,
+          tab: 'heavyMetalFluxCalculation'
+        },
+        {
+          index: '/SoilAcidificationIterativeEvolution',
+          label: '土壤降酸预测',
+          icon: Coin,
+          tab: 'heavyMetalFluxCalculation'
+        }
+      ]
+    },
+    {
+      index: '/TraditionalFarmingRisk',
+      label: '传统耕种习惯风险趋势',
+      icon: MenuIcon,
+      tab: 'scenarioSimulation'
+    },
+    {
+      index: '/HeavyMetalCadmiumControl',
+      label: '重金属镉污染治理',
+      icon: MenuIcon,
+      tab: 'scenarioSimulation'
+    },
+    {
+      index: '/SoilAcidificationControl',
+      label: '土壤酸化治理',
+      icon: MenuIcon,
+      tab: 'scenarioSimulation'
+    },
+    {
+      index: '/DetectionStatistics',
+      label: '检测信息统计',
+      icon: List,
+      tab: 'dataStatistics'
+    },
+    {
+      index: '/FarmlandPollutionStatistics',
+      label: '耕地污染信息统计',
+      icon: List,
+      tab: 'dataStatistics'
+    },
+    {
+      index: '/PlantingRiskStatistics',
+      label: '种植风险信息统计',
+      icon: List,
+      tab: 'dataStatistics'
+    }
+  ].filter(tab => !["shuJuKanBan", "mapView", "introduction"].includes(tab.name));
+  

+ 179 - 0
src/components/layout/menuItems2.ts

@@ -0,0 +1,179 @@
+import {
+  Setting,
+  Menu,
+  Folder,
+  Document,
+  User,
+  Grid,
+  Reading,
+  Location,
+  TrendCharts,
+  Collection,
+  Postcard,
+  Star,
+  Warning,
+  Tickets,
+  List,
+  DataAnalysis,
+  Tools,
+  Histogram,
+  Guide,
+  Platform,
+  Box,
+  Avatar,
+  Briefcase,
+} from "@element-plus/icons-vue";
+
+export const tabMenuMap: Record<string, any[]> = {
+  dataManagement: [
+    {
+      index: "/Soil Acidification and Acid Reduction Data Management",
+      label: "反酸及降酸数据管理",
+      icon: Tools,
+      children: [
+        { index: "/soilAcidReductionData", label: "降酸数据", icon: DataAnalysis },
+        { index: "/soilAcidificationData", label: "反酸数据", icon: Warning },
+      ],
+    },
+    {
+      index: "/Administrative Area Data Management",
+      label: "行政区域数据管理",
+      icon: Location,
+      children: [
+        { index: "/AdminRegionData", label: "行政区域数据", icon: Location },
+      ],
+    },
+    {
+      index: "/Soil Assessment Cell Data Management",
+      label: "土壤评估单元格数据管理",
+      icon: Grid,
+      children: [
+        { index: "/SoilAssessmentUnitData", label: "土壤评估单元格数据", icon: Grid },
+      ],
+    },
+    {
+      index: "/Soil Heavy Metal Sampling Data Management",
+      label: "土壤重金属采集数据管理",
+      icon: Collection,
+      children: [
+        { index: "/SoilHeavyMetalData", label: "土壤重金属采集数据", icon: Collection },
+      ],
+    },
+    {
+      index: "/Crop Heavy Metal Sampling Data Management",
+      label: "农作物重金属采集样数据管理",
+      icon: Postcard,
+      children: [
+        { index: "/CropHeavyMetalData", label: "农作物重金属采集样数据", icon: Postcard },
+      ],
+    },
+    {
+      index: "/Land Use Type Data Management",
+      label: "用地类型数据管理",
+      icon: Briefcase,
+      children: [
+        { index: "/LandUseTypeData", label: "用地类型数据", icon: List },
+      ],
+    },
+    {
+      index: "/Soil Acidification Sampling Data Management",
+      label: "土壤酸化采样数据管理",
+      icon: Warning,
+      children: [
+        { index: "/SoilAcidificationData", label: "土壤酸化采样数据", icon: Warning },
+      ],
+    },
+    {
+      index: "/Climate Information Data Management",
+      label: "气候信息数据管理",
+      icon: TrendCharts,
+      children: [
+        { index: "/ClimateInfoData", label: "气候信息数据", icon: TrendCharts },
+      ],
+    },
+    {
+      index: "/Geographic Environmental Information Management",
+      label: "地理环境信息管理",
+      icon: Reading,
+      children: [
+        { index: "/GeographicEnvInfoData", label: "地理环境信息", icon: Reading },
+      ],
+    },
+  ],
+  infoManagement: [
+    {
+      index: "/IntroductionUpdate",
+      label: "介绍信息管理",
+      icon: Guide,
+    },
+  ],
+  modelManagement: [
+    {
+      index: "/Soil Cadmium Content Prediction Model Management",
+      label: "土壤镉含量预测模型管理",
+      icon: DataAnalysis,
+      children: [
+        {
+          index: "/CadmiumPredictionModel",
+          label: "土壤镉含量预测模型",
+          icon: Star,
+        },
+      ],
+    },
+    {
+      index: "/AcidReductionModel",
+      label: "反酸及降酸模型管理",
+      icon: Tools,
+      children: [
+        {
+          index: "/ModelSelection",
+          label: "模型选择",
+          icon: Setting,
+        },
+        {
+          index: "/thres",
+          label: "阈值选择",
+          icon: Histogram,
+        },
+        {
+          index: "/ModelTrain",
+          label: "模型训练",
+          icon: Folder,
+        },
+      ],
+    },
+    {
+      index: "/Rice Cadmium Pollution Risk Model Management",
+      label: "水稻镉污染风险模型管理",
+      icon: Star,
+      children: [
+        { index: "/RiceRiskModel", label: "水稻镉污染风险模型", icon: Star },
+      ],
+    },
+    {
+      index: "/Wheat Cadmium Pollution Risk Model Management",
+      label: "小麦镉污染风险模型管理",
+      icon: Star,
+      children: [
+        { index: "/WheatRiskModel", label: "小麦镉污染风险模型", icon: Star },
+      ],
+    },
+    {
+      index: "/Vegetable Cadmium Pollution Risk Model Management",
+      label: "蔬菜镉污染风险模型管理",
+      icon: Star,
+      children: [
+        {
+          index: "/VegetableRiskModel",
+          label: "蔬菜镉污染风险模型",
+          icon: Star,
+        },
+      ],
+    },
+    
+  ],
+  userManagement: [
+    { index: "/UserManagement", label: "用户信息", icon: Avatar },
+    { index: "/UserRegistration", label: "普通用户", icon: User },
+  ],
+};

+ 205 - 61
src/router/index.ts

@@ -7,7 +7,7 @@ const routes = [
   {
     path: "/login",
     name: "login",
-    component: () => import("@/views/menu/loginView.vue"), // 修复路径
+    component: () => import("@/views/login/loginView.vue"), // 修复路径
   },
   {
     path: "/",
@@ -65,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/HmOutFlux/irrigationWater/samplingMethodDevice1.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/crossSectionSampleData.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",
@@ -104,6 +266,34 @@ const routes = [
         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",
@@ -120,6 +310,15 @@ const routes = [
           ), // 修复路径
         meta: { title: "土壤镉有效态含量预测" },
       },
+      {
+        path: "CropCadmiumPrediction",
+        name: "CropCadmiumPrediction",
+        component: () =>
+          import(
+            "@/views/User/cadmiumPrediction/CropCadmiumPrediction.vue"
+          ), // 修复路径
+        meta: { title: "土壤镉作物态含量预测" },
+      },
       {
         path: "cropRiskAssessment",
         name: "cropRiskAssessment",
@@ -347,60 +546,6 @@ const routes = [
           import("@/views/Admin/modelManagement/VegetableRiskModel.vue"),
         meta: { title: "蔬菜镉污染风险模型" },
       },
-      {
-        path: "irrigationWater",
-        name: "irrigationWater",
-        component: () =>
-          import(
-            "@/views/User/heavyMetalFluxCalculation/inputFluxCalculation/irrigationWater.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: "大气干湿沉降" },
-      },
-      {
-        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",
@@ -415,21 +560,20 @@ const routes = [
   },
 ];
 
-// 创建 router 实例
 const router = createRouter({
   history: createWebHistory(import.meta.env.BASE_URL),
-  routes, // 使用路由配置数组
+  routes,
 });
 
-// 导航守卫
-// 导航守卫:只做鉴权,不做跳转
 router.beforeEach((to, from, next) => {
   const tokenStore = useTokenStore();
-  if (to.matched.some((r) => r.meta.requiresAuth)) {
+  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();
   }
 });
 
-export default router;
+export default router;

+ 0 - 0
src/views/Admin/selectCityAndCounty.vue


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

+ 3 - 1
src/views/User/heavyMetalFluxCalculation/outputFluxCalculation/cropRemoval.vue → src/views/User/HmOutFlux/agriInput/samplingDesc2.vue

@@ -19,5 +19,7 @@ export default {
 </script>
 
 <style scoped>
- 
+  . {
+    
+  }
 </style>

+ 3 - 1
src/views/User/heavyMetalFluxCalculation/outputFluxCalculation/subsurfaceFlow.vue → src/views/User/HmOutFlux/atmosDeposition/airInputFlux.vue

@@ -19,5 +19,7 @@ export default {
 </script>
 
 <style scoped>
-  
+  . {
+    
+  }
 </style>

+ 559 - 0
src/views/User/HmOutFlux/atmosDeposition/airSampleData.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: 30, // 图标宽度
+          height: 30, // 图标高度
+          anchor: { x: 12.5, y: 12.5 }, // 居中定位
+          src: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTIgMTcuMjdsNi4xOCAzLjYzLTEuNjQtNy4wMyA1LjM0LTQuNjMtNy4xOS0uNjFMMTIgM2wtMy4xOSA2LjYzLTcuMTkuNjFMMTAuNDYgMTMuODkgOC44MiAyMC45IDE4IDE3LjI3eiIgZmlsbD0iI0ZGMDAwMCIvPjwvc3ZnPg=='
+        })
+      }
+    });
+    
+    // 绑定标记点击事件
+    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>

+ 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,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTIgMTcuMjdsNi4xOCAzLjYzLTEuNjQtNy4wMyA1LjM0LTQuNjMtNy4xOS0uNjFMMTIgM2wtMy4xOSA2LjYzLTcuMTkuNjFMMTAuNDYgMTMuODkgOC44MiAyMC45IDE4IDE3LjI3eiIgZmlsbD0iI0ZGMDAwMCIvPjwvc3ZnPg=='
+        })
+      }
+    });
+    
+    // 绑定标记点击事件
+    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,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;
+}
+
+
+
+.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>

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

@@ -0,0 +1,94 @@
+<template>
+  <div class="page-container">
+    
+   <div class="point-map">
+    <div class="component-title">采样点地图展示</div>
+   <AtmCompanytencentMap/>
+   </div>
+
+  
+   <div class="point-line">
+    <div class="component-title">采样点数据列表展示</div>
+    <atmcompanyline/>
+   </div>
+
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, watch, computed } from 'vue';
+import * as echarts from 'echarts';
+import atmcompanyline from './atmcompanyline.vue';
+import AtmCompanytencentMap from './atmCompanytencentMap.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;
+}
+
+.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: 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;          /* 上下间距缩小,更紧凑(原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>

+ 3 - 1
src/views/User/heavyMetalFluxCalculation/outputFluxCalculation/surfaceRunoff.vue → src/views/User/HmOutFlux/atmosDeposition/samplingDesc3.vue

@@ -19,5 +19,7 @@ export default {
 </script>
 
 <style scoped>
- 
+  . {
+    
+  }
 </style>

+ 425 - 0
src/views/User/HmOutFlux/irrigationWater/crossSectionSampleData.vue

@@ -0,0 +1,425 @@
+<template>
+  <div class="map-page">
+    <h3 class="table-title">断面数据地图展示</h3>
+    <div ref="mapContainer" class="map-container"></div>
+  </div>
+
+  <div class="map-page">
+    <!-- 数据表格容器 -->
+    <div class="table-container">
+      <h3 class="table-title">断面数据详情</h3>
+      <table class="data-table">
+        <!-- 表头 -->
+        <thead>
+          <tr>
+            <th>断面编号</th>
+            <th>所属河流</th>
+            <th>断面位置</th>
+            <th>所属区县</th>
+            <th>Cd含量(mg/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 isMapReady = ref(false)
+const mapContainer = ref(null)
+const error = ref(null)
+const TMap = ref(null)
+let infoWindow = ref(null)
+let map = null
+let markersLayer = null
+const state = reactive({
+  excelData: [], // 存储解析后的断面数据
+})
+
+// 腾讯地图配置
+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(mg/L)": 0.00011 },
+    { "断面编号": 1, "所属河流": "浈江", "断面位置": "长坝", "所属区县": "仁化县", "经度": 113.692874, "纬度": 24.874845, "Cd(mg/L)": 0.00116 },
+    { "断面编号": 2, "所属河流": "浈江", "断面位置": "东河桥", "所属区县": "浈江区", "经度": 113.601631, "纬度": 24.80784, "Cd(mg/L)": 0.00346 },
+    { "断面编号": 3, "所属河流": "武江", "断面位置": "坪石", "所属区县": "乐昌市", "经度": 113.066281, "纬度": 25.274421, "Cd(mg/L)": 0.00098 },
+    { "断面编号": 4, "所属河流": "武江", "断面位置": "乐昌", "所属区县": "乐昌市", "经度": 113.338782, "纬度": 25.129212, "Cd(mg/L)": 0.00011 },
+    { "断面编号": 5, "所属河流": "武江", "断面位置": "武江桥", "所属区县": "乐昌市", "经度": 113.349815, "纬度": 25.120278, "Cd(mg/L)": 0.00015 },
+    { "断面编号": 6, "所属河流": "北江", "断面位置": "九公里", "所属区县": "浈江区", "经度": 113.580758, "纬度": 24.761299, "Cd(mg/L)": 0.00783 },
+    { "断面编号": 7, "所属河流": "北江", "断面位置": "白土", "所属区县": "曲江区", "经度": 113.531284, "纬度": 24.679958, "Cd(mg/L)": 0.00594 },
+    { "断面编号": 8, "所属河流": "浈江", "断面位置": "昆仑水站", "所属区县": "南雄市", "经度": 114.3629285, "纬度": 25.10053746, "Cd(mg/L)": 0.000517 },
+    { "断面编号": 9, "所属河流": "北江", "断面位置": "白沙", "所属区县": "曲江", "经度": 113.5707136, "纬度": 24.58139261, "Cd(mg/L)": 0.00154 },
+    { "断面编号": 10, "所属河流": "浈江", "断面位置": "周田水站", "所属区县": "仁化县", "经度": 113.8293461, "纬度": 24.97851516, "Cd(mg/L)": 0.000182 },
+    { "断面编号": 11, "所属河流": "武江", "断面位置": "坪石水站", "所属区县": "乐昌市", "经度": 113.0467854, "纬度": 25.28883459, "Cd(mg/L)": 0.001071 }
+  ];
+
+  // 处理坐标(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(mg/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,
+          //anchor: { x: 15, y: 15 }, // 图标居中
+          src: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPjxwYXRoIGQ9Ik0xMiAyTDIgMjJoMjBMMTIgMnoiIGZpbGw9IiNGRjAwMDAiLz48L3N2Zz4='        })
+      }
+    });
+    // 绑定标记点击事件
+    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} mg/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();
+  } catch (err) {
+    error.value = err.message;
+  }
+})
+
+onBeforeUnmount(() => {
+  if (markersLayer) markersLayer.setMap(null); // 销毁标记图层
+  if (infoWindow.value) infoWindow.value.close(); // 关闭信息窗口
+})
+</script>
+
+<style scoped>
+/* 地图容器保持不变 */
+.map-page { 
+  width: 100vw; 
+  height: 40vw; 
+  background-color: white;
+  border-radius:  12px;
+  margin-bottom: 24px;
+}
+
+.map-container { 
+  width: 100%; 
+  height: 50%; 
+  margin: 1rem auto;
+  padding: 0;
+}
+
+/* 信息窗口核心调整:暴力放大 + 宽高适配 */
+: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;
+}
+
+.table-container {
+  padding: 20px;
+  flex: 1;
+  overflow: auto;
+}
+
+.table-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 */
+.table-title::before {
+  content: "";
+  position: absolute;
+  left: 0;                /* 靠最左侧 */
+  top: 50%;              /* 垂直居中 */
+  transform: translateY(-50%);
+  width: 12px;           /* 方块大小,适中即可 */
+  height: 12px;
+  background-color: #1e88e5; /* 和文字同色,统一感 */
+  border-radius: 2px;    /* 轻微圆角,比直角更柔和 */
+}
+
+.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;
+}
+</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>

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

@@ -0,0 +1,99 @@
+<template>
+  <div class="page-container">
+    
+   <div class="point-map">
+    <div class="component-title">采样点地图展示</div>
+   <TencentMapView/>
+   </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 { 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';
+
+
+
+
+</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;
+}
+
+.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: 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;          /* 上下间距缩小,更紧凑(原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>

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

+ 1045 - 0
src/views/User/HmOutFlux/irrigationWater/tencentMapView.vue

@@ -0,0 +1,1045 @@
+<template>
+  <div class="map-page">
+    <div ref="mapContainer" 
+     class="map-container"
+    ></div>
+  </div>
+</template>
+
+<script setup>
+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 activeTempMarker = ref(null)
+let infoWindow = ref(null)
+let map = null
+let markersLayer = null
+let soilTypeVectorLayer=null;//土壤类型多边形图层
+let waterSystemLayer = 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/'
+})
+
+
+
+let sdkLoaded = false; // 新增:标记 SDK 是否已加载
+const loadSDK = () => {
+  return new Promise((resolve, reject) => {
+    if (sdkLoaded) { // 已加载则直接返回
+      resolve(window.TMap);
+      return;
+    }
+    // 移除旧脚本(避免重复加载)
+    const oldScript = document.querySelector('script[src*="map.qq.com"]');
+    if (oldScript) oldScript.remove();
+    
+    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 = () => {
+      sdkLoaded = true; // 标记为已加载
+      if (!window.TMap?.service?.Geocoder) {
+        reject(new Error('地图SDK加载失败'));
+        return;
+      }
+      TMap.value = window.TMap;
+      resolve(window.TMap);
+    };
+    
+    script.onerror = (err) => {
+      reject(`地图资源加载失败: ${err.message}`);
+      document.head.removeChild(script);
+    };
+    document.head.appendChild(script);
+  });
+};
+
+const WATER_SAMPLING_API='http://localhost:3000/table/Water_sampling_data';
+const fetchWaterSamplingData = async ()=>{
+  try{
+    const response = await axios.get(WATER_SAMPLING_API);
+    return response.data.data || response.data;
+  }catch(err){
+    console.error("接口请求失败:",err);
+    throw new Error(`获取水样数据失败:${err.message || '网络错误'}`)
+  }
+};
+
+const initData =async ()=>{
+  try{
+    const rawData = await fetchWaterSamplingData();
+    if(!Array.isArray(rawData)){
+      throw new Error('接口返回数据格式错误');
+    }
+
+    state.excelData = rawData.map(item=>{
+      const lat=Number(item.latitude);
+      const lng=Number(item.longitude);
+
+      if(isNaN(lat)||isNaN(lng)){
+        console.error('无效经纬度数据',item);
+        return null;
+      }
+
+      return{
+        ...item,
+        latitude:lat,
+        longitude:lng,
+      };
+    }).filter(item=>item !==null)
+    console.log('成功加载${state.excelData.length}条有效数据');
+  }catch(err){
+    console.error('数据初始化失败:',err);
+    error.value = err.message;
+    state.excelData=[];
+  }
+}
+
+// 初始化地图
+const initMap = async () => {
+  try {
+    if (map) {
+      map.destroy();
+      map = null;
+    }
+    await loadSDK()
+    //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,
+      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');
+    
+     if (markersLayer) {
+    markersLayer.setMap(null);
+    markersLayer = null;
+  }
+    // 创建标记点向量图层
+  markersLayer = new TMap.value.MultiMarker({
+  map: map,
+  zIndex:1000,
+  styles: {
+    default: new TMap.value.MarkerStyle({
+      width: 30, // 图标宽度
+      height: 30, // 图标高度
+      anchor: { x: 12.5, y: 12.5 }, // 居中定位
+      src: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTIgMTcuMjdsNi4xOCAzLjYzLTEuNjQtNy4wMyA1LjM0LTQuNjMtNy4xOS0uNjFMMTIgM2wtMy4xOSA2LjYzLTcuMTkuNjFMMTAuNDYgMTMuODkgOC44MiAyMC45IDE4IDE3LjI3eiIgZmlsbD0iI0ZGMDAwMCIvPjwvc3ZnPg=='
+      })
+    }
+  });
+    console.log('markersLayer是否绑定地图:',markersLayer.getMap() === map);
+    
+
+    // 创建土壤类型多边形图层
+    soilTypeVectorLayer = new TMap.value.MultiPolygon({
+      map: map,
+      styles: {
+        default: new TMap.value.PolygonStyle({
+          fillColor: '#cccccc',
+          fillOpacity: 0.4,
+          strokeColor: '#333',
+          strokeWidth: 1
+        })
+      }    
+    });
+    
+  if (typeof handleMarkerClick === 'function' && markersLayer) {
+     markersLayer.on('click', handleMarkerClick); 
+     console.log('[地图] 标记点点击事件绑定成功');
+  }
+    await initData()
+    updateMarkers()
+    // 在updateMarkers()后执行
+   // console.log(markersLayer.getStyles());
+    //console.log(document.querySelector('.tmap-marker img')); 
+    
+
+     // 6. 绑定事件
+    map.on('click', handleMapClick);    
+    //console.log('地图实例创建完成,开始加载水系图');
+    await loadWaterSystemGeoJSON(); // 等待水系图加载完成
+
+    // 标记地图就绪
+    isMapReady.value = true;
+    //console.log('地图初始化完成(含水系图)');
+
+
+    // 新增地图就绪状态监听
+    map.on('idle', () => {
+      isMapReady.value = true;
+      //console.log('地图初始化完成');
+      //console.log('标记点图层初始化:',markersLayer.value);
+    })
+
+  } catch (err) {
+    isMapReady.value = true;
+    console.error('initMap执行异常:',err);
+    error.value = err.message
+  }
+}
+
+// 加载水系 GeoJSON 并添加到地图
+const loadWaterSystemGeoJSON = async () => {
+  try {
+    // 1. 请求 GeoJSON 文件(路径根据实际存放位置修改,如 public 目录下的 water_system.geojson)
+    const response = await fetch('/data/韶关市河流水系图.geojson');
+    const geojson = await response.json();
+    //console.log('水系 GeoJSON 加载成功,要素数量:', geojson.features.length);
+
+    // 2. 销毁旧图层(避免重复加载)
+    if (waterSystemLayer) {
+      waterSystemLayer.setMap(null);
+      waterSystemLayer = null;
+    }
+
+    // 3. 根据 GeoJSON 类型创建图层(水系通常是 LineString,用 MultiPolyline)
+    waterSystemLayer = new TMap.value.MultiPolyline({
+      map: map, // 绑定到地图实例
+      styles: {
+        default: new TMap.value.PolylineStyle({
+          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';
+     }) // 筛选线要素
+        .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
+    };
+        })
+    });
+
+   // console.log('水系图层加载完成');
+
+    // 4. 修正:遍历几何要素,合并边界
+    if (waterSystemLayer) {
+      const geometries = waterSystemLayer.getGeometries(); // 获取所有几何要素
+      if (geometries.length === 0) {
+        console.warn('水系图层无有效几何要素');
+        return;
+      }
+
+      // 初始化边界为第一个要素的边界
+      let bounds = geometries[0].getBounds(); 
+      // 合并剩余要素的边界
+      for (let i = 1; i < geometries.length; i++) {
+        bounds.extend(geometries[i].getBounds()); 
+      }
+
+      // 适配地图视野
+      map.fitBounds(bounds, { padding: [50, 50] }); 
+    }
+
+  } catch (err) {
+    console.error('水系 GeoJSON 加载失败:', err);
+    error.value = `水系图加载失败:${err.message}`;
+  }
+};
+
+
+
+
+// 更新标记点,添加Label显示
+const updateMarkers = () => {
+  // 正确的标记点创建方式
+  const geometries = state.excelData.map(item => {
+   // console.log(`'原始ID:'"${item.water_sample_ID}"`);
+    //console.log(`坐标验证:lat=${item.latitude},lng=${item.longitude}`);
+    
+    return {
+      id: item.water_sample_ID,
+      styleId: 'default',
+      position: new TMap.value.LatLng( item.latitude,item.longitude),
+      properties: {
+        title: item.sampling_location,
+        sampler_id:item.water_sample_ID,
+      }
+    };
+  })
+  
+  // 一次性设置所有标记
+  markersLayer.setGeometries(geometries);
+};
+
+const API_BASE_URL = 'http://localhost:3000/table/Water_assay_data'; 
+
+// 新增Marker点击事件处理
+const handleMarkerClick = async(e) => {
+  //console.log('点击事件已发生');
+  
+  const marker = e.geometry;
+  const markerId=marker.id.trim();
+
+  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 {
+    // 调试信息:显示当前点击的标记点ID
+    //console.log('点击标记点ID:', markerId);
+    //console.log('请求URL:', `${API_BASE_URL}?water_sample_ID=eq.${markerId}`);
+    
+    // 调用API获取水质数据 - 使用 markerId 而不是 marker.id
+    const response = await axios.get(API_BASE_URL, {
+      params: {
+        water_sample_ID: `eq.${markerId}`
+      },
+      timeout: 5000
+    });
+    
+    //console.log('API响应数据:', response.data);
+
+    // 关键:手动筛选出 water_sample_ID 匹配的第一条数据
+  const matchedData = response.data.find(item => 
+    item.water_sample_ID.trim() === markerId
+  );
+
+  if (!matchedData) {
+    throw new Error(`未找到采样点 ${markerId} 的监测数据`);
+  }
+
+    // 获取第一条数据
+    const apiData = matchedData;
+    
+    // 调试信息:显示获取到的数据ID
+    //console.log('获取到的水质数据ID:', apiData.water_sample_ID);
+    
+    // 创建信息窗口内容 - 使用 marker.properties.title 确保显示正确位置
+    const content = `
+  <div class="water-info-window">
+    <!-- 标题区 -->
+    <h3 class="info-title">${marker.properties.title}</h3>
+    
+    <!-- 基础信息区 -->
+    <div class="info-row">
+      <span class="info-label">采样点ID:</span>
+      <span class="info-value">${apiData.water_sample_ID}</span>
+    </div>
+    <div class="info-row">
+      <span class="info-label">样本编号:</span>
+      <span class="info-value">${apiData.sample_code || '无'}</span>
+    </div>
+    <div class="info-row">
+      <span class="info-label">pH值:</span>
+      <span class="info-value">${apiData.pH}</span>
+    </div>
+    
+    <!-- 分隔线 -->
+    <div class="divider"></div>
+    
+    <!-- 重金属区 -->
+    <h4 class="contaminant-title">重金属含量 (mg/L)</h4>
+    <div class="contaminant-grid">
+      <div class="contaminant-item">
+        <span class="contaminant-name">Cr:</span>
+        <span class="contaminant-value">${apiData.Cr}</span>
+      </div>
+      <div class="contaminant-item">
+        <span class="contaminant-name">As:</span>
+        <span class="contaminant-value">${apiData.As}</span>
+      </div>
+      <div class="contaminant-item">
+        <span class="contaminant-name">Cd:</span>
+        <span class="contaminant-value">${apiData.Cd}</span>
+      </div>
+      <div class="contaminant-item">
+        <span class="contaminant-name">Hg:</span>
+        <span class="contaminant-value">${apiData.Hg}</span>
+      </div>
+      <div class="contaminant-item">
+        <span class="contaminant-name">Pb:</span>
+        <span class="contaminant-value">${apiData.Pb}</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>
+        <p>尝试获取的ID: ${markerId}</p>
+      </div>
+    `;
+    
+    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 
+ }
+ }
+
+
+
+onMounted(async () => {
+  //console.log('开始执行 onMounted');
+  
+  try {
+    await loadSDK();
+    //console.log('SDK加载完成,开始initData');
+    await initMap()
+    //console.log('initMap执行完毕');
+    
+  } catch (err) {
+    console.error('onMounted执行异常',err);
+    error.value = err.message
+  }
+})
+
+onBeforeUnmount(() => {
+  // 1. 销毁地图实例(先销毁,再置空)
+  if (map) {
+    try {
+      map.destroy(); // 腾讯地图销毁方法
+      console.log('[地图] 地图实例已销毁');
+    } catch (e) {
+      console.error('[地图] 销毁失败:', e);
+    }
+    map = null;
+  }
+
+  // 2. 销毁图层(逐个检查)
+  const layers = [markersLayer, soilTypeVectorLayer, waterSystemLayer];
+  layers.forEach(layer => {
+    if (layer) {
+      try {
+        layer.setMap(null); // 从地图移除
+        if (layer.destroy) layer.destroy(); // 调用图层销毁方法
+      } catch (e) {
+        console.error('[地图] 图层销毁失败:', e);
+      }
+    }
+  });
+
+  // 3. 清理全局变量
+  if (window.initTMap) {
+    delete window.initTMap; // 移除全局回调
+  }
+});
+
+
+onUpdated(() => {
+  try {
+    if (map.value && farmlandLayer.value) {
+      // 更新地图视图
+    }
+  } catch (error) {
+    console.error("地图更新错误:", error);
+  }
+});
+
+</script>
+
+<style>
+.map-page {
+  position: relative;
+  width: 100vw;
+  height: 100vh;
+}
+
+.map-container {
+  width: 100%;
+  height: 100vh ;
+  min-height: 600px;
+  pointer-events: all;
+}
+
+.control-panel {
+  position: fixed;
+  top: 24px;
+  right: 24px;
+  background: rgba(255, 255, 255, 0.95);
+  padding: 16px;
+  border-radius: 12px;
+  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
+  backdrop-filter: blur(8px);
+  border: 1px solid rgba(255, 255, 255, 0.2);
+  z-index: 1000;
+  min-width: 240px;
+  transition: all 0.3s ease;
+}
+
+.control-panel:hover {
+  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
+  transform: translateY(-2px);
+}
+
+.control-panel label {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 8px 12px;
+  border-radius: 8px;
+  transition: background 0.2s ease;
+  cursor: pointer;
+}
+
+.control-panel label:hover {
+  background: rgba(56, 118, 255, 0.05);
+}
+
+.control-panel input[type="checkbox"] {
+  width: 18px;
+  height: 18px;
+  border: 2px solid #3876ff;
+  border-radius: 4px;
+  appearance: none;
+  cursor: pointer;
+  transition: all 0.2s ease;
+}
+
+.control-panel input[type="checkbox"]:checked {
+  background: #3876ff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24'%3E%3Cpath fill='%23fff' d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/%3E%3C/svg%3E") no-repeat center;
+  background-size: 12px;
+}
+
+.export-controls {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  margin-top: 16px;
+}
+
+.export-controls button {
+  padding: 10px 16px;
+  font-size: 14px;
+  font-weight: 500;
+  border: none;
+  border-radius: 8px;
+  cursor: pointer;
+  transition: all 0.2s ease;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  background: #3876ff;
+  color: white;
+}
+
+.export-controls button:disabled {
+  background: #e0e0e0;
+  color: #9e9e9e;
+  cursor: not-allowed;
+  opacity: 0.8;
+}
+
+.export-controls button:not(:disabled):hover {
+  background: #2b5dc5;
+  box-shadow: 0 4px 12px rgba(56, 118, 255, 0.3);
+}
+
+/* 新增加载动画 */
+@keyframes spin {
+  0% { transform: rotate(0deg); }
+  100% { transform: rotate(360deg); }
+}
+
+.loading-spinner {
+  width: 18px;
+  height: 18px;
+  border: 2px solid rgba(255, 255, 255, 0.3);
+  border-top-color: white;
+  border-radius: 50%;
+  animation: spin 0.8s linear infinite;
+}
+
+/* 响应式调整 */
+@media (max-width: 768px) {
+  .control-panel {
+    top: 16px;
+    right: 16px;
+    left: 16px;
+    width: auto;
+    min-width: auto;
+  }
+  
+  .export-controls {
+    flex-direction: row;
+    flex-wrap: wrap;
+  }
+  
+  .export-controls button {
+    flex: 1;
+    justify-content: center;
+  }
+}
+
+.polygon-info {
+  padding: 12px;
+  max-width: 300px;
+  
+  h3 {
+    margin: 0 0 8px;
+    color: #333;
+    font-size: 16px;
+  }
+
+  table {
+    width: 100%;
+    border-collapse: collapse;
+
+    tr {
+      border-bottom: 1px solid #eee;
+    }
+
+    th, td {
+      padding: 6px 4px;
+      text-align: left;
+      font-size: 14px;
+    }
+
+    th {
+      color: #666;
+      white-space: nowrap;
+      padding-right: 8px;
+    }
+  }
+}
+.point-info {
+  padding: 12px;
+  min-width: 200px;
+  
+  h3 {
+    margin: 0 0 8px;
+    font-size: 14px;
+    color: white;
+    padding: 4px 8px;
+    border-radius: 4px;
+    display: inline-block;
+    background: var(--category-color);
+  }
+  
+  p {
+    margin: 6px 0;
+    font-size: 13px;
+    line-height: 1.4;
+    
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+}
+
+
+.tooltip {
+  position: absolute;
+  padding: 8px 12px;
+  background: rgba(255, 255, 255, 0.9);
+  border-radius: 6px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+  z-index: 1001;
+  font-size: 14px;
+  white-space: nowrap;
+  opacity: 0;
+  transform: translateY(10px);
+  visibility: hidden;
+  transition: opacity 0.2s, transform 0.2s, visibility 0.2s;
+  border: 1px solid #e0e0e0;
+}
+
+.tooltip.visible {
+  opacity: 1;
+  transform: translateY(0);
+  visibility: visible;
+}
+
+.tooltip::after {
+  content: "";
+  position: absolute;
+  width: 0;
+  height: 0;
+  border-left: 6px solid transparent;
+  border-right: 6px solid transparent;
+  top: 100%;
+  left: 50%;
+  transform: translateX(-50%);
+  border-top: 6px solid rgba(255, 255, 255, 0.9);
+  border-top-color: inherit;
+}
+
+:deep(.tmap-vector-label) {
+  white-space: nowrap;
+  pointer-events: none; /* 允许点击穿透,不影响地图交互 */
+}
+
+/* 在style标签中添加以下样式 */
+:deep(.tmap-infowindow) {
+  padding: 12px;
+  min-width: 300px;
+  border-radius: 8px;
+  box-shadow: 0 2px 8px rgba(0,0,0,0.15);
+  background-color: white;
+}
+
+.db-info {
+  margin-top: 10px;
+  padding: 10px;
+  background-color: #f8f9fa;
+  border-left: 3px solid #4285f4;
+  border-radius: 4px;
+}
+
+.db-info h4 {
+  margin-top: 0;
+  color: #4285f4;
+  font-size: 14px;
+}
+
+.db-info pre {
+  margin: 5px 0 0;
+  font-size: 12px;
+  white-space: pre-wrap;
+  word-break: break-word;
+}
+
+.water-info-window {
+  font-family: 'Segoe UI', Tahoma, sans-serif;
+  background: #fff;
+  border-radius: 4px;
+  padding: 4px;
+  width: 200px;
+  height:auto;
+  border: 1px solid #e2e8f0;
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
+  font-size: 0.7rem; /* 基础字体大小调整为0.7rem(约11px) */
+}
+
+.info-title {
+  color: #1e40af;
+  font-size: 0.8rem;
+  margin: 0 0 3px 0;
+  padding-bottom: 2px;
+  border-bottom: 1px solid #e0f2fe;
+  font-weight: 600;
+  text-align: center;
+}
+
+.info-content {
+  padding: 2px;
+}
+
+.info-row {
+  display: flex;
+  margin-bottom: 2px;
+  align-items: center;
+}
+
+.info-label {
+  flex: 0 0 60px; /* 标签宽度调整为60px */
+  color: #475569;
+  font-weight: 500;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.info-value {
+  flex: 1;
+  color: #0f172a;
+  padding: 1px 3px;
+  background: #f8fafc;
+  border-radius: 2px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  font-size: 0.7rem;
+}
+
+.contaminant-section {
+  margin-top: 3px;
+  padding-top: 3px;
+  border-top: 1px dotted #e2e8f0;
+}
+
+.contaminant-title {
+  color: #1e40af;
+  margin: 0 0 2px 0;
+  font-size: 0.7rem;
+  font-weight: 500;
+  padding-left: 2px;
+}
+
+/* 污染物改为网格布局,每行3个 */
+.contaminants {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  gap: 2px;
+}
+
+.contaminant-item {
+  background: #f8fafc;
+  border-radius: 2px;
+  padding: 2px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  border: 0.5px solid #e2e8f0;
+}
+
+.contaminant-name {
+  color: #3b82f6;
+  font-weight: 500;
+  font-size: 0.7rem;
+  white-space: nowrap;
+  max-width: 100%;
+}
+
+.contaminant-value {
+  color: #0f172a;
+  font-size: 0.8rem;
+  background: #e2e8f0;
+  padding: 1px 2px;
+  border-radius: 2px;
+  margin-top: 1px;
+  min-width: 25px;
+  text-align: center;
+}
+  .assay-info {
+    margin-top: 16px;
+    padding: 8px 12px;
+    background-color: #f5f5f5;
+    border-radius: 6px;
+    font-size: 0.85rem;
+    color: #666;
+    text-align: center;
+  }
+
+  /* 在style标签中添加 */
+.crystal-bubble .bubble {
+  width: 24px;
+  height: 24px;
+  border-radius: 50%;
+  background: radial-gradient(circle at 30% 30%, #00b4ff, #0077cc);
+  box-shadow: 
+    0 0 10px rgba(0, 183, 255, 0.7),
+    inset 0 0 15px rgba(0, 100, 200, 0.5);
+  position: relative;
+  animation: pulse 1.5s infinite;
+}
+
+.crystal-bubble .water-drop {
+  position: absolute;
+  width: 10px;
+  height: 10px;
+  background: rgba(255, 255, 255, 0.85);
+  border-radius: 50%;
+  top: 25%;
+  left: 25%;
+  box-shadow: 
+    0 0 5px #fff,
+    inset 0 0 3px rgba(0, 0, 0, 0.2);
+  transform: rotate(-20deg);
+}
+
+@keyframes pulse {
+  0% { transform: scale(1); opacity: 0.8; }
+  50% { transform: scale(1.1); opacity: 1; }
+  100% { transform: scale(1); opacity: 0.8; }
+}
+
+</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>

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

@@ -0,0 +1,290 @@
+<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:14,
+        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:14
+      }
+    },
+    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} mg/L`).join('');
+      },
+      textSize:{
+        fontSize:14
+      }
+    },
+    xAxis: {
+      type: 'category',
+      data: regions,
+      axisLabel: { 
+        rotate: 45,
+        formatter: val => val.replace('韶关市', ''),
+        fontSize:13
+      }
+    },
+    yAxis: { 
+      type: 'value', 
+      name: '浓度(mg/L)' ,
+      axisLabel:{
+        fontSize:13,
+      }
+    },
+    dataZoom: [{
+      type: 'inside',
+      start: 0,
+      end: 100
+    }],
+    series,
+    legend: { 
+      data: series.map(s => s.name), 
+      bottom: 10,
+      textStyle:{
+        fontSize:13
+      }
+    },
+    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;
+}
+.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: '铬含量' },
+  { key: 'As', label: '砷含量' },
+  { key: 'Cd', label: '镉含量' },
+  { key: 'Hg', label: '汞含量' },
+  { key: 'Pb', label: '铅含量' },
+  { 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>

+ 741 - 0
src/views/User/cadmiumPrediction/CropCadmiumPrediction.vue

@@ -0,0 +1,741 @@
+<template>
+  <div class="container">
+    <!-- 顶部操作栏 -->
+    <div class="toolbar">
+      <!-- 文件上传区域 -->
+      <div class="upload-section">
+        <input type="file" ref="fileInput" accept=".csv" @change="handleFileUpload" style="display: none">
+        <el-button class="custom-button" @click="triggerFileUpload">
+          <el-icon class="upload-icon"><Upload /></el-icon>
+          选择CSV文件
+        </el-button>
+        <span v-if="selectedFile" class="file-name">{{ selectedFile.name }}</span>
+        <el-button 
+          class="custom-button" 
+          :loading="isCalculating" 
+          :disabled="!selectedFile" 
+          @click="calculate"
+        >
+        <el-icon class="upload-icon"><Document /></el-icon>  
+        上传并计算
+        </el-button>
+      </div>
+      <!-- 操作按钮 -->
+      <div class="action-buttons">
+        <el-button class="custom-button" :disabled="!mapBlob" @click="exportMap">
+          <el-icon class="upload-icon"><Download /></el-icon>
+          导出地图</el-button>
+        <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="!statisticsData.length" @click="exportData">
+          <el-icon class="upload-icon"><Download /></el-icon>
+          导出数据</el-button>
+      </div>
+    </div>
+
+    <!-- 主体内容区 -->
+    <div class="content-area">
+      <!-- 地图区域 - 修改为横向布局 -->
+      <div class="horizontal-container">
+        <!-- 地图展示 -->
+        <div class="map-section">
+          <h3>作物态Cd预测地图</h3>
+          <div v-if="loadingMap" class="loading-container">
+            <el-icon class="loading-icon"><Loading /></el-icon>
+            <span>地图加载中...</span>
+          </div>
+          <img v-if="mapImageUrl && !loadingMap" :src="mapImageUrl" alt="作物态Cd预测地图" class="map-image">
+          <div v-if="!mapImageUrl && !loadingMap" class="no-data">
+            <el-icon><Picture /></el-icon>
+            <p>暂无地图数据</p>
+          </div>
+        </div>
+        
+        <!-- 直方图展示 -->
+        <div class="histogram-section">
+          <h3>作物态Cd预测直方图</h3>
+          <div v-if="loadingHistogram" class="loading-container">
+            <el-icon class="loading-icon"><Loading /></el-icon>
+            <span>直方图加载中...</span>
+          </div>
+          <img v-if="histogramImageUrl && !loadingHistogram" :src="histogramImageUrl" alt="作物态Cd预测直方图" class="histogram-image">
+          <div v-if="!histogramImageUrl && !loadingHistogram" class="no-data">
+            <el-icon><Histogram /></el-icon>
+            <p>暂无直方图数据</p>
+          </div>
+        </div>
+      </div>
+
+      <!-- 统计图表区域 -->
+      <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>
+</template>
+
+<script>
+import * as XLSX from 'xlsx';
+import { saveAs } from 'file-saver';
+import axios from 'axios';
+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, DataAnalysis 
+  },
+  data() {
+    return {
+      isCalculating: false,
+      loadingMap: false,
+      loadingHistogram: false,
+      loadingStats: false,
+      statisticsData: [],
+      mapImageUrl: null,
+      histogramImageUrl: null,
+      mapBlob: null,
+      histogramBlob: null,
+      selectedFile: null,
+      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: {
+    // 触发文件选择
+    triggerFileUpload() {
+      this.$refs.fileInput.click();
+    },
+    
+    // 处理文件上传
+    handleFileUpload(event) {
+      const files = event.target.files;
+      if (files && files.length > 0) {
+        this.selectedFile = files[0];
+      } else {
+        this.selectedFile = null;
+      }
+    },
+    
+    // 获取最新结果
+    async fetchLatestResults() {
+      try {
+        this.loadingMap = true;
+        this.loadingHistogram = true;
+        
+        // 获取最新地图
+        await this.fetchLatestMap();
+        
+        // 获取最新直方图
+        await this.fetchLatestHistogram();
+        
+      } catch (error) {
+        console.error('获取最新结果失败:', error);
+        this.$message.error('获取最新结果失败');
+      } finally {
+        this.loadingMap = false;
+        this.loadingHistogram = false;
+      }
+    },
+    
+    // 获取最新地图
+    async fetchLatestMap() {
+      try {
+        const response = await axios.get(
+          `https://soilgd.com:8000/api/cd-prediction/crop-cd/latest-map/${this.countyName}`,
+          { responseType: 'blob' }
+        );
+        
+        this.mapBlob = response.data;
+        this.mapImageUrl = URL.createObjectURL(this.mapBlob);
+      } catch (error) {
+        console.error('获取最新地图失败:', error);
+        this.$message.warning('获取最新地图失败,请先执行预测');
+      }
+    },
+    
+    // 获取最新直方图
+    async fetchLatestHistogram() {
+      try {
+        const response = await axios.get(
+          `https://soilgd.com:8000/api/cd-prediction/crop-cd/latest-histogram/${this.countyName}`,
+          { responseType: 'blob' }
+        );
+        
+        this.histogramBlob = response.data;
+        this.histogramImageUrl = URL.createObjectURL(this.histogramBlob);
+      } catch (error) {
+        console.error('获取最新直方图失败:', error);
+        this.$message.warning('获取最新直方图失败,请先执行预测');
+      }
+    },
+    
+    // 格式化统计数据
+    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) {
+        this.$message.warning('请先选择CSV文件');
+        return;
+      }
+      
+      try {
+        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地图接口
+        const mapResponse = await axios.post(
+          'https://soilgd.com:8000/api/cd-prediction/crop-cd/generate-and-get-map',
+          formData,
+          {
+            headers: {
+              'Content-Type': 'multipart/form-data'
+            },
+            responseType: 'blob'
+          }
+        );
+        
+        // 保存地图数据
+        this.mapBlob = mapResponse.data;
+        this.mapImageUrl = URL.createObjectURL(this.mapBlob);
+        
+        // 更新后重新获取直方图和统计数据
+        await this.fetchLatestHistogram();
+        await this.fetchStatistics();
+        
+        this.$message.success('计算完成!');
+        
+      } catch (error) {
+        console.error('计算失败:', error);
+        let errorMessage = '计算失败,请重试';
+        
+        if (error.response) {
+          if (error.response.status === 400) {
+            errorMessage = '文件格式错误:' + (error.response.data.detail || '请上传正确的CSV文件');
+          } else if (error.response.status === 404) {
+            errorMessage = '不支持的县市:' + this.countyName;
+          } else if (error.response.status === 500) {
+            errorMessage = '服务器错误:' + (error.response.data.detail || '请稍后重试');
+          }
+        }
+        
+        this.$message.error(errorMessage);
+      } finally {
+        this.isCalculating = false;
+        this.loadingMap = false;
+        this.loadingHistogram = false;
+        this.loadingStats = false;
+      }
+    },
+    
+    // 导出地图
+    exportMap() {
+      if (!this.mapBlob) {
+        this.$message.warning('请先计算生成地图');
+        return;
+      }
+      
+      const link = document.createElement('a');
+      link.href = URL.createObjectURL(this.mapBlob);
+      link.download = `${this.countyName}_作物态Cd预测地图.jpg`;
+      link.click();
+      URL.revokeObjectURL(link.href);
+    },
+    
+    // 导出直方图
+    exportHistogram() {
+      if (!this.histogramBlob) {
+        this.$message.warning('请先计算生成直方图');
+        return;
+      }
+      
+      const link = document.createElement('a');
+      link.href = URL.createObjectURL(this.histogramBlob);
+      link.download = `${this.countyName}_作物态Cd预测直方图.jpg`;
+      link.click();
+      URL.revokeObjectURL(link.href);
+    },
+    
+    // 导出数据 - 修改为获取作物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 || '请稍后重试'));
+      }
+    }
+  }
+};
+</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;
+  min-height: 100vh;
+  box-sizing: border-box;
+}
+
+.toolbar {
+  display: flex;
+  flex-direction: column;
+  gap: 15px;
+  margin-bottom: 20px;
+  padding: 15px;
+  background-color: white;
+  border-radius: 8px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+}
+
+.upload-section {
+  display: flex;
+  align-items: center;
+  gap: 15px;
+  padding-bottom: 15px;
+  border-bottom: 1px solid #eee;
+}
+
+.file-name {
+  flex: 1;
+  padding: 0 10px;
+  color: #666;
+  font-size: 14px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.action-buttons {
+  display: flex;
+  gap: 10px;
+}
+
+.custom-button {
+  background-color: #47C3B9 !important;
+  color: #DCFFFA !important;
+  border: none;
+  border-radius: 155px;
+  padding: 10px 20px;
+  font-weight: bold;
+  display: flex;
+  align-items: center;
+}
+
+.upload-icon {
+  margin-right: 5px;
+}
+
+.content-area {
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+
+/* 横向布局容器 */
+.horizontal-container {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 20px;
+  width: 100%;
+}
+
+.map-section, .histogram-section {
+  flex: 1;
+  min-width: 300px;
+  background-color: white;
+  border-radius: 8px;
+  padding: 15px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+  position: relative;
+  min-height: 400px;
+}
+
+.map-image, .histogram-image {
+  width: 100%;
+  height: 100%;
+  max-height: 600px;
+  object-fit: contain;
+  border-radius: 4px;
+}
+
+.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;
+  align-items: center;
+  justify-content: center;
+  height: 300px;
+  color: #47C3B9;
+}
+
+.no-data {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  height: 300px;
+  color: #999;
+  font-size: 16px;
+}
+
+.no-data .el-icon {
+  font-size: 48px;
+  margin-bottom: 10px;
+}
+
+.loading-icon {
+  font-size: 36px;
+  margin-bottom: 10px;
+  animation: rotate 2s linear infinite;
+}
+
+@keyframes rotate {
+  from {
+    transform: rotate(0deg);
+  }
+  to {
+    transform: rotate(360deg);
+  }
+}
+
+/* 响应式布局调整 */
+@media (max-width: 992px) {
+  .horizontal-container {
+    flex-direction: column;
+  }
+  
+  .map-section, .histogram-section {
+    width: 100%;
+    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>

+ 665 - 85
src/views/User/cadmiumPrediction/EffectiveCadmiumPrediction.vue

@@ -2,108 +2,550 @@
   <div class="container">
     <!-- 顶部操作栏 -->
     <div class="toolbar">
-      <el-button class="custom-button" @click="calculate">
-  <el-icon><Document /></el-icon>
-  <span style="margin-left: 6px;">计算</span>
-</el-button>
-
-<el-button
-  class="custom-button"
-  :disabled="!result"
-  @click="captureScreenshot"
->
-  <el-icon><Camera /></el-icon>
-  <span style="margin-left: 6px;">截图</span>
-</el-button>
-
-<el-button
-  class="custom-button"
-  :disabled="!result"
-  @click="exportData"
->
-  <el-icon><Download /></el-icon>
-  <span style="margin-left: 6px;">导出</span>
-</el-button>
-
+      <!-- 文件上传区域 -->
+      <div class="upload-section">
+        <input type="file" ref="fileInput" accept=".csv" @change="handleFileUpload" style="display: none">
+        <el-button class="custom-button" @click="triggerFileUpload">
+          <el-icon class="upload-icon"><Upload /></el-icon>
+          选择CSV文件
+        </el-button>
+        <span v-if="selectedFile" class="file-name">{{ selectedFile.name }}</span>
+        <el-button 
+          class="custom-button" 
+          :loading="isCalculating" 
+          :disabled="!selectedFile" 
+          @click="calculate"
+        >
+        <el-icon class="upload-icon"><Document /></el-icon>  
+        上传并计算
+        </el-button>
+      </div>
+      <!-- 操作按钮 -->
+      <div class="action-buttons">
+        <el-button class="custom-button" :disabled="!mapBlob" @click="exportMap">
+          <el-icon class="upload-icon"><Download /></el-icon>
+          导出地图</el-button>
+        <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="!statisticsData.length" @click="exportData">
+          <el-icon class="upload-icon"><Download /></el-icon>
+          导出数据</el-button>
+      </div>
     </div>
 
-    <!-- 主体内容区,计算后显示 -->
-    <div v-if="result" class="content-area" ref="captureArea">
-      <!-- 地图区域 -->
-      <div class="map-area" ref="mapArea">
-        <div class="map-placeholder">地图区域</div>
+    <!-- 主体内容区 -->
+    <div class="content-area">
+      <!-- 地图区域 - 修改为横向布局 -->
+      <div class="horizontal-container">
+        <!-- 地图展示 -->
+        <div class="map-section">
+          <h3>有效态Cd预测地图</h3>
+          <div v-if="loadingMap" class="loading-container">
+            <el-icon class="loading-icon"><Loading /></el-icon>
+            <span>地图加载中...</span>
+          </div>
+          <img v-if="mapImageUrl && !loadingMap" :src="mapImageUrl" alt="有效态Cd预测地图" class="map-image">
+          <div v-if="!mapImageUrl && !loadingMap" class="no-data">
+            <el-icon><Picture /></el-icon>
+            <p>暂无地图数据</p>
+          </div>
+        </div>
+        
+        <!-- 直方图展示 -->
+        <div class="histogram-section">
+          <h3>有效态Cd预测直方图</h3>
+          <div v-if="loadingHistogram" class="loading-container">
+            <el-icon class="loading-icon"><Loading /></el-icon>
+            <span>直方图加载中...</span>
+          </div>
+          <img v-if="histogramImageUrl && !loadingHistogram" :src="histogramImageUrl" alt="有效态Cd预测直方图" class="histogram-image">
+          <div v-if="!histogramImageUrl && !loadingHistogram" class="no-data">
+            <el-icon><Histogram /></el-icon>
+            <p>暂无直方图数据</p>
+          </div>
+        </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>
 </template>
 
 <script>
-import html2canvas from 'html2canvas';
 import * as XLSX from 'xlsx';
 import { saveAs } from 'file-saver';
-import { Document, Camera, Download } from '@element-plus/icons-vue';
-
+import axios from 'axios';
+import * as echarts from 'echarts';
+import { 
+  Loading, Upload, Picture, Histogram, Download, Document, DataAnalysis 
+} from '@element-plus/icons-vue';
 
 export default {
-  name: 'TotalCadmiumPrediction',
+  name: 'CropCadmiumPrediction',
+  components: { 
+    Loading, Upload, Picture, Histogram, Download, Document, DataAnalysis 
+  },
   data() {
     return {
-      result: false,
-      tableData: []
+      isCalculating: false,
+      loadingMap: false,
+      loadingHistogram: false,
+      loadingStats: false,
+      statisticsData: [],
+      mapImageUrl: null,
+      histogramImageUrl: null,
+      mapBlob: null,
+      histogramBlob: null,
+      selectedFile: null,
+      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: {
-    calculate() {
-      this.result = true;
-      this.tableData = [
-        { name: '样本1', value: 10, unit: 'mg/L', description: '描述1' },
-        { name: '样本2', value: 20, unit: 'mg/L', description: '描述2' }
+    // 触发文件选择
+    triggerFileUpload() {
+      this.$refs.fileInput.click();
+    },
+    
+    // 处理文件上传
+    handleFileUpload(event) {
+      const files = event.target.files;
+      if (files && files.length > 0) {
+        this.selectedFile = files[0];
+      } else {
+        this.selectedFile = null;
+      }
+    },
+    
+    // 获取最新结果
+    async fetchLatestResults() {
+      try {
+        this.loadingMap = true;
+        this.loadingHistogram = true;
+        
+        // 获取最新地图
+        await this.fetchLatestMap();
+        
+        // 获取最新直方图
+        await this.fetchLatestHistogram();
+        
+      } catch (error) {
+        console.error('获取最新结果失败:', error);
+        this.$message.error('获取最新结果失败');
+      } finally {
+        this.loadingMap = false;
+        this.loadingHistogram = false;
+      }
+    },
+    
+    // 获取最新地图
+    async fetchLatestMap() {
+      try {
+        const response = await axios.get(
+          `https://soilgd.com:8000/api/cd-prediction/effective-cd/latest-map/${this.countyName}`,
+          { responseType: 'blob' }
+        );
+        
+        this.mapBlob = response.data;
+        this.mapImageUrl = URL.createObjectURL(this.mapBlob);
+      } catch (error) {
+        console.error('获取最新地图失败:', error);
+        this.$message.warning('获取最新地图失败,请先执行预测');
+      }
+    },
+    
+    // 获取最新直方图
+    async fetchLatestHistogram() {
+      try {
+        const response = await axios.get(
+          `https://soilgd.com:8000/api/cd-prediction/effective-cd/latest-histogram/${this.countyName}`,
+          { responseType: 'blob' }
+        );
+        
+        this.histogramBlob = response.data;
+        this.histogramImageUrl = URL.createObjectURL(this.histogramBlob);
+      } catch (error) {
+        console.error('获取最新直方图失败:', error);
+        this.$message.warning('获取最新直方图失败,请先执行预测');
+      }
+    },
+    
+    // 格式化统计数据
+    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);
     },
-    async captureScreenshot() {
-      const element = this.$refs.mapArea; // 修改这里为地图区域
-      if (!element) return;
 
+    // 修改fetchStatistics方法
+    async fetchStatistics() {
       try {
-        const canvas = await html2canvas(element);
-        const dataUrl = canvas.toDataURL('image/png');
+        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) {
+        this.$message.warning('请先选择CSV文件');
+        return;
+      }
+      
+      try {
+        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地图接口
+        const mapResponse = await axios.post(
+          'https://soilgd.com:8000/api/cd-prediction/effective-cd/generate-and-get-map',
+          formData,
+          {
+            headers: {
+              'Content-Type': 'multipart/form-data'
+            },
+            responseType: 'blob'
+          }
+        );
+        
+        // 保存地图数据
+        this.mapBlob = mapResponse.data;
+        this.mapImageUrl = URL.createObjectURL(this.mapBlob);
+        
+        // 更新后重新获取直方图和统计数据
+        await this.fetchLatestHistogram();
+        await this.fetchStatistics();
+        
+        this.$message.success('计算完成!');
+        
+      } catch (error) {
+        console.error('计算失败:', error);
+        let errorMessage = '计算失败,请重试';
+        
+        if (error.response) {
+          if (error.response.status === 400) {
+            errorMessage = '文件格式错误:' + (error.response.data.detail || '请上传正确的CSV文件');
+          } else if (error.response.status === 404) {
+            errorMessage = '不支持的县市:' + this.countyName;
+          } else if (error.response.status === 500) {
+            errorMessage = '服务器错误:' + (error.response.data.detail || '请稍后重试');
+          }
+        }
+        
+        this.$message.error(errorMessage);
+      } finally {
+        this.isCalculating = false;
+        this.loadingMap = false;
+        this.loadingHistogram = false;
+        this.loadingStats = false;
+      }
+    },
+    
+    // 导出地图
+    exportMap() {
+      if (!this.mapBlob) {
+        this.$message.warning('请先计算生成地图');
+        return;
+      }
+      
+      const link = document.createElement('a');
+      link.href = URL.createObjectURL(this.mapBlob);
+      link.download = `${this.countyName}_有效态Cd预测地图.jpg`;
+      link.click();
+      URL.revokeObjectURL(link.href);
+    },
+    
+    // 导出直方图
+    exportHistogram() {
+      if (!this.histogramBlob) {
+        this.$message.warning('请先计算生成直方图');
+        return;
+      }
+      
+      const link = document.createElement('a');
+      link.href = URL.createObjectURL(this.histogramBlob);
+      link.download = `${this.countyName}_有效态Cd预测直方图.jpg`;
+      link.click();
+      URL.revokeObjectURL(link.href);
+    },
+    
+    // 导出数据
+    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 = dataUrl;
-        link.download = '截图.png';
+        link.href = URL.createObjectURL(blob);
+        link.download = `${this.countyName}_有效Cd预测数据.csv`;
         link.click();
-      } catch (err) {
-        console.error('截图失败:', err);
+        URL.revokeObjectURL(link.href);
+        
+        this.$message.success('数据导出成功');
+      } catch (error) {
+        console.error('导出数据失败:', error);
+        this.$message.error('导出数据失败: ' + (error.response?.data?.detail || '请稍后重试'));
       }
-    },
-    exportData() {
-      const worksheet = XLSX.utils.json_to_sheet(this.tableData);
-      const workbook = XLSX.utils.book_new();
-      XLSX.utils.book_append_sheet(workbook, worksheet, '表格数据');
-      const wbout = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
-      const blob = new Blob([wbout], { type: 'application/octet-stream' });
-      saveAs(blob, '导出数据.xlsx');
     }
-  },
-  components: {
-  Document,
-  Camera,
-  Download
-},
+  }
 };
 </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;
@@ -112,10 +554,37 @@ export default {
 }
 
 .toolbar {
+  display: flex;
+  flex-direction: column;
+  gap: 15px;
+  margin-bottom: 20px;
+  padding: 15px;
+  background-color: white;
+  border-radius: 8px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+}
+
+.upload-section {
   display: flex;
   align-items: center;
+  gap: 15px;
+  padding-bottom: 15px;
+  border-bottom: 1px solid #eee;
+}
+
+.file-name {
+  flex: 1;
+  padding: 0 10px;
+  color: #666;
+  font-size: 14px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.action-buttons {
+  display: flex;
   gap: 10px;
-  margin-bottom: 20px;
 }
 
 .custom-button {
@@ -125,37 +594,148 @@ export default {
   border-radius: 155px;
   padding: 10px 20px;
   font-weight: bold;
+  display: flex;
+  align-items: center;
+}
+
+.upload-icon {
+  margin-right: 5px;
 }
 
 .content-area {
   display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+
+/* 横向布局容器 */
+.horizontal-container {
+  display: flex;
+  flex-wrap: wrap;
   gap: 20px;
+  width: 100%;
 }
 
-.map-area {
+.map-section, .histogram-section {
   flex: 1;
   min-width: 300px;
+  background-color: white;
+  border-radius: 8px;
+  padding: 15px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+  position: relative;
+  min-height: 400px;
 }
 
-.map-placeholder {
-  background-color: #cce5ff;
-  height: 400px;
+.map-image, .histogram-image {
+  width: 100%;
+  height: 100%;
+  max-height: 600px;
+  object-fit: contain;
+  border-radius: 4px;
+}
+
+.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;
   align-items: center;
   justify-content: center;
-  font-weight: bold;
-  font-size: 18px;
-  color: #003366;
+  height: 300px;
+  color: #47C3B9;
+}
+
+.no-data {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  height: 300px;
+  color: #999;
+  font-size: 16px;
+}
+
+.no-data .el-icon {
+  font-size: 48px;
+  margin-bottom: 10px;
+}
+
+.loading-icon {
+  font-size: 36px;
+  margin-bottom: 10px;
+  animation: rotate 2s linear infinite;
+}
+
+@keyframes rotate {
+  from {
+    transform: rotate(0deg);
+  }
+  to {
+    transform: rotate(360deg);
+  }
+}
+
+/* 响应式布局调整 */
+@media (max-width: 992px) {
+  .horizontal-container {
+    flex-direction: column;
+  }
+  
+  .map-section, .histogram-section {
+    width: 100%;
+    flex: none;
+  }
+}
+.model-info {
+  display: flex;
+  align-items: center;
+  gap: 15px;
+  margin: 10px 0;
+  color: #666;
+}
+
+.update-time {
+  font-size: 14px;
 }
 
-.table-area {
+.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;
 }
 
-.custom-button:disabled {
-  background-color: #cccccc !important;
-  color: #666666 !important;
-  cursor: not-allowed;
+.el-table {
+  margin-top: 20px;
 }
-</style>
+</style>

+ 363 - 78
src/views/User/cadmiumPrediction/TotalCadmiumPrediction.vue

@@ -2,36 +2,69 @@
   <div class="container">
     <!-- 顶部操作栏 -->
     <div class="toolbar">
-      <el-button class="custom-button" @click="calculate">
-  <el-icon><Document /></el-icon>
-  <span style="margin-left: 6px;">计算</span>
-</el-button>
-
-<el-button
-  class="custom-button"
-  :disabled="!result"
-  @click="captureScreenshot"
->
-  <el-icon><Camera /></el-icon>
-  <span style="margin-left: 6px;">截图</span>
-</el-button>
-
-<el-button
-  class="custom-button"
-  :disabled="!result"
-  @click="exportData"
->
-  <el-icon><Download /></el-icon>
-  <span style="margin-left: 6px;">导出</span>
-</el-button>
-
+      <!-- 文件上传区域 -->
+      <div class="upload-section">
+        <input type="file" ref="fileInput" accept=".csv" @change="handleFileUpload" style="display: none">
+        <el-button class="custom-button" @click="triggerFileUpload">
+          <el-icon class="upload-icon"><Upload /></el-icon>
+          选择CSV文件
+        </el-button>
+        <span v-if="selectedFile" class="file-name">{{ selectedFile.name }}</span>
+        <el-button 
+          class="custom-button" 
+          :loading="isCalculating" 
+          :disabled="!selectedFile" 
+          @click="calculate"
+        >
+        <el-icon class="upload-icon"><Document /></el-icon>  
+        上传并计算
+        </el-button>
+      </div>
+      <!-- 操作按钮 -->
+      <div class="action-buttons">
+        <el-button class="custom-button" :disabled="!mapBlob" @click="exportMap">
+          <el-icon class="upload-icon"><Download /></el-icon>
+          导出地图</el-button>
+        <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-icon class="upload-icon"><Download /></el-icon>
+          导出数据</el-button>
+      </div>
     </div>
 
-    <!-- 主体内容区,计算后显示 -->
-    <div v-if="result" class="content-area" ref="captureArea">
-      <!-- 地图区域 -->
-      <div class="map-area" ref="mapArea">
-        <div class="map-placeholder">地图区域</div>
+    <!-- 主体内容区 -->
+    <div class="content-area">
+      <!-- 地图区域 - 修改为横向布局 -->
+      <div class="horizontal-container">
+        <!-- 地图展示 -->
+        <div class="map-section">
+          <h3>有效态Cd预测地图</h3>
+          <div v-if="loadingMap" class="loading-container">
+            <el-icon class="loading-icon"><Loading /></el-icon>
+            <span>地图加载中...</span>
+          </div>
+          <img v-if="mapImageUrl && !loadingMap" :src="mapImageUrl" alt="有效态Cd预测地图" class="map-image">
+          <div v-if="!mapImageUrl && !loadingMap" class="no-data">
+            <el-icon><Picture /></el-icon>
+            <p>暂无地图数据</p>
+          </div>
+        </div>
+        
+        <!-- 直方图展示 -->
+        <div class="histogram-section">
+          <h3>有效态Cd预测直方图</h3>
+          <div v-if="loadingHistogram" class="loading-container">
+            <el-icon class="loading-icon"><Loading /></el-icon>
+            <span>直方图加载中...</span>
+          </div>
+          <img v-if="histogramImageUrl && !loadingHistogram" :src="histogramImageUrl" alt="有效态Cd预测直方图" class="histogram-image">
+          <div v-if="!histogramImageUrl && !loadingHistogram" class="no-data">
+            <el-icon><Histogram /></el-icon>
+            <p>暂无直方图数据</p>
+          </div>
+        </div>
       </div>
 
       <!-- 表格区域 -->
@@ -49,57 +82,215 @@
 </template>
 
 <script>
-import html2canvas from 'html2canvas';
 import * as XLSX from 'xlsx';
 import { saveAs } from 'file-saver';
-import { Document, Camera, Download } from '@element-plus/icons-vue';
-
+import axios from 'axios';
+import { Loading, Upload, Picture, Histogram, Download, Document } from '@element-plus/icons-vue';
 
 export default {
-  name: 'TotalCadmiumPrediction',
+  name: 'EffectiveCadmiumPrediction',
+  components: { Loading, Upload, Picture, Histogram, Download, Document },
   data() {
     return {
-      result: false,
-      tableData: []
+      isCalculating: false,
+      loadingMap: false,
+      loadingHistogram: false,
+      tableData: [],
+      mapImageUrl: null,
+      histogramImageUrl: null,
+      mapBlob: null,
+      histogramBlob: null,
+      selectedFile: null,
+      countyName: '乐昌市' // 默认县市名称
     };
   },
+  mounted() {
+    // 组件挂载时获取最新数据
+    this.fetchLatestResults();
+  },
   methods: {
-    calculate() {
-      this.result = true;
-      this.tableData = [
-        { name: '样本1', value: 10, unit: 'mg/L', description: '描述1' },
-        { name: '样本2', value: 20, unit: 'mg/L', description: '描述2' }
-      ];
+    // 触发文件选择
+    triggerFileUpload() {
+      this.$refs.fileInput.click();
     },
-    async captureScreenshot() {
-      const element = this.$refs.mapArea; // 修改这里为地图区域
-      if (!element) return;
-
+    
+    // 处理文件上传
+    handleFileUpload(event) {
+      const files = event.target.files;
+      if (files && files.length > 0) {
+        this.selectedFile = files[0];
+      } else {
+        this.selectedFile = null;
+      }
+    },
+    
+    // 获取最新结果
+    async fetchLatestResults() {
+      try {
+        this.loadingMap = true;
+        this.loadingHistogram = true;
+        
+        // 获取最新地图
+        await this.fetchLatestMap();
+        
+        // 获取最新直方图
+        await this.fetchLatestHistogram();
+        
+      } catch (error) {
+        console.error('获取最新结果失败:', error);
+        this.$message.error('获取最新结果失败');
+      } finally {
+        this.loadingMap = false;
+        this.loadingHistogram = false;
+      }
+    },
+    
+    // 获取最新地图
+    async fetchLatestMap() {
+      try {
+        const response = await axios.get(
+          `https://soilgd.com:8000/api/cd-prediction/effective-cd/latest-map/${this.countyName}`,
+          { responseType: 'blob' }
+        );
+        
+        this.mapBlob = response.data;
+        this.mapImageUrl = URL.createObjectURL(this.mapBlob);
+      } catch (error) {
+        console.error('获取最新地图失败:', error);
+        this.$message.warning('获取最新地图失败,请先执行预测');
+      }
+    },
+    
+    // 获取最新直方图
+    async fetchLatestHistogram() {
+      try {
+        const response = await axios.get(
+          `https://soilgd.com:8000/api/cd-prediction/effective-cd/latest-histogram/${this.countyName}`,
+          { responseType: 'blob' }
+        );
+        
+        this.histogramBlob = response.data;
+        this.histogramImageUrl = URL.createObjectURL(this.histogramBlob);
+      } catch (error) {
+        console.error('获取最新直方图失败:', error);
+        this.$message.warning('获取最新直方图失败,请先执行预测');
+      }
+    },
+    
+    // 上传并计算
+    async calculate() {
+      if (!this.selectedFile) {
+        this.$message.warning('请先选择CSV文件');
+        return;
+      }
+      
       try {
-        const canvas = await html2canvas(element);
-        const dataUrl = canvas.toDataURL('image/png');
-        const link = document.createElement('a');
-        link.href = dataUrl;
-        link.download = '截图.png';
-        link.click();
-      } catch (err) {
-        console.error('截图失败:', err);
+        this.isCalculating = true;
+        this.loadingMap = true;
+        this.loadingHistogram = true;
+        
+        // 创建FormData
+        const formData = new FormData();
+        formData.append('county_name', this.countyName);
+        formData.append('data_file', this.selectedFile);
+        
+        // 调用有效态Cd地图接口
+        const mapResponse = await axios.post(
+          'https://soilgd.com:8000/api/cd-prediction/effective-cd/generate-and-get-map',
+          formData,
+          {
+            headers: {
+              'Content-Type': 'multipart/form-data'
+            },
+            responseType: 'blob'
+          }
+        );
+        
+        // 保存地图数据
+        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' }
+        ];
+        
+        this.$message.success('计算完成!');
+        
+      } catch (error) {
+        console.error('计算失败:', error);
+        let errorMessage = '计算失败,请重试';
+        
+        if (error.response) {
+          // 处理不同错误状态码
+          if (error.response.status === 400) {
+            errorMessage = '文件格式错误:' + (error.response.data.detail || '请上传正确的CSV文件');
+          } else if (error.response.status === 404) {
+            errorMessage = '不支持的县市:' + this.countyName;
+          } else if (error.response.status === 500) {
+            errorMessage = '服务器错误:' + (error.response.data.detail || '请稍后重试');
+          }
+        }
+        
+        this.$message.error(errorMessage);
+      } finally {
+        this.isCalculating = false;
+        this.loadingMap = false;
+        this.loadingHistogram = false;
+      }
+    },
+    
+    // 导出地图
+    exportMap() {
+      if (!this.mapBlob) {
+        this.$message.warning('请先计算生成地图');
+        return;
       }
+      
+      const link = document.createElement('a');
+      link.href = URL.createObjectURL(this.mapBlob);
+      link.download = `${this.countyName}_有效态Cd预测地图.jpg`;
+      link.click();
+      URL.revokeObjectURL(link.href);
     },
+    
+    // 导出直方图
+    exportHistogram() {
+      if (!this.histogramBlob) {
+        this.$message.warning('请先计算生成直方图');
+        return;
+      }
+      
+      const link = document.createElement('a');
+      link.href = URL.createObjectURL(this.histogramBlob);
+      link.download = `${this.countyName}_有效态Cd预测直方图.jpg`;
+      link.click();
+      URL.revokeObjectURL(link.href);
+    },
+    
+    // 导出数据
     exportData() {
-      const worksheet = XLSX.utils.json_to_sheet(this.tableData);
+      if (!this.tableData.length) {
+        this.$message.warning('暂无数据可导出');
+        return;
+      }
+      
       const workbook = XLSX.utils.book_new();
-      XLSX.utils.book_append_sheet(workbook, worksheet, '表格数据');
-      const wbout = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
-      const blob = new Blob([wbout], { type: 'application/octet-stream' });
-      saveAs(blob, '导出数据.xlsx');
+      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`);
     }
   },
-  components: {
-  Document,
-  Camera,
-  Download
-},
+  beforeDestroy() {
+    if (this.mapImageUrl) URL.revokeObjectURL(this.mapImageUrl);
+    if (this.histogramImageUrl) URL.revokeObjectURL(this.histogramImageUrl);
+  }
 };
 </script>
 
@@ -112,10 +303,37 @@ export default {
 }
 
 .toolbar {
+  display: flex;
+  flex-direction: column;
+  gap: 15px;
+  margin-bottom: 20px;
+  padding: 15px;
+  background-color: white;
+  border-radius: 8px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+}
+
+.upload-section {
   display: flex;
   align-items: center;
+  gap: 15px;
+  padding-bottom: 15px;
+  border-bottom: 1px solid #eee;
+}
+
+.file-name {
+  flex: 1;
+  padding: 0 10px;
+  color: #666;
+  font-size: 14px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.action-buttons {
+  display: flex;
   gap: 10px;
-  margin-bottom: 20px;
 }
 
 .custom-button {
@@ -125,37 +343,104 @@ export default {
   border-radius: 155px;
   padding: 10px 20px;
   font-weight: bold;
+  display: flex;
+  align-items: center;
+}
+
+.upload-icon {
+  margin-right: 5px;
 }
 
 .content-area {
   display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+
+/* 横向布局容器 */
+.horizontal-container {
+  display: flex;
+  flex-wrap: wrap;
   gap: 20px;
+  width: 100%;
 }
 
-.map-area {
+.map-section, .histogram-section {
   flex: 1;
-  min-width: 300px;
+  min-width: 300px; /* 最小宽度,确保在小屏幕上也能正常显示 */
+  background-color: white;
+  border-radius: 8px;
+  padding: 15px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+  position: relative;
+  min-height: 400px;
+}
+
+.map-image, .histogram-image {
+  width: 100%;
+  height: 100%;
+  max-height: 600px;
+  object-fit: contain;
+  border-radius: 4px;
 }
 
-.map-placeholder {
-  background-color: #cce5ff;
-  height: 400px;
+.table-area {
+  width: 100%;
+  background-color: white;
   border-radius: 8px;
+  padding: 15px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+  margin-top: 20px;
+}
+
+.loading-container {
   display: flex;
+  flex-direction: column;
   align-items: center;
   justify-content: center;
-  font-weight: bold;
-  font-size: 18px;
-  color: #003366;
+  height: 300px;
+  color: #47C3B9;
 }
 
-.custom-button:disabled {
-  background-color: #cccccc !important;
-  color: #666666 !important;
-  cursor: not-allowed;
+.no-data {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  height: 300px;
+  color: #999;
+  font-size: 16px;
 }
 
-.table-area {
-  flex: 1;
+.no-data .el-icon {
+  font-size: 48px;
+  margin-bottom: 10px;
+}
+
+.loading-icon {
+  font-size: 36px;
+  margin-bottom: 10px;
+  animation: rotate 2s linear infinite;
+}
+
+@keyframes rotate {
+  from {
+    transform: rotate(0deg);
+  }
+  to {
+    transform: rotate(360deg);
+  }
+}
+
+/* 响应式布局调整 */
+@media (max-width: 992px) {
+  .horizontal-container {
+    flex-direction: column;
+  }
+  
+  .map-section, .histogram-section {
+    width: 100%;
+    flex: none;
+  }
 }
-</style>
+</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>

+ 0 - 23
src/views/User/heavyMetalFluxCalculation/inputFluxCalculation/agriculturalProductInput.vue

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

+ 0 - 197
src/views/User/heavyMetalFluxCalculation/inputFluxCalculation/irrigationWater.vue

@@ -1,197 +0,0 @@
-<template>
-  <div class="page-container">
-    <!-- 上半部分:地图 + 柱状图 -->
-    <div class="top-content">
-      <div class="map-module">
-        <div class="map-box">地图模块</div>
-      </div>
-      <div class="chart-module">
-        <div ref="chartRef" class="chart-box"></div>
-      </div>
-    </div>
-
-    <!-- 中间间距 -->
-    <div class="middle-gap"></div>
-
-    <!-- 下半部分:表格 -->
-    <div class="table-module">
-      <el-table
-        :data="tableData"
-        style="width: 100%"
-        max-height="100%"
-        border
-        stripe
-        class="scrollable-table"
-      >
-        <el-table-column prop="name" label="项目" width="150" />
-        <el-table-column prop="value" label="数值" width="150" />
-        <el-table-column prop="unit" label="单位" width="100" />
-      </el-table>
-    </div>
-  </div>
-</template>
-
-<script setup>
-import { ref, onMounted, watch, computed } from 'vue';
-import * as echarts from 'echarts';
-
-const form = ref({
-  nutrients: 10,
-  heavyMetals: 5,
-  phValue: 7,
-  yieldQuality: 60,
-  pestRate: 3,
-});
-
-const chartRef = ref(null);
-let chartInstance = null;
-
-const initChart = () => {
-  if (!chartRef.value) return;
-  chartInstance = echarts.init(chartRef.value);
-
-  const option = {
-    title: {
-      text: '传统耕种风险比例',
-      left: 'center',
-    },
-    tooltip: {
-      trigger: 'item',
-    },
-    legend: {
-      orient: 'vertical',
-      left: 'left',
-    },
-    series: [
-      {
-        name: '风险因素',
-        type: 'pie',
-        radius: '50%',
-        data: [
-          { value: form.value.nutrients, name: '养分含量' },
-          { value: form.value.heavyMetals, name: '重金属污染' },
-          { value: form.value.phValue, name: 'pH 值' },
-          { value: form.value.yieldQuality, name: '产量或品质' },
-          { value: form.value.pestRate, name: '病虫害率' },
-        ],
-        emphasis: {
-          itemStyle: {
-            shadowBlur: 10,
-            shadowOffsetX: 0,
-            shadowColor: 'rgba(0,0,0,0.5)',
-          },
-        },
-      },
-    ],
-  };
-  chartInstance.setOption(option);
-};
-
-const updateChart = () => {
-  if (!chartInstance) return;
-  chartInstance.setOption({
-    series: [
-      {
-        data: [
-          { value: form.value.nutrients, name: '养分含量' },
-          { value: form.value.heavyMetals, name: '重金属污染' },
-          { value: form.value.phValue, name: 'pH 值' },
-          { value: form.value.yieldQuality, name: '产量或品质' },
-          { value: form.value.pestRate, name: '病虫害率' },
-        ],
-      },
-    ],
-  });
-};
-
-onMounted(() => {
-  initChart();
-});
-
-watch(form, () => {
-  updateChart();
-}, { deep: true });
-
-const tableData = computed(() => [
-  { name: '养分含量', value: form.value.nutrients, unit: '%' },
-  { name: '重金属污染', value: form.value.heavyMetals, unit: '%' },
-  { name: 'pH 值', value: form.value.phValue, unit: '' },
-  { name: '产量或品质', value: form.value.yieldQuality, unit: '%' },
-  { name: '病虫害率', value: form.value.pestRate, unit: '%' },
-]);
-</script>
-
-<style scoped>
-.page-container {
-  display: flex;
-  flex-direction: column;
-  height: 100vh; /* 整屏高度 */
-  padding: 20px;
-  box-sizing: border-box;
-  background-color: #f5f7fa;
-}
-
-/* 上半部分占47%高度 */
-.top-content {
-  display: flex;
-  height: 47%;
-  gap: 20px;
-}
-
-/* 中间间距 */
-.middle-gap {
-  height: 10px;
-}
-
-/* 下半部分表格占50%高度 */
-.table-module {
-  height: 50%;
-  background: white;
-  border-radius: 12px;
-  box-shadow: 0 0 10px rgb(0 0 0 / 0.1);
-  overflow: auto;
-}
-
-.map-module {
-  flex: 1;
-  background: white;
-  border-radius: 12px;
-  box-shadow: 0 0 10px rgb(0 0 0 / 0.1);
-  display: flex;
-  justify-content: center;
-  align-items: center;
-}
-
-.map-box {
-  width: 100%;
-  height: 100%;
-  border: 2px dashed #bbb;
-  border-radius: 12px;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  font-weight: bold;
-  color: #999;
-}
-
-.chart-module {
-  flex: 1;
-  background: white;
-  border-radius: 12px;
-  box-shadow: 0 0 10px rgb(0 0 0 / 0.1);
-  display: flex;
-  justify-content: center;
-  align-items: center;
-}
-
-.chart-box {
-  width: 100%;
-  height: 100%;
-  min-height: 100%;
-}
-
-.scrollable-table {
-  width: 100%;
-  height: 100%;
-}
-</style>

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

+ 4 - 2
src/views/User/heavyMetalFluxCalculation/inputFluxCalculation/atmosphericDryWetDeposition.vue → src/views/User/hmInFlux/grainRemoval/samplingDesc1.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="">
-    2
+    
   </div>
 </template>
 
@@ -19,5 +19,7 @@ export default {
 </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>

+ 295 - 123
src/views/User/selectCityAndCounty.vue

@@ -1,43 +1,62 @@
 <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>
-
-    <div class="districts" v-if="selectedCity">
-      <div
-        class="district-card"
-        v-for="district in districts[selectedCity]"
-        :key="district.name"
-        @click="goToKanBan(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 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>
         </div>
+      </div>
 
-        <!-- 右侧箭头图标 -->
-        <el-icon class="arrow-icon">
-          <arrow-right />
-        </el-icon>
+      <div class="button-container">
+        <el-button
+          type="primary"
+          class="go-button"
+          :disabled="selectedDistricts.length === 0"
+          @click="goToKanBan"
+        >
+          跳转到重金属输出通量
+        </el-button>
       </div>
     </div>
   </div>
@@ -56,44 +75,44 @@ export default {
     const router = useRouter();
 
     const selectedCity = ref("韶关");
+    const selectedDistricts = ref([]);
     const cities = [{ name: "韶关" }, { name: "河池" }, { name: "腾冲" }];
 
     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" }
       ],
     };
 
@@ -103,18 +122,47 @@ export default {
 
     const selectCity = (cityName) => {
       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 = (districtName) => {
-      localStorage.setItem("selectedDistrict", districtName);
-      router.push({ name: "shuJuKanBan", query: { district: districtName } });
+    const goToKanBan = () => {
+      localStorage.setItem("selectedDistricts", JSON.stringify(selectedDistricts.value));
+      const userType = localStorage.getItem("userType"); // 获取用户类型
+      if (userType === "admin") {
+        router.push({ name: "parameterConfig" }); // 管理员跳转到 parameterConfig
+      } else {
+        router.push({
+          name: "shuJuKanBan", // 普通用户跳转到 shuJuKanBan
+          query: { districts: selectedDistricts.value.join(",") },
+        });
+      }
     };
 
     return {
       selectedCity,
       cities,
       districts,
+      selectedDistricts,
       selectCity,
+      toggleDistrict,
+      isDistrictSelected,
       goToKanBan,
     };
   },
@@ -122,127 +170,251 @@ export default {
 </script>
 
 <style scoped>
+/* 左侧背景图容器 */
+.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 {
-  text-align: center;
-  background: linear-gradient(180deg, #a581ff, #b3e5fc);
-  padding: 20px;
+  display: flex;
+  min-height: 100vh;
+  position: relative;
 }
 
-.city-selection.full-page {
-  min-height: 100vh; /* 改成 min-height 使内容多时撑开 */
+.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;
 }
 
+/* 卡片容器布局 */
+.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: 88px;
   display: flex;
   flex-wrap: wrap;
-  justify-content: center;
-  gap: 20px;
+  justify-content: flex-start;
+  gap: 30px;
+  padding-bottom: 20px;
 }
 
+/* 每个卡片容器 */
+.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: #333;
+  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; /* 确保按钮在底部 */
 }
 
-/* 以下是 body 和 html 的全局样式,去掉白边 */
-:global(html), :global(body) {
-  height: 100%;
-  margin: 0;
-  padding: 0;
-  background: linear-gradient(180deg, #a581ff, #b3e5fc);
+.go-button {
+  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;
+}
+
+/* 滚动条样式 */
+.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>

+ 325 - 227
src/views/login/loginView.vue

@@ -1,121 +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-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') }}
+
+        <!-- 提交登录按钮和注册链接 -->
+        <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"
+      >
+        <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); // 控制显示登录或注册表单
-const userType = ref("user"); // 当前用户类型,默认为普通用户
+// 控制是否显示登录表单
+const isLogin = ref(true);
+// 当前用户类型(用户或管理员)
+const userType = ref("user");
 
+// 登录表单数据模型
 const form = reactive({
   name: "",
   password: "",
 });
 
+// 注册表单数据模型
 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" : "中文";
 });
@@ -123,12 +188,14 @@ 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") : t("login.switchToUser");
+  return userType.value === "user"
+    ? t("login.switchToAdmin")
+    : t("login.switchToUser");
 });
 
 // 切换用户类型
@@ -141,39 +208,44 @@ const toggleForm = () => {
   isLogin.value = !isLogin.value;
 };
 
-// 密码登录提交
+// 提交登录请求
 const onSubmit = async () => {
   if (!formRef.value) return;
   formRef.value.validate(async (valid: boolean) => {
     if (!valid) return;
     loading.value = true;
     try {
-      const res = await login({ name: form.name, password: form.password, userType: userType.value });
+      const res = await login({
+        name: form.name,
+        password: form.password,
+        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'));
-        const redirectPath =
-          typeof route.query.redirect === "string" ? route.query.redirect : "/";
-        router.push(redirectPath);
+        ElMessage.success(i18n.global.t("login.loginSuccess"));
+        // 根据用户类型跳转到不同页面
+        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'));
+        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'));
+        ElMessage.error(i18n.global.t("login.loginFailed"));
       }
     } finally {
       loading.value = false;
@@ -181,14 +253,15 @@ 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'));
+      ElMessage.error(i18n.global.t("register.passwordMismatch"));
       return;
     }
 
@@ -198,20 +271,18 @@ const onRegister = async () => {
         password: registerForm.password,
       });
       if (res.data.success) {
-        ElMessage.success(i18n.global.t('register.registerSuccess'));
-        toggleForm(); // 切换回登录表单
+        ElMessage.success(i18n.global.t("register.registerSuccess"));
+        toggleForm(); // 注册成功后切换回登录表单
       } else {
-        ElMessage.error(res.data.message || i18n.global.t('register.registerFailed'));
+        ElMessage.error(
+          res.data.message || i18n.global.t("register.registerFailed")
+        );
       }
     } 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'));
+        ElMessage.error(i18n.global.t("register.registerFailed"));
       }
     } finally {
       loading.value = false;
@@ -219,189 +290,216 @@ const onRegister = async () => {
   });
 };
 
-// 验证规则
+// 登录表单验证规则
 const rules = reactive({
   name: [
-    { required: true, message: i18n.global.t('validation.usernameRequired'), trigger: 'blur' }
+    {
+      required: true,
+      message: i18n.global.t("validation.usernameRequired"),
+      trigger: "blur",
+    },
   ],
   password: [
-    { required: true, message: i18n.global.t('validation.passwordRequired'), trigger: 'blur' },
-    { min: 3, max: 16, message: i18n.global.t('validation.passwordLength'), trigger: 'blur' }
-  ]
+    {
+      required: true,
+      message: i18n.global.t("validation.passwordRequired"),
+      trigger: "blur",
+    },
+    {
+      min: 3,
+      max: 16,
+      message: i18n.global.t("validation.passwordLength"),
+      trigger: "blur",
+    },
+  ],
 });
 
+// 注册表单验证规则
 const registerRules = reactive({
   name: [
-    { required: true, message: i18n.global.t('validation.usernameRequired'), trigger: 'blur' }
+    {
+      required: true,
+      message: i18n.global.t("validation.usernameRequired"),
+      trigger: "blur",
+    },
   ],
   password: [
-    { required: true, message: i18n.global.t('validation.passwordRequired'), trigger: 'blur' },
-    { min: 3, max: 16, message: i18n.global.t('validation.passwordLength'), trigger: 'blur' }
+    {
+      required: true,
+      message: i18n.global.t("validation.passwordRequired"),
+      trigger: "blur",
+    },
+    {
+      min: 3,
+      max: 16,
+      message: i18n.global.t("validation.passwordLength"),
+      trigger: "blur",
+    },
   ],
   confirmPassword: [
-    { required: true, message: i18n.global.t('validation.confirmPasswordRequired'), trigger: 'blur' },
     {
-      validator: (rule: any, value: string, callback: (error?: Error) => void) => {
+      required: true,
+      message: i18n.global.t("validation.confirmPasswordRequired"),
+      trigger: "blur",
+    },
+    {
+      validator: (
+        rule: any,
+        value: string,
+        callback: (error?: Error) => void
+      ) => {
         if (value !== registerForm.password) {
-          callback(new Error(i18n.global.t('validation.passwordMismatch')));
+          callback(new Error(i18n.global.t("validation.passwordMismatch")));
         } else {
           callback();
         }
       },
-      trigger: 'blur'
-    }
-  ]
+      trigger: "blur",
+    },
+  ],
 });
 </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;
 }
-</style>
 
+.text-link-wrapper {
+  text-align: center;
+  margin-left: -295px;
+  margin-top: 120px;
+}
+
+:deep(.el-input) {
+  max-width: 560px;
+  width: 100%;
+}
+</style>

+ 571 - 57
src/views/menu/tencentMapView.vue

@@ -1,17 +1,28 @@
 <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" />
         显示韶关市评估单元
@@ -27,14 +38,48 @@
         </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)
@@ -61,20 +106,160 @@ const state = reactive({
 let soilTypeLayer = null
 let geoJSONLayer; 
 let currentInfoWindow = null;
-let surveyDataLayer = ref(null);
-let multiPolygon; 
+const surveyDataLayer = ref(null); // 保持响应式引用
+let multiPolygon;
+const districtLayers = ref(new Map()) // 存储区县图层
+const combinedSurveyFeatures = ref([]); // 存储原始数据
+const currentSurveyFilter = ref([]); // 当前选中区域
+const isLoading = ref(false)
 
 const categoryColors = { // 分类颜色配置
   '优先保护类': '#00C853', // 绿色
   '安全利用类': '#FFD600', // 黄色
-  '严格管控类': '#D50000' // 红色
+  '严格管控类': '#D50000', // 红色
+  '其他': '#CCCCCC', // 灰色
+  '农产品样品': '#4CAF50',    // 绿色
+  '土壤样品': '#2196F3'      // 蓝色
 };
 
+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) => {
@@ -182,6 +367,7 @@ const initData = () => {
 // 初始化地图
 const initMap = async () => {
   try {
+   isLoading.value = true
     await loadSDK()
     
     map = new TMap.value.Map(mapContainer.value, {
@@ -204,9 +390,10 @@ const initMap = async () => {
     //   map: map,
     //   styles: { default: defaultStyle }
     // })
-    const geojsonData = await loadGeoJSON('/data/单元格.geojson');
+    const geojsonData = await loadGeoJSON('https://soilgd.com:8000/api/vector/export/all?table_name=unit_ceil');
     initMapWithGeoJSON(geojsonData, map);
     await initSurveyDataLayer(map);
+    filterSurveyDataLayer(currentSurveyFilter.value)
     // 绑定点击事件
     // map.on('click', handleMapClick)
     // markersLayer.on('click', handleMarkerClick)
@@ -219,6 +406,8 @@ const initMap = async () => {
     updateMarkers()
   } catch (err) {
     error.value = err.message
+  } finally {
+    isLoading.value = false
   }
 }
 
@@ -489,6 +678,129 @@ 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) {
@@ -499,7 +811,7 @@ function initMapWithGeoJSON(geojsonData, map) {
   geoJSONLayer = new TMap.value.vector.GeoJSONLayer({
     map: map,
     data: geojsonData,
-    zIndex: 1,
+    zIndex: 10,
     polygonStyle: new TMap.value.PolygonStyle({ // 必须用 PolygonStyle 类实例
       color: 'rgba(255, 0, 0, 0.25)', 
       showBorder: true,
@@ -514,7 +826,7 @@ function initMapWithGeoJSON(geojsonData, map) {
   // 高亮选中图层
   const highlightLayer = new TMap.value.MultiPolygon({
         map,
-        zIndex: 2,
+        zIndex: 20,
         styles: {
           highlight: new TMap.value.PolygonStyle({ // 注意要改为 PolygonStyle
             color: 'rgba(0, 123, 255, 0.5)',      // 半透明蓝色填充
@@ -547,40 +859,56 @@ function initMapWithGeoJSON(geojsonData, map) {
 // 加载调查数据并初始化图层
 const initSurveyDataLayer = async (map) => {
   try {
-    // 加载GeoJSON数据
-    const surveyData = await loadGeoJSON('/data/调查数据.geojson');
+    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);
     
-    // 创建分类样式
-    const pointStyles = Object.keys(categoryColors).map(category => ({
+    // 保存原始数据用于过滤
+    combinedSurveyFeatures.value = features;
+
+    // 合并颜色配置(添加默认分类)
+    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(categoryColors[category]) // 生成圆形图标
+        src: createColoredCircle(mergedCategoryColors[category])
       })
     }));
 
-    // 初始化图层
-    surveyDataLayer = new TMap.value.MultiMarker({
+    // 初始化图层(处理缺失属性)
+    surveyDataLayer.value = new TMap.value.MultiMarker({
       map: map,
       styles: Object.assign({}, ...pointStyles.map(s => ({ [s.id]: s.style }))),
-      geometries: surveyData.features.map(feature => ({
-        id: feature.properties.ID,
-        styleId: feature.properties.H_XTFX,
+      geometries: combinedSurveyFeatures.value.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[1],
           feature.geometry.coordinates[0]
         ),
         properties: {
           ...feature.properties,
-          
+          // 强制添加H_XTFX字段保证数据一致性
+          H_XTFX: feature.properties.H_XTFX|| '其他' 
         }
       }))
     });
 
     // 添加点击事件
-    surveyDataLayer.on('click', (event) => {
+    surveyDataLayer.value.on('click', (event) => {
       const prop = event.geometry.properties;
       if (currentInfoWindow) currentInfoWindow.close();
       currentInfoWindow = new TMap.value.InfoWindow({
@@ -595,7 +923,14 @@ const initSurveyDataLayer = async (map) => {
       });
     });
   } catch (error) {
-    console.error('调查数据加载失败:', error);
+    console.error("调查数据加载失败:", error);
+    // 添加详细错误日志
+    console.groupCollapsed("[错误详情]");
+    console.error("错误对象:", error);
+    console.trace("调用堆栈");
+    console.groupEnd();
+  } finally {
+    isLoading.value = false
   }
 };
 
@@ -636,7 +971,7 @@ const createColoredCircle = (color) => {
       return;
     }
     if (surveyDataLayer) {
-      surveyDataLayer.setVisible(state.showSurveyData);
+      surveyDataLayer.value.setVisible(state.showSurveyData);
     }
   };
 
@@ -664,6 +999,7 @@ onMounted(async () => {
     await loadSDK()
     initData()
     await initMap()
+    await initBaseLayers()
   } catch (err) {
     error.value = err.message
   }
@@ -679,41 +1015,96 @@ 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) {
-    surveyDataLayer.destroy();
-    surveyDataLayer = null;
+  if (surveyDataLayer.value) {
+    surveyDataLayer.value.setMap(null);
+    surveyDataLayer.value.destroy();
   }
+  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 {
-  width: 100%;
-  height: 100vh !important;
-  min-height: 600px;
+  flex: 1;
+  height: calc(100vh - 48px); /* 对应新的工具栏高度 */
+  position: relative;
+  background: #f5f5f7;
 }
-
 .control-panel {
   position: fixed;
-  top: 24px;
+  top: 80px;  /* 下移避开工具栏 */
   right: 24px;
   background: rgba(255, 255, 255, 0.95);
   padding: 16px;
@@ -794,19 +1185,34 @@ onBeforeUnmount(() => {
   box-shadow: 0 4px 12px rgba(56, 118, 255, 0.3);
 }
 
-/* 新增加载动画 */
-@keyframes spin {
-  0% { transform: rotate(0deg); }
-  100% { transform: rotate(360deg); }
+/* 新增加载提示样式 */
+.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;
 }
 
 .loading-spinner {
-  width: 18px;
-  height: 18px;
-  border: 2px solid rgba(255, 255, 255, 0.3);
-  border-top-color: white;
+  width: 50px;
+  height: 50px;
+  border: 5px solid #f3f3f3;
+  border-top: 5px solid #3876ff;
   border-radius: 50%;
-  animation: spin 0.8s linear infinite;
+  animation: spin 1s linear infinite;
+  margin-bottom: 16px;
+}
+
+@keyframes spin {
+  0% { transform: rotate(0deg); }
+  100% { transform: rotate(360deg); }
 }
 
 /* 响应式调整 */
@@ -910,10 +1316,118 @@ 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>

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 46 - 0
vitest.config.ts.timestamp-1753017273303-815a308c407d1.mjs


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 46 - 0
vitest.config.ts.timestamp-1753017288268-2bb6d1b228918.mjs


+ 27 - 0
韶关市河流水系图.qmd

@@ -0,0 +1,27 @@
+<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
+<qgis version="3.40.8-Bratislava">
+  <identifier></identifier>
+  <parentidentifier></parentidentifier>
+  <language></language>
+  <type></type>
+  <title></title>
+  <abstract></abstract>
+  <links/>
+  <dates/>
+  <fees></fees>
+  <encoding></encoding>
+  <crs>
+    <spatialrefsys nativeFormat="Wkt">
+      <wkt></wkt>
+      <proj4></proj4>
+      <srsid>0</srsid>
+      <srid>0</srid>
+      <authid></authid>
+      <description></description>
+      <projectionacronym></projectionacronym>
+      <ellipsoidacronym></ellipsoidacronym>
+      <geographicflag>false</geographicflag>
+    </spatialrefsys>
+  </crs>
+  <extent/>
+</qgis>

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно