Browse Source

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

qw 1 week ago
parent
commit
327fe93a15
62 changed files with 7332 additions and 3898 deletions
  1. 3 1
      .eslintrc-auto-import.json
  2. 0 8
      components.d.ts
  3. 3 3
      index.html
  4. BIN
      src/assets/samplingequipment1.png
  5. BIN
      src/assets/samplingequipment2.png
  6. BIN
      src/assets/samplingequipment3.png
  7. BIN
      src/assets/samplingsite1.png
  8. BIN
      src/assets/samplingsite2.png
  9. BIN
      src/assets/samplingsite3.png
  10. 23 0
      src/views/Admin/Wheat Cadmium Pollution Risk Model Management/WheatRiskModel.vue
  11. 23 0
      src/views/Admin/dataManagement/Administrative Area Data Management/AdminRegionData.vue
  12. 23 0
      src/views/Admin/dataManagement/Climate Information Data Management/ClimateInfoData.vue
  13. 23 0
      src/views/Admin/dataManagement/Crop Heavy Metal Sampling Data Management/CropHeavyMetalData.vue
  14. 8 0
      src/views/Admin/dataManagement/Geographic Environmental Information Management/GeographicEnvInfoData.vue
  15. 23 0
      src/views/Admin/dataManagement/Land Use Type Data Management/LandUseTypeData.vue
  16. 23 0
      src/views/Admin/dataManagement/Soil Acidification Sampling Data Management/SoilAcidificationData.vue
  17. 552 0
      src/views/Admin/dataManagement/Soil Acidification and Acid Reduction Data Management/soilAcidReductionData.vue
  18. 536 0
      src/views/Admin/dataManagement/Soil Acidification and Acid Reduction Data Management/soilAcidificationData.vue
  19. 23 0
      src/views/Admin/dataManagement/Soil Heavy Metal Sampling Data Management/SoilHeavyMetalData.vue
  20. 23 0
      src/views/Admin/dataManagement/SoilAssessmentUnitData/SoilAssessmentUnitData.vue
  21. 353 0
      src/views/Admin/modelManagement/AcidReductionModel/ModelSelection.vue
  22. 326 0
      src/views/Admin/modelManagement/AcidReductionModel/ModelTrain.vue
  23. 148 0
      src/views/Admin/modelManagement/AcidReductionModel/thres.vue
  24. 23 0
      src/views/Admin/modelManagement/Rice Cadmium Pollution Risk Model Management/RiceRiskModel.vue
  25. 32 0
      src/views/Admin/modelManagement/Soil Cadmium Content Prediction Model Management/CadmiumPredictionModel.vue
  26. 23 0
      src/views/Admin/modelManagement/Vegetable Cadmium Pollution Risk Model Management/VegetableRiskModel.vue
  27. 479 0
      src/views/User/heavyMetalFluxCalculation/inputFluxCalculation/atmospheredata/atmCompanytencentMap.vue
  28. 94 0
      src/views/User/heavyMetalFluxCalculation/inputFluxCalculation/atmospheredata/atmcompany.vue
  29. 218 0
      src/views/User/heavyMetalFluxCalculation/inputFluxCalculation/atmospheredata/atmcompanyline.vue
  30. 559 0
      src/views/User/heavyMetalFluxCalculation/inputFluxCalculation/atmospheredata/atmtencentMap.vue
  31. 673 0
      src/views/User/heavyMetalFluxCalculation/inputFluxCalculation/irrigationWater.vue
  32. 99 0
      src/views/User/heavyMetalFluxCalculation/inputFluxCalculation/waterdata/point.vue
  33. 188 0
      src/views/User/heavyMetalFluxCalculation/inputFluxCalculation/waterdata/rivermessage.vue
  34. 425 0
      src/views/User/heavyMetalFluxCalculation/inputFluxCalculation/waterdata/rivertencentMapView.vue
  35. 87 0
      src/views/User/heavyMetalFluxCalculation/inputFluxCalculation/waterdata/riverwaterassay.vue
  36. 1045 0
      src/views/User/heavyMetalFluxCalculation/inputFluxCalculation/waterdata/tencentMapView.vue
  37. 222 0
      src/views/User/heavyMetalFluxCalculation/inputFluxCalculation/waterdata/waterassaydata1.vue
  38. 290 0
      src/views/User/heavyMetalFluxCalculation/inputFluxCalculation/waterdata/waterassaydata2.vue
  39. 197 0
      src/views/User/heavyMetalFluxCalculation/inputFluxCalculation/waterdata/waterassaydata3.vue
  40. 340 0
      src/views/User/heavyMetalFluxCalculation/inputFluxCalculation/waterdata/waterassaydata4.vue
  41. 221 0
      src/views/User/heavyMetalFluxCalculation/inputFluxCalculation/waterdata/waterdataline.vue
  42. 1 1
      src/views/User/selectCityAndCounty.vue
  43. 2 2
      src/views/api.vue
  44. 0 385
      src/views/menu/AcidNeutralizationModel.vue
  45. 0 359
      src/views/menu/Calculation.vue
  46. 0 176
      src/views/menu/IntroUpdateModal.vue
  47. 0 193
      src/views/menu/Introduce.vue
  48. 0 95
      src/views/menu/IntroductionUpdate.vue
  49. 0 315
      src/views/menu/ModelIterationVisualization.vue
  50. 0 17
      src/views/menu/Overview.vue
  51. 0 17
      src/views/menu/ResearchFindings.vue
  52. 0 218
      src/views/menu/RichTextEditor.vue
  53. 0 41
      src/views/menu/SoilAcidReductionIterativeEvolution.vue
  54. 0 41
      src/views/menu/SoilAcidificationIterativeEvolution.vue
  55. 0 18
      src/views/menu/SoilPro.vue
  56. 0 17
      src/views/menu/Unit.vue
  57. 0 77
      src/views/menu/leafletMapView.vue
  58. 0 474
      src/views/menu/loginView.vue
  59. 0 1433
      src/views/menu/tencentMapView.vue
  60. BIN
      structure.txt
  61. 1 2
      tsconfig.app.json
  62. 0 5
      vite.config.ts

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

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

+ 0 - 8
components.d.ts

