瀏覽代碼

合并当前前端代码,补充Cd预测模块,移植旧版本地图

yangtaodemon 16 小時之前
父節點
當前提交
e9ed828374

+ 3 - 1
.eslintrc-auto-import.json

@@ -71,6 +71,8 @@
     "watchPostEffect": true,
     "watchSyncEffect": true,
     "ElMessage": true,
-    "ElM": true
+    "ElM": true,
+    "Slot": true,
+    "Slots": true
   }
 }

+ 1 - 1
auto-imports.d.ts

@@ -68,6 +68,6 @@ declare global {
 // for type re-export
 declare global {
   // @ts-ignore
-  export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
+  export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
   import('vue')
 }

+ 1 - 12
components.d.ts

@@ -2,6 +2,7 @@
 // @ts-nocheck
 // Generated by unplugin-vue-components
 // Read more: https://github.com/vuejs/core/pull/3399
+// biome-ignore lint: disable
 export {}
 
 /* prettier-ignore */
@@ -11,7 +12,6 @@ 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']
@@ -32,22 +32,15 @@ declare module 'vue' {
     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']
     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']
@@ -57,11 +50,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 - 0
src/components/layout/menuItems.ts

@@ -129,6 +129,12 @@ import {
       icon: PieChart,
       tab: 'cadmiumPrediction'
     },
+    {
+      index: '/CropCadmiumPrediction',
+      label: '土壤镉作物态含量预测',
+      icon: PieChart,
+      tab: 'cadmiumPrediction'
+    },
     {
       index: '/cropRiskAssessment',
       label: '水稻镉污染风险',

+ 10 - 1
src/router/index.ts

@@ -100,7 +100,7 @@ const routes = [
       {
         path: "mapView",
         name: "mapView",
-        component: () => import("@/views/User/mapView/leafletMapView.vue"), // 修复路径
+        component: () => import("@/views/User/mapView/tencentMapView.vue"), // 修复路径
         meta: { title: "地图展示" },
       },
       {
@@ -119,6 +119,15 @@ const routes = [
           ), // 修复路径
         meta: { title: "土壤镉有效态含量预测" },
       },
+      {
+        path: "CropCadmiumPrediction",
+        name: "CropCadmiumPrediction",
+        component: () =>
+          import(
+            "@/views/User/cadmiumPrediction/CropCadmiumPrediction.vue"
+          ), // 修复路径
+        meta: { title: "土壤镉作物态含量预测" },
+      },
       {
         path: "cropRiskAssessment",
         name: "cropRiskAssessment",

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

@@ -0,0 +1,298 @@
+<template>
+  <div class="container">
+    <!-- 顶部操作栏 -->
+    <div class="toolbar">
+      <el-button class="custom-button" :loading="isCalculating" @click="calculate">计算</el-button>
+      <el-button class="custom-button" :disabled="isCalculating || !mapBlob" @click="exportMap">导出地图</el-button>
+      <el-button class="custom-button" :disabled="isCalculating || !histogramBlob" @click="exportHistogram">导出直方图</el-button>
+      <el-button class="custom-button" :disabled="isCalculating || !tableData.length" @click="exportData">导出数据</el-button>
+    </div>
+
+    <!-- 主体内容区,计算后显示 -->
+    <div v-if="result" class="content-area">
+      <!-- 地图区域 - 现在包含两个图片展示区 -->
+      <div class="map-area">
+        <div class="map-container">
+          <!-- 地图展示 -->
+          <div class="map-section">
+            <h3>作物态Cd预测地图</h3>
+            <img v-if="mapImageUrl" :src="mapImageUrl" alt="作物态Cd预测地图" class="map-image">
+             <div v-if="loadingMap" class="loading-container">
+              <el-icon class="loading-icon"><Loading /></el-icon>
+              <span>地图加载中...</span>
+            </div>
+          </div>
+          
+          <!-- 直方图展示 -->
+          <div class="histogram-section">
+            <h3>作物态Cd预测直方图</h3>
+            <img v-if="histogramImageUrl" :src="histogramImageUrl" alt="作物态Cd预测直方图" class="histogram-image">
+             <div v-if="loadingMap" class="loading-container">
+              <el-icon class="loading-icon"><Loading /></el-icon>
+              <span>直方图加载中...</span>
+            </div>
+          </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>
+    </div>
+  </div>
+</template>
+
+<script>
+import * as XLSX from 'xlsx';
+import { saveAs } from 'file-saver';
+import axios from 'axios';
+import { Loading } from '@element-plus/icons-vue';
+
+export default {
+  name: 'CropCadmiumPrediction',
+  components: { Loading },
+  data() {
+    return {
+      isCalculating: false,
+      loadingMap: false,
+      loadingHistogram: false,
+      result: false,
+      tableData: [],
+      mapImageUrl: null,       // 存储地图图片 URL
+      histogramImageUrl: null, // 存储直方图图片 URL
+      mapBlob: null,           // 存储地图原始Blob数据
+      histogramBlob: null      // 存储直方图原始Blob数据
+    };
+  },
+  methods: {
+    async calculate() {
+      try {
+        // 重置状态
+        this.isCalculating = true;
+        this.loadingMap = true;
+        this.loadingHistogram = true;
+        this.result = false;
+        this.mapImageUrl = null;
+        this.histogramImageUrl = null;
+        this.mapBlob = null;
+        this.histogramBlob = null;
+        this.tableData = [];
+        
+        // 调用有效态Cd地图接口
+        const mapResponse = await axios.post('https://soilgd.com:8000/api/cd-prediction/crop-cd/generate-and-get-map', {}, {
+          responseType: 'blob'
+        });
+        
+        // 调用有效态Cd直方图接口
+        const histogramResponse = await axios.post('https://soilgd.com:8000/api/cd-prediction/crop-cd/generate-and-get-histogram', {}, {
+          responseType: 'blob'
+        });
+        
+        // 保存原始Blob数据用于导出
+        this.mapBlob = mapResponse.data;
+        this.histogramBlob = histogramResponse.data;
+        
+        // 为图片数据创建 URL
+        this.mapImageUrl = URL.createObjectURL(this.mapBlob);
+        this.histogramImageUrl = URL.createObjectURL(this.histogramBlob);
+        
+        // 更新表格数据
+        this.result = true;
+        this.tableData = [
+          { name: '样本1', value: 10, unit: 'mg/L', description: '描述1' },
+          { name: '样本2', value: 20, unit: 'mg/L', description: '描述2' }
+        ];
+        
+      } catch (error) {
+        console.error('获取地图或直方图失败:', error);
+        this.$message.error('获取预测结果失败,请重试');
+      } 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 = '作物态Cd预测地图.jpg';
+      link.style.display = 'none';
+      document.body.appendChild(link);
+      
+      // 触发点击事件
+      link.click();
+      
+      // 清理临时URL和链接元素
+      setTimeout(() => {
+        document.body.removeChild(link);
+        URL.revokeObjectURL(link.href);
+      }, 100);
+    },
+    
+    // 导出直方图方法 - 修复:确保链接元素被添加到文档中
+    exportHistogram() {
+      if (!this.histogramBlob) {
+        this.$message.warning('请先计算生成直方图');
+        return;
+      }
+      
+      // 创建下载链接并添加到文档中
+      const link = document.createElement('a');
+      link.href = URL.createObjectURL(this.histogramBlob);
+      link.download = '作物态Cd预测直方图.jpg';
+      link.style.display = 'none';
+      document.body.appendChild(link);
+      
+      // 触发点击事件
+      link.click();
+      
+      // 清理临时URL和链接元素
+      setTimeout(() => {
+        document.body.removeChild(link);
+        URL.revokeObjectURL(link.href);
+      }, 100);
+    },
+    
+    // 导出数据方法
+    exportData() {
+      // 创建工作簿
+      const workbook = XLSX.utils.book_new();
+      
+      // 创建数据表
+      const worksheet = XLSX.utils.json_to_sheet(this.tableData);
+      
+      // 将工作表添加到工作簿
+      XLSX.utils.book_append_sheet(workbook, worksheet, '作物态Cd数据');
+      
+      // 生成Excel文件
+      const excelBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
+      const excelData = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
+      
+      // 保存文件
+      saveAs(excelData, 'crop-cd-data.xlsx');
+    }
+  },
+  beforeDestroy() {
+    // 组件销毁时释放图片 URL 防止内存泄漏
+    if (this.mapImageUrl) URL.revokeObjectURL(this.mapImageUrl);
+    if (this.histogramImageUrl) URL.revokeObjectURL(this.histogramImageUrl);
+  }
+};
+</script>
+
+<style scoped>
+/* 样式保持不变 */
+.container {
+  padding: 20px;
+  background-color: #f5f7fa;
+  min-height: 100vh;
+  box-sizing: border-box;
+}
+
+.toolbar {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  margin-bottom: 20px;
+}
+
+.custom-button {
+  background-color: #47C3B9 !important;
+  color: #DCFFFA !important;
+  border: none;
+  border-radius: 155px;
+  padding: 10px 20px;
+  font-weight: bold;
+}
+
+.content-area {
+  display: flex;
+  gap: 20px;
+}
+
+.map-area {
+  flex: 1;
+  min-width: 300px;
+}
+
+.map-container {
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+
+.map-section, .histogram-section {
+  background-color: white;
+  border-radius: 8px;
+  padding: 15px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+  position: relative;
+}
+
+.map-image, .histogram-image {
+  width: 100%;
+  max-height: 600px;
+  object-fit: contain;
+  border-radius: 4px;
+}
+
+.map-placeholder {
+  background-color: #cce5ff;
+  height: 200px;
+  border-radius: 8px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-weight: bold;
+  font-size: 16px;
+  color: #003366;
+}
+
+.table-area {
+  flex: 1;
+  background-color: white;
+  border-radius: 8px;
+  padding: 15px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+}
+
+.loading-container {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  height: 200px;
+  color: #47C3B9;
+}
+
+.loading-icon {
+  font-size: 36px;
+  margin-bottom: 10px;
+  animation: rotate 2s linear infinite;
+}
+
+@keyframes rotate {
+  from {
+    transform: rotate(0deg);
+  }
+  to {
+    transform: rotate(360deg);
+  }
+}
+</style>

+ 205 - 68
src/views/User/cadmiumPrediction/EffectiveCadmiumPrediction.vue

@@ -2,36 +2,37 @@
   <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>
-
+      <el-button class="custom-button" :loading="isCalculating" @click="calculate">计算</el-button>
+      <el-button class="custom-button" :disabled="isCalculating || !mapBlob" @click="exportMap">导出地图</el-button>
+      <el-button class="custom-button" :disabled="isCalculating || !histogramBlob" @click="exportHistogram">导出直方图</el-button>
+      <el-button class="custom-button" :disabled="isCalculating || !tableData.length" @click="exportData">导出数据</el-button>
     </div>
 
     <!-- 主体内容区,计算后显示 -->
-    <div v-if="result" class="content-area" ref="captureArea">
-      <!-- 地图区域 -->
-      <div class="map-area" ref="mapArea">
-        <div class="map-placeholder">地图区域</div>
+    <div v-if="result" class="content-area">
+      <!-- 地图区域 - 现在包含两个图片展示区 -->
+      <div class="map-area">
+        <div class="map-container">
+          <!-- 地图展示 -->
+          <div class="map-section">
+            <h3>有效态Cd预测地图</h3>
+            <img v-if="mapImageUrl" :src="mapImageUrl" alt="有效态Cd预测地图" class="map-image">
+             <div v-if="loadingMap" class="loading-container">
+              <el-icon class="loading-icon"><Loading /></el-icon>
+              <span>地图加载中...</span>
+            </div>
+          </div>
+          
+          <!-- 直方图展示 -->
+          <div class="histogram-section">
+            <h3>有效态Cd预测直方图</h3>
+            <img v-if="histogramImageUrl" :src="histogramImageUrl" alt="有效态Cd预测直方图" class="histogram-image">
+             <div v-if="loadingMap" class="loading-container">
+              <el-icon class="loading-icon"><Loading /></el-icon>
+              <span>直方图加载中...</span>
+            </div>
+          </div>
+        </div>
       </div>
 
       <!-- 表格区域 -->
@@ -49,61 +50,154 @@
 </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 } from '@element-plus/icons-vue';
 
 export default {
-  name: 'TotalCadmiumPrediction',
+  name: 'EffectiveCadmiumPrediction',
+  components: { Loading },
   data() {
     return {
+      isCalculating: false,
+      loadingMap: false,
+      loadingHistogram: false,
       result: false,
-      tableData: []
+      tableData: [],
+      mapImageUrl: null,       // 存储地图图片 URL
+      histogramImageUrl: null, // 存储直方图图片 URL
+      mapBlob: null,           // 存储地图原始Blob数据
+      histogramBlob: null      // 存储直方图原始Blob数据
     };
   },
   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' }
-      ];
-    },
-    async captureScreenshot() {
-      const element = this.$refs.mapArea; // 修改这里为地图区域
-      if (!element) return;
-
+    async calculate() {
       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;
+        this.result = false;
+        this.mapImageUrl = null;
+        this.histogramImageUrl = null;
+        this.mapBlob = null;
+        this.histogramBlob = null;
+        this.tableData = [];
+        
+        // 调用有效态Cd地图接口
+        const mapResponse = await axios.post('https://soilgd.com:8000/api/cd-prediction/effective-cd/generate-and-get-map', {}, {
+          responseType: 'blob'
+        });
+        
+        // 调用有效态Cd直方图接口
+        const histogramResponse = await axios.post('https://soilgd.com:8000/api/cd-prediction/effective-cd/generate-and-get-histogram', {}, {
+          responseType: 'blob'
+        });
+        
+        // 保存原始Blob数据用于导出
+        this.mapBlob = mapResponse.data;
+        this.histogramBlob = histogramResponse.data;
+        
+        // 为图片数据创建 URL
+        this.mapImageUrl = URL.createObjectURL(this.mapBlob);
+        this.histogramImageUrl = URL.createObjectURL(this.histogramBlob);
+        
+        // 更新表格数据
+        this.result = true;
+        this.tableData = [
+          { name: '样本1', value: 10, unit: 'mg/L', description: '描述1' },
+          { name: '样本2', value: 20, unit: 'mg/L', description: '描述2' }
+        ];
+        
+      } catch (error) {
+        console.error('获取地图或直方图失败:', error);
+        this.$message.error('获取预测结果失败,请重试');
+      } 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 = '有效态Cd预测地图.jpg';
+      link.style.display = 'none';
+      document.body.appendChild(link);
+      
+      // 触发点击事件
+      link.click();
+      
+      // 清理临时URL和链接元素
+      setTimeout(() => {
+        document.body.removeChild(link);
+        URL.revokeObjectURL(link.href);
+      }, 100);
+    },
+    
+    // 导出直方图方法 - 修复:确保链接元素被添加到文档中
+    exportHistogram() {
+      if (!this.histogramBlob) {
+        this.$message.warning('请先计算生成直方图');
+        return;
       }
+      
+      // 创建下载链接并添加到文档中
+      const link = document.createElement('a');
+      link.href = URL.createObjectURL(this.histogramBlob);
+      link.download = '有效态Cd预测直方图.jpg';
+      link.style.display = 'none';
+      document.body.appendChild(link);
+      
+      // 触发点击事件
+      link.click();
+      
+      // 清理临时URL和链接元素
+      setTimeout(() => {
+        document.body.removeChild(link);
+        URL.revokeObjectURL(link.href);
+      }, 100);
     },
+    
+    // 导出数据方法
     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');
+      
+      // 创建数据表
+      const worksheet = XLSX.utils.json_to_sheet(this.tableData);
+      
+      // 将工作表添加到工作簿
+      XLSX.utils.book_append_sheet(workbook, worksheet, '有效态Cd数据');
+      
+      // 生成Excel文件
+      const excelBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
+      const excelData = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
+      
+      // 保存文件
+      saveAs(excelData, 'effective-cd-data.xlsx');
     }
   },
-  components: {
-  Document,
-  Camera,
-  Download
-},
+  beforeDestroy() {
+    // 组件销毁时释放图片 URL 防止内存泄漏
+    if (this.mapImageUrl) URL.revokeObjectURL(this.mapImageUrl);
+    if (this.histogramImageUrl) URL.revokeObjectURL(this.histogramImageUrl);
+  }
 };
 </script>
 
 <style scoped>
+/* 样式保持不变 */
 .container {
   padding: 20px;
   background-color: #f5f7fa;
@@ -137,25 +231,68 @@ export default {
   min-width: 300px;
 }
 
+.map-container {
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+
+.map-section, .histogram-section {
+  background-color: white;
+  border-radius: 8px;
+  padding: 15px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+  position: relative;
+}
+
+.map-image, .histogram-image {
+  width: 100%;
+  max-height: 600px;
+  object-fit: contain;
+  border-radius: 4px;
+}
+
 .map-placeholder {
   background-color: #cce5ff;
-  height: 400px;
+  height: 200px;
   border-radius: 8px;
   display: flex;
   align-items: center;
   justify-content: center;
   font-weight: bold;
-  font-size: 18px;
+  font-size: 16px;
   color: #003366;
 }
 
 .table-area {
   flex: 1;
+  background-color: white;
+  border-radius: 8px;
+  padding: 15px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+}
+
+.loading-container {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  height: 200px;
+  color: #47C3B9;
+}
+
+.loading-icon {
+  font-size: 36px;
+  margin-bottom: 10px;
+  animation: rotate 2s linear infinite;
 }
 
-.custom-button:disabled {
-  background-color: #cccccc !important;
-  color: #666666 !important;
-  cursor: not-allowed;
+@keyframes rotate {
+  from {
+    transform: rotate(0deg);
+  }
+  to {
+    transform: rotate(360deg);
+  }
 }
-</style>
+</style>

+ 207 - 70
src/views/User/cadmiumPrediction/TotalCadmiumPrediction.vue

@@ -2,36 +2,37 @@
   <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>
-
+      <el-button class="custom-button" :loading="isCalculating" @click="calculate">计算</el-button>
+      <el-button class="custom-button" :disabled="isCalculating || !mapBlob" @click="exportMap">导出地图</el-button>
+      <el-button class="custom-button" :disabled="isCalculating || !histogramBlob" @click="exportHistogram">导出直方图</el-button>
+      <el-button class="custom-button" :disabled="isCalculating || !tableData.length" @click="exportData">导出数据</el-button>
     </div>
 
     <!-- 主体内容区,计算后显示 -->
-    <div v-if="result" class="content-area" ref="captureArea">
-      <!-- 地图区域 -->
-      <div class="map-area" ref="mapArea">
-        <div class="map-placeholder">地图区域</div>
+    <div v-if="result" class="content-area">
+      <!-- 地图区域 - 现在包含两个图片展示区 -->
+      <div class="map-area">
+        <div class="map-container">
+          <!-- 地图展示 -->
+          <div class="map-section">
+            <h3>有效态Cd预测地图</h3>
+            <img v-if="mapImageUrl" :src="mapImageUrl" alt="有效态Cd预测地图" class="map-image">
+             <div v-if="loadingMap" class="loading-container">
+              <el-icon class="loading-icon"><Loading /></el-icon>
+              <span>地图加载中...</span>
+            </div>
+          </div>
+          
+          <!-- 直方图展示 -->
+          <div class="histogram-section">
+            <h3>有效态Cd预测直方图</h3>
+            <img v-if="histogramImageUrl" :src="histogramImageUrl" alt="有效态Cd预测直方图" class="histogram-image">
+             <div v-if="loadingMap" class="loading-container">
+              <el-icon class="loading-icon"><Loading /></el-icon>
+              <span>直方图加载中...</span>
+            </div>
+          </div>
+        </div>
       </div>
 
       <!-- 表格区域 -->
@@ -49,61 +50,154 @@
 </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 } from '@element-plus/icons-vue';
 
 export default {
-  name: 'TotalCadmiumPrediction',
+  name: 'EffectiveCadmiumPrediction',
+  components: { Loading },
   data() {
     return {
+      isCalculating: false,
+      loadingMap: false,
+      loadingHistogram: false,
       result: false,
-      tableData: []
+      tableData: [],
+      mapImageUrl: null,       // 存储地图图片 URL
+      histogramImageUrl: null, // 存储直方图图片 URL
+      mapBlob: null,           // 存储地图原始Blob数据
+      histogramBlob: null      // 存储直方图原始Blob数据
     };
   },
   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' }
-      ];
-    },
-    async captureScreenshot() {
-      const element = this.$refs.mapArea; // 修改这里为地图区域
-      if (!element) return;
-
+    async calculate() {
       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;
+        this.result = false;
+        this.mapImageUrl = null;
+        this.histogramImageUrl = null;
+        this.mapBlob = null;
+        this.histogramBlob = null;
+        this.tableData = [];
+        
+        // 调用有效态Cd地图接口
+        const mapResponse = await axios.post('https://soilgd.com:8000/api/cd-prediction/effective-cd/generate-and-get-map', {}, {
+          responseType: 'blob'
+        });
+        
+        // 调用有效态Cd直方图接口
+        const histogramResponse = await axios.post('https://soilgd.com:8000/api/cd-prediction/effective-cd/generate-and-get-histogram', {}, {
+          responseType: 'blob'
+        });
+        
+        // 保存原始Blob数据用于导出
+        this.mapBlob = mapResponse.data;
+        this.histogramBlob = histogramResponse.data;
+        
+        // 为图片数据创建 URL
+        this.mapImageUrl = URL.createObjectURL(this.mapBlob);
+        this.histogramImageUrl = URL.createObjectURL(this.histogramBlob);
+        
+        // 更新表格数据
+        this.result = true;
+        this.tableData = [
+          { name: '样本1', value: 10, unit: 'mg/L', description: '描述1' },
+          { name: '样本2', value: 20, unit: 'mg/L', description: '描述2' }
+        ];
+        
+      } catch (error) {
+        console.error('获取地图或直方图失败:', error);
+        this.$message.error('获取预测结果失败,请重试');
+      } 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 = '有效态Cd预测地图.jpg';
+      link.style.display = 'none';
+      document.body.appendChild(link);
+      
+      // 触发点击事件
+      link.click();
+      
+      // 清理临时URL和链接元素
+      setTimeout(() => {
+        document.body.removeChild(link);
+        URL.revokeObjectURL(link.href);
+      }, 100);
+    },
+    
+    // 导出直方图方法 - 修复:确保链接元素被添加到文档中
+    exportHistogram() {
+      if (!this.histogramBlob) {
+        this.$message.warning('请先计算生成直方图');
+        return;
       }
+      
+      // 创建下载链接并添加到文档中
+      const link = document.createElement('a');
+      link.href = URL.createObjectURL(this.histogramBlob);
+      link.download = '有效态Cd预测直方图.jpg';
+      link.style.display = 'none';
+      document.body.appendChild(link);
+      
+      // 触发点击事件
+      link.click();
+      
+      // 清理临时URL和链接元素
+      setTimeout(() => {
+        document.body.removeChild(link);
+        URL.revokeObjectURL(link.href);
+      }, 100);
     },
+    
+    // 导出数据方法
     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');
+      
+      // 创建数据表
+      const worksheet = XLSX.utils.json_to_sheet(this.tableData);
+      
+      // 将工作表添加到工作簿
+      XLSX.utils.book_append_sheet(workbook, worksheet, '有效态Cd数据');
+      
+      // 生成Excel文件
+      const excelBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
+      const excelData = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
+      
+      // 保存文件
+      saveAs(excelData, 'effective-cd-data.xlsx');
     }
   },
