Преглед изворни кода

修复数据管理部分问题

qw пре 5 месеци
родитељ
комит
1219ae6a65
40 измењених фајлова са 8846 додато и 4255 уклоњено
  1. 2 0
      auto-imports.d.ts
  2. 2 0
      components.d.ts
  3. 6 1
      env.d.ts
  4. 470 43
      package-lock.json
  5. 4 2
      package.json
  6. 130 145
      src/API/admin.ts
  7. 264 90
      src/API/menus.ts
  8. 1 1
      src/API/users.ts
  9. 1 1
      src/components/detectionStatistics/crosscetionStatistics.vue
  10. 101 84
      src/components/layout/AppAside.vue
  11. 74 60
      src/components/layout/AppAsideForTab2.vue
  12. 49 39
      src/components/layout/AppLayout.vue
  13. 340 366
      src/components/layout/menuItems.ts
  14. 3 2
      src/components/layout/menuItems2.ts
  15. 13 31
      src/locales/en.json
  16. 11 35
      src/locales/zh.json
  17. 43 36
      src/router/index.ts
  18. 1 1
      src/stores/mytoken.ts
  19. 8 15
      src/utils/request.ts
  20. 0 1
      src/views/AboutView.vue
  21. 549 281
      src/views/Admin/dataManagement/HeavyMetalInputFluxManager/agriProductInputFluxData.vue
  22. 528 273
      src/views/Admin/dataManagement/HeavyMetalInputFluxManager/atmosphericInputFluxData.vue
  23. 526 286
      src/views/Admin/dataManagement/HeavyMetalInputFluxManager/atmosphericSampleData.vue
  24. 588 315
      src/views/Admin/dataManagement/HeavyMetalInputFluxManager/crossSectionSampleData.vue
  25. 557 282
      src/views/Admin/dataManagement/HeavyMetalInputFluxManager/heavyMetalEnterpriseData.vue
  26. 689 319
      src/views/Admin/dataManagement/HeavyMetalInputFluxManager/irrigationWaterInputFluxData.vue
  27. 535 277
      src/views/Admin/dataManagement/HeavyMetalInputFluxManager/irrigationWaterSampleData.vue
  28. 1251 315
      src/views/Admin/dataManagement/Soil Acidification and Acid Reduction Data Management/soilAcidReductionData.vue
  29. 1260 301
      src/views/Admin/dataManagement/Soil Acidification and Acid Reduction Data Management/soilAcidificationData.vue
  30. 123 135
      src/views/Admin/parameterConfig/ModelSelection.vue
  31. 97 107
      src/views/Admin/parameterConfig/ModelTrain.vue
  32. 81 71
      src/views/Admin/parameterConfig/thres.vue
  33. 107 52
      src/views/Admin/userManagement/UserManagement.vue
  34. 77 142
      src/views/User/acidModel/Calculation.vue
  35. 0 1
      src/views/User/acidModel/ModelIterationVisualization.vue
  36. 50 6
      src/views/User/neutralizationModel/AcidNeutralizationModel.vue
  37. 249 119
      src/views/login/loginView.vue
  38. 26 12
      tsconfig.app.json
  39. 23 0
      tsconfig.base.json
  40. 7 8
      tsconfig.vitest.json

+ 2 - 0
auto-imports.d.ts