@@ -16,10 +16,8 @@ declare module 'vue' {
     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']
     ElDropdown: typeof import('element-plus/es')['ElDropdown']
     ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
     ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
@@ -29,20 +27,14 @@ declare module 'vue' {
     ElIcon: typeof import('element-plus/es')['ElIcon']
     ElImage: typeof import('element-plus/es')['ElImage']
     ElInput: typeof import('element-plus/es')['ElInput']
-    ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
     ElMain: typeof import('element-plus/es')['ElMain']
     ElMenu: typeof import('element-plus/es')['ElMenu']
     ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
-    ElOption: typeof import('element-plus/es')['ElOption']
     ElRow: typeof import('element-plus/es')['ElRow']
     ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
-    ElSelect: typeof import('element-plus/es')['ElSelect']
     ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
-    ElTable: typeof import('element-plus/es')['ElTable']
-    ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
     ElTabPane: typeof import('element-plus/es')['ElTabPane']
     ElTabs: typeof import('element-plus/es')['ElTabs']
-    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']

+ 3 - 3
index.html

@@ -1,9 +1,9 @@
 <!DOCTYPE html>
 <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" />
+    <meta charset="UTF-8">
+    <link rel="icon" href="/logo.ico">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>区域土壤重金属评估</title>
   </head>
   <body>

BIN
src/assets/samplingequipment1.png


BIN
src/assets/samplingequipment2.png


BIN
src/assets/samplingequipment3.png


BIN
src/assets/samplingsite1.png


BIN
src/assets/samplingsite2.png


BIN
src/assets/samplingsite3.png


+ 23 - 0
src/views/Admin/Wheat Cadmium Pollution Risk Model Management/WheatRiskModel.vue

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

+ 23 - 0
src/views/Admin/dataManagement/Administrative Area Data Management/AdminRegionData.vue

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

+ 23 - 0
src/views/Admin/dataManagement/Climate Information Data Management/ClimateInfoData.vue

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

+ 23 - 0
src/views/Admin/dataManagement/Crop Heavy Metal Sampling Data Management/CropHeavyMetalData.vue

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

+ 8 - 0
src/views/Admin/dataManagement/Geographic Environmental Information Management/GeographicEnvInfoData.vue

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

+ 23 - 0
src/views/Admin/dataManagement/Land Use Type Data Management/LandUseTypeData.vue

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

+ 23 - 0
src/views/Admin/dataManagement/Soil Acidification Sampling Data Management/SoilAcidificationData.vue

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

+ 552 - 0
src/views/Admin/dataManagement/Soil Acidification and Acid Reduction Data Management/soilAcidReductionData.vue

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

+ 536 - 0
src/views/Admin/dataManagement/Soil Acidification and Acid Reduction Data Management/soilAcidificationData.vue

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

+ 23 - 0
src/views/Admin/dataManagement/Soil Heavy Metal Sampling Data Management/SoilHeavyMetalData.vue

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

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

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

+ 353 - 0
src/views/Admin/modelManagement/AcidReductionModel/ModelSelection.vue

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

+ 326 - 0
src/views/Admin/modelManagement/AcidReductionModel/ModelTrain.vue

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

+ 148 - 0
src/views/Admin/modelManagement/AcidReductionModel/thres.vue

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

+ 23 - 0
src/views/Admin/modelManagement/Rice Cadmium Pollution Risk Model Management/RiceRiskModel.vue

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

+ 32 - 0
src/views/Admin/modelManagement/Soil Cadmium Content Prediction Model Management/CadmiumPredictionModel.vue

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

+ 23 - 0
src/views/Admin/modelManagement/Vegetable Cadmium Pollution Risk Model Management/VegetableRiskModel.vue

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

+ 479 - 0
src/views/User/heavyMetalFluxCalculation/inputFluxCalculation/atmospheredata/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>

+ 94 - 0
src/views/User/heavyMetalFluxCalculation/inputFluxCalculation/atmospheredata/atmcompany.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>

+ 218 - 0
src/views/User/heavyMetalFluxCalculation/inputFluxCalculation/atmospheredata/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>

+ 559 - 0
src/views/User/heavyMetalFluxCalculation/inputFluxCalculation/atmospheredata/atmtencentMap.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>

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

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

+ 99 - 0
src/views/User/heavyMetalFluxCalculation/inputFluxCalculation/waterdata/point.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>

+ 188 - 0
src/views/User/heavyMetalFluxCalculation/inputFluxCalculation/waterdata/rivermessage.vue

@@ -0,0 +1,188 @@
+<template>
+  <div>
+    <router-view></router-view> <!-- 关键:子路由渲染位置 -->
+  </div>
+  <div class="container">
+    <div class="main-content">
+      <section id="sampling" class="section">
+        <h3>采样过程</h3>
+        
+        <!-- 采样设备图片:3张并排展示 -->
+        <div class="image-container">
+          <div class="image-row">
+            <div class="image-item">
+              <img :src="samplingDeviceImg1" alt="灌溉水采样设备1">
+              <div class="image-caption">图3-1 灌溉水采样设备</div>
+            </div>
+            <div class="image-item">
+              <img :src="samplingDeviceImg2" alt="灌溉水采样设备2">
+              <div class="image-caption">图3-2 灌溉水采样设备</div>
+            </div>
+            <div class="image-item">
+              <img :src="samplingDeviceImg3" alt="灌溉水采样设备3">
+              <div class="image-caption">图3-3 灌溉水采样设备</div>
+            </div>
+          </div>
+        </div>
+
+        <h4>采样容器均为500mL的白色聚乙烯瓶,采样体积均为500mL,采样过程在不同天气条件下进行,主要天气状况包括多云、阴天和小雨,采样点周边环境主要为河流,只有少数样品采集于水渠或瀑布区域。</h4>
+        <br>
+        <h4>绝大多数样品状态为无色、无沉淀、无味、无悬浮物,只有少量样品稍显浑浊并含有沉淀物。为了保证样品的完整性和数据的准确性,采样后的保存方式包括了冷藏、避光、确保标签完好以及采取有效的减震措施,以避免运输过程中的振动和损坏。</h4>
+
+        <!-- 工作人员采样现场:3张并排展示 -->
+        <div class="image-container">
+          <div class="image-row">
+            <div class="image-item">
+              <img :src="workerSamplingImg1" alt="工作人员采样现场1">
+              <div class="image-caption">图4-1 工作人员采样现场</div>
+            </div>
+            <div class="image-item">
+              <img :src="workerSamplingImg2" alt="工作人员采样现场2">
+              <div class="image-caption">图4-2 工作人员采样现场</div>
+            </div>
+            <div class="image-item">
+              <img :src="workerSamplingImg3" alt="工作人员采样现场3">
+              <div class="image-caption">图4-3 工作人员采样现场</div>
+            </div>
+          </div>
+        </div>
+      </section>
+    </div>
+    
+    <footer>
+      <p>韶关市环境科学研究院 &copy; 2024 | 水质研究报告</p>
+    </footer>
+  </div>
+</template>
+
+<script setup>
+import { ref } from 'vue';
+
+// 导入所有图片(确保文件名和路径完全匹配!)
+import samplingDeviceImg1 from '@/assets/samplingequipment1.png';
+import samplingDeviceImg2 from '@/assets/samplingequipment2.png';
+import samplingDeviceImg3 from '@/assets/samplingequipment3.png';
+import workerSamplingImg1 from '@/assets/samplingsite1.png';
+import workerSamplingImg2 from '@/assets/samplingsite2.png';
+import workerSamplingImg3 from '@/assets/samplingsite3.png';
+import { onMounted } from 'vue'
+import { useRoute } from 'vue-router'
+
+const route = useRoute()
+
+onMounted(() => {
+  console.log('当前路由参数:', route.params)
+  console.log('匹配的子路由:', route.matched)
+})
+</script>
+
+<style scoped>
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+  font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
+}
+
+body {
+  background-color: #f5f9fc;
+  color: #333;
+  line-height: 1.6;
+  padding: 20px;
+}
+
+.container {
+  max-width: 1200px;
+  margin: 0 auto;
+  padding: 20px 0;
+}
+
+.main-content {
+  width: 100%;
+  background: white;
+  border-radius: 12px;
+  padding: 30px;
+  box-shadow: 0 5px 20px rgba(0, 0, 0, 0.08);
+}
+
+.section {
+  margin-bottom: 40px;
+  padding-bottom: 30px;
+}
+
+h3 {
+  color: #1e5799;
+  font-size: 1.6rem;
+  margin-bottom: 20px;
+  position: relative;
+  padding-left: 25px;
+}
+
+h3:before {
+  content: "";
+  position: absolute;
+  left: 0;
+  top: 50%;
+  transform: translateY(-50%);
+  width: 15px;
+  height: 15px;
+  background: #2989d8;
+  border-radius: 4px;
+}
+
+/* 缩小段落字体 */
+p {
+  font-size: 0.9rem; /* 原来可能是1rem,现在缩小 */
+  color: #444;
+  line-height: 1.8;
+  text-align: justify;
+  margin-bottom: 15px;
+}
+
+/* 图片容器:放大图片 + 并排布局 */
+.image-container {
+  background-color: #f8fbff;
+  border: 1px solid #dbe9f5;
+  border-radius: 10px;
+  margin: 20px auto;
+  padding: 20px;
+  text-align: center;
+  max-width: 90%;
+}
+
+/* 图片行:并排展示 */
+.image-row {
+  display: flex;
+  gap: 20px;
+  justify-content: center;
+  flex-wrap: wrap; /* 屏幕小的时候自动换行 */
+}
+
+/* 单个图片项:放大显示 */
+.image-item {
+  flex: 1;
+  min-width: 250px; /* 保证最小宽度 */
+}
+
+.image-item img {
+  width: 100%; /* 占满容器,自然放大 */
+  height: auto; /* 保持比例 */
+  border-radius: 8px;
+}
+
+.image-caption {
+  font-style: italic;
+  color: #666;
+  margin-top: 8px;
+  font-size: 0.85rem;
+}
+
+footer {
+  text-align: center;
+  margin-top: 30px;
+  padding-top: 20px;
+  color: #777;
+  font-size: 0.9rem;
+  border-top: 1px solid #eee;
+}
+</style>

+ 425 - 0
src/views/User/heavyMetalFluxCalculation/inputFluxCalculation/waterdata/rivertencentMapView.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>

+ 87 - 0
src/views/User/heavyMetalFluxCalculation/inputFluxCalculation/waterdata/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>

+ 1045 - 0
src/views/User/heavyMetalFluxCalculation/inputFluxCalculation/waterdata/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/heavyMetalFluxCalculation/inputFluxCalculation/waterdata/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/heavyMetalFluxCalculation/inputFluxCalculation/waterdata/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/heavyMetalFluxCalculation/inputFluxCalculation/waterdata/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/heavyMetalFluxCalculation/inputFluxCalculation/waterdata/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/heavyMetalFluxCalculation/inputFluxCalculation/waterdata/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>

+ 1 - 1
src/views/User/selectCityAndCounty.vue

@@ -149,7 +149,7 @@ export default {
         router.push({ name: "parameterConfig" }); // 管理员跳转到 parameterConfig
       } else {
         router.push({
-          name: "shuJuKanBan", // 普通用户跳转到 shuJuKanBan
+          name: "samplingMethodDevice1", // 普通用户跳转到 samplingMethodDevice1
           query: { districts: selectedDistricts.value.join(",") },
         });
       }

+ 2 - 2
src/views/api.vue

@@ -3,12 +3,12 @@
 import axios from 'axios';
 
 export const fetchData = async () => {
-  const response = await axios.post('https://soilgd.com:5000/table', { table: 'Datasets' });
+  const response = await axios.post('https://www.soilgd.com:5000/table', { table: 'Datasets' });
   return response.data.rows;
 };
 
 export const trainAndSaveModel = async (trainData) => {
-  const response = await axios.post('https://soilgd.com:5000/train-and-save-model', trainData);
+  const response = await axios.post('https://www.soilgd.com:5000/train-and-save-model', trainData);
   return response;
 };
 </script>

+ 0 - 385
src/views/menu/AcidNeutralizationModel.vue

@@ -1,385 +0,0 @@
-<template>
-  <el-card class="box-card">
-    <template #header>
-      <div class="card-header">
-        <span>降酸模型</span>
-      </div>
-    </template>
-
-    <el-form
-      :model="form"
-      ref="predictForm"
-      label-width="240px"
-      label-position="left"
-    >
-      <el-form-item
-        label="土壤初始 pH"
-        prop="init_pH"
-        :error="errorMessages.init_pH"
-        required
-      >
-        <el-input
-          v-model="form.init_pH"
-          size="large"
-          placeholder="请输入土壤初始 3~6 pH"
-          ref="inputRefs.init_pH"
-          @input="handleInput('init_pH', $event, 3, 6)"
-        >
-        </el-input>
-      </el-form-item>
-
-      <el-form-item
-        label="土壤目标 pH"
-        prop="target_pH"
-        :error="errorMessages.target_pH"
-        required
-      >
-        <el-input
-          v-model="form.target_pH"
-          size="large"
-          placeholder="请输入土壤目标 5~7 pH"
-          ref="inputRefs.target_pH"
-          @input="handleInput('target_pH', $event, 5, 7)"
-        >
-        </el-input>
-      </el-form-item>
-
-      <el-form-item
-        label="土壤有机质(g/kg)"
-        prop="OM"
-        :error="errorMessages.OM"
-        required
-      >
-        <el-input
-          v-model="form.OM"
-          size="large"
-          placeholder="请输入土壤有机质 10~30 (g/kg)"
-          ref="inputRefs.OM"
-          @input="handleInput('OM', $event, 10, 30)"
-        >
-        </el-input>
-      </el-form-item>
-
-      <el-form-item
-        label="土壤粘粒(g/kg)"
-        prop="CL"
-        :error="errorMessages.CL"
-        required
-      >
-        <el-input
-          v-model="form.CL"
-          size="large"
-          placeholder="请输入土壤粘粒 50~600 (g/kg)"
-          ref="inputRefs.CL"
-          @input="handleInput('CL', $event, 50, 600)"
-        >
-        </el-input>
-      </el-form-item>
-
-      <el-form-item
-        label="交换性氢(cmol/kg)"
-        prop="H"
-        :error="errorMessages.H"
-        required
-      >
-        <el-input
-          v-model="form.H"
-          size="large"
-          placeholder="请输入交换性氢 0~4 (cmol/kg)"
-          ref="inputRefs.H"
-          @input="handleInput('H', $event, 0, 4)"
-        >
-        </el-input>
-      </el-form-item>
-
-      <el-form-item
-        label="交换性铝(cmol/kg)"
-        prop="Al"
-        :error="errorMessages.Al"
-        required
-      >
-        <el-input
-          v-model="form.Al"
-          size="large"
-          placeholder="请输入交换性铝 0~4 (cmol/kg)"
-          ref="inputRefs.Al"
-          @input="handleInput('Al', $event, 0, 4)"
-        >
-        </el-input>
-      </el-form-item>
-
-      <el-button type="primary" @click="onSubmit" class="onSubmit"
-        >计算</el-button
-      >
-
-      <el-dialog
-        v-model="dialogVisible"
-        @close="onDialogClose"
-        :close-on-click-modal="false"
-        width="500px"
-        align-center
-        class="dialog-class"
-        title="计算结果"
-      >
-        <span class="dialog-class"
-          >每亩地土壤表层(20cm)撒 {{ result }}吨生石灰</span
-        ><br />
-        <span class="dialog-class">{{ result }}吨</span>
-        <template #footer>
-          <el-button @click="dialogVisible = false">关闭</el-button>
-        </template>
-      </el-dialog>
-    </el-form>
-  </el-card>
-</template>
-
-<script setup lang="ts">
-import { reactive, ref, nextTick } from "vue";
-import { ElMessage } from 'element-plus';
-import axios from 'axios';
-import request from '../../utils/request';
-
-const form = reactive<{
-  init_pH: number | null,
-  target_pH: number | null,
-  OM: number | null,
-  CL: number | null,
-  H: number | null,
-  Al: number | null,
-}>({
-  init_pH: null,
-  target_pH: null,
-  OM: null,
-  CL: null,
-  H: null,
-  Al: null,
-});
-
-const result = ref<number | null>(null);
-const dialogVisible = ref(false);
-const predictForm = ref<any>(null);
-const errorMessages = reactive({
-  init_pH: '',
-  target_pH: '',
-  OM: '',
-  CL: '',
-  H: '',
-  Al: '',
-});
-
-// 使用 ref 来存储输入框元素
-const inputRefs = reactive<{
-  [key in keyof typeof form]: any;
-}>({
-  init_pH: ref<HTMLInputElement | null>(null),
-  target_pH: ref<HTMLInputElement | null>(null),
-  OM: ref<HTMLInputElement | null>(null),
-  CL: ref<HTMLInputElement | null>(null),
-  H: ref<HTMLInputElement | null>(null),
-  Al: ref<HTMLInputElement | null>(null),
-});
-
-// 限制输入为数字并校验范围
-const handleInput = (field: keyof typeof form, event: Event, min: number, max: number) => {
-  try {
-    const inputElement = inputRefs[field].value;
-    if (!inputElement) {
-      console.error('未找到输入框元素');
-      return;
-    }
-
-    // 过滤非数字和小数点字符
-    const rawValue = (event.target as HTMLInputElement).value;
-    const filteredValue = rawValue.replace(/[^0-9.]/g, '');
-    inputElement.value = filteredValue;
-
-    if (filteredValue === '') {
-      form[field] = null; // 当输入为空时设置为 null
-      errorMessages[field] = '';
-      return;
-    }
-
-    const numValue = parseFloat(filteredValue);
-    if (isNaN(numValue) || numValue < min || numValue > max) {
-      form[field] = null; // 如果值无效,则设为 null
-      errorMessages[field] = `输入值应在 ${min} 到 ${max} 之间且为有效数字`;
-    } else {
-      form[field] = numValue; // 如果值有效,则设为解析后的数值
-      errorMessages[field] = '';
-    }
-  } catch (error) {
-    console.error(`处理输入时出错:`, error);
-    errorMessages[field] = '输入处理出错,请检查输入';
-  }
-};
-
-const validateInput = (value: string | number, min: number, max: number): boolean => {
-  const numValue = parseFloat(value as string);
-  if (isNaN(numValue)) {
-    return false;
-  }
-  return numValue >= min && numValue <= max;
-};
-
-// 计算方法
-const onSubmit = async () => {
-  const inputConfigs = [
-    { field: 'init_pH' as keyof typeof form, min: 3, max: 6 },
-    { field: 'target_pH' as keyof typeof form, min: 5, max: 7 },
-    { field: 'OM' as keyof typeof form, min: 10, max: 30 },
-    { field: 'CL' as keyof typeof form, min: 50, max: 600 },
-    { field: 'H' as keyof typeof form, min: 0, max: 4 },
-    { field: 'Al' as keyof typeof form, min: 0, max: 4 },
-  ];
-
-  let isValid = true;
-  inputConfigs.forEach((config) => {
-    const { field, min, max } = config;
-    const value = form[field];
-    
-    // 检查是否为 null
-    if (value === null) {
-      isValid = false;
-      errorMessages[field] = `输入值应在 ${min} 到 ${max} 之间且为有效数字`;
-      return;
-    }
-
-    if (!validateInput(value, min, max)) {
-      isValid = false;
-      errorMessages[field] = `输入值应在 ${min} 到 ${max} 之间且为有效数字`;
-    } else {
-      errorMessages[field] = '';
-    }
-  });
-
-  if (!isValid) {
-    ElMessage.error('输入值不符合要求,请检查输入');
-    return;
-  }
-
-  console.log('开始计算...');
-  const data = {
-    model_id: 6,
-    parameters: {
-      init_pH: form.init_pH,
-      target_pH: form.target_pH,
-      OM: form.OM,
-      CL: form.CL,
-      H: form.H,
-      Al: form.Al,
-    }
-  };
-  console.log('提交的数据:', data);
-  try {
-    const response = await request.post('/predict', data, {
-      headers: {
-        'Content-Type': 'application/json'
-      }
-    });
-    console.log('预测结果:', response.data);
-    if (response.data && typeof response.data.result === 'number') {
-      result.value = parseFloat(response.data.result.toFixed(2));
-    } else {
-      console.error('未获取到有效的预测结果');
-      ElMessage.error('未获取到有效的预测结果');
-    }
-    dialogVisible.value = true;
-  } catch (error: unknown) {
-    console.error('请求失败:', error);
-    if (axios.isAxiosError(error)) {
-      if (error.response) {
-        ElMessage.error(`请求失败,状态码: ${error.response.status}`);
-      } else if (error.request) {
-        ElMessage.error('请求发送成功,但没有收到响应');
-      } else {
-        ElMessage.error('请求过程中发生错误: ' + error.message);
-      }
-    } else {
-      ElMessage.error('请求过程中发生错误: ' + (error as Error).message);
-    }
-  }
-};
-
-// 监听弹窗关闭事件,关闭后初始化值
-const onDialogClose = () => {
-  dialogVisible.value = false;
-
-  // 初始化 form 中的所有字段
-  Object.keys(form).forEach((key) => {
-    const typedKey = key as keyof typeof form;
-    form[typedKey] = null;
-  });
-
-  // 清除所有的错误信息
-  Object.keys(errorMessages).forEach((key) => {
-    const typedKey = key as keyof typeof errorMessages;
-    errorMessages[typedKey] = '';
-  });
-
-  nextTick(() => {
-    if (predictForm.value) {
-      predictForm.value.resetFields();
-    }
-  });
-};
-</script>
-
-<style scoped>
-.box-card {
-  max-width: 850px;
-  margin: 0 auto;
-  padding: 20px;
-  background-color: #f0f5ff;
-  border-radius: 10px;
-  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
-}
-
-.card-header {
-  font-size: 25px;
-  text-align: center;
-  color: #333;
-  margin-bottom: 30px;
-}
-
-.el-form-item {
-  margin-bottom: 20px;
-}
-
-:deep(.el-form-item__label) {
-  font-size: 18px;
-  color: #666;
-}
-
-.el-input {
-  width: 80%;
-}
-
-.onSubmit {
-  display: block;
-  margin: 0 auto;
-  background-color: #007bff;
-  color: white;
-  padding: 10px 20px;
-  border-radius: 5px;
-  font-size: 16px;
-  transition: background-color 0.3s ease;
-}
-
-.onSubmit:hover {
-  background-color: #0056b3;
-}
-
-.dialog-class {
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  height: 100%; /* 确保容器占满对话框内容区域 */
-  font-size: 22px;
-}
-
-@media (max-width: 576px) {
-  .el-form {
-    --el-form-label-width: 100px;
-  }
-}
-</style>

+ 0 - 359
src/views/menu/Calculation.vue

@@ -1,359 +0,0 @@
-<template>
-  <el-card class="box-card">
-    <template #header>
-      <div class="card-header">
-        <span>反酸模型</span>
-      </div>
-    </template>
-    <el-form
-      :model="form"
-      ref="predictForm"
-      label-width="240px"
-      label-position="left"
-    >
-      <el-form-item
-        label="土壤有机质(g/kg)"
-        prop="OM"
-        :error="errorMessages.OM"
-        required
-      >
-        <el-input
-          v-model="form.OM"
-          size="large"
-          placeholder="请输入土壤有机质0~30(g/kg)"
-          @input="handleInput('OM', $event, 0, 30)"
-        >
-        </el-input>
-      </el-form-item>
-
-      <el-form-item
-        label="土壤粘粒(g/kg)"
-        prop="CL"
-        :error="errorMessages.CL"
-        required
-      >
-        <el-input
-          v-model="form.CL"
-          size="large"
-          placeholder="请输入土壤粘粒50~400(g/kg)"
-          @input="handleInput('CL', $event, 50, 400)"
-        >
-        </el-input>
-      </el-form-item>
-
-      <el-form-item
-        label="阳离子交换量(cmol/kg)"
-        prop="CEC"
-        :error="errorMessages.CEC"
-        required
-      >
-        <el-input
-          v-model="form.CEC"
-          size="large"
-          placeholder="请输入阳离子交换量0~15(cmol/kg)"
-          @input="handleInput('CEC', $event, 0, 15)"
-        >
-        </el-input>
-      </el-form-item>
-
-      <el-form-item
-        label="交换性氢(cmol/kg)"
-        prop="H_plus"
-        :error="errorMessages.H_plus"
-        required
-      >
-        <el-input
-          v-model="form.H_plus"
-          size="large"
-          placeholder="请输入交换性氢0~1(cmol/kg)"
-          @input="handleInput('H_plus', $event, 0, 1)"
-        >
-        </el-input>
-      </el-form-item>
-
-      <el-form-item
-        label="水解氮(g/kg)"
-        prop="N"
-        :error="errorMessages.N"
-        required
-      >
-        <el-input
-          v-model="form.N"
-          size="large"
-          placeholder="请输入水解氮0~0.2(g/kg)"
-          @input="handleInput('N', $event, 0, 0.2)"
-        >
-        </el-input>
-      </el-form-item>
-
-      <el-form-item
-        label="交换性铝(cmol/kg)"
-        prop="Al3_plus"
-        :error="errorMessages.Al3_plus"
-        required
-      >
-        <el-input
-          v-model="form.Al3_plus"
-          size="large"
-          placeholder="请输入交换性铝0~6(cmol/kg)"
-          @input="handleInput('Al3_plus', $event, 0, 6)"
-        >
-        </el-input>
-      </el-form-item>
-
-      <el-button type="primary" @click="onSubmit" class="onSubmit"
-        >计算</el-button
-      >
-
-      <el-dialog
-        v-model="dialogVisible"
-        @close="onDialogClose"
-        :close-on-click-modal="false"
-        width="500px"
-        align-center
-        title="计算结果"
-      >
-        <span class="dialog-class">pH值: {{ result }}</span>
-        <template #footer>
-          <el-button @click="dialogVisible = false">关闭</el-button>
-        </template>
-      </el-dialog>
-    </el-form>
-  </el-card>
-</template>
-
-<script setup lang="ts">
-import { reactive, ref, nextTick } from "vue";
-import { ElMessage } from "element-plus";
-import axios from "axios";
-import { useRouter } from "vue-router";
-import request from "../../utils/request";
-
-// 定义表单数据接口
-interface Form {
-  OM: number | null;
-  CL: number | null;
-  CEC: number | null;
-  H_plus: number | null;
-  N: number | null;
-  Al3_plus: number | null;
-  delta_ph: number | null;
-}
-
-const form = reactive<Form>({
-  OM: null,
-  CL: null,
-  CEC: null,
-  H_plus: null,
-  N: null,
-  Al3_plus: null,
-  delta_ph: null,
-});
-
-const router = useRouter();
-const result = ref<number | null>(null);
-const dialogVisible = ref(false);
-const predictForm = ref<any>(null);
-const errorMessages = reactive<Record<string, string>>({
-  OM: "",
-  CL: "",
-  CEC: "",
-  H_plus: "",
-  N: "",
-  Al3_plus: "",
-});
-
-// 限制输入为数字并校验范围
-const handleInput = (
-  field: keyof Form,
-  event: Event,
-  min: number,
-  max: number
-) => {
-  const target = event.target as HTMLInputElement;
-  let value = target.value.replace(/[^0-9.]/g, "");
-  form[field] = value ? parseFloat(value) : null;
-
-  if (!value) {
-    errorMessages[field] = "";
-    return;
-  }
-
-  const numValue = parseFloat(value);
-  if (isNaN(numValue) || numValue < min || numValue > max) {
-    errorMessages[field] = `输入值应在 ${min} 到 ${max} 之间且为有效数字`;
-  } else {
-    errorMessages[field] = "";
-  }
-};
-
-const validateInput = (value: string, min: number, max: number): boolean => {
-  const numValue = parseFloat(value);
-  return !isNaN(numValue) && numValue >= min && numValue <= max;
-};
-
-// 计算方法
-const onSubmit = async () => {
-  const inputConfigs = [
-    { field: "OM" as keyof Form, min: 0, max: 30 },
-    { field: "CL" as keyof Form, min: 50, max: 400 },
-    { field: "CEC" as keyof Form, min: 0, max: 15 },
-    { field: "H_plus" as keyof Form, min: 0, max: 1 },
-    { field: "N" as keyof Form, min: 0, max: 0.2 },
-    { field: "Al3_plus" as keyof Form, min: 0, max: 6 },
-  ];
-
-  let isValid = true;
-  for (const config of inputConfigs) {
-    const { field, min, max } = config;
-    const value = form[field]; // 现在 TypeScript 明白 field 是 keyof Form 类型
-    if (value === null || !validateInput(value.toString(), min, max)) {
-      isValid = false;
-      errorMessages[field] = `输入值应在 ${min} 到 ${max} 之间且为有效数字`;
-    } else {
-      errorMessages[field] = "";
-    }
-  }
-
-  if (!isValid) {
-    ElMessage.error("输入值不符合要求,请检查输入");
-    return;
-  }
-
-  console.log("开始计算...");
-  const data = {
-    model_id: 25,
-    parameters: {
-      OM: form.OM, // 土壤有机质
-      CL: form.CL, // 土壤粘粒
-      CEC: form.CEC, // 阳离子交换量
-      H: form.H_plus, // 交换性氢
-      N: form.N, // 水解氮
-      Al: form.Al3_plus, // 交换性铝
-    },
-  };
-
-  try {
-    const response = await request.post("/predict", data, {
-      headers: {
-        "Content-Type": "application/json",
-      },
-    });
-    console.log("预测结果:", response.data);
-    if (
-      response.data &&
-      response.data.result &&
-      response.data.result.length > 0
-    ) {
-      result.value = parseFloat(response.data.result[0].toFixed(2));
-    } else {
-      console.error("未获取到有效的预测结果");
-      ElMessage.error("未获取到有效的预测结果");
-    }
-    dialogVisible.value = true;
-  } catch (error: any) {
-    console.error("请求失败:", error);
-    if (error.response) {
-      ElMessage.error(`请求失败,状态码: ${error.response.status}`);
-    } else if (error.request) {
-      ElMessage.error("请求发送成功,但没有收到响应");
-    } else {
-      ElMessage.error("请求过程中发生错误: " + error.message);
-    }
-  }
-};
-
-// 监听弹窗关闭事件,关闭后初始化值
-const onDialogClose = () => {
-  dialogVisible.value = false;
-
-  // 使用类型断言确保 key 是 keyof Form 类型
-  Object.keys(form).forEach((key) => {
-    if (key in form) {
-      const typedKey = key as keyof Form;
-      form[typedKey] = null;
-    }
-  });
-
-  // 同样地处理 errorMessages
-  Object.keys(errorMessages).forEach((key) => {
-    if (key in errorMessages) {
-      const typedKey = key as keyof typeof errorMessages;
-      errorMessages[typedKey] = "";
-    }
-  });
-
-  nextTick(() => {
-    if (predictForm.value) {
-      (predictForm.value as any).resetFields(); // 使用类型断言避免类型错误
-    }
-  });
-};
-</script>
-
-<style scoped>
-.box-card {
-  max-width: 850px;
-  margin: 0 auto;
-  padding: 20px;
-  background-color: #f0f5ff;
-  border-radius: 10px;
-  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
-}
-
-.card-header {
-  text-align: center;
-  color: #333;
-  margin-bottom: 30px;
-  font-size: 25px;
-}
-
-.el-form-item {
-  margin-bottom: 20px;
-}
-
-:deep(.el-form-item__label) {
-  font-size: 18px;
-  color: #666;
-}
-
-.el-input {
-  width: 80%;
-}
-
-.onSubmit {
-  display: block;
-  margin: 0 auto;
-  background-color: #007bff;
-  color: white;
-  padding: 10px 20px;
-  border-radius: 5px;
-  font-size: 16px;
-  transition: background-color 0.3s ease;
-}
-
-.onSubmit:hover {
-  background-color: #0056b3;
-}
-
-.dialog-class {
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  height: 100%; /* 确保容器占满对话框内容区域 */
-  font-size: 20px;
-}
-
-@media (max-width: 576px) {
-  .el-form {
-    --el-form-label-width: 100px;
-  }
-}
-
-/* 自定义 tooltip 样式 */
-:deep(.el-tooltip__popper.is-light) {
-  background-color: white;
-  border-color: #dcdfe6;
-  color: #606266;
-}
-</style>

+ 0 - 176
src/views/menu/IntroUpdateModal.vue

@@ -1,176 +0,0 @@
-<template>
-  <!-- 只有当数据加载完成且 isVisible 为 true 时才显示页面 -->
-  <div class="introduce-update-container" v-if="isVisible && !isLoading">
-    <div class="input-group">
-      <label for="title-input">标题:</label>
-      <input id="title-input" v-model="updatedIntro.title" type="text" placeholder="请输入修改后标题">
-    </div>
-    <div class="input-group">
-      <label>内容:</label>
-      <!-- 使用富文本框组件 -->
-      <RichTextEditor v-model="updatedIntro.intro" />
-    </div>
-    <div class="button-container">
-      <button @click="updateIntro" :disabled="isUpdating" class="submit-button">提交更新</button>
-    </div>
-  </div>
-</template>
-
-<script setup lang="ts">
-import { ref, onMounted, watch } from 'vue';
-import axios from 'axios';
-import RichTextEditor from './RichTextEditor.vue';
-import { ElMessage } from 'element-plus';
-
-// 定义 targetId 和 isVisible 为 prop
-const props = defineProps({
-  targetId: {
-    type: Number,
-    required: true
-  },
-  isVisible: {
-    type: Boolean,
-    default: false
-  }
-});
-
-const emits = defineEmits(['updateSuccess']);
-
-// 存储更新用的介绍数据
-const updatedIntro = ref({
-  title: '',
-  intro: ''
-});
-// 更新状态
-const isUpdating = ref(false);
-// 加载状态
-const isLoading = ref(true);
-
-// 从后端获取介绍数据
-const fetchData = async () => {
-  try {
-    isLoading.value = true;
-    const response = await axios.get(`https://soilgd.com:5000/software-intro/${props.targetId}`);
-    const { title, intro } = response.data;
-    // 仅将换行符替换为 <br>,避免影响视频标签
-    let processedIntro = intro.replace(/\r\n/g, '<br>');
-    updatedIntro.value.title = title;
-    updatedIntro.value.intro = processedIntro;
-  } catch (err: any) {
-    console.error('请求数据时发生错误:', err.message);
-  } finally {
-    // 数据加载完成,将加载状态设为 false
-    isLoading.value = false;
-  }
-};
-
-onMounted(() => {
-  console.log('子组件挂载时接收到的 targetId:', props.targetId);
-  if (props.isVisible) {
-    // 发起数据请求
-    fetchData();
-  }
-});
-
-// 监听 targetId 变化,当 targetId 改变时重新获取数据
-watch(() => props.targetId, (newTargetId, oldTargetId) => {
-  console.log(`targetId 从 ${oldTargetId} 变为 ${newTargetId}`);
-  if (props.isVisible) {
-    fetchData();
-  }
-});
-
-// 更新介绍信息
-const updateIntro = async () => {
-  isUpdating.value = true;
-  try {
-    // 将 <br> 标签替换回换行符
-    let processedIntro = updatedIntro.value.intro.replace(/<br>/gi, '\r\n');
-    const dataToSend = {
-      title: updatedIntro.value.title,
-      intro: processedIntro
-    };
-    // 发送 PUT 请求更新数据http
-    const response = await axios.put(`https://soilgd.com:5000/software-intro/${props.targetId}`, dataToSend, {
-      headers: {
-        'Content-Type': 'application/json'
-      }
-    });
-    console.log('介绍更新成功!');
-    ElMessage.success('更新成功!');
-    emits('updateSuccess');
-  } catch (err: any) {
-    if (err.response) {
-      console.error(`服务器错误: ${err.response.status} - ${err.response.data.message}`);
-    } else if (err.request) {
-      console.error('网络连接错误,请检查网络设置。');
-    } else {
-      console.error('请求设置错误: ' + err.message);
-    }
-  } finally {
-    isUpdating.value = false;
-  }
-};
-</script>
-
-<style scoped lang="scss">
-.introduce-update-container {
-  margin-top: 20px;
-  padding: 20px;
-  border: 1px solid #ccc;
-  border-radius: 5px;
-  width: 90%;
-  margin-left: auto;
-  margin-right: auto;
-  /* 让容器高度自适应内容 */
-  height: auto;
-  overflow: auto;
-}
-
-.input-group {
-  margin-bottom: 20px;
-  display: flex;
-  align-items: start; /* 让标签和输入框顶部对齐 */
-}
-
-.input-group label {
-  min-width: 80px; /* 固定标签宽度 */
-  margin-right: 10px;
-  text-align: left; /* 文字左对齐 */
-}
-
-.input-group input,
-.input-group .rich-text-editor { 
-  flex: 1;
-  padding: 10px;
-  border: 1px solid #ccc;
-  border-radius: 3px;
-  /* 让输入框和富文本编辑器高度自适应内容 */
-  height: auto;
-  min-height: auto;
-}
-
-.button-container {
-  text-align: right;
-  margin-top: 10px; /* 增加按钮与输入框的间距 */
-}
-
-.submit-button {
-  padding: 10px 20px;
-  background-color: #007BFF;
-  color: white;
-  border: none;
-  border-radius: 3px;
-  cursor: pointer;
-  transition: background-color 0.3s ease;
-
-  &:hover {
-    background-color: #0056b3;
-  }
-
-  &:disabled {
-    background-color: #ccc;
-    cursor: not-allowed;
-  }
-}
-</style>

+ 0 - 193
src/views/menu/Introduce.vue

@@ -1,193 +0,0 @@
-<template>
-  <div class="software-intro-container">
-    <!-- 加载状态 -->
-    <div v-if="isLoading" class="loading-message">加载中...</div>
-    <!-- 错误信息 -->
-    <div v-else-if="error" class="error-message">{{ error }}</div>
-    <!-- 正常展示内容 -->
-    <div v-else class="content-wrapper">
-      <h1 class="title">{{ softwareIntro.title }}</h1>
-      <div v-for="(paragraph, index) in softwareIntro.introParagraphs" :key="index" class="paragraph">
-        <p v-html="paragraph"></p>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script setup lang="ts">
-import { ref, onMounted, defineProps } from 'vue';
-import axios from 'axios';
-
-// 定义 targetId 为 prop
-const props = defineProps({
-  targetId: {
-    type: Number,
-    default: 1
-  }
-});
-
-// 存储介绍内容
-const softwareIntro = ref({
-  title: '',
-  introParagraphs: []
-});
-// 加载状态
-const isLoading = ref(true);
-// 错误信息
-const error = ref('');
-
-// 判断是否为标题段落
-const isTitle = (paragraph: string) => {
-  return /^[^\s].*[::]$/.test(paragraph);
-};
-
-const imageBaseUrl = ref(import.meta.env.VITE_IMAGE_BASE_URL);
-// 处理段落,将图片路径转换为 <img> 标签
-const processParagraph = (paragraph: string) => {
-  // 假设图片路径是相对路径
-  const imgRegex = /([^\s]+(\.jpg|\.jpeg|\.png|\.gif))/g;
-  return paragraph.replace(imgRegex, `<img src="${imageBaseUrl.value}$1" alt="图片" class="intro-image">`);
-};
-
-onMounted(async () => {
-  try {
-    // 从后端获取介绍数据
-    const response = await axios.get(`https://soilgd.com:5000/software-intro/${props.targetId}`);
-    const { title, intro } = response.data;
-    // 保留 \r\n 换行符
-    const processedIntro = intro.replace(/<p>/g, '').replace(/<\/p>/g, '\r\n').replace(/<br>/g, '');
-    softwareIntro.value.introParagraphs = processedIntro.split('\r\n').filter((paragraph: string) => paragraph.trim()!== '');
-    softwareIntro.value.title = title;
-  } catch (err: any) {
-    error.value = err.message || '请求数据时发生错误';
-  } finally {
-    isLoading.value = false;
-  }
-
-  // 页面加载完成后,监听视频播放事件
-  const container = document.querySelector('.software-intro-container');
-  if (container) {
-    container.addEventListener('play', (event) => {
-      if (event.target instanceof HTMLVideoElement) {
-        const video = event.target;
-        setVideoStyle(video);
-      }
-    }, true);
-
-    // 监听窗口大小变化事件,确保视频在窗口大小改变时也能适应
-    window.addEventListener('resize', () => {
-      const videos = container.querySelectorAll('video');
-      videos.forEach((video) => {
-        if (video instanceof HTMLVideoElement) {
-          setVideoStyle(video);
-        }
-      });
-    });
-  }
-});
-
-// 封装设置视频样式的函数
-const setVideoStyle = (video: HTMLVideoElement) => {
-  const containerWidth = (document.querySelector('.software-intro-container') as HTMLElement).offsetWidth;
-  const maxWidth = containerWidth * 0.8; // 最大宽度为容器宽度的 80%
-  if (video.videoWidth > maxWidth) {
-    video.style.width = `${maxWidth}px`;
-    video.style.height = 'auto';
-  } else {
-    video.style.width = 'auto';
-    video.style.height = 'auto';
-  }
-  video.style.maxWidth = '80%';
-  video.style.objectFit = 'contain';
-};
-</script>
-
-<style scoped lang="scss">
-* {
-  margin: 0;
-  padding: 0;
-  box-sizing: border-box;
-}
-
-body {
-  background-color: white;
-  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; /* 更美观的字体 */
-  padding: 0;
-}
-
-.software-intro-container {
-  width: 95%;
-  margin: 0 auto; 
-  padding: 20px;
-  background-color: #fff; 
-  border-radius: 8px; /* 圆角 */
-  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); /* 阴影 */
-  height: 80vh; /* 设置容器高度为视口高度的 80% */
-  overflow-y: auto; /* 当内容超出容器高度时,显示垂直滚动条 */
-  scrollbar-width: none; /* Firefox */
-  -ms-overflow-style: none; /* Internet Explorer 10+ */
-}
-
-.loading-message {
-  text-align: center;
-  font-size: 18px;
-  color: #999;
-  padding: 20px;
-}
-
-.error-message {
-  text-align: center;
-  font-size: 18px;
-  color: #ff0000; /* 红色错误信息 */
-  padding: 20px;
-}
-
-.title {
-  font-size: 36px;
-  text-align: center;
-  margin-bottom: 20px;
-  color: #333; /* 深灰色标题 */
-}
-
-.paragraph {
-  margin-bottom: 20px;
-}
-
-p {
-  font-size: 16px;
-  line-height: 1.8; /* 增大行高 */
-  color: #666; /* 灰色文字 */
-}
-
-.intro-image {
-  max-width: 100%;
-  height: auto;
-  border-radius: 4px; /* 图片圆角 */
-  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* 图片阴影 */
-  margin-bottom: 10px;
-}
-
-/* 控制视频尺寸 */
-video {
-  width: 80%;
-  max-width: 80%;
-  height: auto;
-  object-fit: contain;
-}
-
-/* 响应式设计 */
-@media (max-width: 768px) {
-  .software-intro-container {
-    padding: 15px;
-    height: 90vh; /* 在小屏幕上,调整容器高度为视口高度的 90% */
-  }
-
-  .title {
-    font-size: 28px;
-  }
-
-  p {
-    font-size: 14px;
-  }
-}
-</style>

