Browse Source

整合代码,完善地图功能

yangtaodemon 3 months ago
parent
commit
1dbe0654db

+ 2 - 1
.env

@@ -1 +1,2 @@
-VITE_API_URL= 'https://127.0.0.1:5000'
+VITE_API_URL= 'https://soilgd.com:5000'
+VITE_TMAP_KEY='2R4BZ-FF4RM-Q6C6U-6TCJL-O2EN5-DVFH5'

+ 0 - 1
components.d.ts

@@ -13,7 +13,6 @@ declare module 'vue' {
     AppLayout: typeof import('./src/components/layout/AppLayout.vue')['default']
     ElAlert: typeof import('element-plus/es')['ElAlert']
     ElAside: typeof import('element-plus/es')['ElAside']
-    ElAvarar: typeof import('element-plus/es')['ElAvarar']
     ElAvatar: typeof import('element-plus/es')['ElAvatar']
     ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
     ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']

BIN
src/assets/marker.png


+ 2 - 1
src/main.ts

@@ -17,4 +17,5 @@ app.use(router)
 app.use(ElementPlus, {
   locale: zhCn, // 设置Element Plus的语言为中文
 })
-app.mount('#app')
+app.mount('#app')
+

+ 16 - 0
src/shims-vue.d.ts

@@ -14,4 +14,20 @@ declare module '*.vue' {
   import { DefineComponent } from 'vue';
   const component: DefineComponent<{}, {}, any>;
   export default component;
+}
+
+// global.d.ts 或者您的项目中的某个 .d.ts 文件
+
+interface CustomError extends Error {
+  response?: {
+    status: number;
+    data?: any;
+  };
+  message?: string;
+}
+
+declare global {
+  interface ErrorConstructor {
+    new(message?: string): CustomError;
+  }
 }

+ 148 - 78
src/views/ModelSelection.vue

@@ -1,23 +1,42 @@
 <template>
   <div class="model-container">
     <!-- 模型选择 -->
-    <el-select v-model="selectedModelType" placeholder="请选择模型类型" style="margin-bottom: 5px;" @change="selectedModelTypeChanged">
+    <el-select
+      v-model="selectedModelType"
+      placeholder="请选择模型类型"
+      style="margin-bottom: 5px"
+      @change="selectedModelTypeChanged"
+    >
       <el-option
         v-for="type in modelTypes"
         :key="type.value"
         :label="type.label"
-        :value="type.value">
+        :value="type.value"
+      >
       </el-option>
     </el-select>
 
     <!-- 加载失败提示 -->
-    <el-alert v-if="errorOccurred" title="加载数据失败,请检查网络连接或稍后再试。" type="error" show-icon style="margin-bottom: 5px;"></el-alert>
+    <el-alert
+      v-if="errorOccurred"
+      title="加载数据失败,请检查网络连接或稍后再试。"
+      type="error"
+      show-icon
+      style="margin-bottom: 5px"
+    ></el-alert>
 
     <!-- 动态加载的模型板块 -->
-    <el-card class="model-section" v-if="selectedModelType && pagedFilteredModelData.length > 0">
+    <el-card
+      class="model-section"
+      v-if="selectedModelType && pagedFilteredModelData.length > 0"
+    >
       <template #header>
         <div class="card-header">
-          <span>{{ selectedModelType === 'reduce_model' ? '降酸模型(Reduce Model)' : '反酸模型(Reflux Model)' }}</span>
+          <span>{{
+            selectedModelType === "reduce_model"
+              ? "降酸模型(Reduce Model)"
+              : "反酸模型(Reflux Model)"
+          }}</span>
         </div>
       </template>
       <el-table
@@ -29,19 +48,29 @@
         <!-- 复选框列 -->
         <el-table-column align="center" width="50">
           <template #default="scope">
-            <el-checkbox v-model="scope.row.selected" @change="handleSelectionChange(scope.row)"></el-checkbox>
+            <el-checkbox
+              v-model="scope.row.selected"
+              @change="handleSelectionChange(scope.row)"
+            ></el-checkbox>
           </template>
         </el-table-column>
 
         <el-table-column prop="Model_name" label="模型名称" width="180" />
         <el-table-column prop="Data_type" label="数据类型" width="100" />
-        <el-table-column prop="Performance_score" label="性能评分" width="120" />
+        <el-table-column
+          prop="Performance_score"
+          label="性能评分"
+          width="120"
+        />
         <el-table-column prop="Created_at" label="创建时间" width="180" />
       </el-table>
     </el-card>
 
-   <!-- 分页控制 -->
-   <div class="demo-pagination-block" v-if="selectedModelType && filteredModelData.length > 0">
+    <!-- 分页控制 -->
+    <div
+      class="demo-pagination-block"
+      v-if="selectedModelType && filteredModelData.length > 0"
+    >
       <el-pagination
         v-model:current-page="currentPage"
         v-model:page-size="pageSize"
@@ -54,130 +83,171 @@
     </div>
 
     <!-- 切换模型按钮,位于表格下方并居中 -->
-    <div v-if="selectedModelType && pagedFilteredModelData.length > 0" style="text-align:center; margin-top:5px;">
-      <el-button type="primary" size="small" @click="switchModel">切换模型</el-button>
+    <div
+      v-if="selectedModelType && pagedFilteredModelData.length > 0"
+      style="text-align: center; margin-top: 5px"
+    >
+      <el-button type="primary" size="small" @click="switchModel"
+        >切换模型</el-button
+      >
     </div>
 
     <!-- 当没有选择任何模型类型时显示提示信息 -->
-    <div v-if="!selectedModelType && !loading && !errorOccurred" style="text-align:center; padding-top:5px;">
+    <div
+      v-if="!selectedModelType && !loading && !errorOccurred"
+      style="text-align: center; padding-top: 5px"
+    >
       请选择一个模型类型以查看详细信息。
     </div>
 
     <!-- 当筛选结果为空时显示提示信息 -->
-    <div v-if="selectedModelType && filteredModelData.length === 0 && !loading && !errorOccurred" style="text-align:center; padding-top:5px;">
+    <div
+      v-if="
+        selectedModelType &&
+        filteredModelData.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, ElAlert } from 'element-plus'
+import { ref, onMounted, computed } from "vue";
+import axios from "axios";
+import { ElMessage, ElAlert } from "element-plus";
 
 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
-  selected?: boolean // 添加一个可选的selected属性来跟踪是否被选中
+  ModelID: number;
+  Model_name: string;
+  Model_type: string;
+  Created_at: string;
+  Description: string;
+  DatasetID: string;
+  ModelFilePath: string;
+  Data_type: string;
+  Performance_score: number;
+  selected?: boolean; // 添加一个可选的selected属性来跟踪是否被选中
 }
 
-const loading = ref(true)
-const allModels = ref<ModelData[]>([])
-const selectedModelType = ref<string>('reflux_model')
+const loading = ref(true);
+const allModels = ref<ModelData[]>([]);
+const selectedModelType = ref<string>("reflux_model");
 const modelTypes = [
-  { value: 'reduce_model', label: '降酸模型(Reduce Model)' },
-  { value: 'reflux_model', label: '反酸模型(Reflux Model)' }
-]
-const multipleSelection = ref<ModelData[]>([])
-const errorOccurred = ref(false)
+  { value: "reduce_model", label: "降酸模型(Reduce Model)" },
+  { value: "reflux_model", label: "反酸模型(Reflux Model)" },
+];
+const multipleSelection = ref<ModelData[]>([]);
+const errorOccurred = ref(false);
 
 // 分页相关的变量
-const currentPage = ref(1)
-const pageSize = ref(10)
+const currentPage = ref(1);
+const pageSize = ref(10);
 
 // 获取所有模型数据
 const fetchAllModels = async () => {
   try {
-    const requestBody = { table: 'Models' }
-    const response = await axios.post('https://127.0.0.1:5000/table', requestBody)
-    allModels.value = response.data.rows.map(row => ({ ...row, selected: false }))
+    const requestBody = { table: "Models" };
+    const response = await axios.post(
+      "https://127.0.0.1:5000/table",
+      requestBody
+    );
+    // 更新所有模型列表,并设置 selected 属性为 false
+    allModels.value = response.data.rows.map((row: any) => ({
+      ...row,
+      selected: false,
+    }));
   } catch (error) {
-    console.error("Error fetching data:", error.message)
-    ElMessage.error('数据加载失败:' + error.message)
-    errorOccurred.value = true
+    // 类型检查:确认 error 是否具有 message 属性
+    let errorMessage = "未知错误";
+    
+    if (error && typeof error === 'object' && 'message' in error) {
+      errorMessage = (error as { message?: string }).message || errorMessage;
+    }
+
+    console.error("Error fetching data:", errorMessage);
+    ElMessage.error("数据加载失败:" + errorMessage);
+    errorOccurred.value = true;
   } finally {
-    loading.value = false
+    loading.value = false;
   }
-}
+};
 
 // 根据选择的模型类型计算要展示的数据
-const filteredModelData = computed(() => 
-  selectedModelType.value 
-    ? allModels.value.filter(m => m.Model_name.includes(selectedModelType.value))
+const filteredModelData = computed(() =>
+  selectedModelType.value
+    ? allModels.value.filter((m) =>
+        m.Model_name.includes(selectedModelType.value)
+      )
     : []
-)
+);
 
 // 分页后的过滤数据
 const pagedFilteredModelData = computed(() => {
-  const start = (currentPage.value - 1) * pageSize.value
-  const end = start + pageSize.value
-  return filteredModelData.value.slice(start, end)
-})
+  const start = (currentPage.value - 1) * pageSize.value;
+  const end = start + pageSize.value;
+  return filteredModelData.value.slice(start, end);
+});
 
 // 更新选择状态
 const handleSelectionChange = (row: ModelData) => {
   if (row.selected) {
-    multipleSelection.value.push(row)
+    multipleSelection.value.push(row);
   } else {
-    const index = multipleSelection.value.findIndex(r => r.ModelID === row.ModelID)
+    const index = multipleSelection.value.findIndex(
+      (r) => r.ModelID === row.ModelID
+    );
     if (index > -1) {
-      multipleSelection.value.splice(index, 1)
+      multipleSelection.value.splice(index, 1);
     }
   }
-}
+};
 
 // 当模型类型改变时触发的函数