@@ -9,6 +9,8 @@ declare global {
   const EffectScope: typeof import('vue')['EffectScope']
   const ElM: typeof import('element-plus/es')['ElM']
   const ElMessage: typeof import('element-plus/es')['ElMessage']
+  const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
+  const ElNotification: typeof import('element-plus/es')['ElNotification']
   const computed: typeof import('vue')['computed']
   const createApp: typeof import('vue')['createApp']
   const customRef: typeof import('vue')['customRef']

+ 2 - 0
components.d.ts

@@ -37,6 +37,7 @@ declare module 'vue' {
     ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
     ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
     ElContainer: typeof import('element-plus/es')['ElContainer']
+    ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
     ElDialog: typeof import('element-plus/es')['ElDialog']
     ElDropdown: typeof import('element-plus/es')['ElDropdown']
     ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
@@ -47,6 +48,7 @@ declare module 'vue' {
     ElIcon: typeof import('element-plus/es')['ElIcon']
     ElImage: typeof import('element-plus/es')['ElImage']
     ElInput: typeof import('element-plus/es')['ElInput']
+    ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
     ElMain: typeof import('element-plus/es')['ElMain']
     ElMenu: typeof import('element-plus/es')['ElMenu']
     ElMenuItem: typeof import('element-plus/es')['ElMenuItem']

+ 6 - 1
env.d.ts

@@ -1 +1,6 @@
-/// <reference types="vite/client" />
+// src/env.d.ts
+declare module '*.vue' {
+  import type { DefineComponent } from 'vue'
+  const component: DefineComponent<{}, {}, any>
+  export default component
+}

Разлика између датотеке није приказан због своје велике величине
+ 470 - 43
package-lock.json


+ 4 - 2
package.json

@@ -13,12 +13,12 @@
   },
   "dependencies": {
     "@element-plus/icons-vue": "^2.3.1",
-    "@intlify/unplugin-vue-i18n": "^6.0.8",
     "@turf/turf": "^7.2.0",
     "@types/d3": "^7.4.3",
     "@vue-leaflet/vue-leaflet": "^0.10.1",
     "@wangeditor/editor": "^5.1.23",
     "@wangeditor/editor-for-vue": "^5.1.12",
+    "antd": "^5.27.4",
     "axios": "^1.11.0",
     "chart.js": "^4.4.9",
     "coordtransform": "^2.1.2",
@@ -47,11 +47,12 @@
   },
   "devDependencies": {
     "@iconify-json/ep": "^1.2.2",
+    "@intlify/unplugin-vue-i18n": "^11.0.1",
     "@tsconfig/node22": "^22.0.2",
+    "@types/file-saver": "^2.0.7",
     "@types/jsdom": "^21.1.7",
     "@types/leaflet": "^1.9.16",
     "@types/node": "^22.15.31",
-    "@types/vue": "^2.0.0",
     "@vitejs/plugin-vue": "^5.2.1",
     "@vue/test-utils": "^2.4.6",
     "@vue/tsconfig": "^0.7.0",
@@ -61,6 +62,7 @@
     "unplugin-auto-import": "^19.0.0",
     "unplugin-icons": "^22.0.0",
     "unplugin-vue-components": "^28.0.0",
+    "unplugin-vue-i18n": "^1.0.11",
     "vite": "^6.3.5",
     "vite-plugin-vue-devtools": "^7.6.8",
     "vitest": "^2.1.9",

+ 130 - 145
src/API/admin.ts

@@ -1,23 +1,17 @@
-// @/API/admin.ts
-import { api8000 } from '@/utils/request';
-import { ElMessage } from 'element-plus';
-import 'element-plus/es/components/message/style/css';
+import { api5000 } from '@/utils/request';
+import { AxiosResponse } from 'axios';
 
 // 🔹 获取表格数据
 export const table = (params: { table: string }) => {
-  return api8000({
+  return api5000({
     url: "/admin/table",
-    method: "GET",
-    params: params,
-  })
-    .then((res) => {
-      // 适配后端返回的直接数组格式
-      return { data: Array.isArray(res.data) ? res.data : [] };
-    })
-    .catch((err) => {
-      if (err.response?.status === 401) throw err;
-      throw new Error(err.message || "获取数据失败");
-    });
+    method: "POST",
+    data: params,
+  }).then((res) => {
+    return {
+      data: res.data?.rows ?? [],
+    };
+  });
 };
 
 // 🔹 新增数据
@@ -25,16 +19,10 @@ export const addItem = (data: {
   table: string;
   item: Record<string, any>;
 }) => {
-  return api8000({
+  return api5000({
     url: "/admin/add_item",
     method: "POST",
-    params: { table: data.table },
-    data: data.item,
-  }).catch((error) => {
-    if (error.response?.status === 400 && error.response.data.detail === "重复数据") {
-      ElMessage.error("数据重复,请重新添加");
-    }
-    throw error;
+    data: { table: data.table, item: data.item },
   });
 };
 
@@ -44,16 +32,15 @@ export const updateItem = (data: {
   id: number;
   update_data: Record<string, any>;
 }) => {
-  return api8000({
+  const payload = {
+    table: data.table,
+    item: { ...data.update_data, id: data.id }
+  };
+
+  return api5000({
     url: "/admin/update_item",
     method: "PUT",
-    params: { table: data.table, id: data.id },
-    data: data.update_data,
-  }).catch((error) => {
-    if (error.response?.status === 400 && error.response.data.detail === "重复数据") {
-      ElMessage.error("数据重复,无法更新");
-    }
-    throw error;
+    data: payload,
   });
 };
 
@@ -62,147 +49,145 @@ export const deleteItemApi = (params: {
   table: string;
   id: number;
 }) => {
-  return api8000({
+  const requestBody = {
+    table: params.table,
+    condition: `id=${params.id}`
+  };
+
+  return api5000({
     url: "/admin/delete_item",
-    method: "DELETE",
-    params: params,
+    method: "POST",
+    data: requestBody,
   });
 };
 
 // 🔹 导出数据
 export const exportData = (table: string, format: string = "xlsx") => {
-  return api8000({
+  let backendFormat = format === 'xlsx' ? 'excel' : format === 'csv' ? 'csv' : 'excel';
+
+  return api5000({
     url: "/admin/export_data",
     method: "GET",
-    params: { table, fmt: format },
+    params: { table, format: backendFormat },
     responseType: "blob",
-  })
-    .then((response) => {
-      const contentDisposition = response.headers["content-disposition"];
-      if (!contentDisposition) {
-        throw new Error("无法获取文件名");
+  }).then((response: AxiosResponse<Blob>) => {
+    const contentDisposition = response.headers["content-disposition"] || response.headers["Content-Disposition"];
+    let filename = "";
+
+    const utf8Match = contentDisposition?.match(/filename\*=UTF-8''(.+)/i);
+    if (utf8Match && utf8Match[1]) {
+      try {
+        filename = decodeURIComponent(utf8Match[1]);
+      } catch (e) { /* ignore */ }
+    }
+
+    if (!filename) {
+      const asciiMatch = contentDisposition?.match(/filename\*?=?"?([^";]+)"?/i);
+      if (asciiMatch && asciiMatch[1]) {
+        filename = asciiMatch[1].replace(/['"]/g, '');
       }
-      const filenameMatch = contentDisposition.match(/filename=([^;]+)/);
-      const filename = filenameMatch ? decodeURIComponent(filenameMatch[1]) : `${table}_data.${format === "xlsx" ? "xlsx" : "csv"}`;
-
-      const blob = new Blob([response.data], {
-        type: format === "xlsx"
-          ? "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
-          : "text/csv",
-      });
-      const url = window.URL.createObjectURL(blob);
-      const link = document.createElement("a");
-      link.href = url;
-      link.download = filename;
-      link.click();
-      window.URL.revokeObjectURL(url);
-    })
-    .catch((error) => {
-      console.error("❌ 导出数据失败:", error);
-      throw error;
-    });
+    }
+
+    if (!filename) {
+      filename = `${table}_data.${backendFormat === "excel" ? "xlsx" : "csv"}`;
+    }
+
+    const mimeType = backendFormat === "excel"
+      ? "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
+      : "text/csv";
+
+    const blob = new Blob([response.data], { type: mimeType });
+    const url = window.URL.createObjectURL(blob);
+    const link = document.createElement("a");
+    link.href = url;
+    link.download = filename;
+    document.body.appendChild(link);
+    link.click();
+    document.body.removeChild(link);
+    window.URL.revokeObjectURL(url);
+  });
 };
 
 // 🔹 导入数据
-export const importData = (table: string, file: File) => {
-  // 1. 前置校验提示(文件为空/格式错误)
-  if (!file || file.size === 0) {
-    const errorMsg = "❌ 请选择非空的Excel/CSV文件,空文件无法导入";
-    console.error(errorMsg);
-    throw new Error(errorMsg);
-  }
-
-  const validExtensions = ['xlsx', 'csv'];
-  const fileExtension = file.name.split('.').pop()?.toLowerCase();
-  if (!fileExtension || !validExtensions.includes(fileExtension)) {
-    const errorMsg = `❌ 仅支持 .xlsx 和 .csv 格式文件,当前文件格式为 .${fileExtension || '未知'}`;
-    console.error(errorMsg);
-    throw new Error(errorMsg);
+export const importData = (table: string, formData: FormData) => {
+  const file = formData.get('file');
+  if (!file || !(file instanceof File) || file.size === 0) {
+    return Promise.reject(new Error("File is empty or invalid"));
   }
 
-  // 2. 创建FormData(与后端参数名对齐)
-  const formData = new FormData();
-  formData.append('table', table);
-  formData.append('file', file);
+  formData.append("dataset_type", table); 
 
-  return api8000({
-    url: "/admin/import_data",
+  return api5000({
+    url: "upload-dataset",
     method: "POST",
     data: formData,
     headers: {
-      'Content-Type': 'multipart/form-data'
     }
-  })
-  .then((response) => {
-    const { total_data, new_data, duplicate_data } = response.data;
-    let successMsg = "";
-    if (total_data === 0) {
-      successMsg = "✅ 文件导入成功,但文件中无有效数据(空内容)";
-    } else {
-      successMsg = `✅ 数据导入成功!共${total_data}条数据,新增${new_data}条,重复${duplicate_data}条`;
-    }
-    console.log(successMsg, "详情:", response.data);
-    ElMessage.success(successMsg);
-    return response.data;
-  })
-  .catch((error) => {
-    let errorMsg = "❌ 导入失败,请稍后重试";
-
-    if (error.response?.data) {
-      const resData = error.response.data;
-      if (resData.detail?.duplicate_count && resData.detail?.duplicates) {
-        const { duplicate_count, duplicates, check_fields } = resData.detail;
-        const duplicateDetails = duplicates.map((item: any) => 
-          `第${item.row_number}行:${check_fields.map((f: string) => `${f}=${item.duplicate_fields[f] || '空值'}`).join(',')}`
-        ).join('\n');
-        errorMsg = `❌ 检测到${duplicate_count}条重复数据(基于${check_fields.join('、')}字段):\n${duplicateDetails}`;
-      } else if (resData.detail) {
-        errorMsg = `❌ ${resData.detail.message || resData.detail}`;
-      } else if (resData.message) {
-        errorMsg = `❌ ${resData.message}`;
-      }
-    } else if (!error.response) {
-      errorMsg = "❌ 网络异常或服务器无响应,请检查网络连接后重试";
-    } else {
-      errorMsg = "❌ 未知错误,可能是文件损坏或服务器繁忙";
-    }
-
-    console.error(errorMsg, "错误详情:", error);
-    ElMessage.error(errorMsg);
-    throw new Error(errorMsg);
+  }).then(response => {
+    return response;
   });
 };
 
 // 🔹 下载模板
 export const downloadTemplate = (table: string, format: string = "excel") => {
-  return api8000({
+  return api5000({
     url: "/admin/download_template",
     method: "GET",
     params: { table, format },
     responseType: "blob",
-  })
-    .then((response) => {
-      const contentDisposition = response.headers["content-disposition"];
-      if (!contentDisposition) {
-        throw new Error("无法获取文件名");
+  }).then((response) => {
+    const contentDisposition = response.headers["content-disposition"];
+    let filename = "";
+
+    const utf8Match = contentDisposition?.match(/filename\*=UTF-8''(.+)/i);
+    if (utf8Match && utf8Match[1]) {
+      try {
+        filename = decodeURIComponent(utf8Match[1]);
+      } catch (e) { /* ignore */ }
+    }
+
+    if (!filename) {
+      const asciiMatch = contentDisposition?.match(/filename="?([^"]+)"?/i);
+      if (asciiMatch && asciiMatch[1]) {
+        filename = asciiMatch[1].replace(/['"]/g, '');
       }
-      const filenameMatch = contentDisposition.match(/filename=([^;]+)/);
-      const filename = filenameMatch ? decodeURIComponent(filenameMatch[1]) : `${table}_template.${format === "excel" ? "xlsx" : "csv"}`;
-
-      const blob = new Blob([response.data], {
-        type: format === "excel"
-          ? "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
-          : "text/csv",
-      });
-      const url = window.URL.createObjectURL(blob);
-      const link = document.createElement("a");
-      link.href = url;
-      link.download = filename;
-      link.click();
-      window.URL.revokeObjectURL(url);
-    })
-    .catch((error) => {
-      console.error("❌ 下载模板失败:", error);
-      throw error;
-    });
+    }
+
+    if (!filename) {
+      filename = `${table}_template.${format === "excel" ? "xlsx" : "csv"}`;
+    }
+
+    const contentType = format === "excel"
+      ? "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
+      : "text/csv";
+
+    const blob = new Blob([response.data], { type: contentType });
+    const url = window.URL.createObjectURL(blob);
+    const link = document.createElement("a");
+    link.href = url;
+    link.download = filename;
+    link.style.display = 'none';
+    document.body.appendChild(link);
+    link.click();
+    document.body.removeChild(link);
+    window.URL.revokeObjectURL(url);
+  });
+};
+
+export const changeLanguageAPI = async (data: { language: string; userId?: number; timestamp: number }) => {
+  const response = await fetch('/admin/change-language', {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+      // 如果需要认证,可以添加 token
+      // 'Authorization': `Bearer ${store.token}`,
+    },
+    body: JSON.stringify(data),
+  });
+
+  if (!response.ok) {
+    throw new Error(`HTTP error! status: ${response.status}`);
+  }
+  
+  return await response.json();
 };

+ 264 - 90
src/API/menus.ts

@@ -1,122 +1,296 @@
-import { api8000 } from "@/utils/request";
+import { api8000 } from "@/utils/request"; // 确保这是您项目中正确的请求函数
+import { ElMessage } from 'element-plus';
+import 'element-plus/es/components/message/style/css';
+
+// 🔹 定义查询参数类型
+interface QueryTableParams {
+  table: string;
+  filters?: string; // JSON 字符串格式的过滤条件
+  page?: number;
+  page_size?: number;
+  sort_by?: string;
+  sort_order?: 'asc' | 'desc';
+}
+
+// 🔹 定义查询返回结果类型
+interface QueryTableResponse {
+  data: Record<string, any>[];
+  pagination: {
+    total: number;
+    page: number;
+    page_size: number;
+    total_pages: number;
+  };
+}
+
+/// 🔹 通用查询表格数据 (增强版)
+export const queryTable = (params: QueryTableParams): Promise<QueryTableResponse> => {
+  // 设置默认分页参数
+  const requestParams = {
+    page: 1,
+    page_size: 10,
+    sort_order: 'asc' as const, // 默认升序
+    ...params, // 覆盖默认值
+  };
 
-// 使用导入的 api8000 函数进行网络请求
-export const table = (params: { table: string }) => {
   return api8000({
-    url: "/admin/table",
+    url: "/admin/query_table", // 确保后端有此对应路由
     method: "GET",
-    data: params,
-  });
-};
-
-const customRequest = (options: any) => {
-  return api8000(options)
-    .then(response => response.data)
-    .catch(error => {
-      if (error.response && error.response.status === 409) {
-        console.error('数据重复:', error.response.data);
-        throw new Error('数据重复,请重新添加');
-      } else {
-        console.error(`请求失败: ${error.message || '未知错误'}`);
-        throw new Error(`请求失败: ${error.message || '未知错误'}`);
-      }
+    params: requestParams,
+  })
+    .then((res) => {
+      // 假设后端严格按照定义返回结构
+      return res.data as QueryTableResponse;
+    })
+    .catch((err) => {
+      console.error("查询数据失败:", err);
+      if (err.response?.status === 401) throw err;
+      throw new Error(err.message || "获取数据失败");
     });
 };
 
-// 提交编辑数据
-export const updateItem = (data: { table: string, item: any }) => {
-  return customRequest({
-    url: "/admin/update_item",
-    method: "PUT",
-    data: data,
-  });
+// 🔹 获取表格数据 (保持兼容性,仅获取所有数据)
+export const table = (params: { table: string }) => {
+  return api8000({
+    url: "/admin/table",
+    method: "GET",
+    params: params,
+  })
+    .then((res) => {
+      // 适配后端返回的直接数组格式
+      return { data: Array.isArray(res.data) ? res.data : [] };
+    })
+    .catch((err) => {
+      if (err.response?.status === 401) throw err;
+      throw new Error(err.message || "获取数据失败");
+    });
 };
 
-// 提交新增数据
-export const addItem = (data: { table: string, item: any }) => {
-  return customRequest({
+// 🔹 新增数据
+export const addItem = (data: {
+  table: string;
+  item: Record<string, any>;
+}) => {
+  return api8000({
     url: "/admin/add_item",
     method: "POST",
-    data: data,
-  }).catch(error => {
-    if (error.message.includes("数据重复")) {
-      alert(error.message); 
+    params: { table: data.table },
+    data: data.item,
+  }).catch((error) => {
+    if (error.response?.status === 400 && error.response.data.detail === "重复数据") {
+      ElMessage.error("数据重复,请重新添加");
     }
-    throw error; 
+    throw error;
   });
 };
 
-// 删除
-export const deleteItemApi = (params: { table: string; condition: any }) => {
-  const conditionString = `${Object.keys(params.condition)[0]}=${params.condition[Object.keys(params.condition)[0]]}`;
-  return customRequest({
-    url: "/admin/delete_item",
-    method: "POST",
-    data: {
-      table: params.table,
-      condition: conditionString,
-    },
+// 🔹 更新数据
+export const updateItem = (data: {
+  table: string;
+  id: number;
+  update_data: Record<string, any>;
+}) => {
+  return api8000({
+    url: "/admin/update_item",
+    method: "PUT",
+    params: { table: data.table, id: data.id },
+    data: data.update_data,
+  }).catch((error) => {
+    if (error.response?.status === 400 && error.response.data.detail === "重复数据") {
+      ElMessage.error("数据重复,无法更新");
+    }
+    throw error;
   });
 };
 
-// 下载模板
-export const downloadTemplate = (table: string, format: string = 'xlsx') => {
-  return customRequest({
-    url: "/admin/download_template",
-    method: "GET",
-    params: { table, format },
-    responseType: 'blob',
-  }).then(response => {
-    const blob = new Blob([response], {
-      type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
-    });
-    const url = window.URL.createObjectURL(blob);
-    const link = document.createElement('a');
-    link.href = url;
-    link.download = `${table}_template.${format}`;
-    link.click();
-    window.URL.revokeObjectURL(url);
+// 🔹 删除数据
+export const deleteItemApi = (params: {
+  table: string;
+  id: number;
+}) => {
+  return api8000({
+    url: "/admin/delete_item",
+    method: "DELETE",
+    params: params,
   });
 };
 
-export const exportData = (table: string, format: string = 'excel') => {
-  const backendFormat = format.toLowerCase() === 'xlsx' ? 'excel' : format;
-
-  //表格数据并导出
-  return customRequest({
+// 🔹 导出数据 (改进版)
+export const exportData = (table: string, format: string = "xlsx") => {
+  return api8000({
     url: "/admin/export_data",
     method: "GET",
-    params: { table, format: backendFormat },
-    responseType: 'blob',
-  }).then(response => {
-    const blob = new Blob([response], {
-      type: backendFormat === 'excel' 
-        ? 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' 
-        : 'text/csv',
+    params: { table, fmt: format }, // 注意后端参数是 fmt
+    responseType: "blob",
+  })
+    .then((response) => {
+      // --- 修改从这里开始 ---
+      let filename = `${table}_data.${format === "xlsx" ? "xlsx" : "csv"}`; // 默认文件名
+      const contentDisposition = response.headers["content-disposition"];
+      if (contentDisposition) {
+        const filenameMatch = contentDisposition.match(/filename[^;]*=([^;]+)/);
+        // 使用 filename*= 优先匹配 UTF-8 编码的文件名
+        const utf8FilenameMatch = contentDisposition.match(/filename\*=UTF-8''(.+)/);
+        if (utf8FilenameMatch) {
+           try {
+              // 解码 UTF-8 文件名
+              filename = decodeURIComponent(utf8FilenameMatch[1]);
+           } catch (e) {
+              console.warn("解码 UTF-8 文件名失败:", e);
+           }
+        } else if (filenameMatch) {
+          try {
+            // 解码普通文件名 (可能包含引号)
+            filename = decodeURIComponent(filenameMatch[1].trim().replace(/['"]/g, ''));
+          } catch (e) {
+             console.warn("解码文件名失败:", e);
+          }
+        }
+      } else {
+         console.warn("响应头中未找到 Content-Disposition,使用默认文件名");
+      }
+      // --- 修改到这里结束 ---
+
+      const blob = new Blob([response.data], {
+        type: format === "xlsx"
+          ? "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
+          : "text/csv",
+      });
+      const url = window.URL.createObjectURL(blob);
+      const link = document.createElement("a");
+      link.href = url;
+      link.download = filename; // 使用解析或默认的文件名
+      document.body.appendChild(link); // 兼容性建议
+      link.click();
+      document.body.removeChild(link); // 兼容性建议
+      window.URL.revokeObjectURL(url);
+    })
+    .catch((error) => {
+      console.error("❌ 导出数据失败:", error);
+      throw error;
     });
-    const url = window.URL.createObjectURL(blob);
-    const link = document.createElement('a');
-    link.href = url;
-    link.download = `${table}_data.${backendFormat === 'excel' ? 'xlsx' : 'csv'}`;
-    link.click();
-    window.URL.revokeObjectURL(url);
-  }).catch(error => {
-    console.error('导出数据时发生错误:', error);
-    throw error;
-  });
 };
 
- // 导入数据
-export const importData = (table: string, file: File) => {
+// 🔹 导入数据 (修正:使用 api8000 替代 customRequest)
+export const importData = (table: string, file: File) => { // <--- 添加类型注解 here
   const formData = new FormData();
-  formData.append('file', file);
-  formData.append('table', table);
-  return customRequest({
+  formData.append("file", file);
+  formData.append("table", table);
+
+  // 返回 api8000 的 Promise 链
+  return api8000({
     url: "/admin/import_data",
     method: "POST",
     data: formData,
     headers: {
-      'Content-Type': 'multipart/form-data',
+      "Content-Type": "multipart/form-data",
     },
+  }).then(response => {
+    // 检查响应数据
+    if (response.data && response.data.total_data === 0) {
+      // 如果 total_data 为 0,认为是空数据导入,抛出一个错误
+      // 可以自定义错误信息或使用 Error 对象
+      throw new Error('导入的数据为空,请检查上传的文件是否包含有效数据。');
+      // 或者使用后端可能返回的特定错误码/信息
+      // throw { code: 'EMPTY_IMPORT_DATA', message: '导入的数据为空...', details: response.data };
+    }
+    // 如果数据不为空,正常返回响应
+    return response;
   });
-};
+  // 调用者可以通过 .catch() 或 async/await 的 try...catch 捕获上面抛出的错误
+};
+
+// 🔹 下载模板
+// 辅助函数:解析 Content-Disposition 头以获取文件名
+function getFileNameFromContentDisposition(contentDispositionHeader: string): string | null {
+  if (!contentDispositionHeader) {
+    return null;
+  }
+
+  let fileName: string | null = null;
+
+  // 尝试匹配 filename* (支持编码)
+  // 格式: filename*=UTF-8''%E4%B8%AD%E6%96%87%E6%96%87%E4%BB%B6%E5%90%8D.xlsx
+  const fileNameStarRegex = /filename\*=(?:UTF-8'')?([^;]+)/i;
+  const fileNameStarMatch = contentDispositionHeader.match(fileNameStarRegex);
+  if (fileNameStarMatch && fileNameStarMatch[1]) {
+    try {
+      // filename* 的值通常是百分号编码的 (Percent-encoding)
+      fileName = decodeURIComponent(fileNameStarMatch[1]);
+      return fileName;
+    } catch (e) {
+      console.warn("解码 filename* 失败:", e);
+      // 如果解码失败,继续尝试 filename
+    }
+  }
+
+  // 如果 filename* 不存在或解码失败,则尝试匹配 filename
+  const fileNameRegex = /filename=([^;]+)/i; // 移除了开头的 ^,以防前面有空格或其他内容
+  const fileNameMatch = contentDispositionHeader.match(fileNameRegex);
+  if (fileNameMatch && fileNameMatch[1]) {
+    fileName = fileNameMatch[1].trim(); // 去除首尾空格
+
+    // 检查是否被双引号包围
+    if (fileName.startsWith('"') && fileName.endsWith('"')) {
+      fileName = fileName.substring(1, fileName.length - 1); // 移除双引号
+    }
+
+    // 尝试解码,以防它是 URL 编码的 (虽然不如 filename* 标准)
+    try {
+      fileName = decodeURIComponent(fileName);
+    } catch (e) {
+      console.warn("解码 filename 失败:", e);
+      // 如果解码失败,就使用原始提取的字符串(可能包含引号或编码字符)
+      // 或者可以在这里决定是返回 null 还是原始字符串
+    }
+    return fileName;
+  }
+
+  // 如果都未匹配到,则返回 null
+  return null;
+}
+
+
+// 🔹 下载模板 (已修复文件名解析)
+export const downloadTemplate = (table: string, format: string = "excel") => {
+  return api8000({
+    url: "/admin/download_template",
+    method: "GET",
+    params: { table, format },
+    responseType: "blob",
+  })
+    .then((response) => {
+      const contentDisposition = response.headers["content-disposition"];
+      
+      let filename: string | null = null;
+      if (contentDisposition) {
+        filename = getFileNameFromContentDisposition(contentDisposition);
+      }
+
+      // 如果未能从响应头解析出文件名,则使用默认文件名
+      if (!filename) {
+        console.warn("无法从 Content-Disposition 头解析文件名,使用默认文件名。");
+        filename = `${table}_template.${format === "excel" ? "xlsx" : "csv"}`;
+      }
+
+      const blob = new Blob([response.data], {
+        type:
+          format === "excel"
+            ? "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
+            : "text/csv",
+      });
+      const url = window.URL.createObjectURL(blob);
+      const link = document.createElement("a");
+      link.href = url;
+      link.download = filename; // 使用解析或默认的文件名
+      document.body.appendChild(link); // 推荐添加到 DOM 再点击
+      link.click();
+      document.body.removeChild(link); // 点击后移除
+      window.URL.revokeObjectURL(url);
+    })
+    .catch((error) => {
+      console.error("❌ 下载模板失败:", error);
+      // 可以在这里添加用户提示,例如 ElMessage.error("模板下载失败");
+      throw error;
+    });
+};

+ 1 - 1
src/API/users.ts

@@ -91,4 +91,4 @@ export const logout = async () => {
   store.clearToken();
   localStorage.removeItem("userInfo");
   return { success: true, message: "已退出登录" };
-};
+};

+ 1 - 1
src/components/detectionStatistics/crosscetionStatistics.vue

@@ -88,7 +88,7 @@ export default {
           throw new Error(`网络请求失败: ${response.status}`);
         }
         
-        const data = response.data;
+        let data = response.data;
         
         // 处理可能的字符串响应
         if (typeof data === 'string') {

+ 101 - 84
src/components/layout/AppAside.vue

@@ -1,25 +1,30 @@
 <template>
-  <el-scrollbar v-if="showTabs && activeTab !== 'selectCityAndCounty'">
+  <el-scrollbar v-show="showTabs && activeTab !== 'selectCityAndCounty'">
     <el-menu
       :collapse="isCollapse"
       router
       unique-opened
-      :default-active="activeMenuItem.index"
+      :default-active="currentActiveIndex"
+      :default-openeds="openKeys"
       class="professional-menu"
+      @select="onMenuSelect"
     >
       <template v-for="item in filteredMenuItems" :key="item.index">
-        <el-sub-menu v-if="item.children" :index="item.index">
+        <el-sub-menu
+          v-if="item.children?.length"
+          :index="item.index"
+        >
           <template #title>
-            <el-icon><component :is="item.icon" /></el-icon>
-            <span>{{$t(item.label)}}</span>
+            <el-icon v-if="item.icon"><component :is="item.icon" /></el-icon>
+            <span>{{ $t(item.label) }}</span>
           </template>
+
           <el-menu-item
             v-for="child in item.children"
             :key="child.index"
             :index="child.index"
-            @click="handleMenuClick(child.index)"
           >
-            <el-icon><component :is="child.icon" /></el-icon>
+            <el-icon v-if="child.icon"><component :is="child.icon" /></el-icon>
             <span>{{ $t(child.label) }}</span>
           </el-menu-item>
         </el-sub-menu>
@@ -27,9 +32,8 @@
         <el-menu-item
           v-else
           :index="item.index"
-          @click="handleMenuClick(item.index)"
         >
-          <el-icon><component :is="item.icon" /></el-icon>
+          <el-icon v-if="item.icon"><component :is="item.icon" /></el-icon>
           <span>{{ $t(item.label) }}</span>
         </el-menu-item>
       </template>
@@ -38,61 +42,93 @@
 </template>
 
 <script setup lang="ts">
-import { reactive, computed, inject, toRefs, watch } from 'vue';
-import { useRouter } from 'vue-router';
-import { ElMessage } from 'element-plus';
-import { menuItems } from './menuItems';
-
-const props = defineProps({
-  activeTab: {
-    type: String,
-    required: true,
-    default: 'introduction'
-  },
-  showTabs: {
-    type: Boolean,
-    required: false,
-    default: true
-  }
-});
+import { computed, inject, reactive, watch, nextTick } from 'vue';
+import { useRouter, useRoute } from 'vue-router';
+import { tabMenuMap, MenuItem } from './menuItems';
 
-const { activeTab } = toRefs(props);
-const isCollapse = inject('isCollapse', false);
-const userPermissions = inject('userPermissions', [] as string[]);
+interface Props {
+  activeTab: string;
+  showTabs?: boolean;
+}
+const props = defineProps<Props>();
 
 const router = useRouter();
-const activeMenuItem = reactive({ index: '' });
-
-const filteredMenuItems = computed(() =>
-  menuItems
-    .filter(item => item.tab === activeTab.value)
-    .map(item => {
-      if (!item.children) return item;
-      return {
-        ...item,
-        children: item.children.filter(
-          child => !child.permission || userPermissions.includes(child.permission)
-        )
-      };
-    })
-);
+const route = useRoute();
+const isCollapse = inject<boolean>('isCollapse', false);
+
+const filteredMenuItems = computed<MenuItem[]>(() => tabMenuMap[props.activeTab] || []);
+const currentActiveIndex = computed(() => route.path);
+const openKeys = reactive<string[]>([]);
 
-function handleMenuClick(index: string) {
+/** 安全跳转,保证不会被组件报错阻断 */
+function safeNavigate(path: string) {
+  if (!path) return;
   try {
-    if (router.currentRoute.value.path !== index) {
-      router.push(index);
-      activeMenuItem.index = index;
+    router.replace(path).catch(err => {
+      console.warn('Navigation error:', err?.message ?? err);
+    });
+  } catch (e) {
+    console.warn('Unexpected navigation error:', e);
+  }
+}
+
+/** 查找父菜单展开 */
+function findOpenKeys(menuItems: MenuItem[], path: string): string[] {
+  for (const item of menuItems) {
+    if (item.children?.some(child => child.index === path)) return [item.index];
+  }
+  return [];
+}
+
+/** 处理菜单 select 事件 */
+function onMenuSelect(index: string) {
+  // 找对应菜单项
+  let parent: MenuItem | undefined;
+  let target: MenuItem | undefined;
+
+  for (const item of filteredMenuItems.value) {
+    if (item.children?.length) {
+      const child = item.children.find(c => c.index === index);
+      if (child) {
+        parent = item;
+        target = child;
+        break;
+      }
+    } else if (item.index === index) {
+      target = item;
+      break;
     }
-  } catch (error) {
-    ElMessage.error('导航失败,请检查网络或联系管理员');
   }
+
+  if (!target) return;
+
+  // 更新父菜单展开
+  if (parent?.index) openKeys.splice(0, openKeys.length, parent.index);
+
+  // 安全跳转
+  safeNavigate(target.index);
 }
 
+/** 路由变化自动同步展开 */
+watch(
+  () => route.path,
+  (newPath) => {
+    const keys = findOpenKeys(filteredMenuItems.value, newPath);
+    openKeys.splice(0, openKeys.length, ...keys);
+  },
+  { immediate: true, flush: 'post' }
+);
+
+/** Tab 切换时自动跳转第一个菜单 */
 watch(
-  activeTab,
-  newVal => {
-    const current = filteredMenuItems.value?.[0];
-    activeMenuItem.index = current?.children?.[0]?.index || current?.index || '';
+  () => props.activeTab,
+  (newTab) => {
+    const items = tabMenuMap[newTab] || [];
+    if (!items.length) return;
+
+    const firstItem = items[0];
+    const path = firstItem.children?.[0]?.index || firstItem.index;
+    if (path && route.path !== path) nextTick(() => safeNavigate(path));
   },
   { immediate: true }
 );
@@ -100,12 +136,11 @@ watch(
 
 <style scoped>
 :deep(.el-scrollbar) {
-  background: linear-gradient(to bottom, #B7F1FC , #FFF8F0) !important;
+  background: linear-gradient(to bottom, #b7f1fc, #fff8f0) !important;
   height: 100%;
   border-right: none !important;
   padding-top: 12px;
 }
-
 :deep(.el-scrollbar__wrap),
 :deep(.el-scrollbar__view) {
   background: transparent;
@@ -113,7 +148,6 @@ watch(
   display: flex;
   flex-direction: column;
 }
-
 .professional-menu {
   background: transparent;
   border-right: none;
@@ -123,26 +157,18 @@ watch(
   flex-direction: column;
   min-height: 100%;
 }
-
-/* 统一菜单项和子菜单标题的内边距 */
 :deep(.el-menu-item),
 :deep(.el-sub-menu__title) {
-  margin: 0 !important;
+  margin-left: 0 !important;
+  margin-right: 0 !important;
   width: 100%;
   box-sizing: border-box;
+  padding-left: 40px !important;
   padding-right: 20px !important;
 }
-
-/* 子菜单项样式调整 */
 :deep(.el-sub-menu .el-menu-item) {
   background-color: rgba(252, 234, 183, 0.3) !important;
-  padding-left: 40px !important; /* 确保子菜单项与主菜单标题对齐 */
-}
-
-:deep(.el-sub-menu .el-menu-item:not(:last-child)) {
-  margin-bottom: 0;
 }
-
 :deep(.el-sub-menu__title) {
   font-size: 18px;
   font-weight: 500;
@@ -150,35 +176,26 @@ watch(
   border-radius: 6px;
   padding: 12px 16px !important;
   transition: all 0.2s ease;
+  display: flex;
+  align-items: center;
+  padding-left: 20px !important;
 }
-
-/* Hover 效果 */
 :deep(.el-menu-item:hover),
 :deep(.el-sub-menu__title:hover) {
   background-color: rgba(16, 146, 216, 0.1);
-  color: #1092D8;
+  color: #1092d8;
 }
-
-/* 激活高亮 */
 :deep(.el-menu-item.is-active),
 :deep(.el-sub-menu__title.is-active) {
-  background: linear-gradient(to right, #1092D8, #02C3AD);
+  background: linear-gradient(to right, #1092d8, #02c3ad);
   color: #ffffff !important;
   border-radius: 8px;
   font-weight: 600;
   box-shadow: 0 2px 8px rgba(16, 146, 216, 0.25);
 }
-
-/* 子菜单标题样式 */
-:deep(.el-sub-menu__title) {
-  display: flex;
-  align-items: center;
-}
-
-/* 下拉图标右移调整 */
 :deep(.el-sub-menu__icon-arrow) {
   margin-left: auto;
-  margin-right: 0; /* 移除负边距 */
+  margin-right: -160px;
   transition: transform 0.3s ease;
 }
-</style>
+</style>

+ 74 - 60
src/components/layout/AppAsideForTab2.vue

@@ -4,32 +4,45 @@
       :collapse="isCollapse"
       router
       unique-opened
-      :default-active="activeMenuItem.index"
+      :default-active="route.path"
       :default-openeds="openKeys"
-      @select="handleMenuClick"
+      @select="handleMenuSelect"
+      class="professional-menu"
     >
       <template v-if="menuList.length">
         <template v-for="item in menuList" :key="item.index">
-          <el-sub-menu v-if="item.children && item.children.length" :index="item.index">
+          <!-- 父菜单 -->
+          <el-sub-menu v-if="item.children?.length" :index="item.index">
             <template #title>
-              <el-icon v-if="item.icon"><component :is="item.icon" /></el-icon>
+              <el-icon v-if="item.icon">
+                <component :is="item.icon" />
+              </el-icon>
               <span>{{ item.label }}</span>
             </template>
+
+            <!-- 子菜单 -->
             <el-menu-item
               v-for="child in item.children"
               :key="child.index"
               :index="child.index"
             >
-              <el-icon v-if="child.icon"><component :is="child.icon" /></el-icon>
+              <el-icon v-if="child.icon">
+                <component :is="child.icon" />
+              </el-icon>
               <span>{{ child.label }}</span>
             </el-menu-item>
           </el-sub-menu>
+
+          <!-- 普通菜单 -->
           <el-menu-item v-else :index="item.index">
-            <el-icon v-if="item.icon"><component :is="item.icon" /></el-icon>
+            <el-icon v-if="item.icon">
+              <component :is="item.icon" />
+            </el-icon>
             <span>{{ item.label }}</span>
           </el-menu-item>
         </template>
       </template>
+
       <template v-else>
         <el-menu-item disabled>未知的 Tab:{{ activeTab }}</el-menu-item>
       </template>
@@ -39,90 +52,87 @@
 
 <script setup lang="ts">
 import { inject, reactive, watch, toRefs, computed } from "vue";
-import { useRouter, useRoute, type RouteLocationAsPathGeneric, type RouteLocationAsRelativeGeneric } from "vue-router";
+import { useRouter, useRoute } from "vue-router";
 import { ElMessage } from "element-plus";
 import { tabMenuMap } from "./menuItems2";
 
+// Props
 const props = defineProps({
-  activeTab: {
-    type: String,
-    required: true,
-    default: "introduction",
-  },
-  showTabs: {
-    type: Boolean,
-    required: true,
-    default: true,
-  },
+  activeTab: { type: String, required: true, default: "introduction" },
+  showTabs: { type: Boolean, default: true },
 });
 
 const { activeTab } = toRefs(props);
 const isCollapse = inject("isCollapse", false);
+
 const router = useRouter();
 const route = useRoute();
 
-const activeMenuItem = reactive({ index: route.path });
-const openKeys = reactive<string[]>([]);
+const menuList = computed(() => tabMenuMap[activeTab.value] || []);
 
-const menuList = computed(() => {
-  return tabMenuMap[activeTab.value] || [];
-});
+// 展开的父菜单 key
+const openKeys = reactive<string[]>([]);
 
-// 查找当前路由对应的父级菜单index,用于默认展开子菜单
-function findOpenKeys(menuItems: any[], currentPath: string, parents: string[] = []): string[] {
+// 根据当前 path 找到需要展开的父菜单
+function findOpenKeys(menuItems: any[], currentPath: string): string[] {
   for (const item of menuItems) {
-    if (item.index === currentPath) {
-      return parents;
-    } else if (item.children) {
-      const found = findOpenKeys(item.children, currentPath, [...parents, item.index]);
-      if (found.length) return found;
+    if (item.children?.some((child: any) => child.index === currentPath)) {
+      return [item.index];
     }
   }
   return [];
 }
 
+// 统一的菜单点击处理(父、子、普通菜单都走这里)
+const handleMenuSelect = (index: string) => {
+  if (index !== route.path) {
+    router.push(index).catch((err) => {
+      if (err.name !== "NavigationDuplicated") {
+        ElMessage.error("导航失败");
+        console.error("[Menu] 跳转失败:", err);
+      }
+    });
+  }
+  // 同步展开父菜单
+  const keys = findOpenKeys(menuList.value, index);
+  openKeys.splice(0, openKeys.length, ...keys);
+};
+
+// 监听路由变化 → 同步展开 & 高亮
 watch(
   () => route.path,
-  (newPath) => {
-    activeMenuItem.index = newPath;
-    openKeys.splice(0, openKeys.length, ...findOpenKeys(menuList.value, newPath));
+  (newPath: string) => {
+    const keys = findOpenKeys(menuList.value, newPath);
+    openKeys.splice(0, openKeys.length, ...keys);
   },
   { immediate: true }
 );
 
+// Tab 切换 → 自动跳第一个菜单
 watch(
   activeTab,
-  (newVal) => {
-    const first = tabMenuMap[newVal]?.[0];
-    if (first) activeMenuItem.index = first.index;
-    else activeMenuItem.index = "";
-    openKeys.splice(0, openKeys.length, ...findOpenKeys(menuList.value, activeMenuItem.index));
+  (newTab: string) => {
+    const items = tabMenuMap[newTab] || [];
+    const firstItem = items[0];
+    if (!firstItem) return;
+
+    const firstPath = firstItem.children?.[0]?.index || firstItem.index;
+    if (firstPath && firstPath !== route.path) {
+      router.replace(firstPath).catch(() => {});
+    }
   },
   { immediate: true }
 );
-
-async function handleMenuClick(index: string | RouteLocationAsRelativeGeneric | RouteLocationAsPathGeneric) {
-  try {
-    if (index === route.path) return;
-    await router.push(index);
-    activeMenuItem.index = index as string;
-  } catch (error) {
-    console.error(`[AppAsideForTab2] 路由跳转失败: ${index}`, error);
-    ElMessage.error("导航失败,请检查网络连接或联系管理员。");
-  }
-}
 </script>
 
 <style scoped>
-/* 给 el-menu 组件设置背景渐变 */
-:deep(.el-menu) {
-  background: linear-gradient(to bottom, #B7F1FC , #FFF8F0) !important;
+:deep(.el-scrollbar) {
+  background: linear-gradient(to bottom, #b7f1fc, #fff8f0) !important;
   height: 100%;
   border-right: none !important;
   padding-top: 12px;
 }
 
-/* 保持滚动条内容透明 */
 :deep(.el-scrollbar__wrap),
 :deep(.el-scrollbar__view) {
   background: transparent !important;
@@ -131,7 +141,16 @@ async function handleMenuClick(index: string | RouteLocationAsRelativeGeneric |
   flex-direction: column;
 }
 
-/* 菜单项和子菜单标题 */
+.professional-menu {
+  background: transparent;
+  border-right: none;
+  padding-top: 12px;
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  min-height: 100%;
+}
+
 :deep(.el-menu-item),
 :deep(.el-sub-menu__title) {
   margin-left: 0 !important;
@@ -142,36 +161,31 @@ async function handleMenuClick(index: string | RouteLocationAsRelativeGeneric |
   padding-right: 20px !important;
 }
 
-/* 子菜单中菜单项背景色 */
 :deep(.el-sub-menu .el-menu-item) {
   background-color: rgba(252, 234, 183, 0.3) !important;
 }
 
-/* Hover 效果 */
 :deep(.el-menu-item:hover),
 :deep(.el-sub-menu__title:hover) {
   background-color: rgba(16, 146, 216, 0.1) !important;
-  color: #1092D8 !important;
+  color: #1092d8 !important;
 }
 
-/* 激活高亮 */
 :deep(.el-menu-item.is-active),
 :deep(.el-sub-menu__title.is-active) {
-  background: linear-gradient(to right, #1092D8, #02C3AD) !important;
+  background: linear-gradient(to right, #1092d8, #02c3ad) !important;
   color: #ffffff !important;
   border-radius: 8px !important;
   font-weight: 600 !important;
   box-shadow: 0 2px 8px rgba(16, 146, 216, 0.25) !important;
 }
 
-/* 子菜单标题样式 + 图标右移 */
 :deep(.el-sub-menu__title) {
   display: flex;
   align-items: center;
   padding-left: 20px !important;
 }
 
-/* 下拉图标右移 */
 :deep(.el-sub-menu__icon-arrow) {
   margin-left: auto;
   margin-right: -160px;

+ 49 - 39
src/components/layout/AppLayout.vue

@@ -247,13 +247,13 @@ const tabs = computed(() => {
         label: "信息管理",
         icon: "el-icon-document",
         routes: ["/IntroductionUpdate"],
-      },*/
+      },
       {
         name: "modelManagement",
         label: "模型管理及配置",
         icon: "el-icon-cpu",
         routes: ["/CadmiumPredictionModel"],
-      },
+      },*/
       {
         name: "userManagement",
         label: "用户管理",
@@ -488,9 +488,10 @@ const mainStyle = computed(() => ({
   flex-direction: column;
   height: 100vh;
   overflow: hidden;
-  position: relative;
-  background-color: #f5f7fa;
-  /* 默认背景色 */
+  position: relative; /* 创建层叠上下文 */
+  background-color: #f5f7fa; /* 默认背景色 */
+  /* 确保 layout-wrapper 自身的 z-index 不干扰子元素,或根据需要设置 */
+  /* z-index: 0; */ /* 通常不需要显式设置,除非有特殊需求 */
 }
 
 /* 背景层 - 关键修改 */
@@ -500,7 +501,8 @@ const mainStyle = computed(() => ({
   left: 0;
   width: 100%;
   height: 100%;
-  z-index: 0;
+  /* 将背景层的 z-index 设为一个较低的负值或非常小的正值,确保它在最底层 */
+  z-index: -1; /* 修改点:使用负值确保其在最底层,避免任何可能的遮挡 */
   background-size: cover;
   background-position: center;
   background-repeat: no-repeat;
@@ -516,22 +518,18 @@ const mainStyle = computed(() => ({
 /* 特殊背景页面的Header样式 */
 .transparent-header {
   background: transparent !important;
-  backdrop-filter: blur(2px);
-  /* 添加轻微模糊效果增强可读性 */
+  backdrop-filter: blur(2px); /* 添加轻微模糊效果增强可读性 */
 }
 
 /* 特殊背景页面的文字颜色 */
 .light-text {
-  color: #ffffff !important;
-  /* 白色文字 */
-  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);
-  /* 文字阴影增强可读性 */
+  color: #ffffff !important; /* 白色文字 */
+  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.7); /* 文字阴影增强可读性 */
 }
 
 /* 特殊背景页面的头像边框 */
 .light-avatar-border {
-  border: 2px solid #ffffff !important;
-  /* 白色边框 */
+  border: 2px solid #ffffff !important; /* 白色边框 */
 }
 
 /* 内容区域在特殊背景页面透明 */
@@ -545,14 +543,13 @@ const mainStyle = computed(() => ({
   display: flex;
   align-items: center;
   justify-content: space-between;
-  color: #333;
-  /* 深色文字 */
+  color: #333; /* 深色文字 */
   flex-shrink: 0;
-  position: relative;
-  z-index: 2;
   /* 确保在背景层上方 */
-  background-color: white;
-  /* 默认背景色 */
+  position: relative;
+  /* 修改点:增大 z-index 确保在背景层之上 */
+  z-index: 10; /* 增大 z-index */
+  background-color: white; /* 默认背景色 */
 }
 
 .logo-title-row {
@@ -574,18 +571,17 @@ const mainStyle = computed(() => ({
   align-items: center;
   justify-content: flex-end;
   gap: 24px;
-  color: #333;
-  /* 深色文字 */
+  color: #333; /* 深色文字 */
   padding-top: 1px;
   position: static;
-  z-index: 1;
+  /* 修改点:增大 z-index 确保在背景层之上 */
+  z-index: 10; /* 增大 z-index */
 }
 
 .welcome-text {
   font-size: 28px;
   font-weight: 500;
-  color: #ffffff !important;
-  /* 深色文字 */
+  color: #ffffff !important; /* 深色文字 */
 }
 
 /* Tab 区域 - 不透明 */
@@ -598,9 +594,10 @@ const mainStyle = computed(() => ({
   background: linear-gradient(to right, #1092d8, #02c3ad);
   border-bottom: none !important;
   flex-shrink: 0;
-  position: relative;
-  z-index: 2;
   /* 确保在背景层上方 */
+  position: relative;
+  /* 修改点:增大 z-index 确保在背景层之上 */
+  z-index: 10; /* 增大 z-index */
 }
 
 /* el-tabs 外层容器 */
@@ -641,7 +638,8 @@ const mainStyle = computed(() => ({
   transition: all 0.2s ease-in-out;
   background-color: transparent;
   position: relative;
-  z-index: 1;
+  /* 修改点:增大 z-index 确保在背景层之上 */
+  z-index: 10; /* 增大 z-index */
 }
 
 /* 激活 Tab */
@@ -650,13 +648,16 @@ const mainStyle = computed(() => ({
   color: #ffffff;
   font-weight: 700;
   box-shadow: 0 4px 16px rgba(26, 188, 156, 0.4);
-  z-index: 2;
+  /* 修改点:增大 z-index 确保在背景层之上 */
+  z-index: 11; /* 激活项可以更高一点 */
 }
 
 /* 鼠标悬停 */
 .el-tabs__item:hover {
   background-color: #455a64;
   color: #ffffff;
+  /* 修改点:增大 z-index 确保在背景层之上 */
+  z-index: 11; /* 悬停时也可以更高 */
 }
 
 /* 图标样式 */
@@ -682,8 +683,7 @@ const mainStyle = computed(() => ({
   font-size: 48px;
   font-weight: bold;
   margin-top: 30px;
-  color: #333;
-  /* 深色文字 */
+  color: #333; /* 深色文字 */
 }
 
 .layout-main-container {
@@ -691,9 +691,10 @@ const mainStyle = computed(() => ({
   display: flex;
   overflow: hidden;
   min-height: 0;
-  position: relative;
-  z-index: 1;
   /* 确保在背景层上方 */
+  position: relative;
+  /* 修改点:增大 z-index 确保在背景层之上 */
+  z-index: 10; /* 增大 z-index */
 }
 
 /* 侧边栏 - 白色背景 */
@@ -706,8 +707,8 @@ const mainStyle = computed(() => ({
   padding-top: 8px;
   height: 100%;
   position: relative;
-  z-index: 2;
-  /* 确保在背景层上方 */
+  /* 修改点:增大 z-index 确保在背景层之上 */
+  z-index: 10; /* 增大 z-index */
 }
 
 /* 隐藏侧边栏滚动条 */
@@ -739,6 +740,8 @@ const mainStyle = computed(() => ({
   border-radius: 8px;
   font-weight: 600;
   box-shadow: 0 2px 8px rgba(16, 146, 216, 0.25);
+  /* 修改点:增大 z-index 确保在背景层之上 */
+  z-index: 11; /* 激活项可以更高一点 */
 }
 
 .layout-content-wrapper {
@@ -747,6 +750,8 @@ const mainStyle = computed(() => ({
   display: flex;
   flex-direction: column;
   position: relative;
+  /* 修改点:增大 z-index 确保在背景层之上 */
+  z-index: 10; /* 增大 z-index */
 }
 
 /* 强制重置 el-tabs header 高度/边距/背景/阴影,避免背景层穿透错位 */
@@ -757,7 +762,8 @@ const mainStyle = computed(() => ({
   border: none !important;
   background: transparent !important;
   box-shadow: none !important;
-  z-index: 0 !important;
+  /* 修改点:增大 z-index 确保在背景层之上 */
+  z-index: 10 !important; /* 增大 z-index */
 }
 
 /* 全屏页面特殊处理 */
@@ -770,11 +776,15 @@ const mainStyle = computed(() => ({
   overflow: auto;
   padding: 0 20px;
   box-sizing: border-box;
-  background-color: white;
-  /* 默认背景色 */
+  background-color: white; /* 默认背景色 */
+  position: relative; /* 确保它可以参与 z-index 计算 */
+  /* 修改点:增大 z-index 确保在背景层之上 */
+  z-index: 10; /* 增大 z-index */
 }
 
 .scrollable-content.transparent-scroll {
   background-color: transparent;
+  /* 修改点:增大 z-index 确保在背景层之上 */
+  z-index: 10; /* 即使透明也应保持在上层 */
 }
-</style>
+</style>

+ 340 - 366
src/components/layout/menuItems.ts

@@ -1,6 +1,5 @@
 // src/config/menuItems.ts
 import {
-  Menu as MenuIcon,
   Monitor,
   InfoFilled,
   DataLine,
@@ -16,376 +15,351 @@ import {
   Collection,
   MagicStick,
   HelpFilled,
-  Coin
-} from '@element-plus/icons-vue';
+  Coin,
+  Menu as MenuIcon,
+} from "@element-plus/icons-vue";
 
 export interface MenuItem {
   index: string;
   label: string;
-  icon?: any;
-  tab: string;
-  permission?: string;
+  icon: any;
   children?: MenuItem[];
 }
 
-export const menuItems: MenuItem[] = [
-  {
-    index: '/shuJuKanBan',
-    label: 'shuJuKanBan.Title',//<!--i18n:shuJuKanBan.Title-->数据看板
-    icon: Monitor,
-    tab: 'shuJuKanBan'
-  },
-  {
-    index: '/SoilPro',
-    label: 'SoilPro.Title',//<!--i18n:SoilPro.Title-->软件简介
-    icon: InfoFilled,
-    tab: 'introduction'
-  },
-  {
-    index: '/Overview',
-    label: 'Overview.Title',//<!--i18n:Overview.Title-->项目简介
-    icon: Collection,
-    tab: 'introduction'
-  },
-  {
-    index: '/ResearchFindings',
-    label: 'ResearchFindings.Title',//<!--i18n:ResearchFindings.Title-->研究成果
-    icon: Histogram,
-    tab: 'introduction'
-  },
-  {
-    index: '/Unit',
-    label: 'Unit.Title',//<!--i18n:Unit.Title-->团队信息
-    icon: HelpFilled,
-    tab: 'introduction'
-  },
-  {
-    index: 'irrigationWater',
-    label: 'irrigationwater.Title',//灌溉水
-    icon: Watermelon,
-    tab: 'HmOutFlux',
-    children: [
-      {
-        index: '/samplingMethodDevice1',
-        label: 'irrigationwater.irrigationwaterMethodsTitle',
-        icon: Sunny,
-        tab: 'HmOutFlux'
-      },
-      {
-        index: '/irriSampleData',
-        label: 'irrigationwater.pointTitle',
-        icon: Coin,
-        tab: 'HmOutFlux'
-      },
-      {
-        index: '/csSampleData',
-        label: 'irrigationwater.crosssectionTitle',
-        icon: Cloudy,
-        tab: 'HmOutFlux'
-      },
-      {
-        index: '/irriInputFlux',
-        label: 'irrigationwater.InputfluxTitle',
-        icon: Cloudy,
-        tab: 'HmOutFlux'
-      }
-    ]
-  },
-  {
-    index: 'inputFlux',
-    label: 'agriInput.Title',//农产品投入
-    icon: Watermelon,
-    tab: 'HmOutFlux',
-    children: [
-      {
-        index: '/farmInputSamplingDesc',
-        label: 'agriInput.farmInputSamplingDescTitle',//采样说明
-        icon: Sunny,
-        tab: 'HmOutFlux'
-      },
-      {
-        index: '/prodInputFlux',
-        label: 'agriInput.prodInputFluxTitle',//农产品输入通量
-        icon: Coin,
-        tab: 'HmOutFlux'
-      },
-    ]
-  },
-  {
-    index: 'atmosDeposition',
-    label: 'atmosDeposition.Title',//大气干湿沉降
-    icon: Watermelon,
-    tab: 'HmOutFlux',
-    children: [
-      {
-        index: '/AtmosDepositionSamplingDesc',
-        label: 'atmosDeposition.AtmosDepositionSamplingDescTitle',//采样说明
-        icon: Sunny,
-        tab: 'HmOutFlux'
-      },
-      {
-        index: '/heavyMetalEnterprise',
-        label: 'atmosDeposition.heavyMetalEnterpriseTitle',//涉重企业
-        icon: Coin,
-        tab: 'HmOutFlux'
-      },
-      {
-        index: '/airSampleData',
-        label: 'atmosDeposition.airSampleDataTitle',//大气采样数据
-        icon: Sunny,
-        tab: 'HmOutFlux'
-      },
-      {
-        index: '/airInputFlux',
-        label: 'atmosDeposition.airInputFluxTitle',//大气输入通量
-        icon: Coin,
-        tab: 'HmOutFlux'
-      },
-    ]
-  },
-  {
-    index: 'totalInputFlux',
-    label: '输入总通量',
-    icon: WindPower,
-    tab: 'HmOutFlux',
-    children: [
-      {
-        index: '/totalInputFluxDesc',
-        label: '输入总通量说明',
-        icon: Watermelon,
-        tab: 'HmOutFlux',
-      },
-      {
-        index: '/totalInputFlux',
-        label: '输入总通量结果',
-        icon: List,
-        tab: 'HmOutFlux',
-      },
-    ]
-  },
-  {
-    index: 'grainRemoval',
-    label: 'grainRemoval.Title',//<!--i18n:grainRemoval.Title-->籽粒移除
-    icon: WindPower,
-    tab: 'hmInFlux',
-    children: [
-      {
-        index: '/samplingDesc1',
-        label: 'grainRemoval.samplingDesc1',//<!--i18n:grainRemoval.samplingDesc1-->采样说明
-        icon: Watermelon,
-        tab: 'hmInFlux'
-      },
-      {
-        index: '/grainRemovalInputFlux',
-        label: 'grainRemoval.grainRemovalInputFlux',//<!--i18n:grainRemoval.grainRemovalInputFlux-->籽粒移除输出通量
-        icon: List,
-        tab: 'hmInFlux'
-      }
-    ]
-  },
-  {
-    index: 'strawRemoval',
-    label: 'strawRemoval.Title',//<!--i18n:strawRemoval.Title-->秸秆移除
-    icon: WindPower,
-    tab: 'hmInFlux',
-    children: [
-      {
-        index: '/samplingDesc2',
-        label: 'strawRemoval.samplingDesc2',//<!--i18n:strawRemoval.samplingDesc2-->采样说明
-        icon: Watermelon,
-        tab: 'hmInFlux'
-      },
-      {
-        index: '/strawRemovalInputFlux',
-        label: 'strawRemoval.strawRemovalInputFlux',//<!--i18n:strawRemoval.strawRemovalInputFlux-->秸秆移除输出通量
-        icon: List,
-        tab: 'hmInFlux'
-      }
-    ]
-  },
-  {
-    index: 'subsurfaceLeakage',
-    label: 'subsurfaceLeakage.Title',//<!--i18n:subsurfaceLeakage.Title-->地下渗漏
-    icon: WindPower,
-    tab: 'hmInFlux',
-    children: [
-      {
-        index: '/samplingDesc3',
-        label: 'subsurfaceLeakage.samplingDesc3',//<!--i18n:subsurfaceLeakage.samplingDesc3-->采样说明
-        icon: Watermelon,
-        tab: 'hmInFlux'
-      },
-      {
-        index: '/subsurfaceLeakageInputFlux',
-        label: 'subsurfaceLeakage.subsurfaceLeakageInputFlux',//<!--i18n:subsurfaceLeakage.subsurfaceLeakageInputFlux-->地下渗漏输入通量
-        icon: List,
-        tab: 'hmInFlux'
-      }
-    ]
-  },
-  {
-    index: 'surfaceRunoff',
-    label: 'surfaceRunoff.Title',//<!--i18n:surfaceRunoff.Title-->地表径流
-    icon: WindPower,
-    tab: 'hmInFlux',
-    children: [
-      {
-        index: '/samplingDesc4',
-        label: 'surfaceRunoff.samplingDesc4',//<!--i18n:surfaceRunoff.samplingDesc4-->采样说明
-        icon: Watermelon,
-        tab: 'hmInFlux'
-      },
-      {
-        index: '/surfaceRunoffInputFlux',
-        label: 'surfaceRunoff.surfaceRunoffInputFlux',//<!--i18n:surfaceRunoff.surfaceRunoffInputFlux-->地表径流输入通量
-        icon: List,
-        tab: 'hmInFlux'
-      }
-    ]
-  },
-  {
-    index: 'totalOutputFlux',
-    label: '输出总通量',
-    icon: WindPower,
-    tab: 'hmInFlux',
-    children: [
-      {
-        index: '/totalOutputFluxDesc',
-        label: '输出总通量说明',
-        icon: Watermelon,
-        tab: 'hmInFlux',
-      },
-      {
-        index: '/totalOutputFlux',
-        label: '输出总通量结果',
-        icon: List,
-        tab: 'hmInFlux',
-      },
-    ]
-  },
-  {
-    index: '/mapView',
-    label: 'mapView.Title',//<!--i18n:mapView.Title-->地图展示
-    icon: Location,
-    tab: 'mapView'
-  },
-  {
-    index: '/netFlux',
-    label: 'netFlux.Title',//<!--i18n:netFlux.Title-->净通量
-    icon: PieChart,
-    tab: 'cadmiumPrediction'
-  },
-  {
-    index: '/currentYearConcentration',
-    label: 'currentYearConcentration.Title',//<!--i18n:currentYearConcentration.Title-->当年浓度
-    icon: PieChart,
-    tab: 'cadmiumPrediction'
-  },
-  {
-    index: '/EffectiveCadmiumPrediction',
-    label: 'EffectiveCadmiumPrediction.Title',//<!--i18n:EffectiveCadmiumPrediction.Title-->土壤镉有效态含量预测
-    icon: PieChart,
-    tab: 'cadmiumPrediction'
-  },
-  {
-    index: '/CropCadmiumPrediction',
-    label: 'CropCadmiumPrediction.Title',//<!--i18n:CropCadmiumPrediction.Title-->土壤镉作物态含量预测
-    icon: PieChart,
-    tab: 'cadmiumPrediction'
-  },
-  // {
-  //   index: '/cropRiskAssessment',
-  //   label: 'cropRiskAssessment.Title',//<!--i18n:cropRiskAssessment.Title-->水稻镉污染风险
-  //   icon: Compass,
-  //   tab: 'cropRiskAssessment'
-  // },
-  {
-    index: '/farmlandQualityAssessment',
-    label: 'farmlandQualityAssessment.Title',//<!--i18n:farmlandQualityAssessment.Title-->韶关
-    icon: DataLine,
-    tab: 'farmlandQualityAssessment'
-  },
-  {
-    index: '/acidModel',
-    label: 'acidModel.Title',//<!--i18n:acidModel.Title-->土壤反酸
-    icon: MagicStick,
-    tab: 'soilAcidificationPrediction',
-    children: [
-      {
-        index: '/Calculation',
-        label: 'acidModel.CalculationTitle',//<!--i18n:acidModel.CalculationTitle-->土壤反酸预测
-        icon: Sunny,
-        tab: 'heavyMetalFluxCalculation'
-      },
-      {
-        index: '/SoilAcidReductionIterativeEvolution',
-        label: 'acidModel.SoilAcidReductionIterativeEvolutionTitle',//<!--i18n:acidModel.SoilAcidReductionIterativeEvolutionTitle-->反酸模型迭代可视化
-        icon: Coin,
-        tab: 'heavyMetalFluxCalculation'
-      }
-    ]
-  },
-  {
-    index: '/neutralizationModel',
-    label: 'neutralizationModel.Title',//<!--i18n:neutralizationModel.Title-->土壤降酸
-    icon: MagicStick,
-    tab: 'soilAcidificationPrediction',
-    children: [
-      {
-        index: '/AcidNeutralizationModel',
-        label: 'neutralizationModel.AcidNeutralizationModelTitle',//<!--i18n:neutralizationModel.AcidNeutralizationModelTitle-->土壤降酸预测
-        icon: Sunny,
-        tab: 'heavyMetalFluxCalculation'
-      },
-      {
-        index: '/SoilAcidificationIterativeEvolution',
-        label: 'neutralizationModel.SoilAcidificationIterativeEvolutionTitle',//<!--i18n:neutralizationModel.SoilAcidificationIterativeEvolutionTitle-->土壤降酸可视化
-        icon: Coin,
-        tab: 'heavyMetalFluxCalculation'
-      }
-    ]
-  },
-  // {
-  //   index: '/TraditionalFarmingRisk',
-  //   label: 'TraditionalFarmingRisk.Title',//<!--i18n:TraditionalFarmingRisk.Title-->传统耕种习惯风险趋势
-  //   icon: MenuIcon,
-  //   tab: 'scenarioSimulation'
-  // },
-  // {
-  //   index: '/HeavyMetalCadmiumControl',
-  //   label: 'HeavyMetalCadmiumControl.Title',//<!--i18n:HeavyMetalCadmiumControl.Title-->重金属镉污染治理
-  //   icon: MenuIcon,
-  //   tab: 'scenarioSimulation'
-  // },
-  // {
-  //   index: '/SoilAcidificationControl',
-  //   label: 'SoilAcidificationControl.Title',//<!--i18n:SoilAcidificationControl.Title-->土壤酸化治理
-  //   icon: MenuIcon,
-  //   tab: 'scenarioSimulation'
-  // },
-  {
-    index: '/DetectionStatistics',
-    label: 'DetectionStatistics.Title',//<!--i18n:DetectionStatistics.Title-->检测信息统计
-    icon: List,
-    tab: 'dataStatistics'
-  },
-  {
-    index: '/FarmlandPollutionStatistics',
-    label: 'FarmlandPollutionStatistics.Title',//<!--i18n:FarmlandPollutionStatistics.Title-->土壤镉含量统计
-    icon: List,
-    tab: 'dataStatistics'
-  },
-  {
-    index: '/LandClutivatesStatistics',
-    label: 'LandClutivatesStatistics.Title',//<!--i18n:LandClutivatesStatistics.Title-->作物风险评估统计
-    icon: List,
-    tab: 'dataStatistics'
-  },
-  {
-    index: '/SoilacidificationStatistics',
-    label: 'SoilacidificationStatistics.Title',//<!--i18n:SoilacidificationStatistics.Title-->酸化预测数据统计
-    icon: List,
-    tab: 'dataStatistics'
-  }
-].filter(({ tab: menuTab }) => !["shuJuKanBan", "mapView", "introduction"].includes(menuTab));
+// 按 tab 分组的菜单映射
+export const tabMenuMap: Record<string, MenuItem[]> = {
+  shuJuKanBan: [
+    {
+      index: "/shuJuKanBan",
+      label: "shuJuKanBan.Title",
+      icon: Monitor,
+    },
+  ],
+
+  introduction: [
+    {
+      index: "/SoilPro",
+      label: "SoilPro.Title",
+      icon: InfoFilled,
+    },
+    {
+      index: "/Overview",
+      label: "Overview.Title",
+      icon: Collection,
+    },
+    {
+      index: "/ResearchFindings",
+      label: "ResearchFindings.Title",
+      icon: Histogram,
+    },
+    {
+      index: "/Unit",
+      label: "Unit.Title",
+      icon: HelpFilled,
+    },
+  ],
+
+  mapView: [
+    {
+      index: "/mapView",
+      label: "mapView.Title",
+      icon: Location,
+    },
+  ],
+
+  HmOutFlux: [
+    {
+      index: "irrigationWater",
+      label: "irrigationwater.Title",
+      icon: Watermelon,
+      children: [
+        {
+          index: "/samplingMethodDevice1",
+          label: "irrigationwater.irrigationwaterMethodsTitle",
+          icon: Sunny,
+        },
+        {
+          index: "/irriSampleData",
+          label: "irrigationwater.pointTitle",
+          icon: Coin,
+        },
+        {
+          index: "/csSampleData",
+          label: "irrigationwater.crosssectionTitle",
+          icon: Cloudy,
+        },
+        {
+          index: "/irriInputFlux",
+          label: "irrigationwater.InputfluxTitle",
+          icon: Cloudy,
+        },
+      ],
+    },
+    {
+      index: "inputFlux",
+      label: "agriInput.Title",
+      icon: Watermelon,
+      children: [
+        {
+          index: "/farmInputSamplingDesc",
+          label: "agriInput.farmInputSamplingDescTitle",
+          icon: Sunny,
+        },
+        {
+          index: "/prodInputFlux",
+          label: "agriInput.prodInputFluxTitle",
+          icon: Coin,
+        },
+      ],
+    },
+    {
+      index: "atmosDeposition",
+      label: "atmosDeposition.Title",
+      icon: Watermelon,
+      children: [
+        {
+          index: "/AtmosDepositionSamplingDesc",
+          label: "atmosDeposition.AtmosDepositionSamplingDescTitle",
+          icon: Sunny,
+        },
+        {
+          index: "/heavyMetalEnterprise",
+          label: "atmosDeposition.heavyMetalEnterpriseTitle",
+          icon: Coin,
+        },
+        {
+          index: "/airSampleData",
+          label: "atmosDeposition.airSampleDataTitle",
+          icon: Sunny,
+        },
+        {
+          index: "/airInputFlux",
+          label: "atmosDeposition.airInputFluxTitle",
+          icon: Coin,
+        },
+      ],
+    },
+    {
+      index: "/totalInputFlux",
+      label: "totalInputFlux.Title",
+      icon: Watermelon,
+      children: [
+        {
+          index: "/totalInputFluxDesc",
+          label: "输入总通量说明",
+          icon: Watermelon,
+        },
+        {
+          index: "/totalInputFlux",
+          label: "输入总通量结果",
+          icon: List,
+        },
+      ],
+    },
+  ],
+
+  hmInFlux: [
+    {
+      index: "grainRemoval",
+      label: "grainRemoval.Title",
+      icon: WindPower,
+      children: [
+        {
+          index: "/samplingDesc1",
+          label: "grainRemoval.samplingDesc1",
+          icon: Watermelon,
+        },
+        {
+          index: "/grainRemovalInputFlux",
+          label: "grainRemoval.grainRemovalInputFlux",
+          icon: List,
+        },
+      ],
+    },
+    {
+      index: "strawRemoval",
+      label: "strawRemoval.Title",
+      icon: WindPower,
+      children: [
+        {
+          index: "/samplingDesc2",
+          label: "strawRemoval.samplingDesc2",
+          icon: Watermelon,
+        },
+        {
+          index: "/strawRemovalInputFlux",
+          label: "strawRemoval.strawRemovalInputFlux",
+          icon: List,
+        },
+      ],
+    },
+    {
+      index: "subsurfaceLeakage",
+      label: "subsurfaceLeakage.Title",
+      icon: WindPower,
+      children: [
+        {
+          index: "/samplingDesc3",
+          label: "subsurfaceLeakage.samplingDesc3",
+          icon: Watermelon,
+        },
+        {
+          index: "/subsurfaceLeakageInputFlux",
+          label: "subsurfaceLeakage.subsurfaceLeakageInputFlux",
+          icon: List,
+        },
+      ],
+    },
+    {
+      index: "surfaceRunoff",
+      label: "surfaceRunoff.Title",
+      icon: WindPower,
+      children: [
+        {
+          index: "/samplingDesc4",
+          label: "surfaceRunoff.samplingDesc4",
+          icon: Watermelon,
+        },
+        {
+          index: "/surfaceRunoffInputFlux",
+          label: "surfaceRunoff.surfaceRunoffInputFlux",
+          icon: List,
+        },
+      ],
+    },
+    {
+      index: "totalOutputFlux",
+      label: "totalOutputFlux.Title",
+      icon: WindPower,
+      children: [
+        {
+          index: "/totalOutputFluxDesc",
+          label: "输入总通量说明",
+          icon: Watermelon,
+        },
+        {
+          index: "/totalOutputFlux",
+          label: "输出总通量结果",
+          icon: List,
+        },
+      ],
+    },
+  ],
+
+  cadmiumPrediction: [
+    {
+      index: "/netFlux",
+      label: "netFlux.Title",
+      icon: PieChart,
+    },
+    {
+      index: "/currentYearConcentration",
+      label: "currentYearConcentration.Title",
+      icon: PieChart,
+    },
+    {
+      index: "/EffectiveCadmiumPrediction",
+      label: "EffectiveCadmiumPrediction.Title",
+      icon: PieChart,
+    },
+    {
+      index: "/CropCadmiumPrediction",
+      label: "CropCadmiumPrediction.Title",
+      icon: PieChart,
+    },
+  ],
+
+  cropRiskAssessment: [
+    {
+      index: "/cropRiskAssessment",
+      label: "cropRiskAssessment.Title",
+      icon: Compass,
+    },
+  ],
+
+  farmlandQualityAssessment: [
+    {
+      index: "/farmlandQualityAssessment",
+      label: "farmlandQualityAssessment.Title",
+      icon: DataLine,
+    },
+  ],
+
+  soilAcidificationPrediction: [
+    {
+      index: "acidModel",
+      label: "acidModel.Title",
+      icon: MagicStick,
+      children: [
+        {
+          index: "/Calculation",
+          label: "acidModel.CalculationTitle",
+          icon: Sunny,
+        },
+        {
+          index: "/SoilAcidReductionIterativeEvolution",
+          label: "acidModel.SoilAcidReductionIterativeEvolutionTitle",
+          icon: Coin,
+        },
+      ],
+    },
+    {
+      index: "neutralizationModel",
+      label: "neutralizationModel.Title",
+      icon: MagicStick,
+      children: [
+        {
+          index: "/AcidNeutralizationModel",
+          label: "neutralizationModel.AcidNeutralizationModelTitle",
+          icon: Sunny,
+        },
+        {
+          index: "/SoilAcidificationIterativeEvolution",
+          label: "neutralizationModel.SoilAcidificationIterativeEvolutionTitle",
+          icon: Coin,
+        },
+      ],
+    },
+  ],
+
+  scenarioSimulation: [
+    {
+      index: "/TraditionalFarmingRisk",
+      label: "TraditionalFarmingRisk.Title",
+      icon: MenuIcon,
+    },
+    {
+      index: "/HeavyMetalCadmiumControl",
+      label: "HeavyMetalCadmiumControl.Title",
+      icon: MenuIcon,
+    },
+    {
+      index: "/SoilAcidificationControl",
+      label: "SoilAcidificationControl.Title",
+      icon: MenuIcon,
+    },
+  ],
 
+  dataStatistics: [
+    {
+      index: "/DetectionStatistics",
+      label: "DetectionStatistics.Title",
+      icon: List,
+    },
+    {
+      index: "/FarmlandPollutionStatistics",
+      label: "FarmlandPollutionStatistics.Title",
+      icon: List,
+    },
+    {
+      index: "/LandClutivatesStatistics",
+      label: "LandClutivatesStatistics.Title", //<!--i18n:LandClutivatesStatistics.Title-->作物风险评估统计
+      icon: List,
+    },
+    {
+      index: "/SoilacidificationStatistics",
+      label: "SoilacidificationStatistics.Title", //<!--i18n:SoilacidificationStatistics.Title-->酸化预测数据统计
+      icon: List,
+    },
+  ],
+};

+ 3 - 2
src/components/layout/menuItems2.ts

@@ -122,6 +122,7 @@ export const tabMenuMap: Record<string, any[]> = {
       icon: Guide,
     },
   ],
+  
   modelManagement: [
     {
       index: "/Soil Cadmium Content Prediction Model Management",
@@ -157,6 +158,7 @@ export const tabMenuMap: Record<string, any[]> = {
         },
       ],
     },
+    
     {
       index: "/Rice Cadmium Pollution Risk Model Management",
       label: "水稻镉污染风险模型管理",
@@ -185,10 +187,9 @@ export const tabMenuMap: Record<string, any[]> = {
         },
       ],
     },
-    
   ],
   userManagement: [
     { index: "/UserManagement", label: "用户信息", icon: Avatar },
-    { index: "/UserRegistration", label: "普通用户", icon: User },
+    //{ index: "/UserRegistration", label: "普通用户", icon: User },
   ],
 };

+ 13 - 31
src/locales/en.json

@@ -1,46 +1,28 @@
-{
+{"role": {
+    "admin": "管理员",
+    "user": "用户"
+  },
   "login": {
-    "title": "Login",
-    "username": "Username",
-    "password": "Password",
+    "userTitle": "User Login",
     "loginButton": "Login",
-    "registerButton": "Register",
     "registerLink": "Don't have an account? Click to register",
-    "userLoginTitle": "User Login",
-    "adminLoginTitle": "Admin Login",
-    "userRegisterTitle": "User Registration",
-    "adminRegisterTitle": "Admin Registration",
-    "successMessage": "Login successful",
-    "errorMessage": "Login failed",
     "loginSuccess": "Login successful",
-    "loginFailed": "Login failed, please check your username or password",
-    "userTypeMismatch": "User type mismatch",
-    "userTitle": "User Login",
-    "adminTitle": "Admin Login",
-    "switchToAdmin": "Switch to Admin",
-    "switchToUser": "Switch to User"
+    "loginFailed": "Login failed, please check your username or password"
   },
   "register": {
-    "title": "Register",
-    "username": "Username",
-    "password": "Password",
-    "confirmPassword": "Confirm Password",
-    "backToLoginButton": "Back to Login",
+    "title": "User Registration",
     "registerButton": "Register",
-    "passwordMismatch": "Passwords do not match",
-    "successMessage": "Registration successful, please log in",
-    "errorMessage": "Registration failed",
-    "registerFailed": "Registration failed"
+    "backToLoginButton": "Back to Login",
+    "registerSuccess": "Registration successful",
+    "registerFailed": "Registration failed",
+    "autoLoginPrompt": "Registration successful, redirecting to login page..."
   },
   "validation": {
     "usernameRequired": "Please enter your username",
     "passwordRequired": "Please enter your password",
-    "passwordLength": "Password length must be between 3 and 16 characters",
+    "passwordLength": "Password must be between 3 and 16 characters",
     "confirmPasswordRequired": "Please confirm your password",
-    "passwordMismatch": "Passwords do not match",
-    "requiredUsername": "Username is required",
-    "requiredPassword": "Password is required",
-    "requiredConfirmPassword": "Confirm password is required"
+    "passwordMismatch": "The two passwords you entered do not match"
   },
   "irrigationwater": {
     "Title": "irrigationwater",

+ 11 - 35
src/locales/zh.json

@@ -1,49 +1,25 @@
 {
-  "role": {
-    "admin": "管理员",
-    "user": "用户"
-  },
   "login": {
-    "title": "登录",
-    "username": "账号",
-    "password": "密码",
+    "userTitle": "用户登录",
     "loginButton": "登录",
-    "registerButton": "注册",
-    "registerLink": "还没有账号?点击注册",
-    "userLoginTitle": "普通用户登录",
-    "adminLoginTitle": "管理员登录",
-    "userRegisterTitle": "普通用户注册",
-    "adminRegisterTitle": "管理员注册",
-    "successMessage": "登录成功",
-    "errorMessage": "登录失败",
+    "registerLink": "没有账号?点击注册",
     "loginSuccess": "登录成功",
-    "loginFailed": "登录失败,请检查用户名或密码",
-    "userTypeMismatch": "用户类型不匹配",
-    "userTitle": "普通用户登录",
-    "adminTitle": "管理员登录",
-    "switchToAdmin": "切换到管理员",
-    "switchToUser": "切换到普通用户"
+    "loginFailed": "登录失败,请检查用户名或密码"
   },
   "register": {
-    "title": "注册",
-    "username": "账号",
-    "password": "密码",
-    "confirmPassword": "确认密码",
-    "backToLoginButton": "返回登录",
+    "title": "用户注册",
     "registerButton": "注册",
-    "passwordMismatch": "两次输入的密码不一致",
-    "successMessage": "注册成功,请登录",
-    "registerFailed": "注册失败"
+    "backToLoginButton": "返回登录",
+    "registerSuccess": "注册成功",
+    "registerFailed": "注册失败",
+    "autoLoginPrompt": "注册成功,正在跳转到登录页面..."
   },
   "validation": {
-    "usernameRequired": "请输入账号",
+    "usernameRequired": "请输入用户名",
     "passwordRequired": "请输入密码",
-    "passwordLength": "密码长度应在 3 到 16 个字符之间",
+    "passwordLength": "密码长度为 3 到 16 个字符",
     "confirmPasswordRequired": "请确认密码",
-    "passwordMismatch": "两次输入的密码不一致",
-    "requiredUsername": "账号为必填项",
-    "requiredPassword": "密码为必填项",
-    "requiredConfirmPassword": "确认密码为必填项"
+    "passwordMismatch": "两次输入的密码不一致"
   },
   "irrigationwater": {
     "Title": "灌溉水",

+ 43 - 36
src/router/index.ts

@@ -4,16 +4,20 @@ import { useTokenStore } from "@/stores/mytoken"; // 确保正确导入 useToken
 
 // 定义路由配置
 const routes = [
-  { path: "/login", name: "login", component: () => import("@/views/login/loginView.vue") },
+  {
+    path: "/login",
+    name: "login",
+    component: () => import("@/views/login/loginView.vue"),
+  },
   {
     path: "/",
     component: AppLayout,
     meta: { requiresAuth: true },
-
     children: [
       {
+        // 捕获所有未匹配的路径,重定向到 404
         path: "/:catchAll(.*)",
-        redirect: "/404", // 确保重定向到有效页面
+        redirect: "/404",
       },
       {
         path: "select-city", // remove leading slash
@@ -94,9 +98,7 @@ const routes = [
         path: "csSampleData",
         name: "csSampleData",
         component: () =>
-          import(
-            "@/views/User/HmOutFlux/irrigationWater/crossSection.vue"
-          ),
+          import("@/views/User/HmOutFlux/irrigationWater/crossSection.vue"),
         meta: { title: "断面采样数据" },
       },
       {
@@ -566,7 +568,7 @@ const routes = [
           import(
             "@/views/Admin/dataManagement/Soil Acidification and Acid Reduction Data Management/soilAcidReductionData.vue"
           ),
-        meta: { title: "土壤酸采样数据" },
+        meta: { title: "土壤酸采样数据" },
       },
 
       {
@@ -672,49 +674,54 @@ const routes = [
   },
 ];
 
+// ========== 2. ✅ 创建 router 实例 ==========
 const router = createRouter({
   history: createWebHistory(),
   routes,
 });
 
+// ========== 3. ✅ 添加全局前置守卫 ==========
+// src/router/index.ts
 router.beforeEach((to, from, next) => {
-  const tokenStore = useTokenStore();
-  const user = tokenStore.userInfo;
-  const isSameRoute = (path: string) => to.fullPath === path;
+  const store = useTokenStore();
 
-  // 已登录用户访问 login 页面
-  if (to.name === "login" && user?.userId) {
-    return next("/select-city");
+  // 1. 未登录:只能访问登录页
+  if (!store.isLoggedIn) {
+    if (to.path === "/login") {
+      next(); // 放行登录页
+    } else {
+      next("/login"); // 其他页面重定向到登录
+    }
+    return;
   }
 
-  // 需要登录才能访问的页面
-  if (to.matched.some(r => r.meta.requiresAuth)) {
-    if (!user?.userId) {
-      return next({ name: "login" });
-    }
+  // 2. 已登录:检查权限
+  const { userType } = store;
 
-    // 管理员权限页面
-    if (to.matched.some(r => r.meta.requiresAdmin)) {
-      if (user.loginType !== "admin") {
-        // 普通用户访问管理员页面,提示并重定向
-        // Replace with your notification library, e.g. for naive-ui:
-        // import { useMessage } from 'naive-ui' at the top of the file if not already imported
-        // const message = useMessage();
-        // message.error("登录失败:用户类型不匹配");
+  // ✅ 定义路由权限映射
+  const routeRoles: Record<string, "user" | "admin"> = {
+    "/soilAcidReductionData": "admin",
+    "/samplingMethodDevice1": "user",
+    // 其他需要权限的页面...
+  };
 
-        // If using Element Plus:
-        // import { ElMessage } from 'element-plus' at the top of the file
-        // ElMessage.error("登录失败:用户类型不匹配");
+  const requiredRole = routeRoles[to.path];
 
-        // Example using Element Plus:
-        // ElMessage.error("登录失败:用户类型不匹配");
-        if (!isSameRoute("/select-city")) return next({ name: "selectCityAndCounty" });
-        return next();
-      }
-    }
+  if (requiredRole && userType !== requiredRole) {
+    console.warn(`[权限拒绝] 用户类型 '${userType}' 无法访问: ${to.path}`);
+    // ❌ 避免回到 /,否则会再次触发守卫,无限跳转
+    // next("/"); // 错误!
+
+    // ✅ 正确做法:跳转到该用户类型的首页
+    next(
+      userType === "admin" ? "/soilAcidReductionData" : "/samplingMethodDevice1"
+    );
+    return;
   }
 
+  // 3. 放行
   next();
 });
 
-export default router;
+// ========== 4. ✅ 导出 router 实例 ==========
+export default router;

+ 1 - 1
src/stores/mytoken.ts

@@ -36,4 +36,4 @@ export const useTokenStore = defineStore("mytoken", {
     userName: (state) => state.userInfo?.name || "",
     userType: (state) => state.userInfo?.loginType || "user",
   },
-});
+});

+ 8 - 15
src/utils/request.ts

@@ -23,7 +23,7 @@ export const api5000: CustomAxiosInstance = axios.create({
     ? 'http://localhost:5000' 
     : 'https://www.soilgd.com:5000',
   timeout: 100000,
-  withCredentials: false  // 关键修改:除非需要cookie,否则设为false
+  withCredentials: true  // 关键修改:除非需要cookie,否则设为false
 });
 
 export const api8000: CustomAxiosInstance = axios.create({
@@ -55,28 +55,24 @@ function isGeoJSONResponse(response: AxiosResponse): boolean {
 }
 
 const setupInterceptors = (instance: CustomAxiosInstance) => {
-  // 请求拦截器
+  // 请求拦截器 (保持不变)
   instance.interceptors.request.use(
     (config: InternalAxiosRequestConfig) => {
       const token = localStorage.getItem('token');
       if (token) {
-        // 使用新的headers API
         if (!config.headers) {
           config.headers = new axios.AxiosHeaders();
         }
         config.headers.set('Authorization', `Bearer ${token}`);
       }
       
-      // 为GeoJSON请求设置Accept头
       if (config.url?.match(/\/geojson|\/vector/i)) {
         if (!config.headers) {
           config.headers = new axios.AxiosHeaders();
         }
         config.headers.set('Accept', 'application/geo+json, application/json');
-        
-        // 为GeoJSON请求设置更长的超时时间
         if (!config.timeout || config.timeout < 180000) {
-          config.timeout = 180000; // 3分钟
+          config.timeout = 180000;
         }
       }
       
@@ -85,10 +81,9 @@ const setupInterceptors = (instance: CustomAxiosInstance) => {
     (error: AxiosError) => Promise.reject(error)
   );
   
-  // 响应拦截器
+  // 响应拦截器 (修改部分)
   instance.interceptors.response.use(
     (response: AxiosResponse) => {
-      // 根据响应类型和内容类型决定如何处理
       const contentType = response.headers['content-type'] || '';
       const isBlob = response.config.responseType === 'blob';
       const isImage = contentType.includes('image/');
@@ -103,30 +98,28 @@ const setupInterceptors = (instance: CustomAxiosInstance) => {
         isJSON
       });
       
-      // 1. 处理二进制响应(图片/文件下载)
       if (isBlob || isImage) {
         return response;
       }
       
-      // 2. 处理GeoJSON响应(新增部分)
       if (isGeoJSONResponse(response)) {
         console.log('检测到GeoJSON响应,返回完整响应对象');
         return response;
       }
       
-      // 3. 处理普通JSON响应
       if (isJSON) {
         return response;
       }
       
-      // 4. 其他类型响应
       return response;
     },
     (error: AxiosError) => {
+      // --- 修改开始 ---
       if (error.response?.status === 401) {
+        console.warn('401 Unauthorized intercepted by Axios. Token removed.');
         localStorage.removeItem('token');
-        window.location.href = '/login';
-      }
+        }
+      // --- 修改结束 ---
       return Promise.reject(error);
     }
   );

+ 0 - 1
src/views/AboutView.vue

@@ -13,4 +13,3 @@
   }
 }
 </style>
-}

Разлика између датотеке није приказан због своје велике величине
+ 549 - 281
src/views/Admin/dataManagement/HeavyMetalInputFluxManager/agriProductInputFluxData.vue


Разлика између датотеке није приказан због своје велике величине
+ 528 - 273
src/views/Admin/dataManagement/HeavyMetalInputFluxManager/atmosphericInputFluxData.vue


Разлика између датотеке није приказан због своје велике величине
+ 526 - 286
src/views/Admin/dataManagement/HeavyMetalInputFluxManager/atmosphericSampleData.vue


Разлика између датотеке није приказан због своје велике величине
+ 588 - 315
src/views/Admin/dataManagement/HeavyMetalInputFluxManager/crossSectionSampleData.vue


Разлика између датотеке није приказан због своје велике величине
+ 557 - 282
src/views/Admin/dataManagement/HeavyMetalInputFluxManager/heavyMetalEnterpriseData.vue


Разлика између датотеке није приказан због своје велике величине
+ 689 - 319
src/views/Admin/dataManagement/HeavyMetalInputFluxManager/irrigationWaterInputFluxData.vue


Разлика између датотеке није приказан због своје велике величине
+ 535 - 277
src/views/Admin/dataManagement/HeavyMetalInputFluxManager/irrigationWaterSampleData.vue


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

@@ -1,137 +1,237 @@
 <template>
   <div class="app-container">
-    <!-- 操作按钮区 -->
-    <div class="button-group">
-      <el-button
-        :icon="Plus"
-        type="primary"
-        @click="openDialog('add')"
-        class="custom-button add-button"
-        >新增记录</el-button
-      >
-      <el-button
-        :icon="Download"
-        type="primary"
-        @click="downloadTemplateAction"
-        class="custom-button download-button"
-        >下载模板</el-button
-      >
-      <el-button
-        :icon="Download"
-        type="primary"
-        @click="exportDataAction"
-        class="custom-button export-button"
-        >导出数据</el-button
-      >
-      <el-upload :before-upload="importDataAction" accept=".xlsx, .csv">
+    <div class="button-section">
+      <div class="button-group">
         <el-button
-          :icon="Upload"
+          :icon="Plus"
           type="primary"
-          class="custom-button import-button"
-          >导入数据</el-button
+          @click="openDialog('add')"
+          class="custom-button add-button"
         >
-      </el-upload>
-    </div>
+          新增记录
+        </el-button>
 
-    <!-- 数据表格 -->
-    <el-table
-      :data="pagedTableDataWithIndex"
-      fit
-      style="width: 100%"
-      @row-click="handleRowClick"
-      highlight-current-row
-      class="custom-table"
-      v-loading="loading"
-      table-layout="auto"
-    >
-      <el-table-column
-        key="displayIndex"
-        prop="displayIndex"
-        label="序号"
-        width="80"
-        align="center"
-      ></el-table-column>
-      <el-table-column
-        v-for="col in columns.filter((col) => col.key !== 'id')"
-        :key="col.key"
-        :prop="col.dataKey"
-        :label="col.title"
-        :min-width="col.title.length * 20 + 40"
-        :formatter="formatNumber"
-        align="center"
-        header-align="center"
-      ></el-table-column>
-      <el-table-column label="操作" width="120" align="center">
-        <template #default="scope">
-          <span class="action-buttons">
-            <el-tooltip
-              class="item"
-              effect="dark"
-              content="编辑"
-              placement="top"
-            >
-              <el-button
-                circle
-                :icon="EditPen"
-                @click.stop="openDialog('edit', scope.row)"
-                class="action-button edit-button"
-              ></el-button>
-            </el-tooltip>
-            <el-tooltip
-              class="item"
-              effect="dark"
-              content="删除"
-              placement="top"
+        <div class="button-group-right">
+          <el-upload
+            :auto-upload="false"
+            :on-change="handleFileSelect"
+            :show-file-list="false"
+            accept=".xlsx, .csv"
+            class="import-upload"
+          >
+            <el-button
+              :icon="Upload"
+              type="primary"
+              class="custom-button import-button"
             >
-              <el-button
-                circle
-                :icon="DeleteFilled"
-                @click.stop="deleteItem(scope.row)"
-                class="action-button delete-button"
-              ></el-button>
-            </el-tooltip>
-          </span>
-        </template>
-      </el-table-column>
-    </el-table>
-
-    <!-- 分页控制 -->
-    <PaginationComponent
-      :total="tableData.length"
-      :currentPage="currentPage4"
-      :pageSize="pageSize4"
-      @update:currentPage="currentPage4 = $event"
-      @update:pageSize="pageSize4 = $event"
-      @size-change="handleSizeChange"
-      @current-change="handleCurrentChange"
-      class="pagination-container"
-    />
-
-    <!-- 新增/编辑对话框 -->
-    <el-dialog :title="dialogTitle" v-model="dialogVisible" width="50%">
-      <el-form :model="formData" label-width="150px">
-        <el-form-item
-          v-for="col in editableColumns"
+              导入降酸数据
+            </el-button>
+          </el-upload>
+
+          <el-button
+            :icon="Download"
+            type="primary"
+            @click="downloadTemplateAction"
+            class="custom-button download-button"
+          >
+            下载模板
+          </el-button>
+
+          <el-button
+            :icon="Download"
+            type="primary"
+            @click="exportDataAction"
+            class="custom-button export-button"
+          >
+            导出数据
+          </el-button>
+
+          <el-button
+            :icon="Refresh"
+            type="primary"
+            @click="refreshData"
+            class="custom-button refresh-button"
+          >
+            刷新
+          </el-button>
+        </div>
+      </div>
+    </div>
+
+    <div class="table-section">
+      <el-table
+        :data="pagedTableDataWithIndex"
+        height="615"
+        fit
+        style="width: 100%"
+        @row-click="handleRowClick"
+        highlight-current-row
+        class="custom-table"
+        v-loading="loading"
+        table-layout="auto"
+      >
+        <el-table-column
+          key="displayIndex"
+          prop="displayIndex"
+          label="序号"
+          width="80"
+          align="center"
+          fixed
+          :header-class-name="'fixed-column-header'"
+        ></el-table-column>
+
+        <el-table-column
+          v-for="col in filteredColumns"
           :key="col.key"
+          :prop="col.dataKey"
           :label="col.title"
+          :width="col.width"
+          :formatter="formatTableValue"
+          align="center"
+          :show-overflow-tooltip="true"
+        ></el-table-column>
+
+        <el-table-column
+          label="操作"
+          width="120"
+          align="center"
+          fixed="right"
+          :header-class-name="'fixed-column-header'"
         >
-          <el-input
-            v-model="formData[col.dataKey]"
-            :type="col.inputType || 'text'"
-            class="custom-input"
-          ></el-input>
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <el-button @click="dialogVisible = false" class="custom-cancel-button"
-          >取消</el-button
-        >
-        <el-button
-          type="primary"
-          @click="submitForm"
-          class="custom-submit-button"
-          >{{ dialogSubmitButtonText }}</el-button
+          <template #default="scope">
+            <span class="action-buttons">
+              <el-tooltip
+                class="item"
+                effect="dark"
+                content="编辑"
+                placement="top"
+              >
+                <el-button
+                  circle
+                  :icon="EditPen"
+                  @click.stop="openDialog('edit', scope.row)"
+                  class="action-button edit-button"
+                ></el-button>
+              </el-tooltip>
+              <el-tooltip
+                class="item"
+                effect="dark"
+                content="删除"
+                placement="top"
+              >
+                <el-button
+                  circle
+                  :icon="DeleteFilled"
+                  @click.stop="deleteItem(scope.row.id)"
+                  class="action-button delete-button"
+                >
+                </el-button>
+              </el-tooltip>
+            </span>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <div class="pagination-wrapper">
+        <PaginationComponent
+          :total="tableData.length"
+          :currentPage="currentPage4"
+          :pageSize="pageSize4"
+          @update:currentPage="currentPage4 = $event"
+          @update:pageSize="pageSize4 = $event"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange"
+          class="pagination-container"
+        />
+      </div>
+    </div>
+
+    <el-dialog
+      :title="dialogTitle"
+      v-model="dialogVisible"
+      width="60%"
+      :custom-class="['custom-dialog']"
+      :close-on-click-modal="false"
+      center
+    >
+      <div class="dialog-header">
+        <h2>{{ dialogTitle }}</h2>
+        <div class="header-decoration"></div>
+      </div>
+      <el-scrollbar max-height="calc(90vh - 300px)" style="width: 90%">
+        <el-form
+          ref="formRef"
+          :model="formData"
+          label-position="top"
+          :rules="formRules"
+          class="custom-dialog-form"
         >
+          <el-form-item
+            v-for="col in editableColumns"
+            :key="col.key"
+            :label="col.title"
+            :prop="col.dataKey"
+            class="custom-form-item"
+          >
+            <el-input
+              v-model="formData[col.dataKey]"
+              :type="col.inputType || 'text'"
+              :placeholder="`请输入${col.title}`"
+            />
+          </el-form-item>
+        </el-form>
+      </el-scrollbar>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="handleCancel" class="custom-cancel-button"
+            >取消</el-button
+          >
+          <el-button
+            type="primary"
+            @click="submitForm"
+            class="custom-submit-button"
+            >{{ dialogSubmitButtonText }}</el-button
+          >
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 详情弹窗 -->
+    <el-dialog
+      title="导入成功详情"
+      v-model="detailsVisible"
+      width="50%"
+      :close-on-click-modal="false"
+      center
+    >
+      <div class="details-content" v-html="detailsContent"></div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="detailsVisible = false" class="custom-cancel-button">关闭</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 空值错误提示弹窗 -->
+    <el-dialog
+      title="空值错误"
+      v-model="emptyCellDialogVisible"
+      width="40%"
+      :close-on-click-modal="false"
+      center
+    >
+      <div class="error-content">
+        <p>以下单元格存在空值,请检查:</p>
+        <ul>
+          <li v-for="(error, index) in emptyCellErrors" :key="index">
+            第{{ error.row + 1 }}行,{{ error.column }}列
+          </li>
+        </ul>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="emptyCellDialogVisible = false" class="custom-cancel-button">关闭</el-button>
+        </div>
       </template>
     </el-dialog>
   </div>
@@ -139,81 +239,189 @@
 
 <script lang="ts" setup>
 import { ref, reactive, computed, onMounted } from "vue";
+import * as XLSX from "xlsx";
+
 import {
   DeleteFilled,
   Download,
   Upload,
   Plus,
   EditPen,
+  Refresh,
 } from "@element-plus/icons-vue";
-import { ElMessage } from "element-plus";
+import {
+  ElMessage,
+  ElForm,
+  ElMessageBox,
+  ElNotification,
+  FormRules,
+  FormItemRule,
+} from "element-plus";
 import {
   table,
   updateItem,
   addItem,
   deleteItemApi,
-  downloadTemplate,
   exportData,
   importData,
-} from "@/API/menus";
+  downloadTemplate,
+} from "@/API/admin";
 import PaginationComponent from "@/components/PaginationComponent.vue";
 
+interface EmptyCellError {
+  row: number; // Excel 行号
+  column: string; // 缺失列名
+}
 interface Column {
   key: string;
   dataKey: string;
   title: string;
   width: number;
   inputType?: string;
+  step?: string;
+  precision?: number;
+  options?: Array<{ label: string; value: string | number }>;
 }
 
+// 表格列定义
 const columns: Column[] = [
   { key: "id", dataKey: "id", title: "ID", width: 100 },
-  { key: "OM", dataKey: "OM", title: "有机质含量", width: 150 },
-  { key: "CL", dataKey: "CL", title: "土壤粘粒", width: 150 },
-  { key: "CEC", dataKey: "CEC", title: "阳离子交换量", width: 150 },
-  { key: "H_plus", dataKey: "H_plus", title: "交换性氢", width: 150 },
-  { key: "N", dataKey: "N", title: "水解氮", width: 150 },
-  { key: "Al3_plus", dataKey: "Al3_plus", title: "交换性铝", width: 150 },
-  { key: "Delta_pH", dataKey: "Delta_pH", title: "ΔpH", width: 170 },
+  {
+    key: "OM",
+    dataKey: "OM",
+    title: "有机质含量",
+    width: 130,
+  },
+  {
+    key: "CL",
+    dataKey: "CL",
+    title: "土壤粘粒",
+    width: 130,
+  },
+  {
+    key: "CEC",
+    dataKey: "CEC",
+    title: "阳离子交换量",
+    width: 130,
+  },
+  {
+    key: "H_plus",
+    dataKey: "H_plus",
+    title: "交换性氢",
+    width: 130,
+  },
+  {
+    key: "N",
+    dataKey: "N",
+    title: "水解氮",
+    width: 130,
+  },
+  {
+    key: "Al3_plus",
+    dataKey: "Al3_plus",
+    title: "交换性铝",
+    width: 130,
+  },
+  {
+    key: "Delta_pH",
+    dataKey: "Delta_pH",
+    title: "ΔpH",
+    width: 130,
+  },
 ];
-
 const editableColumns = columns.filter((col) => col.key !== "id");
 
+type TableName = "current_reduce" | "current_reflux";
+const currentTableName: TableName = "current_reflux";
+
+const emptyCellErrors = ref<EmptyCellError[]>([]);
+const emptyCellDialogVisible = ref(false);
+
+const showEmptyCellErrors = (errors: EmptyCellError[]) => {
+  emptyCellErrors.value = errors;
+  emptyCellDialogVisible.value = true;
+};
+
 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 loading = ref(false);
+const formRef = ref<InstanceType<typeof ElForm> | null>(null);
+
+const filteredColumns = computed(() =>
+  columns.filter((col) => col.key !== "id")
+);
 
 const currentPage4 = ref(1);
 const pageSize4 = ref(10);
 
-const pagedTableData = computed(() => {
-  const start = (currentPage4.value - 1) * pageSize4.value;
-  const end = start + pageSize4.value;
-  return tableData.value.slice(start, end);
+const dialogVisible = ref(false);
+const formData = reactive<any>({});
+const dialogMode = ref<"add" | "edit">("add");
+
+// 定义支持的列名映射,包括中英文
+const REQUIRED_COLUMNS = {
+  "OM": "有机质含量",
+  "CL": "土壤粘粒",
+  "CEC": "阳离子交换量",
+  "H_plus": "交换性氢",
+  "N": "水解氮",
+  "Al3_plus": "交换性铝",
+  "Delta_pH": "ΔpH"
+};
+
+// 反向映射,用于将中文列名转换为英文列名
+const COLUMN_NAME_MAPPING: Record<string, string> = {};
+Object.entries(REQUIRED_COLUMNS).forEach(([enKey, zhValue]) => {
+  COLUMN_NAME_MAPPING[enKey] = enKey; // 英文映射
+  COLUMN_NAME_MAPPING[zhValue] = enKey; // 中文映射
 });
 
+const normalizeValue = (val: any): any => {
+  if (val === undefined) {
+    return "";
+  }
+  if (typeof val === "number" && isNaN(val)) {
+    return "";
+  }
+  if (typeof val === "string" && val.toLowerCase() === "nan") {
+    return "";
+  }
+  return val;
+};
+
 const pagedTableDataWithIndex = computed(() => {
-  return pagedTableData.value.map((item, index) => ({
-    ...item,
-    displayIndex: (currentPage4.value - 1) * pageSize4.value + index + 1,
-  }));
+  const start = (currentPage4.value - 1) * pageSize4.value;
+  const end = start + pageSize4.value;
+  return tableData.value
+    .slice(start, end)
+    .map((row: { [s: string]: unknown } | ArrayLike<unknown>, idx: number) => {
+      const processedRow = Object.entries(row).reduce((acc, [key, val]) => {
+        acc[key] = normalizeValue(val);
+        return acc;
+      }, {} as any);
+      return {
+        ...processedRow,
+        displayIndex: start + idx + 1,
+      };
+    });
 });
 
-const loading = ref(false);
-const currentTableName = "current_reflux";
-
 const fetchTable = async () => {
-  console.log("正在获取表格数据...");
   try {
     loading.value = true;
-    const response = await table({ table: currentTableName });
-    console.log("获取到的数据:", response);
-    tableData.value = response.data.rows;
+    const response: any = await table({ table: currentTableName });
+
+    const rawData = Array.isArray(response.data) ? response.data : [];
+
+    tableData.value = rawData.map((row: any) =>
+      Object.entries(row).reduce((acc: any, [key, val]) => {
+        acc[key] = normalizeValue(val);
+        return acc;
+      }, {})
+    );
   } catch (error) {
     console.error("获取数据时出错:", error);
-    ElMessage.error("获取数据失败,请检查网络连接或服务器状态");
+    showMessage("获取数据失败,请检查网络连接或服务器状态", "error");
   } finally {
     loading.value = false;
   }
@@ -225,23 +433,35 @@ onMounted(() => {
 
 const handleRowClick = (row: any) => {
   selectedRow.value = row;
-  Object.assign(formData, row);
+  Object.assign(
+    formData,
+    Object.entries(row).reduce((acc: any, [key, val]) => {
+      acc[key] = normalizeValue(val);
+      return acc;
+    }, {})
+  );
 };
 
 const openDialog = (mode: "add" | "edit", row?: any) => {
-  console.log(`${mode === "add" ? "打开新增记录" : "打开编辑记录"}对话框`);
+  dialogMode.value = mode;
+  dialogVisible.value = true;
+
   if (mode === "add") {
     selectedRow.value = null;
     editableColumns.forEach((col) => {
-      formData[col.dataKey] = "";
+      const dataKey = col.dataKey;
+      formData[dataKey] = "";
     });
   } else if (row) {
-    console.log("编辑记录:", row);
     selectedRow.value = row;
-    Object.assign(formData, row);
+    Object.assign(
+      formData,
+      Object.entries(row).reduce((acc: any, [key, val]) => {
+        acc[key] = normalizeValue(val);
+        return acc;
+      }, {})
+    );
   }
-  dialogMode.value = mode;
-  dialogVisible.value = true;
 };
 
 function prepareFormData(
@@ -252,301 +472,1017 @@ function prepareFormData(
   for (const key in formData) {
     if (!excludeKeys.includes(key)) {
       let value = formData[key];
-      if (typeof value === "string" && value.startsWith("displayIndex-")) {
-        value = value.replace("displayIndex-", "");
+      if (value === "") {
+        result[key] = undefined;
+      } else {
+        result[key] = value;
       }
-      result[key] = value;
     }
   }
   return result;
 }
 
+const formRules = computed<FormRules>(() => {
+  const rules: FormRules = {};
+  editableColumns.forEach((col) => {
+    const fieldKey = col.dataKey;
+
+    const fieldRules: FormItemRule[] = [];
+
+    fieldRules.push({
+      required: true,
+      message: `请输入${col.title}`,
+      trigger: "blur",
+    });
+
+    rules[fieldKey] = fieldRules;
+  });
+  return rules;
+});
+
 const submitForm = async () => {
-  console.log("开始提交表单...");
+  if (!formRef.value) return;
+
   try {
-    const isValid = validateFormData(formData);
+    const isValid = await formRef.value.validate();
     if (!isValid) {
-      console.error("表单验证失败,请检查输入的数据");
-      alert("请检查输入的数据");
+      console.log("表单验证未通过");
+      showMessage("表单验证未通过,请检查输入", "warning");
       return;
     }
-    const dataToSubmit = prepareFormData(formData);
-    if (!dataToSubmit.id && dialogMode.value !== "add") {
-      console.error("无法找到记录ID,请联系管理员");
-      alert("无法找到记录ID,请联系管理员");
-      return;
+  } catch (validationError: any) {
+    console.error("表单验证失败:", validationError);
+    let messageToShow = "表单验证失败,请检查输入";
+    if (
+      validationError &&
+      typeof validationError === "object" &&
+      !Array.isArray(validationError)
+    ) {
+      const firstFieldErrors = Object.values(validationError)[0];
+      if (
+        Array.isArray(firstFieldErrors) &&
+        firstFieldErrors.length > 0 &&
+        firstFieldErrors[0].message
+      ) {
+        messageToShow = firstFieldErrors[0].message;
+      }
+    } else if (Array.isArray(validationError)) {
+      const firstError = validationError[0];
+      if (firstError && firstError.message) {
+        messageToShow = `表单验证失败: ${firstError.message}`;
+      }
     }
-    let response;
+    showMessage(messageToShow, "error");
+    return;
+  }
+
+  try {
+    const dataToSubmit = prepareFormData(formData);
+    let response: { status?: number; data?: any };
+
     if (dialogMode.value === "add") {
-      console.log("正在添加新记录...");
-      response = await addItem({ table: currentTableName, item: dataToSubmit });
-    } else {
-      console.log("正在更新现有记录...");
-      response = await updateItem({
+      response = (await addItem({
         table: currentTableName,
         item: dataToSubmit,
-      });
+      })) as { status?: number; data?: any };
+
+      // 修改成功判断逻辑:只检查状态码,不再检查 data.success
+      if (response && (response.status === 200 || response.status === 201)) {
+        showMessage("添加成功", "success");
+        dialogVisible.value = false;
+        await fetchTable();
+      } else {
+        console.warn("addItem 响应结构异常或状态码非200/201:", response);
+        // 根据实际返回内容决定提示信息
+        if (response && response.data) {
+          const serverMsg =
+            response.data.detail ||
+            response.data.message ||
+            response.data.error ||
+            "未知错误";
+          showMessage(`添加操作完成,但服务器返回: ${serverMsg}`, "warning");
+        } else {
+          showMessage("添加操作完成,但响应数据异常。", "warning");
+        }
+      }
+    } else {
+      console.log("编辑模式 - selectedRow:", selectedRow.value);
+
+      if (!selectedRow.value) {
+        showMessage("未选中任何记录,请先选择要编辑的记录", "error");
+        return;
+      }
+
+      response = (await updateItem({
+        table: currentTableName,
+        id: selectedRow.value.id,
+        update_data: dataToSubmit,
+      })) as { status?: number; data?: any };
+
+      const index = tableData.value.findIndex(
+        (item: { id: any }) => item.id === selectedRow.value!.id
+      );
+      if (index > -1) {
+        tableData.value[index] = {
+          ...tableData.value[index],
+          ...dataToSubmit,
+        };
+        showMessage("修改成功", "success");
+      } else {
+        console.warn("本地未找到对应记录,重新获取数据");
+        await fetchTable();
+        showMessage("修改成功,数据已刷新", "success");
+      }
+
+      dialogVisible.value = false;
     }
-    console.log(
-      dialogMode.value === "add" ? "添加响应:" : "更新响应:",
-      response
-    );
-    dialogVisible.value = false;
-    fetchTable();
-    alert(dialogMode.value === "add" ? "添加成功" : "修改成功");
-  } catch (error) {
+  } catch (error: any) {
     console.error("提交表单时发生错误:", error);
-    let errorMessage = "未知错误";
-    if (error && typeof error === "object" && "response" in error) {
-      const response = (error as { response?: { data?: { message?: string } } })
-        .response;
-      if (
-        response &&
-        response.data &&
-        typeof response.data.message === "string"
-      ) {
-        errorMessage = response.data.message;
+
+    let errorMessage = "提交失败";
+
+    if (error.response) {
+      console.log("服务器响应错误:", error.response);
+      const status = error.response.status;
+      const data = error.response.data;
+
+      const errorDetail = data?.error || data?.detail || "";
+
+      if (typeof errorDetail === "string" && errorDetail.includes("已存在")) {
+        errorMessage = "提交失败:数据已存在,请勿重复添加。";
+      } else if (status === 409) {
+        errorMessage = "提交失败:数据已存在,请勿重复添加。";
+      } else if (status === 400) {
+        errorMessage = "提交失败:请求参数有误。";
+      } else if (status === 500) {
+        if (data && data.error) {
+          errorMessage = `服务器内部错误: ${data.error}`;
+        } else {
+          errorMessage = "提交失败: 服务器内部错误。";
+        }
+      } else {
+        errorMessage = `提交失败: 服务器错误 (${status})`;
+        if (data && data.error) {
+          errorMessage += ` - ${data.error}`;
+        } else if (data && data.message) {
+          errorMessage += ` - ${data.message}`;
+        }
       }
+    } else if (error.request) {
+      console.error("网络错误或请求无响应:", error.request);
+      errorMessage = "提交失败: 网络连接问题或服务器无响应。";
+    } else {
+      console.error("请求配置错误:", error.message);
+      errorMessage = `提交失败: ${error.message}`;
     }
-    console.error(`提交失败原因: ${errorMessage}`);
-    alert(`提交失败: ${errorMessage}`);
+
+    showMessage(errorMessage, "error");
   }
 };
 
-function validateFormData(data: { [x: string]: undefined }) {
-  for (let key in data) {
-    if (data[key] === "" || data[key] === undefined) {
-      return false;
-    }
-  }
-  return true;
-}
+const deleteItem = async (rowId: number) => {
+  const index = tableData.value.findIndex((item) => item.id === rowId);
 
-const deleteItem = async (row: any) => {
-  console.log("准备删除记录:", row);
-  if (!row) {
-    ElMessage.warning("请先选择一行记录");
+  if (index === -1) {
+    showMessage("无法找到要删除的记录或记录ID无效", "error");
+    console.error("DeleteItem: Row ID not found:", rowId);
     return;
   }
+
   try {
-    const condition = { id: row.id };
-    await deleteItemApi({ table: "current_reflux", condition });
-    const index = tableData.value.findIndex((item) => item.id === row.id);
-    if (index > -1) {
-      tableData.value.splice(index, 1);
+    await ElMessageBox.confirm(
+      `确定要删除该记录吗?此操作不可恢复。`,
+      "删除确认",
+      {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning",
+        customClass: "unified-confirm-dialog",
+      }
+    );
+
+    await deleteItemApi({
+      table: currentTableName,
+      id: rowId,
+    });
+
+    tableData.value.splice(index, 1);
+
+    showMessage("记录删除成功", "success");
+  } catch (error: any) {
+    if (error === "cancel") {
+      console.log("用户取消删除");
+      return;
     }
-    fetchTable();
-    console.log("记录删除成功");
-  } catch (error) {
     console.error("删除记录时发生错误:", error);
+    showMessage("删除失败,请重试", "error");
   }
 };
 
 const downloadTemplateAction = async () => {
   try {
-    await downloadTemplate("current_reflux");
+    await downloadTemplate(currentTableName);
+    showMessage("模板下载成功", "success");
   } catch (error) {
     console.error("下载模板时发生错误:", error);
+    showMessage("下载模板失败,请重试", "error");
   }
 };
 
 const exportDataAction = async () => {
   try {
-    await exportData("current_reflux");
+    await exportData(currentTableName);
+    showMessage("数据导出成功", "success");
   } catch (error) {
     console.error("导出数据时发生错误:", error);
+    showMessage("导出数据失败,请重试", "error");
   }
 };
 
+const handleFileSelect = async (uploadFile: any) => {
+  const file = uploadFile.raw;
+  if (!file) {
+    showMessage("请选择有效的 .xlsx 或 .csv 文件", "warning");
+    return;
+  }
+  await importDataAction(file);
+};
+
+interface AxiosError extends Error {
+  response?: {
+    data?: {
+      detail?: any;
+      message?: string;
+      [key: string]: any;
+    };
+    status?: number;
+    statusText?: string;
+    headers?: any;
+    config?: any;
+  };
+  request?: any;
+  config?: any;
+}
+
+interface ErrorResponse {
+  error?: string;
+  message?: string;
+  [key: string]: any;
+}
+
+// 定义详细信息展示函数的类型
+interface ShowDetailedInfo {
+  (title: string, content: string): void;
+}
+
+// 优化后的数据导入函数
 const importDataAction = async (file: File) => {
   try {
-    const response = await importData("reduce", file); // 传递 dataset_type 的值(如 'reduce')
-    if (response && response.data) {
-      const { total_data, new_data, duplicate_data, message } = response.data;
-      ElMessage({
-        message: `导入结果: ${message} 新增总数:${total_data}, 成功新增:${new_data}, 数据重复:${duplicate_data}`,
-        type: "success",
+    // 1. 读取文件
+    const dataArrayBuffer = await file.arrayBuffer();
+    const workbook = XLSX.read(dataArrayBuffer, { type: "array" });
+    const firstSheetName = workbook.SheetNames[0];
+    const worksheet = workbook.Sheets[firstSheetName];
+    const jsonData: any[] = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
+
+    // 2. 检查是否为空文件
+    const hasData = jsonData.some(row => 
+      Array.isArray(row) && row.some(cell => 
+        cell !== null && cell !== undefined && String(cell).trim() !== ""
+      )
+    );
+    if (!hasData) {
+      showMessage("❌ 文件为空", "error");
+      return;
+    }
+
+    // 3. 查找表头行
+    const headerRowIndex = jsonData.findIndex(row => 
+      Array.isArray(row) && row.some(cell => 
+        cell !== null && cell !== undefined && String(cell).trim() !== ""
+      )
+    );
+    if (headerRowIndex === -1) {
+      showMessage("❌ 未找到表头", "error");
+      return;
+    }
+
+    // 4. 获取列名并映射
+    const headerRow = jsonData[headerRowIndex];
+    const rawColumns = (headerRow as string[]).map(col => String(col).trim());
+    const columns = rawColumns.map(col => COLUMN_NAME_MAPPING[col] || col);
+
+    // 5. 检查必要列
+    const requiredCols = Object.keys(REQUIRED_COLUMNS);
+    const missingCols = requiredCols.filter(col => !columns.includes(col));
+    if (missingCols.length > 0) {
+      showMessage(`❌ 缺少列: ${missingCols.join(", ")}`, "error");
+      return;
+    }
+
+    // 6. 检查数据行
+    const dataRows = jsonData.slice(headerRowIndex + 1);
+    const validRows = dataRows.filter(row => 
+      requiredCols.some(col => {
+        const idx = columns.indexOf(col);
+        return idx !== -1 && row?.[idx] != null && String(row[idx]).trim() !== "";
+      })
+    );
+
+    if (validRows.length === 0) {
+      showMessage("⚠️ 无有效数据", "error");
+      return;
+    }
+
+    // 7. 检查必填列是否为空
+    const emptyErrors: string[] = [];
+    validRows.forEach((row, rowIdx) => {
+      requiredCols.forEach(col => {
+        const colIndex = columns.indexOf(col);
+        if (colIndex !== -1 && (row?.[colIndex] == null || String(row[colIndex]).trim() === "")) {
+          emptyErrors.push(`第${rowIdx + 2}行,${col}列为空`);
+        }
       });
-      fetchTable(); // 假设存在 fetchTable 方法刷新表格
+    });
+
+    if (emptyErrors.length > 0) {
+      showMessage(`❌ 必填列为空:\n${emptyErrors.slice(0, 5).join('\n')}`, "error");
+      return;
     }
-  } catch (error) {
-    let errorMessage = "数据导入失败";
-    if (error && typeof error === "object" && "response" in error) {
-      const response = (error as { response?: { data?: { message?: string } } })
-        .response;
-      if (response && response.data && typeof response.data.message === "string") {
-        errorMessage += `: ${response.data.message}`;
+
+    // 8. 转换数据格式
+    const transformedData = validRows.map(row => {
+      const newRow: any = {};
+      rawColumns.forEach((origCol, idx) => {
+        const engCol = COLUMN_NAME_MAPPING[origCol] || origCol;
+        if (requiredCols.includes(engCol)) {
+          newRow[engCol] = row?.[idx];
+        }
+      });
+      return newRow;
+    });
+
+    // 9. 创建新工作簿
+    const newWorkbook = XLSX.utils.book_new();
+    const newWorksheet = XLSX.utils.json_to_sheet(transformedData);
+    XLSX.utils.book_append_sheet(newWorkbook, newWorksheet, "Sheet1");
+    const outputArrayBuffer = XLSX.write(newWorkbook, { bookType: "xlsx", type: "array" });
+    const newFile = new File([outputArrayBuffer], file.name, {
+      type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+    });
+
+    // 10. 构造表单数据
+    const formData = new FormData();
+    formData.append("file", newFile);
+    formData.append("dataset_name", file.name.replace(/\.[^/.]+$/, "") || "Imported Dataset");
+    formData.append("dataset_description", `通过前端上传的 ${file.name} 数据集`);
+
+    // 11. 上传数据
+    const response: any = await importData(currentTableName, formData);
+
+    // 12. 处理响应
+    if ((response.status === 200 || response.status === 201) && response.data) {
+      const data = response.data;
+      const stats = data.data_stats || {};
+
+      // 主要成功信息
+      let mainMessage = `🎉 数据集上传成功!\n\n`;
+      mainMessage += `📌 数据集名称: ${data.dataset_name || data.dataset_id || "未知"}\n`;
+      mainMessage += `📌 唯一ID: ${data.dataset_id}\n`;
+
+      // 详细统计信息(可折叠或单独显示)
+      let detailMessage = `\n📊 数据统计详情:\n`;
+      detailMessage += `   • 原始行数: ${stats.original_count || 0}\n`;
+      detailMessage += `   • 文件内重复: ${stats.duplicates_in_file || 0}\n`;
+      detailMessage += `   • 与现有数据重复: ${stats.duplicates_with_existing || 0}\n`;
+      detailMessage += `   • 与测试集冲突: ${stats.test_overlap_count || 0}\n`;
+      detailMessage += `   • 最终入库: ${stats.final_count || 0}\n`;
+
+      // 附加信息
+      let additionalInfo = '';
+      if (data.training_triggered) {
+        additionalInfo += `\n🚀 自动训练已触发,任务ID: ${data.task_id}`;
+      }
+
+      if (data.message) {
+        additionalInfo += `\n\n💡 系统提示: ${data.message.replace("✅", "").replace("🎉", "").trim()}`;
       }
+
+      // 分步骤显示消息
+      showMessage(mainMessage, "success");
+      
+      // 在控制台显示详细信息
+      console.log(detailMessage + additionalInfo);
+      
+      // 显示详细信息弹窗
+      detailsContent.value = (mainMessage + detailMessage + additionalInfo).replace(/\n/g, '<br/>');
+      detailsVisible.value = true;
+      
+      await fetchTable(); // 刷新表格
+    } else {
+      const errorMsg = response.data?.error || "服务器未返回预期数据";
+      showMessage(`❌ 导入失败\n服务器返回错误:${errorMsg}\n状态码:${response.status}\n\n请检查网络或联系管理员。`, "error");
     }
-    ElMessage.error(errorMessage);
+  } catch (error: any) {
+    let errorMsg = "导入失败";
+    let errorDetail = "未知错误";
+
+    if (error.name === "AbortError") {
+      errorMsg = "请求已取消";
+      errorDetail = "网络请求被中断,请检查连接后重试。";
+    } else if (error.message.includes("Invalid data")) {
+      errorMsg = "文件数据格式错误";
+      errorDetail = "Excel 文件中包含不支持的数据类型(如公式、图片、合并单元格),请使用纯数据表格。";
+    } else if (error.message.includes("Unable to parse") || error.message.includes("Bad file")) {
+      errorMsg = "无法解析文件";
+      errorDetail = "文件可能已损坏或不是有效的 .xlsx 格式,请重新保存或另存为 Excel 文件。";
+    } else if (error.message.includes("Network Error")) {
+      errorMsg = "网络错误";
+      errorDetail = "无法连接到服务器,请检查网络连接。";
+    } else {
+      errorDetail = error.message || "请检查文件格式和内容是否符合要求。";
+    }
+
+    showMessage(`❌ ${errorMsg}\n${errorDetail}`, "error");
+
+    console.error("导入文件出错:", error);
   }
 };
 
-const handleFileChange = (event: Event) => {
-  const target = event.target as HTMLInputElement;
-  if (target.files && target.files.length > 0) {
-    importDataAction(target.files[0]);
-  }
+// 定义详细信息展示函数,带有明确的参数类型
+const showDetailedInfo: ShowDetailedInfo = (title: string, content: string) => {
+  // 这里可以实现一个模态框或侧边栏来展示详细内容
+  console.log(`=== ${title} ===`);
+  console.log(content);
+  console.log('====================================');
 };
 
-const handleSizeChange = (val: number) => {};
-const handleCurrentChange = (val: number) => {};
+const handleSizeChange = (val: number) => {
+  pageSize4.value = val;
+  currentPage4.value = 1;
+};
 
-const formatNumber = (row: any, column: any, cellValue: any) => {
-  if (typeof cellValue === "number") {
-    return cellValue.toFixed(3);
-  }
-  return cellValue;
+const handleCurrentChange = (val: number) => {
+  currentPage4.value = val;
+};
+
+const formatTableValue = (row: any, column: any, cellValue: any) => {
+  return normalizeValue(cellValue);
 };
 
 const dialogTitle = computed(() => {
-  return dialogMode.value === "add" ? "新增记录" : "编辑记录";
+  return dialogMode.value === "add" ? `新增记录` : "编辑记录";
 });
 
 const dialogSubmitButtonText = computed(() => {
   return dialogMode.value === "add" ? "添加" : "保存";
 });
+
+const handleCancel = () => {
+  if (formRef.value) {
+    formRef.value.resetFields();
+    console.log("表单已重置");
+  } else {
+    console.warn("表单引用无效,无法重置表单");
+  }
+  dialogVisible.value = false;
+  console.log("对话框已关闭");
+};
+
+// 新增刷新数据功能
+const refreshData = async () => {
+  try {
+    loading.value = true;
+    await fetchTable();
+    showMessage("数据刷新成功", "success");
+  } catch (error) {
+    console.error("刷新数据失败:", error);
+    showMessage("数据刷新失败,请重试", "error");
+  } finally {
+    loading.value = false;
+  }
+};
+
+// 统一提示消息函数
+const showMessage = (
+  message: string,
+  type: "success" | "warning" | "info" | "error" = "info",
+  duration: number = 3000
+) => {
+  ElMessage.closeAll();
+  ElMessage({
+    message,
+    type,
+    duration,
+    customClass: "unified-message",
+    showClose: true,
+  });
+};
+
+const showNotification = (
+  title: string,
+  message: string,
+  type: "success" | "warning" | "info" | "error" = "info",
+  duration: number = 4500
+) => {
+  ElNotification({
+    title,
+    message,
+    type,
+    duration,
+    customClass: `unified-notification notification-${type}`,
+    dangerouslyUseHTMLString: true,
+  });
+};
+
+// 详情弹窗相关
+const detailsVisible = ref(false);
+const detailsContent = ref('');
 </script>
 
-<style scoped>
+<style scoped lang="scss">
+$primary-color: #409eff;
+$success-color: #67c23a;
+$warning-color: #e6a23c;
+$error-color: #f56c6c;
+$info-color: #909399;
+$alert-success-bg: #f0f9eb;
+$alert-success-border: #e1f3d8;
+$alert-success-text: #67c23a;
+$alert-error-bg: #fef0f0;
+$alert-error-border: #fbc4c4;
+$alert-error-text: #f56c6c;
+$alert-warning-bg: #fdf6ec;
+$alert-warning-border: #f5dab1;
+$alert-warning-text: #e6a23c;
+$alert-info-bg: #f4f4f5;
+$alert-info-border: #e9e9eb;
+$alert-info-text: #909399;
+
 .app-container {
-  padding: 20px 40px;
+  padding: 10px;
   min-height: 100vh;
-  background-color: #f0f2f5;
+  background: linear-gradient(135deg, #f5f7fa 0%, #e4edf9 100%);
   box-sizing: border-box;
   display: flex;
   flex-direction: column;
-  align-items: center;
 }
+
+/* 统一消息样式 */
+:global(.unified-message) {
+  border-radius: 8px !important;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
+  padding: 12px 20px !important;
+  font-size: 14px !important;
+  line-height: 1.5 !important;
+  border: 1px solid transparent !important;
+  text-align: center;
+  font-weight: 500;
+}
+:global(.unified-message.el-message--success) {
+  background-color: $alert-success-bg !important;
+  border-color: $alert-success-border !important;
+  color: $alert-success-text !important;
+}
+:global(.unified-message.el-message--error) {
+  background-color: $alert-error-bg !important;
+  border-color: $alert-error-border !important;
+  color: $alert-error-text !important;
+}
+:global(.unified-message.el-message--warning) {
+  background-color: $alert-warning-bg !important;
+  border-color: $alert-warning-border !important;
+  color: $alert-warning-text !important;
+}
+:global(.unified-message.el-message--info) {
+  background-color: $alert-info-bg !important;
+  border-color: $alert-info-border !important;
+  color: $alert-info-text !important;
+}
+
+/* 修改后的统一通知样式 - 居中显示 */
+:global(.unified-notification) {
+  border-radius: 8px !important;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
+  padding: 16px 20px !important;
+  font-size: 14px !important;
+  line-height: 1.5 !important;
+  border: 1px solid transparent !important;
+  text-align: center !important;
+  /* Element Plus 的通知默认是居中的,所以移除之前的 left/right/top/transform 定位 */
+  max-width: 80vw;
+  /* 可选:添加一些内边距或边框来美化 */
+  /* background-color: #fff !important; */ /* 如果需要纯白背景 */
+}
+/* 为不同类型的 notification 添加特定样式 */
+:global(.unified-notification.notification-success) {
+  background-color: $alert-success-bg !important;
+  border-color: $alert-success-border !important;
+  color: $alert-success-text !important;
+}
+:global(.unified-notification.notification-error) {
+  background-color: $alert-error-bg !important;
+  border-color: $alert-error-border !important;
+  color: $alert-error-text !important;
+}
+:global(.unified-notification.notification-warning) {
+  background-color: $alert-warning-bg !important;
+  border-color: $alert-warning-border !important;
+  color: $alert-warning-text !important;
+}
+:global(.unified-notification.notification-info) {
+  background-color: $alert-info-bg !important;
+  border-color: $alert-info-border !important;
+  color: $alert-info-text !important;
+}
+:global(.unified-notification .el-notification__title) {
+  font-weight: bold !important;
+  text-align: center !important;
+  margin-bottom: 8px !important; /* 标题和内容之间增加间距 */
+}
+:global(.unified-notification .el-notification__content) {
+  text-align: center !important;
+  word-wrap: break-word !important;
+}
+
+.content-wrapper {
+  width: 100%;
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+
+.button-section {
+  background: #ffffff;
+  padding: 20px;
+  border-radius: 8px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+  display: flex;
+  flex-direction: column;
+  gap: 15px;
+}
+
+.table-section {
+  background: #ffffff;
+  padding: 20px;
+  border-radius: 8px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+  overflow: hidden;
+}
+
 .button-group {
   display: flex;
-  gap: 12px;
-  justify-content: flex-end;
+  gap: 15px;
+  justify-content: space-between;
   width: 100%;
-  margin-bottom: 20px;
 }
+
+.button-group-right {
+  display: flex;
+  gap: 15px;
+}
+
 .custom-button {
   color: #fff;
   border: none;
-  border-radius: 8px;
+  border-radius: 6px;
   font-size: 14px;
-  padding: 10px 18px;
-  transition: transform 0.3s ease, background-color 0.3s ease;
-  min-width: 130px;
+  padding: 10px 20px;
+  transition: all 0.3s ease;
+  min-width: 110px;
   display: flex;
   align-items: center;
   justify-content: center;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
+  font-weight: 500;
 }
-.download-button,
-.export-button,
-.add-button,
-.import-button {
-  background-color: #67c23a;
+
+.custom-button:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
 }
-.download-button:hover,
-.export-button:hover,
-.import-button:hover,
+
+.custom-button:active {
+  transform: translateY(0);
+}
+
+.add-button {
+  background: linear-gradient(135deg, $success-color, #4ebc4e);
+}
+
 .add-button:hover {
-  background-color: #85ce61;
-  transform: scale(1.05);
+  background: linear-gradient(135deg, #85ce61, $success-color);
+}
+
+.download-button {
+  background: linear-gradient(135deg, $primary-color, #2d8cf0);
+}
+
+.download-button:hover {
+  background: linear-gradient(135deg, #66b1ff, $primary-color);
+}
+
+.export-button {
+  background: linear-gradient(135deg, $warning-color, #d6902d);
+}
+
+.export-button:hover {
+  background: linear-gradient(135deg, #ebb563, $warning-color);
+}
+
+.import-button {
+  background: linear-gradient(135deg, $info-color, #7d8086);
+}
+
+.import-button:hover {
+  background: linear-gradient(135deg, #a6a9ad, $info-color);
 }
+
+.refresh-button {
+  background: linear-gradient(135deg, #409eff, #2d8cf0);
+}
+
+.refresh-button:hover {
+  background: linear-gradient(135deg, #66b1ff, #409eff);
+}
+
 .custom-table {
   width: 100%;
   border-radius: 8px;
   overflow: hidden;
-  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
   background-color: #fff;
-  margin-top: 10px;
 }
-:deep( .el-table th) {
-  background: linear-gradient(180deg, #61e054, #4db944);
+
+:deep(.el-table th) {
   color: #fff;
   font-weight: bold;
   text-align: center;
-  padding: 14px 0;
+  padding: 12px 0;
   font-size: 14px;
 }
-:deep( .el-table__row:nth-child(odd)) {
-  background-color: #E4FBE5;
+
+:deep(.fixed-column-header) {
+  background-color: $primary-color !important;
+}
+
+:deep(.el-table th:not(.fixed-column-header)) {
+  background: linear-gradient(180deg, $primary-color, #2d8cf0);
 }
-:deep( .el-table__row:nth-child(even)) {
+
+:deep(.el-table__row:nth-child(odd)) {
+  background-color: #f9fbfd;
+}
+
+:deep(.el-table__row:nth-child(even)) {
   background-color: #ffffff;
 }
-:deep( .el-table td) {
-  padding: 12px 10px;
+
+:deep(.el-table td) {
+  padding: 12px 8px;
   text-align: center;
   border-bottom: 1px solid #ebeef5;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
 }
+
 .action-button {
   font-size: 16px;
   margin-right: 5px;
   border-radius: 50%;
-  transition: transform 0.3s ease, background-color 0.3s ease;
+  transition: all 0.3s ease;
 }
+
 .edit-button {
-  background-color: #409eff;
+  background-color: $primary-color;
   color: #fff;
+  border: none;
 }
+
 .edit-button:hover {
   background-color: #66b1ff;
   transform: scale(1.1);
 }
+
 .delete-button {
-  background-color: #f56c6c;
+  background-color: $error-color;
   color: #fff;
+  border: none;
 }
+
 .delete-button:hover {
   background-color: #f78989;
   transform: scale(1.1);
 }
-.el-form-item__label {
-  width: 150px;
-  font-weight: bold;
+
+.action-buttons {
+  display: flex;
+  justify-content: center;
 }
-.el-form-item__content {
-  margin-left: 150px;
+
+.pagination-wrapper {
+  display: flex;
+  justify-content: center;
+  width: 100%;
+  margin: 25px 0 10px 0;
 }
-.custom-input .el-input__inner {
-  border-radius: 6px;
-  border: 1px solid #dcdcdc;
-  transition: border-color 0.3s ease;
-  padding: 10px;
+
+.pagination-container {
+  display: flex;
+  justify-content: center;
+}
+
+:deep(.el-dialog) {
+  border-radius: 12px !important;
+  overflow: hidden;
+  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15) !important;
+  background: linear-gradient(145deg, #ffffff, #f5f9ff);
+  border: 1px solid #e0e7ff;
+  margin-top: calc(var(--el-dialog-margin-top, 15vh) + 30px) !important;
+}
+
+.dialog-header {
+  position: relative;
+  padding: 20px 24px 10px;
+  text-align: center;
+  background: linear-gradient(90deg, $primary-color, #2d8cf0);
+}
+
+.dialog-header h2 {
+  margin: 0;
+  font-size: 22px;
+  font-weight: 600;
+  color: #fff;
+  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
+}
+
+.header-decoration {
+  height: 4px;
+  background: linear-gradient(90deg, #2d8cf0, $primary-color);
+  border-radius: 2px;
+  margin-top: 12px;
+  width: 60%;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+.custom-dialog-form {
+  padding: 25px 10px 15px 180px;
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  gap: 20px;
+  max-height: calc(100vh - 300px);
+  overflow-y: auto;
+  scrollbar-width: thin;
+  scrollbar-color: #c0c4cc #f1f3f4;
+}
+
+.custom-dialog-form::-webkit-scrollbar {
+  width: 6px;
+}
+
+.custom-dialog-form::-webkit-scrollbar-track {
+  background: #f1f3f4;
+  border-radius: 3px;
+}
+
+.custom-dialog-form::-webkit-scrollbar-thumb {
+  background: #c0c4cc;
+  border-radius: 3px;
+}
+
+.custom-dialog-form::-webkit-scrollbar-thumb:hover {
+  background: #a8abb3;
+}
+
+@media (max-width: 1200px) {
+  .custom-dialog-form {
+    grid-template-columns: 1fr;
+    padding: 25px 30px 15px 30px;
+  }
+}
+
+.custom-form-item {
+  margin-bottom: 0 !important;
+}
+
+:deep(.el-form-item__label) {
+  display: block;
+  text-align: left;
+  margin-bottom: 6px !important;
   font-size: 14px;
+  font-weight: 500;
+  color: #2d3748;
+  padding: 0 !important;
+  line-height: 1.5;
 }
-.custom-input .el-input__inner:focus {
-  border-color: #409eff;
+
+.dialog-footer {
+  display: flex;
+  gap: 16px;
+  justify-content: center;
+  padding: 15px 40px 25px;
 }
+
 .custom-cancel-button,
 .custom-submit-button {
-  border: none;
+  min-width: 120px;
+  height: 40px;
   border-radius: 6px;
-  font-size: 14px;
-  padding: 10px 20px;
-  transition: transform 0.3s ease, background-color 0.3s ease;
-  min-width: 100px;
+  font-size: 15px;
+  font-weight: 500;
+  transition: all 0.3s ease;
+  letter-spacing: 0.5px;
+  border: none;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
 }
+
 .custom-cancel-button {
-  background-color: #f4f4f5;
-  color: #606266;
+  background: linear-gradient(145deg, #f7fafc, #edf2f7);
+  color: #4a5568;
 }
+
 .custom-cancel-button:hover {
-  background-color: #e4e7ed;
-  transform: scale(1.05);
+  background: linear-gradient(145deg, #e2e8f0, #cbd5e0);
+  transform: translateY(-1px);
+  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
 }
+
 .custom-submit-button {
-  background-color: #409eff;
-  color: #fff;
+  background: linear-gradient(145deg, $primary-color, #2d8cf0);
+  color: white;
+  position: relative;
+  overflow: hidden;
+}
+
+.custom-submit-button::before {
+  content: "";
+  position: absolute;
+  top: 0;
+  left: -100%;
+  width: 100%;
+  height: 100%;
+  background: linear-gradient(
+    90deg,
+    transparent,
+    rgba(255, 255, 255, 0.3),
+    transparent
+  );
+  transition: 0.5s;
 }
+
+.custom-submit-button:hover::before {
+  left: 100%;
+}
+
 .custom-submit-button:hover {
-  background-color: #66b1ff;
-  transform: scale(1.05);
+  transform: translateY(-1px);
+  box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
 }
-.pagination-container {
-  margin-top: 30px;
-  text-align: center;
+
+:deep(.import-upload .el-upload-list) {
+  display: none !important;
 }
-.action-buttons {
-  display: flex;
-  justify-content: center;
+
+:deep(.el-table__row:hover) {
+  background-color: #e6f7ff !important;
+  transition: background-color 0.3s;
+}
+
+:deep(.el-table) {
+  border: 1px solid #ebeef5;
+  border-radius: 8px;
+  overflow: hidden;
+}
+
+:deep(.el-table__header-wrapper) {
+  border-bottom: 1px solid #ebeef5;
+}
+
+:deep(.el-table td) {
+  border-right: 1px solid #ebeef5;
+}
+
+:deep(.el-table td:last-child) {
+  border-right: none;
+}
+
+.details-content {
+  line-height: 1.8;
+  font-size: 14px;
+  color: #333;
+}
+
+.error-content {
+  ul {
+    list-style-type: disc;
+    padding-left: 20px;
+    margin-top: 10px;
+    li {
+      margin-bottom: 8px;
+    }
+  }
 }
-</style>
+</style>

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

@@ -1,138 +1,237 @@
 <template>
   <div class="app-container">
-    <!-- 操作按钮区 -->
-    <div class="button-group">
-      <el-button
-        :icon="Plus"
-        type="primary"
-        @click="openDialog('add')"
-        class="custom-button add-button"
-        >新增记录</el-button
-      >
-      <el-button
-        :icon="Download"
-        type="primary"
-        @click="downloadTemplateAction"
-        class="custom-button download-button"
-        >下载模板</el-button
-      >
-      <el-button
-        :icon="Download"
-        type="primary"
-        @click="exportDataAction"
-        class="custom-button export-button"
-        >导出数据</el-button
-      >
-      <el-upload :before-upload="importDataAction" accept=".xlsx, .csv">
+    <div class="button-section">
+      <div class="button-group">
         <el-button
-          :icon="Upload"
+          :icon="Plus"
           type="primary"
-          class="custom-button import-button"
-          >导入数据</el-button
+          @click="openDialog('add')"
+          class="custom-button add-button"
         >
-      </el-upload>
-    </div>
+          新增记录
+        </el-button>
 
-    <!-- 数据表格 -->
-    <el-table
-      :data="pagedTableDataWithIndex"
-      fit
-      style="width: 100%"
-      @row-click="handleRowClick"
-      highlight-current-row
-      class="custom-table"
-      v-loading="loading"
-      table-layout="auto"
-    >
-      <el-table-column
-        key="displayIndex"
-        prop="displayIndex"
-        label="序号"
-        width="80"
-        align="center"
-      ></el-table-column>
-      <el-table-column
-        v-for="col in columns.filter((col) => col.key !== 'id')"
-        :key="col.key"
-        :prop="col.dataKey"
-        :label="col.title"
-        :min-width="col.title.length * 20 + 40"
-        :formatter="formatNumber"
-        align="center"
-        header-align="center"
-      ></el-table-column>
-      <el-table-column label="操作" width="120" align="center">
-        <template #default="scope">
-          <span class="action-buttons">
-            <el-tooltip
-              class="item"
-              effect="dark"
-              content="编辑"
-              placement="top"
+        <div class="button-group-right">
+          <el-upload
+            :auto-upload="false"
+            :on-change="handleFileSelect"
+            :show-file-list="false"
+            accept=".xlsx, .csv"
+            class="import-upload"
+          >
+            <el-button
+              :icon="Upload"
+              type="primary"
+              class="custom-button import-button"
             >
-              <el-button
-                circle
-                :icon="EditPen"
-                @click.stop="openDialog('edit', scope.row)"
-                class="action-button edit-button"
-              ></el-button>
-            </el-tooltip>
-            <el-tooltip
-              class="item"
-              effect="dark"
-              content="删除"
-              placement="top"
-            >
-              <el-button
-                circle
-                :icon="DeleteFilled"
-                @click.stop="deleteItem(scope.row)"
-                class="action-button delete-button"
-              ></el-button>
-            </el-tooltip>
-          </span>
-        </template>
-      </el-table-column>
-    </el-table>
-
-    <!-- 分页控制 -->
-    <PaginationComponent
-      :total="tableData.length"
-      :currentPage="currentPage4"
-      :pageSize="pageSize4"
-      @update:currentPage="currentPage4 = $event"
-      @update:pageSize="pageSize4 = $event"
-      @size-change="handleSizeChange"
-      @current-change="handleCurrentChange"
-      class="pagination-container"
-    />
-
-    <!-- 新增/编辑对话框 -->
-    <el-dialog :title="dialogTitle" v-model="dialogVisible" width="50%">
-      <el-form :model="formData" label-width="120px">
-        <el-form-item
-          v-for="col in editableColumns"
+              导入降酸数据
+            </el-button>
+          </el-upload>
+
+          <el-button
+            :icon="Download"
+            type="primary"
+            @click="downloadTemplateAction"
+            class="custom-button download-button"
+          >
+            下载模板
+          </el-button>
+
+          <el-button
+            :icon="Download"
+            type="primary"
+            @click="exportDataAction"
+            class="custom-button export-button"
+          >
+            导出数据
+          </el-button>
+
+          <el-button
+            :icon="Refresh"
+            type="primary"
+            @click="refreshData"
+            class="custom-button refresh-button"
+          >
+            刷新
+          </el-button>
+        </div>
+      </div>
+    </div>
+
+    <div class="table-section">
+      <el-table
+        :data="pagedTableDataWithIndex"
+        height="615"
+        fit
+        style="width: 100%"
+        @row-click="handleRowClick"
+        highlight-current-row
+        class="custom-table"
+        v-loading="loading"
+        table-layout="auto"
+      >
+        <el-table-column
+          key="displayIndex"
+          prop="displayIndex"
+          label="序号"
+          width="80"
+          align="center"
+          fixed
+          :header-class-name="'fixed-column-header'"
+        ></el-table-column>
+
+        <el-table-column
+          v-for="col in filteredColumns"
           :key="col.key"
+          :prop="col.dataKey"
           :label="col.title"
+          :width="col.width"
+          :formatter="formatTableValue"
+          align="center"
+          :show-overflow-tooltip="true"
+        ></el-table-column>
+
+        <el-table-column
+          label="操作"
+          width="120"
+          align="center"
+          fixed="right"
+          :header-class-name="'fixed-column-header'"
         >
-          <el-input
-            v-model="formData[col.dataKey]"
-            :type="col.inputType || 'text'"
-            class="custom-input"
-          ></el-input>
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <el-button @click="dialogVisible = false" class="custom-cancel-button">
-          取消
-        </el-button>
-        <el-button
-          type="primary"
-          @click="submitForm"
-          class="custom-submit-button"
+          <template #default="scope">
+            <span class="action-buttons">
+              <el-tooltip
+                class="item"
+                effect="dark"
+                content="编辑"
+                placement="top"
+              >
+                <el-button
+                  circle
+                  :icon="EditPen"
+                  @click.stop="openDialog('edit', scope.row)"
+                  class="action-button edit-button"
+                ></el-button>
+              </el-tooltip>
+              <el-tooltip
+                class="item"
+                effect="dark"
+                content="删除"
+                placement="top"
+              >
+                <el-button
+                  circle
+                  :icon="DeleteFilled"
+                  @click.stop="deleteItem(scope.row.id)"
+                  class="action-button delete-button"
+                >
+                </el-button>
+              </el-tooltip>
+            </span>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <div class="pagination-wrapper">
+        <PaginationComponent
+          :total="tableData.length"
+          :currentPage="currentPage4"
+          :pageSize="pageSize4"
+          @update:currentPage="currentPage4 = $event"
+          @update:pageSize="pageSize4 = $event"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange"
+          class="pagination-container"
+        />
+      </div>
+    </div>
+
+    <el-dialog
+      :title="dialogTitle"
+      v-model="dialogVisible"
+      width="60%"
+      :custom-class="['custom-dialog']"
+      :close-on-click-modal="false"
+      center
+    >
+      <div class="dialog-header">
+        <h2>{{ dialogTitle }}</h2>
+        <div class="header-decoration"></div>
+      </div>
+      <el-scrollbar max-height="calc(90vh - 300px)" style="width: 90%">
+        <el-form
+          ref="formRef"
+          :model="formData"
+          label-position="top"
+          :rules="formRules"
+          class="custom-dialog-form"
         >
-          {{ dialogSubmitButtonText }}
-        </el-button>
+          <el-form-item
+            v-for="col in editableColumns"
+            :key="col.key"
+            :label="col.title"
+            :prop="col.dataKey"
+            class="custom-form-item"
+          >
+            <el-input
+              v-model="formData[col.dataKey]"
+              :type="col.inputType || 'text'"
+              :placeholder="`请输入${col.title}`"
+            />
+          </el-form-item>
+        </el-form>
+      </el-scrollbar>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="handleCancel" class="custom-cancel-button"
+            >取消</el-button
+          >
+          <el-button
+            type="primary"
+            @click="submitForm"
+            class="custom-submit-button"
+            >{{ dialogSubmitButtonText }}</el-button
+          >
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 详情弹窗 -->
+    <el-dialog
+      title="导入成功详情"
+      v-model="detailsVisible"
+      width="50%"
+      :close-on-click-modal="false"
+      center
+    >
+      <div class="details-content" v-html="detailsContent"></div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="detailsVisible = false" class="custom-cancel-button">关闭</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 空值错误提示弹窗 -->
+    <el-dialog
+      title="空值错误"
+      v-model="emptyCellDialogVisible"
+      width="40%"
+      :close-on-click-modal="false"
+      center
+    >
+      <div class="error-content">
+        <p>以下单元格存在空值,请检查:</p>
+        <ul>
+          <li v-for="(error, index) in emptyCellErrors" :key="index">
+            第{{ error.row + 1 }}行,{{ error.column }}列
+          </li>
+        </ul>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="emptyCellDialogVisible = false" class="custom-cancel-button">关闭</el-button>
+        </div>
       </template>
     </el-dialog>
   </div>
@@ -140,78 +239,196 @@
 
 <script lang="ts" setup>
 import { ref, reactive, computed, onMounted } from "vue";
+import * as XLSX from "xlsx";
+
 import {
   DeleteFilled,
   Download,
   Upload,
   Plus,
   EditPen,
+  Refresh,
 } from "@element-plus/icons-vue";
-import { ElMessage } from "element-plus";
+import {
+  ElMessage,
+  ElForm,
+  ElMessageBox,
+  ElNotification,
+  FormRules,
+  FormItemRule,
+} from "element-plus";
 import {
   table,
   updateItem,
   addItem,
   deleteItemApi,
-  downloadTemplate,
   exportData,
   importData,
-} from "@/API/menus";
+  downloadTemplate,
+} from "@/API/admin";
 import PaginationComponent from "@/components/PaginationComponent.vue";
 
+interface EmptyCellError {
+  row: number; // Excel 行号
+  column: string; // 缺失列名
+}
 interface Column {
   key: string;
   dataKey: string;
   title: string;
   width: number;
   inputType?: string;
+  step?: string;
+  precision?: number;
+  options?: Array<{ label: string; value: string | number }>;
 }
 
+const fieldTypeMap: Record<string, "text" | "number"> = {
+  Q_over_b: "number",
+  pH: "number",
+  OM: "number",
+  CL: "number",
+  H: "number",
+  Al: "number",
+};
+
 const columns: Column[] = [
-  { key: "id", dataKey: "id", title: "序号", width: 100 },
-  { key: "Q_over_b", dataKey: "Q_over_b", title: "Q/ΔpH", width: 180 },
-  { key: "pH", dataKey: "pH", title: "初始pH", width: 180 },
-  { key: "OM", dataKey: "OM", title: "有机质含量", width: 180 },
-  { key: "CL", dataKey: "CL", title: "土壤粘粒", width: 180 },
-  { key: "H", dataKey: "H", title: "交换性氢", width: 180 },
-  { key: "Al", dataKey: "Al", title: "交换性铝", width: 180 },
+  { key: "id", dataKey: "id", title: "ID", width: 100 },
+  {
+    key: "Q_over_b",
+    dataKey: "Q_over_b",
+    title: "Q/ΔpH",
+    width: 170,
+    precision: 2,
+  },
+  {
+    key: "pH",
+    dataKey: "pH",
+    title: "初始pH",
+    width: 150,
+    precision: 2,
+  },
+  {
+    key: "OM",
+    dataKey: "OM",
+    title: "有机质含量",
+    width: 150,
+    precision: 2,
+  },
+  {
+    key: "CL",
+    dataKey: "CL",
+    title: "土壤粘粒",
+    width: 150,
+    precision: 2,
+  },
+  {
+    key: "H",
+    dataKey: "H",
+    title: "交换性氢",
+    width: 150,
+    precision: 2,
+  },
+  {
+    key: "Al",
+    dataKey: "Al",
+    title: "交换性铝",
+    width: 160,
+    precision: 2,
+  },
 ];
-
 const editableColumns = columns.filter((col) => col.key !== "id");
 
+type TableName = "current_reduce" | "current_reflux";
+const currentTableName: TableName = "current_reduce";
+
+const emptyCellErrors = ref<EmptyCellError[]>([]);
+const emptyCellDialogVisible = ref(false);
+
+const showEmptyCellErrors = (errors: EmptyCellError[]) => {
+  emptyCellErrors.value = errors;
+  emptyCellDialogVisible.value = true;
+};
+
 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 loading = ref(false);
+const formRef = ref<InstanceType<typeof ElForm> | null>(null);
+
+const filteredColumns = computed(() =>
+  columns.filter((col) => col.key !== "id")
+);
 
 const currentPage4 = ref(1);
 const pageSize4 = ref(10);
 
-const pagedTableData = computed(() => {
-  const start = (currentPage4.value - 1) * pageSize4.value;
-  const end = start + pageSize4.value;
-  return tableData.value.slice(start, end);
+const dialogVisible = ref(false);
+const formData = reactive<any>({});
+const dialogMode = ref<"add" | "edit">("add");
+
+// 定义支持的列名映射,包括中英文
+const REQUIRED_COLUMNS = {
+  "Q_over_b": "Q/ΔpH",
+  "pH": "初始pH",
+  "OM": "有机质含量",
+  "CL": "土壤粘粒",
+  "H": "交换性氢",
+  "Al": "交换性铝"
+};
+
+// 反向映射,用于将中文列名转换为英文列名
+const COLUMN_NAME_MAPPING: Record<string, string> = {};
+Object.entries(REQUIRED_COLUMNS).forEach(([enKey, zhValue]) => {
+  COLUMN_NAME_MAPPING[enKey] = enKey; // 英文映射
+  COLUMN_NAME_MAPPING[zhValue] = enKey; // 中文映射
 });
 
+const normalizeValue = (val: any): any => {
+  if (val === undefined) {
+    return "";
+  }
+  if (typeof val === "number" && isNaN(val)) {
+    return "";
+  }
+  if (typeof val === "string" && val.toLowerCase() === "nan") {
+    return "";
+  }
+  return val;
+};
+
 const pagedTableDataWithIndex = computed(() => {
-  return pagedTableData.value.map((item, index) => ({
-    ...item,
-    displayIndex: (currentPage4.value - 1) * pageSize4.value + index + 1,
-  }));
+  const start = (currentPage4.value - 1) * pageSize4.value;
+  const end = start + pageSize4.value;
+  return tableData.value
+    .slice(start, end)
+    .map((row: { [s: string]: unknown } | ArrayLike<unknown>, idx: number) => {
+      const processedRow = Object.entries(row).reduce((acc, [key, val]) => {
+        acc[key] = normalizeValue(val);
+        return acc;
+      }, {} as any);
+      return {
+        ...processedRow,
+        displayIndex: start + idx + 1,
+      };
+    });
 });
 
-const loading = ref(false);
-const currentTableName = "current_reduce";
-
 const fetchTable = async () => {
   try {
     loading.value = true;
-    const response = await table({ table: currentTableName });
-    tableData.value = response.data.rows;
+    const response: any = await table({ table: currentTableName });
+
+    const rawData = Array.isArray(response.data) ? response.data : [];
+
+    tableData.value = rawData.map((row: any) =>
+      Object.entries(row).reduce((acc: any, [key, val]) => {
+        acc[key] = normalizeValue(val);
+        return acc;
+      }, {})
+    );
   } catch (error) {
     console.error("获取数据时出错:", error);
-    ElMessage.error("获取数据失败,请检查网络连接或服务器状态");
+    showMessage("获取数据失败,请检查网络连接或服务器状态", "error");
   } finally {
     loading.value = false;
   }
@@ -223,21 +440,35 @@ onMounted(() => {
 
 const handleRowClick = (row: any) => {
   selectedRow.value = row;
-  Object.assign(formData, row);
+  Object.assign(
+    formData,
+    Object.entries(row).reduce((acc: any, [key, val]) => {
+      acc[key] = normalizeValue(val);
+      return acc;
+    }, {})
+  );
 };
 
 const openDialog = (mode: "add" | "edit", row?: any) => {
+  dialogMode.value = mode;
+  dialogVisible.value = true;
+
   if (mode === "add") {
     selectedRow.value = null;
     editableColumns.forEach((col) => {
-      formData[col.dataKey] = "";
+      const dataKey = col.dataKey;
+      formData[dataKey] = "";
     });
   } else if (row) {
     selectedRow.value = row;
-    Object.assign(formData, row);
+    Object.assign(
+      formData,
+      Object.entries(row).reduce((acc: any, [key, val]) => {
+        acc[key] = normalizeValue(val);
+        return acc;
+      }, {})
+    );
   }
-  dialogMode.value = mode;
-  dialogVisible.value = true;
 };
 
 function prepareFormData(
@@ -248,289 +479,1017 @@ function prepareFormData(
   for (const key in formData) {
     if (!excludeKeys.includes(key)) {
       let value = formData[key];
-      if (typeof value === "string" && value.startsWith("displayIndex-")) {
-        value = value.replace("displayIndex-", "");
+      if (value === "") {
+        result[key] = undefined;
+      } else {
+        result[key] = value;
       }
-      result[key] = value;
     }
   }
   return result;
 }
 
+const formRules = computed<FormRules>(() => {
+  const rules: FormRules = {};
+  editableColumns.forEach((col) => {
+    const fieldKey = col.dataKey;
+
+    const fieldRules: FormItemRule[] = [];
+
+    fieldRules.push({
+      required: true,
+      message: `请输入${col.title}`,
+      trigger: "blur",
+    });
+
+    rules[fieldKey] = fieldRules;
+  });
+  return rules;
+});
+
 const submitForm = async () => {
+  if (!formRef.value) return;
+
   try {
-    const isValid = validateFormData(formData);
+    const isValid = await formRef.value.validate();
     if (!isValid) {
-      alert("请检查输入的数据");
+      console.log("表单验证未通过");
+      showMessage("表单验证未通过,请检查输入", "warning");
       return;
     }
-    const dataToSubmit = prepareFormData(formData);
-    if (!dataToSubmit.id && dialogMode.value !== "add") {
-      alert("无法找到记录ID,请联系管理员");
-      return;
+  } catch (validationError: any) {
+    console.error("表单验证失败:", validationError);
+    let messageToShow = "表单验证失败,请检查输入";
+    if (
+      validationError &&
+      typeof validationError === "object" &&
+      !Array.isArray(validationError)
+    ) {
+      const firstFieldErrors = Object.values(validationError)[0];
+      if (
+        Array.isArray(firstFieldErrors) &&
+        firstFieldErrors.length > 0 &&
+        firstFieldErrors[0].message
+      ) {
+        messageToShow = firstFieldErrors[0].message;
+      }
+    } else if (Array.isArray(validationError)) {
+      const firstError = validationError[0];
+      if (firstError && firstError.message) {
+        messageToShow = `表单验证失败: ${firstError.message}`;
+      }
     }
-    let response;
+    showMessage(messageToShow, "error");
+    return;
+  }
+
+  try {
+    const dataToSubmit = prepareFormData(formData);
+    let response: { status?: number; data?: any };
+
     if (dialogMode.value === "add") {
-      response = await addItem({ table: currentTableName, item: dataToSubmit });
-    } else {
-      response = await updateItem({
+      response = (await addItem({
         table: currentTableName,
         item: dataToSubmit,
-      });
+      })) as { status?: number; data?: any };
+
+      // 修改成功判断逻辑:只检查状态码,不再检查 data.success
+      if (response && (response.status === 200 || response.status === 201)) {
+        showMessage("添加成功", "success");
+        dialogVisible.value = false;
+        await fetchTable();
+      } else {
+        console.warn("addItem 响应结构异常或状态码非200/201:", response);
+        // 根据实际返回内容决定提示信息
+        if (response && response.data) {
+          const serverMsg =
+            response.data.detail ||
+            response.data.message ||
+            response.data.error ||
+            "未知错误";
+          showMessage(`添加操作完成,但服务器返回: ${serverMsg}`, "warning");
+        } else {
+          showMessage("添加操作完成,但响应数据异常。", "warning");
+        }
+      }
+    } else {
+      console.log("编辑模式 - selectedRow:", selectedRow.value);
+
+      if (!selectedRow.value) {
+        showMessage("未选中任何记录,请先选择要编辑的记录", "error");
+        return;
+      }
+
+      response = (await updateItem({
+        table: currentTableName,
+        id: selectedRow.value.id,
+        update_data: dataToSubmit,
+      })) as { status?: number; data?: any };
+
+      const index = tableData.value.findIndex(
+        (item: { id: any }) => item.id === selectedRow.value!.id
+      );
+      if (index > -1) {
+        tableData.value[index] = {
+          ...tableData.value[index],
+          ...dataToSubmit,
+        };
+        showMessage("修改成功", "success");
+      } else {
+        console.warn("本地未找到对应记录,重新获取数据");
+        await fetchTable();
+        showMessage("修改成功,数据已刷新", "success");
+      }
+
+      dialogVisible.value = false;
     }
-    dialogVisible.value = false;
-    fetchTable();
-    alert(dialogMode.value === "add" ? "添加成功" : "修改成功");
-  } catch (error) {
-    let errorMessage = "未知错误";
-    if (error && typeof error === "object" && "response" in error) {
-      const response = (error as { response?: { data?: { message?: string } } })
-        .response;
-      if (
-        response &&
-        response.data &&
-        typeof response.data.message === "string"
-      ) {
-        errorMessage = response.data.message;
+  } catch (error: any) {
+    console.error("提交表单时发生错误:", error);
+
+    let errorMessage = "提交失败";
+
+    if (error.response) {
+      console.log("服务器响应错误:", error.response);
+      const status = error.response.status;
+      const data = error.response.data;
+
+      const errorDetail = data?.error || data?.detail || "";
+
+      if (typeof errorDetail === "string" && errorDetail.includes("已存在")) {
+        errorMessage = "提交失败:数据已存在,请勿重复添加。";
+      } else if (status === 409) {
+        errorMessage = "提交失败:数据已存在,请勿重复添加。";
+      } else if (status === 400) {
+        errorMessage = "提交失败:请求参数有误。";
+      } else if (status === 500) {
+        if (data && data.error) {
+          errorMessage = `服务器内部错误: ${data.error}`;
+        } else {
+          errorMessage = "提交失败: 服务器内部错误。";
+        }
+      } else {
+        errorMessage = `提交失败: 服务器错误 (${status})`;
+        if (data && data.error) {
+          errorMessage += ` - ${data.error}`;
+        } else if (data && data.message) {
+          errorMessage += ` - ${data.message}`;
+        }
       }
+    } else if (error.request) {
+      console.error("网络错误或请求无响应:", error.request);
+      errorMessage = "提交失败: 网络连接问题或服务器无响应。";
+    } else {
+      console.error("请求配置错误:", error.message);
+      errorMessage = `提交失败: ${error.message}`;
     }
-    alert(`提交失败: ${errorMessage}`);
+
+    showMessage(errorMessage, "error");
   }
 };
 
-function validateFormData(data: { [x: string]: undefined }) {
-  for (let key in data) {
-    if (data[key] === "" || data[key] === undefined) {
-      return false;
-    }
-  }
-  return true;
-}
+const deleteItem = async (rowId: number) => {
+  const index = tableData.value.findIndex((item) => item.id === rowId);
 
-const deleteItem = async (row: any) => {
-  if (!row) {
-    ElMessage.warning("请先选择一行记录");
+  if (index === -1) {
+    showMessage("无法找到要删除的记录或记录ID无效", "error");
+    console.error("DeleteItem: Row ID not found:", rowId);
     return;
   }
+
   try {
-    const condition = { id: row.id };
-    await deleteItemApi({ table: "current_reduce", condition });
-    const index = tableData.value.findIndex((item) => item.id === row.id);
-    if (index > -1) {
-      tableData.value.splice(index, 1);
+    await ElMessageBox.confirm(
+      `确定要删除该记录吗?此操作不可恢复。`,
+      "删除确认",
+      {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning",
+        customClass: "unified-confirm-dialog",
+      }
+    );
+
+    await deleteItemApi({
+      table: currentTableName,
+      id: rowId,
+    });
+
+    tableData.value.splice(index, 1);
+
+    showMessage("记录删除成功", "success");
+  } catch (error: any) {
+    if (error === "cancel") {
+      console.log("用户取消删除");
+      return;
     }
-    fetchTable();
-  } catch (error) {
     console.error("删除记录时发生错误:", error);
+    showMessage("删除失败,请重试", "error");
   }
 };
 
 const downloadTemplateAction = async () => {
   try {
-    await downloadTemplate("current_reduce");
+    await downloadTemplate(currentTableName);
+    showMessage("模板下载成功", "success");
   } catch (error) {
     console.error("下载模板时发生错误:", error);
+    showMessage("下载模板失败,请重试", "error");
   }
 };
 
 const exportDataAction = async () => {
   try {
-    await exportData("current_reduce");
+    await exportData(currentTableName);
+    showMessage("数据导出成功", "success");
   } catch (error) {
     console.error("导出数据时发生错误:", error);
+    showMessage("导出数据失败,请重试", "error");
   }
 };
 
-// 导入
+const handleFileSelect = async (uploadFile: any) => {
+  const file = uploadFile.raw;
+  if (!file) {
+    showMessage("请选择有效的 .xlsx 或 .csv 文件", "warning");
+    return;
+  }
+  await importDataAction(file);
+};
+
+interface AxiosError extends Error {
+  response?: {
+    data?: {
+      detail?: any;
+      message?: string;
+      [key: string]: any;
+    };
+    status?: number;
+    statusText?: string;
+    headers?: any;
+    config?: any;
+  };
+  request?: any;
+  config?: any;
+}
+
+interface ErrorResponse {
+  error?: string;
+  message?: string;
+  [key: string]: any;
+}
+
+// 定义详细信息展示函数的类型
+interface ShowDetailedInfo {
+  (title: string, content: string): void;
+}
+
+// 优化后的数据导入函数
 const importDataAction = async (file: File) => {
   try {
-    const response = await importData("reduce", file); // 传递 dataset_type 的值(如 'reduce')
-    if (response && response.data) {
-      const { total_data, new_data, duplicate_data, message } = response.data;
-      ElMessage({
-        message: `导入结果: ${message} 新增总数:${total_data}, 成功新增:${new_data}, 数据重复:${duplicate_data}`,
-        type: "success",
+    // 1. 读取文件
+    const dataArrayBuffer = await file.arrayBuffer();
+    const workbook = XLSX.read(dataArrayBuffer, { type: "array" });
+    const firstSheetName = workbook.SheetNames[0];
+    const worksheet = workbook.Sheets[firstSheetName];
+    const jsonData: any[] = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
+
+    // 2. 检查是否为空文件
+    const hasData = jsonData.some(row => 
+      Array.isArray(row) && row.some(cell => 
+        cell !== null && cell !== undefined && String(cell).trim() !== ""
+      )
+    );
+    if (!hasData) {
+      showMessage("❌ 文件为空", "error");
+      return;
+    }
+
+    // 3. 查找表头行
+    const headerRowIndex = jsonData.findIndex(row => 
+      Array.isArray(row) && row.some(cell => 
+        cell !== null && cell !== undefined && String(cell).trim() !== ""
+      )
+    );
+    if (headerRowIndex === -1) {
+      showMessage("❌ 未找到表头", "error");
+      return;
+    }
+
+    // 4. 获取列名并映射
+    const headerRow = jsonData[headerRowIndex];
+    const rawColumns = (headerRow as string[]).map(col => String(col).trim());
+    const columns = rawColumns.map(col => COLUMN_NAME_MAPPING[col] || col);
+
+    // 5. 检查必要列
+    const requiredCols = Object.keys(REQUIRED_COLUMNS);
+    const missingCols = requiredCols.filter(col => !columns.includes(col));
+    if (missingCols.length > 0) {
+      showMessage(`❌ 缺少列: ${missingCols.join(", ")}`, "error");
+      return;
+    }
+
+    // 6. 检查数据行
+    const dataRows = jsonData.slice(headerRowIndex + 1);
+    const validRows = dataRows.filter(row => 
+      requiredCols.some(col => {
+        const idx = columns.indexOf(col);
+        return idx !== -1 && row?.[idx] != null && String(row[idx]).trim() !== "";
+      })
+    );
+
+    if (validRows.length === 0) {
+      showMessage("⚠️ 无有效数据", "error");
+      return;
+    }
+
+    // 7. 检查必填列是否为空
+    const emptyErrors: string[] = [];
+    validRows.forEach((row, rowIdx) => {
+      requiredCols.forEach(col => {
+        const colIndex = columns.indexOf(col);
+        if (colIndex !== -1 && (row?.[colIndex] == null || String(row[colIndex]).trim() === "")) {
+          emptyErrors.push(`第${rowIdx + 2}行,${col}列为空`);
+        }
       });
-      fetchTable(); // 假设存在 fetchTable 方法刷新表格
+    });
+
+    if (emptyErrors.length > 0) {
+      showMessage(`❌ 必填列为空:\n${emptyErrors.slice(0, 5).join('\n')}`, "error");
+      return;
     }
-  } catch (error) {
-    let errorMessage = "数据导入失败";
-    if (error && typeof error === "object" && "response" in error) {
-      const response = (error as { response?: { data?: { message?: string } } })
-        .response;
-      if (response && response.data && typeof response.data.message === "string") {
-        errorMessage += `: ${response.data.message}`;
+
+    // 8. 转换数据格式
+    const transformedData = validRows.map(row => {
+      const newRow: any = {};
+      rawColumns.forEach((origCol, idx) => {
+        const engCol = COLUMN_NAME_MAPPING[origCol] || origCol;
+        if (requiredCols.includes(engCol)) {
+          newRow[engCol] = row?.[idx];
+        }
+      });
+      return newRow;
+    });
+
+    // 9. 创建新工作簿
+    const newWorkbook = XLSX.utils.book_new();
+    const newWorksheet = XLSX.utils.json_to_sheet(transformedData);
+    XLSX.utils.book_append_sheet(newWorkbook, newWorksheet, "Sheet1");
+    const outputArrayBuffer = XLSX.write(newWorkbook, { bookType: "xlsx", type: "array" });
+    const newFile = new File([outputArrayBuffer], file.name, {
+      type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+    });
+
+    // 10. 构造表单数据
+    const formData = new FormData();
+    formData.append("file", newFile);
+    formData.append("dataset_name", file.name.replace(/\.[^/.]+$/, "") || "Imported Dataset");
+    formData.append("dataset_description", `通过前端上传的 ${file.name} 数据集`);
+
+    // 11. 上传数据
+    const response: any = await importData(currentTableName, formData);
+
+    // 12. 处理响应
+    if ((response.status === 200 || response.status === 201) && response.data) {
+      const data = response.data;
+      const stats = data.data_stats || {};
+
+      // 主要成功信息
+      let mainMessage = `🎉 数据集上传成功!\n\n`;
+      mainMessage += `📌 数据集名称: ${data.dataset_name || data.dataset_id || "未知"}\n`;
+      mainMessage += `📌 唯一ID: ${data.dataset_id}\n`;
+
+      // 详细统计信息(可折叠或单独显示)
+      let detailMessage = `\n📊 数据统计详情:\n`;
+      detailMessage += `   • 原始行数: ${stats.original_count || 0}\n`;
+      detailMessage += `   • 文件内重复: ${stats.duplicates_in_file || 0}\n`;
+      detailMessage += `   • 与现有数据重复: ${stats.duplicates_with_existing || 0}\n`;
+      detailMessage += `   • 与测试集冲突: ${stats.test_overlap_count || 0}\n`;
+      detailMessage += `   • 最终入库: ${stats.final_count || 0}\n`;
+
+      // 附加信息
+      let additionalInfo = '';
+      if (data.training_triggered) {
+        additionalInfo += `\n🚀 自动训练已触发,任务ID: ${data.task_id}`;
       }
+
+      if (data.message) {
+        additionalInfo += `\n\n💡 系统提示: ${data.message.replace("✅", "").replace("🎉", "").trim()}`;
+      }
+
+      // 分步骤显示消息
+      showMessage(mainMessage, "success");
+      
+      // 在控制台显示详细信息
+      console.log(detailMessage + additionalInfo);
+      
+      // 显示详细信息弹窗
+      detailsContent.value = (mainMessage + detailMessage + additionalInfo).replace(/\n/g, '<br/>');
+      detailsVisible.value = true;
+      
+      await fetchTable(); // 刷新表格
+    } else {
+      const errorMsg = response.data?.error || "服务器未返回预期数据";
+      showMessage(`❌ 导入失败\n服务器返回错误:${errorMsg}\n状态码:${response.status}\n\n请检查网络或联系管理员。`, "error");
+    }
+  } catch (error: any) {
+    let errorMsg = "导入失败";
+    let errorDetail = "未知错误";
+
+    if (error.name === "AbortError") {
+      errorMsg = "请求已取消";
+      errorDetail = "网络请求被中断,请检查连接后重试。";
+    } else if (error.message.includes("Invalid data")) {
+      errorMsg = "文件数据格式错误";
+      errorDetail = "Excel 文件中包含不支持的数据类型(如公式、图片、合并单元格),请使用纯数据表格。";
+    } else if (error.message.includes("Unable to parse") || error.message.includes("Bad file")) {
+      errorMsg = "无法解析文件";
+      errorDetail = "文件可能已损坏或不是有效的 .xlsx 格式,请重新保存或另存为 Excel 文件。";
+    } else if (error.message.includes("Network Error")) {
+      errorMsg = "网络错误";
+      errorDetail = "无法连接到服务器,请检查网络连接。";
+    } else {
+      errorDetail = error.message || "请检查文件格式和内容是否符合要求。";
     }
-    ElMessage.error(errorMessage);
+
+    showMessage(`❌ ${errorMsg}\n${errorDetail}`, "error");
+
+    console.error("导入文件出错:", error);
   }
 };
 
-const handleFileChange = (event: Event) => {
-  const target = event.target as HTMLInputElement;
-  if (target.files && target.files.length > 0) {
-    importDataAction(target.files[0]);
-  }
+// 定义详细信息展示函数,带有明确的参数类型
+const showDetailedInfo: ShowDetailedInfo = (title: string, content: string) => {
+  // 这里可以实现一个模态框或侧边栏来展示详细内容
+  console.log(`=== ${title} ===`);
+  console.log(content);
+  console.log('====================================');
 };
 
+const handleSizeChange = (val: number) => {
+  pageSize4.value = val;
+  currentPage4.value = 1;
+};
 
-const handleSizeChange = (val: number) => {};
-const handleCurrentChange = (val: number) => {};
+const handleCurrentChange = (val: number) => {
+  currentPage4.value = val;
+};
 
-const formatNumber = (row: any, column: any, cellValue: any) => {
-  if (typeof cellValue === "number") {
-    return cellValue.toFixed(3);
-  }
-  return cellValue;
+const formatTableValue = (row: any, column: any, cellValue: any) => {
+  return normalizeValue(cellValue);
 };
 
 const dialogTitle = computed(() => {
-  return dialogMode.value === "add" ? "新增记录" : "编辑记录";
+  return dialogMode.value === "add" ? `新增记录` : "编辑记录";
 });
 
 const dialogSubmitButtonText = computed(() => {
   return dialogMode.value === "add" ? "添加" : "保存";
 });
+
+const handleCancel = () => {
+  if (formRef.value) {
+    formRef.value.resetFields();
+    console.log("表单已重置");
+  } else {
+    console.warn("表单引用无效,无法重置表单");
+  }
+  dialogVisible.value = false;
+  console.log("对话框已关闭");
+};
+
+// 新增刷新数据功能
+const refreshData = async () => {
+  try {
+    loading.value = true;
+    await fetchTable();
+    showMessage("数据刷新成功", "success");
+  } catch (error) {
+    console.error("刷新数据失败:", error);
+    showMessage("数据刷新失败,请重试", "error");
+  } finally {
+    loading.value = false;
+  }
+};
+
+// 统一提示消息函数
+const showMessage = (
+  message: string,
+  type: "success" | "warning" | "info" | "error" = "info",
+  duration: number = 3000
+) => {
+  ElMessage.closeAll();
+  ElMessage({
+    message,
+    type,
+    duration,
+    customClass: "unified-message",
+    showClose: true,
+  });
+};
+
+const showNotification = (
+  title: string,
+  message: string,
+  type: "success" | "warning" | "info" | "error" = "info",
+  duration: number = 4500
+) => {
+  ElNotification({
+    title,
+    message,
+    type,
+    duration,
+    customClass: `unified-notification notification-${type}`,
+    dangerouslyUseHTMLString: true,
+  });
+};
+
+// 详情弹窗相关
+const detailsVisible = ref(false);
+const detailsContent = ref('');
 </script>
 
-<style scoped>
+<style scoped lang="scss">
+$primary-color: #409eff;
+$success-color: #67c23a;
+$warning-color: #e6a23c;
+$error-color: #f56c6c;
+$info-color: #909399;
+$alert-success-bg: #f0f9eb;
+$alert-success-border: #e1f3d8;
+$alert-success-text: #67c23a;
+$alert-error-bg: #fef0f0;
+$alert-error-border: #fbc4c4;
+$alert-error-text: #f56c6c;
+$alert-warning-bg: #fdf6ec;
+$alert-warning-border: #f5dab1;
+$alert-warning-text: #e6a23c;
+$alert-info-bg: #f4f4f5;
+$alert-info-border: #e9e9eb;
+$alert-info-text: #909399;
+
 .app-container {
-  padding: 15px 40px;
+  padding: 10px;
   min-height: 100vh;
-  background-color: #f0f2f5;
+  background: linear-gradient(135deg, #f5f7fa 0%, #e4edf9 100%);
+  box-sizing: border-box;
   display: flex;
   flex-direction: column;
-  align-items: center;
 }
+
+/* 统一消息样式 */
+:global(.unified-message) {
+  border-radius: 8px !important;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
+  padding: 12px 20px !important;
+  font-size: 14px !important;
+  line-height: 1.5 !important;
+  border: 1px solid transparent !important;
+  text-align: center;
+  font-weight: 500;
+}
+:global(.unified-message.el-message--success) {
+  background-color: $alert-success-bg !important;
+  border-color: $alert-success-border !important;
+  color: $alert-success-text !important;
+}
+:global(.unified-message.el-message--error) {
+  background-color: $alert-error-bg !important;
+  border-color: $alert-error-border !important;
+  color: $alert-error-text !important;
+}
+:global(.unified-message.el-message--warning) {
+  background-color: $alert-warning-bg !important;
+  border-color: $alert-warning-border !important;
+  color: $alert-warning-text !important;
+}
+:global(.unified-message.el-message--info) {
+  background-color: $alert-info-bg !important;
+  border-color: $alert-info-border !important;
+  color: $alert-info-text !important;
+}
+
+/* 修改后的统一通知样式 - 居中显示 */
+:global(.unified-notification) {
+  border-radius: 8px !important;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
+  padding: 16px 20px !important;
+  font-size: 14px !important;
+  line-height: 1.5 !important;
+  border: 1px solid transparent !important;
+  text-align: center !important;
+  /* Element Plus 的通知默认是居中的,所以移除之前的 left/right/top/transform 定位 */
+  max-width: 80vw;
+  /* 可选:添加一些内边距或边框来美化 */
+  /* background-color: #fff !important; */ /* 如果需要纯白背景 */
+}
+/* 为不同类型的 notification 添加特定样式 */
+:global(.unified-notification.notification-success) {
+  background-color: $alert-success-bg !important;
+  border-color: $alert-success-border !important;
+  color: $alert-success-text !important;
+}
+:global(.unified-notification.notification-error) {
+  background-color: $alert-error-bg !important;
+  border-color: $alert-error-border !important;
+  color: $alert-error-text !important;
+}
+:global(.unified-notification.notification-warning) {
+  background-color: $alert-warning-bg !important;
+  border-color: $alert-warning-border !important;
+  color: $alert-warning-text !important;
+}
+:global(.unified-notification.notification-info) {
+  background-color: $alert-info-bg !important;
+  border-color: $alert-info-border !important;
+  color: $alert-info-text !important;
+}
+:global(.unified-notification .el-notification__title) {
+  font-weight: bold !important;
+  text-align: center !important;
+  margin-bottom: 8px !important; /* 标题和内容之间增加间距 */
+}
+:global(.unified-notification .el-notification__content) {
+  text-align: center !important;
+  word-wrap: break-word !important;
+}
+
+.content-wrapper {
+  width: 100%;
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+
+.button-section {
+  background: #ffffff;
+  padding: 20px;
+  border-radius: 8px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+  display: flex;
+  flex-direction: column;
+  gap: 15px;
+}
+
+.table-section {
+  background: #ffffff;
+  padding: 20px;
+  border-radius: 8px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+  overflow: hidden;
+}
+
 .button-group {
   display: flex;
-  gap: 12px;
-  justify-content: flex-end;
+  gap: 15px;
+  justify-content: space-between;
   width: 100%;
-  margin-bottom: 20px;
 }
+
+.button-group-right {
+  display: flex;
+  gap: 15px;
+}
+
 .custom-button {
   color: #fff;
   border: none;
-  border-radius: 8px;
+  border-radius: 6px;
   font-size: 14px;
-  padding: 10px 18px;
-  transition: transform 0.3s ease, background-color 0.3s ease;
-  min-width: 130px;
+  padding: 10px 20px;
+  transition: all 0.3s ease;
+  min-width: 110px;
   display: flex;
   align-items: center;
   justify-content: center;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
+  font-weight: 500;
 }
-.download-button,
-.export-button,
-.add-button,
-.import-button {
-  background-color: #67c23a;
+
+.custom-button:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
 }
-.download-button:hover,
-.export-button:hover,
-.import-button:hover,
+
+.custom-button:active {
+  transform: translateY(0);
+}
+
+.add-button {
+  background: linear-gradient(135deg, $success-color, #4ebc4e);
+}
+
 .add-button:hover {
-  background-color: #85ce61;
-  transform: scale(1.05);
+  background: linear-gradient(135deg, #85ce61, $success-color);
+}
+
+.download-button {
+  background: linear-gradient(135deg, $primary-color, #2d8cf0);
+}
+
+.download-button:hover {
+  background: linear-gradient(135deg, #66b1ff, $primary-color);
 }
+
+.export-button {
+  background: linear-gradient(135deg, $warning-color, #d6902d);
+}
+
+.export-button:hover {
+  background: linear-gradient(135deg, #ebb563, $warning-color);
+}
+
+.import-button {
+  background: linear-gradient(135deg, $info-color, #7d8086);
+}
+
+.import-button:hover {
+  background: linear-gradient(135deg, #a6a9ad, $info-color);
+}
+
+.refresh-button {
+  background: linear-gradient(135deg, #409eff, #2d8cf0);
+}
+
+.refresh-button:hover {
+  background: linear-gradient(135deg, #66b1ff, #409eff);
+}
+
 .custom-table {
   width: 100%;
   border-radius: 8px;
   overflow: hidden;
-  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
   background-color: #fff;
-  margin-top: 10px;
 }
-:deep( .el-table th) {
-  background: linear-gradient(180deg, #61e054, #4db944);
+
+:deep(.el-table th) {
   color: #fff;
   font-weight: bold;
   text-align: center;
-  padding: 14px 0;
+  padding: 12px 0;
   font-size: 14px;
 }
-:deep( .el-table__row:nth-child(odd)) {
-  background-color: #E4FBE5;
+
+:deep(.fixed-column-header) {
+  background-color: $primary-color !important;
+}
+
+:deep(.el-table th:not(.fixed-column-header)) {
+  background: linear-gradient(180deg, $primary-color, #2d8cf0);
+}
+
+:deep(.el-table__row:nth-child(odd)) {
+  background-color: #f9fbfd;
 }
-:deep( .el-table__row:nth-child(even)) {
+
+:deep(.el-table__row:nth-child(even)) {
   background-color: #ffffff;
 }
-:deep( .el-table td) {
-  padding: 12px 10px;
+
+:deep(.el-table td) {
+  padding: 12px 8px;
   text-align: center;
   border-bottom: 1px solid #ebeef5;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
 }
+
 .action-button {
   font-size: 16px;
   margin-right: 5px;
   border-radius: 50%;
-  transition: transform 0.3s ease, background-color 0.3s ease;
+  transition: all 0.3s ease;
 }
+
 .edit-button {
-  background-color: #409eff;
+  background-color: $primary-color;
   color: #fff;
+  border: none;
 }
+
 .edit-button:hover {
   background-color: #66b1ff;
   transform: scale(1.1);
 }
+
 .delete-button {
-  background-color: #f56c6c;
+  background-color: $error-color;
   color: #fff;
+  border: none;
 }
+
 .delete-button:hover {
   background-color: #f78989;
   transform: scale(1.1);
 }
-.el-form-item__label {
-  width: 150px;
-  font-weight: bold;
+
+.action-buttons {
+  display: flex;
+  justify-content: center;
 }
-.el-form-item__content {
-  margin-left: 150px;
+
+.pagination-wrapper {
+  display: flex;
+  justify-content: center;
+  width: 100%;
+  margin: 25px 0 10px 0;
 }
-.custom-input .el-input__inner {
-  border-radius: 6px;
-  border: 1px solid #dcdcdc;
-  transition: border-color 0.3s ease;
-  padding: 10px;
+
+.pagination-container {
+  display: flex;
+  justify-content: center;
+}
+
+:deep(.el-dialog) {
+  border-radius: 12px !important;
+  overflow: hidden;
+  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15) !important;
+  background: linear-gradient(145deg, #ffffff, #f5f9ff);
+  border: 1px solid #e0e7ff;
+  margin-top: calc(var(--el-dialog-margin-top, 15vh) + 30px) !important;
+}
+
+.dialog-header {
+  position: relative;
+  padding: 20px 24px 10px;
+  text-align: center;
+  background: linear-gradient(90deg, $primary-color, #2d8cf0);
+}
+
+.dialog-header h2 {
+  margin: 0;
+  font-size: 22px;
+  font-weight: 600;
+  color: #fff;
+  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
+}
+
+.header-decoration {
+  height: 4px;
+  background: linear-gradient(90deg, #2d8cf0, $primary-color);
+  border-radius: 2px;
+  margin-top: 12px;
+  width: 60%;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+.custom-dialog-form {
+  padding: 25px 10px 15px 180px;
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  gap: 20px;
+  max-height: calc(100vh - 300px);
+  overflow-y: auto;
+  scrollbar-width: thin;
+  scrollbar-color: #c0c4cc #f1f3f4;
+}
+
+.custom-dialog-form::-webkit-scrollbar {
+  width: 6px;
+}
+
+.custom-dialog-form::-webkit-scrollbar-track {
+  background: #f1f3f4;
+  border-radius: 3px;
+}
+
+.custom-dialog-form::-webkit-scrollbar-thumb {
+  background: #c0c4cc;
+  border-radius: 3px;
+}
+
+.custom-dialog-form::-webkit-scrollbar-thumb:hover {
+  background: #a8abb3;
+}
+
+@media (max-width: 1200px) {
+  .custom-dialog-form {
+    grid-template-columns: 1fr;
+    padding: 25px 30px 15px 30px;
+  }
+}
+
+.custom-form-item {
+  margin-bottom: 0 !important;
+}
+
+:deep(.el-form-item__label) {
+  display: block;
+  text-align: left;
+  margin-bottom: 6px !important;
   font-size: 14px;
+  font-weight: 500;
+  color: #2d3748;
+  padding: 0 !important;
+  line-height: 1.5;
 }
-.custom-input .el-input__inner:focus {
-  border-color: #409eff;
+
+.dialog-footer {
+  display: flex;
+  gap: 16px;
+  justify-content: center;
+  padding: 15px 40px 25px;
 }
+
 .custom-cancel-button,
 .custom-submit-button {
-  border: none;
+  min-width: 120px;
+  height: 40px;
   border-radius: 6px;
-  font-size: 14px;
-  padding: 10px 20px;
-  transition: transform 0.3s ease, background-color 0.3s ease;
-  min-width: 100px;
+  font-size: 15px;
+  font-weight: 500;
+  transition: all 0.3s ease;
+  letter-spacing: 0.5px;
+  border: none;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
 }
+
 .custom-cancel-button {
-  background-color: #f4f4f5;
-  color: #606266;
+  background: linear-gradient(145deg, #f7fafc, #edf2f7);
+  color: #4a5568;
 }
+
 .custom-cancel-button:hover {
-  background-color: #e4e7ed;
-  transform: scale(1.05);
+  background: linear-gradient(145deg, #e2e8f0, #cbd5e0);
+  transform: translateY(-1px);
+  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
 }
+
 .custom-submit-button {
-  background-color: #409eff;
-  color: #fff;
+  background: linear-gradient(145deg, $primary-color, #2d8cf0);
+  color: white;
+  position: relative;
+  overflow: hidden;
+}
+
+.custom-submit-button::before {
+  content: "";
+  position: absolute;
+  top: 0;
+  left: -100%;
+  width: 100%;
+  height: 100%;
+  background: linear-gradient(
+    90deg,
+    transparent,
+    rgba(255, 255, 255, 0.3),
+    transparent
+  );
+  transition: 0.5s;
+}
+
+.custom-submit-button:hover::before {
+  left: 100%;
 }
+
 .custom-submit-button:hover {
-  background-color: #66b1ff;
-  transform: scale(1.05);
+  transform: translateY(-1px);
+  box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
 }
-.pagination-container {
-  margin-top: 30px;
-  text-align: center;
+
+:deep(.import-upload .el-upload-list) {
+  display: none !important;
 }
-.action-buttons {
-  display: flex;
-  justify-content: center;
+
+:deep(.el-table__row:hover) {
+  background-color: #e6f7ff !important;
+  transition: background-color 0.3s;
+}
+
+:deep(.el-table) {
+  border: 1px solid #ebeef5;
+  border-radius: 8px;
+  overflow: hidden;
+}
+
+:deep(.el-table__header-wrapper) {
+  border-bottom: 1px solid #ebeef5;
+}
+
+:deep(.el-table td) {
+  border-right: 1px solid #ebeef5;
+}
+
+:deep(.el-table td:last-child) {
+  border-right: none;
+}
+
+.details-content {
+  line-height: 1.8;
+  font-size: 14px;
+  color: #333;
+}
+
+.error-content {
+  ul {
+    list-style-type: disc;
+    padding-left: 20px;
+    margin-top: 10px;
+    li {
+      margin-bottom: 8px;
+    }
+  }
 }
-</style>
+</style>

+ 123 - 135
src/views/Admin/parameterConfig/ModelSelection.vue

@@ -1,9 +1,15 @@
 <template>
   <div class="model-container">
     <!-- 模型选择下拉菜单 -->
-    <el-dropdown @command="handleModelTypeChange" trigger="click" :hide-on-click="true" style="margin-bottom: 5px">
+    <el-dropdown
+      @command="handleModelTypeChange"
+      trigger="click"
+      :hide-on-click="true"
+      style="margin-bottom: 5px"
+    >
       <span class="el-dropdown-link">
-        {{ selectedModelTypeLabel }}<el-icon class="el-icon--right"><ArrowDown /></el-icon>
+        {{ selectedModelTypeLabel
+        }}<el-icon class="el-icon--right"><ArrowDown /></el-icon>
       </span>
       <template #dropdown>
         <el-dropdown-menu>
@@ -34,16 +40,22 @@
       :data="pagedFilteredModels"
       style="width: 100%"
       border
-      height="calc(100vh - 300px)" 
+      height="calc(100vh - 300px)"
     >
       <el-table-column prop="ModelID" label="序号" min-width="80" />
       <el-table-column prop="Model_name" label="模型名称" min-width="180" />
       <el-table-column prop="Data_type" label="数据类型" min-width="120" />
-      <el-table-column prop="Performance_score" label="性能评分" min-width="120" />
+      <el-table-column
+        prop="Performance_score"
+        label="性能评分"
+        min-width="120"
+      />
       <el-table-column prop="Created_at" label="创建时间" min-width="180" />
       <el-table-column label="操作" min-width="100">
         <template #default="scope">
-          <el-button size="small" @click="selectModel(scope.row)">选择</el-button>
+          <el-button size="small" @click="selectModel(scope.row)"
+            >选择</el-button
+          >
         </template>
       </el-table-column>
     </el-table>
@@ -52,7 +64,7 @@
     <div
       class="demo-pagination-block"
       v-if="selectedModelType && filteredModels.length > 0"
-      style="text-align: center; margin-top: 20px;"
+      style="text-align: center; margin-top: 20px"
     >
       <el-pagination
         v-model:current-page="currentPage"
@@ -65,14 +77,6 @@
       />
     </div>
 
-    <!-- 切换模型按钮 -->
-    <div
-      v-if="selectedModel && !loading"
-      style="text-align: center; margin-top: 5px"
-    >
-      <el-button type="success" size="small" @click="switchModel">切换模型</el-button>
-    </div>
-
     <!-- 当没有选择任何模型类型时显示提示信息 -->
     <div
       v-if="!selectedModelType && !loading && !errorOccurred"
@@ -83,7 +87,12 @@
 
     <!-- 当筛选结果为空时显示提示信息 -->
     <div
-      v-if="selectedModelType && filteredModels.length === 0 && !loading && !errorOccurred"
+      v-if="
+        selectedModelType &&
+        filteredModels.length === 0 &&
+        !loading &&
+        !errorOccurred
+      "
       style="text-align: center; padding-top: 5px"
     >
       没有找到与所选模型类型匹配的数据。
@@ -92,11 +101,11 @@
 </template>
 
 <script lang="ts" setup>
-import { ref, onMounted, computed } from 'vue';
-import axios from 'axios';
-import { ElMessage } from 'element-plus';
-import { ArrowDown } from '@element-plus/icons-vue';
-import { table } from '@/API/menus';
+import { ref, onMounted, computed } from "vue";
+import axios from "axios";
+import { ElMessage, ElMessageBox } from "element-plus";
+import { ArrowDown } from "@element-plus/icons-vue";
+import { table } from "@/API/menus";
 
 interface ModelData {
   ModelID: number;
@@ -130,16 +139,20 @@ const rows = ref<Dataset[]>([]);
 const selectedModelType = ref<string>("");
 const selectedModelTypeLabel = ref<string>("请选择模型类型");
 const selectedModel = ref<ModelData | null>(null);
+const errorOccurred = ref(false);
+const errorMessage = ref("");
+const selectedRows = ref<Dataset[]>([]);
+
 const modelTypes = [
   { value: "reduce", label: "降酸模型(Reduce Model)" },
   { value: "reflux", label: "反酸模型(Reflux Model)" },
 ];
 
-// 分页相关的变量
+// 分页相关
 const currentPage = ref(1);
 const pageSize = ref(10);
 
-// 获取所有模型数据
+// 获取所有模型
 const fetchAllModels = async () => {
   try {
     const requestBody = { table: "Models" };
@@ -149,148 +162,128 @@ const fetchAllModels = async () => {
       selected: false,
     }));
   } catch (error) {
-    let errorMessageVal = "未知错误";
-    if (error && typeof error === 'object' && 'message' in error) {
-      errorMessageVal = (error as { message?: string }).message || errorMessageVal;
-    }
-    console.error("Error fetching data:", errorMessageVal);
-    ElMessage.error("数据加载失败:" + errorMessageVal);
+    let msg = "未知错误";
+    if (error && typeof error === "object" && "message" in error)
+      msg = (error as any).message || msg;
+    console.error("Error fetching models:", msg);
+    ElMessage.error("数据加载失败:" + msg);
     errorOccurred.value = true;
-    errorMessage.value = errorMessageVal;
+    errorMessage.value = msg;
   } finally {
     loading.value = false;
   }
 };
 
-// 获取所有数据集数据
+// 获取所有数据集
 const fetchData = async () => {
   try {
-    const response = await table({ table: 'Datasets' });
+    const response = await table({ table: "Datasets" });
     return response.data.rows;
   } catch (error) {
-    let errorMessageVal = '未知错误';
-    if (error && typeof error === 'object' && 'message' in error) {
-      errorMessageVal = (error as { message?: string }).message || errorMessageVal;
-    }
-    console.error('Error fetching data:', errorMessageVal);
-    throw new Error(errorMessageVal); // 重新抛出带有详细信息的新错误
+    let msg = "未知错误";
+    if (error && typeof error === "object" && "message" in error)
+      msg = (error as any).message || msg;
+    console.error("Error fetching datasets:", msg);
+    throw new Error(msg);
   }
 };
 
-// 加载数据集数据
+// 加载数据集
 const LoadData = async () => {
   try {
-    rows.value = (await fetchData()).map(
-      (row: {
-        Dataset_ID: any;
-        Dataset_name: any;
-        Dataset_description: any;
-        Dataset_type: any;
-        Row_count: any;
-        Status: any;
-        Uploaded_at: any;
-      }) => ({
-        id: row.Dataset_ID,
-        name: row.Dataset_name,
-        description: row.Dataset_description,
-        type: row.Dataset_type,
-        count: row.Row_count,
-        status: row.Status,
-        uploadTime: row.Uploaded_at,
-        selected: false,
-      })
-    );
+    rows.value = (await fetchData()).map((row: any) => ({
+      id: row.Dataset_ID,
+      name: row.Dataset_name,
+      description: row.Dataset_description,
+      type: row.Dataset_type,
+      count: row.Row_count,
+      status: row.Status,
+      uploadTime: row.Uploaded_at,
+      selected: false,
+    }));
   } catch (error) {
-    let errorMessageVal = '未知错误';
-    if (error && typeof error === 'object' && 'message' in error) {
-      errorMessageVal = (error as { message?: string }).message || errorMessageVal;
-    }
-    console.error('Error fetching data:', errorMessageVal);
-    ElMessage.error('数据加载失败:' + errorMessageVal);
+    let msg = "未知错误";
+    if (error && typeof error === "object" && "message" in error)
+      msg = (error as any).message || msg;
+    console.error("Error loading datasets:", msg);
+    ElMessage.error("数据加载失败:" + msg);
     errorOccurred.value = true;
-    errorMessage.value = errorMessageVal;
+    errorMessage.value = msg;
   } finally {
     loading.value = false;
   }
 };
 
-// 根据选择的模型类型计算要展示的数据
+// 过滤模型
 const filteredModels = computed(() => {
   if (!selectedModelType.value) return [];
-
-  // 根据选定的模型类型(降酸或反酸)进行筛选
-  return allModels.value.filter(m => m.Data_type === selectedModelType.value);
+  return allModels.value.filter((m) => m.Data_type === selectedModelType.value);
 });
 
-// 分页后的过滤数据
+// 分页后的模型
 const pagedFilteredModels = computed(() => {
   const start = (currentPage.value - 1) * pageSize.value;
-  const end = start + pageSize.value;
-  return filteredModels.value.slice(start, end);
-});
-
-// 过滤后的数据集数据
-const filteredRows = computed(() =>
-  selectedModelType.value ? rows.value.filter((row) => row.type === selectedModelType.value) : []
-);
-
-// 分页后的过滤数据集数据
-const pagedFilteredRows = computed(() => {
-  const start = (currentPage.value - 1) * pageSize.value;
-  const end = start + pageSize.value;
-  return filteredRows.value.slice(start, end);
+  return filteredModels.value.slice(start, start + pageSize.value);
 });
 
-const handleModelTypeChange = (value: string) => {
+// 切换模型类型
+const handleModelTypeChange = async (value: string) => {
   selectedModelType.value = value;
-  const selectedType = modelTypes.find(type => type.value === value);
-  if (selectedType) {
-    selectedModelTypeLabel.value = selectedType.label;
-    ElMessage.success(`已切换到 ${selectedType.label}`);
-  }
-  selectedModel.value = null; // 清空已选中的模型
-  currentPage.value = 1; // 当模型类型改变时重置当前页码
-};
-
-const selectModel = (model: ModelData) => {
-  selectedModel.value = model;
-};
+  const selectedType = modelTypes.find((t) => t.value === value);
+  if (selectedType) selectedModelTypeLabel.value = selectedType.label;
+  selectedModel.value = null;
+  currentPage.value = 1;
 
-// 分页事件处理
-const handleSizeChange = (val: number) => {
-  pageSize.value = val;
-};
-const handleCurrentChange = (val: number) => {
-  currentPage.value = val;
-};
-
-// 切换模型状态
-const switchModel = async () => {
-  if (!selectedModel.value) {
-    ElMessage.warning("请先选择一个模型");
-    return;
+  // 尝试恢复上次保存的模型
+  try {
+    const resp = await axios.get(
+      `https://www.soilgd.com:5000/model-selection?model_type=${value}`
+    ); //https://www.soilgd.com:5000
+    if (resp.data && resp.data.data && resp.data.data.model_id) {
+      const found = allModels.value.find(
+        (m) => m.ModelID === resp.data.data.model_id
+      );
+      if (found) {
+        selectedModel.value = found;
+        ElMessage.info(`已恢复上次选择的模型:${found.Model_name}`);
+      }
+    }
+  } catch (e) {
+    console.warn("恢复上次模型失败", e);
   }
+};
 
+// 选择并保存模型(弹框确认)
+const selectModel = async (model: ModelData) => {
   try {
-    await axios.post("https://127.0.0.1:5000/switch-model", {
-      model_id: selectedModel.value.ModelID,
-      model_name: selectedModel.value.Model_name,
+    await ElMessageBox.confirm(
+      `确认选择模型:${model.ModelID} 吗?`,
+      "确认操作",
+      { confirmButtonText: "确定", cancelButtonText: "取消", type: "warning" }
+    );
+
+    selectedModel.value = model;
+    //https://www.soilgd.com:5000
+    await axios.post("https://www.soilgd.com:5000/model-selection", {
+      model_type: selectedModelType.value,
+      model_id: model.ModelID,
+      model_name: model.Model_name,
     });
-    ElMessage.success(`模型 ${selectedModel.value.Model_name} 切换成功`);
-    selectedModel.value = null; // 清空已选中的模型
+    ElMessage.success(`已保存选中模型:${model.Model_name}`);
   } catch (error) {
-    let message = "未知错误";
-    if (error && typeof error === "object" && "message" in error) {
-      message = (error as { message?: string }).message || message;
+    if (error !== "cancel" && error !== "close") {
+      let msg = "未知错误";
+      if (error && typeof error === "object" && "message" in error)
+        msg = (error as any).message || msg;
+      console.error("保存模型失败:", msg);
+      ElMessage.error("保存失败:" + msg);
     }
-    console.error("Failed to switch model:", message);
-    ElMessage.error("模型切换失败:" + message);
   }
 };
 
-const errorOccurred = ref(false);
-const errorMessage = ref('');
-const selectedRows = ref<Dataset[]>([]);
+// 分页事件
+const handleSizeChange = (val: number) => (pageSize.value = val);
+const handleCurrentChange = (val: number) => (currentPage.value = val);
 
 onMounted(() => {
   fetchAllModels();
@@ -306,7 +299,7 @@ onMounted(() => {
   background-color: #f9fafb;
   border-radius: 8px;
   box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
-  height: 100%; /* 让容器高度占满父元素 */
+  height: 100%;
 }
 
 .example-showcase .el-dropdown-link {
@@ -316,7 +309,6 @@ onMounted(() => {
   align-items: center;
 }
 
-/* 移除下拉菜单的边框 */
 .el-popper.is-light {
   border: none !important;
   box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
@@ -325,29 +317,25 @@ onMounted(() => {
 .el-table {
   width: 100%;
   margin-top: 10px;
-  border-radius: 8px; /* 添加圆角 */
+  border-radius: 8px;
 }
 
-:deep( .el-table th) {
+:deep(.el-table th) {
   background-color: #61e054 !important;
   color: #fff;
 }
 
-:deep( .el-table td),
-:deep( .el-table th) {
+:deep(.el-table td),
+:deep(.el-table th) {
   padding: 8px 0;
 }
 
-:deep( .el-table__row:nth-child(odd)) {
+:deep(.el-table__row:nth-child(odd)) {
   background-color: #e4fbe5 !important;
 }
 
-/* 禁用项样式 */
-:deep( .el-dropdown-menu__item.is-disabled) {
+:deep(.el-dropdown-menu__item.is-disabled) {
   color: #c0c4cc !important;
   cursor: not-allowed !important;
 }
 </style>
-
-
-

+ 97 - 107
src/views/Admin/parameterConfig/ModelTrain.vue

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

+ 81 - 71
src/views/Admin/parameterConfig/thres.vue

@@ -3,17 +3,17 @@
     <!-- 降酸阈值 -->
     <el-card class="threshold-card">
       <h3>降酸阈值</h3>
-      <p><strong>当前阈值:</strong> {{ currentThresholdReduce }}</p>
+      <p><strong>当前阈值:</strong> {{ currentThresholdReduce ?? "--" }}</p>
 
-      <el-input 
-        v-model="newThresholdReduce" 
-        placeholder="请输入新的降酸阈值" 
+      <el-input
+        v-model="newThresholdReduce"
+        placeholder="请输入新的降酸阈值"
         class="input"
       ></el-input>
 
-      <el-button 
-        type="success" 
-        @click="updateThreshold('reduce')" 
+      <el-button
+        type="success"
+        @click="confirmUpdateThreshold('reduce')"
         class="button"
       >
         更新降酸阈值
@@ -23,17 +23,17 @@
     <!-- 反酸阈值 -->
     <el-card class="threshold-card">
       <h3>反酸阈值</h3>
-      <p><strong>当前阈值:</strong> {{ currentThresholdReflux }}</p>
+      <p><strong>当前阈值:</strong> {{ currentThresholdReflux ?? "--" }}</p>
 
-      <el-input 
-        v-model="newThresholdReflux" 
-        placeholder="请输入新的反酸阈值" 
+      <el-input
+        v-model="newThresholdReflux"
+        placeholder="请输入新的反酸阈值"
         class="input"
       ></el-input>
 
-      <el-button 
-        type="success" 
-        @click="updateThreshold('reflux')" 
+      <el-button
+        type="success"
+        @click="confirmUpdateThreshold('reflux')"
         class="button"
       >
         更新反酸阈值
@@ -43,90 +43,101 @@
 </template>
 
 <script>
-import axios from 'axios';
+import axios from "axios";
+import { ElMessage, ElMessageBox } from "element-plus";
 
 export default {
   data() {
     return {
-      // 当前阈值
       currentThresholdReduce: null,
       currentThresholdReflux: null,
-      // 新增阈值输入框绑定值
-      newThresholdReduce: '',
-      newThresholdReflux: ''
+      newThresholdReduce: "",
+      newThresholdReflux: "",
     };
   },
   methods: {
-    /**
-     * 获取当前阈值信息
-     */
-    fetchThresholds() {
-      axios.get('https://127.0.0.1:5000/get-threshold')
-        .then(response => {
-          const { reduce, reflux } = response.data;
-          this.currentThresholdReduce = reduce.current_threshold;
-          this.currentThresholdReflux = reflux.current_threshold;
-        })
-        .catch(error => {
-          console.error("获取阈值失败:", error);
-          this.$message.error('无法加载阈值,请稍后再试');
-        });
-    },
-
-    /**
-     * 更新阈值
-     * @param {string} dataType - 数据类型 ('reduce' 或 'reflux')
-     */
-    updateThreshold(dataType) {
-      let thresholdValue, targetVariable;
-
-      if (dataType === 'reduce') {
-        thresholdValue = parseFloat(this.newThresholdReduce);
-        targetVariable = 'currentThresholdReduce';
-      } else if (dataType === 'reflux') {
-        thresholdValue = parseFloat(this.newThresholdReflux);
-        targetVariable = 'currentThresholdReflux';
+    // 获取当前阈值
+    async fetchThresholds() {
+      try {
+        const response = await axios.get(
+          "https://www.soilgd.com:5000/get-threshold"
+        ); //https://www.soilgd.com:5000
+        const { reduce, reflux } = response.data;
+        this.currentThresholdReduce = reduce.current_threshold;
+        this.currentThresholdReflux = reflux.current_threshold;
+      } catch (error) {
+        console.error("获取阈值失败:", error);
+        ElMessage.error("无法加载阈值,请稍后再试");
       }
+    },
 
-      if (isNaN(thresholdValue) || thresholdValue <= 0) {
-        this.$message.error('请输入有效的正数');
+    // 确认更新阈值
+    confirmUpdateThreshold(type) {
+      let value =
+        type === "reduce"
+          ? parseFloat(this.newThresholdReduce)
+          : parseFloat(this.newThresholdReflux);
+      if (isNaN(value) || value <= 0) {
+        ElMessage.error("请输入有效的正数");
         return;
       }
 
-      axios.post('https://127.0.0.1:5000/update-threshold', {
-        threshold: thresholdValue,
-        data_type: dataType
-      })
-        .then(response => {
-          if (response.data.success) {
-            this[targetVariable] = thresholdValue; // 动态更新当前阈值
-            this.$message.success(response.data.message);
-          } else {
-            this.$message.error(response.data.error);
-          }
+      ElMessageBox.confirm(
+        `确认将 ${
+          type === "reduce" ? "降酸阈值" : "反酸阈值"
+        } 更新为 ${value} 吗?`,
+        "确认更新",
+        {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning",
+        }
+      )
+        .then(() => {
+          this.updateThreshold(type, value);
         })
-        .catch(error => {
-          console.error("更新阈值失败:", error);
-          this.$message.error('更新阈值时发生错误');
+        .catch(() => {
+          // 用户取消,不做操作
         });
-    }
+    },
+
+    // 更新阈值请求
+    async updateThreshold(type, value) {
+      try {
+        const response = await axios.post(
+          "https://www.soilgd.com:5000/update-threshold",
+          {
+            threshold: value,
+            data_type: type,
+          }
+        ); //'https://www.soilgd.com:5000
+        if (response.data.success) {
+          if (type === "reduce") this.currentThresholdReduce = value;
+          else this.currentThresholdReflux = value;
+
+          ElMessage.success(response.data.message);
+        } else {
+          ElMessage.error(response.data.error || "更新失败");
+        }
+      } catch (error) {
+        console.error("更新阈值失败:", error);
+        ElMessage.error("更新阈值时发生错误");
+      }
+    },
   },
   mounted() {
-    // 页面加载时获取当前阈值
     this.fetchThresholds();
-  }
+  },
 };
 </script>
 
 <style scoped>
-/* 容器样式 */
 .container {
   display: flex;
   justify-content: space-around;
   gap: 20px;
 }
 
-/* 卡片样式 */
 .threshold-card {
   flex: 1;
   max-width: 400px;
@@ -135,7 +146,6 @@ export default {
   padding: 16px;
 }
 
-/* 输入框和按钮样式 */
 .input {
   width: 100%;
   margin-top: 10px;
@@ -145,4 +155,4 @@ export default {
   width: 100%;
   margin-top: 10px;
 }
-</style>
+</style>

+ 107 - 52
src/views/Admin/userManagement/UserManagement.vue

@@ -3,7 +3,7 @@
     <el-card class="box-card">
       <h2>用户管理</h2>
 
-      <el-table :data="users" style="width: 100%" border>
+      <el-table :data="users" style="width: 100%" border v-loading="tableLoading">
         <el-table-column prop="name" label="用户名" width="180" />
         <el-table-column prop="userType" label="用户类型" width="120" :formatter="typeFormatter" />
         <el-table-column prop="created_at" label="注册时间" />
@@ -16,21 +16,23 @@
       </el-table>
     </el-card>
 
-    <!-- 编辑用户 Dialog -->
     <el-dialog
       title="编辑用户"
       v-model="showDialog"
       :modal-append-to-body="false"
       :style="{ top: '120px' }"
+      :before-close="handleDialogClose"
     >
       <el-form :model="form" ref="formRef" label-width="100px">
-        <el-form-item label="用户名">
+        <el-form-item label="用户名" prop="name" :rules="[{ required: true, message: '用户名不能为空', trigger: 'blur' }]">
           <el-input v-model="form.name" />
         </el-form-item>
-        <el-form-item label="密码">
+        <el-form-item label="密码" prop="password" :rules="[
+          { min: 3, message: '密码长度至少为3个字符', trigger: 'blur' }
+        ]">
           <el-input type="password" v-model="form.password" placeholder="留空则不修改密码" />
         </el-form-item>
-        <el-form-item label="用户类型">
+        <el-form-item label="用户类型" prop="userType" :rules="[{ required: true, message: '请选择用户类型', trigger: 'change' }]">
           <el-select v-model="form.userType" placeholder="请选择">
             <el-option label="普通用户" value="user" />
             <el-option label="管理员" value="admin" />
@@ -39,16 +41,18 @@
       </el-form>
 
       <template #footer>
-        <el-button @click="showDialog = false">取消</el-button>
-        <el-button type="primary" @click="onSubmit">确定</el-button>
+        <span class="dialog-footer">
+          <el-button @click="handleDialogClose">取消</el-button>
+          <el-button type="primary" @click="onSubmit" :loading="submitLoading">确定</el-button>
+        </span>
       </template>
     </el-dialog>
   </div>
 </template>
 
 <script setup lang="ts">
-import { ref, reactive, onMounted } from "vue";
-import { ElMessage, ElForm } from "element-plus";
+import { ref, reactive, onMounted, nextTick } from "vue";
+import { ElMessage, ElForm, ElMessageBox } from "element-plus";
 import { getUsers, updateUser, deleteUser } from "@/API/users";
 
 interface User {
@@ -69,15 +73,15 @@ const form = reactive({
   userType: "user",
 });
 
-// 用户类型映射函数
+const tableLoading = ref(false);
+const submitLoading = ref(false);
+
 const typeFormatter = (row: any, column: any, cellValue: string) => {
   if (cellValue === "admin") return "管理员";
   if (cellValue === "user") return "普通用户";
   return cellValue;
 };
 
-
-// 格式化注册时间
 const formatTime = (timeStr: string) => {
   const date = new Date(timeStr);
   const yyyy = date.getFullYear();
@@ -89,8 +93,8 @@ const formatTime = (timeStr: string) => {
   return `${yyyy}-${MM}-${dd} ${hh}:${mm}:${ss}`;
 };
 
-// 加载用户列表
 const loadUsers = async () => {
+  tableLoading.value = true;
   try {
     const res = await getUsers();
     users.value = res.data.users.map((u: User) => ({
@@ -98,61 +102,106 @@ const loadUsers = async () => {
       created_at: formatTime(u.created_at),
     }));
   } catch (err: any) {
-    ElMessage.error("加载用户失败");
-    console.error(err);
+    ElMessage.error("加载用户失败: " + (err.message || err.response?.data?.detail || '未知错误'));
+    console.error("加载用户失败:", err);
+  } finally {
+    tableLoading.value = false;
   }
 };
 
-// 编辑用户
 const onEdit = (user: User) => {
   editUser.value = user;
   form.name = user.name;
   form.userType = user.userType;
   form.password = "";
-  showDialog.value = true;
+
+  nextTick(() => {
+    showDialog.value = true;
+    if (formRef.value) {
+       formRef.value.clearValidate();
+    }
+  });
 };
 
-// 删除用户
 const onDelete = async (user: User) => {
-  try {
-    await deleteUser(user.id);
-    ElMessage.success("用户删除成功");
-    loadUsers();
-  } catch (err: any) {
-    ElMessage.error(err.response?.data?.detail || "删除失败");
-    console.error(err);
-  }
+  ElMessageBox.confirm(
+    `确定要删除用户 "${user.name}" 吗?此操作不可恢复。`,
+    '确认删除',
+    {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning',
+    }
+  )
+  .then(async () => {
+    try {
+      await deleteUser(user.id);
+      ElMessage.success("用户删除成功");
+      loadUsers();
+    } catch (err: any) {
+      ElMessage.error(err.response?.data?.detail || "删除失败");
+      console.error("删除用户失败:", err);
+    }
+  })
+  .catch(() => {
+  });
 };
 
-// 提交编辑(三选一校验)
 const onSubmit = async () => {
-  if (!editUser.value) return;
-
-  // 检查是否至少修改一项
-  const changed = form.name !== editUser.value.name ||
-                  form.password ||
-                  form.userType !== editUser.value.userType;
+  if (!editUser.value || !formRef.value) return;
+
+  formRef.value.validate(async (valid) => {
+    if (valid) {
+
+      const changed = form.name !== editUser.value!.name ||
+                      form.password.trim() !== '' ||
+                      form.userType !== editUser.value!.userType;
+
+      if (!changed) {
+        ElMessage.warning("请至少修改用户名、密码或用户类型中的一项");
+        return;
+      }
+
+      submitLoading.value = true;
+      try {
+        const data: any = {};
+        if (form.name !== editUser.value!.name) data.name = form.name.trim();
+        if (form.password.trim()) data.password = form.password.trim();
+        if (form.userType !== editUser.value!.userType) data.userType = form.userType;
+
+        await updateUser(editUser.value!.id, data);
+        ElMessage.success("用户更新成功");
+        showDialog.value = false;
+        editUser.value = null;
+        loadUsers();
+      } catch (err: any) {
+        ElMessage.error(err.response?.data?.detail || "操作失败");
+        console.error("更新用户失败:", err);
+      } finally {
+        submitLoading.value = false;
+      }
+    } else {
+      console.log('表单校验失败!');
+    }
+  });
+};
 
-  if (!changed) {
-    ElMessage.warning("请至少修改用户名、密码或用户类型中的一项");
-    return;
-  }
+const handleDialogClose = (done: () => void) => {
+    resetForm();
+    done();
+};
 
-  try {
-    const data: any = {};
-    if (form.name !== editUser.value.name) data.name = form.name;
-    if (form.password) data.password = form.password;
-    if (form.userType !== editUser.value.userType) data.userType = form.userType;
-
-    await updateUser(editUser.value.id, data);
-    ElMessage.success("用户更新成功");
-    showDialog.value = false;
-    editUser.value = null;
-    loadUsers();
-  } catch (err: any) {
-    ElMessage.error(err.response?.data?.detail || "操作失败");
-    console.error(err);
-  }
+const resetForm = () => {
+    form.name = '';
+    form.password = '';
+    form.userType = 'user';
+
+    nextTick(() => {
+        if (formRef.value) {
+            formRef.value.resetFields();
+            formRef.value.clearValidate();
+        }
+    });
 };
 
 onMounted(loadUsers);
@@ -165,4 +214,10 @@ onMounted(loadUsers);
   align-items: center;
   margin-bottom: 20px;
 }
+.dialog-footer {
+  text-align: right;
+}
 </style>
+
+
+

+ 77 - 142
src/views/User/acidModel/Calculation.vue

@@ -5,115 +5,74 @@
         <span>反酸模型</span>
       </div>
     </template>
+
+    <div class="model-info">
+      <p><strong>当前选中模型 ID:</strong> {{ selectedModelId }}</p>
+    </div>
+
     <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-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-input>
       </el-form-item>
 
-      <el-form-item
-        label="土壤粘粒(g/kg)"
-        prop="CL"
-        :error="errorMessages.CL"
-        required
-      >
+      <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-input>
       </el-form-item>
 
-      <!-- 修改:将pH改为CEC -->
-      <el-form-item
-        label="阳离子交换量(cmol/kg)"
-        prop="CEC"
-        :error="errorMessages.CEC"
-        required
-      >
+      <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-input>
       </el-form-item>
 
-      <el-form-item
-        label="交换性氢(cmol/kg)"
-        prop="H_plus"
-        :error="errorMessages.H_plus"
-        required
-      >
+      <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-input>
       </el-form-item>
 
-      <el-form-item
-        label="水解氮(g/kg)"
-        prop="N"
-        :error="errorMessages.N"
-        required
-      >
+      <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-input>
       </el-form-item>
 
-      <el-form-item
-        label="交换性铝(cmol/kg)"
-        prop="Al3_plus"
-        :error="errorMessages.Al3_plus"
-        required
-      >
+      <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-input>
       </el-form-item>
 
-      <el-button type="primary" @click="onSubmit" class="onSubmit"
-        >计算</el-button
-      >
-
-      <el-dialog
-        v-model="dialogVisible"
-        @close="onDialogClose"
-        :close-on-click-modal="false"
-        width="500px"
-        align-center
-        title="计算结果"
-      >
+      <el-button type="primary" @click="onSubmit" class="onSubmit">计算</el-button>
+
+      <el-dialog v-model="dialogVisible" @close="onDialogClose" :close-on-click-modal="false" width="500px" align-center title="计算结果">
         <span class="dialog-class">pH值: {{ result }}</span>
         <template #footer>
           <el-button @click="dialogVisible = false">关闭</el-button>
@@ -124,16 +83,15 @@
 </template>
 
 <script setup lang="ts">
-import { reactive, ref, nextTick } from "vue";
+import { reactive, ref, nextTick, onMounted } from "vue";
 import { ElMessage } from "element-plus";
 import { api5000 } from "../../../utils/request"; // 使用api5000
 
-
-// 定义表单数据接口
+// 表单接口
 interface Form {
   OM: number | null;
   CL: number | null;
-  CEC: number | null; // 修改:将pH改为CEC
+  CEC: number | null;
   H_plus: number | null;
   N: number | null;
   Al3_plus: number | null;
@@ -142,7 +100,7 @@ interface Form {
 const form = reactive<Form>({
   OM: null,
   CL: null,
-  CEC: null, // 修改:将pH改为CEC
+  CEC: null,
   H_plus: null,
   N: null,
   Al3_plus: null,
@@ -154,19 +112,18 @@ const predictForm = ref<any>(null);
 const errorMessages = reactive<Record<string, string>>({
   OM: "",
   CL: "",
-  CEC: "", // 修改:将pH改为CEC
+  CEC: "",
   H_plus: "",
   N: "",
   Al3_plus: "",
 });
 
-// 限制输入为数字并校验范围
-const handleInput = (
-  field: keyof Form,
-  event: Event,
-  min: number,
-  max: number
-) => {
+// 当前选中模型
+const selectedModelId = ref<number>(24); // 默认值
+const selectedModelName = ref<string>("默认模型");
+
+// 输入校验
+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;
@@ -177,11 +134,9 @@ const handleInput = (
   }
 
   const numValue = parseFloat(value);
-  if (isNaN(numValue) || numValue < min || numValue > max) {
-    errorMessages[field] = `输入值应在 ${min} 到 ${max} 之间且为有效数字`;
-  } else {
-    errorMessages[field] = "";
-  }
+  errorMessages[field] = (isNaN(numValue) || numValue < min || numValue > max)
+    ? `输入值应在 ${min} 到 ${max} 之间且为有效数字`
+    : "";
 };
 
 const validateInput = (value: string, min: number, max: number): boolean => {
@@ -189,12 +144,26 @@ const validateInput = (value: string, min: number, max: number): boolean => {
   return !isNaN(numValue) && numValue >= min && numValue <= max;
 };
 
+// 获取当前保存的模型
+const fetchSelectedModel = async () => {
+  try {
+    const resp = await api5000.post('/model-selection?model_type=reflux');
+    if (resp.data && resp.data.data) {
+      selectedModelId.value = resp.data.data.model_id;
+      selectedModelName.value = resp.data.data.model_name;
+    }
+  } catch (error) {
+    console.warn("获取当前模型失败,使用默认 model_id:", selectedModelId.value);
+  }
+};
+
 // 计算方法
 const onSubmit = async () => {
+  // 输入校验
   const inputConfigs = [
     { field: "OM" as keyof Form, min: 0, max: 30 },
     { field: "CL" as keyof Form, min: 50, max: 400 },
-    { field: "CEC" as keyof Form, min: 0, max: 15 }, // 修改:将pH改为CEC
+    { 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 },
@@ -217,83 +186,55 @@ const onSubmit = async () => {
     return;
   }
 
-  console.log("开始计算...");
   const data = {
-    model_id: 24,
+    model_id: selectedModelId.value,
     parameters: {
       OM: form.OM,
       CL: form.CL,
-      CEC: form.CEC, // 修改:将pH改为CEC
+      CEC: form.CEC,
       H_plus: form.H_plus,
       N: form.N,
       Al3_plus: form.Al3_plus,
     },
   };
-
   try {
     const response = await api5000.post('/predict', data, {
       headers: {
         "Content-Type": "application/json",
       },
     });
-    
-    if (response.data?.result?.length > 0) {
+    if (response.data && response.data.result && response.data.result.length > 0) {
       result.value = parseFloat(response.data.result[0].toFixed(2));
       dialogVisible.value = true;
     } else {
-      throw new Error("未获取到有效的预测结果");
+      ElMessage.error("未获取到有效的预测结果");
     }
-    dialogVisible.value = true;
   } catch (error: any) {
-    console.error("请求失败详情:", error);
-    
-    let errorMessage = "请求失败";
-    if (error.code === "ECONNABORTED") {
-      errorMessage = "请求超时,请稍后再试";
-    } else if (error.response) {
-      // 服务器有响应但状态码不在2xx范围
-      errorMessage = `请求失败,状态码: ${error.response.status}`;
-      if (error.response.data?.detail) {
-        errorMessage += ` - ${error.response.data.detail}`;
-      }
+    console.error("请求失败:", error);
+    if (error.response) {
+      ElMessage.error(`请求失败,状态码: ${error.response.status}`);
     } else if (error.request) {
-      // 请求已发出但没有收到响应
-      errorMessage = "无法连接到服务器,请检查网络连接";
-      if (error.message.includes("Network Error")) {
-        errorMessage = "网络错误,请检查后端服务是否运行";
-      }
+      ElMessage.error("请求发送成功,但没有收到响应");
+    } else {
+      ElMessage.error("请求过程中发生错误: " + error.message);
     }
-    
-    ElMessage.error(errorMessage);
   }
 };
 
-// 监听弹窗关闭事件,关闭后初始化值
+// 弹框关闭后重置表单
 const onDialogClose = () => {
   dialogVisible.value = false;
-
-  // 重置表单
-  Object.keys(form).forEach((key) => {
-    if (key in form) {
-      const typedKey = key as keyof Form;
-      form[typedKey] = null;
-    }
-  });
-
-  // 重置错误消息
-  Object.keys(errorMessages).forEach((key) => {
-    if (key in errorMessages) {
-      const typedKey = key as keyof typeof errorMessages;
-      errorMessages[typedKey] = "";
-    }
-  });
-
+  Object.keys(form).forEach((key) => (form[key as keyof Form] = null));
+  Object.keys(errorMessages).forEach((key) => (errorMessages[key as keyof typeof errorMessages] = ""));
   nextTick(() => {
-    if (predictForm.value) {
-      (predictForm.value as any).resetFields();
-    }
+    if (predictForm.value) (predictForm.value as any).resetFields();
   });
 };
+
+// 页面挂载时获取当前模型
+onMounted(() => {
+  fetchSelectedModel();
+});
 </script>
 
 <style scoped>
@@ -313,6 +254,13 @@ const onDialogClose = () => {
   font-size: 25px;
 }
 
+.model-info {
+  text-align: center;
+  margin-bottom: 20px;
+  font-size: 16px;
+  color: #555;
+}
+
 .el-form-item {
   margin-bottom: 20px;
 }
@@ -345,20 +293,7 @@ const onDialogClose = () => {
   display: flex;
   justify-content: center;
   align-items: center;
-  height: 100%; /* 确保容器占满对话框内容区域 */
+  height: 100%;
   font-size: 20px;
 }
-
-@media (max-width: 576px) {
-  .el-form {
-    --el-form-label-width: 100px;
-  }
-}
-
-/* 自定义 tooltip 样式 */
-:deep(.el-tooltip__popper.is-light) {
-  background-color: white;
-  border-color: #dcdfe6;
-  color: #606266;
-}
-</style>
+</style>

+ 0 - 1
src/views/User/acidModel/ModelIterationVisualization.vue

@@ -345,4 +345,3 @@ onUnmounted(() => {
 </style>
 
 
-

+ 50 - 6
src/views/User/neutralizationModel/AcidNeutralizationModel.vue

@@ -6,6 +6,11 @@
       </div>
     </template>
 
+    
+    <div class="model-info">
+      <p><strong>当前选中模型 ID:</strong> {{ selectedModelId }}</p>
+    </div>
+
     <el-form
       :model="form"
       ref="predictForm"
@@ -134,10 +139,10 @@
 </template>
 
 <script setup lang="ts">
-import { reactive, ref, nextTick } from "vue";
+import { reactive, ref, nextTick, onMounted } from "vue";
 import { ElMessage } from 'element-plus';
 import axios from 'axios';
-import { api5000 } from '../../../utils/request'; // 修改导入
+import { api5000 } from "../../../utils/request"; // 使用api5000
 
 const form = reactive<{
   init_pH: number | null,
@@ -167,6 +172,10 @@ const errorMessages = reactive({
   Al: '',
 });
 
+// 当前选中模型
+const selectedModelId = ref<number>(6); // 默认值
+const selectedModelName = ref<string>("默认模型");
+
 // 使用 ref 来存储输入框元素
 const inputRefs = reactive<{
   [key in keyof typeof form]: any;
@@ -221,6 +230,34 @@ const validateInput = (value: string | number, min: number, max: number): boolea
   return numValue >= min && numValue <= max;
 };
 
+// 获取当前保存的模型
+const fetchSelectedModel = async () => {
+  try {
+    const resp = await api5000.post('/model-selection?model_type=reflux');
+    // 如果接口返回了有效数据,使用它;否则使用默认
+    if (resp.data && resp.data.data && resp.data.data.model_id) {
+      selectedModelId.value = resp.data.data.model_id;
+      selectedModelName.value = resp.data.data.model_name;
+    } else {
+      // 接口没有返回选中模型,使用默认值
+      selectedModelId.value = 6;
+      selectedModelName.value = "默认降酸模型";
+      console.warn("未获取到选中模型,使用默认 model_id=6");
+    }
+  } catch (error) {
+    // 接口请求失败,也使用默认
+    selectedModelId.value = 6;
+    selectedModelName.value = "默认降酸模型";
+    console.warn("获取当前模型失败,使用默认 model_id=6:", error);
+  }
+};
+
+// 页面挂载时调用
+onMounted(() => {
+  fetchSelectedModel();
+});
+
+
 // 计算方法
 const onSubmit = async () => {
   const inputConfigs = [
@@ -259,7 +296,7 @@ const onSubmit = async () => {
 
   console.log('开始计算...');
   const data = {
-    model_id: 6,
+    model_id: selectedModelId.value,
     parameters: {
       init_pH: form.init_pH,
       target_pH: form.target_pH,
@@ -269,12 +306,12 @@ const onSubmit = async () => {
       Al: form.Al,
     }
   };
-  console.log('提交的数据:', data);
+  console.log('提交的数据:', data);//https://www.soilgd.com:5000
   try {
     const response = await api5000.post('/predict', data, {
       headers: {
-        'Content-Type': 'application/json'
-      }
+        "Content-Type": "application/json",
+      },
     });
     console.log('预测结果:', response.data);
     if (response.data && typeof response.data.result === 'number') {
@@ -350,6 +387,13 @@ const onDialogClose = () => {
   color: #666;
 }
 
+.model-info {
+  text-align: center;
+  margin-bottom: 20px;
+  font-size: 16px;
+  color: #555;
+}
+
 .el-input {
   width: 80%;
 }

+ 249 - 119
src/views/login/loginView.vue

@@ -15,26 +15,19 @@
         class="login-form"
       >
         <div class="form-header">
-          <h2 class="form-title">
-            {{
-              userType === "user" ? t("login.userTitle") : t("login.adminTitle")
-            }}
-          </h2>
-          <el-button class="user-type-toggle" @click="toggleUserType" link>
-            <el-icon><User /></el-icon>
-            <span>{{ currentUserTypeName }}</span>
-          </el-button>
+          <h2 class="form-title">{{ t("login.userTitle") }}</h2>
         </div>
 
-        <div class="input-frame">
+        <div class="input-frame login-input-frame">
           <el-form-item label="账号:" prop="name">
             <el-input v-model="form.name" />
           </el-form-item>
         </div>
 
-        <div class="input-frame">
+        <div class="input-frame login-input-frame">
           <el-form-item label="密码:" prop="password">
-            <el-input type="password" v-model="form.password" />
+            <!-- 使用 Element Plus 的密码输入框 -->
+            <el-input v-model="form.password" type="password" show-password />
           </el-form-item>
         </div>
 
@@ -74,38 +67,48 @@
         ref="registerFormRef"
         :model="registerForm"
         :rules="registerRules"
-        label-width="116px"
-        class="login-form"
+        class="login-form register-form" 
       >
         <div class="form-header">
           <h2 class="form-title">{{ t("register.title") }}</h2>
-          <el-button class="user-type-toggle" @click="toggleUserType" link>
-            <el-icon><User /></el-icon>
-            <span>{{ currentUserTypeName }}</span>
-          </el-button>
         </div>
 
-        <div class="input-frame">
+        <!-- 调整后的注册账号输入框 -->
+        <div class="input-frame register-input-frame register-account-frame">
           <el-form-item label="账号:" prop="name">
             <el-input v-model="registerForm.name" />
           </el-form-item>
         </div>
 
-        <div class="input-frame">
+        <!-- 调整后的注册密码输入框 -->
+        <div class="input-frame register-input-frame register-password-frame">
           <el-form-item label="密码:" prop="password">
-            <el-input type="password" v-model="registerForm.password" />
+            <!-- 使用 Element Plus 的密码输入框 -->
+            <el-input
+              v-model="registerForm.password"
+              type="password"
+              show-password
+            />
           </el-form-item>
         </div>
 
-        <div class="input-frame">
-          <el-form-item label="确认密码:" prop="confirmPassword">
-            <el-input type="password" v-model="registerForm.confirmPassword" />
+        <div class="input-frame register-input-frame">
+          <el-form-item 
+            label="确认密码:" 
+            prop="confirmPassword"
+          >
+            <!-- 使用 Element Plus 的密码输入框 -->
+            <el-input
+              v-model="registerForm.confirmPassword"
+              type="password"
+              show-password
+            />
           </el-form-item>
         </div>
 
-        <!-- 错误提示区域 -->
-        <div v-if="showError" class="error-message">
-          {{ errorMessage }}
+        <!-- 成功/错误提示区域 -->
+        <div v-if="showMessage" :class="messageType">
+          {{ messageContent }}
         </div>
 
         <div class="language-toggle-wrapper">
@@ -120,7 +123,7 @@
               type="primary"
               @click="onRegister"
               :loading="loading"
-              class="login-button"
+              class="login-button register-button-adjusted"
             >
               {{ t("register.registerButton") }}
             </el-button>
@@ -138,12 +141,12 @@
 
 <script setup lang="ts">
 import { reactive, ref, computed, watch, onMounted } from "vue";
-import { ElForm } from "element-plus";
+import { ElForm, ElMessage } from "element-plus";
 import type { FormRules } from "element-plus";
 import { login, register } from "@/API/users";
+import { changeLanguageAPI } from "@/API/admin";
 import { useTokenStore } from "@/stores/mytoken";
 import { useI18n } from "vue-i18n";
-import { User } from "@element-plus/icons-vue";
 import { useRouter } from "vue-router";
 
 // ============ 类型定义 ============
@@ -164,11 +167,15 @@ const router = useRouter();
 
 // ============ 状态 ============
 const isLogin = ref(true);
-const userType = ref<"user" | "admin">("user");
 const loading = ref(false);
 const showError = ref(false);
 const errorMessage = ref("");
 
+// 新增状态用于注册成功/失败提示
+const showMessage = ref(false);
+const messageContent = ref("");
+const messageType = ref<"success-message" | "error-message">("error-message"); // 'success-message' 或 'error-message'
+
 const form = reactive<LoginForm>({ name: "", password: "" });
 const registerForm = reactive<RegisterForm>({
   name: "",
@@ -183,38 +190,76 @@ const registerFormRef = ref<InstanceType<typeof ElForm> | null>(null);
 const toggleForm = () => {
   isLogin.value = !isLogin.value;
   showError.value = false; // 切换表单时隐藏错误提示
+  showMessage.value = false; // 切换表单时也隐藏注册消息
 };
-const toggleUserType = () => {
-  userType.value = userType.value === "user" ? "admin" : "user";
-};
-const toggleLanguage = () => {
-  locale.value = locale.value === "zh" ? "en" : "zh";
-  localStorage.setItem("lang", locale.value);
+
+/**
+ * 切换语言并通知后端
+ */
+const toggleLanguage = async () => {
+  // 计算新的语言
+  const newLocale = locale.value === "zh" ? "en" : "zh";
+  
+  // 更新本地语言设置
+  locale.value = newLocale;
+  localStorage.setItem("lang", newLocale);
+
+  try {
+    // 发送语言切换信息到后端
+    const response = await changeLanguageAPI({
+      language: newLocale,
+      userId: store.userInfo?.userId, // 修复:使用 userInfo?.userId
+      timestamp: Date.now(), // 添加时间戳防止重复请求
+    });
+
+    if (!response.success) {
+      console.warn('语言切换通知后端失败:', response.message);
+      // 即使后端请求失败,前端语言切换仍然生效
+    } else {
+      console.log('语言切换已通知后端:', newLocale);
+      // 可选:如果后端需要返回特定数据,可以处理响应
+      if (response.message) {
+        console.log('后端响应:', response.message);
+      }
+    }
+  } catch (error) {
+    console.error('发送语言切换请求到后端时出错:', error);
+    // 错误处理,但不影响前端语言切换
+  }
 };
 
 // ============ 计算属性 ============
 const currentLanguageName = computed(() =>
   locale.value === "zh" ? "English" : "中文"
 );
-const currentUserTypeName = computed(() =>
-  userType.value === "user" ? t("login.switchToAdmin") : t("login.switchToUser")
-);
 
-// ============ 显示错误消息 ============
+// ============ 显示错误消息 (保留原逻辑) ============
 const showErrorMsg = (message: string) => {
   errorMessage.value = message;
   showError.value = true;
-  
+
   // 3秒后自动隐藏消息
   setTimeout(() => {
     showError.value = false;
   }, 3000);
 };
 
+// ============ 显示通用消息 (新增) ============
+const showMsg = (content: string, type: "success" | "error" = "success") => {
+  messageContent.value = content;
+  messageType.value = type === "success" ? "success-message" : "error-message";
+  showMessage.value = true;
+
+  // 3秒后自动隐藏消息
+  setTimeout(() => {
+    showMessage.value = false;
+  }, 3000);
+};
+
 // ============ 登录逻辑 ============
 const onSubmit = async () => {
   if (!formRef.value) return;
-  
+
   try {
     // 验证表单
     await formRef.value.validate();
@@ -225,45 +270,63 @@ const onSubmit = async () => {
     const response = await login({
       name: form.name,
       password: form.password,
-      usertype: userType.value,
     });
 
-    console.log('完整登录响应:', response);
+    console.log("完整登录响应:", response);
 
     // 检查响应结构
     if (!response.data?.user) {
-      throw new Error('后端返回的用户信息不完整');
+      throw new Error("后端返回的用户信息不完整");
     }
 
     // 提取用户信息
     const userData = response.data.user;
 
     if (!userData.id || !userData.name) {
-      throw new Error('缺少必要的用户字段');
+      throw new Error("缺少必要的用户字段");
     }
 
-    // 保存用户信息到store
+    // --- 修改部分开始 (解决 TypeScript 错误) ---
+    // 确定 loginType 的值
+    // 假设后端返回的字段是 userType
+    let loginTypeFromResponse = userData.userType;
+
+    // 如果后端没有返回 userType,使用默认值 'user' 以避免程序崩溃
+    // 你可以根据实际业务需求调整默认值或处理逻辑
+    if (!loginTypeFromResponse) {
+       console.warn("后端未返回 userType,使用默认值 'user'");
+       loginTypeFromResponse = 'user';
+    }
+
+    // 保存用户信息到store (包含必需的 loginType)
     store.saveToken({
       userId: Number(userData.id),
       name: userData.name,
-      loginType: userData.userType || userType.value, // 优先使用后端返回的userType
+      loginType: loginTypeFromResponse, // 确保提供 loginType
+      // 如果 UserInfo 还有其他必需字段,也需要在这里提供
     });
+    // --- 修改部分结束 ---
 
-    // 跳转到目标页面
-    await router.push({ name: 'samplingMethodDevice1' });
+    // 使用 Element Plus 的成功提示
+    ElMessage.success(t("login.loginSuccess"));
 
+    // 跳转到目标页面
+    await router.push({ name: "samplingMethodDevice1" });
   } catch (error: any) {
-    console.error('登录失败:', {
+    console.error("登录失败:", {
       error: error.message,
       stack: error.stack,
-      response: error.response?.data
+      response: error.response?.data,
     });
-    
+
     // 显示错误消息
-    const errorMsg = error.response?.data.message || 
-                    error.message || 
-                    '登录失败,请检查用户名和密码';
+    const errorMsg =
+      error.response?.data?.message ||
+      error.message ||
+      t("login.loginFailed"); // 使用翻译键
     showErrorMsg(errorMsg);
+    // 使用 Element Plus 的错误提示
+    ElMessage.error(errorMsg);
   } finally {
     loading.value = false;
   }
@@ -276,55 +339,48 @@ const onRegister = async () => {
     await registerFormRef.value.validate();
     loading.value = true;
     showError.value = false; // 开始验证时隐藏错误提示
+    showMessage.value = false; // 开始验证时隐藏消息
 
+    // 调用注册API
     const res = await register({
       name: registerForm.name,
       password: registerForm.password,
-      userType: userType.value,
     });
 
+    // 注册成功
     if (res.data?.message) {
-      // 注册成功后自动登录
-      try {
-        // 调用登录API
-        const loginResponse = await login({
-          name: registerForm.name,
-          password: registerForm.password,
-          usertype: userType.value,
-        });
-
-        // 检查登录响应结构
-        if (loginResponse.data?.user) {
-          const userData = loginResponse.data.user;
-          
-          // 保存用户信息到store
-          store.saveToken({
-            userId: Number(userData.id),
-            name: userData.name,
-            loginType: userData.userType || userType.value,
-          });
-
-          // 跳转到目标页面
-          await router.push({ name: 'samplingMethodDevice1' });
-        } else {
-          showErrorMsg(loginResponse.data?.message || '自动登录失败,请手动登录');
-          toggleForm(); // 返回登录页面
-        }
-      } catch (loginError: any) {
-        console.error('自动登录失败:', loginError);
-        showErrorMsg(
-          loginError?.response?.data?.message || '自动登录失败,请手动登录'
-        );
-        toggleForm(); // 返回登录页面
-      }
+      // 显示注册成功消息
+      showMsg(t("register.registerSuccess"), "success");
+      ElMessage.success(t("register.registerSuccess"));
+
+      // 延迟一段时间后自动跳转到登录页并填充信息
+      setTimeout(() => {
+        // 切换到登录表单
+        isLogin.value = true;
+
+        // 将注册的用户名和密码填入登录表单
+        form.name = registerForm.name;
+        form.password = registerForm.password; // 填充密码
+
+        // 清空注册表单
+        registerForm.name = "";
+        registerForm.password = "";
+        registerForm.confirmPassword = "";
+
+        // 可选:给用户一个提示
+        ElMessage.info(t("register.autoLoginPrompt"));
+      }, 1500); // 1.5秒后执行
     } else {
-      showErrorMsg(res.data?.message || t("register.registerFailed"));
+      const errorMsg = res.data?.message || t("register.registerFailed");
+      showMsg(errorMsg, "error");
+      ElMessage.error(errorMsg);
     }
   } catch (error: any) {
     console.error("注册异常:", error);
-    showErrorMsg(
-      error?.response?.data?.message || t("register.registerFailed")
-    );
+    const errorMsg =
+      error?.response?.data?.message || t("register.registerFailed");
+    showMsg(errorMsg, "error");
+    ElMessage.error(errorMsg);
   } finally {
     loading.value = false;
   }
@@ -383,6 +439,7 @@ watch(locale, () => {
   rules = newRules.rules;
   registerRules = newRules.registerRules;
 });
+
 watch(
   () => registerForm.password,
   () => {
@@ -394,7 +451,6 @@ onMounted(() => {
   console.log("登录/注册页初始化", {
     form,
     registerForm,
-    userType: userType.value,
     locale: locale.value,
   });
 });
@@ -439,14 +495,6 @@ onMounted(() => {
   font-weight: 600;
   color: #333;
 }
-.user-type-toggle {
-  font-size: 36px;
-  color: #333;
-}
-.user-type-toggle span {
-  margin-left: 6px;
-  font-size: 36px;
-}
 .language-toggle-wrapper {
   text-align: right;
   margin: 15px 0 20px;
@@ -458,19 +506,33 @@ onMounted(() => {
   font-size: 24px;
   padding-bottom: 8px;
   color: #7e7878;
+  white-space: nowrap; /* 防止标签内文字换行 */
+  overflow: hidden;
+  text-overflow: ellipsis;
 }
-:deep(.el-input .el-input__inner) {
+/* 更新输入框样式以适应 Element Plus 的结构 */
+:deep(.el-input__wrapper) {
+  box-shadow: 0 0 0 1px #dcdfe6 inset; /* 模拟之前的边框 */
+  border-radius: 0;
+}
+:deep(.el-input__inner) {
   height: 50px;
   font-size: 20px;
-  border-radius: 0;
-  border: 1px solid #dcdfe6;
   background-color: #fff;
   padding: 0 15px;
 }
+/* 更新密码图标按钮样式 */
+:deep(.el-input__suffix) {
+  cursor: pointer;
+  font-size: 20px;
+  color: #aaa;
+  margin-right: 10px;
+}
+
 .login-button {
   background: linear-gradient(to right, #8df9f0, #26b046);
   width: 100%;
-  max-width: 400px;
+  max-width: 300px; /* 缩小按钮宽度 */
   height: 56px;
   color: white;
   border: none;
@@ -478,6 +540,8 @@ onMounted(() => {
   font-size: 24px;
   cursor: pointer;
   margin-top: 10px;
+  margin: 10px auto; /* 居中显示 */
+  display: block; /* 使 margin: auto 生效 */
 }
 .login-button:hover {
   opacity: 0.9;
@@ -513,24 +577,90 @@ onMounted(() => {
   text-align: center;
   margin-top: 20px;
 }
-.input-frame {
-  background-color: #fff;
-  width: 100%;
-  padding: 15px 10px;
-  margin-bottom: 20px;
-}
 
 /* 错误提示样式 */
-.error-message {
-  color: #f56c6c;
+.error-message,
+.success-message {
   font-size: 16px;
   text-align: center;
   margin: 15px 0;
   animation: fadeIn 0.3s ease-out forwards;
+  padding: 10px;
+  border-radius: 4px;
+}
+.error-message {
+  color: #f56c6c;
+  background-color: #fef0f0;
+  border: 1px solid #fde2e2;
+}
+.success-message {
+  color: #67c23a;
+  background-color: #f0f9ec;
+  border: 1px solid #e1f3d8;
 }
 
 @keyframes fadeIn {
-  from { opacity: 0; }
-  to { opacity: 1; }
+  from {
+    opacity: 0;
+  }
+  to {
+    opacity: 1;
+  }
+}
+
+/* 针对登录和注册表单中的标签 */
+.login-form :deep(.el-form-item__label),
+.register-form :deep(.el-form-item__label) {
+    /* 为登录和注册表单中的标签设置一个稍大的最大宽度,以适应"确认密码"等较长标签 */
+    max-width: 200px; /* 根据实际需要调整 */
+    width: auto;
+    text-align: right; /* 标签靠右对齐 */
+    flex-shrink: 0; /* 防止标签被压缩 */
+    margin-right: 15px; /* 标签和输入框之间的间距 */
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+}
+
+/* 针对登录和注册表单中的内容区域 */
+.login-form :deep(.el-form-item__content),
+.register-form :deep(.el-form-item__content) {
+    display: flex;
+    justify-content: flex-end; /* 内容整体靠右 */
+    width: 100%;
+}
+
+/* 针对登录和注册表单中的输入框包装器 */
+.login-form :deep(.el-input__wrapper),
+.register-form :deep(.el-input__wrapper) {
+    max-width: 350px; /* 设置你想要的统一缩短后的宽度 */
+    width: 100%; 
+    margin-left: 0; 
+}
+
+/* 特别为登录和注册表单的输入框框架增加类名并设置间距 */
+.login-input-frame,
+.register-input-frame {
+  margin-left: 48px;
 }
-</style>
+
+/* --- 修改部分:为注册表单的账号和密码输入框容器添加特定类名并设置新的左边距 --- */
+.register-form .register-account-frame,
+.register-form .register-password-frame {
+  margin-left: 96px; /* 原来的 48px + 新增的 20px */
+}
+/* --------------------------------------------------------------------------------- */
+
+/* 确认密码输入框间距保持不变 */
+.register-form .input-frame:last-of-type {
+  margin-bottom: 20px; /* 或者你希望的其他值 */
+}
+
+/* 注册按钮特殊调整 */
+.register-button-adjusted {
+  max-width: 250px; 
+}
+</style>
+
+
+

+ 26 - 12
tsconfig.app.json

@@ -1,20 +1,34 @@
 {
-  "extends": "@vue/tsconfig/tsconfig.dom.json",
-  "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
+  "include": ["env.d.ts", "src/**/*"],
   "exclude": [
-    "src/**/__tests__/*", // 移除了 src/views/menu/loginView.vue
-    "src/views/userManagement/UserRegistration.vue", // Exclude missing file
-    "src/views/userManagement/UserManagement.vue", // Exclude missing file
-    "src/views/User/introduction/IntroUpdateModal.vue", // Exclude missing file
-    "src/views/Admin/dataManagement/Visualizatio.vue", // Exclude missing file
-    "src/views/Admin/dataManagement/Visualization.vue", // Exclude missing file
-    "src/views/Admin/modelManagement/AcidReductionModel/thres.vue", // Exclude missing file
-    "src/views/User/heavyMetalFluxCalculation/outputFluxCalculation/outputFluxCalculation.vue" // Exclude missing file
+    "src/**/__tests__/*",
+    "src/views/userManagement/UserRegistration.vue",
+    "src/views/userManagement/UserManagement.vue",
+    "src/views/User/introduction/IntroUpdateModal.vue",
+    "src/views/Admin/dataManagement/Visualizatio.vue",
+    "src/views/Admin/dataManagement/Visualization.vue",
+    "src/views/Admin/modelManagement/AcidReductionModel/thres.vue",
+    "src/views/User/heavyMetalFluxCalculation/outputFluxCalculation/outputFluxCalculation.vue"
   ],
   "compilerOptions": {
-    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+    "target": "ES2020",
+    "useDefineForClassFields": true,
+    "module": "ESNext",
+    "lib": ["ES2020", "DOM", "DOM.Iterable"],
+    "strict": true,
+    "skipLibCheck": true,
+    "esModuleInterop": true,
+    "noEmit": true,
+    "resolveJsonModule": true,
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "isolatedModules": true,
+    "jsx": "preserve",
+    "incremental": true,
+    "types": ["vite/client"],
     "paths": {
       "@/*": ["./src/*"]
-    }
+    },
+    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo"
   }
 }

+ 23 - 0
tsconfig.base.json

@@ -0,0 +1,23 @@
+// tsconfig.base.json
+{
+  "compilerOptions": {
+    "target": "ES2020",
+    "useDefineForClassFields": true,
+    "module": "ESNext",
+    "lib": ["ES2020", "DOM", "DOM.Iterable"],
+    "skipLibCheck": true,
+    "allowJs": true,
+    "strict": true,
+    "forceConsistentCasingInFileNames": true,
+    "esModuleInterop": true,
+    "noEmit": true,
+    "types": ["unplugin-vue-i18n/client"],
+    "moduleResolution": "bundler",
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "noFallthroughCasesInSwitch": true
+  },
+  "include": ["src"]
+}

+ 7 - 8
tsconfig.vitest.json

@@ -1,11 +1,10 @@
+// tsconfig.vitest.json
 {
   "extends": "./tsconfig.app.json",
-  "include": ["src/**/__tests__/*", "env.d.ts"],
-  "exclude": [],
   "compilerOptions": {
-    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.vitest.tsbuildinfo",
-
-    "lib": [],
-    "types": ["node", "jsdom"]
-  }
-}
+    "composite": true,
+    "lib": ["ES2020", "DOM"],
+    "types": ["node", "jsdom", "vitest/globals"]
+  },
+  "include": ["src/**/__tests__/*", "env.d.ts"]
+}

Неке датотеке нису приказане због велике количине промена