+ 0 - 95
src/views/menu/IntroductionUpdate.vue

@@ -1,95 +0,0 @@
-<template>
-  <div class="parent-container">
-    <div class="button-group">
-      <el-button
-        v-for="(button, index) in buttons"
-        :key="index"
-        type="primary"
-        :class="{ 'is-active': currentTargetId === parseInt(button.targetId) }"
-        @click="showComponent(parseInt(button.targetId))"
-      >
-        {{ button.label }}
-      </el-button>
-    </div>
-    <!-- 确保只有在 currentTargetId 不为 null 时渲染 IntroUpdateModal -->
-    <IntroUpdateModal
-      v-if="currentTargetId !== null"
-      :targetId="currentTargetId as number"
-      :isVisible="!!currentTargetId"
-      @updateSuccess="handleUpdateSuccess"
-    />
-  </div>
-</template>
-
-<script setup lang="ts">
-import { ref, onMounted } from 'vue';
-import IntroUpdateModal from './IntroUpdateModal.vue';
-
-const buttons = [
-  { label: '软件简介更新', targetId: '1' },
-  { label: '项目简介更新', targetId: '2' },
-  { label: '研究成果更新', targetId: '3' },
-  { label: '团队信息更新', targetId: '4' }
-];
-
-const currentTargetId = ref<number | null>(null);
-
-const showComponent = (id: number) => {
-  currentTargetId.value = id;
-};
-
-const handleUpdateSuccess = () => {
-  console.log('子组件更新成功事件已接收');
-  // 可以在此处添加更多的逻辑处理
-};
-
-// 页面加载时默认选中第一个按钮
-onMounted(() => {
-  showComponent(parseInt(buttons[0].targetId));
-});
-</script>
-
-<style scoped>
-.parent-container {
-  padding: 10px;
-  width: 80%;
-  max-width: 1200px;
-  margin: 0 auto;
-  background: #f0f8ff;
-  border-radius: 15px;
-  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
-}
-
-.button-group {
-  margin-bottom: 15px;
-  display: flex;
-  flex-wrap: wrap;
-  gap: 15px;
-}
-
-.el-button {
-  padding: 8px 15px;
-  border-radius: 8px;
-  font-size: 16px;
-  font-weight: 500;
-  background-color: #e0f2ff; /* 正常状态背景色 */
-  color: #007bff; /* 正常状态文字颜色 */
-  border: none;
-  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
-  transition: all 0.3s ease;
-}
-
-.el-button:hover {
-  background-color: #b3d9ff; /* 悬停状态背景色 */
-  color: #0056b3; /* 悬停状态文字颜色 */
-  transform: translateY(-2px);
-  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
-}
-
-.is-active {
-  background-color: #007bff; /* 选中状态背景色 */
-  color: white; /* 选中状态文字颜色 */
-  transform: scale(1.05);
-  box-shadow: 0 4px 15px rgba(0, 123, 255, 0.2);
-}
-</style>