-const selectedModelTypeChanged = () => {}
+const selectedModelTypeChanged = () => {};
 
 // 分页事件处理
 const handleSizeChange = (val: number) => {
-  pageSize.value = val
-}
+  pageSize.value = val;
+};
 const handleCurrentChange = (val: number) => {
-  currentPage.value = val
-}
+  currentPage.value = val;
+};
 
 // 切换模型状态
 const switchModel = async () => {
   if (!multipleSelection.value.length) {
-    ElMessage.warning('请先选择至少一个模型')
-    return
+    ElMessage.warning("请先选择至少一个模型");
+    return;
   }
 
   try {
     for (const model of multipleSelection.value) {
-      await axios.post('https://127.0.0.1:5000/switch-model', {
+      await axios.post("https://127.0.0.1:5000/switch-model", {
         model_id: model.ModelID,
-        model_name: model.Model_name
-      })
-      ElMessage.success(`模型 ${model.Model_name} 切换成功`)
+        model_name: model.Model_name,
+      });
+      ElMessage.success(`模型 ${model.Model_name} 切换成功`);
     }
   } catch (error) {
-    console.error("Failed to switch model:", error.message)
-    ElMessage.error('模型切换失败:' + error.message)
+    // 类型检查:确认 error 是否具有 message 属性
+    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);
   }
-}
+};
 
 onMounted(() => {
-  fetchAllModels()
-})
+  fetchAllModels();
+});
 </script>
 
 <style scoped>