-  components: {
-  Document,
-  Camera,
-  Download
-},
+  beforeDestroy() {
+    // 组件销毁时释放图片 URL 防止内存泄漏
+    if (this.mapImageUrl) URL.revokeObjectURL(this.mapImageUrl);
+    if (this.histogramImageUrl) URL.revokeObjectURL(this.histogramImageUrl);
+  }
 };
 </script>
 
 <style scoped>
+/* 样式保持不变 */
 .container {
   padding: 20px;
   background-color: #f5f7fa;
@@ -137,25 +231,68 @@ export default {
   min-width: 300px;
 }
 
+.map-container {
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+
+.map-section, .histogram-section {
+  background-color: white;
+  border-radius: 8px;
+  padding: 15px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+  position: relative;
+}
+
+.map-image, .histogram-image {
+  width: 100%;
+  max-height: 600px;
+  object-fit: contain;
+  border-radius: 4px;
+}
+
 .map-placeholder {
   background-color: #cce5ff;
-  height: 400px;
+  height: 200px;
   border-radius: 8px;
   display: flex;
   align-items: center;
   justify-content: center;
   font-weight: bold;
-  font-size: 18px;
+  font-size: 16px;
   color: #003366;
 }
 
-.custom-button:disabled {
-  background-color: #cccccc !important;
-  color: #666666 !important;
-  cursor: not-allowed;
-}
-
 .table-area {
   flex: 1;
+  background-color: white;
+  border-radius: 8px;
+  padding: 15px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+}
+
+.loading-container {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  height: 200px;
+  color: #47C3B9;
+}
+
+.loading-icon {
+  font-size: 36px;
+  margin-bottom: 10px;
+  animation: rotate 2s linear infinite;
+}
+
+@keyframes rotate {
+  from {
+    transform: rotate(0deg);
+  }
+  to {
+    transform: rotate(360deg);
+  }
 }
-</style>
+</style>

+ 0 - 2
src/views/User/mapView/tencentMapView.vue

@@ -32,8 +32,6 @@
 
 <script setup>
 import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
-import axios from 'axios'
-import markerIcon from '@/assets/dot.png' 
 import html2canvas from 'html2canvas'
 
 const isExporting = ref(false)

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