+ 0 - 315
src/views/menu/ModelIterationVisualization.vue

@@ -1,315 +0,0 @@
-<template>
-    <div class="container">
-      <template v-if="showLineChart">
-        <!-- 折线图表头 -->
-        <div class="chart-container">
-          <VueEcharts :option="ecLineOption" ref="ecLineOptionRef" />
-        </div>
-      </template>
-      <template v-if="showInitScatterChart">
-        <!-- 初代散点图表头 -->
-        <h2 class="chart-header">初代散点图</h2>
-        <div class="chart-container">
-          <VueEcharts :option="ecInitScatterOption" ref="ecInitScatterOptionRef" />
-        </div>
-      </template>
-      <template v-if="showMidScatterChart">
-        <!-- 中间代散点图表头 -->
-        <h2 class="chart-header">中间代散点图</h2>
-        <div class="chart-container">
-          <VueEcharts :option="ecMidScatterOption" ref="ecMidScatterOptionRef" />
-        </div>
-      </template>
-      <template v-if="showFinalScatterChart">
-        <!-- 最终代散点图表头 -->
-        <h2 class="chart-header">最终代散点图</h2>
-        <div class="chart-container">
-          <VueEcharts :option="ecFinalScatterOption" ref="ecFinalScatterOptionRef" />
-        </div>
-      </template>
-    </div>
-  </template>
-  
-  <script setup lang='ts'>
-  import { ref, onMounted, nextTick, onUnmounted, defineProps } from 'vue';
-  import VueEcharts from 'vue-echarts';
-  import 'echarts';
-  import request from '../../utils/request';
-  
-  interface HistoryDataItem {
-    dataset_id: number;
-    row_count: number;
-    model_id: number;
-    model_name: string;
-    performance_score: number;
-    timestamp: string;
-  }
-  
-  interface HistoryDataResponse {
-    data_type: string;
-    timestamps: string[];
-    row_counts: number[];
-    performance_scores: number[];
-    model_details: HistoryDataItem[];
-  }
-  
-  interface ScatterDataResponse {
-    scatter_data: [number, number][];
-    r2_score: number;
-    y_range: [number, number];
-    model_name: string;
-    model_type: string;
-  }
-  
-  const props = defineProps({
-    showLineChart: {
-      type: Boolean,
-      default: true
-    },
-    showInitScatterChart: {
-      type: Boolean,
-      default: true
-    },
-    showMidScatterChart: {
-      type: Boolean,
-      default: true
-    },
-    showFinalScatterChart: {
-      type: Boolean,
-      default: true
-    },
-    lineChartPathParam: {
-      type: String,
-      default: 'reduce'
-    },
-    initScatterModelId: {
-      type: Number,
-      default: 6
-    },
-    midScatterModelId: {
-      type: Number,
-      default: 7
-    },
-    finalScatterModelId: {
-      type: Number,
-      default: 17
-    }
-  });
-  
-  // 定义响应式变量
-  const ecLineOption = ref({});
-  const ecInitScatterOption = ref({});
-  const ecMidScatterOption = ref({});
-  const ecFinalScatterOption = ref({});
-  
-  // 定义图表引用
-  const ecLineOptionRef = ref<InstanceType<typeof VueEcharts>>();
-  const ecInitScatterOptionRef = ref<InstanceType<typeof VueEcharts>>();
-  const ecMidScatterOptionRef = ref<InstanceType<typeof VueEcharts>>();
-  const ecFinalScatterOptionRef = ref<InstanceType<typeof VueEcharts>>();
-  
-  // 计算数据范围的函数
-  const calculateDataRange = (data: [number, number][]) => {
-    const xValues = data.map(item => item[0]);
-    const yValues = data.map(item => item[1]);
-    return {
-      xMin: Math.min(...xValues),
-      xMax: Math.max(...xValues),
-      yMin: Math.min(...yValues),
-      yMax: Math.max(...yValues)
-    };
-  };
-  
-  // 获取折线图数据
-  const fetchLineData = async () => {
-    try {
-      const response = await request.get<HistoryDataResponse>(`/get-model-history/${props.lineChartPathParam}`);
-      const data = response.data;
-  
-      const timestamps = data.timestamps;
-      const performanceScores: Record<string, number[]> = {};
-  
-      data.model_details.forEach((item: HistoryDataItem) => {
-        const score = Number(item.performance_score);
-        if (!performanceScores[item.model_name]) {
-          performanceScores[item.model_name] = [];
-        }
-        performanceScores[item.model_name].push(score);
-      });
-  
-      const series = Object.keys(performanceScores).map(modelName => ({
-        name: modelName,
-        type: 'line',
-        data: performanceScores[modelName]
-      }));
-  
-      ecLineOption.value = {
-        tooltip: {
-          trigger: 'axis'
-        },
-        legend: {
-          data: Object.keys(performanceScores)
-        },
-        grid: {
-          left: '3%',
-          right: '17%',
-          bottom: '3%',
-          containLabel: true
-        },
-        xAxis: {
-          name: '模型迭代',
-          type: 'category',
-          boundaryGap: false,
-          data: timestamps.map((_, index) => `${index + 1}代`)
-        },
-        yAxis: {
-          name: 'Score (R^2)',
-          type: 'value'
-        },
-        series
-      };
-      console.log('ecLineOption updated:', ecLineOption.value);
-    } catch (error) {
-      console.error('获取折线图数据失败:', error);
-    }
-  };
-  
-  // 获取散点图数据
-  const fetchScatterData = async (modelId: number, optionRef: any) => {
-    try {
-      const response = await request.get<ScatterDataResponse>(`/model-scatter-data/${modelId}`);
-      const data = response.data;
-  
-      const scatterData = data.scatter_data;
-      const range = calculateDataRange(scatterData);
-      const padding = 0.1;
-      const xMin = range.xMin - Math.abs(range.xMin * padding);
-      const xMax = range.xMax + Math.abs(range.xMax * padding);
-      const yMin = range.yMin - Math.abs(range.yMin * padding);
-      const yMax = range.yMax + Math.abs(range.yMax * padding);
-      const min = Math.min(xMin, yMin);
-      const max = Math.max(xMax, yMax);
-  
-      optionRef.value = {
-        tooltip: {
-          trigger: 'axis',
-          axisPointer: {
-            type: 'cross'
-          }
-        },
-        legend: {
-          data: ['True vs Predicted']
-        },
-        grid: {
-          left: '3%',
-          right: '22%',
-          bottom: '3%',
-          containLabel: true
-        },
-        xAxis: {
-          name: 'True Values',
-          type: 'value',
-          min: min,
-          max: max
-        },
-        yAxis: {
-          name: 'Predicted Values',
-          type: 'value',
-          min: parseFloat(min.toFixed(2)),
-          max: parseFloat(max.toFixed(2))
-        },
-        series: [
-          {
-            name: 'True vs Predicted',
-            type: 'scatter',
-            data: scatterData,
-            symbolSize: 10,
-            itemStyle: {
-              color: '#1f77b4',
-              opacity: 0.7
-            }
-          },
-          {
-            name: 'Trendline',
-            type: 'line',
-            data: [
-              [min, min],
-              [max, max]
-            ],
-            lineStyle: {
-              type: 'dashed',
-              color: '#ff7f0e',
-              width: 2
-            }
-          }
-        ]
-      };
-    } catch (error) {
-      console.error('获取散点图数据失败:', error);
-    }
-  };
-  
-  // 定义调整图表大小的函数
-  const resizeCharts = () => {
-    nextTick(() => {
-      if (props.showLineChart) ecLineOptionRef.value?.resize();
-      if (props.showInitScatterChart) ecInitScatterOptionRef.value?.resize();
-      if (props.showMidScatterChart) ecMidScatterOptionRef.value?.resize();
-      if (props.showFinalScatterChart) ecFinalScatterOptionRef.value?.resize();
-    });
-  };
-  
-  onMounted(async () => {
-    if (props.showLineChart) await fetchLineData();
-    if (props.showInitScatterChart) await fetchScatterData(props.initScatterModelId, ecInitScatterOption);
-    if (props.showMidScatterChart) await fetchScatterData(props.midScatterModelId, ecMidScatterOption);
-    if (props.showFinalScatterChart) await fetchScatterData(props.finalScatterModelId, ecFinalScatterOption);
-  
-    // 页面加载完成后调整图表大小
-    resizeCharts();
-  
-    // 监听窗口大小变化,调整图表大小
-    window.addEventListener('resize', resizeCharts);
-  });
-  
-  // 组件卸载时移除事件监听器
-  onUnmounted(() => {
-    window.removeEventListener('resize', resizeCharts);
-  });
-  </script>
-  
-  <style scoped>
-  .container {
-    display: flex;
-    flex-direction: column;
-    align-items: center;
-    justify-content: center;
-    width: 100%;
-    height: 100%;
-    gap: 20px;
-  }
-  
-  .chart-header {
-    font-size: 18px;
-    font-weight: bold;
-    margin-bottom: 10px;
-  }
-  
-  .sub-title {
-    font-size: 14px;
-    font-weight: 700;
-  }
-  
-  .chart-container {
-    width: 85%;
-    height: 450px;
-    margin: 0 auto;
-    margin-bottom: 20px;
-  }
-  
-  .VueEcharts {
-    width: 100%;
-    height: 100%;
-    margin: 0 10px;
-  }
-  </style>    