@@ -194,7 +264,7 @@ onMounted(() => {
 .card-header {
   font-size: 16px; /* 略微减小字体大小 */
   font-weight: bold;
-  color: #28A745;
+  color: #28a745;
 }
 
 .el-table {
@@ -203,17 +273,17 @@ onMounted(() => {
 
 /* 设置表头背景色 */
 ::v-deep .el-table th {
-  background-color: #61E054 !important; /* 设置你想要的颜色 */
+  background-color: #61e054 !important; /* 设置你想要的颜色 */
   color: #030803; /* 表头文字颜色 */
 }
 
-
 /* 减少表格单元格的内边距 */
-::v-deep .el-table td, ::v-deep .el-table th {
+::v-deep .el-table td,
+::v-deep .el-table th {
   padding: 4px 0;
 }
 /* 设置奇数行背景色 */
 ::v-deep .el-table__row:nth-child(odd) {
-  background-color: #E4FBE5 !important;
+  background-color: #e4fbe5 !important;
 }
-</style>
+</style>

+ 139 - 52
src/views/ModelTrain.vue

@@ -1,23 +1,42 @@
 <template>
   <div class="model-container">
     <!-- 模型选择 -->
-    <el-select v-model="currentType" placeholder="请选择模型类型" style="margin-bottom: 5px;" @change="onTypeChange">
+    <el-select
+      v-model="currentType"
+      placeholder="请选择模型类型"
+      style="margin-bottom: 5px"
+      @change="onTypeChange"
+    >
       <el-option
         v-for="(type, index) in types"
         :key="index"
         :label="type.display"
-        :value="type.name">
+        :value="type.name"
+      >
       </el-option>
     </el-select>
 
     <!-- 加载失败提示 -->
-    <el-alert v-if="errorOccurred" title="加载数据失败,请检查网络连接或稍后再试。" type="error" show-icon style="margin-bottom: 5px;"></el-alert>
+    <el-alert
+      v-if="errorOccurred"
+      title="加载数据失败,请检查网络连接或稍后再试。"
+      type="error"
+      show-icon
+      style="margin-bottom: 5px"
+    ></el-alert>
 
     <!-- 动态加载的数据板块 -->
-    <el-card class="model-section" v-if="currentType && pagedFilteredRows.length > 0">
+    <el-card
+      class="model-section"
+      v-if="currentType && pagedFilteredRows.length > 0"
+    >
       <template #header>
         <div class="card-header">
-          <span>{{ currentType === 'reduce' ? '降酸模型(Reduce Model)' : '反酸模型(Reflux Model)' }}</span>
+          <span>{{
+            currentType === "reduce"
+              ? "降酸模型(Reduce Model)"
+              : "反酸模型(Reflux Model)"
+          }}</span>
         </div>
       </template>
       <el-table
@@ -29,7 +48,10 @@
         <!-- 复选框列 -->
         <el-table-column align="center" width="50">
           <template #default="scope">
-            <el-checkbox v-model="scope.row.selected" @change="handleSelectionChange(scope.row)"></el-checkbox>
+            <el-checkbox
+              v-model="scope.row.selected"
+              @change="handleSelectionChange(scope.row)"
+            ></el-checkbox>
           </template>
         </el-table-column>
 
@@ -40,7 +62,10 @@
     </el-card>
 
     <!-- 分页控制 -->
-    <div class="demo-pagination-block" v-if="currentType && filteredRows.length > 0">
+    <div
+      class="demo-pagination-block"
+      v-if="currentType && filteredRows.length > 0"
+    >
       <el-pagination
         v-model:current-page="currentPage"
         v-model:page-size="pageSize"
@@ -48,46 +73,77 @@
         layout="total, sizes, prev, pager, next, jumper"
         :total="filteredRows.length"
         @size-change="handleSizeChange"
-        @current-change="handleCurrentChange" 
+        @current-change="handleCurrentChange"
       />
     </div>
 
     <!-- 训练模型按钮,位于表格下方并居中 -->
-    <div v-if="currentType && pagedFilteredRows.length > 0" style="text-align:center; margin-top:5px;">
-      <el-button type="primary" size="small" @click="trainModel" :disabled="selectedRows.length === 0">训练模型</el-button>
+    <div
+      v-if="currentType && pagedFilteredRows.length > 0"
+      style="text-align: center; margin-top: 5px"
+    >
+      <el-button
+        type="primary"
+        size="small"
+        @click="trainModel"
+        :disabled="selectedRows.length === 0"
+        >训练模型</el-button
+      >
     </div>
 
     <!-- 当没有选择任何模型类型时显示提示信息 -->
-    <div v-if="!currentType && !loading && !errorOccurred" style="text-align:center; padding-top:5px;">
+    <div
+      v-if="!currentType && !loading && !errorOccurred"
+      style="text-align: center; padding-top: 5px"
+    >
       请选择一个模型类型以查看详细信息。
     </div>
 
     <!-- 当筛选结果为空时显示提示信息 -->
-    <div v-if="currentType && filteredRows.length === 0 && !loading && !errorOccurred" style="text-align:center; padding-top:5px;">
+    <div
+      v-if="
+        currentType && 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 { ElMessageBox } from 'element-plus'; // 引入 Element Plus 的 MessageBox 组件
+import { ref, onMounted, computed } from "vue";
+import axios from "axios";
+import { ElMessageBox, ElMessage } from "element-plus";
+
+interface Dataset {
+  id: any;
+  name: string;
+  description: string;
+  type: string;
+  count: number;
+  status: string;
+  uploadTime: string;
+  selected: boolean;
+}
 
 const loading = ref(true);
-const rows = ref([]);
+const rows = ref<Dataset[]>([]);
 const errorOccurred = ref(false);
-const selectedRows = ref([]); // 新增:用于存储被选中的行
+const selectedRows = ref<Dataset[]>([]);
 const currentPage = ref(1);
 const pageSize = ref(10);
 
-const types = [{ name: 'reduce', display: '降酸模型' }, { name: 'reflux', display: '反酸模型' }];
-const currentType = ref('reflux'); // 默认设置为反酸模型
+const types = [
+  { name: "reduce", display: "降酸模型" },
+  { name: "reflux", display: "反酸模型" },
+];
+const currentType = ref("reflux"); // 默认设置为反酸模型
 
 // 数据过滤后的结果
-const filteredRows = computed(() => 
-  currentType.value 
-    ? rows.value.filter(row => row.type === currentType.value)
+const filteredRows = computed(() =>
+  currentType.value
+    ? rows.value.filter((row) => row.type === currentType.value)
     : []
 );
 
@@ -101,28 +157,55 @@ const pagedFilteredRows = computed(() => {
 // 定义 fetchData 方法
 const fetchData = async () => {
   try {
-    const response = await axios.post('https://127.0.0.1:5000/table', { table: 'Datasets' });
+    const response = await axios.post("https://127.0.0.1:5000/table", {
+      table: "Datasets",
+    });
     return response.data.rows;
   } catch (error) {
-    console.error("Error fetching data:", error.message);
-    throw error;
+    // 类型检查:确认 error 是否具有 message 属性
+    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 => ({
-      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
-    }));
+    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 errorMessage = "未知错误";
+
+    if (error && typeof error === "object" && "message" in error) {
+      errorMessage = (error as { message?: string }).message || errorMessage;
+    }
+
+    console.error("Error fetching data:", errorMessage);
+    ElMessage.error("数据加载失败:" + errorMessage);
     errorOccurred.value = true;
   } finally {
     loading.value = false;
@@ -136,30 +219,33 @@ const onTypeChange = (newType: string) => {
   // 例如 LoadData();
 };
 
-const handleSelectionChange = (row) => {
+const handleSelectionChange = (row: Dataset) => {
   if (row.selected) {
     selectedRows.value.push(row);
   } else {
-    const index = selectedRows.value.findIndex(r => r.id === row.id);
+    const index = selectedRows.value.findIndex((r) => r.id === row.id);
     if (index > -1) {
       selectedRows.value.splice(index, 1);
     }
   }
 };
 
-const showMessageDialog = (message, type) => {
+const showMessageDialog = (
+  message: string,
+  type: "success" | "warning" | "info" | "error"
+) => {
   ElMessageBox({
-    title: '提示',
+    title: "提示",
     message: message,
     type: type,
-    confirmButtonText: '确定',
+    confirmButtonText: "确定",
     showClose: false,
   });
 };
 
 const trainModel = async () => {
   if (!selectedRows.value.length) {
-    showMessageDialog('请先选择一行或多行数据', 'warning');
+    showMessageDialog("请先选择一行或多行数据", "warning");
     return;
   }
 
@@ -169,17 +255,17 @@ const trainModel = async () => {
     model_name: "ForestModel1",
     model_description: "A random forest model trained on current data.",
     data_type: firstSelectedRow.type,
-    dataset_id: firstSelectedRow.id
+    dataset_id: firstSelectedRow.id,
   };
 
   try {
-    await axios.post('https://127.0.0.1:5000/train-and-save-model', trainData);
-    showMessageDialog('模型训练完成', 'success');
-    selectedRows.value.forEach(row => row.selected = false);
+    await axios.post("https://127.0.0.1:5000/train-and-save-model", trainData);
+    showMessageDialog("模型训练完成", "success");
+    selectedRows.value.forEach((row) => (row.selected = false));
     selectedRows.value = [];
   } catch (error) {
-    console.error('模型训练失败:', error);
-    showMessageDialog('模型训练失败', 'error');
+    console.error("模型训练失败:", error);
+    showMessageDialog("模型训练失败", "error");
   }
 };
 
@@ -212,7 +298,7 @@ onMounted(() => {
 .card-header {
   font-size: 16px; /* 略微减小字体大小 */
   font-weight: bold;
-  color: #28A745;
+  color: #28a745;
 }
 
 .el-table {
@@ -221,16 +307,17 @@ onMounted(() => {
 
 /* 设置表头背景色 */
 ::v-deep .el-table th {
-  background-color: #61E054 !important; /* 设置你想要的颜色 */
+  background-color: #61e054 !important; /* 设置你想要的颜色 */
   color: #030803; /* 表头文字颜色 */
 }
 
 /* 减少表格单元格的内边距 */
-::v-deep .el-table td, ::v-deep .el-table th {
+::v-deep .el-table td,
+::v-deep .el-table th {
   padding: 4px 0;
 }
 /* 设置奇数行背景色 */
 ::v-deep .el-table__row:nth-child(odd) {
-  background-color: #E4FBE5 !important;
+  background-color: #e4fbe5 !important;
 }
-</style>
+</style>

+ 298 - 181
src/views/Visualizatio.vue

@@ -1,172 +1,258 @@
 <template>
   <div>
     <!-- 操作按钮区 -->
-    <el-row :gutter="10" style="margin-bottom: 20px;">
+    <el-row :gutter="10" style="margin-bottom: 20px">
       <el-col :span="4">
-        <el-button :icon="Plus" type="primary" @click="openDialog('add')" style="background-color: #3EC01E; border-color: #67C23A; color: white;">新增记录</el-button>
+        <el-button
+          :icon="Plus"
+          type="primary"
+          @click="openDialog('add')"
+          style="background-color: #3ec01e; border-color: #67c23a; color: white"
+          >新增记录</el-button
+        >
       </el-col>
       <el-col :span="4">
-        <el-button :icon="Download" type="primary" @click="downloadTemplateAction" style="background-color: #3EC01E; border-color: #67C23A; color: white;">下载模板</el-button>
+        <el-button
+          :icon="Download"
+          type="primary"
+          @click="downloadTemplateAction"
+          style="background-color: #3ec01e; border-color: #67c23a; color: white"
+          >下载模板</el-button
+        >
       </el-col>
       <el-col :span="4">
-        <el-button :icon="Download" type="primary" @click="exportDataAction" style="background-color: #3EC01E; border-color: #67C23A; color: white;">导出数据</el-button>
+        <el-button
+          :icon="Download"
+          type="primary"
+          @click="exportDataAction"
+          style="background-color: #3ec01e; border-color: #67c23a; color: white"
+          >导出数据</el-button
+        >
       </el-col>
       <el-col :span="4">
         <el-upload :before-upload="importDataAction" accept=".xlsx, .csv">
-          <el-button :icon="Upload" type="primary" style="background-color: #3EC01E; border-color: #67C23A; color: white;">导入数据</el-button>
+          <el-button
+            :icon="Upload"
+            type="primary"
+            style="
+              background-color: #3ec01e;
+              border-color: #67c23a;
+              color: white;
+            "
+            >导入数据</el-button
+          >
         </el-upload>
       </el-col>
     </el-row>
 
     <!-- 数据表格 -->
-    <el-table :data="pagedTableDataWithIndex" style="width: 100%" @row-click="handleRowClick" highlight-current-row class="custom-table"
-              v-loading="loading">
-      <el-table-column key="displayIndex" prop="displayIndex" label="序号" width="80"></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" :width="col.width"
-                       :formatter="formatNumber">
+    <el-table
+      :data="pagedTableDataWithIndex"
+      style="width: 100%"
+      @row-click="handleRowClick"
+      highlight-current-row
+      class="custom-table"
+      v-loading="loading"
+    >
+      <el-table-column
+        key="displayIndex"
+        prop="displayIndex"
+        label="序号"
+        width="80"
+      ></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"
+        :width="col.width"
+        :formatter="formatNumber"
+      >
       </el-table-column>
       <el-table-column label="操作" width="120">
         <template #default="scope">
           <el-tooltip class="item" effect="dark" content="编辑" placement="top">
-            <el-button circle :icon="EditPen" @click.stop="openDialog('edit', scope.row)" style="font-size: 16px; color: #409EFF;"></el-button>
+            <el-button
+              circle
+              :icon="EditPen"
+              @click.stop="openDialog('edit', scope.row)"
+              style="font-size: 16px; color: #409eff"
+            ></el-button>
           </el-tooltip>
           <el-tooltip class="item" effect="dark" content="删除" placement="top">
-            <el-button circle :icon="DeleteFilled" @click.stop="deleteItem(scope.row)" style="font-size: 16px; color: red;"></el-button>
+            <el-button
+              circle
+              :icon="DeleteFilled"
+              @click.stop="deleteItem(scope.row)"
+              style="font-size: 16px; color: red"
+            ></el-button>
           </el-tooltip>
         </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" 
+    <PaginationComponent
+      :total="tableData.length"
+      :currentPage="currentPage4"
+      :pageSize="pageSize4"
+      @update:currentPage="currentPage4 = $event"
+      @update:pageSize="pageSize4 = $event"
+      @size-change="handleSizeChange"
       @current-change="handleCurrentChange"
     />
 
     <!-- 新增/编辑对话框 -->
     <el-dialog :title="dialogTitle" v-model="dialogVisible">
       <el-form :model="formData">
-        <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'"></el-input>
+        <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'"
+          ></el-input>
         </el-form-item>
       </el-form>
       <template #footer>
         <el-button @click="dialogVisible = false">取消</el-button>
-        <el-button type="primary" @click="submitForm">{{ dialogSubmitButtonText }}</el-button>
+        <el-button type="primary" @click="submitForm">{{
+          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 './packaged/PaginationComponent.vue'
+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 "./packaged/PaginationComponent.vue";
 
 interface Column {
- key: string;
- dataKey: string;
- title: string;
- width: number;
- inputType?: string;
+  key: string;
+  dataKey: string;
+  title: string;
+  width: number;
+  inputType?: string;
 }
 
 const columns: Column[] = [
- { key: 'id', dataKey: 'id', title: 'ID', width: 80 },
- { 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, inputType: 'number' },
- { 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: 150 },
-]
-
-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)
+  { key: "id", dataKey: "id", title: "ID", width: 80 },
+  { 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,
+    inputType: "number",
+  },
+  { 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: 150 },
+];
+
+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 pagedTableDataWithIndex = computed(() => {
- return pagedTableData.value.map((item, index) => ({
-   ...item,
-   displayIndex: (currentPage4.value - 1) * pageSize4.value + index + 1
- }));
-})
+  return pagedTableData.value.map((item, index) => ({
+    ...item,
+    displayIndex: (currentPage4.value - 1) * pageSize4.value + index + 1,
+  }));
+});
 
 const pagedTableData = computed(() => {
- const start = (currentPage4.value - 1) * pageSize4.value
- const end = start + pageSize4.value
- return tableData.value.slice(start, end)
-})
+  const start = (currentPage4.value - 1) * pageSize4.value;
+  const end = start + pageSize4.value;
+  return tableData.value.slice(start, end);
+});
 
 const loading = ref(false);
 
 const currentTableName = "current_reflux";
 
 const fetchTable = async () => {
-  console.log('正在获取表格数据...');
+  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('获取数据失败,请检查网络连接或服务器状态');
+    console.error("获取数据时出错:", error);
+    ElMessage.error("获取数据失败,请检查网络连接或服务器状态");
   } finally {
     loading.value = false;
   }
 };
 
 onMounted(() => {
- fetchTable()
-})
+  fetchTable();
+});
 
 const handleRowClick = (row: any) => {
- selectedRow.value = row
- Object.assign(formData, row)
-}
+  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;
-}
+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; } = {};
+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];
-      
+
       // 如果是 'displayIndex-' 格式的键,则提取原始 id
-      if (typeof value === 'string' && value.startsWith('displayIndex-')) {
-        value = value.replace('displayIndex-', '');
+      if (typeof value === "string" && value.startsWith("displayIndex-")) {
+        value = value.replace("displayIndex-", "");
       }
 
       result[key] = value;
@@ -177,58 +263,76 @@ function prepareFormData(formData: { [x: string]: any; }, excludeKeys = ['displa
 }
 
 const submitForm = async () => {
-  console.log('开始提交表单...');
+  console.log("开始提交表单...");
   try {
-    console.log('验证表单数据...');
+    console.log("验证表单数据...");
     const isValid = validateFormData(formData);
     if (!isValid) {
-      console.error('表单验证失败,请检查输入的数据');
-      alert('请检查输入的数据');
+      console.error("表单验证失败,请检查输入的数据");
+      alert("请检查输入的数据");
       return;
     }
 
     // 准备要提交的数据
-    console.log('准备提交的数据:', formData);
+    console.log("准备提交的数据:", formData);
     const dataToSubmit = prepareFormData(formData);
 
-    console.log('已准备的数据:', dataToSubmit);
+    console.log("已准备的数据:", dataToSubmit);
 
     // 检查是否存在 id 字段
-    if (!dataToSubmit.id && dialogMode.value !== 'add') {
-      console.error('无法找到记录ID,请联系管理员');
-      alert('无法找到记录ID,请联系管理员');
+    if (!dataToSubmit.id && dialogMode.value !== "add") {
+      console.error("无法找到记录ID,请联系管理员");
+      alert("无法找到记录ID,请联系管理员");
       return;
     }
 
     let response;
-    if (dialogMode.value === 'add') {
-      console.log('正在添加新记录...');
+    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("正在更新现有记录...");
+      response = await updateItem({
+        table: currentTableName,
+        item: dataToSubmit,
+      });
     }
 
-    console.log(dialogMode.value === 'add' ? '添加响应:' : '更新响应:', response);
+    console.log(
+      dialogMode.value === "add" ? "添加响应:" : "更新响应:",
+      response
+    );
 
     dialogVisible.value = false;
     fetchTable();
-    alert(dialogMode.value === 'add' ? '添加成功' : '修改成功');
+    alert(dialogMode.value === "add" ? "添加成功" : "修改成功");
   } catch (error) {
-    console.error('提交表单时发生错误:', error);
-    let errorMessage = '未知错误';
-    if (error.response && error.response.data && error.response.data.message) {
-      errorMessage = error.response.data.message;
+    console.error("提交表单时发生错误:", error);
+
+    let errorMessage = "未知错误";
+
+    // 类型检查:确认 error 是否具有 response 属性,并且该属性是否包含 data 和 message 属性
+    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}`);
   }
 };
 
 // 示例 validateFormData 函数,需根据实际需求调整
-function validateFormData(data) {
+function validateFormData(data: { [x: string]: undefined }) {
   for (let key in data) {
-    if (data[key] === '' || data[key] === undefined) {
+    if (data[key] === "" || data[key] === undefined) {
       return false;
     }
   }
@@ -236,90 +340,102 @@ function validateFormData(data) {
 }
 
 const deleteItem = async (row: any) => {
- console.log('准备删除记录:', row);
- if (!row) {
-   ElMessage.warning('请先选择一行记录')
-   return
- }
- try {
-   const condition = { id: row.id };
-   console.log('删除记录条件:', condition);
-   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);
- }
-}
+  console.log("准备删除记录:", row);
+  if (!row) {
+    ElMessage.warning("请先选择一行记录");
+    return;
+  }
+  try {
+    const condition = { id: row.id };
+    console.log("删除记录条件:", condition);
+    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 {
-   console.log('正在下载模板...');
-   await downloadTemplate('current_reflux');
- } catch (error) {
-   console.error('下载模板时发生错误:', error);
- }
-}
+  try {
+    console.log("正在下载模板...");
+    await downloadTemplate("current_reflux");
+  } catch (error) {
+    console.error("下载模板时发生错误:", error);
+  }
+};
 
 const exportDataAction = async () => {
- try {
-   console.log('正在导出数据...');
-   await exportData('current_reflux');
- } catch (error) {
-   console.error('导出数据时发生错误:', error);
- }
-}
+  try {
+    console.log("正在导出数据...");
+    await exportData("current_reflux");
+  } catch (error) {
+    console.error("导出数据时发生错误:", error);
+  }
+};
 
 const importDataAction = async (file: File) => {
- try {
-   console.log('正在导入数据...');
-   const response = await importData('current_reflux', file);
-   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(); // 刷新表格数据
-   }
- } catch (error) {
-   console.error('数据导入时发生错误:', error);
-   let errorMessage = '数据导入失败';
-   if (error.response && error.response.data && error.response.data.message) {
-     errorMessage += `: ${error.response.data.message}`;
-   }
-   ElMessage.error(errorMessage);
- }
+  try {
+    console.log("正在导入数据...");
+    const response = await importData("current_reflux", file);
+    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(); // 刷新表格数据
+    }
+  } catch (error) {
+    console.error("数据导入时发生错误:", error);
+
+    let errorMessage = "数据导入失败";
+
+    // 类型检查:确认 error 是否具有 response 属性,并且该属性是否包含 data 和 message 属性
+    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 target = event.target as HTMLInputElement;
+  if (target.files && target.files.length > 0) {
+    importDataAction(target.files[0]);
+  }
 };
 
-const handleSizeChange = (val: number) => {}
+const handleSizeChange = (val: number) => {};
 
-const handleCurrentChange = (val: number) => {}
+const handleCurrentChange = (val: number) => {};
 
 const formatNumber = (row: any, column: any, cellValue: any) => {
- if (typeof cellValue === 'number') {
-   return cellValue.toFixed(3)
- }
- return cellValue
-}
+  if (typeof cellValue === "number") {
+    return cellValue.toFixed(3);
+  }
+  return cellValue;
+};
 
 const dialogTitle = computed(() => {
-  return dialogMode.value === 'add' ? '新增记录' : '编辑记录';
+  return dialogMode.value === "add" ? "新增记录" : "编辑记录";
 });
 
 const dialogSubmitButtonText = computed(() => {
-  return dialogMode.value === 'add' ? '添加' : '保存';
+  return dialogMode.value === "add" ? "添加" : "保存";
 });
 </script>
 
@@ -346,22 +462,23 @@ const dialogSubmitButtonText = computed(() => {
 
 /* 设置表头背景色 */
 ::v-deep .el-table th {
-  background-color: #61E054 !important; /* 保持原有颜色 */
+  background-color: #61e054 !important; /* 保持原有颜色 */
   color: #030803; /* 保持原有颜色 */
 }
 
 /* 设置奇数行背景色 */
 ::v-deep .el-table__row:nth-child(odd) {
-  background-color: #E4FBE5 !important; /* 保持原有颜色 */
+  background-color: #e4fbe5 !important; /* 保持原有颜色 */
 }
 
 /* 设置偶数行背景色 */
 ::v-deep .el-table__row:nth-child(even) {
-  background-color: #FFFFFF !important; /* 新增:设置偶数行为白色 */
+  background-color: #ffffff !important; /* 新增:设置偶数行为白色 */
 }
 
 /* 减少表格单元格的内边距 */
-::v-deep .el-table td, ::v-deep .el-table th.is-leaf {
+::v-deep .el-table td,
+::v-deep .el-table th.is-leaf {
   padding: 8px 0; /* 减少内边距 */
 }
 
@@ -393,7 +510,7 @@ const dialogSubmitButtonText = computed(() => {
 .el-table-column--action .el-button.el-button--primary {
   background-color: transparent;
   border-color: transparent;
-  color: #409EFF;
+  color: #409eff;
 }
 
 .el-table-column--action .el-button.el-button--danger {
@@ -401,4 +518,4 @@ const dialogSubmitButtonText = computed(() => {
   border-color: transparent;
   color: red;
 }
-</style>
+</style>

+ 291 - 180
src/views/Visualization.vue

@@ -1,171 +1,251 @@
 <template>
   <div>
     <!-- 操作按钮区 -->
-    <el-row :gutter="10" style="margin-bottom: 20px;">
+    <el-row :gutter="10" style="margin-bottom: 20px">
       <el-col :span="4">
-        <el-button :icon="Plus" type="primary" @click="openDialog('add')" style="background-color: #3EC01E; border-color: #67C23A; color: white;">新增记录</el-button>
+        <el-button
+          :icon="Plus"
+          type="primary"
+          @click="openDialog('add')"
+          style="background-color: #3ec01e; border-color: #67c23a; color: white"
+          >新增记录</el-button
+        >
       </el-col>
       <el-col :span="4">
-        <el-button :icon="Download" type="primary" @click="downloadTemplateAction" style="background-color: #3EC01E; border-color: #67C23A; color: white;">下载模板</el-button>
+        <el-button
+          :icon="Download"
+          type="primary"
+          @click="downloadTemplateAction"
+          style="background-color: #3ec01e; border-color: #67c23a; color: white"
+          >下载模板</el-button
+        >
       </el-col>
       <el-col :span="4">
-        <el-button :icon="Download" type="primary" @click="exportDataAction" style="background-color: #3EC01E; border-color: #67C23A; color: white;">导出数据</el-button>
+        <el-button
+          :icon="Download"
+          type="primary"
+          @click="exportDataAction"
+          style="background-color: #3ec01e; border-color: #67c23a; color: white"
+          >导出数据</el-button
+        >
       </el-col>
       <el-col :span="4">
         <el-upload :before-upload="importDataAction" accept=".xlsx, .csv">
-          <el-button :icon="Upload" type="primary" style="background-color: #3EC01E; border-color: #67C23A; color: white;">导入数据</el-button>
+          <el-button
+            :icon="Upload"
+            type="primary"
+            style="
+              background-color: #3ec01e;
+              border-color: #67c23a;
+              color: white;
+            "
+            >导入数据</el-button
+          >
         </el-upload>
       </el-col>
     </el-row>
 
     <!-- 数据表格 -->
-    <el-table :data="pagedTableDataWithIndex" style="width: 100%" @row-click="handleRowClick" highlight-current-row class="custom-table"
-              v-loading="loading">
-      <el-table-column key="displayIndex" prop="displayIndex" label="序号" width="80"></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" :width="col.width"
-                       :formatter="formatNumber">
+    <el-table
+      :data="pagedTableDataWithIndex"
+      style="width: 100%"
+      @row-click="handleRowClick"
+      highlight-current-row
+      class="custom-table"
+      v-loading="loading"
+    >
+      <el-table-column
+        key="displayIndex"
+        prop="displayIndex"
+        label="序号"
+        width="80"
+      ></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"
+        :width="col.width"
+        :formatter="formatNumber"
+      >
       </el-table-column>
       <el-table-column label="操作" width="120">
         <template #default="scope">
           <el-tooltip class="item" effect="dark" content="编辑" placement="top">
-            <el-button circle :icon="EditPen" @click.stop="openDialog('edit', scope.row)" style="font-size: 16px; color: #409EFF;"></el-button>
+            <el-button
+              circle
+              :icon="EditPen"
+              @click.stop="openDialog('edit', scope.row)"
+              style="font-size: 16px; color: #409eff"
+            ></el-button>
           </el-tooltip>
           <el-tooltip class="item" effect="dark" content="删除" placement="top">
-            <el-button circle :icon="DeleteFilled" @click.stop="deleteItem(scope.row)" style="font-size: 16px; color: red;"></el-button>
+            <el-button
+              circle
+              :icon="DeleteFilled"
+              @click.stop="deleteItem(scope.row)"
+              style="font-size: 16px; color: red"
+            ></el-button>
           </el-tooltip>
         </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" 
+    <PaginationComponent
+      :total="tableData.length"
+      :currentPage="currentPage4"
+      :pageSize="pageSize4"
+      @update:currentPage="currentPage4 = $event"
+      @update:pageSize="pageSize4 = $event"
+      @size-change="handleSizeChange"
       @current-change="handleCurrentChange"
     />
 
     <!-- 新增/编辑对话框 -->
     <el-dialog :title="dialogTitle" v-model="dialogVisible">
       <el-form :model="formData">
-        <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'"></el-input>
+        <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'"
+          ></el-input>
         </el-form-item>
       </el-form>
       <template #footer>
         <el-button @click="dialogVisible = false">取消</el-button>
-        <el-button type="primary" @click="submitForm">{{ dialogSubmitButtonText }}</el-button>
+        <el-button type="primary" @click="submitForm">{{
+          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 './packaged/PaginationComponent.vue'
+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 "./packaged/PaginationComponent.vue";
 
 interface Column {
- key: string;
- dataKey: string;
- title: string;
- width: number;
- inputType?: string;
+  key: string;
+  dataKey: string;
+  title: string;
+  width: number;
+  inputType?: string;
 }
 
 const columns: Column[] = [
-  { key: 'id', dataKey: 'id', title: '序号', width: 80 },
-  { key: 'Q_over_b', dataKey: 'Q_over_b', title: 'Q/ΔpH', width: 150 },
-  { key: 'pH', dataKey: 'pH', title: '初始pH', width: 150 },
-  { key: 'OM', dataKey: 'OM', title: '有机质含量', width: 150 },
-  { key: 'CL', dataKey: 'CL', title: '土壤粘粒', width: 150 },
-  { key: 'H', dataKey: 'H', title: '交换性氢', width: 150 },
-  { key: 'Al', dataKey: 'Al', title: '交换性铝', width: 150 },
-]
-
-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)
+  { key: "id", dataKey: "id", title: "序号", width: 80 },
+  { key: "Q_over_b", dataKey: "Q_over_b", title: "Q/ΔpH", width: 150 },
+  { key: "pH", dataKey: "pH", title: "初始pH", width: 150 },
+  { key: "OM", dataKey: "OM", title: "有机质含量", width: 150 },
+  { key: "CL", dataKey: "CL", title: "土壤粘粒", width: 150 },
+  { key: "H", dataKey: "H", title: "交换性氢", width: 150 },
+  { key: "Al", dataKey: "Al", title: "交换性铝", width: 150 },
+];
+
+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 pagedTableDataWithIndex = computed(() => {
- return pagedTableData.value.map((item, index) => ({
-   ...item,
-   displayIndex: (currentPage4.value - 1) * pageSize4.value + index + 1
- }));
-})
+  return pagedTableData.value.map((item, index) => ({
+    ...item,
+    displayIndex: (currentPage4.value - 1) * pageSize4.value + index + 1,
+  }));
+});
 
 const pagedTableData = computed(() => {
- const start = (currentPage4.value - 1) * pageSize4.value
- const end = start + pageSize4.value
- return tableData.value.slice(start, end)
-})
+  const start = (currentPage4.value - 1) * pageSize4.value;
+  const end = start + pageSize4.value;
+  return tableData.value.slice(start, end);
+});
 
 const loading = ref(false);
 
 const currentTableName = "current_reduce";
 
 const fetchTable = async () => {
-  console.log('正在获取表格数据...');
+  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('获取数据失败,请检查网络连接或服务器状态');
+    console.error("获取数据时出错:", error);
+    ElMessage.error("获取数据失败,请检查网络连接或服务器状态");
   } finally {
     loading.value = false;
   }
 };
 
 onMounted(() => {
- fetchTable()
-})
+  fetchTable();
+});
 
 const handleRowClick = (row: any) => {
- selectedRow.value = row
- Object.assign(formData, row)
-}
+  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;
-}
+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; } = {};
+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];
-      
+
       // 如果是 'displayIndex-' 格式的键,则提取原始 id
-      if (typeof value === 'string' && value.startsWith('displayIndex-')) {
-        value = value.replace('displayIndex-', '');
+      if (typeof value === "string" && value.startsWith("displayIndex-")) {
+        value = value.replace("displayIndex-", "");
       }
 
       result[key] = value;
@@ -176,58 +256,76 @@ function prepareFormData(formData: { [x: string]: any; }, excludeKeys = ['displa
 }
 
 const submitForm = async () => {
-  console.log('开始提交表单...');
+  console.log("开始提交表单...");
   try {
-    console.log('验证表单数据...');
+    console.log("验证表单数据...");
     const isValid = validateFormData(formData);
     if (!isValid) {
-      console.error('表单验证失败,请检查输入的数据');
-      alert('请检查输入的数据');
+      console.error("表单验证失败,请检查输入的数据");
+      alert("请检查输入的数据");
       return;
     }
 
     // 准备要提交的数据
-    console.log('准备提交的数据:', formData);
+    console.log("准备提交的数据:", formData);
     const dataToSubmit = prepareFormData(formData);
 
-    console.log('已准备的数据:', dataToSubmit);
+    console.log("已准备的数据:", dataToSubmit);
 
     // 检查是否存在 id 字段
-    if (!dataToSubmit.id && dialogMode.value !== 'add') {
-      console.error('无法找到记录ID,请联系管理员');
-      alert('无法找到记录ID,请联系管理员');
+    if (!dataToSubmit.id && dialogMode.value !== "add") {
+      console.error("无法找到记录ID,请联系管理员");
+      alert("无法找到记录ID,请联系管理员");
       return;
     }
 
     let response;
-    if (dialogMode.value === 'add') {
-      console.log('正在添加新记录...');
+    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("正在更新现有记录...");
+      response = await updateItem({
+        table: currentTableName,
+        item: dataToSubmit,
+      });
     }
 
-    console.log(dialogMode.value === 'add' ? '添加响应:' : '更新响应:', response);
+    console.log(
+      dialogMode.value === "add" ? "添加响应:" : "更新响应:",
+      response
+    );
 
     dialogVisible.value = false;
     fetchTable();
-    alert(dialogMode.value === 'add' ? '添加成功' : '修改成功');
+    alert(dialogMode.value === "add" ? "添加成功" : "修改成功");
   } catch (error) {
-    console.error('提交表单时发生错误:', error);
-    let errorMessage = '未知错误';
-    if (error.response && error.response.data && error.response.data.message) {
-      errorMessage = error.response.data.message;
+    console.error("提交表单时发生错误:", error);
+
+    let errorMessage = "未知错误";
+
+    // 类型检查:确认 error 是否具有 response 属性,并且该属性是否包含 data 和 message 属性
+    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}`);
   }
 };
 
 // 示例 validateFormData 函数,需根据实际需求调整
-function validateFormData(data) {
+function validateFormData(data: { [x: string]: undefined }) {
   for (let key in data) {
-    if (data[key] === '' || data[key] === undefined) {
+    if (data[key] === "" || data[key] === undefined) {
       return false;
     }
   }
@@ -235,90 +333,102 @@ function validateFormData(data) {
 }
 
 const deleteItem = async (row: any) => {
- console.log('准备删除记录:', row);
- if (!row) {
-   ElMessage.warning('请先选择一行记录')
-   return
- }
- try {
-   const condition = { id: row.id };
-   console.log('删除记录条件:', condition);
-   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()
-   console.log('记录删除成功');
- } catch (error) {
-   console.error('删除记录时发生错误:', error);
- }
-}
+  console.log("准备删除记录:", row);
+  if (!row) {
+    ElMessage.warning("请先选择一行记录");
+    return;
+  }
+  try {
+    const condition = { id: row.id };
+    console.log("删除记录条件:", condition);
+    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();
+    console.log("记录删除成功");
+  } catch (error) {
+    console.error("删除记录时发生错误:", error);
+  }
+};
 
 const downloadTemplateAction = async () => {
- try {
-   console.log('正在下载模板...');
-   await downloadTemplate('current_reduce');
- } catch (error) {
-   console.error('下载模板时发生错误:', error);
- }
-}
+  try {
+    console.log("正在下载模板...");
+    await downloadTemplate("current_reduce");
+  } catch (error) {
+    console.error("下载模板时发生错误:", error);
+  }
+};
 
 const exportDataAction = async () => {
- try {
-   console.log('正在导出数据...');
-   await exportData('current_reduce');
- } catch (error) {
-   console.error('导出数据时发生错误:', error);
- }
-}
+  try {
+    console.log("正在导出数据...");
+    await exportData("current_reduce");
+  } catch (error) {
+    console.error("导出数据时发生错误:", error);
+  }
+};
 
 const importDataAction = async (file: File) => {
- try {
-   console.log('正在导入数据...');
-   const response = await importData('current_reduce', file);
-   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(); // 刷新表格数据
-   }
- } catch (error) {
-   console.error('数据导入时发生错误:', error);
-   let errorMessage = '数据导入失败';
-   if (error.response && error.response.data && error.response.data.message) {
-     errorMessage += `: ${error.response.data.message}`;
-   }
-   ElMessage.error(errorMessage);
- }
+  try {
+    console.log("正在导入数据...");
+    const response = await importData("current_reduce", file);
+    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(); // 刷新表格数据
+    }
+  } catch (error) {
+    console.error("数据导入时发生错误:", error);
+
+    let errorMessage = "数据导入失败";
+
+    // 类型检查:确认 error 是否具有 response 属性,并且该属性是否包含 data 和 message 属性
+    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 target = event.target as HTMLInputElement;
+  if (target.files && target.files.length > 0) {
+    importDataAction(target.files[0]);
+  }
 };
 
-const handleSizeChange = (val: number) => {}
+const handleSizeChange = (val: number) => {};
 
-const handleCurrentChange = (val: number) => {}
+const handleCurrentChange = (val: number) => {};
 
 const formatNumber = (row: any, column: any, cellValue: any) => {
- if (typeof cellValue === 'number') {
-   return cellValue.toFixed(3)
- }
- return cellValue
-}
+  if (typeof cellValue === "number") {
+    return cellValue.toFixed(3);
+  }
+  return cellValue;
+};
 
 const dialogTitle = computed(() => {
-  return dialogMode.value === 'add' ? '新增记录' : '编辑记录';
+  return dialogMode.value === "add" ? "新增记录" : "编辑记录";
 });
 
 const dialogSubmitButtonText = computed(() => {
-  return dialogMode.value === 'add' ? '添加' : '保存';
+  return dialogMode.value === "add" ? "添加" : "保存";
 });
 </script>
 
@@ -345,22 +455,23 @@ const dialogSubmitButtonText = computed(() => {
 
 /* 设置表头背景色 */
 ::v-deep .el-table th {
-  background-color: #61E054 !important; /* 保持原有颜色 */
+  background-color: #61e054 !important; /* 保持原有颜色 */
   color: #030803; /* 保持原有颜色 */
 }
 
 /* 设置奇数行背景色 */
 ::v-deep .el-table__row:nth-child(odd) {
-  background-color: #E4FBE5 !important; /* 保持原有颜色 */
+  background-color: #e4fbe5 !important; /* 保持原有颜色 */
 }
 
 /* 设置偶数行背景色 */
 ::v-deep .el-table__row:nth-child(even) {
-  background-color: #FFFFFF !important; /* 新增:设置偶数行为白色 */
+  background-color: #ffffff !important; /* 新增:设置偶数行为白色 */
 }
 
 /* 减少表格单元格的内边距 */
-::v-deep .el-table td, ::v-deep .el-table th.is-leaf {
+::v-deep .el-table td,
+::v-deep .el-table th.is-leaf {
   padding: 8px 0; /* 减少内边距 */
 }
 
@@ -392,7 +503,7 @@ const dialogSubmitButtonText = computed(() => {
 .el-table-column--action .el-button.el-button--primary {
   background-color: transparent;
   border-color: transparent;
-  color: #409EFF;
+  color: #409eff;
 }
 
 .el-table-column--action .el-button.el-button--danger {
@@ -400,4 +511,4 @@ const dialogSubmitButtonText = computed(() => {
   border-color: transparent;
   color: red;
 }
-</style>
+</style>

+ 14 - 0
src/views/api.vue

@@ -0,0 +1,14 @@
+<!-- api.js 文件内容 -->
+<script>
+import axios from 'axios';
+
+export const fetchData = async () => {
+  const response = await axios.post('https://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);
+  return response;
+};
+</script>

+ 84 - 38
src/views/login/loginView.vue

@@ -1,7 +1,13 @@
 <template>
   <div class="auth">
     <!-- 登录表单 -->
-    <el-form v-if="isLogin" ref="formRef" :model="form" :rules="rules" label-width="100px">
+    <el-form
+      v-if="isLogin"
+      ref="formRef"
+      :model="form"
+      :rules="rules"
+      label-width="100px"
+    >
       <h2>登录</h2>
       <el-form-item prop="name" label="账号">
         <el-input v-model="form.name"></el-input>
@@ -10,14 +16,23 @@
         <el-input type="password" v-model="form.password"></el-input>
       </el-form-item>
       <el-form-item>
-        <el-button type="primary" @click="onSubmit" :loading="loading">密码登录</el-button>
+        <el-button type="primary" @click="onSubmit" :loading="loading"
+          >密码登录</el-button
+        >
         <el-button @click="toggleForm">注册新账户</el-button>
-        <el-button @click="mockUserLogin">普通用户登录</el-button> <!-- 普通用户登录按钮 -->
+        <el-button @click="mockUserLogin">普通用户登录</el-button>
+        <!-- 普通用户登录按钮 -->
       </el-form-item>
     </el-form>
 
     <!-- 注册表单 -->
-    <el-form v-else ref="registerFormRef" :model="registerForm" :rules="registerRules" label-width="100px">
+    <el-form
+      v-else
+      ref="registerFormRef"
+      :model="registerForm"
+      :rules="registerRules"
+      label-width="100px"
+    >
       <h2>注册</h2>
       <el-form-item prop="name" label="账号">
         <el-input v-model="registerForm.name"></el-input>
@@ -26,10 +41,15 @@
         <el-input type="password" v-model="registerForm.password"></el-input>
       </el-form-item>
       <el-form-item prop="confirmPassword" label="确认密码">
-        <el-input type="password" v-model="registerForm.confirmPassword"></el-input>
+        <el-input
+          type="password"
+          v-model="registerForm.confirmPassword"
+        ></el-input>
       </el-form-item>
       <el-form-item>
-        <el-button type="primary" @click="onRegister" :loading="loading">注册</el-button>
+        <el-button type="primary" @click="onRegister" :loading="loading"
+          >注册</el-button
+        >
         <el-button @click="toggleForm">返回登录</el-button>
       </el-form-item>
     </el-form>
@@ -37,9 +57,9 @@
 </template>
 
 <script lang="ts" setup>
-import { reactive, ref, computed } from 'vue';
+import { reactive, ref, computed } from "vue";
 import { ElMessage, ElForm } from "element-plus";
-import axios from 'axios';
+import axios from "axios";
 import { login, register } from "@/API/users"; // 假设已封装好的接口
 import { useRouter, useRoute } from "vue-router";
 import { useTokenStore } from "@/stores/mytoken";
@@ -52,13 +72,13 @@ const isLogin = ref(true); // 控制显示登录或注册表单
 
 const form = reactive({
   name: "",
-  password: ""
+  password: "",
 });
 
 const registerForm = reactive({
   name: "",
   password: "",
-  confirmPassword: ""
+  confirmPassword: "",
 });
 const formRef = ref<InstanceType<typeof ElForm> | null>(null);
 const registerFormRef = ref<InstanceType<typeof ElForm> | null>(null);
@@ -78,19 +98,26 @@ const onSubmit = async () => {
     try {
       const res = await login({ name: form.name, password: form.password });
       if (res.data.success) {
-        console.log('密码登录成功返回的信息:', res.data);
-        const userInfo = { userId: parseInt(res.data.userId), name: res.data.name, loginType: 'password' }; 
+        console.log("密码登录成功返回的信息:", res.data);
+        const userInfo = {
+          userId: parseInt(res.data.userId),
+          name: res.data.name,
+          loginType: "password",
+        };
         store.saveToken(userInfo);
-        console.log('保存的Token信息:', store.token);
+        console.log("保存的Token信息:", store.token);
         ElMessage.success("密码登录成功");
-        const redirectPath = typeof route.query.redirect === 'string' ? route.query.redirect : '/';
+        const redirectPath =
+          typeof route.query.redirect === "string" ? route.query.redirect : "/";
         router.push(redirectPath);
       } else {
         ElMessage.error(res.data.message || "登录失败");
       }
     } catch (error) {
       if (axios.isAxiosError(error)) {
-        console.error(`HTTP Error: ${error.message}, status code: ${error.response?.status}`);
+        console.error(
+          `HTTP Error: ${error.message}, status code: ${error.response?.status}`
+        );
         ElMessage.error(`HTTP Error: ${error.response?.statusText}`);
       } else {
         console.error(error);
@@ -104,10 +131,11 @@ const onSubmit = async () => {
 
 // 模拟普通用户登录
 const mockUserLogin = () => {
-  const mockUserInfo = { userId: 1, name: '普通用户', loginType: 'mock' }; // 添加loginType字段以示区别
+  const mockUserInfo = { userId: 1, name: "普通用户", loginType: "mock" }; // 添加loginType字段以示区别
   store.saveToken(mockUserInfo);
   ElMessage.success("普通用户登录成功");
-  const redirectPath = typeof route.query.redirect === 'string' ? route.query.redirect : '/';
+  const redirectPath =
+    typeof route.query.redirect === "string" ? route.query.redirect : "/";
   router.push(redirectPath);
 };
 
@@ -123,8 +151,11 @@ const onRegister = async () => {
     }
 
     try {
-      const res = await register({ name: registerForm.name, password: registerForm.password });
-        if (res.data.success) {
+      const res = await register({
+        name: registerForm.name,
+        password: registerForm.password,
+      });
+      if (res.data.success) {
         ElMessage.success("注册成功,请登录");
         toggleForm(); // 切换回登录表单
       } else {
@@ -132,7 +163,9 @@ const onRegister = async () => {
       }
     } catch (error) {
       if (axios.isAxiosError(error)) {
-        console.error(`HTTP Error: ${error.message}, status code: ${error.response?.status}`);
+        console.error(
+          `HTTP Error: ${error.message}, status code: ${error.response?.status}`
+        );
         ElMessage.error(`HTTP Error: ${error.response?.statusText}`);
       } else {
         console.error(error);
@@ -146,33 +179,46 @@ const onRegister = async () => {
 
 // 验证规则
 const rules = reactive({
-  name: [
-    { required: true, message: "请输入账号", trigger: "blur" }
-  ],
+  name: [{ required: true, message: "请输入账号", trigger: "blur" }],
   password: [
     { required: true, message: "请输入密码", trigger: "blur" },
-    { min: 3, max: 16, message: "密码长度应在 3 到 16 个字符之间", trigger: "blur" }
-  ]
+    {
+      min: 3,
+      max: 16,
+      message: "密码长度应在 3 到 16 个字符之间",
+      trigger: "blur",
+    },
+  ],
 });
 
 const registerRules = reactive({
-  name: [
-    { required: true, message: "请输入账号", trigger: "blur" }
-  ],
+  name: [{ required: true, message: "请输入账号", trigger: "blur" }],
   password: [
     { required: true, message: "请输入密码", trigger: "blur" },
-    { min: 3, max: 16, message: "密码长度应在 3 到 16 个字符之间", trigger: "blur" }
+    {
+      min: 3,
+      max: 16,
+      message: "密码长度应在 3 到 16 个字符之间",
+      trigger: "blur",
+    },
   ],
   confirmPassword: [
     { required: true, message: "请确认密码", trigger: "blur" },
-    { validator: (rule, value, callback) => {
-      if (value !== registerForm.password) {
-        callback(new Error("两次输入的密码不一致"));
-      } else {
-        callback();
-      }
-    }, trigger: "blur" }
-  ]
+    {
+      validator: (
+        rule: any,
+        value: string,
+        callback: (error?: Error) => void
+      ) => {
+        if (value !== registerForm.password) {
+          callback(new Error("两次输入的密码不一致"));
+        } else {
+          callback();
+        }
+      },
+      trigger: "blur",
+    },
+  ],
 });
 </script>
 
@@ -206,4 +252,4 @@ const registerRules = reactive({
   width: 100%;
   margin: 10px 0;
 }
-</style>
+</style>

+ 164 - 64
src/views/menu/AcidNeutralizationModel.vue

@@ -6,54 +6,124 @@
       </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-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-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-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-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-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-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-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>
+      <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>
@@ -67,10 +137,16 @@
 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';
 
-const form = reactive({
+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,
@@ -79,10 +155,9 @@ const form = reactive({
   Al: null,
 });
 
-const router = useRouter();
-const result = ref(null);
+const result = ref<number | null>(null);
 const dialogVisible = ref(false);
-const predictForm = ref(null);
+const predictForm = ref<any>(null);
 const errorMessages = reactive({
   init_pH: '',
   target_pH: '',
@@ -93,7 +168,9 @@ const errorMessages = reactive({
 });
 
 // 使用 ref 来存储输入框元素
-const inputRefs = reactive({
+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),
@@ -103,27 +180,31 @@ const inputRefs = reactive({
 });
 
 // 限制输入为数字并校验范围
-const handleInput = (field: string, event: Event, min: number, max: number) => {
+const handleInput = (field: keyof typeof form, event: Event, min: number, max: number) => {
   try {
-    const input = inputRefs[field].value;
-    if (!input) {
+    const inputElement = inputRefs[field].value;
+    if (!inputElement) {
       console.error('未找到输入框元素');
       return;
     }
+
     // 过滤非数字和小数点字符
-    const filteredValue = input.value.replace(/[^0-9.]/g, '');
-    input.value = filteredValue;
-    form[field] = filteredValue;
+    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) {
@@ -132,7 +213,7 @@ const handleInput = (field: string, event: Event, min: number, max: number) => {
   }
 };
 
-const validateInput = (field: string, value: string | number, min: number, max: number) => {
+const validateInput = (value: string | number, min: number, max: number): boolean => {
   const numValue = parseFloat(value as string);
   if (isNaN(numValue)) {
     return false;
@@ -140,22 +221,31 @@ const validateInput = (field: string, value: string | number, min: number, max:
   return numValue >= min && numValue <= max;
 };
 
+// 计算方法
 // 计算方法
 const onSubmit = async () => {
   const inputConfigs = [
-    { field: 'init_pH', min: 3, max: 6 },
-    { field: 'target_pH', min: 5, max: 7 },
-    { field: 'OM', min: 10, max: 30 },
-    { field: 'CL', min: 50, max: 600 },
-    { field: 'H', min: 0, max: 4 },
-    { field: 'Al', min: 0, max: 4 },
+    { 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];
-    if (!validateInput(field, value, min, max)) {
+    
+    // 检查是否为 null
+    if (value === null) {
+      isValid = false;
+      errorMessages[field] = `输入值应在 ${min} 到 ${max} 之间且为有效数字`;
+      return;
+    }
+
+    if (!validateInput(value, min, max)) {
       isValid = false;
       errorMessages[field] = `输入值应在 ${min} 到 ${max} 之间且为有效数字`;
     } else {
@@ -195,14 +285,18 @@ const onSubmit = async () => {
       ElMessage.error('未获取到有效的预测结果');
     }
     dialogVisible.value = true;
-  } catch (error) {
+  } catch (error: unknown) {
     console.error('请求失败:', error);
-    if (error.response) {
-      ElMessage.error(`请求失败,状态码: ${error.response.status}`);
-    } else if (error.request) {
-      ElMessage.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.message);
+      ElMessage.error('请求过程中发生错误: ' + (error as Error).message);
     }
   }
 };
@@ -210,13 +304,19 @@ const onSubmit = async () => {
 // 监听弹窗关闭事件,关闭后初始化值
 const onDialogClose = () => {
   dialogVisible.value = false;
-  form.init_pH = null;
-  form.target_pH = null;
-  form.OM = null;
-  form.CL = null;
-  form.H = null;
-  form.Al = null;
-  Object.keys(errorMessages).forEach(key => errorMessages[key] = '');
+
+  // 初始化 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();
@@ -258,7 +358,7 @@ const onDialogClose = () => {
 .onSubmit {
   display: block;
   margin: 0 auto;
-  background-color: #007BFF;
+  background-color: #007bff;
   color: white;
   padding: 10px 20px;
   border-radius: 5px;
@@ -283,4 +383,4 @@ const onDialogClose = () => {
     --el-form-label-width: 100px;
   }
 }
-</style>
+</style>

+ 192 - 125
src/views/menu/Calculation.vue

@@ -5,53 +5,114 @@
         <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)"
-                    ref="inputRefs.OM"
-                    @input="handleInput('OM', $event, 0, 30)">
-          </el-input>
+    <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)"
-                    ref="inputRefs.CL"
-                    @input="handleInput('CL', $event, 50, 400)">
-          </el-input>
+      <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)"
-                    ref="inputRefs.CEC"
-                    @input="handleInput('CEC', $event, 0, 15)">
-          </el-input>
+      <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)"
-                    ref="inputRefs.H_plus"
-                    @input="handleInput('H_plus', $event, 0, 1)">
-          </el-input>
+      <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)"
-                    ref="inputRefs.N"
-                    @input="handleInput('N', $event, 0, 0.2)">
-          </el-input>
+      <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)"
-                    ref="inputRefs.Al3_plus"
-                    @input="handleInput('Al3_plus', $event, 0, 6)">
-          </el-input>
+      <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-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="计算结果">
+      <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>
@@ -62,13 +123,24 @@
 </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';
+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";
 
-const form = reactive({
+// 定义表单数据接口
+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,
@@ -77,96 +149,78 @@ const form = reactive({
   Al3_plus: null,
   delta_ph: null,
 });
+
 const router = useRouter();
-const result = ref(null);
+const result = ref<number | null>(null);
 const dialogVisible = ref(false);
-const predictForm = ref(null);
-const errorMessages = reactive({
-  OM: '',
-  CL: '',
-  CEC: '',
-  H_plus: '',
-  N: '',
-  Al3_plus: '',
-});
-
-// 使用 ref 来存储输入框元素
-const inputRefs = reactive({
-  OM: ref<HTMLInputElement | null>(null),
-  CL: ref<HTMLInputElement | null>(null),
-  CEC: ref<HTMLInputElement | null>(null),
-  H_plus: ref<HTMLInputElement | null>(null),
-  N: ref<HTMLInputElement | null>(null),
-  Al3_plus: ref<HTMLInputElement | null>(null),
+const predictForm = ref<any>(null);
+const errorMessages = reactive<Record<string, string>>({
+  OM: "",
+  CL: "",
+  CEC: "",
+  H_plus: "",
+  N: "",
+  Al3_plus: "",
 });
 
 // 限制输入为数字并校验范围
-const handleInput = (field: string, event: Event, min: number, max: number) => {
-  try {
-    const input = inputRefs[field].value;
-    if (!input) {
-      console.error('未找到输入框元素');
-      return;
-    }
-    // 过滤非数字和小数点字符
-    const filteredValue = input.value.replace(/[^0-9.]/g, '');
-    input.value = filteredValue;
-    form[field] = filteredValue;
-
-    if (filteredValue === '') {
-      errorMessages[field] = '';
-      return;
-    }
+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;
 
-    const numValue = parseFloat(filteredValue);
-    if (isNaN(numValue) || numValue < min || numValue > max) {
-      errorMessages[field] = `输入值应在 ${min} 到 ${max} 之间且为有效数字`;
-    } else {
-      errorMessages[field] = '';
-    }
-  } catch (error) {
-    console.error(`处理输入时出错:`, error);
-    errorMessages[field] = '输入处理出错,请检查输入';
+  if (!value) {
+    errorMessages[field] = "";
+    return;
   }
-};
 
-const validateInput = (field: string, value: string, min: number, max: number) => {
   const numValue = parseFloat(value);
-  if (isNaN(numValue)) {
-    return false;
+  if (isNaN(numValue) || numValue < min || numValue > max) {
+    errorMessages[field] = `输入值应在 ${min} 到 ${max} 之间且为有效数字`;
+  } else {
+    errorMessages[field] = "";
   }
-  return numValue >= min && numValue <= max;
+};
+
+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', min: 0, max: 30 },
-    { field: 'CL', min: 50, max: 400 },
-    { field: 'CEC', min: 0, max: 15 },
-    { field: 'H_plus', min: 0, max: 1 },
-    { field: 'N', min: 0, max: 0.2 },
-    { field: 'Al3_plus', min: 0, max: 6 },
+    { 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;
-  inputConfigs.forEach((config) => {
+  for (const config of inputConfigs) {
     const { field, min, max } = config;
-    const value = form[field];
-    if (!validateInput(field, value, min, max)) {
+    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] = '';
+      errorMessages[field] = "";
     }
-  });
+  }
 
   if (!isValid) {
-    ElMessage.error('输入值不符合要求,请检查输入');
+    ElMessage.error("输入值不符合要求,请检查输入");
     return;
   }
 
-  console.log('开始计算...');
+  console.log("开始计算...");
   const data = {
     model_id: 9,
     parameters: {
@@ -176,31 +230,35 @@ const onSubmit = async () => {
       h_concentration: form.H_plus,
       n: form.N,
       al_concentration: form.Al3_plus,
-    }
+    },
   };
 
   try {
-    const response = await request.post('/predict', data, {
+    const response = await request.post("/predict", data, {
       headers: {
-        'Content-Type': 'application/json'
-      }
+        "Content-Type": "application/json",
+      },
     });
-    console.log('预测结果:', response.data);
-    if (response.data && response.data.result && response.data.result.length > 0) {
+    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('未获取到有效的预测结果');
+      console.error("未获取到有效的预测结果");
+      ElMessage.error("未获取到有效的预测结果");
     }
     dialogVisible.value = true;
-  } catch (error) {
-    console.error('请求失败:', error);
+  } catch (error: any) {
+    console.error("请求失败:", error);
     if (error.response) {
       ElMessage.error(`请求失败,状态码: ${error.response.status}`);
     } else if (error.request) {
-      ElMessage.error('请求发送成功,但没有收到响应');
+      ElMessage.error("请求发送成功,但没有收到响应");
     } else {
-      ElMessage.error('请求过程中发生错误: ' + error.message);
+      ElMessage.error("请求过程中发生错误: " + error.message);
     }
   }
 };
@@ -208,17 +266,26 @@ const onSubmit = async () => {
 // 监听弹窗关闭事件,关闭后初始化值
 const onDialogClose = () => {
   dialogVisible.value = false;
-  form.OM = null;
-  form.CL = null;
-  form.CEC = null;
-  form.H_plus = null;
-  form.N = null;
-  form.Al3_plus = null;
-  form.delta_ph = null;
-  Object.keys(errorMessages).forEach(key => errorMessages[key] = '');
+
+  // 使用类型断言确保 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.resetFields();
+      (predictForm.value as any).resetFields(); // 使用类型断言避免类型错误
     }
   });
 };
@@ -257,7 +324,7 @@ const onDialogClose = () => {
 .onSubmit {
   display: block;
   margin: 0 auto;
-  background-color: #007BFF;
+  background-color: #007bff;
   color: white;
   padding: 10px 20px;
   border-radius: 5px;
@@ -289,4 +356,4 @@ const onDialogClose = () => {
   border-color: #dcdfe6;
   color: #606266;
 }
-</style>
+</style>

+ 2 - 2
src/views/menu/IntroUpdateModal.vue

@@ -50,7 +50,7 @@ const isLoading = ref(true);
 const fetchData = async () => {
   try {
     isLoading.value = true;
-    const response = await axios.get(`http://127.0.0.1:5000/software-intro/${props.targetId}`);
+    const response = await axios.get(`https://soilgd.com:5000/software-intro/${props.targetId}`);
     const { title, intro } = response.data;
     let processedIntro = intro;
     // 处理多余的换行符
@@ -100,7 +100,7 @@ const updateIntro = async () => {
       intro: processedIntro
     };
     // 发送 PUT 请求更新数据
-    const response = await axios.put(`http://127.0.0.1:5000/software-intro/${props.targetId}`, dataToSend, {
+    const response = await axios.put(`https://soilgd.com:5000/software-intro/${props.targetId}`, dataToSend, {
       headers: {
         'Content-Type': 'application/json'
       }

+ 3 - 3
src/views/menu/Introduce.vue

@@ -38,7 +38,7 @@ const error = ref('');
 
 // 判断是否为标题段落
 const isTitle = (paragraph: string) => {
-  return /^[^\s].*[:]$/.test(paragraph);
+  return /^[^\s].*[::]$/.test(paragraph);
 };
 
 const imageBaseUrl = ref(import.meta.env.VITE_IMAGE_BASE_URL);
@@ -52,11 +52,11 @@ const processParagraph = (paragraph: string) => {
 onMounted(async () => {
   try {
     // 从后端获取介绍数据
-    const response = await axios.get(`http://127.0.0.1:5000/software-intro/${props.targetId}`);
+    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 => paragraph.trim()!== '');
+    softwareIntro.value.introParagraphs = processedIntro.split('\r\n').filter((paragraph: string) => paragraph.trim()!== '');
     softwareIntro.value.title = title;
   } catch (err: any) {
     error.value = err.message || '请求数据时发生错误';

+ 4 - 1
src/views/menu/IntroductionUpdate.vue

@@ -11,8 +11,10 @@
         {{ button.label }}
       </el-button>
     </div>
+    <!-- 确保只有在 currentTargetId 不为 null 时渲染 IntroUpdateModal -->
     <IntroUpdateModal
-      :targetId="currentTargetId"
+      v-if="currentTargetId !== null"
+      :targetId="currentTargetId as number"
       :isVisible="!!currentTargetId"
       @updateSuccess="handleUpdateSuccess"
     />
@@ -38,6 +40,7 @@ const showComponent = (id: number) => {
 
 const handleUpdateSuccess = () => {
   console.log('子组件更新成功事件已接收');
+  // 可以在此处添加更多的逻辑处理
 };
 
 // 页面加载时默认选中第一个按钮

+ 3 - 3
src/views/menu/SoilAcidReductionIterativeEvolution.vue

@@ -29,9 +29,9 @@ const ecMidScatterOptionRef = ref<InstanceType<typeof VueEcharts> | null>(null);
 const ecFinalScatterOptionRef = ref<InstanceType<typeof VueEcharts> | null>(null);
 
 // 计算数据范围的函数
-const calculateDataRange = (data) => {
-  const xValues = data.map(item => item[0]);
-  const yValues = data.map(item => item[1]);
+const calculateDataRange = (data: any[]) => {
+  const xValues = data.map((item: any[]) => item[0]);
+  const yValues = data.map((item: any[]) => item[1]);
   return {
     xMin: Math.min(...xValues),
     xMax: Math.max(...xValues),

+ 3 - 3
src/views/menu/SoilAcidificationIterativeEvolution.vue

@@ -29,9 +29,9 @@ const ecMidScatterOptionRef = ref<InstanceType<typeof VueEcharts> | null>(null);
 const ecFinalScatterOptionRef = ref<InstanceType<typeof VueEcharts> | null>(null);
 
 // 计算数据范围的函数
-const calculateDataRange = (data) => {
-  const xValues = data.map(item => item[0]);
-  const yValues = data.map(item => item[1]);
+const calculateDataRange = (data: any[]) => {
+  const xValues = data.map((item: any[]) => item[0]);
+  const yValues = data.map((item: any[]) => item[1]);
   return {
     xMin: Math.min(...xValues),
     xMax: Math.max(...xValues),

+ 480 - 5
src/views/menu/mapView.vue

@@ -1,8 +1,483 @@
-<script setup lang='ts' name=''>
-</script>
-
 <template>
-  <div class="">地图</div>
+  <div class="map-page">
+    <div ref="mapContainer" 
+    class="map-container"
+    ></div>
+    <div v-if="error" class="error">{{ error }}</div>
+    <!-- 覆盖层控制 -->
+    <div class="control-panel">
+      <label>
+        <input type="checkbox" v-model="state.showOverlay" @change="toggleOverlay" />
+        显示土壤类型覆盖
+      </label>
+    </div>
+  </div>
 </template>
 
-<style scoped></style>
+<script setup>
+import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
+import axios from 'axios'
+import markerIcon from '@/assets/marker.png' 
+
+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,
+  excelData: [],
+  lastTapTime: 0
+})
+
+const tMapConfig = reactive({
+  key: import.meta.env.VITE_TMAP_KEY, // 请替换为你的开发者密钥
+  geocoderURL: 'https://apis.map.qq.com/ws/geocoder/v1/'
+})
+
+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&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 {
+    await loadSDK()
+    
+    map = new TMap.value.Map(mapContainer.value, {
+      center: new TMap.value.LatLng(23.891756, 110.273511),
+      zoom: 6,
+    })
+
+    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 }
+    })
+
+    // 绑定点击事件
+    map.on('click', handleMapClick)
+    markersLayer.on('click', handleMarkerClick)
+
+    loadData()
+    updateMarkers()
+  } catch (err) {
+    error.value = err.message
+  }
+}
+
+// 加载数据并创建标记
+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 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) => {
+  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 createTempMarker = (lat, lng, phValue) => {
+  return new TMap.Marker({
+    position: new TMap.value.LatLng(lat, lng),
+    styleId: 'temp',
+    properties: {
+      title: '克里金插值',
+      phValue: parseFloat(phValue).toFixed(2),
+      isTemp: true
+    }
+  })
+}
+
+
+
+// 验证坐标有效性
+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 
+}
+}
+
+
+
+// 切换覆盖层
+const toggleOverlay = () => {
+  if (state.showOverlay) {
+    overlay = new TMap.value.ImageGroundLayer({
+      map: map,
+      bounds: new TMap.value.LatLngBounds(
+        new TMap.value.LatLng(18.03, 104.25), 
+        new TMap.value.LatLng(31.26, 119.86)
+      ),
+      src: 'https://soilgd.com/images/farmland_cut.png'
+    })
+  } else {
+    if (overlay) {
+      overlay.setMap(null)
+      overlay = null
+    }
+  }
+}
+
+onMounted(async () => {
+  try {
+    await loadSDK()
+    initData()
+    await initMap()
+  } 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
+  }
+})
+</script>
+
+<style scoped>
+.map-page {
+  position: relative;
+  width: 100vw;
+  height: 100vh;
+}
+
+.map-container {
+  width: 100%;
+  height: 100vh !important;
+  min-height: 600px;
+}
+
+.control-panel {
+  position: fixed;
+  top: 20px;
+  right: 20px;
+  background: white;
+  padding: 10px;
+  border-radius: 4px;
+  box-shadow: 0 2px 6px rgba(0,0,0,0.15);
+  z-index: 999;
+}
+
+/* 信息窗口样式 */
+: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;
+}
+</style>

+ 39 - 17
src/views/packaged/PaginationComponent.vue

@@ -13,40 +13,62 @@
 </template>
 
 <script lang="ts" setup>
-import { ref, watch, onMounted } from 'vue'
-import { useAttrs } from '@vue/runtime-core'
+import { ref, watch, onMounted } from 'vue';
 
-const props = defineProps<{
+// 定义 props
+interface Props {
   total: number;
   currentPage?: number;
   pageSize?: number;
-}>()
+}
 
+const props = defineProps<Props>();
+
+// 定义 emits
 const emit = defineEmits<{
   (e: 'update:currentPage', value: number): void;
   (e: 'update:pageSize', value: number): void;
   (e: 'size-change', value: number): void;
   (e: 'current-change', value: number): void;
-}>()
+}>();
 
-let currentPage = ref(props.currentPage || 1)
-let pageSize = ref(props.pageSize || 10)
+// 初始化 currentPage 和 pageSize
+const currentPage = ref<number>(props.currentPage ?? 1);
+const pageSize = ref<number>(props.pageSize ?? 10);
 
+// 监听 props.currentPage 变化
 watch(() => props.currentPage, (newVal) => {
-  if (newVal !== currentPage.value) currentPage.value = newVal
-})
+  if (newVal !== undefined && newVal !== currentPage.value) {
+    currentPage.value = newVal;
+  }
+});
 
+// 监听 props.pageSize 变化
 watch(() => props.pageSize, (newVal) => {
-  if (newVal !== pageSize.value) pageSize.value = newVal
-})
+  if (newVal !== undefined && newVal !== pageSize.value) {
+    pageSize.value = newVal;
+  }
+});
 
+// 监听 currentPage 变化并触发事件
 watch(currentPage, (newVal) => {
-  emit('update:currentPage', newVal)
-  emit('current-change', newVal)
-})
+  emit('update:currentPage', newVal);
+  emit('current-change', newVal);
+});
 
+// 监听 pageSize 变化并触发事件
 watch(pageSize, (newVal) => {
-  emit('update:pageSize', newVal)
-  emit('size-change', newVal)
-})
+  emit('update:pageSize', newVal);
+  emit('size-change', newVal);
+});
+
+// 处理页面大小变化
+const handleSizeChange = (newSize: number) => {
+  pageSize.value = newSize;
+};
+
+// 处理当前页变化
+const handleCurrentChange = (newPage: number) => {
+  currentPage.value = newPage;
+};
 </script>