+ 0 - 17
src/views/menu/Overview.vue

@@ -1,17 +0,0 @@
-<!--项目简介-->
-<template>
-  <div class="parent-container">
-    <Introduce :targetId="2" />
-  </div>
-</template>
-
-<script setup lang="ts">
-import Introduce from './Introduce.vue'
-</script>
-
-<style scoped>
-.parent-container {
-  padding: 20px;
-  background-color: #f0f8ff; 
-}
-</style>

+ 0 - 17
src/views/menu/ResearchFindings.vue

@@ -1,17 +0,0 @@
-<!--项目成果-->
-<template>
-  <div class="parent-container">
-    <Introduce :targetId="3" />
-  </div>
-</template>
-
-<script setup lang="ts">
-import Introduce from './Introduce.vue'
-</script>
-
-<style scoped>
-.parent-container {
-  padding: 20px;
-  background-color: #f0f8ff; 
-}
-</style>

+ 0 - 218
src/views/menu/RichTextEditor.vue

@@ -1,218 +0,0 @@
-<template>
-  <div class="rich-text-editor-container">
-    <Toolbar
-      class="rich-text-toolbar"
-      :editor="editorRef"
-      :defaultConfig="toolbarConfig"
-      mode="default"
-    />
-    <Editor
-      class="rich-text-editor"
-      :style="{ height: '300px', overflowY: 'hidden' }"
-      v-model="localValue"
-      :defaultConfig="editorConfig"
-      mode="default"
-      @onCreated="handleCreated"
-      @customPaste="customPaste"
-    />
-  </div>
-</template>
-
-<script setup>
-import { ref, defineProps, defineEmits, watch, onMounted } from 'vue';
-import '@wangeditor/editor/dist/css/style.css';
-import { onBeforeUnmount, shallowRef } from 'vue';
-import { Editor, Toolbar } from '@wangeditor/editor-for-vue';
-import axios from 'axios';
-
-const props = defineProps({
-  modelValue: String
-});
-
-const emits = defineEmits(['update:modelValue']);
-
-const localValue = ref(props.modelValue);
-
-const editorRef = shallowRef();
-
-const toolbarConfig = ref();
-const editorConfig = ref({ placeholder: '请输入内容...', MENU_CONF: {} });
-
-// 自定义图片上传
-editorConfig.value.MENU_CONF['uploadImage'] = {
-  async customUpload(file, insertFn) {
-    try {
-      const formData = new FormData();
-      formData.append('image', file);
-      const response = await axios.post('https://soilgd.com:5000/upload-image', formData, {
-        headers: {
-          'Content-Type': 'multipart/form-data'
-        }
-      });
-      const imageUrl = response.data.imageUrl;
-      insertFn(imageUrl, 'img');
-    } catch (error) {
-      console.error('图片上传失败:', error);
-    }
-  },
-};
-
-// 自定义视频上传
-editorConfig.value.MENU_CONF['uploadVideo'] = {
-  async customUpload(file, insertFn) {
-    try {
-      const formData = new FormData();
-      formData.append('video', file);
-      const response = await axios.post('https://soilgd.com:5000/upload-video', formData, {
-        headers: {
-          'Content-Type': 'multipart/form-data'
-        }
-      });
-      const videoUrl = response.data.videoUrl;
-      insertFn(videoUrl, 'video');
-    } catch (error) {
-      console.error('视频上传失败:', error);
-    }
-  },
-};
-
-const handleCreated = (editor) => {
-  editorRef.value = editor;
-  console.log(editorConfig.value.MENU_CONF, 'editorConfig.value');
-  console.log('Editor created:', editor);
-};
-
-const customPaste = (editor, event, callback) => {
-  const text = event.clipboardData.getData('text/plain');
-  if (text) {
-    console.log('Pasted text:', text);
-    editor.insertText(text);
-    event.preventDefault();
-    callback(false);
-  }
-};
-
-onBeforeUnmount(() => {
-  const editor = editorRef.value;
-  if (editor) {
-    editor.destroy();
-  }
-});
-
-const updateValue = () => {
-  emits('update:modelValue', localValue.value);
-};
-
-// 监听 localValue 变化,更新父组件的值
-watch(localValue, (newValue, oldValue) => {
-  if (newValue!== props.modelValue) {
-    updateValue();
-  }
-});
-
-onMounted(() => {
-  const editorElement = document.querySelector('.rich-text-editor');
-  if (editorElement) {
-    editorElement.addEventListener('play', (event) => {
-      if (event.target instanceof HTMLVideoElement) {
-        const video = event.target;
-        video.style.width = '80%';
-        video.style.maxWidth = '80%';
-        video.style.height = 'auto';
-        video.style.objectFit = 'contain';
-      }
-    }, true);
-  }
-});
-</script>
-
-<style scoped lang="scss">
-.rich-text-editor-container {
-  width: 90%;
-  border: 1px solid #e0e0e0;
-  border-radius: 8px;
-  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
-  overflow: hidden;
-  margin: 0 auto;
-
-  /* 让容器在小屏幕下也能自适应 */
-  @media (max-width: 768px) {
-    width: 100%;
-  }
-}
-
-.rich-text-toolbar {
-  background-color: #f8f9fa;
-  border-bottom: 1px solid #e0e0e0;
-  padding: 5px 8px;
-  display: flex;
-  align-items: center;
-  flex-wrap: wrap;
-
-  /* 工具栏按钮样式 */
-  .w-e-menu {
-    margin-right: 5px;
-    margin-bottom: 4px;
-
-    button {
-      color: #333;
-      border: none;
-      background: transparent;
-      cursor: pointer;
-      padding: 4px 6px;
-      border-radius: 4px;
-      transition: background-color 0.3s ease;
-
-      &:hover {
-        background-color: #e9ecef;
-      }
-
-      &:active {
-        background-color: #dce0e4;
-      }
-    }
-
-    /* 下拉菜单样式 */
-    .w-e-select {
-      color: #333;
-      border: 1px solid #e0e0e0;
-      background-color: white;
-      border-radius: 4px;
-      padding: 3px 6px;
-      cursor: pointer;
-      transition: border-color 0.3s ease;
-
-      &:hover {
-        border-color: #999;
-      }
-    }
-  }
-}
-
-.rich-text-editor {
-  padding: 12px;
-  font-size: 16px;
-  line-height: 1.6;
-  color: #333;
-  /* 编辑区域滚动条样式 */
-  &::-webkit-scrollbar {
-    width: 8px;
-  }
-
-  &::-webkit-scrollbar-thumb {
-    background-color: #ccc;
-    border-radius: 4px;
-  }
-
-  &::-webkit-scrollbar-track {
-    background-color: #f1f1f1;
-  }
-
-  video {
-    width: 80%;
-    max-width: 80%;
-    height: auto;
-    object-fit: contain;
-  }
-}
-</style>

+ 0 - 41
src/views/menu/SoilAcidReductionIterativeEvolution.vue

@@ -1,41 +0,0 @@
-<template>
-  <div id="app">
-    <ModelIterationVisualization
-      :showLineChart="showLineChart"
-      :showInitScatterChart="showInitScatterChart"
-      :showMidScatterChart="showMidScatterChart"
-      :showFinalScatterChart="showFinalScatterChart"
-      :lineChartPathParam="lineChartPathParam"
-      :initScatterModelId="initScatterModelId"
-      :midScatterModelId="midScatterModelId"
-      :finalScatterModelId="finalScatterModelId"
-    />
-  </div>
-</template>
-
-<script setup lang='ts'>
-import { ref } from 'vue';
-import ModelIterationVisualization from './ModelIterationVisualization.vue';
-
-const showLineChart = ref(true);
-const showInitScatterChart = ref(true);
-const showMidScatterChart = ref(true);
-const showFinalScatterChart = ref(true);
-
-// 直接在代码里定义好参数
-const lineChartPathParam = ref('reduce'); 
-const initScatterModelId = ref(24);
-const midScatterModelId = ref(25);
-const finalScatterModelId = ref(26);
-</script>
-
-<style scoped>
-#app {
-  font-family: Avenir, Helvetica, Arial, sans-serif;
-  -webkit-font-smoothing: antialiased;
-  -moz-osx-font-smoothing: grayscale;
-  text-align: center;
-  color: #2c3e50;
-  margin-top: 60px;
-}
-</style>    

+ 0 - 41
src/views/menu/SoilAcidificationIterativeEvolution.vue

@@ -1,41 +0,0 @@
-<template>
-  <div id="app">
-    <ModelIterationVisualization
-      :showLineChart="showLineChart"
-      :showInitScatterChart="showInitScatterChart"
-      :showMidScatterChart="showMidScatterChart"
-      :showFinalScatterChart="showFinalScatterChart"
-      :lineChartPathParam="lineChartPathParam"
-      :initScatterModelId="initScatterModelId"
-      :midScatterModelId="midScatterModelId"
-      :finalScatterModelId="finalScatterModelId"
-    />
-  </div>
-</template>
-
-<script setup lang='ts'>
-import { ref } from 'vue';
-import ModelIterationVisualization from './ModelIterationVisualization.vue';
-
-const showLineChart = ref(true);
-const showInitScatterChart = ref(true);
-const showMidScatterChart = ref(true);
-const showFinalScatterChart = ref(true);
-
-// 直接在代码里定义好参数
-const lineChartPathParam = ref('reflux'); 
-const initScatterModelId = ref(17);
-const midScatterModelId = ref(18);
-const finalScatterModelId = ref(19);
-</script>
-
-<style scoped>
-#app {
-  font-family: Avenir, Helvetica, Arial, sans-serif;
-  -webkit-font-smoothing: antialiased;
-  -moz-osx-font-smoothing: grayscale;
-  text-align: center;
-  color: #2c3e50;
-  margin-top: 60px;
-}
-</style>    

+ 0 - 18
src/views/menu/SoilPro.vue

@@ -1,18 +0,0 @@
-<!-- 引入组件的父组件 -->
-<template>
-  <div class="parent-container">
-    <Introduce :targetId="1" />
-  </div>
-</template>
-
-<script setup lang="ts">
-import Introduce from './Introduce.vue';
-</script>
-
-<style scoped lang="scss">
-.parent-container {
-  padding: 20px;
-  background-color: #f0f8ff; 
-}
-</style>
-

+ 0 - 17
src/views/menu/Unit.vue

@@ -1,17 +0,0 @@
-<!--团体信息-->
-<template>
-  <div class="parent-container">
-    <Introduce :targetId="4" />
-  </div>
-</template>
-
-<script setup lang="ts">
-import Introduce from './Introduce.vue'
-</script>
-
-<style scoped>
-.parent-container {
-  padding: 20px;
-  background-color: #f0f8ff; 
-}
-</style>

+ 0 - 77
src/views/menu/leafletMapView.vue

@@ -1,77 +0,0 @@
-<template>
-  <div class="map-container">
-    <div ref="mapContainer" class="leaflet-map"></div>
-  </div>
-</template>
-
-<script setup>
-import { ref, onMounted, onBeforeUnmount } from 'vue'
-import L from 'leaflet'
-import 'leaflet/dist/leaflet.css'
-import 'leaflet.chinatmsproviders'
-
-// 地图实例引用
-const mapContainer = ref(null)
-let map = null
-
-// 初始化地图
-const initMap = () => {
-  if (!mapContainer.value) return
-
-  // 天地图配置(需要有效token)
-  const tileOptions = {
-    key: '1f743f9cbdde9055c268c6e56dc32714', // 替换为实际token
-    minZoom: 1,
-    maxZoom: 19
-  }
-
-  // 创建图层
-  const normalm = L.tileLayer.chinaProvider('TianDiTu.Normal.Map', tileOptions)
-  const normala = L.tileLayer.chinaProvider('TianDiTu.Normal.Annotion', tileOptions)
-  const normal = L.layerGroup([normalm, normala])
-
-  // 初始化地图实例
-  map = L.map(mapContainer.value, {  // 正确使用ref引用
-    center: [39.736861, 116.139913],
-    zoom: 16,
-    layers: [normal]
-  })
-
-  // 添加标记
-  L.marker([39.736861, 116.139913])
-    .addTo(map)
-    .bindPopup("坐标位置")
-}
-
-// 生命周期钩子
-onMounted(() => {
-  initMap()
-})
-
-onBeforeUnmount(() => {
-  if (map) {
-    map.remove()
-  }
-})
-</script>
-
-<style scoped>
-.map-container {
-  width: 100%;
-  height: 100vh;
-}
-
-.leaflet-map {
-  height: 600px;
-  width: 100%;
-  margin: 0 auto;
-  border-radius: 8px;
-  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
-}
-
-@media (max-width: 768px) {
-  .leaflet-map {
-    height: 400px;
-  }
-}
-</style>

+ 0 - 474
src/views/menu/loginView.vue

@@ -1,474 +0,0 @@
-<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") }}
-          </el-button>
-        </div>
-        <div class="register-link" style="margin-top: 20px; text-align: center">
-          <a @click="toggleForm">{{ $t("login.registerLink") }}</a>
-        </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-button>
-        </div>
-        <div class="button-group">
-          <!-- 返回登录按钮 -->
-          <el-button @click="toggleForm">
-            {{ $t("register.backToLoginButton") }}
-          </el-button>
-        </div>
-      </el-form-item>
-    </el-form>
-  </div>
-</template>
-
-<script lang="ts" setup>
-import { reactive, ref, computed } from "vue";
-import { ElMessage, ElForm } from "element-plus";
-import axios from "axios";
-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 form = reactive({
-  name: "",
-  password: "",
-});
-console.log("form 的类型:", typeof form);
-
-const registerForm = reactive({
-  name: "",
-  password: "",
-  confirmPassword: "",
-});
-const formRef = ref<InstanceType<typeof ElForm> | null>(null);
-const registerFormRef = ref<InstanceType<typeof ElForm> | null>(null);
-const loading = ref(false);
-const { t, locale } = useI18n(); // 使用 Composition API 模式
-
-// 获取当前语言名称
-const currentLanguageName = computed(() => {
-  return locale.value === "zh" ? "English" : "中文";
-});
-
-// 切换语言
-const toggleLanguage = () => {
-  locale.value = locale.value === "zh" ? "en" : "zh";
-  localStorage.setItem("lang", locale.value); // 更新 localStorage
-};
-
-// 获取当前用户类型名称
-const currentUserTypeName = computed(() => {
-  return userType.value === "user"
-    ? t("login.switchToAdmin")
-    : t("login.switchToUser");
-});
-
-// 切换用户类型
-const toggleUserType = () => {
-  userType.value = userType.value === "user" ? "admin" : "user";
-};
-
-// 切换登录/注册表单
-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,
-      });
-      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
-        };
-        store.saveToken(userInfo);
-        console.log("保存的Token信息:", store.token);
-        ElMessage.success(i18n.global.t("login.loginSuccess"));
-        if (res.data.success) {
-          if (userType.value === "user") {
-            console.log("普通用户登录成功,准备跳转到 selectCityAndCounty");
-            router.push({ name: "selectCityAndCounty" }).then(() => {
-              console.log("loginView.vue:185 成功跳转到 selectCityAndCounty");
-            }).catch((err) => {
-              console.error("跳转到 selectCityAndCounty 失败:", err);
-            });
-          } else {
-            const redirect =
-              typeof route.query.redirect === "string"
-                ? route.query.redirect
-                : "/";
-            router.push(redirect);
-          }
-        }
-      } else {
-        ElMessage.error(res.data.message || i18n.global.t("login.loginFailed"));
-      }
-    } catch (error) {
-      if (axios.isAxiosError(error)) {
-        console.error(
-          `HTTP Error: ${error.message}, status code: ${error.response?.status}`
-        );
-        ElMessage.error(`HTTP Error: ${error.response?.statusText}`);
-      } else {
-        console.error(error);
-        ElMessage.error(i18n.global.t("login.loginFailed"));
-      }
-    } finally {
-      loading.value = false;
-    }
-  });
-};
-
-// 注册提交
-const onRegister = async () => {
-  if (!registerFormRef.value) return;
-  registerFormRef.value.validate(async (valid: boolean) => {
-    if (!valid) return;
-
-    if (registerForm.password !== registerForm.confirmPassword) {
-      ElMessage.error(i18n.global.t("register.passwordMismatch"));
-      return;
-    }
-
-    try {
-      const res = await register({
-        name: registerForm.name,
-        password: registerForm.password,
-      });
-      if (res.data.success) {
-        ElMessage.success(i18n.global.t("register.registerSuccess"));
-        toggleForm(); // 切换回登录表单
-      } else {
-        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"));
-      }
-    } finally {
-      loading.value = false;
-    }
-  });
-};
-
-// 验证规则
-const rules = reactive({
-  name: [
-    {
-      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",
-    },
-  ],
-});
-
-const registerRules = reactive({
-  name: [
-    {
-      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",
-    },
-  ],
-  confirmPassword: [
-    {
-      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")));
-        } else {
-          callback();
-        }
-      },
-      trigger: "blur",
-    },
-  ],
-});
-</script>
-
-<style scoped>
-.auth {
-  background: linear-gradient(135deg, #74ebd5, #acb6e5); /* 渐变背景 */
-  height: 100vh;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  font-family: "Arial", sans-serif; /* 更现代的字体 */
-}
-
-.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);
-  }
-}
-
-.form-header {
-  display: flex;
-  justify-content: space-between; /* 两端对齐 */
-  margin-bottom: 30x; /* 更大的间距 */
-}
-
-.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; /* 居中文本 */
-}
-
-.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); /* 增强阴影 */
-}
-
-.form-title {
-  text-align: center;
-  font-size: 24px; /* 更大的字体 */
-  font-weight: bold;
-  margin: 20px 20px; /* 更大的外边距 */
-}
-
-.el-form-item__label {
-  font-weight: bold;
-  color: #555; /* 更柔和的颜色 */
-}
-
-.el-input__inner {
-  border: 1px solid #ccc;
-  border-radius: 8px; /* 更大的圆角 */
-  padding: 12px; /* 更大的内边距 */
-  transition: border-color 0.3s ease, box-shadow 0.3s ease;
-}
-
-.el-input__inner:focus {
-  border-color: #409eff;
-  box-shadow: 0 0 5px rgba(64, 158, 255, 0.5); /* 聚焦时的阴影 */
-}
-
-.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; /* 增加内边距 */
-}
-
-.el-button:hover {
-  background: linear-gradient(135deg, #66a6ff, #89f7fe); /* 悬停时渐变反转 */
-  transform: translateY(-3px); /* 悬停时轻微上移 */
-  box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); /* 增强阴影 */
-}
-
-.el-button--primary {
-  background: linear-gradient(135deg, #43cea2, #185a9d); /* 主按钮渐变背景 */
-}
-
-.el-button--primary:hover {
-  background: linear-gradient(135deg, #185a9d, #43cea2); /* 悬停时渐变反转 */
-}
-
-.button-group {
-  display: flex;
-  justify-content: center; /* 按钮居中 */
-  margin-top: 30px; /* 更大的间距 */
-}
-
-.register-link {
-  text-align: center;
-  margin: 10px 80px;
-  font-size: 14px;
-  color: #409eff;
-  cursor: pointer;
-}
-.register-link a:hover {
-  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;
-  }
-}
-</style>

+ 0 - 1433
src/views/menu/tencentMapView.vue

@@ -1,1433 +0,0 @@
-<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>
-    <div v-if="error" class="error">{{ error }}</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" />
-        显示韶关市评估单元
-      </label>
-      <label>
-        <input type="checkbox" v-model="state.showSurveyData" @change="toggleSurveyDataLayer" />
-        显示韶关市调查数据
-      </label>
-      <!-- 截图控制 -->
-      <div class="export-controls">
-        <button @click="exportMapImage" :disabled="!isMapReady">
-          {{ isExporting ? '生成中...' : '导出截图' }}
-        </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 html2canvas from 'html2canvas'
-import RegionSelector from '@/components/RegionSelector.vue'
-
-const isExporting = ref(false)
-const isMapReady = ref(false)
-const exportSettings = reactive({
-  quality: 0.9,
-  showMapControls: false,
-  showWatermark: true
-})
-const mapContainer = ref(null)
-const activeMarker = ref(null)
-const error = ref(null)
-let activeTempMarker = ref(null)
-let infoWindow = ref(null)
-let map = null
-let markersLayer = null
-let overlay = null
-const state = reactive({
-  showOverlay: false,
-  showSoilTypes: true,
-  showSurveyData: true,
-  excelData: [],
-  lastTapTime: 0
-})
-let soilTypeLayer = null
-let geoJSONLayer; 
-let currentInfoWindow = null;
-const surveyDataLayer = ref(null); // 保持响应式引用
-let multiPolygon;
-const districtLayers = ref(new Map()) // 存储区县图层
-const combinedSurveyFeatures = ref([]); // 存储原始数据
-const currentSurveyFilter = ref([]); // 当前选中区域
-const isLoading = ref(false)
-
-const categoryColors = { // 分类颜色配置
-  '优先保护类': '#00C853', // 绿色
-  '安全利用类': '#FFD600', // 黄色
-  '严格管控类': '#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) => {
-    if (window.TMap?.service?.Geocoder) {
-      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) {
-        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 initData = () => {
-  state.excelData = [
-      { 土壤编号: "土1", 地点: "广西武鸣", dust_emissions: 5.34, longitude: 106.476143, latitude: 23.891756 },
-      { 土壤编号: "土2", 地点: "广西河池", dust_emissions: 3.96, longitude: 107.476143, latitude: 24.891756 },
-      { 土壤编号: "土3", 地点: "海南澄迈老城镇罗驿村委会罗驿洋", dust_emissions: 4.56, longitude: 110.125, latitude: 19.901756 },
-      { 土壤编号: "土4", 地点: "广东江门新会", dust_emissions: 4.0, longitude: 109.476143, latitude: 22.461756 },
-      { 土壤编号: "土5", 地点: "广州增城Z6", dust_emissions: 4.77, longitude: 110.476143, latitude: 21.891756 },
-      { 土壤编号: "土6", 地点: "广州增城Z8", dust_emissions: 4.59, longitude: 111.476143, latitude: 22.891756 },
-      { 土壤编号: "土7", 地点: "湖南岳阳", dust_emissions: 5.14, longitude: 112.476143, latitude: 23.891756 },
-      { 土壤编号: "土8", 地点: "广东韶关武江", dust_emissions: 5.07, longitude: 113.476143, latitude: 24.891756 },
-      { 土壤编号: "土9", 地点: "海南临高头星村", dust_emissions: 4.12, longitude: 109.684993, latitude: 19.83774 },
-      { 土壤编号: "土10", 地点: "海南临高周礼村", dust_emissions: 5.0, longitude: 109.710703, latitude: 19.89222 },
-      { 土壤编号: "土11", 地点: "海南澄迈金江", dust_emissions: 4.6, longitude: 110.069537, latitude: 19.81189 },
-      { 土壤编号: "土12", 地点: "海南临高南贤村", dust_emissions: 4.2, longitude: 109.768714, latitude: 19.874323 },
-      { 土壤编号: "土13", 地点: "海南澄迈金江北让村", dust_emissions: 4.5, longitude: 110.096765, latitude: 19.814288 },
-      { 土壤编号: "土14", 地点: "广西扶绥", dust_emissions: 4.71, longitude: 107.7717789, latitude: 22.5166902 },
-      { 土壤编号: "土15", 地点: "广西江州", dust_emissions: 4.31, longitude: 107.56347787, latitude: 22.6022203 },
-      { 土壤编号: "土16", 地点: "广西龙州", dust_emissions: 5.15, longitude: 106.7870847, latitude: 22.3496497 },
-      { 土壤编号: "土17", 地点: "广西大新", dust_emissions: 4.71, longitude: 107.0230641, latitude: 22.5857946 },
-      { 土壤编号: "土18", 地点: "湖南岳阳荣家湾", dust_emissions: 5.04, longitude: 113.059629, latitude: 29.267061 },
-      { 土壤编号: "土19", 地点: "湖南长沙", dust_emissions: 5.08, longitude: 113.059629, latitude: 28.440613 },
-      { 土壤编号: "土20", 地点: "浙江", dust_emissions: 4.8, longitude: 111.45527, latitude: 24.395235 },
-      { 土壤编号: "土21", 地点: "云南陆良", dust_emissions: 4.67, longitude: 112.45527, latitude: 25.395235 },
-      { 土壤编号: "土22", 地点: "南昌横龙镇南园组", dust_emissions: 4.8, longitude: 113.45527, latitude: 26.395235 },
-      { 土壤编号: "土23", 地点: "南昌横龙枫塘南园", dust_emissions: 5.1, longitude: 114.45527, latitude: 27.395235 },
-      { 土壤编号: "土24", 地点: "南昌横龙镇院塘村", dust_emissions: 4.27, longitude: 114.852, latitude: 27.3947 },
-      { 土壤编号: "土25", 地点: "江西山庄乡秀水村黄田组", dust_emissions: 4.27, longitude: 114.852, latitude: 27.5247 },
-      { 土壤编号: "土26", 地点: "贵州双星村", dust_emissions: 4.7, longitude: 106.852, latitude: 27.3147},
-      { 土壤编号: "土27", 地点: "湖南永州八宝镇唐家州", dust_emissions: 4.57, longitude: 113.952, latitude: 26.08147 },
-      { 土壤编号: "土28", 地点: "湖南永州金洞", dust_emissions: 5.3, longitude: 112.1564, latitude: 26.1685 },
-      { 土壤编号: "土29", 地点: "祁阳县中国农业科学院红壤实验室", dust_emissions: 4.75, longitude: 111.4, latitude: 22.24 },
-      { 土壤编号: "土30", 地点: "福建福州1", dust_emissions: 4.31, longitude: 112.4, latitude: 23.24 },
-      { 土壤编号: "土31", 地点: "福建福州2", dust_emissions: 4.38, longitude: 113.4, latitude: 24.24 },
-      { 土壤编号: "土32", 地点: "广东省韶关市南雄市下塅村", dust_emissions: 5.51, longitude: 114.4, latitude: 25.24 },
-      { 土壤编号: "土33", 地点: "广东省韶关市南雄市河塘西216米", dust_emissions: 6.44, longitude: 114.28, latitude: 25.14 },
-      { 土壤编号: "土34", 地点: "广东省韶关市南雄市上何屋西南500米", dust_emissions: 5.25, longitude: 114.15, latitude: 24.86 },
-      { 土壤编号: "土35", 地点: "广东省南雄市雄州街道林屋", dust_emissions: 4.62333333333333, longitude: 114.23, latitude: 25.4 },
-      { 土壤编号: "土36", 地点: "广东省台山都斛镇", dust_emissions: 3.0, longitude: 112.34, latitude: 27.31 },
-      { 土壤编号: "土52", 地点: "湖南省长沙市浏阳市永安镇千鹭湖", dust_emissions: 4.72333333333333, longitude: 113.34, latitude: 28.31 },
-      { 土壤编号: "土53", 地点: "湖南省长沙市浏阳市湖南农大实习基地", dust_emissions: 5.55333333333333, longitude: 113.83, latitude: 28.3 },
-      { 土壤编号: "土54", 地点: "湖南省邵阳市罗市镇1", dust_emissions: 4.64, longitude: 110.35, latitude: 25.47 },
-      { 土壤编号: "土55", 地点: "湖南省邵阳市罗市镇2", dust_emissions: 5.01333333333333, longitude: 111.35, latitude: 26.47 },
-      { 土壤编号: "土56", 地点: "湖南省邵阳市罗市镇3", dust_emissions: 5.18, longitude: 112.35, latitude: 27.47 },
-      { 土壤编号: "土57", 地点: "长沙县高桥镇的省农科院高桥科研基地1", dust_emissions: 5.1, longitude: 113.35, latitude: 28.47 },
-      { 土壤编号: "土58", 地点: "长沙县高桥镇的省农科院高桥科研基地2", dust_emissions: 4.92, longitude: 113.35, latitude: 28.47 },
-      { 土壤编号: "土59", 地点: "湖南省长沙市望城区桐林坳社区", dust_emissions: 3.0, longitude: 112.8, latitude: 28.37 },
-      { 土壤编号: "土60", 地点: "湖南省益阳市赫山区泥江口镇", dust_emissions: 3.0, longitude: 107.37, latitude: 21.92 },
-      { 土壤编号: "土70", 地点: "南宁市兴宁区柳杨路26号", dust_emissions: 3.0, longitude: 108.37, latitude: 22.92 },
-      { 土壤编号: "土71", 地点: "南宁市兴宁区柳杨路广西私享家家具用品", dust_emissions: 3.0, longitude: 108.37, latitude: 23.94 },
-      { 土壤编号: "土72", 地点: "南宁市兴宁区004乡道", dust_emissions: 6.24666666666667, longitude: 108.39, latitude: 24.92 },
-      { 土壤编号: "土73", 地点: "南宁市兴宁区G7201南宁绕城高速", dust_emissions: 3.0, longitude: 108.4, latitude: 25.94 },
-      { 土壤编号: "土74", 地点: "南宁市兴宁区012县道", dust_emissions: 3.0, longitude: 108.41, latitude: 26.92 },
-      { 土壤编号: "土75", 地点: "南宁市兴宁区那况路168号", dust_emissions: 3.0, longitude: 108.4, latitude: 27.9 },
-      { 土壤编号: "土76", 地点: "南宁市西乡塘区翊武路", dust_emissions: 5.37, longitude: 108.35, latitude: 28.96 },
-      { 土壤编号: "土77", 地点: "南宁市西乡塘区坛洛镇", dust_emissions: 3.0, longitude: 107.85, latitude: 29.92 },
-      { 土壤编号: "土81", 地点: "铜仁职业技术学院", dust_emissions: 4.0, longitude: 108.85, latitude: 27.34 },
-      { 土壤编号: "土87", 地点: "江西省红壤及种质资源研究所(进贤基地)1", dust_emissions: 4.55, longitude: 116.17, latitude: 28.34 },
-      { 土壤编号: "土88", 地点: "江西省红壤及种质资源研究所(进贤基地)2", dust_emissions: 4.99333333333333, longitude: 116.17, latitude: 28.34 }
-  ].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);
-}
-
-// 初始化地图
-const initMap = async () => {
-  try {
-   isLoading.value = true
-    await loadSDK()
-    
-    map = new TMap.value.Map(mapContainer.value, {
-      center: new TMap.value.LatLng(24.81088,113.59762),
-      zoom: 12,
-      renderOptions: {
-        preserveDrawingBuffer: true, // 必须开启以支持截图
-        antialias: true
-      }
-    })
-
-    // const defaultStyle = new TMap.value.MarkerStyle({
-    //   width: 34,
-    //   height: 34,
-    //   anchor: { x: 17, y: 34 },
-    //   src: markerIcon
-    // })
-    
-    // markersLayer = new TMap.value.MultiMarker({
-    //   map: map,
-    //   styles: { default: defaultStyle }
-    // })
-    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)
-    // 新增地图就绪状态监听
-    map.on('idle', () => {
-      isMapReady.value = true
-    })
-
-    loadData()
-    updateMarkers()
-  } catch (err) {
-    error.value = err.message
-  } finally {
-    isLoading.value = false
-  }
-}
-
-// 加载数据并创建标记
-const loadData = () => {
-  const geometries = state.excelData.map(item => ({
-    id: item.土壤编号,
-    styleId: 'default',
-    position: new TMap.value.LatLng(item.latitude, item.longitude),
-    properties: {
-      title: item.地点,
-      phValue: parseFloat(item.dust_emissions).toFixed(2),
-      isTemp: false
-    },
-  }))
-  markersLayer.setGeometries(geometries)
-}
-
-// 新增截图方法
-const exportMapImage = async () => {
-  try {
-    isExporting.value = true
-    
-    // 等待地图稳定
-    await new Promise(resolve => setTimeout(resolve, 300))
-
-    const canvas = await html2canvas(mapContainer.value, {
-      useCORS: true,
-      scale: window.devicePixelRatio || 2,
-      backgroundColor: null,
-      logging: true,
-      onclone: (clonedDoc) => {
-        // 处理控件可见性
-        clonedDoc.querySelectorAll('.tmap-control').forEach(control => {
-          control.style.visibility = exportSettings.showMapControls ? 'visible' : 'hidden'
-        })
-        
-        // 添加水印
-        if(exportSettings.showWatermark){
-          const watermark = document.createElement('div')
-          watermark.style = `
-            position: absolute;
-            bottom: 20px;
-            right: 20px;
-            color: rgba(0,0,0,0.2);
-            font-size: 24px;
-            transform: rotate(-15deg);
-            z-index: 9999;
-          `
-          watermark.textContent = '机密地图 - 禁止外传'
-          clonedDoc.body.appendChild(watermark)
-        }
-      }
-    })
-
-    // 转换为Blob
-    canvas.toBlob(blob => {
-      const link = document.createElement('a')
-      link.download = `土壤地图_${new Date().toISOString().slice(0,10)}.png`
-      link.href = URL.createObjectURL(blob)
-      link.click()
-      URL.revokeObjectURL(link.href)
-    }, 'image/png', exportSettings.quality)
-
-  } catch (error) {
-    console.error('截图失败:', error)
-    error.value = '截图失败,请尝试缩小地图层级'
-    setTimeout(() => error.value = null, 3000)
-  } finally {
-    isExporting.value = false
-  }
-}
-
-// 更新标记
-const updateMarkers = () => {
-  const markers = state.excelData.map((item, index) => ({
-    id: `marker-${index + 1}`,
-    styleId: 'default',
-    position: new TMap.value.LatLng(item.latitude, item.longitude),
-    properties: {
-      title: item.地点,
-      phValue: item.dust_emissions,
-      isTemp: false
-    },
-  }))
-  markersLayer.setGeometries(markers)
-}
-
-// 新增Marker点击事件处理
-const handleMarkerClick = (e) => {
-  const marker = e.geometry
-  if (!marker) return
-
-  // 关闭之前的信息窗口
-  if (activeMarker.value?.id === marker.id) {
-    infoWindow.close()
-    activeMarker.value = null
-    return
-  }
-
-  // 创建信息窗口内容
-  const content = `
-    <div style="padding:12px">
-      <h3>${marker.properties.title}</h3>
-      <p>PH值: ${marker.properties.phValue}</p>
-    </div>
-  `
-
-  // 打开信息窗口
-  infoWindow = new TMap.value.InfoWindow({
-    map: map,
-    position: marker.position,
-    content: content,
-    offset: {x: 0, y: -32}
-  })
-
-  // 记录当前激活的Marker
-  activeMarker.value = marker
-
-  // 点击其他区域关闭窗口
-  map.on('click', closeInfoWindow)
-}
-
-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: manageTempMarker.activeTempMarker.value.getPosition(),
-//       content: `
-//         <div style="padding:12px">
-//           <h3>${manageTempMarker.activeTempMarker.value.properties.title}</h3>
-//           <p>PH值: ${manageTempMarker.activeTempMarker.value.properties.phValue}</p>
-//         </div>
-//       `
-//     })
-//     infoWindow.value.open()
-//   } catch (error) {
-//     console.error('操作失败详情:', error)
-//     error.value = error.message.includes('非法坐标') 
-//       ? '请点击有效地图区域' 
-//       : '服务暂时不可用,请稍后重试'
-//     setTimeout(() => error.value = null, 3000)
-//   }
-// }
-
-// // 关闭信息窗口时同步移除临时标记
-// const closeInfoWindow = () => {
-//   if (activeTempMarker.value) {
-//     manageTempMarker.remove()
-//   }
-//   if (infoWindow.value) {
-//     infoWindow.value.close()
-//     infoWindow.value = null
-//   }
-//   map.off('click', closeInfoWindow)
-// }
-
-
-// // 验证坐标有效性
-// 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 
-// }
-// }
-
-async function loadGeoJSON(url) {
-  const response = await fetch(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) {
-    geoJSONLayer.setMap(null);
-    geoJSONLayer = null;
-  }
-  // 创建 GeoJSONLayer
-  geoJSONLayer = new TMap.value.vector.GeoJSONLayer({
-    map: map,
-    data: geojsonData,
-    zIndex: 10,
-    polygonStyle: new TMap.value.PolygonStyle({ // 必须用 PolygonStyle 类实例
-      color: 'rgba(255, 0, 0, 0.25)', 
-      showBorder: true,
-      borderColor: '#FF0000', 
-      borderWidth: 2
-    })
-  });
-
-  // 获取多边形覆盖层
-  multiPolygon = geoJSONLayer.getGeometryOverlay('polygon');
-
-  // 高亮选中图层
-  const highlightLayer = new TMap.value.MultiPolygon({
-        map,
-        zIndex: 20,
-        styles: {
-          highlight: new TMap.value.PolygonStyle({ // 注意要改为 PolygonStyle
-            color: 'rgba(0, 123, 255, 0.5)',      // 半透明蓝色填充
-            borderColor: '#00FF00',               // 荧光绿边框
-            borderWidth: 2,                       // 加粗边框
-            showBorder: true,
-            extrudeHeight: 15                      // 3D拔起效果[1](@ref)
-          })
-      }});
-   // 高亮区域
-   let highlightGeometry = {
-        id: 'highlightGeo',
-        styleId: 'highlight'
-      }  
-
-    // 绑定点击事件(替换原有的事件监听)
-    multiPolygon.on('hover', (e) => {
-      if (e.geometry) {
-          // 鼠标选中时高亮区域覆盖
-          highlightGeometry.paths = e.geometry.paths;
-          highlightLayer.updateGeometries([highlightGeometry]);
-        } else {
-          // 鼠标移出时取消高亮区域覆盖
-          highlightLayer.setGeometries([]);
-        }
-      })
-};
-
-
-// 加载调查数据并初始化图层
-const initSurveyDataLayer = async (map) => {
-  try {
-    isLoading.value = true
-    const geoJsonFiles = [
-      'https://soilgd.com:8000/api/vector/export/all?table_name=surveydata',
-      '/data/河池土壤样品.geojson',
-      '/data/河池农产品样品.geojson',
-    ];
-
-    const surveyDataArray = await Promise.all(geoJsonFiles.map(loadGeoJSON));
-    const features = surveyDataArray.flatMap(geoData => geoData.features);
-    
-    // 保存原始数据用于过滤
-    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(mergedCategoryColors[category])
-      })
-    }));
-
-    // 初始化图层(处理缺失属性)
-    surveyDataLayer.value = new TMap.value.MultiMarker({
-      map: map,
-      styles: Object.assign({}, ...pointStyles.map(s => ({ [s.id]: s.style }))),
-      geometries: combinedSurveyFeatures.value.map(feature => ({
-        id: feature.properties.ID || feature.properties.OBJECTID,
-        styleId: feature.properties.H_XTFX || feature.properties.h_xtfx || '其他', // 设置默认值
-        position: new TMap.value.LatLng(
-          feature.geometry.coordinates[1],
-          feature.geometry.coordinates[0]
-        ),
-        properties: {
-          ...feature.properties,
-          // 强制添加H_XTFX字段保证数据一致性
-          H_XTFX: feature.properties.H_XTFX|| '其他' 
-        }
-      }))
-    });
-
-    // 添加点击事件
-    surveyDataLayer.value.on('click', (event) => {
-      const prop = event.geometry.properties;
-      if (currentInfoWindow) currentInfoWindow.close();
-      currentInfoWindow = new TMap.value.InfoWindow({
-        map: map,
-        position: event.geometry.position,
-        content: `
-          <div class="point-info">
-            <h3>${prop.XMC} ${prop.ZMC} ${prop.CMC}</h3>
-            <p>${prop.H_XTFX}</p>
-          </div>
-        `
-      });
-    });
-  } catch (error) {
-    console.error("调查数据加载失败:", error);
-    // 添加详细错误日志
-    console.groupCollapsed("[错误详情]");
-    console.error("错误对象:", error);
-    console.trace("调用堆栈");
-    console.groupEnd();
-  } finally {
-    isLoading.value = false
-  }
-};
-
-// 生成圆形图标的base64数据
-const createColoredCircle = (color) => {
-  const canvas = document.createElement('canvas');
-  canvas.width = 24;
-  canvas.height = 24;
-  const ctx = canvas.getContext('2d');
-  
-  // 绘制圆形
-  ctx.beginPath();
-  ctx.arc(12, 12, 8, 0, 2 * Math.PI);
-  ctx.fillStyle = color;
-  ctx.fill();
-  
-  // 添加白色边框
-  ctx.strokeStyle = 'black';
-  ctx.lineWidth = 2;
-  ctx.stroke();
-  
-  return canvas.toDataURL();
-};
-
-  const toggleSoilTypeLayer = () => {
-    if (!multiPolygon) {
-      console.error('利用类型图层未初始化');
-      return;
-    }
-    if (multiPolygon) {
-      multiPolygon.setVisible(state.showSoilTypes);
-    }
-  };
-
-  const toggleSurveyDataLayer = () => {
-    if (!surveyDataLayer) {
-      console.error('调查数据图层未初始化');
-      return;
-    }
-    if (surveyDataLayer) {
-      surveyDataLayer.value.setVisible(state.showSurveyData);
-    }
-  };
-
-// // 切换覆盖层
-// const toggleOverlay = () => {
-//   if (state.showOverlay) {
-//     overlay = new TMap.value.ImageGroundLayer({
-//       map: map,
-//       bounds: new TMap.value.LatLngBounds(
-//         new TMap.value.LatLng(18.17, 103.55), 
-//         new TMap.value.LatLng(32.32, 119.82)
-//       ),
-//       src: 'https://soilgd.com/images/farmland_cut.png'
-//     })
-//   } else {
-//     if (overlay) {
-//       overlay.setMap(null)
-//       overlay = null
-//     }
-//   }
-// }
-
-onMounted(async () => {
-  try {
-    await loadSDK()
-    initData()
-    await initMap()
-    await initBaseLayers()
-  } catch (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 (soilTypeLayer) {
-    soilTypeLayer.destroy();
-    soilTypeLayer = 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 {
-  flex: 1;
-  height: calc(100vh - 48px); /* 对应新的工具栏高度 */
-  position: relative;
-  background: #f5f5f7;
-}
-.control-panel {
-  position: fixed;
-  top: 80px;  /* 下移避开工具栏 */
-  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);
-}
-
-/* 新增加载提示样式 */
-.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: 50px;
-  height: 50px;
-  border: 5px solid #f3f3f3;
-  border-top: 5px solid #3876ff;
-  border-radius: 50%;
-  animation: spin 1s linear infinite;
-  margin-bottom: 16px;
-}
-
-@keyframes spin {
-  0% { transform: rotate(0deg); }
-  100% { transform: rotate(360deg); }
-}
-
-/* 响应式调整 */
-@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;
-  }
-}
-
-/* 信息窗口样式 */
-:deep(.tmap-infowindow) {
-  padding: 12px;
-  min-width: 200px;
-  border-radius: 8px;
-  box-shadow: 0 2px 8px rgba(0,0,0,0.15);
-  background-color: white;
-}
-
-:deep(.tmap-infowindow h3) {
-  margin: 0 0 8px;
-  font-size: 16px;
-  color: #333;
-}
-
-:deep(.tmap-infowindow p) {
-  margin: 4px 0;
-  color: #666;
-  font-size: 14px;
-}
-.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;
-    }
-  }
-}
-
-/* 动态类别颜色 */
-.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>

BIN
structure.txt


+ 1 - 2
tsconfig.app.json

@@ -1,4 +1,3 @@
-// filepath: c:\Users\lhf\OneDrive\Desktop\AcidWeb\tsconfig.app.json
 {
   "extends": "@vue/tsconfig/tsconfig.dom.json",
   "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
@@ -9,7 +8,7 @@
     "src/views/User/introduction/IntroUpdateModal.vue", // Exclude missing file
     "src/views/Admin/dataManagement/Visualizatio.vue", // Exclude missing file
     "src/views/Admin/dataManagement/Visualization.vue", // Exclude missing file
-    "src/views/Admin/parameterConfig/thres.vue", // Exclude missing file
+    "src/views/Admin/modelManagement/AcidReductionModel/thres.vue", // Exclude missing file
     "src/views/User/heavyMetalFluxCalculation/outputFluxCalculation/outputFluxCalculation.vue" // Exclude missing file
   ],
   "compilerOptions": {

+ 0 - 5
vite.config.ts

@@ -14,11 +14,6 @@ import IconsResolver from 'unplugin-icons/resolver'
 // https://vite.dev/config/
 export default defineConfig({
   plugins: [
-    {
-      "compilerOptions": {
-        "types": ["node"]
-      }
-    },
     vue(),
     vueDevTools(),
     AutoImport({