Browse Source

调整登录逻辑

yangtaodemon 16 hours ago
parent
commit
6b8e786a6f

+ 1 - 1
.env

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

+ 6 - 0
components.d.ts

@@ -23,6 +23,7 @@ declare module 'vue' {
     CrossSetionData1: typeof import('./src/components/irrpollution/crossSetionData1.vue')['default']
     CrossSetionData2: typeof import('./src/components/irrpollution/crossSetionData2.vue')['default']
     CrossSetionTencentmap: typeof import('./src/components/irrpollution/crossSetionTencentmap.vue')['default']
+    ElAlert: typeof import('element-plus/es')['ElAlert']
     ElAside: typeof import('element-plus/es')['ElAside']
     ElAvatar: typeof import('element-plus/es')['ElAvatar']
     ElButton: typeof import('element-plus/es')['ElButton']
@@ -56,6 +57,8 @@ declare module 'vue' {
     ElTabPane: typeof import('element-plus/es')['ElTabPane']
     ElTabs: typeof import('element-plus/es')['ElTabs']
     ElTag: typeof import('element-plus/es')['ElTag']
+    ElTooltip: typeof import('element-plus/es')['ElTooltip']
+    ElUpload: typeof import('element-plus/es')['ElUpload']
     HeavyMetalEnterprisechart: typeof import('./src/components/atmpollution/heavyMetalEnterprisechart.vue')['default']
     HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
     IconCommunity: typeof import('./src/components/icons/IconCommunity.vue')['default']
@@ -77,4 +80,7 @@ declare module 'vue' {
     Waterdataline: typeof import('./src/components/irrpollution/waterdataline.vue')['default']
     WelcomeItem: typeof import('./src/components/WelcomeItem.vue')['default']
   }
+  export interface ComponentCustomProperties {
+    vLoading: typeof import('element-plus/es')['ElLoadingDirective']
+  }
 }

+ 225 - 0
src/API/admin.ts

@@ -0,0 +1,225 @@
+// @/API/admin.ts(修复后)
+import requestAdmin from '@/utils/request';
+import { ElMessage } from 'element-plus';
+import 'element-plus/es/components/message/style/css';
+
+// 🔹 获取表格数据
+export const table = (params: { table: string }) => {
+  return requestAdmin({
+    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: Record<string, any>;
+}) => {
+  return requestAdmin({
+    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;
+  });
+};
+
+// 🔹 更新数据
+export const updateItem = (data: {
+  table: string;
+  id: number;
+  update_data: Record<string, any>;
+}) => {
+  return requestAdmin({
+    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 deleteItemApi = (params: {
+  table: string;
+  id: number;
+}) => {
+  return requestAdmin({
+    url: "/admin/delete_item",
+    method: "DELETE",
+    params: params,
+  });
+};
+
+// 🔹 导出数据
+export const exportData = (table: string, format: string = "xlsx") => {
+  return requestAdmin({
+    url: "/api/admin/export_data",
+    method: "GET",
+    params: { table, fmt: format },
+    responseType: "blob",
+  })
+    .then((response) => {
+      const contentDisposition = response.headers["content-disposition"];
+      if (!contentDisposition) {
+        throw new Error("无法获取文件名");
+      }
+      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;
+    });
+};
+
+// 🔹 导入数据
+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);
+  }
+
+  // 2. 创建FormData(与后端参数名对齐)
+  const formData = new FormData();
+  formData.append('table', table);
+  formData.append('file', file); // 与后端File(...)参数名一致
+
+  return requestAdmin({
+    url: "/admin/import_data",
+    method: "POST",
+    data: formData,
+    // 浏览器自动处理multipart边界,无需手动设置Content-Type(避免边界符缺失问题)
+    headers: {
+      'Content-Type': undefined 
+    }
+  })
+  .then((response) => {
+    const { total_data, new_data, duplicate_data } = response.data;
+    // 3. 成功场景提示(区分无数据/有数据)
+    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) => {
+    // 4. 错误场景分类提示(适配后端结构化异常)
+    let errorMsg = "❌ 导入失败,请稍后重试";
+
+    // 4.1 后端返回的结构化错误(重复记录/字段缺失等)
+    if (error.response?.data) {
+      const resData = error.response.data;
+      // 处理重复记录详情(后端返回的duplicates数组)
+      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}`;
+      }
+    } 
+    // 4.2 网络异常/请求超时
+    else if (!error.response) {
+      errorMsg = "❌ 网络异常或服务器无响应,请检查网络连接后重试";
+    } 
+    // 4.3 其他未知错误
+    else {
+      errorMsg = "❌ 未知错误,可能是文件损坏或服务器繁忙";
+    }
+
+    console.error(errorMsg, "错误详情:", error);
+    ElMessage.error(errorMsg);
+    throw new Error(errorMsg);
+  });
+};
+
+// 🔹 下载模板
+export const downloadTemplate = (table: string, format: string = "excel") => {
+  return requestAdmin({
+    url: "/admin/download_template",
+    method: "GET",
+    params: { table, format },
+    responseType: "blob",
+  })
+    .then((response) => {
+      // 1. 从响应头提取带时间戳的文件名
+      const contentDisposition = response.headers["content-disposition"];
+      if (!contentDisposition) {
+        throw new Error("无法获取文件名");
+      }
+      // 解析 Content-Disposition: attachment; filename=atmo_company_template_20240520153045.xlsx
+      const filenameMatch = contentDisposition.match(/filename=([^;]+)/);
+      const filename = filenameMatch ? decodeURIComponent(filenameMatch[1]) : `${table}_template.${format === "excel" ? "xlsx" : "csv"}`;
+
+      // 2. 处理文件流
+      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;
+    });
+};

+ 14 - 8
src/API/menus.ts

@@ -3,8 +3,8 @@ import request from "@/utils/request";
 // 使用导入的 request 函数进行网络请求
 export const table = (params: { table: string }) => {
   return request({
-    url: "/table",
-    method: "POST",
+    url: "/admin/table",
+    method: "GET",
     data: params,
   });
 };
@@ -23,17 +23,19 @@ const customRequest = (options: any) => {
     });
 };
 
+// 提交编辑数据
 export const updateItem = (data: { table: string, item: any }) => {
   return customRequest({
-    url: "/update_item",
+    url: "/admin/update_item",
     method: "PUT",
     data: data,
   });
 };
 
+// 提交新增数据
 export const addItem = (data: { table: string, item: any }) => {
   return customRequest({
-    url: "/add_item",
+    url: "/admin/add_item",
     method: "POST",
     data: data,
   }).catch(error => {
@@ -44,10 +46,11 @@ export const addItem = (data: { table: string, item: any }) => {
   });
 };
 
+// 删除
 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: "/delete_item",
+    url: "/admin/delete_item",
     method: "POST",
     data: {
       table: params.table,
@@ -56,9 +59,10 @@ export const deleteItemApi = (params: { table: string; condition: any }) => {
   });
 };
 
+// 下载模板
 export const downloadTemplate = (table: string, format: string = 'xlsx') => {
   return customRequest({
-    url: "/download_template",
+    url: "/admin/download_template",
     method: "GET",
     params: { table, format },
     responseType: 'blob',
@@ -78,8 +82,9 @@ export const downloadTemplate = (table: string, format: string = 'xlsx') => {
 export const exportData = (table: string, format: string = 'excel') => {
   const backendFormat = format.toLowerCase() === 'xlsx' ? 'excel' : format;
 
+  //表格数据并导出
   return customRequest({
-    url: "/export_data",
+    url: "/admin/export_data",
     method: "GET",
     params: { table, format: backendFormat },
     responseType: 'blob',
@@ -101,12 +106,13 @@ export const exportData = (table: string, format: string = 'excel') => {
   });
 };
 
+ // 导入数据
 export const importData = (table: string, file: File) => {
   const formData = new FormData();
   formData.append('file', file);
   formData.append('table', table);
   return customRequest({
-    url: "/import_data",
+    url: "/admin/import_data",
     method: "POST",
     data: formData,
     headers: {

+ 75 - 29
src/API/users.ts

@@ -1,48 +1,94 @@
+// src/API/users.ts
 import request from "@/utils/request";
-import axios from "axios";
+import { useTokenStore } from "@/stores/mytoken";
 
-// 定义登录信息的类型
+// =========================
+// 登录
+// =========================
 export interface LoginInfo {
-  name: string; // 用户名
-  password: string; // 密码
-  userType: string; // 添加 userType 属性
+  username: string;
+  password: string;
+  usertype?: string; // 可选,前端可以传入
 }
 
 export const login = (loginInfo: LoginInfo) => {
   return request({
-    url: "/login",
+    url: "/admin/login",
     method: "POST",
     data: {
-      name: loginInfo.name,
-      password: loginInfo.password
-    }
+      name: loginInfo.username,
+      password: loginInfo.password,
+    },
   });
 };
 
-// 定义 token 接口
-type userInfo = {
-  content: {
-  userid: string
-  name: string
+// =========================
+// 注册 / 新增用户
+// =========================
+export interface RegisterInfo {
+  name: string;
+  password: string;
+  userType?: string; // 默认 "user"
 }
+
+export const addUser = (data: RegisterInfo) => {
+  return request({
+    url: "/admin/register", // 或者根据后端实际路由修改
+    method: "POST",
+    data: {
+      name: data.name,
+      password: data.password,
+      userType: data.userType ?? "user",
+    },
+  });
+};
+
+// 兼容老代码:register 导出
+export const register = addUser;
+
+// =========================
+// 获取用户列表
+// =========================
+export const getUsers = () => {
+  return request({
+    url: "/admin/list_users",
+    method: "GET",
+  });
+};
+
+// =========================
+// 更新用户
+// =========================
+export interface UpdateUserInfo {
+  name?: string;
+  userType?: string;
+  password?: string;
 }
 
-export const logout = async () => {
-  try {
-    return await axios.get("/api/logout"); // Changed POST to GET
-  } catch (error) {
-    console.error("Logout failed:", error);
-    throw error;
-  }
+export const updateUser = (userId: number, data: UpdateUserInfo) => {
+  return request({
+    url: `/admin/update_user/${userId}`,
+    method: "PUT",
+    data,
+  });
 };
 
-export const register = (registerInfo: { name: any; password: any; }) => {
+// =========================
+// 删除用户
+// =========================
+export const deleteUser = (userId: number) => {
   return request({
-    url: "/register",
-    method: "POST",
-    data: {
-      name: registerInfo.name,
-      password: registerInfo.password
-    }
+    url: `/admin/delete_user/${userId}`,
+    method: "DELETE",
   });
-};
+};
+
+// =========================
+// 登出(前端本地清理)
+// =========================
+export const logout = async () => {
+  const store = useTokenStore();
+  store.clearToken();
+  localStorage.removeItem("userInfo");
+  return { success: true, message: "已退出登录" };
+};

+ 121 - 108
src/components/layout/AppLayout.vue

@@ -1,10 +1,18 @@
 <template>
-  <div class="layout-wrapper" :class="{ 'full-screen': isFullScreen }" :style="isSpecialBg ? backgroundStyle : {}">
-    <!-- 背景层 - 关键修改 -->
+  <div
+    class="layout-wrapper"
+    :class="{ 'full-screen': isFullScreen }"
+    :style="isSpecialBg ? backgroundStyle : {}"
+  >
+    <!-- 背景层 -->
     <div v-if="isSpecialBg" class="background-layer"></div>
 
-    <!-- Header 部分(透明背景) -->
-    <el-header class="layout-header" v-if="!isFullScreen" :class="{ 'transparent-header': isSpecialBg }">
+    <!-- Header -->
+    <el-header
+      class="layout-header"
+      v-if="!isFullScreen"
+      :class="{ 'transparent-header': isSpecialBg }"
+    >
       <div class="logo-title-row">
         <img src="@/assets/logo.png" alt="Logo" class="logo" />
         <div class="title-and-user">
@@ -12,22 +20,27 @@
             区域土壤重金属污染风险评估
           </span>
 
-          <!-- 用户信息 - 在select-city页面隐藏 -->
+          <!-- 用户信息 -->
           <div class="user-info-row" v-if="!isSelectCity">
             <span class="welcome-text" :class="{ 'light-text': isSpecialBg }">
-              欢迎{{
-                tokenStore.token.loginType === "admin" ? "管理员" : "用户"
-              }}登录成功
+              欢迎 {{ userInfo.type === "admin" ? "管理员" : "用户" }} 登录成功
             </span>
             <el-dropdown>
               <span class="el-dropdown-link">
-                <el-avatar :size="40" :class="{ 'light-avatar-border': isSpecialBg }"
-                  src="https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png" />
+                <el-avatar
+                  :size="40"
+                  :class="{ 'light-avatar-border': isSpecialBg }"
+                  src="https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png"
+                />
               </span>
               <template #dropdown>
                 <el-dropdown-menu>
-                  <el-dropdown-item disabled>用户名:{{ userInfo.name }}</el-dropdown-item>
-                  <el-dropdown-item divided @click="handleLogout">退出登录</el-dropdown-item>
+                  <el-dropdown-item disabled
+                    >用户名:{{ userInfo.name }}</el-dropdown-item
+                  >
+                  <el-dropdown-item divided @click="handleLogout"
+                    >退出登录</el-dropdown-item
+                  >
                 </el-dropdown-menu>
               </template>
             </el-dropdown>
@@ -36,9 +49,15 @@
       </div>
     </el-header>
 
-    <!-- Tab 区域(不加背景图) -->
+    <!-- Tabs -->
     <div class="tabs-row" v-if="!isFullScreen">
-      <el-tabs v-if="showTabs" v-model="activeName" class="demo-tabs" :style="tabStyle" @tab-click="handleClick">
+      <el-tabs
+        v-if="showTabs"
+        v-model="activeName"
+        class="demo-tabs"
+        :style="tabStyle"
+        @tab-click="handleClick"
+      >
         <el-tab-pane v-for="tab in tabs" :key="tab.name" :name="tab.name">
           <template #label>
             <i :class="['tab-icon', tab.icon]"></i>
@@ -52,16 +71,21 @@
       </div>
     </div>
 
-    <!-- 主体区域 -->
+    <!-- Main Content -->
     <el-container class="layout-main-container">
-      <!-- 侧边栏(不加背景图) -->
       <el-aside v-if="showAside && showTabs" class="layout-aside">
-        <component :is="AsideComponent" :activeTab="activeName" :showTabs="showTabs" />
+        <component
+          :is="AsideComponent"
+          :activeTab="activeName"
+          :showTabs="showTabs"
+        />
       </el-aside>
 
-      <!-- 内容区域(透明背景) -->
       <el-main class="layout-content-wrapper" :style="mainStyle">
-        <div class="scrollable-content" :class="{ 'transparent-scroll': isSpecialBg }">
+        <div
+          class="scrollable-content"
+          :class="{ 'transparent-scroll': isSpecialBg }"
+        >
           <div :class="{ 'select-city-container': isSelectCity }">
             <RouterView />
           </div>
@@ -83,31 +107,24 @@ const route = useRoute();
 const tokenStore = useTokenStore();
 const currentBgImage = ref("");
 
-// 定义需要背景图的特殊路由和对应图片文件名
+// 特殊背景路由映射
 const bgRouteMap: Record<string, string> = {
-  // 添加灌溉水相关页面的背景图
   "/samplingMethodDevice1": "irrigation.jpg",
   "/irriSampleData": "irrigation.jpg",
   "/csSampleData": "irrigation.jpg",
   "/irriInputFlux": "irrigation.jpg",
-  // 添加大农产品投入相关页面的背景图
   "/farmInputSamplingDesc": "agricultural_input.png",
   "/prodInputFlux": "agricultural_input.png",
-  // 添加大气干湿沉降相关页面的背景图
   "/AtmosDepositionSamplingDesc": "atmospheric_deposition.png",
   "/heavyMetalEnterprise": "atmospheric_deposition.png",
   "/airSampleData": "atmospheric_deposition.png",
   "/airInputFlux": "atmospheric_deposition.png",
-  //添加籽粒移除相关页面的背景图
   "/samplingDesc1": "rain_removal.png",
   "/grainRemovalInputFlux": "rain_removal.png",
-  //添加秸秆移除相关页面的背景图
   "/samplingDesc2": "straw-removal.png",
   "/strawRemovalInputFlux": "straw-removal.png",
-  //添加地下渗漏相关页面的背景图
   "/samplingDesc3": "subsurface-leakage.jpg",
   "/subsurfaceLeakageInputFlux": "subsurface-leakage.jpg",
-  //添加地表径流相关页面的背景图
   "/samplingDesc4": "surface-runoff.jpg",
   "/surfaceRunoffInputFlux": "surface-runoff.jpg",
   "/totalInputFlux": "background.jpg",
@@ -129,14 +146,30 @@ const bgRouteMap: Record<string, string> = {
   "/DetectionStatistics": "background.jpg",
   "/FarmlandPollutionStatistics": "background.jpg",
   "/PlantingRiskStatistics": "background.jpg",
+  "/soilAcidReductionData": "background.jpg",
+  "/soilAcidificationData": "background.jpg",
+  "/crossSectionSampleData": "background.jpg",
+  "/irrigationWaterInputFluxData": "background.jpg",
+  "/heavyMetalEnterpriseData": "background.jpg",
+  "/atmosphericSampleData": "background.jpg",
+  "/CadmiumPredictionModel": "background.jpg",
+  "/ModelSelection": "background.jpg",
+  "/thres": "background.jpg",
+  "/ModelTrain": "background.jpg",
+  "/RiceRiskModel": "background.jpg",
+  "/WheatRiskModel": "background.jpg",
+  "/VegetableRiskModel": "background.jpg",
+  "/UserManagement": "background.jpg",
+  "/UserRegistration": "background.jpg",
 };
 
-// 当前是否为特殊背景图页面
-const isSpecialBg = computed(() => Object.keys(bgRouteMap).includes(route.path));
+// 当前是否特殊背景
+const isSpecialBg = computed(() =>
+  Object.keys(bgRouteMap).includes(route.path)
+);
 
 function getBgImageUrl(): string {
   const bgFile = bgRouteMap[route.path];
-  console.log("根据路径查找背景文件名:", bgFile);
   if (bgFile) {
     try {
       const url = `url(${new URL(`../../assets/bg/background.jpg`, import.meta.url).href})`;
@@ -149,36 +182,34 @@ function getBgImageUrl(): string {
   return "";
 }
 
-// 监听路由变化更新背景图,并打印日志
 watch(
   () => route.path,
-  (newPath) => {
+  () => {
     currentBgImage.value = getBgImageUrl();
-    console.log(`[背景图更新] 当前路径: ${newPath}, 背景图: ${currentBgImage.value}`);
   },
   { immediate: true }
 );
 
-// 背景图样式 - 关键修改
-const backgroundStyle = computed(() => {
-  return {
-    backgroundImage: currentBgImage.value,
-    backgroundSize: 'cover',
-    backgroundPosition: 'center',
-    backgroundRepeat: 'no-repeat',
-    backgroundAttachment: 'fixed'
-  };
-});
+const backgroundStyle = computed(() => ({
+  backgroundImage: currentBgImage.value,
+  backgroundSize: "cover",
+  backgroundPosition: "center",
+  backgroundRepeat: "no-repeat",
+  backgroundAttachment: "fixed",
+}));
 
-// 是否为全屏页面
 const isFullScreen = computed(() => route.meta.fullScreen === true);
-
-// 判断是否是select-city页面
 const isSelectCity = computed(() => route.path === "/select-city");
 
-// Tab 配置
+// 当前用户信息
+const userInfo = reactive({
+  name: tokenStore.userName,
+  type: tokenStore.userType,
+});
+
+// Tabs 配置
 const tabs = computed(() => {
-  if (tokenStore.token.loginType === "admin") {
+  if (userInfo.type === "admin") {
     return [
       {
         name: "dataManagement",
@@ -187,46 +218,35 @@ const tabs = computed(() => {
         routes: [
           "/soilAcidReductionData",
           "/soilAcidificationData",
-          "/AdminRegionData",
-          "/SoilAssessmentUnitData",
-          "/SoilHeavyMetalData",
-          "/CropHeavyMetalData",
-          "/LandUseTypeData",
-          "/ClimateInfoData",
-          "/GeographicEnvInfoData",
+          "/crossSectionSampleData",
+          "irrigationWaterInputFluxData",
+          "heavyMetalEnterpriseData",
+          "atmosphericSampleData"
         ],
       },
+      /*
       {
         name: "infoManagement",
         label: "信息管理",
         icon: "el-icon-document",
         routes: ["/IntroductionUpdate"],
-      },
+      },*/
       {
         name: "modelManagement",
         label: "模型管理及配置",
         icon: "el-icon-cpu",
-        routes: [
-          "/CadmiumPredictionModel",
-          "/EffectiveCadmiumModel",
-          "/Admin/RiceRiskModel",
-          "/AdminModelSelection",
-          "/Admin/thres",
-          "/Admin/ModelTrain",
-          "/Admin/WheatRiskModel",
-          "/Admin/VegetableRiskModel",
-        ],
+        routes: ["/CadmiumPredictionModel"],
       },
       {
         name: "userManagement",
         label: "用户管理",
         icon: "el-icon-user",
-        routes: ["/UserManagement", "/UserRegistration"],
+        routes: ["/UserManagement"],
       },
     ];
   } else {
     return [
-      {
+      /*{
         name: "shuJuKanBan",
         label: "数据看板",
         icon: "el-icon-data-analysis",
@@ -237,7 +257,7 @@ const tabs = computed(() => {
         label: "软件简介",
         icon: "el-icon-info-filled",
         routes: ["/SoilPro", "/Overview", "/ResearchFindings", "/Unit"],
-      },
+      },*/
       {
         name: "HmOutFlux",
         label: "重金属输入通量",
@@ -270,12 +290,12 @@ const tabs = computed(() => {
           "/surfaceRunoffInputFlux",
         ],
       },
-      {
+      /*{
         name: "mapView",
         label: "地图展示",
         icon: "el-icon-map-location",
         routes: ["/mapView"],
-      },
+      },*/
       {
         name: "cadmiumPrediction",
         label: "土壤污染物含量预测",
@@ -287,7 +307,7 @@ const tabs = computed(() => {
           "/netFlux",
           "/currentYearConcentration",
           "/EffectiveCadmiumPrediction",
-          "CropCadmiumPrediction",
+          "/CropCadmiumPrediction",
         ],
       },
       {
@@ -328,11 +348,10 @@ const tabs = computed(() => {
           "/PlantingRiskStatistics",
         ],
       },
-    ].filter(tab => !["shuJuKanBan", "mapView", "introduction"].includes(tab.name));
+    ];
   }
 });
 
-// 当前激活 tab
 const activeName = ref(tabs.value[0]?.name || "");
 const showTabs = computed(() => tabs.value.length > 1);
 const tabStyle = computed(() =>
@@ -349,43 +368,37 @@ watch(
       hasNavigated = true;
       return;
     }
-    if (tab && targetPath && router.currentRoute.value.path !== targetPath) {
+    if (tab && targetPath && router.currentRoute.value.path !== targetPath)
       router.push({ path: targetPath });
-    }
   },
   { immediate: true }
 );
 
-// 点击切换 tab
-const handleClick = (tab: any, event: Event) => {
-  activeAsideTab.value = tab.props.name;
+// 点击 tab
+const activeAsideTab = ref(activeName.value || "");
+const handleClick = (tab: any, event?: Event) => {
+  activeAsideTab.value = tab.name || tab.props?.name;
 };
 
-// 动态加载侧边栏组件
+// 动态加载侧边栏
 const AsideComponent = computed(() => {
-  if (
-    ["parameterConfig", "dataManagement", "infoManagement", "modelManagement", "userManagement"]
-      .includes(activeName.value)
-  ) {
-    return defineAsyncComponent(() => import("./AppAsideForTab2.vue"));
-  } else {
-    return defineAsyncComponent(() => import("./AppAside.vue"));
-  }
+  return [
+    "dataManagement",
+    "infoManagement",
+    "modelManagement",
+    "userManagement",
+  ].includes(activeName.value)
+    ? defineAsyncComponent(() => import("./AppAsideForTab2.vue"))
+    : defineAsyncComponent(() => import("./AppAside.vue"));
 });
 
 // 是否显示侧边栏
-const showAside = computed(() => {
-  return (
-    !isFullScreen.value &&
-    activeName.value !== "mapView" &&
-    activeName.value !== "cropRiskAssessment" &&
-    activeName.value !== "farmlandQualityAssessment"
-  );
-});
-
-const activeAsideTab = ref(activeName.value || "");
-const userInfo = reactive({ name: tokenStore.token.name || "未登录" });
+const showAside = computed(
+  () =>
+    !isFullScreen.value && !["cropRiskAssessment"].includes(activeName.value)
+);
 
+// 登出逻辑
 const handleLogout = async () => {
   try {
     await ElMessageBox.confirm("确定要退出登录吗?", "提示", {
@@ -403,12 +416,12 @@ const handleLogout = async () => {
 };
 
 // 内容区样式
-const mainStyle = computed(() => {
-  return {
-    padding: ["mapView", "infoManagement"].includes(activeName.value) ? "0" : "20px",
-    overflow: "hidden",
-  };
-});
+const mainStyle = computed(() => ({
+  padding: ["mapView", "infoManagement"].includes(activeName.value)
+    ? "0"
+    : "20px",
+  overflow: "hidden",
+}));
 </script>
 
 <style>
@@ -620,6 +633,7 @@ const mainStyle = computed(() => {
 .logo {
   height: 60px;
 }
+
 .project-name {
   font-size: 48px;
   font-weight: bold;
@@ -641,7 +655,7 @@ const mainStyle = computed(() => {
 /* 侧边栏 - 白色背景 */
 .layout-aside {
   width: 360px;
-  background: linear-gradient(to bottom, #B7F1FC, #FFF8F0);
+  background: linear-gradient(to bottom, #b7f1fc, #fff8f0);
   border-right: 1px solid;
   overflow-y: auto;
   color: #000000;
@@ -683,7 +697,6 @@ const mainStyle = computed(() => {
   box-shadow: 0 2px 8px rgba(16, 146, 216, 0.25);
 }
 
-
 .layout-content-wrapper {
   flex: 1;
   overflow: hidden;
@@ -720,4 +733,4 @@ const mainStyle = computed(() => {
 .scrollable-content.transparent-scroll {
   background-color: transparent;
 }
-</style>
+</style>

+ 16 - 1
src/components/layout/menuItems2.ts

@@ -35,6 +35,21 @@ export const tabMenuMap: Record<string, any[]> = {
         { index: "/soilAcidificationData", label: "反酸数据", icon: Warning },
       ],
     },
+    {
+      index: "/HeavyMetalInputFluxManager",
+      label: "重金属输入通量数据管理",
+      icon: Tools,
+      children: [
+        //{ index: "/irrigationWaterSampleData", label: "灌溉水采样数据", icon: DataAnalysis },
+        { index: "/crossSectionSampleData", label: "断面采样数据", icon: Warning },
+        { index: "/irrigationWaterInputFluxData", label: "灌溉水输入通量数据", icon: Warning },
+        //{ index: "/agriProductInputFluxData", label: "农产品输入通量数据", icon: Warning },
+        { index: "/heavyMetalEnterpriseData", label: "涉重企业数据", icon: Warning },
+        { index: "/atmosphericSampleData", label: "大气采样数据", icon: Warning },
+        //{ index: "/atmosphericInputFluxData", label: "大气输入通量数据", icon: Warning },
+      ],
+    },
+    /*
     {
       index: "/Administrative Area Data Management",
       label: "行政区域数据管理",
@@ -98,7 +113,7 @@ export const tabMenuMap: Record<string, any[]> = {
       children: [
         { index: "/GeographicEnvInfoData", label: "地理环境信息", icon: Reading },
       ],
-    },
+    },*/
   ],
   infoManagement: [
     {

+ 7 - 5
src/locales/en.json

@@ -10,13 +10,15 @@
     "adminLoginTitle": "Admin Login",
     "userRegisterTitle": "User Registration",
     "adminRegisterTitle": "Admin Registration",
-    "switchToUser": "Switch to User",
-    "switchToAdmin": "Switch to Admin",
-    "userTitle": "User Login",
-    "adminTitle": "Admin Login",
     "successMessage": "Login successful",
     "errorMessage": "Login failed",
-    "loginSuccess": "Login successful"
+    "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"
   },
   "register": {
     "title": "Register",

+ 7 - 5
src/locales/zh.json

@@ -10,13 +10,15 @@
     "adminLoginTitle": "管理员登录",
     "userRegisterTitle": "普通用户注册",
     "adminRegisterTitle": "管理员注册",
-    "switchToUser": "切换到普通用户",
-    "switchToAdmin": "切换到管理员",
-    "userTitle": "普通用户登录",
-    "adminTitle": "管理员登录",
     "successMessage": "登录成功",
     "errorMessage": "登录失败",
-    "loginSuccess": "登录成功"
+    "loginSuccess": "登录成功",
+    "loginFailed": "登录失败,请检查用户名或密码",
+    "userTypeMismatch": "用户类型不匹配",
+    "userTitle": "普通用户登录",
+    "adminTitle": "管理员登录",
+    "switchToAdmin": "切换到管理员",
+    "switchToUser": "切换到普通用户"
   },
   "register": {
     "title": "注册",

+ 12 - 9
src/main.ts

@@ -3,25 +3,28 @@ import './assets/main.scss';
 import { createApp } from 'vue';
 import { createPinia } from 'pinia';
 import ElementPlus from 'element-plus';
-import 'element-plus/dist/index.css';
+import 'element-plus/dist/index.css'; 
 import 'leaflet/dist/leaflet.css';
 
 import App from './App.vue';
 import router from './router';
-import i18n from './i18n'; // 引入 i18n 配置
+import i18n from './i18n'; 
+
+// 导入 Element Plus 中文语言包(保留)
+import zhCn from 'element-plus/es/locale/lang/zh-cn';
 
 // 创建 Vue 应用实例
 const app = createApp(App);
 
+//  ElementPlus 安装(带中文配置)
+app.use(ElementPlus, {
+  locale: zhCn,
+});
 
-// 使用插件和服务
+// 使用其他插件
 app.use(createPinia());
 app.use(router);
-app.use(ElementPlus,);
-app.use(i18n); // 使用 i18n
+app.use(i18n); 
 
 // 挂载应用
-app.mount('#app');
-
-
-
+app.mount('#app');

+ 205 - 85
src/router/index.ts

@@ -4,17 +4,11 @@ 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: "/",
-    name: "home",
     component: AppLayout,
-    meta: { requiresAuth: true, title: "模型" },
-    redirect: { name: "login" }, // 修改默认重定向为 loginView
+    meta: { requiresAuth: true },
 
     children: [
       {
@@ -65,168 +59,207 @@ const routes = [
           import("@/views/User/introduction/IntroductionUpdate.vue"), // 修复路径
         meta: { title: "更新介绍" },
       },
+      //管理员
       // {
       //   path: "HmOutFlux",
       //   name: "HmOutFlux",
-      //   component: () => import("@/views/User/HmOutFlux"), 
+      //   component: () => import("@/views/User/HmOutFlux"),
       //   meta: { title: "重金属输出通量" },
       // },
       // {
       //   path: "irrigationWater",
       //   name: "irrigationWater",
-      //   component: () => import("@/views/User/HmOutFlux/irrigationWater"), 
+      //   component: () => import("@/views/User/HmOutFlux/irrigationWater"),
       //   meta: { title: "灌溉水" },
       // },
       {
         path: "samplingMethodDevice1",
         name: "samplingMethodDevice1",
-        component: () => import("@/views/User/HmOutFlux/irrigationWater/samplingMethodDevice1.vue"), 
+        component: () =>
+          import(
+            "@/views/User/HmOutFlux/irrigationWater/samplingMethodDevice1.vue"
+          ),
         meta: { title: "采样方法和装置" },
       },
       {
         path: "irriSampleData",
         name: "irriSampleData",
-        component: () => import("@/views/User/HmOutFlux/irrigationWater/irriWaterSampleData.vue"), 
+        component: () =>
+          import(
+            "@/views/User/HmOutFlux/irrigationWater/irriWaterSampleData.vue"
+          ),
         meta: { title: "灌溉水采样数据" },
       },
       {
         path: "csSampleData",
         name: "csSampleData",
-        component: () => import("@/views/User/HmOutFlux/irrigationWater/crossSection.vue"), 
+        component: () =>
+          import(
+            "@/views/User/HmOutFlux/irrigationWater/crossSection.vue"
+          ),
         meta: { title: "断面采样数据" },
       },
       {
         path: "irriInputFlux",
         name: "irriInputFlux",
-        component: () => import("@/views/User/HmOutFlux/irrigationWater/irriWaterInputFlux.vue"), 
+        component: () =>
+          import(
+            "@/views/User/HmOutFlux/irrigationWater/irriWaterInputFlux.vue"
+          ),
         meta: { title: "灌溉水输入通量" },
       },
       // {
       //   path: "agriInput",
       //   name: "agriInput",
-      //   component: () => import("@/views/User/HmOutFlux/agriInput"), 
+      //   component: () => import("@/views/User/HmOutFlux/agriInput"),
       //   meta: { title: "农产品投入" },
       // },
       {
         path: "farmInputSamplingDesc",
         name: "farmInputSamplingDesc",
-        component: () => import("@/views/User/HmOutFlux/agriInput/farmInputSamplingDesc.vue"),  
+        component: () =>
+          import("@/views/User/HmOutFlux/agriInput/farmInputSamplingDesc.vue"),
         meta: { title: "采样说明" },
       },
       {
         path: "prodInputFlux",
         name: "prodInputFlux",
-        component: () => import("@/views/User/HmOutFlux/agriInput/prodInputFlux.vue"),  
+        component: () =>
+          import("@/views/User/HmOutFlux/agriInput/prodInputFlux.vue"),
         meta: { title: "农产品输入通量" },
       },
       // {
       //   path: "atmosDeposition",
       //   name: "atmosDeposition",
-      //   component: () => import("@/views/User/HmOutFlux/atmosDeposition"), 
+      //   component: () => import("@/views/User/HmOutFlux/atmosDeposition"),
       //   meta: { title: "大气干湿沉降" },
       // },
       {
         path: "AtmosDepositionSamplingDesc",
         name: "AtmosDepositionSamplingDesc",
-        component: () => import("@/views/User/HmOutFlux/atmosDeposition/AtmosDepositionSamplingDesc.vue"), 
+        component: () =>
+          import(
+            "@/views/User/HmOutFlux/atmosDeposition/AtmosDepositionSamplingDesc.vue"
+          ),
         meta: { title: "采样说明" },
       },
       {
         path: "heavyMetalEnterprise",
         name: "heavyMetalEnterprise",
-        component: () => import("@/views/User/HmOutFlux/atmosDeposition/heavyMetalEnterprise.vue"), 
+        component: () =>
+          import(
+            "@/views/User/HmOutFlux/atmosDeposition/heavyMetalEnterprise.vue"
+          ),
         meta: { title: "涉重企业" },
       },
       {
         path: "airSampleData",
         name: "airSampleData",
-        component: () => import("@/views/User/HmOutFlux/atmosDeposition/airSampleData.vue"), 
+        component: () =>
+          import("@/views/User/HmOutFlux/atmosDeposition/airSampleData.vue"),
         meta: { title: "大气采样数据" },
       },
       {
         path: "airInputFlux",
         name: "airInputFlux",
-        component: () => import("@/views/User/HmOutFlux/atmosDeposition/airInputFlux.vue"), 
+        component: () =>
+          import("@/views/User/HmOutFlux/atmosDeposition/airInputFlux.vue"),
         meta: { title: "大气输入通量" },
       },
       // {
       //   path: "hmInFlux",
       //   name: "hmInFlux",
-      //   component: () => import("@/views/User/hmInFlux"), 
+      //   component: () => import("@/views/User/hmInFlux"),
       //   meta: { title: "重金属输入通量" },
       // },
       // {
       //   path: "grainRemoval",
       //   name: "grainRemoval",
-      //   component: () => import("@/views/User/hmInFlux/grainRemoval"), 
+      //   component: () => import("@/views/User/hmInFlux/grainRemoval"),
       //   meta: { title: "籽粒移除" },
       // },
       {
         path: "samplingDesc1",
         name: "samplingDesc1",
-        component: () => import("@/views/User/hmInFlux/grainRemoval/samplingDesc1.vue"), 
+        component: () =>
+          import("@/views/User/hmInFlux/grainRemoval/samplingDesc1.vue"),
         meta: { title: "采样说明" },
       },
       {
         path: "grainRemovalInputFlux",
         name: "grainRemovalInputFlux",
-        component: () => import("@/views/User/hmInFlux/grainRemoval/grainRemovalInputFlux.vue"), 
+        component: () =>
+          import(
+            "@/views/User/hmInFlux/grainRemoval/grainRemovalInputFlux.vue"
+          ),
         meta: { title: "籽粒移除输入通量" },
       },
       // {
       //   path: "strawRemoval",
       //   name: "strawRemoval",
-      //   component: () => import("@/views/User/hmInFlux/strawRemoval"), 
+      //   component: () => import("@/views/User/hmInFlux/strawRemoval"),
       //   meta: { title: "秸秆移除" },
       // },
-       {
+      {
         path: "samplingDesc2",
         name: "samplingDesc2",
-        component: () => import("@/views/User/hmInFlux/strawRemoval/samplingDesc2.vue"), 
+        component: () =>
+          import("@/views/User/hmInFlux/strawRemoval/samplingDesc2.vue"),
         meta: { title: "采样说明" },
-       },
-       {
+      },
+      {
         path: "strawRemovalInputFlux",
         name: "strawRemovalInputFlux",
-        component: () => import("@/views/User/hmInFlux/strawRemoval/strawRemovalInputFlux.vue"), 
+        component: () =>
+          import(
+            "@/views/User/hmInFlux/strawRemoval/strawRemovalInputFlux.vue"
+          ),
         meta: { title: "秸秆移除输入通量" },
-       },
+      },
       // {
       //   path: "subsurfaceLeakage",
       //   name: "subsurfaceLeakage",
-      //   component: () => import("@/views/User/hmInFlux/subsurfaceLeakage"), 
+      //   component: () => import("@/views/User/hmInFlux/subsurfaceLeakage"),
       //   meta: { title: "地下渗漏" },
       // },
       {
-         path: "samplingDesc3",
-         name: "samplingDesc3",
-         component: () => import("@/views/User/hmInFlux/subsurfaceLeakage/samplingDesc3.vue"), 
-         meta: { title: "采样说明" },
-       },
-       {
-         path: "subsurfaceLeakageInputFlux",
-         name: "subsurfaceLeakageInputFlux",
-         component: () => import("@/views/User/hmInFlux/subsurfaceLeakage/subsurfaceLeakageInputFlux.vue"), 
-         meta: { title: "地下渗漏输入通量" },
-       },
+        path: "samplingDesc3",
+        name: "samplingDesc3",
+        component: () =>
+          import("@/views/User/hmInFlux/subsurfaceLeakage/samplingDesc3.vue"),
+        meta: { title: "采样说明" },
+      },
+      {
+        path: "subsurfaceLeakageInputFlux",
+        name: "subsurfaceLeakageInputFlux",
+        component: () =>
+          import(
+            "@/views/User/hmInFlux/subsurfaceLeakage/subsurfaceLeakageInputFlux.vue"
+          ),
+        meta: { title: "地下渗漏输入通量" },
+      },
       // {
       //   path: "surfaceRunoff",
       //   name: "surfaceRunoff",
-      //   component: () => import("@/views/User/hmInFlux/surfaceRunoff"), 
+      //   component: () => import("@/views/User/hmInFlux/surfaceRunoff"),
       //   meta: { title: "地表径流" },
       // },
-       {
-         path: "samplingDesc4",
-         name: "samplingDesc4",
-         component: () => import("@/views/User/hmInFlux/surfaceRunoff/samplingDesc4.vue"), 
-         meta: { title: "采样说明" },
-       },
-       {
-         path: "surfaceRunoffInputFlux",
-         name: "surfaceRunoffInputFlux",
-         component: () => import("@/views/User/hmInFlux/surfaceRunoff/surfaceRunoffInputFlux.vue"), 
-         meta: { title: "地表径流输入通量" },
-       },
+      {
+        path: "samplingDesc4",
+        name: "samplingDesc4",
+        component: () =>
+          import("@/views/User/hmInFlux/surfaceRunoff/samplingDesc4.vue"),
+        meta: { title: "采样说明" },
+      },
+      {
+        path: "surfaceRunoffInputFlux",
+        name: "surfaceRunoffInputFlux",
+        component: () =>
+          import(
+            "@/views/User/hmInFlux/surfaceRunoff/surfaceRunoffInputFlux.vue"
+          ),
+        meta: { title: "地表径流输入通量" },
+      },
       {
         path: "Calculation",
         name: "Calculation",
@@ -270,7 +303,7 @@ const routes = [
         path: "totalInputFlux",
         name: "totalInputFlux",
         component: () =>
-          import("@/views/User/cadmiumPrediction/totalInputFlux.vue"), 
+          import("@/views/User/cadmiumPrediction/totalInputFlux.vue"),
         meta: { title: "输入总通量" },
       },
       {
@@ -283,8 +316,7 @@ const routes = [
       {
         path: "netFlux",
         name: "netFlux",
-        component: () =>
-          import("@/views/User/cadmiumPrediction/netFlux.vue"), // 修复路径
+        component: () => import("@/views/User/cadmiumPrediction/netFlux.vue"), // 修复路径
         meta: { title: "净通量" },
       },
       {
@@ -314,9 +346,7 @@ const routes = [
         path: "CropCadmiumPrediction",
         name: "CropCadmiumPrediction",
         component: () =>
-          import(
-            "@/views/User/cadmiumPrediction/CropCadmiumPrediction.vue"
-          ), // 修复路径
+          import("@/views/User/cadmiumPrediction/CropCadmiumPrediction.vue"), // 修复路径
         meta: { title: "土壤镉作物态含量预测" },
       },
       {
@@ -402,25 +432,74 @@ const routes = [
         meta: { title: "种植风险信息统计" },
       },
       {
-        path: "Visualizatio",
-        name: "Visualizatio",
+        path: "AdminRegionData",
+        name: "AdminRegionData",
         component: () =>
-          import("@/views/Admin/dataManagement/Visualizatio.vue"), // 修复路径
-        meta: { title: "降酸数据管理" },
+          import("@/views/Admin/dataManagement/AdminRegionData.vue"), // 修复路径
+        meta: { title: "行政区域数据" },
       },
       {
-        path: "Visualization",
-        name: "Visualization",
+        path: "irrigationWaterSampleData",
+        name: "irrigationWaterSampleData",
         component: () =>
-          import("@/views/Admin/dataManagement/Visualization.vue"), // 修复路径
-        meta: { title: "反酸数据管理" },
+          import(
+            "@/views/Admin/dataManagement/HeavyMetalInputFluxManager/irrigationWaterSampleData.vue"
+          ),
+        meta: { title: "灌溉水采样数据" },
       },
       {
-        path: "AdminRegionData",
-        name: "AdminRegionData",
+        path: "crossSectionSampleData",
+        name: "crossSectionSampleData",
         component: () =>
-          import("@/views/Admin/dataManagement/AdminRegionData.vue"), // 修复路径
-        meta: { title: "行政区域数据" },
+          import(
+            "@/views/Admin/dataManagement/HeavyMetalInputFluxManager/crossSectionSampleData.vue"
+          ),
+        meta: { title: "断面采样数据" },
+      },
+      {
+        path: "irrigationWaterInputFluxData",
+        name: "irrigationWaterInputFluxData",
+        component: () =>
+          import(
+            "@/views/Admin/dataManagement/HeavyMetalInputFluxManager/irrigationWaterInputFluxData.vue"
+          ),
+        meta: { title: "灌溉水输入通量数据" },
+      },
+      {
+        path: "agriProductInputFluxData",
+        name: "agriProductInputFluxData",
+        component: () =>
+          import(
+            "@/views/Admin/dataManagement/HeavyMetalInputFluxManager/agriProductInputFluxData.vue"
+          ),
+        meta: { title: "农产品输入通量数据" },
+      },
+      {
+        path: "heavyMetalEnterpriseData",
+        name: "heavyMetalEnterpriseData",
+        component: () =>
+          import(
+            "@/views/Admin/dataManagement/HeavyMetalInputFluxManager/heavyMetalEnterpriseData.vue"
+          ),
+        meta: { title: "涉重企业数据" },
+      },
+      {
+        path: "atmosphericSampleData",
+        name: "atmosphericSampleData",
+        component: () =>
+          import(
+            "@/views/Admin/dataManagement/HeavyMetalInputFluxManager/atmosphericSampleData.vue"
+          ),
+        meta: { title: "大气采样数据" },
+      },
+      {
+        path: "atmosphericInputFluxData",
+        name: "atmosphericInputFluxData",
+        component: () =>
+          import(
+            "@/views/Admin/dataManagement/HeavyMetalInputFluxManager/atmosphericInputFluxData.vue"
+          ),
+        meta: { title: "大气输入通量数据" },
       },
       {
         path: "SoilAssessmentUnitData",
@@ -454,9 +533,21 @@ const routes = [
         path: "SoilAcidificationData",
         name: "SoilAcidificationData",
         component: () =>
-          import("@/views/Admin/dataManagement/SoilAcidificationData.vue"),
+          import(
+            "@/views/Admin/dataManagement/Soil Acidification and Acid Reduction Data Management/soilAcidificationData.vue"
+          ),
         meta: { title: "土壤酸化采样数据" },
       },
+      {
+        path: "soilAcidReductionData",
+        name: "soilAcidReductionData",
+        component: () =>
+          import(
+            "@/views/Admin/dataManagement/Soil Acidification and Acid Reduction Data Management/soilAcidReductionData.vue"
+          ),
+        meta: { title: "土壤酸化采样数据" },
+      },
+
       {
         path: "ClimateInfoData",
         name: "ClimateInfoData",
@@ -494,7 +585,7 @@ const routes = [
         path: "UserRegistration",
         name: "UserRegistration",
         component: () =>
-          import("../views/Admin/userManagement/UserRegistration.vue"),
+          import("@/views/Admin/userManagement/UserRegistration.vue"),
         meta: { title: "普通用户" },
       },
       {
@@ -561,19 +652,48 @@ const routes = [
 ];
 
 const router = createRouter({
-  history: createWebHistory(import.meta.env.BASE_URL),
+  history: createWebHistory(),
   routes,
 });
 
 router.beforeEach((to, from, next) => {
   const tokenStore = useTokenStore();
-  if (to.name === "login" && tokenStore.token.userid) {
-    next({ name: "selectCityAndCounty" });
-  } else if (to.matched.some((r) => r.meta.requiresAuth)) {
-    tokenStore.token.userid ? next() : next({ name: "login" });
-  } else {
-    next();
+  const user = tokenStore.userInfo;
+  const isSameRoute = (path: string) => to.fullPath === path;
+
+  // 已登录用户访问 login 页面
+  if (to.name === "login" && user?.userId) {
+    return next("/select-city");
   }
+
+  // 需要登录才能访问的页面
+  if (to.matched.some(r => r.meta.requiresAuth)) {
+    if (!user?.userId) {
+      return next({ name: "login" });
+    }
+
+    // 管理员权限页面
+    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("登录失败:用户类型不匹配");
+
+        // If using Element Plus:
+        // import { ElMessage } from 'element-plus' at the top of the file
+        // ElMessage.error("登录失败:用户类型不匹配");
+
+        // Example using Element Plus:
+        // ElMessage.error("登录失败:用户类型不匹配");
+        if (!isSameRoute("/select-city")) return next({ name: "selectCityAndCounty" });
+        return next();
+      }
+    }
+  }
+
+  next();
 });
 
 export default router;

+ 33 - 37
src/stores/mytoken.ts

@@ -1,42 +1,38 @@
-import { defineStore } from 'pinia';
-import { ref, computed } from 'vue';
+import { defineStore } from "pinia";
 
-interface Token {
-  access_token: any;
-  userid: string | null;
+export interface UserInfo {
+  userId: number;
   name: string;
-  loginType?: string | symbol;
+  loginType: "user" | "admin";
 }
 
-// Removed the token computed property from here as it is redefined inside the useTokenStore function.
-
-export const useTokenStore = defineStore('mytoken', () => {
-  const tokenJson = ref<string>(window.localStorage.getItem("TokenInfo") || "{}");
-
-  const token = computed<Token>(() => {
-    try {
-      return JSON.parse(tokenJson.value) as Token;
-    } catch (e) {
-      console.error("Failed to parse token JSON", e);
-      return { access_token: null, userid: null, name: '' };
-    }
-  });
-
-  function setToken(newToken: string) {
-    tokenJson.value = newToken;
-    window.localStorage.setItem("TokenInfo", newToken);
-  }
-
-  function saveToken(userInfo: { userId: number | null, name: string, loginType?: string }) {
-    const userIdString = userInfo.userId !== null && userInfo.userId !== undefined ? userInfo.userId.toString() : null;
-    const loginTypeString = userInfo.loginType || ''; // 确保 loginType 是字符串
-    const tokenInfo = JSON.stringify({ userid: userIdString, name: userInfo.name, loginType: loginTypeString });
-    setToken(tokenInfo);
-  }
-
-  function clearToken() {
-    setToken("{}"); // 设置为空对象以清除 token
-  }
+interface TokenState {
+  userInfo: UserInfo | null;
+}
 
-  return { token, setToken, saveToken, clearToken };
-});
+export const useTokenStore = defineStore("mytoken", {
+  state: (): TokenState => ({
+    // 初始化时从 localStorage 恢复
+    userInfo: JSON.parse(localStorage.getItem("userInfo") || "null"),
+  }),
+
+  actions: {
+    // 保存用户信息
+    saveToken(userInfo: UserInfo) {
+      this.userInfo = userInfo;
+      localStorage.setItem("userInfo", JSON.stringify(userInfo));
+    },
+
+    // 清理用户信息
+    clearToken() {
+      this.userInfo = null;
+      localStorage.removeItem("userInfo");
+    },
+  },
+
+  getters: {
+    isLoggedIn: (state) => !!state.userInfo,
+    userName: (state) => state.userInfo?.name || "",
+    userType: (state) => state.userInfo?.loginType || "user",
+  },
+});

+ 22 - 22
src/utils/request.ts

@@ -1,48 +1,48 @@
+// @/utils/request.ts
 import axios, { type AxiosRequestConfig, type AxiosResponse, isAxiosError } from "axios";
-import router from '@/router'; // 引入Vue Router实例
-import { useTokenStore } from '@/stores/mytoken'; // 引入Pinia store
+import router from '@/router';
+import { useTokenStore } from '@/stores/mytoken';
 
-// 创建 axios 实例
-const request = axios.create({ 
-    baseURL: import.meta.env.VITE_API_URL, // 使用环境变量配置的API URL
-    timeout: 1000,
+const requestAdmin = axios.create({
+    baseURL: import.meta.env.VITE_API_URL,
+    timeout: 10000,
     headers: {
         'Content-Type': 'application/json',
     },
 });
 
-// 请求拦截器
-request.interceptors.request.use(
+requestAdmin.interceptors.request.use(
     (config) => {
-        if (!config.headers) {
-            config.headers = {} as any; // 强制转换以避免类型不匹配的问题
-        }
-        console.log('Starting Request', config); // 调试信息
-        return config; // 记得返回配置
+        console.log('Starting Request', config);
+        return config;
     },
-    (error) => {
+    (error: unknown) => {
         console.error('Request error:', error);
         return Promise.reject(error);
     }
 );
 
-// 响应拦截器(可选)
-request.interceptors.response.use(
+requestAdmin.interceptors.response.use(
     (response: AxiosResponse) => response,
-    async (error) => {
+    async (error: unknown) => {
         if (isAxiosError(error)) {
             console.error('Response error:', error.message);
             if (error.response && error.response.status === 401) {
-                // 如果是401未授权错误,则清除token并重定向到登录页
                 const tokenStore = useTokenStore();
-                tokenStore.clearToken(); // 清除token信息
-                router.push('/login'); // 重定向到登录页面
+                tokenStore.clearToken();
+                router.push('/login');
             }
+        } else {
+            console.error('Non-Axios error:', error);
         }
         return Promise.reject(error);
     }
 );
 
-console.log('Base URL:', request.defaults.baseURL); // 打印检查
+console.log('Base URL:', requestAdmin.defaults.baseURL);
+
+// ✅ 改为默认导出
+export default requestAdmin; // 👈 修改这里
 
-export default request;
+// ❌ 移除或注释掉原来的命名导出
+// export { requestAdmin };

+ 943 - 0
src/views/Admin/dataManagement/HeavyMetalInputFluxManager/agriProductInputFluxData.vue

@@ -0,0 +1,943 @@
+<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
+        :auto-upload="false"
+        :on-change="handleFileSelect"
+        accept=".xlsx, .csv"
+        class="import-upload"
+      >
+        <el-button
+          :icon="Upload"
+          type="primary"
+          class="custom-button import-button"
+          >导入数据</el-button
+        >
+      </el-upload>
+    </div>
+
+    <!-- 数据表格:仅数字字段NaN显示0,文字字段不处理 -->
+    <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="formatTableValue"
+        align="center"
+        header-align="center"
+      ></el-table-column>
+      <el-table-column label="操作" width="120" align="center">
+        <template #default="scope">
+          <span class="action-buttons">
+            <el-tooltip
+              class="item"
+              effect="dark"
+              content="编辑"
+              placement="top"
+            >
+              <el-button
+                circle
+                :icon="EditPen"
+                @click.stop="openDialog('edit', scope.row)"
+                class="action-button edit-button"
+              ></el-button
+            ></el-tooltip>
+            <el-tooltip
+              class="item"
+              effect="dark"
+              content="删除"
+              placement="top"
+            >
+              <el-button
+                circle
+                :icon="DeleteFilled"
+                @click.stop="deleteItem(scope.row)"
+                class="action-button delete-button"
+              ></el-button
+            ></el-tooltip>
+          </span>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 分页控制 -->
+    <PaginationComponent
+      :total="tableData.length"
+      :currentPage="currentPage4"
+      :pageSize="pageSize4"
+      @update:currentPage="currentPage4 = $event"
+      @update:pageSize="pageSize4 = $event"
+      @size-change="handleSizeChange"
+      @current-change="handleCurrentChange"
+      class="pagination-container"
+    />
+
+    <!-- 新增/编辑对话框:优化样式 -->
+    <el-dialog
+      :title="dialogTitle"
+      v-model="dialogVisible"
+      width="50%"
+      :custom-class="['custom-dialog']"
+      :close-on-click-modal="false"
+      center
+    >
+      <div class="dialog-header">
+        <h2>{{ dialogTitle }}</h2>
+        <div class="header-decoration"></div>
+      </div>
+      <el-form
+        ref="formRef"
+        :model="formData"
+        label-position="top"
+        class="custom-dialog-form"
+      >
+        <el-form-item
+          v-for="col in editableColumns"
+          :key="col.key"
+          :label="col.title"
+          class="custom-form-item"
+          :rules="[
+            {
+              required: isTextField(col.dataKey),
+              message: `请输入${col.title}`,
+              trigger: 'blur',
+            },
+          ]"
+        >
+          <el-input
+            v-model="formData[col.dataKey]"
+            :type="col.inputType || 'text'"
+            class="custom-dialog-input"
+            :placeholder="`请输入${col.title}`"
+          ></el-input>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-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
+          >
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive, computed, onMounted } from "vue";
+import {
+  DeleteFilled,
+  Download,
+  Upload,
+  Plus,
+  EditPen,
+} from "@element-plus/icons-vue";
+import { ElMessage, ElForm } from "element-plus";
+import {
+  table,
+  updateItem,
+  addItem,
+  deleteItemApi,
+  downloadTemplate,
+  exportData,
+  importData,
+} from "@/API/admin";
+import PaginationComponent from "@/components/PaginationComponent.vue";
+
+// 核心:定义字段类型映射(区分文字/数字字段)
+const fieldTypeMap: Record<string, "text" | "number"> = {
+  longitude: "number",
+  latitude: "number",
+  company_name: "text",
+  company_type: "text",
+  county: "text",
+  particulate_emission: "number",
+};
+
+interface Column {
+  key: string;
+  dataKey: string;
+  title: string;
+  width: number;
+  inputType?: string;
+}
+
+// 表字段定义(与fieldTypeMap对应)
+const columns: Column[] = [
+  { key: "id", dataKey: "id", title: "ID", width: 100 },
+  {
+    key: "longitude",
+    dataKey: "longitude",
+    title: "经度坐标",
+    width: 150,
+    inputType: "number",
+  },
+  {
+    key: "latitude",
+    dataKey: "latitude",
+    title: "纬度坐标",
+    width: 150,
+    inputType: "number",
+  },
+  {
+    key: "company_name",
+    dataKey: "company_name",
+    title: "企业名称",
+    width: 200,
+    inputType: "text",
+  },
+  {
+    key: "company_type",
+    dataKey: "company_type",
+    title: "企业类型",
+    width: 150,
+    inputType: "text",
+  },
+  {
+    key: "county",
+    dataKey: "county",
+    title: "所属区县",
+    width: 150,
+    inputType: "text",
+  },
+  {
+    key: "particulate_emission",
+    dataKey: "particulate_emission",
+    title: "大气颗粒物排放量(吨/年)",
+    width: 250,
+    inputType: "number",
+  },
+];
+
+const editableColumns = columns.filter((col) => col.key !== "id");
+
+// 核心变量:统一表名
+const currentTableName = "atmo_company";
+
+// 表格数据相关
+const tableData = ref<any[]>([]);
+const selectedRow = ref<any | null>(null);
+const loading = ref(false);
+const formRef = ref<InstanceType<typeof ElForm> | null>(null); // 表单引用
+
+// 分页相关
+const currentPage4 = ref(1);
+const pageSize4 = ref(10);
+
+// 对话框相关
+const dialogVisible = ref(false);
+const formData = reactive<any>({});
+const dialogMode = ref<"add" | "edit">("add");
+
+// 辅助函数:判断是否为文字字段
+const isTextField = (dataKey: string): boolean => {
+  return fieldTypeMap[dataKey]?.toLowerCase() === "text";
+};
+
+const isNumberField = (dataKey: string): boolean => {
+  return fieldTypeMap[dataKey]?.toLowerCase() === "number";
+};
+
+// 分页和序号处理:仅数字字段处理NaN为0,文字字段保留空
+const pagedTableDataWithIndex = computed(() => {
+  const start = (currentPage4.value - 1) * pageSize4.value;
+  const end = start + pageSize4.value;
+  return tableData.value.slice(start, end).map((row, idx) => {
+    const processedRow = Object.entries(row).reduce((acc, [key, val]) => {
+      // 仅数字字段:NaN/undefined转为0;文字字段:保留原值(空/文字)
+      if (isNumberField(key)) {
+        acc[key] =
+          (typeof val === "number" && isNaN(val)) || val === undefined
+            ? 0
+            : val;
+      } else {
+        acc[key] = val === undefined || val === null ? "" : val; // 文字字段空值显示空字符串
+      }
+      return acc;
+    }, {} as any);
+    return {
+      ...processedRow,
+      displayIndex: start + idx + 1,
+    };
+  });
+});
+
+// 获取表格数据:仅数字字段处理NaN为0
+const fetchTable = async () => {
+  console.log("正在获取表格数据...");
+  try {
+    loading.value = true;
+    const response = await table({ table: currentTableName });
+    console.log("获取到的数据:", response);
+    
+    // 适配后端返回的直接数组格式
+    const rawData = Array.isArray(response.data) ? response.data : [];
+    
+    // 预处理数据:区分字段类型处理
+    tableData.value = rawData.map((row: any) =>
+      Object.entries(row).reduce((acc: any, [key, val]) => {
+        if (isNumberField(key)) {
+          acc[key] =
+            (typeof val === "number" && isNaN(val)) || val === undefined
+              ? 0
+              : val;
+        } else {
+          acc[key] = val === undefined || val === null ? "" : val; // 文字字段空值存空字符串
+        }
+        return acc;
+      }, {})
+    );
+  } catch (error) {
+    console.error("获取数据时出错:", error);
+    ElMessage.error("获取数据失败,请检查网络连接或服务器状态");
+  } finally {
+    loading.value = false;
+  }
+};
+
+onMounted(() => {
+  fetchTable();
+});
+
+// 表格行点击事件:文字字段保留原值,数字字段处理NaN为0
+const handleRowClick = (row: any) => {
+  console.log("点击行数据:", row);
+  selectedRow.value = row;
+  Object.assign(
+    formData,
+    Object.entries(row).reduce((acc: any, [key, val]) => {
+      if (isNumberField(key)) {
+        acc[key] =
+          (typeof val === "number" && isNaN(val)) || val === undefined
+            ? 0
+            : val;
+      } else {
+        acc[key] = val === undefined || val === null ? "" : val; // 文字字段空值显示空字符串
+      }
+      return acc;
+    }, {})
+  );
+};
+
+// 打开新增/编辑对话框:文字字段置空,数字字段可选置0
+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] = isTextField(col.dataKey) ? "" : "";
+    });
+  } else if (row) {
+    selectedRow.value = row;
+    // 编辑时:区分字段类型赋值
+    Object.assign(
+      formData,
+      Object.entries(row).reduce((acc: any, [key, val]) => {
+        if (isNumberField(key)) {
+          acc[key] =
+            (typeof val === "number" && isNaN(val)) || val === undefined
+              ? 0
+              : val;
+        } else {
+          acc[key] = val === undefined || val === null ? "" : val;
+        }
+        return acc;
+      }, {})
+    );
+  }
+};
+
+// 表单数据预处理:文字字段空字符串转为undefined(适配后端)
+function prepareFormData(
+  formData: { [x: string]: any },
+  excludeKeys = ["displayIndex"]
+): { [x: string]: any } {
+  const result: { [x: string]: any } = {};
+  for (const key in formData) {
+    if (!excludeKeys.includes(key)) {
+      let value = formData[key];
+      // 文字字段:空字符串转为undefined(后端可能需要NULL);数字字段:保留原值
+      if (isTextField(key)) {
+        result[key] = value === "" ? undefined : value;
+      } else {
+        result[key] = value === "" ? undefined : value; // 数字字段空值也转为undefined
+      }
+    }
+  }
+  return result;
+}
+
+// 表单提交:文字字段必填校验
+const submitForm = async () => {
+  // 表单校验:文字字段必填
+  if (!formRef.value) return;
+  
+  try {
+    const isValid = await formRef.value.validate();
+    if (!isValid) return;
+  } catch (error) {
+    console.error("表单验证失败:", error);
+    return;
+  }
+
+  try {
+    const dataToSubmit = prepareFormData(formData);
+    let response;
+
+    if (dialogMode.value === "add") {
+      response = await addItem({
+        table: currentTableName,
+        item: dataToSubmit,
+      });
+      
+      // 添加成功后,直接将新数据添加到本地数组
+      if (response.data && response.data.inserted) {
+        tableData.value.push(response.data.inserted);
+      }
+    } else {
+      // 更详细的错误检查和日志
+      console.log("编辑模式 - selectedRow:", selectedRow.value);
+      
+      if (!selectedRow.value) {
+        ElMessage.error("未选中任何记录,请先选择要编辑的记录");
+        return;
+      }
+      
+      // 检查记录是否有 ID 字段
+      if (!selectedRow.value.hasOwnProperty('id')) {
+        console.error("记录缺少ID字段:", selectedRow.value);
+        ElMessage.error("记录格式错误,缺少ID字段");
+        return;
+      }
+      
+      if (!selectedRow.value.id) {
+        console.error("记录ID为空:", selectedRow.value);
+        ElMessage.error("无法找到记录ID,请联系管理员");
+        return;
+      }
+      
+      response = await updateItem({
+        table: currentTableName,
+        id: selectedRow.value.id,
+        update_data: dataToSubmit,
+      });
+      
+      // 更新成功后,更新本地数据
+      const index = tableData.value.findIndex(item => item.id === selectedRow.value.id);
+      if (index > -1) {
+        tableData.value[index] = {...tableData.value[index], ...dataToSubmit};
+      } else {
+        // 如果本地找不到记录,重新获取数据
+        console.warn("本地未找到对应记录,重新获取数据");
+        fetchTable();
+      }
+    }
+
+    dialogVisible.value = false;
+    ElMessage.success(dialogMode.value === "add" ? "添加成功" : "修改成功");
+  } catch (error) {
+    console.error("提交表单时发生错误:", error);
+    let errorMessage = "未知错误";
+    
+    // 更详细的错误信息提取
+    if (error && typeof error === "object") {
+      const err = error as any;
+      if (err.response && err.response.data) {
+        errorMessage = err.response.data.detail || err.response.data.message || JSON.stringify(err.response.data);
+      } else if (err.message) {
+        errorMessage = err.message;
+      }
+    }    
+    ElMessage.error(`提交失败: ${errorMessage}`);
+  }
+};
+
+
+// 表单验证:文字字段必填,数字字段可选(根据业务调整)
+function validateFormData(data: { [x: string]: any }) {
+  return editableColumns.every((col) => {
+    const value = data[col.dataKey];
+    // 文字字段:必填(非空字符串);数字字段:可选(允许0或空)
+    if (isTextField(col.dataKey)) {
+      return value !== "" && value !== undefined && value !== null;
+    } else {
+      return true; // 数字字段可选,空值会被后端处理为NULL
+    }
+  });
+}
+
+// 删除记录
+const deleteItem = async (row: any) => {
+  if (!row) {
+    ElMessage.warning("请先选择一行记录");
+    return;
+  }
+
+  try {
+    await deleteItemApi({
+      table: currentTableName,
+      id: row.id,
+    });
+    
+    // 直接从本地数据中删除,避免重新获取全部数据
+    const index = tableData.value.findIndex((item) => item.id === row.id);
+    if (index > -1) {
+      tableData.value.splice(index, 1);
+    }
+    
+    ElMessage.success("记录删除成功");
+  } catch (error) {
+    console.error("删除记录时发生错误:", error);
+    ElMessage.error("删除失败,请重试");
+  }
+};
+
+// 下载模板
+const downloadTemplateAction = async () => {
+  try {
+    await downloadTemplate(currentTableName);
+    ElMessage.success("模板下载成功");
+  } catch (error) {
+    console.error("下载模板时发生错误:", error);
+    ElMessage.error("下载模板失败,请重试");
+  }
+};
+
+// 导出数据
+const exportDataAction = async () => {
+  try {
+    await exportData(currentTableName);
+    ElMessage.success("数据导出成功");
+  } catch (error) {
+    console.error("导出数据时发生错误:", error);
+    ElMessage.error("导出数据失败,请重试");
+  }
+};
+
+// 处理文件选择(导入数据)
+const handleFileSelect = async (uploadFile: any) => {
+  const file = uploadFile.raw;
+  if (!file) {
+    ElMessage.warning("请选择有效的 .xlsx 或 .csv 文件");
+    return;
+  }
+  await importDataAction(file);
+};
+
+// 导入数据:区分字段类型处理
+const importDataAction = async (file: File) => {
+  try {
+    const response = await importData(currentTableName, file);
+    if (response.success) {
+      const { total_data, new_data, duplicate_data } = response;
+      ElMessage({
+        message: `导入成功!共${total_data}条,新增${new_data}条,重复${duplicate_data}条`,
+        type: "success",
+        duration: 3000,
+      });
+      fetchTable(); // 刷新表格,应用字段类型处理逻辑
+    }
+  } catch (error) {
+    let errorMessage = "数据导入失败";
+    if (
+      error &&
+      typeof error === "object" &&
+      "message" in error &&
+      "response" in error &&
+      typeof (error as any).response === "object"
+    ) {
+      // 适配后端返回的重复记录详情
+      if ((error as any).response?.data?.detail) {
+        const detail = (error as any).response.data.detail;
+        if (detail.duplicates) {
+          // 显示重复记录的行号和文字字段内容
+          const duplicateMsg = detail.duplicates
+            .map(
+              (item: any) =>
+                `第${item.row_number}行(${Object.entries(item.duplicate_fields)
+                  .map(([k, v]) => `${k}=${v || "空"}`)
+                  .join(",")}`
+            )
+            .join(";");
+          errorMessage = `导入失败:${detail.message},重复记录:${duplicateMsg}`;
+        } else {
+          errorMessage = `导入失败:${detail.message || detail}`;
+        }
+      } else {
+        errorMessage += `: ${(error as unknown as Error).message}`;
+      }
+    }
+    ElMessage.error({
+      message: errorMessage,
+      duration: 5000, // 长一点的提示时间,让用户看清重复详情
+    });
+  }
+};
+
+// 分页大小改变
+const handleSizeChange = (val: number) => {
+  pageSize4.value = val;
+  currentPage4.value = 1;
+};
+
+// 当前页改变
+const handleCurrentChange = (val: number) => {
+  currentPage4.value = val;
+};
+
+// 表格值格式化:仅数字字段保留3位小数,文字字段显示原始内容
+const formatTableValue = (row: any, column: any, cellValue: any) => {
+  const dataKey = column.prop;
+  // 文字字段:空值显示空字符串,非空显示原始文字
+  if (isTextField(dataKey)) {
+    return cellValue === undefined || cellValue === null ? "" : cellValue;
+  }
+  // 数字字段:NaN/undefined显示0,保留3位小数
+  if (isNumberField(dataKey)) {
+    if (isNaN(cellValue) || cellValue === undefined || cellValue === null) {
+      return 0;
+    }
+    return typeof cellValue === "number" ? cellValue.toFixed(3) : cellValue;
+  }
+  // 其他字段:默认显示
+  return cellValue;
+};
+
+// 对话框标题
+const dialogTitle = computed(() => {
+  return dialogMode.value === "add"
+    ? `新增记录`
+    : "编辑记录";
+});
+
+// 对话框提交按钮文本
+const dialogSubmitButtonText = computed(() => {
+  return dialogMode.value === "add" ? "添加" : "保存";
+});
+</script>
+
+<style scoped>
+.app-container {
+  padding: 20px 40px;
+  min-height: 100vh;
+  background-color: #f0f2f5;
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.button-group {
+  display: flex;
+  gap: 12px;
+  justify-content: flex-end;
+  width: 100%;
+  margin-bottom: 20px;
+}
+
+.custom-button {
+  color: #fff;
+  border: none;
+  border-radius: 8px;
+  font-size: 14px;
+  padding: 10px 18px;
+  transition: transform 0.3s ease, background-color 0.3s ease;
+  min-width: 130px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.download-button,
+.export-button,
+.add-button,
+.import-button {
+  background-color: #67c23a;
+}
+
+.download-button:hover,
+.export-button:hover,
+.import-button:hover,
+.add-button:hover {
+  background-color: #85ce61;
+  transform: scale(1.05);
+}
+
+.custom-table {
+  width: 100%;
+  border-radius: 8px;
+  overflow: hidden;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+  background-color: #fff;
+  margin-top: 10px;
+}
+
+:deep(.el-table th) {
+  background: linear-gradient(180deg, #61e054, #4db944);
+  color: #fff;
+  font-weight: bold;
+  text-align: center;
+  padding: 14px 0;
+  font-size: 14px;
+}
+
+:deep(.el-table__row:nth-child(odd)) {
+  background-color: #e4fbe5;
+}
+
+:deep(.el-table__row:nth-child(even)) {
+  background-color: #ffffff;
+}
+
+:deep(.el-table td) {
+  padding: 12px 10px;
+  text-align: center;
+  border-bottom: 1px solid #ebeef5;
+}
+
+.action-button {
+  font-size: 16px;
+  margin-right: 5px;
+  border-radius: 50%;
+  transition: transform 0.3s ease, background-color 0.3s ease;
+}
+
+.edit-button {
+  background-color: #409eff;
+  color: #fff;
+}
+
+.edit-button:hover {
+  background-color: #66b1ff;
+  transform: scale(1.1);
+}
+
+.delete-button {
+  background-color: #f56c6c;
+  color: #fff;
+}
+
+.delete-button:hover {
+  background-color: #f78989;
+  transform: scale(1.1);
+}
+
+.el-form-item__label {
+  width: 150px;
+  font-weight: bold;
+}
+
+.el-form-item__content {
+  margin-left: 150px;
+}
+
+.pagination-container {
+  margin-top: 30px;
+  text-align: center;
+}
+
+.action-buttons {
+  display: flex;
+  justify-content: center;
+}
+
+/* ================= 弹框样式优化 ================= */
+:deep(.el-dialog) {
+  border-radius: 16px !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;
+}
+
+/* 弹框头部样式 */
+.dialog-header {
+  position: relative;
+  padding: 20px 24px 10px;
+  text-align: center;
+  background: linear-gradient(90deg, #4db944, #61e054);
+}
+
+.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, #3a9a32, #4fc747);
+  border-radius: 2px;
+  margin-top: 12px;
+  width: 60%;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+/* 表单容器样式 */
+.custom-dialog-form {
+  padding: 25px 40px 15px;
+}
+
+/* 表单项样式优化 */
+.custom-form-item {
+  margin-bottom: 22px !important;
+}
+
+:deep(.el-form-item__label) {
+  display: block;
+  text-align: left;
+  margin-bottom: 8px !important;
+  font-size: 15px;
+  font-weight: 600;
+  color: #4a5568;
+  padding: 0 !important;
+  line-height: 1.5;
+}
+
+/* 输入框样式优化 */
+:deep(.custom-dialog-input .el-input__inner) {
+  height: 44px !important;
+  padding: 0 16px !important;
+  font-size: 15px;
+  border: 1px solid #cbd5e0 !important;
+  border-radius: 10px !important;
+  background-color: #f8fafc;
+  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05);
+  transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
+}
+
+:deep(.custom-dialog-input .el-input__inner:hover) {
+  border-color: #a0aec0 !important;
+}
+
+:deep(.custom-dialog-input .el-input__inner:focus) {
+  border-color: #4db944 !important;
+  box-shadow: 0 0 0 3px rgba(77, 185, 68, 0.15) !important;
+  background-color: #fff;
+}
+
+/* 弹框底部按钮容器 */
+.dialog-footer {
+  display: flex;
+  gap: 16px;
+  justify-content: center;
+  padding: 15px 40px 25px;
+}
+
+/* 按钮基础样式 */
+.custom-cancel-button,
+.custom-submit-button {
+  min-width: 120px;
+  height: 44px;
+  border-radius: 10px;
+  font-size: 16px;
+  font-weight: 500;
+  transition: all 0.3s ease;
+  letter-spacing: 0.5px;
+  border: none;
+  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+}
+
+/* 取消按钮样式优化 */
+.custom-cancel-button {
+  background: linear-gradient(145deg, #f7fafc, #edf2f7);
+  color: #4a5568;
+}
+
+.custom-cancel-button:hover {
+  background: linear-gradient(145deg, #e2e8f0, #cbd5e0);
+  transform: translateY(-2px);
+  box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
+}
+
+/* 提交按钮样式优化 */
+.custom-submit-button {
+  background: linear-gradient(145deg, #4db944, #61e054);
+  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 {
+  transform: translateY(-2px);
+  box-shadow: 0 6px 15px rgba(77, 185, 68, 0.4);
+}
+
+/* 彻底隐藏文件信息相关元素 */
+:deep(.import-upload .el-upload-list) {
+  display: none !important;
+}
+</style>

+ 945 - 0
src/views/Admin/dataManagement/HeavyMetalInputFluxManager/atmosphericInputFluxData.vue

@@ -0,0 +1,945 @@
+<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
+        :auto-upload="false"
+        :on-change="handleFileSelect"
+        accept=".xlsx, .csv"
+        class="import-upload"
+      >
+        <el-button
+          :icon="Upload"
+          type="primary"
+          class="custom-button import-button"
+          >导入数据</el-button
+        >
+      </el-upload>
+    </div>
+
+    <!-- 数据表格:仅数字字段NaN显示0,文字字段不处理 -->
+    <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="formatTableValue"
+        align="center"
+        header-align="center"
+      ></el-table-column>
+      <el-table-column label="操作" width="120" align="center">
+        <template #default="scope">
+          <span class="action-buttons">
+            <el-tooltip
+              class="item"
+              effect="dark"
+              content="编辑"
+              placement="top"
+            >
+              <el-button
+                circle
+                :icon="EditPen"
+                @click.stop="openDialog('edit', scope.row)"
+                class="action-button edit-button"
+              ></el-button
+            ></el-tooltip>
+            <el-tooltip
+              class="item"
+              effect="dark"
+              content="删除"
+              placement="top"
+            >
+              <el-button
+                circle
+                :icon="DeleteFilled"
+                @click.stop="deleteItem(scope.row)"
+                class="action-button delete-button"
+              ></el-button
+            ></el-tooltip>
+          </span>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 分页控制 -->
+    <PaginationComponent
+      :total="tableData.length"
+      :currentPage="currentPage4"
+      :pageSize="pageSize4"
+      @update:currentPage="currentPage4 = $event"
+      @update:pageSize="pageSize4 = $event"
+      @size-change="handleSizeChange"
+      @current-change="handleCurrentChange"
+      class="pagination-container"
+    />
+
+    <!-- 新增/编辑对话框:优化样式 -->
+    <el-dialog
+      :title="dialogTitle"
+      v-model="dialogVisible"
+      width="50%"
+      :custom-class="['custom-dialog']"
+      :close-on-click-modal="false"
+      center
+    >
+      <div class="dialog-header">
+        <h2>{{ dialogTitle }}</h2>
+        <div class="header-decoration"></div>
+      </div>
+      <el-form
+        ref="formRef"
+        :model="formData"
+        label-position="top"
+        class="custom-dialog-form"
+      >
+        <el-form-item
+          v-for="col in editableColumns"
+          :key="col.key"
+          :label="col.title"
+          class="custom-form-item"
+          :rules="[
+            {
+              required: isTextField(col.dataKey),
+              message: `请输入${col.title}`,
+              trigger: 'blur',
+            },
+          ]"
+        >
+          <el-input
+            v-model="formData[col.dataKey]"
+            :type="col.inputType || 'text'"
+            class="custom-dialog-input"
+            :placeholder="`请输入${col.title}`"
+          ></el-input>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-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
+          >
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive, computed, onMounted } from "vue";
+import {
+  DeleteFilled,
+  Download,
+  Upload,
+  Plus,
+  EditPen,
+} from "@element-plus/icons-vue";
+import { ElMessage, ElForm } from "element-plus";
+import {
+  table,
+  updateItem,
+  addItem,
+  deleteItemApi,
+  downloadTemplate,
+  exportData,
+  importData,
+} from "@/API/admin.ts";
+import PaginationComponent from "@/components/PaginationComponent.vue";
+
+// 核心:定义字段类型映射(区分文字/数字字段)
+const fieldTypeMap: Record<string, "text" | "number"> = {
+  longitude: "number",
+  latitude: "number",
+  company_name: "text",
+  company_type: "text",
+  county: "text",
+  particulate_emission: "number",
+};
+
+interface Column {
+  key: string;
+  dataKey: string;
+  title: string;
+  width: number;
+  inputType?: string;
+}
+
+// 表字段定义(与fieldTypeMap对应)
+const columns: Column[] = [
+  { key: "id", dataKey: "id", title: "ID", width: 100 },
+  {
+    key: "longitude",
+    dataKey: "longitude",
+    title: "经度坐标",
+    width: 150,
+    inputType: "number",
+  },
+  {
+    key: "latitude",
+    dataKey: "latitude",
+    title: "纬度坐标",
+    width: 150,
+    inputType: "number",
+  },
+  {
+    key: "company_name",
+    dataKey: "company_name",
+    title: "企业名称",
+    width: 200,
+    inputType: "text",
+  },
+  {
+    key: "company_type",
+    dataKey: "company_type",
+    title: "企业类型",
+    width: 150,
+    inputType: "text",
+  },
+  {
+    key: "county",
+    dataKey: "county",
+    title: "所属区县",
+    width: 150,
+    inputType: "text",
+  },
+  {
+    key: "particulate_emission",
+    dataKey: "particulate_emission",
+    title: "大气颗粒物排放量(吨/年)",
+    width: 250,
+    inputType: "number",
+  },
+];
+
+const editableColumns = columns.filter((col) => col.key !== "id");
+
+// 核心变量:统一表名
+const currentTableName = "atmo_company";
+
+// 表格数据相关
+const tableData = ref<any[]>([]);
+const selectedRow = ref<any | null>(null);
+const loading = ref(false);
+const formRef = ref<InstanceType<typeof ElForm> | null>(null); // 表单引用
+
+// 分页相关
+const currentPage4 = ref(1);
+const pageSize4 = ref(10);
+
+// 对话框相关
+const dialogVisible = ref(false);
+const formData = reactive<any>({});
+const dialogMode = ref<"add" | "edit">("add");
+
+// 辅助函数:判断是否为文字字段
+const isTextField = (dataKey: string): boolean => {
+  return fieldTypeMap[dataKey]?.toLowerCase() === "text";
+};
+
+const isNumberField = (dataKey: string): boolean => {
+  return fieldTypeMap[dataKey]?.toLowerCase() === "number";
+};
+
+// 分页和序号处理:仅数字字段处理NaN为0,文字字段保留空
+const pagedTableDataWithIndex = computed(() => {
+  const start = (currentPage4.value - 1) * pageSize4.value;
+  const end = start + pageSize4.value;
+  return tableData.value.slice(start, end).map((row, idx) => {
+    const processedRow = Object.entries(row).reduce((acc, [key, val]) => {
+      // 仅数字字段:NaN/undefined转为0;文字字段:保留原值(空/文字)
+      if (isNumberField(key)) {
+        acc[key] =
+          (typeof val === "number" && isNaN(val)) || val === undefined
+            ? 0
+            : val;
+      } else {
+        acc[key] = val === undefined || val === null ? "" : val; // 文字字段空值显示空字符串
+      }
+      return acc;
+    }, {} as any);
+    return {
+      ...processedRow,
+      displayIndex: start + idx + 1,
+    };
+  });
+});
+
+// 获取表格数据:仅数字字段处理NaN为0
+const fetchTable = async () => {
+  console.log("正在获取表格数据...");
+  try {
+    loading.value = true;
+    const response = await table({ table: currentTableName });
+    console.log("获取到的数据:", response);
+
+    // 适配后端返回的直接数组格式
+    const rawData = Array.isArray(response.data) ? response.data : [];
+
+    // 预处理数据:区分字段类型处理
+    tableData.value = rawData.map((row: any) =>
+      Object.entries(row).reduce((acc: any, [key, val]) => {
+        if (isNumberField(key)) {
+          acc[key] =
+            (typeof val === "number" && isNaN(val)) || val === undefined
+              ? 0
+              : val;
+        } else {
+          acc[key] = val === undefined || val === null ? "" : val; // 文字字段空值存空字符串
+        }
+        return acc;
+      }, {})
+    );
+  } catch (error) {
+    console.error("获取数据时出错:", error);
+    ElMessage.error("获取数据失败,请检查网络连接或服务器状态");
+  } finally {
+    loading.value = false;
+  }
+};
+
+onMounted(() => {
+  fetchTable();
+});
+
+// 表格行点击事件:文字字段保留原值,数字字段处理NaN为0
+const handleRowClick = (row: any) => {
+  console.log("点击行数据:", row);
+  selectedRow.value = row;
+  Object.assign(
+    formData,
+    Object.entries(row).reduce((acc: any, [key, val]) => {
+      if (isNumberField(key)) {
+        acc[key] =
+          (typeof val === "number" && isNaN(val)) || val === undefined
+            ? 0
+            : val;
+      } else {
+        acc[key] = val === undefined || val === null ? "" : val; // 文字字段空值显示空字符串
+      }
+      return acc;
+    }, {})
+  );
+};
+
+// 打开新增/编辑对话框:文字字段置空,数字字段可选置0
+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] = isTextField(col.dataKey) ? "" : "";
+    });
+  } else if (row) {
+    selectedRow.value = row;
+    // 编辑时:区分字段类型赋值
+    Object.assign(
+      formData,
+      Object.entries(row).reduce((acc: any, [key, val]) => {
+        if (isNumberField(key)) {
+          acc[key] =
+            (typeof val === "number" && isNaN(val)) || val === undefined
+              ? 0
+              : val;
+        } else {
+          acc[key] = val === undefined || val === null ? "" : val;
+        }
+        return acc;
+      }, {})
+    );
+  }
+};
+
+// 表单数据预处理:文字字段空字符串转为undefined(适配后端)
+function prepareFormData(
+  formData: { [x: string]: any },
+  excludeKeys = ["displayIndex"]
+): { [x: string]: any } {
+  const result: { [x: string]: any } = {};
+  for (const key in formData) {
+    if (!excludeKeys.includes(key)) {
+      let value = formData[key];
+      // 文字字段:空字符串转为undefined(后端可能需要NULL);数字字段:保留原值
+      if (isTextField(key)) {
+        result[key] = value === "" ? undefined : value;
+      } else {
+        result[key] = value === "" ? undefined : value; // 数字字段空值也转为undefined
+      }
+    }
+  }
+  return result;
+}
+
+// 表单提交:文字字段必填校验
+const submitForm = async () => {
+  // 表单校验:文字字段必填
+  if (!formRef.value) return;
+
+  try {
+    const isValid = await formRef.value.validate();
+    if (!isValid) return;
+  } catch (error) {
+    console.error("表单验证失败:", error);
+    return;
+  }
+
+  try {
+    const dataToSubmit = prepareFormData(formData);
+    let response;
+
+    if (dialogMode.value === "add") {
+      response = await addItem({
+        table: currentTableName,
+        item: dataToSubmit,
+      });
+
+      // 添加成功后,直接将新数据添加到本地数组
+      if (response.data && response.data.inserted) {
+        tableData.value.push(response.data.inserted);
+      }
+    } else {
+      // 更详细的错误检查和日志
+      console.log("编辑模式 - selectedRow:", selectedRow.value);
+
+      if (!selectedRow.value) {
+        ElMessage.error("未选中任何记录,请先选择要编辑的记录");
+        return;
+      }
+
+      // 检查记录是否有 ID 字段
+      if (!selectedRow.value.hasOwnProperty("id")) {
+        console.error("记录缺少ID字段:", selectedRow.value);
+        ElMessage.error("记录格式错误,缺少ID字段");
+        return;
+      }
+
+      if (!selectedRow.value.id) {
+        console.error("记录ID为空:", selectedRow.value);
+        ElMessage.error("无法找到记录ID,请联系管理员");
+        return;
+      }
+
+      response = await updateItem({
+        table: currentTableName,
+        id: selectedRow.value.id,
+        update_data: dataToSubmit,
+      });
+
+      // 更新成功后,更新本地数据
+      const index = tableData.value.findIndex(
+        (item) => item.id === selectedRow.value.id
+      );
+      if (index > -1) {
+        tableData.value[index] = { ...tableData.value[index], ...dataToSubmit };
+      } else {
+        // 如果本地找不到记录,重新获取数据
+        console.warn("本地未找到对应记录,重新获取数据");
+        fetchTable();
+      }
+    }
+
+    dialogVisible.value = false;
+    ElMessage.success(dialogMode.value === "add" ? "添加成功" : "修改成功");
+  } catch (error) {
+    console.error("提交表单时发生错误:", error);
+    let errorMessage = "未知错误";
+
+    // 更详细的错误信息提取
+    if (error && typeof error === "object") {
+      const err = error as any;
+      if (err.response && err.response.data) {
+        errorMessage =
+          err.response.data.detail ||
+          err.response.data.message ||
+          JSON.stringify(err.response.data);
+      } else if (err.message) {
+        errorMessage = err.message;
+      }
+    }
+    ElMessage.error(`提交失败: ${errorMessage}`);
+  }
+};
+
+// 表单验证:文字字段必填,数字字段可选(根据业务调整)
+function validateFormData(data: { [x: string]: any }) {
+  return editableColumns.every((col) => {
+    const value = data[col.dataKey];
+    // 文字字段:必填(非空字符串);数字字段:可选(允许0或空)
+    if (isTextField(col.dataKey)) {
+      return value !== "" && value !== undefined && value !== null;
+    } else {
+      return true; // 数字字段可选,空值会被后端处理为NULL
+    }
+  });
+}
+
+// 删除记录
+const deleteItem = async (row: any) => {
+  if (!row) {
+    ElMessage.warning("请先选择一行记录");
+    return;
+  }
+
+  try {
+    await deleteItemApi({
+      table: currentTableName,
+      id: row.id,
+    });
+
+    // 直接从本地数据中删除,避免重新获取全部数据
+    const index = tableData.value.findIndex((item) => item.id === row.id);
+    if (index > -1) {
+      tableData.value.splice(index, 1);
+    }
+
+    ElMessage.success("记录删除成功");
+  } catch (error) {
+    console.error("删除记录时发生错误:", error);
+    ElMessage.error("删除失败,请重试");
+  }
+};
+
+// 下载模板
+const downloadTemplateAction = async () => {
+  try {
+    await downloadTemplate(currentTableName);
+    ElMessage.success("模板下载成功");
+  } catch (error) {
+    console.error("下载模板时发生错误:", error);
+    ElMessage.error("下载模板失败,请重试");
+  }
+};
+
+// 导出数据
+const exportDataAction = async () => {
+  try {
+    await exportData(currentTableName);
+    ElMessage.success("数据导出成功");
+  } catch (error) {
+    console.error("导出数据时发生错误:", error);
+    ElMessage.error("导出数据失败,请重试");
+  }
+};
+
+// 处理文件选择(导入数据)
+const handleFileSelect = async (uploadFile: any) => {
+  const file = uploadFile.raw;
+  if (!file) {
+    ElMessage.warning("请选择有效的 .xlsx 或 .csv 文件");
+    return;
+  }
+  await importDataAction(file);
+};
+
+// 导入数据:区分字段类型处理
+const importDataAction = async (file: File) => {
+  try {
+    const response = await importData(currentTableName, file);
+    if (response.success) {
+      const { total_data, new_data, duplicate_data } = response;
+      ElMessage({
+        message: `导入成功!共${total_data}条,新增${new_data}条,重复${duplicate_data}条`,
+        type: "success",
+        duration: 3000,
+      });
+      fetchTable(); // 刷新表格,应用字段类型处理逻辑
+    }
+  } catch (error) {
+    let errorMessage = "数据导入失败";
+    if (
+      error &&
+      typeof error === "object" &&
+      "message" in error &&
+      "response" in error &&
+      typeof (error as any).response === "object"
+    ) {
+      // 适配后端返回的重复记录详情
+      if ((error as any).response?.data?.detail) {
+        const detail = (error as any).response.data.detail;
+        if (detail.duplicates) {
+          // 显示重复记录的行号和文字字段内容
+          const duplicateMsg = detail.duplicates
+            .map(
+              (item: any) =>
+                `第${item.row_number}行(${Object.entries(item.duplicate_fields)
+                  .map(([k, v]) => `${k}=${v || "空"}`)
+                  .join(",")}`
+            )
+            .join(";");
+          errorMessage = `导入失败:${detail.message},重复记录:${duplicateMsg}`;
+        } else {
+          errorMessage = `导入失败:${detail.message || detail}`;
+        }
+      } else {
+        errorMessage += `: ${(error as unknown as Error).message}`;
+      }
+    }
+    ElMessage.error({
+      message: errorMessage,
+      duration: 5000, // 长一点的提示时间,让用户看清重复详情
+    });
+  }
+};
+
+// 分页大小改变
+const handleSizeChange = (val: number) => {
+  pageSize4.value = val;
+  currentPage4.value = 1;
+};
+
+// 当前页改变
+const handleCurrentChange = (val: number) => {
+  currentPage4.value = val;
+};
+
+// 表格值格式化:仅数字字段保留3位小数,文字字段显示原始内容
+const formatTableValue = (row: any, column: any, cellValue: any) => {
+  const dataKey = column.prop;
+  // 文字字段:空值显示空字符串,非空显示原始文字
+  if (isTextField(dataKey)) {
+    return cellValue === undefined || cellValue === null ? "" : cellValue;
+  }
+  // 数字字段:NaN/undefined显示0,保留3位小数
+  if (isNumberField(dataKey)) {
+    if (isNaN(cellValue) || cellValue === undefined || cellValue === null) {
+      return 0;
+    }
+    return typeof cellValue === "number" ? cellValue.toFixed(3) : cellValue;
+  }
+  // 其他字段:默认显示
+  return cellValue;
+};
+
+// 对话框标题
+const dialogTitle = computed(() => {
+  return dialogMode.value === "add" ? `新增记录` : "编辑记录";
+});
+
+// 对话框提交按钮文本
+const dialogSubmitButtonText = computed(() => {
+  return dialogMode.value === "add" ? "添加" : "保存";
+});
+</script>
+
+<style scoped>
+.app-container {
+  padding: 20px 40px;
+  min-height: 100vh;
+  background-color: #f0f2f5;
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.button-group {
+  display: flex;
+  gap: 12px;
+  justify-content: flex-end;
+  width: 100%;
+  margin-bottom: 20px;
+}
+
+.custom-button {
+  color: #fff;
+  border: none;
+  border-radius: 8px;
+  font-size: 14px;
+  padding: 10px 18px;
+  transition: transform 0.3s ease, background-color 0.3s ease;
+  min-width: 130px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.download-button,
+.export-button,
+.add-button,
+.import-button {
+  background-color: #67c23a;
+}
+
+.download-button:hover,
+.export-button:hover,
+.import-button:hover,
+.add-button:hover {
+  background-color: #85ce61;
+  transform: scale(1.05);
+}
+
+.custom-table {
+  width: 100%;
+  border-radius: 8px;
+  overflow: hidden;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+  background-color: #fff;
+  margin-top: 10px;
+}
+
+:deep(.el-table th) {
+  background: linear-gradient(180deg, #61e054, #4db944);
+  color: #fff;
+  font-weight: bold;
+  text-align: center;
+  padding: 14px 0;
+  font-size: 14px;
+}
+
+:deep(.el-table__row:nth-child(odd)) {
+  background-color: #e4fbe5;
+}
+
+:deep(.el-table__row:nth-child(even)) {
+  background-color: #ffffff;
+}
+
+:deep(.el-table td) {
+  padding: 12px 10px;
+  text-align: center;
+  border-bottom: 1px solid #ebeef5;
+}
+
+.action-button {
+  font-size: 16px;
+  margin-right: 5px;
+  border-radius: 50%;
+  transition: transform 0.3s ease, background-color 0.3s ease;
+}
+
+.edit-button {
+  background-color: #409eff;
+  color: #fff;
+}
+
+.edit-button:hover {
+  background-color: #66b1ff;
+  transform: scale(1.1);
+}
+
+.delete-button {
+  background-color: #f56c6c;
+  color: #fff;
+}
+
+.delete-button:hover {
+  background-color: #f78989;
+  transform: scale(1.1);
+}
+
+.el-form-item__label {
+  width: 150px;
+  font-weight: bold;
+}
+
+.el-form-item__content {
+  margin-left: 150px;
+}
+
+.pagination-container {
+  margin-top: 30px;
+  text-align: center;
+}
+
+.action-buttons {
+  display: flex;
+  justify-content: center;
+}
+
+/* ================= 弹框样式优化 ================= */
+:deep(.el-dialog) {
+  border-radius: 16px !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;
+}
+
+/* 弹框头部样式 */
+.dialog-header {
+  position: relative;
+  padding: 20px 24px 10px;
+  text-align: center;
+  background: linear-gradient(90deg, #4db944, #61e054);
+}
+
+.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, #3a9a32, #4fc747);
+  border-radius: 2px;
+  margin-top: 12px;
+  width: 60%;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+/* 表单容器样式 */
+.custom-dialog-form {
+  padding: 25px 40px 15px;
+}
+
+/* 表单项样式优化 */
+.custom-form-item {
+  margin-bottom: 22px !important;
+}
+
+:deep(.el-form-item__label) {
+  display: block;
+  text-align: left;
+  margin-bottom: 8px !important;
+  font-size: 15px;
+  font-weight: 600;
+  color: #4a5568;
+  padding: 0 !important;
+  line-height: 1.5;
+}
+
+/* 输入框样式优化 */
+:deep(.custom-dialog-input .el-input__inner) {
+  height: 44px !important;
+  padding: 0 16px !important;
+  font-size: 15px;
+  border: 1px solid #cbd5e0 !important;
+  border-radius: 10px !important;
+  background-color: #f8fafc;
+  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05);
+  transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
+}
+
+:deep(.custom-dialog-input .el-input__inner:hover) {
+  border-color: #a0aec0 !important;
+}
+
+:deep(.custom-dialog-input .el-input__inner:focus) {
+  border-color: #4db944 !important;
+  box-shadow: 0 0 0 3px rgba(77, 185, 68, 0.15) !important;
+  background-color: #fff;
+}
+
+/* 弹框底部按钮容器 */
+.dialog-footer {
+  display: flex;
+  gap: 16px;
+  justify-content: center;
+  padding: 15px 40px 25px;
+}
+
+/* 按钮基础样式 */
+.custom-cancel-button,
+.custom-submit-button {
+  min-width: 120px;
+  height: 44px;
+  border-radius: 10px;
+  font-size: 16px;
+  font-weight: 500;
+  transition: all 0.3s ease;
+  letter-spacing: 0.5px;
+  border: none;
+  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+}
+
+/* 取消按钮样式优化 */
+.custom-cancel-button {
+  background: linear-gradient(145deg, #f7fafc, #edf2f7);
+  color: #4a5568;
+}
+
+.custom-cancel-button:hover {
+  background: linear-gradient(145deg, #e2e8f0, #cbd5e0);
+  transform: translateY(-2px);
+  box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
+}
+
+/* 提交按钮样式优化 */
+.custom-submit-button {
+  background: linear-gradient(145deg, #4db944, #61e054);
+  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 {
+  transform: translateY(-2px);
+  box-shadow: 0 6px 15px rgba(77, 185, 68, 0.4);
+}
+
+/* 彻底隐藏文件信息相关元素 */
+:deep(.import-upload .el-upload-list) {
+  display: none !important;
+}
+</style>

+ 1127 - 0
src/views/Admin/dataManagement/HeavyMetalInputFluxManager/atmosphericSampleData.vue

@@ -0,0 +1,1127 @@
+<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
+        :auto-upload="false"
+        :on-change="handleFileSelect"
+        accept=".xlsx, .csv"
+        class="import-upload"
+      >
+        <el-button
+          :icon="Upload"
+          type="primary"
+          class="custom-button import-button"
+          >导入数据</el-button
+        >
+      </el-upload>
+    </div>
+
+    <!-- 数据表格:仅数字字段NaN显示0,文字字段不处理 -->
+    <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="formatTableValue"
+        align="center"
+        header-align="center"
+      ></el-table-column>
+      <el-table-column label="操作" width="120" align="center">
+        <template #default="scope">
+          <span class="action-buttons">
+            <el-tooltip
+              class="item"
+              effect="dark"
+              content="编辑"
+              placement="top"
+            >
+              <el-button
+                circle
+                :icon="EditPen"
+                @click.stop="openDialog('edit', scope.row)"
+                class="action-button edit-button"
+              ></el-button
+            ></el-tooltip>
+            <el-tooltip
+              class="item"
+              effect="dark"
+              content="删除"
+              placement="top"
+            >
+              <el-button
+                circle
+                :icon="DeleteFilled"
+                @click.stop="deleteItem(scope.row)"
+                class="action-button delete-button"
+              ></el-button
+            ></el-tooltip>
+          </span>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 分页控制 -->
+    <PaginationComponent
+      :total="tableData.length"
+      :currentPage="currentPage4"
+      :pageSize="pageSize4"
+      @update:currentPage="currentPage4 = $event"
+      @update:pageSize="pageSize4 = $event"
+      @size-change="handleSizeChange"
+      @current-change="handleCurrentChange"
+      class="pagination-container"
+    />
+
+    <!-- 新增/编辑对话框:优化样式 -->
+    <el-dialog
+      :title="dialogTitle"
+      v-model="dialogVisible"
+      width="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-form
+        ref="formRef"
+        :model="formData"
+        label-position="top"
+        class="custom-dialog-form"
+      >
+        <el-form-item
+          v-for="col in editableColumns"
+          :key="col.key"
+          :label="col.title"
+          class="custom-form-item"
+          :rules="[
+            {
+              required: isTextField(col.dataKey),
+              message: `请输入${col.title}`,
+              trigger: 'blur',
+            },
+          ]"
+        >
+          <el-input
+            v-model="formData[col.dataKey]"
+            :type="col.inputType || 'text'"
+            class="custom-dialog-input"
+            :placeholder="`请输入${col.title}`"
+            :step="col.step || undefined"
+          ></el-input>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-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
+          >
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive, computed, onMounted } from "vue";
+import {
+  DeleteFilled,
+  Download,
+  Upload,
+  Plus,
+  EditPen,
+} from "@element-plus/icons-vue";
+import { ElMessage, ElForm } from "element-plus";
+import {
+  table,
+  updateItem,
+  addItem,
+  deleteItemApi,
+  downloadTemplate,
+  exportData,
+  importData,
+} from "@/API/admin";
+import PaginationComponent from "@/components/PaginationComponent.vue";
+
+//  核心1:字段类型映射(区分文字/数字,控制NaN处理)
+const fieldTypeMap: Record<string, "text" | "number"> = {
+  id: "number", // 采样点ID(数字)
+  longitude: "number", // 经度坐标(数字)
+  latitude: "number", // 纬度坐标(数字)
+  sampling_location: "text", // 采样地点(文字)
+  start_time: "text", // 采样开始时间(文字,建议后端接收格式:YYYY-MM-DD HH:mm:ss)
+  end_time: "text", // 采样结束时间(文字)
+  cumulative_time: "number", // 累计采样时长(数字)
+  average_flow_rate: "number", // 平均流量(L/min)(数字)
+  cumulative_true_volume: "number", // 实况采样体积(L)(数字)
+  cumulative_standard_volume: "number", // 标准采样体积(L)(数字)
+  sample_type: "text", // 样品类型(文字)
+  sample_name: "text", // 样品名称(文字)
+  Cr_particulate: "number", // Cr含量(mg/kg)(数字)
+  As_particulate: "number", // As含量(mg/kg)(数字)
+  Cd_particulate: "number", // Cd含量(mg/kg)(数字)
+  Hg_particulate: "number", // Hg含量(mg/kg)(数字)
+  Pb_particulate: "number", // Pb含量(mg/kg)(数字)
+  particle_weight: "number", // 颗粒物重量(mg)(数字)
+  standard_volume: "number", // 标准体积(m³)(数字)
+  particle_concentration: "number", // 颗粒物浓度(μg/m³)(数字)
+  sample_code: "text", // 样品编码(文字)
+  temperature: "number", // 温度(℃)(数字)
+  pressure: "number", // 气压(kPa)(数字)
+  humidity: "number", // 湿度(%)(数字)
+  wind_speed: "number", // 风速(m/s)(数字)
+  wind_direction: "text", // 风向(文字)
+};
+
+// 核心2:必填字段定义(控制表单校验)
+const requiredNumberFields = [
+  "longitude",
+  "latitude",
+  "sampling_location",
+  "start_time",
+  "end_time",
+  "sample_type",
+  "sample_name",
+  "sample_code",
+];
+
+interface Column {
+  key: string;
+  dataKey: string;
+  title: string;
+  width: number;
+  inputType?: string;
+  step?: string; // 数字输入框精度(如小数位)
+}
+
+// 核心3:新表字段定义(与新需求完全匹配)
+const columns: Column[] = [
+  {
+    key: "id",
+    dataKey: "id",
+    title: "采样点ID",
+    width: 120,
+    inputType: "number",
+  },
+  {
+    key: "longitude",
+    dataKey: "longitude",
+    title: "经度坐标",
+    width: 160,
+    inputType: "number",
+    step: "0.000001",
+  },
+  {
+    key: "latitude",
+    dataKey: "latitude",
+    title: "纬度坐标",
+    width: 160,
+    inputType: "number",
+    step: "0.000001",
+  },
+  {
+    key: "sampling_location",
+    dataKey: "sampling_location",
+    title: "采样地点",
+    width: 220,
+    inputType: "text",
+  },
+  {
+    key: "start_time",
+    dataKey: "start_time",
+    title: "采样开始时间",
+    width: 220,
+    inputType: "text",
+  },
+  {
+    key: "end_time",
+    dataKey: "end_time",
+    title: "采样结束时间",
+    width: 220,
+    inputType: "text",
+  },
+  {
+    key: "cumulative_time",
+    dataKey: "cumulative_time",
+    title: "累计采样时长",
+    width: 160,
+    inputType: "number",
+  },
+  {
+    key: "average_flow_rate",
+    dataKey: "average_flow_rate",
+    title: "平均流量(L/min)",
+    width: 180,
+    inputType: "number",
+    step: "0.01",
+  },
+  {
+    key: "cumulative_true_volume",
+    dataKey: "cumulative_true_volume",
+    title: "实况采样体积(L)",
+    width: 180,
+    inputType: "number",
+    step: "0.01",
+  },
+  {
+    key: "cumulative_standard_volume",
+    dataKey: "cumulative_standard_volume",
+    title: "标准采样体积(L)",
+    width: 180,
+    inputType: "number",
+    step: "0.01",
+  },
+  {
+    key: "sample_type",
+    dataKey: "sample_type",
+    title: "样品类型",
+    width: 160,
+    inputType: "text",
+  },
+  {
+    key: "sample_name",
+    dataKey: "sample_name",
+    title: "样品名称",
+    width: 180,
+    inputType: "text",
+  },
+  {
+    key: "Cr_particulate",
+    dataKey: "Cr_particulate",
+    title: "Cr含量(mg/kg)",
+    width: 180,
+    inputType: "number",
+    step: "0.001",
+  },
+  {
+    key: "As_particulate",
+    dataKey: "As_particulate",
+    title: "As含量(mg/kg)",
+    width: 180,
+    inputType: "number",
+    step: "0.001",
+  },
+  {
+    key: "Cd_particulate",
+    dataKey: "Cd_particulate",
+    title: "Cd含量(mg/kg)",
+    width: 180,
+    inputType: "number",
+    step: "0.001",
+  },
+  {
+    key: "Hg_particulate",
+    dataKey: "Hg_particulate",
+    title: "Hg含量(mg/kg)",
+    width: 180,
+    inputType: "number",
+    step: "0.001",
+  },
+  {
+    key: "Pb_particulate",
+    dataKey: "Pb_particulate",
+    title: "Pb含量(mg/kg)",
+    width: 180,
+    inputType: "number",
+    step: "0.001",
+  },
+  {
+    key: "particle_weight",
+    dataKey: "particle_weight",
+    title: "颗粒物重量(mg)",
+    width: 180,
+    inputType: "number",
+    step: "0.001",
+  },
+  {
+    key: "standard_volume",
+    dataKey: "standard_volume",
+    title: "标准体积(m³)",
+    width: 180,
+    inputType: "number",
+    step: "0.001",
+  },
+  {
+    key: "particle_concentration",
+    dataKey: "particle_concentration",
+    title: "颗粒物浓度(μg/m³)",
+    width: 200,
+    inputType: "number",
+    step: "0.1",
+  },
+  {
+    key: "sample_code",
+    dataKey: "sample_code",
+    title: "样品编码",
+    width: 180,
+    inputType: "text",
+  },
+  {
+    key: "temperature",
+    dataKey: "temperature",
+    title: "温度(℃)",
+    width: 140,
+    inputType: "number",
+    step: "0.1",
+  },
+  {
+    key: "pressure",
+    dataKey: "pressure",
+    title: "气压(kPa)",
+    width: 140,
+    inputType: "number",
+    step: "0.01",
+  },
+  {
+    key: "humidity",
+    dataKey: "humidity",
+    title: "湿度(%)",
+    width: 140,
+    inputType: "number",
+    step: "0.1",
+  },
+  {
+    key: "wind_speed",
+    dataKey: "wind_speed",
+    title: "风速(m/s)",
+    width: 140,
+    inputType: "number",
+    step: "0.1",
+  },
+  {
+    key: "wind_direction",
+    dataKey: "wind_direction",
+    title: "风向",
+    width: 140,
+    inputType: "text",
+  },
+];
+
+const editableColumns = columns.filter((col) => col.key !== "id"); // 编辑排除自增ID
+const currentTableName = "Atmo_sample_data"; //  核心4:更新表名
+
+// 表格数据相关
+const tableData = ref<any[]>([]);
+const selectedRow = ref<any | null>(null);
+const loading = ref(false);
+const formRef = ref<InstanceType<typeof ElForm> | null>(null); // 表单引用
+
+// 分页相关
+const currentPage4 = ref(1);
+const pageSize4 = ref(10);
+
+// 对话框相关
+const dialogVisible = ref(false);
+const formData = reactive<any>({});
+const dialogMode = ref<"add" | "edit">("add");
+
+// 辅助函数:判断是否为文字字段
+const isTextField = (dataKey: string): boolean => {
+  return fieldTypeMap[dataKey]?.toLowerCase() === "text";
+};
+
+const isNumberField = (dataKey: string): boolean => {
+  return fieldTypeMap[dataKey]?.toLowerCase() === "number";
+};
+
+// 分页和序号处理:仅数字字段处理NaN为0,文字字段保留空
+const pagedTableDataWithIndex = computed(() => {
+  const start = (currentPage4.value - 1) * pageSize4.value;
+  const end = start + pageSize4.value;
+  return tableData.value.slice(start, end).map((row, idx) => {
+    const processedRow = Object.entries(row).reduce((acc, [key, val]) => {
+      // 仅数字字段:NaN/undefined转为0;文字字段:保留原值(空/文字)
+      if (isNumberField(key)) {
+        acc[key] =
+          (typeof val === "number" && isNaN(val)) || val === undefined
+            ? 0
+            : val;
+      } else {
+        acc[key] = val === undefined || val === null ? "" : val; // 文字字段空值显示空字符串
+      }
+      return acc;
+    }, {} as any);
+    return {
+      ...processedRow,
+      displayIndex: start + idx + 1,
+    };
+  });
+});
+
+// 获取表格数据:仅数字字段处理NaN为0
+const fetchTable = async () => {
+  console.log("正在获取表格数据...");
+  try {
+    loading.value = true;
+    const response = await table({ table: currentTableName });
+    console.log("获取到的数据:", response);
+
+    // 适配后端返回的直接数组格式
+    const rawData = Array.isArray(response.data) ? response.data : [];
+
+    // 预处理数据:区分字段类型处理
+    tableData.value = rawData.map((row: any) =>
+      Object.entries(row).reduce((acc: any, [key, val]) => {
+        if (isNumberField(key)) {
+          acc[key] =
+            (typeof val === "number" && isNaN(val)) || val === undefined
+              ? 0
+              : val;
+        } else {
+          acc[key] = val === undefined || val === null ? "" : val; // 文字字段空值存空字符串
+        }
+        return acc;
+      }, {})
+    );
+  } catch (error) {
+    console.error("获取数据时出错:", error);
+    ElMessage.error("获取数据失败,请检查网络连接或服务器状态");
+  } finally {
+    loading.value = false;
+  }
+};
+
+onMounted(() => {
+  fetchTable();
+});
+
+// 表格行点击事件:文字字段保留原值,数字字段处理NaN为0
+const handleRowClick = (row: any) => {
+  console.log("点击行数据:", row);
+  selectedRow.value = row;
+  Object.assign(
+    formData,
+    Object.entries(row).reduce((acc: any, [key, val]) => {
+      if (isNumberField(key)) {
+        acc[key] =
+          (typeof val === "number" && isNaN(val)) || val === undefined
+            ? 0
+            : val;
+      } else {
+        acc[key] = val === undefined || val === null ? "" : val; // 文字字段空值显示空字符串
+      }
+      return acc;
+    }, {})
+  );
+};
+
+// 打开新增/编辑对话框:文字字段置空,数字字段可选置0
+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] = isTextField(col.dataKey) ? "" : "";
+    });
+  } else if (row) {
+    selectedRow.value = row;
+    // 编辑时:区分字段类型赋值
+    Object.assign(
+      formData,
+      Object.entries(row).reduce((acc: any, [key, val]) => {
+        if (isNumberField(key)) {
+          acc[key] =
+            (typeof val === "number" && isNaN(val)) || val === undefined
+              ? 0
+              : val;
+        } else {
+          acc[key] = val === undefined || val === null ? "" : val;
+        }
+        return acc;
+      }, {})
+    );
+  }
+};
+
+// 表单数据预处理:文字字段空字符串转为undefined(适配后端)
+function prepareFormData(
+  formData: { [x: string]: any },
+  excludeKeys = ["displayIndex"]
+): { [x: string]: any } {
+  const result: { [x: string]: any } = {};
+  for (const key in formData) {
+    if (!excludeKeys.includes(key)) {
+      let value = formData[key];
+      // 文字字段:空字符串转为undefined(后端可能需要NULL);数字字段:保留原值
+      if (isTextField(key)) {
+        result[key] = value === "" ? undefined : value;
+      } else {
+        result[key] = value === "" ? undefined : value; // 数字字段空值也转为undefined
+      }
+    }
+  }
+  return result;
+}
+
+// 表单提交:文字字段必填校验
+const submitForm = async () => {
+  // 表单校验:文字字段必填
+  if (!formRef.value) return;
+
+  try {
+    const isValid = await formRef.value.validate();
+    if (!isValid) return;
+  } catch (error) {
+    console.error("表单验证失败:", error);
+    return;
+  }
+
+  try {
+    const dataToSubmit = prepareFormData(formData);
+    let response;
+
+    if (dialogMode.value === "add") {
+      response = await addItem({
+        table: currentTableName,
+        item: dataToSubmit,
+      });
+
+      // 添加成功后,直接将新数据添加到本地数组
+      if (response.data && response.data.inserted) {
+        tableData.value.push(response.data.inserted);
+      }
+    } else {
+      // 更详细的错误检查和日志
+      console.log("编辑模式 - selectedRow:", selectedRow.value);
+
+      if (!selectedRow.value) {
+        ElMessage.error("未选中任何记录,请先选择要编辑的记录");
+        return;
+      }
+
+      // 检查记录是否有 ID 字段
+      if (!selectedRow.value.hasOwnProperty("id")) {
+        console.error("记录缺少ID字段:", selectedRow.value);
+        ElMessage.error("记录格式错误,缺少ID字段");
+        return;
+      }
+
+      if (!selectedRow.value.id) {
+        console.error("记录ID为空:", selectedRow.value);
+        ElMessage.error("无法找到记录ID,请联系管理员");
+        return;
+      }
+
+      response = await updateItem({
+        table: currentTableName,
+        id: selectedRow.value.id,
+        update_data: dataToSubmit,
+      });
+
+      // 更新成功后,更新本地数据
+      const index = tableData.value.findIndex(
+        (item) => item.id === selectedRow.value.id
+      );
+      if (index > -1) {
+        tableData.value[index] = { ...tableData.value[index], ...dataToSubmit };
+      } else {
+        // 如果本地找不到记录,重新获取数据
+        console.warn("本地未找到对应记录,重新获取数据");
+        fetchTable();
+      }
+    }
+
+    dialogVisible.value = false;
+    ElMessage.success(dialogMode.value === "add" ? "添加成功" : "修改成功");
+  } catch (error) {
+    console.error("提交表单时发生错误:", error);
+    let errorMessage = "未知错误";
+
+    // 更详细的错误信息提取
+    if (error && typeof error === "object") {
+      const err = error as any;
+      if (err.response && err.response.data) {
+        errorMessage =
+          err.response.data.detail ||
+          err.response.data.message ||
+          JSON.stringify(err.response.data);
+      } else if (err.message) {
+        errorMessage = err.message;
+      }
+    }
+    ElMessage.error(`提交失败: ${errorMessage}`);
+  }
+};
+
+// 表单验证:文字字段必填,数字字段可选(根据业务调整)
+function validateFormData(data: { [x: string]: any }) {
+  return editableColumns.every((col) => {
+    const value = data[col.dataKey];
+    // 文字字段:必填(非空字符串);数字字段:可选(允许0或空)
+    if (isTextField(col.dataKey)) {
+      return value !== "" && value !== undefined && value !== null;
+    } else {
+      return true; // 数字字段可选,空值会被后端处理为NULL
+    }
+  });
+}
+
+// 删除记录
+const deleteItem = async (row: any) => {
+  if (!row) {
+    ElMessage.warning("请先选择一行记录");
+    return;
+  }
+
+  try {
+    await deleteItemApi({
+      table: currentTableName,
+      id: row.id,
+    });
+
+    // 直接从本地数据中删除,避免重新获取全部数据
+    const index = tableData.value.findIndex((item) => item.id === row.id);
+    if (index > -1) {
+      tableData.value.splice(index, 1);
+    }
+
+    ElMessage.success("记录删除成功");
+  } catch (error) {
+    console.error("删除记录时发生错误:", error);
+    ElMessage.error("删除失败,请重试");
+  }
+};
+
+// 下载模板
+const downloadTemplateAction = async () => {
+  try {
+    await downloadTemplate(currentTableName);
+    ElMessage.success("模板下载成功");
+  } catch (error) {
+    console.error("下载模板时发生错误:", error);
+    ElMessage.error("下载模板失败,请重试");
+  }
+};
+
+// 导出数据
+const exportDataAction = async () => {
+  try {
+    await exportData(currentTableName);
+    ElMessage.success("数据导出成功");
+  } catch (error) {
+    console.error("导出数据时发生错误:", error);
+    ElMessage.error("导出数据失败,请重试");
+  }
+};
+
+// 处理文件选择(导入数据)
+const handleFileSelect = async (uploadFile: any) => {
+  const file = uploadFile.raw;
+  if (!file) {
+    ElMessage.warning("请选择有效的 .xlsx 或 .csv 文件");
+    return;
+  }
+  await importDataAction(file);
+};
+
+// 导入数据:区分字段类型处理
+const importDataAction = async (file: File) => {
+  try {
+    const response = await importData(currentTableName, file);
+    if (response.success) {
+      const { total_data, new_data, duplicate_data } = response;
+      ElMessage({
+        message: `导入成功!共${total_data}条,新增${new_data}条,重复${duplicate_data}条`,
+        type: "success",
+        duration: 3000,
+      });
+      fetchTable(); // 刷新表格,应用字段类型处理逻辑
+    }
+  } catch (error) {
+    let errorMessage = "数据导入失败";
+    if (
+      error &&
+      typeof error === "object" &&
+      "message" in error &&
+      "response" in error &&
+      typeof (error as any).response === "object"
+    ) {
+      // 适配后端返回的重复记录详情
+      if ((error as any).response?.data?.detail) {
+        const detail = (error as any).response.data.detail;
+        if (detail.duplicates) {
+          // 显示重复记录的行号和文字字段内容
+          const duplicateMsg = detail.duplicates
+            .map(
+              (item: any) =>
+                `第${item.row_number}行(${Object.entries(item.duplicate_fields)
+                  .map(([k, v]) => `${k}=${v || "空"}`)
+                  .join(",")}`
+            )
+            .join(";");
+          errorMessage = `导入失败:${detail.message},重复记录:${duplicateMsg}`;
+        } else {
+          errorMessage = `导入失败:${detail.message || detail}`;
+        }
+      } else {
+        errorMessage += `: ${(error as unknown as Error).message}`;
+      }
+    }
+    ElMessage.error({
+      message: errorMessage,
+      duration: 5000, // 长一点的提示时间,让用户看清重复详情
+    });
+  }
+};
+
+// 分页大小改变
+const handleSizeChange = (val: number) => {
+  pageSize4.value = val;
+  currentPage4.value = 1;
+};
+
+// 当前页改变
+const handleCurrentChange = (val: number) => {
+  currentPage4.value = val;
+};
+
+// 表格值格式化:仅数字字段保留3位小数,文字字段显示原始内容
+const formatTableValue = (row: any, column: any, cellValue: any) => {
+  const dataKey = column.prop;
+  // 文字字段:空值显示空字符串,非空显示原始文字
+  if (isTextField(dataKey)) {
+    return cellValue === undefined || cellValue === null ? "" : cellValue;
+  }
+  // 数字字段:NaN/undefined显示0,保留3位小数
+  if (isNumberField(dataKey)) {
+    if (isNaN(cellValue) || cellValue === undefined || cellValue === null) {
+      return 0;
+    }
+    return typeof cellValue === "number" ? cellValue.toFixed(3) : cellValue;
+  }
+  // 其他字段:默认显示
+  return cellValue;
+};
+
+// 对话框标题
+const dialogTitle = computed(() => {
+  return dialogMode.value === "add" ? `新增记录` : "编辑记录";
+});
+
+// 对话框提交按钮文本
+const dialogSubmitButtonText = computed(() => {
+  return dialogMode.value === "add" ? "添加" : "保存";
+});
+</script>
+
+<style scoped>
+.app-container {
+  padding: 20px 40px;
+  min-height: 100vh;
+  background-color: #f0f2f5;
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.button-group {
+  display: flex;
+  gap: 12px;
+  justify-content: flex-end;
+  width: 100%;
+  margin-bottom: 20px;
+}
+
+.custom-button {
+  color: #fff;
+  border: none;
+  border-radius: 8px;
+  font-size: 14px;
+  padding: 10px 18px;
+  transition: transform 0.3s ease, background-color 0.3s ease;
+  min-width: 130px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.download-button,
+.export-button,
+.add-button,
+.import-button {
+  background-color: #67c23a;
+}
+
+.download-button:hover,
+.export-button:hover,
+.import-button:hover,
+.add-button:hover {
+  background-color: #85ce61;
+  transform: scale(1.05);
+}
+
+.custom-table {
+  width: 100%;
+  border-radius: 8px;
+  overflow: hidden;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+  background-color: #fff;
+  margin-top: 10px;
+}
+
+:deep(.el-table th) {
+  background: linear-gradient(180deg, #61e054, #4db944);
+  color: #fff;
+  font-weight: bold;
+  text-align: center;
+  padding: 14px 0;
+  font-size: 14px;
+}
+
+:deep(.el-table__row:nth-child(odd)) {
+  background-color: #e4fbe5;
+}
+
+:deep(.el-table__row:nth-child(even)) {
+  background-color: #ffffff;
+}
+
+:deep(.el-table td) {
+  padding: 12px 10px;
+  text-align: center;
+  border-bottom: 1px solid #ebeef5;
+  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;
+}
+
+.edit-button {
+  background-color: #409eff;
+  color: #fff;
+}
+
+.edit-button:hover {
+  background-color: #66b1ff;
+  transform: scale(1.1);
+}
+
+.delete-button {
+  background-color: #f56c6c;
+  color: #fff;
+}
+
+.delete-button:hover {
+  background-color: #f78989;
+  transform: scale(1.1);
+}
+
+.pagination-container {
+  margin-top: 30px;
+  text-align: center;
+}
+
+.action-buttons {
+  display: flex;
+  justify-content: center;
+}
+
+/* 弹框样式 */
+:deep(.el-dialog) {
+  border-radius: 16px !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;
+}
+
+.dialog-header {
+  position: relative;
+  padding: 20px 24px 10px;
+  text-align: center;
+  background: linear-gradient(90deg, #4db944, #61e054);
+}
+
+.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, #3a9a32, #4fc747);
+  border-radius: 2px;
+  margin-top: 12px;
+  width: 60%;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+.custom-dialog-form {
+  padding: 25px 40px 15px;
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  gap: 20px;
+}
+
+@media (max-width: 1200px) {
+  .custom-dialog-form {
+    grid-template-columns: 1fr;
+  }
+}
+
+.custom-form-item {
+  margin-bottom: 0 !important;
+}
+
+:deep(.el-form-item__label) {
+  display: block;
+  text-align: left;
+  margin-bottom: 8px !important;
+  font-size: 15px;
+  font-weight: 600;
+  color: #4a5568;
+  padding: 0 !important;
+  line-height: 1.5;
+}
+
+:deep(.custom-dialog-input .el-input__inner) {
+  height: 44px !important;
+  padding: 0 16px !important;
+  font-size: 15px;
+  border: 1px solid #cbd5e0 !important;
+  border-radius: 10px !important;
+  background-color: #f8fafc;
+  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05);
+  transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
+}
+
+:deep(.custom-dialog-input .el-input__inner:hover) {
+  border-color: #a0aec0 !important;
+}
+
+:deep(.custom-dialog-input .el-input__inner:focus) {
+  border-color: #4db944 !important;
+  box-shadow: 0 0 0 3px rgba(77, 185, 68, 0.15) !important;
+  background-color: #fff;
+}
+
+.dialog-footer {
+  display: flex;
+  gap: 16px;
+  justify-content: center;
+  padding: 15px 40px 25px;
+}
+
+.custom-cancel-button,
+.custom-submit-button {
+  min-width: 120px;
+  height: 44px;
+  border-radius: 10px;
+  font-size: 16px;
+  font-weight: 500;
+  transition: all 0.3s ease;
+  letter-spacing: 0.5px;
+  border: none;
+  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+}
+
+.custom-cancel-button {
+  background: linear-gradient(145deg, #f7fafc, #edf2f7);
+  color: #4a5568;
+}
+
+.custom-cancel-button:hover {
+  background: linear-gradient(145deg, #e2e8f0, #cbd5e0);
+  transform: translateY(-2px);
+  box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
+}
+
+.custom-submit-button {
+  background: linear-gradient(145deg, #4db944, #61e054);
+  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 {
+  transform: translateY(-2px);
+  box-shadow: 0 6px 15px rgba(77, 185, 68, 0.4);
+}
+
+:deep(.import-upload .el-upload-list) {
+  display: none !important;
+}
+</style>

+ 962 - 0
src/views/Admin/dataManagement/HeavyMetalInputFluxManager/crossSectionSampleData.vue

@@ -0,0 +1,962 @@
+<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
+        :auto-upload="false"
+        :on-change="handleFileSelect"
+        accept=".xlsx, .csv"
+        class="import-upload"
+      >
+        <el-button
+          :icon="Upload"
+          type="primary"
+          class="custom-button import-button"
+          >导入数据</el-button
+        >
+      </el-upload>
+    </div>
+
+    <!-- 数据表格:仅数字字段NaN显示0,文字字段不处理 -->
+    <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 * 18 + 50"
+        :formatter="formatTableValue"
+        align="center"
+        header-align="center"
+      ></el-table-column>
+      <el-table-column label="操作" width="120" align="center">
+        <template #default="scope">
+          <span class="action-buttons">
+            <el-tooltip
+              class="item"
+              effect="dark"
+              content="编辑"
+              placement="top"
+            >
+              <el-button
+                circle
+                :icon="EditPen"
+                @click.stop="openDialog('edit', scope.row)"
+                class="action-button edit-button"
+              ></el-button
+            ></el-tooltip>
+            <el-tooltip
+              class="item"
+              effect="dark"
+              content="删除"
+              placement="top"
+            >
+              <el-button
+                circle
+                :icon="DeleteFilled"
+                @click.stop="deleteItem(scope.row)"
+                class="action-button delete-button"
+              ></el-button
+            ></el-tooltip>
+          </span>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 分页控制 -->
+    <PaginationComponent
+      :total="tableData.length"
+      :currentPage="currentPage4"
+      :pageSize="pageSize4"
+      @update:currentPage="currentPage4 = $event"
+      @update:pageSize="pageSize4 = $event"
+      @size-change="handleSizeChange"
+      @current-change="handleCurrentChange"
+      class="pagination-container"
+    />
+
+    <!-- 新增/编辑对话框:优化样式 -->
+    <el-dialog
+      :title="dialogTitle"
+      v-model="dialogVisible"
+      width="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-form
+        ref="formRef"
+        :model="formData"
+        label-position="top"
+        class="custom-dialog-form"
+      >
+        <el-form-item
+          v-for="col in editableColumns"
+          :key="col.key"
+          :label="col.title"
+          class="custom-form-item"
+          :rules="[
+            {
+              required: isTextField(col.dataKey) || isRequiredNumberField(col.dataKey),
+              message: `请输入${col.title}`,
+              trigger: 'blur',
+            },
+          ]"
+        >
+          <el-input
+            v-model="formData[col.dataKey]"
+            :type="col.inputType || 'text'"
+            class="custom-dialog-input"
+            :placeholder="`请输入${col.title}`"
+            :step="col.step || undefined"
+          ></el-input>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-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
+          >
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive, computed, onMounted } from "vue";
+import {
+  DeleteFilled,
+  Download,
+  Upload,
+  Plus,
+  EditPen,
+} from "@element-plus/icons-vue";
+import { ElMessage, ElForm } from "element-plus";
+import {
+  table,
+  updateItem,
+  addItem,
+  deleteItemApi,
+  downloadTemplate,
+  exportData,
+  importData,
+} from "@/API/admin";
+import PaginationComponent from "@/components/PaginationComponent.vue";
+
+// 核心:定义字段类型映射(仅保留6个核心字段)
+const fieldTypeMap: Record<string, "text" | "number"> = {
+  id: "number", // 主键ID(数字)
+  river_name: "text", // 所属河流(文字-中文)
+  position: "text", // 断面位置(文字-中文)
+  county: "text", // 所属行政区(文字-中文)
+  longitude: "number", // 经度坐标(数字)
+  latitude: "number", // 纬度坐标(数字)
+  cd_concentration: "number" // 镉浓度(mg/L)(数字)
+};
+
+// 核心:定义必填数字字段(坐标、镉浓度为必填)
+const requiredNumberFields = [
+  "longitude",
+  "latitude",
+  "cd_concentration"
+];
+
+interface Column {
+  key: string;
+  dataKey: string;
+  title: string;
+  width: number;
+  inputType?: string;
+  step?: string; // 数字输入框步长
+}
+
+// 核心:表字段定义(仅保留6个核心字段,适配中文显示)
+const columns: Column[] = [
+  { key: "id", dataKey: "id", title: "主键ID", width: 120, inputType: "number" },
+  { key: "river_name", dataKey: "river_name", title: "所属河流", width: 180, inputType: "text" },
+  { key: "position", dataKey: "position", title: "断面位置", width: 220, inputType: "text" },
+  { key: "county", dataKey: "county", title: "所属行政区", width: 180, inputType: "text" },
+  { 
+    key: "longitude", 
+    dataKey: "longitude", 
+    title: "经度坐标", 
+    width: 160, 
+    inputType: "number", 
+    step: "0.000001" // 经度精确到6位小数
+  },
+  { 
+    key: "latitude", 
+    dataKey: "latitude", 
+    title: "纬度坐标", 
+    width: 160, 
+    inputType: "number", 
+    step: "0.000001" // 纬度精确到6位小数
+  },
+  { 
+    key: "cd_concentration", 
+    dataKey: "cd_concentration", 
+    title: "镉浓度(mg/L)", 
+    width: 180, 
+    inputType: "number", 
+    step: "0.0001" // 镉浓度精确到4位小数(适配低浓度场景)
+  }
+];
+
+const editableColumns = columns.filter((col) => col.key !== "id"); // 编辑时排除自增主键ID
+
+//  核心变量:更新表名为 cross_section
+const currentTableName = "cross_section";
+
+// 表格数据相关
+const tableData = ref<any[]>([]);
+const selectedRow = ref<any | null>(null);
+const loading = ref(false);
+const formRef = ref<InstanceType<typeof ElForm> | null>(null); // 表单引用
+
+// 分页相关
+const currentPage4 = ref(1);
+const pageSize4 = ref(10);
+
+// 对话框相关
+const dialogVisible = ref(false);
+const formData = reactive<any>({});
+const dialogMode = ref<"add" | "edit">("add");
+
+//  辅助函数:判断是否为文字字段
+const isTextField = (dataKey: string): boolean => {
+  return fieldTypeMap[dataKey]?.toLowerCase() === "text";
+};
+
+//  辅助函数:判断是否为数字字段
+const isNumberField = (dataKey: string): boolean => {
+  return fieldTypeMap[dataKey]?.toLowerCase() === "number";
+};
+
+//  辅助函数:判断是否为必填数字字段
+const isRequiredNumberField = (dataKey: string): boolean => {
+  return requiredNumberFields.includes(dataKey);
+};
+
+// 分页和序号处理:仅数字字段处理NaN为0,文字字段保留空
+const pagedTableDataWithIndex = computed(() => {
+  const start = (currentPage4.value - 1) * pageSize4.value;
+  const end = start + pageSize4.value;
+  return tableData.value.slice(start, end).map((row, idx) => {
+    const processedRow = Object.entries(row).reduce((acc, [key, val]) => {
+      // 仅数字字段:NaN/undefined转为0;文字字段:保留原值(空/中文)
+      if (isNumberField(key)) {
+        acc[key] =
+          (typeof val === "number" && isNaN(val)) || val === undefined
+            ? 0
+            : val;
+      } else {
+        acc[key] = val === undefined || val === null ? "" : val; // 文字字段空值显示空字符串
+      }
+      return acc;
+    }, {} as any);
+    return {
+      ...processedRow,
+      displayIndex: start + idx + 1,
+    };
+  });
+});
+
+// 获取表格数据:仅数字字段处理NaN为0
+const fetchTable = async () => {
+  console.log("正在获取表格数据...");
+  try {
+    loading.value = true;
+    const response = await table({ table: currentTableName });
+    console.log("获取到的数据:", response);
+    
+    // 适配后端返回的直接数组格式
+    const rawData = Array.isArray(response.data) ? response.data : [];
+    
+    // 预处理数据:区分字段类型处理
+    tableData.value = rawData.map((row: any) =>
+      Object.entries(row).reduce((acc: any, [key, val]) => {
+        if (isNumberField(key)) {
+          acc[key] =
+            (typeof val === "number" && isNaN(val)) || val === undefined
+              ? 0
+              : val;
+        } else {
+          acc[key] = val === undefined || val === null ? "" : val; // 文字字段空值存空字符串
+        }
+        return acc;
+      }, {})
+    );
+  } catch (error) {
+    console.error("获取数据时出错:", error);
+    ElMessage.error("获取数据失败,请检查网络连接或服务器状态");
+  } finally {
+    loading.value = false;
+  }
+};
+
+onMounted(() => {
+  fetchTable();
+});
+
+// 表格行点击事件:文字字段保留原值,数字字段处理NaN为0
+const handleRowClick = (row: any) => {
+  console.log("点击行数据:", row);
+  selectedRow.value = row;
+  Object.assign(
+    formData,
+    Object.entries(row).reduce((acc: any, [key, val]) => {
+      if (isNumberField(key)) {
+        acc[key] =
+          (typeof val === "number" && isNaN(val)) || val === undefined
+            ? 0
+            : val;
+      } else {
+        acc[key] = val === undefined || val === null ? "" : val; // 文字字段空值显示空字符串
+      }
+      return acc;
+    }, {})
+  );
+};
+
+// 打开新增/编辑对话框:文字字段置空,数字字段可选置0
+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] = "";
+    });
+  } else if (row) {
+    selectedRow.value = row;
+    // 编辑时:区分字段类型赋值
+    Object.assign(
+      formData,
+      Object.entries(row).reduce((acc: any, [key, val]) => {
+        if (isNumberField(key)) {
+          acc[key] =
+            (typeof val === "number" && isNaN(val)) || val === undefined
+              ? 0
+              : val;
+        } else {
+          acc[key] = val === undefined || val === null ? "" : val;
+        }
+        return acc;
+      }, {})
+    );
+  }
+};
+
+// 表单数据预处理:文字字段空字符串转为undefined(适配后端)
+function prepareFormData(
+  formData: { [x: string]: any },
+  excludeKeys = ["displayIndex"]
+): { [x: string]: any } {
+  const result: { [x: string]: any } = {};
+  for (const key in formData) {
+    if (!excludeKeys.includes(key)) {
+      let value = formData[key];
+      // 文字字段:空字符串转为undefined;数字字段:空字符串转为undefined(后端处理NULL)
+      if (isTextField(key) || isNumberField(key)) {
+        result[key] = value === "" ? undefined : value;
+      }
+    }
+  }
+  return result;
+}
+
+// 表单提交:文字字段必填校验
+const submitForm = async () => {
+  // 表单校验:文字字段必填
+  if (!formRef.value) return;
+  
+  try {
+    const isValid = await formRef.value.validate();
+    if (!isValid) return;
+  } catch (error) {
+    console.error("表单验证失败:", error);
+    return;
+  }
+
+  try {
+    const dataToSubmit = prepareFormData(formData);
+    let response;
+
+    if (dialogMode.value === "add") {
+      response = await addItem({
+        table: currentTableName,
+        item: dataToSubmit,
+      });
+      
+      // 添加成功后,直接将新数据添加到本地数组
+      if (response.data && response.data.inserted) {
+        tableData.value.push(response.data.inserted);
+      }
+    } else {
+      // 更详细的错误检查和日志
+      console.log("编辑模式 - selectedRow:", selectedRow.value);
+      
+      if (!selectedRow.value) {
+        ElMessage.error("未选中任何记录,请先选择要编辑的记录");
+        return;
+      }
+      
+      // 检查记录是否有 ID 字段
+      if (!selectedRow.value.hasOwnProperty('id')) {
+        console.error("记录缺少ID字段:", selectedRow.value);
+        ElMessage.error("记录格式错误,缺少ID字段");
+        return;
+      }
+      
+      if (!selectedRow.value.id) {
+        console.error("记录ID为空:", selectedRow.value);
+        ElMessage.error("无法找到记录ID,请联系管理员");
+        return;
+      }
+      
+      response = await updateItem({
+        table: currentTableName,
+        id: selectedRow.value.id,
+        update_data: dataToSubmit,
+      });
+      
+      // 更新成功后,更新本地数据
+      const index = tableData.value.findIndex(item => item.id === selectedRow.value.id);
+      if (index > -1) {
+        tableData.value[index] = {...tableData.value[index], ...dataToSubmit};
+      } else {
+        // 如果本地找不到记录,重新获取数据
+        console.warn("本地未找到对应记录,重新获取数据");
+        fetchTable();
+      }
+    }
+
+    dialogVisible.value = false;
+    ElMessage.success(dialogMode.value === "add" ? "添加成功" : "修改成功");
+  } catch (error) {
+    console.error("提交表单时发生错误:", error);
+    let errorMessage = "未知错误";
+    
+    // 更详细的错误信息提取
+    if (error && typeof error === "object") {
+      const err = error as any;
+      if (err.response && err.response.data) {
+        errorMessage = err.response.data.detail || err.response.data.message || JSON.stringify(err.response.data);
+      } else if (err.message) {
+        errorMessage = err.message;
+      }
+    }    
+    ElMessage.error(`提交失败: ${errorMessage}`);
+  }
+};
+
+// 表单验证:文字字段+必填数字字段必填
+function validateFormData(data: { [x: string]: any }) {
+  return editableColumns.every((col) => {
+    const value = data[col.dataKey];
+    // 文字字段:必填;必填数字字段:必填;非必填数字字段:可选
+    if (isTextField(col.dataKey) || isRequiredNumberField(col.dataKey)) {
+      return value !== "" && value !== undefined && value !== null;
+    } else {
+      return true;
+    }
+  });
+}
+
+// 删除记录
+const deleteItem = async (row: any) => {
+  if (!row) {
+    ElMessage.warning("请先选择一行记录");
+    return;
+  }
+
+  try {
+    await deleteItemApi({
+      table: currentTableName,
+      id: row.id,
+    });
+    
+    // 直接从本地数据中删除,避免重新获取全部数据
+    const index = tableData.value.findIndex((item) => item.id === row.id);
+    if (index > -1) {
+      tableData.value.splice(index, 1);
+    }
+    
+    ElMessage.success("记录删除成功");
+  } catch (error) {
+    console.error("删除记录时发生错误:", error);
+    ElMessage.error("删除失败,请重试");
+  }
+};
+
+// 下载模板
+const downloadTemplateAction = async () => {
+  try {
+    await downloadTemplate(currentTableName);
+    ElMessage.success("模板下载成功");
+  } catch (error) {
+    console.error("下载模板时发生错误:", error);
+    const apiError = error as { response?: { data?: { message?: string } } };
+    const errorMessage = apiError.response?.data?.message || "下载模板失败,请重试";
+    ElMessage.error(errorMessage);
+  }
+};
+
+// 导出数据
+const exportDataAction = async () => {
+  try {
+    await exportData(currentTableName);
+    ElMessage.success("数据导出成功");
+  } catch (error) {
+    console.error("导出数据时发生错误:", error);
+    const apiError = error as { response?: { data?: { message?: string } } };
+    const errorMessage = apiError.response?.data?.message || "导出数据失败,请重试";
+    ElMessage.error(errorMessage);
+  }
+};
+
+// 处理文件选择(导入数据)
+const handleFileSelect = async (uploadFile: any) => {
+  const file = uploadFile.raw;
+  if (!file) {
+    ElMessage.warning("请选择有效的 .xlsx 或 .csv 文件");
+    return;
+  }
+  await importDataAction(file);
+};
+
+// 导入数据:区分字段类型处理
+const importDataAction = async (file: File) => {
+  try {
+    const response = await importData(currentTableName, file);
+    if (response.success) {
+      const { total_data, new_data, duplicate_data } = response;
+      ElMessage({
+        message: `导入成功!共${total_data}条,新增${new_data}条,重复${duplicate_data}条`,
+        type: "success",
+        duration: 3000,
+      });
+      fetchTable(); // 刷新表格,应用字段类型处理逻辑
+    }
+  } catch (error) {
+    let errorMessage = "数据导入失败";
+    const apiError = error as { response?: { data?: { detail?: { message?: string; duplicates?: any[] } } } };
+    if (
+      error &&
+      typeof error === "object" &&
+      "message" in error &&
+      "response" in error &&
+      typeof apiError.response === "object"
+    ) {
+      // 适配后端返回的重复记录详情
+      if (apiError.response?.data?.detail) {
+        const detail = apiError.response.data.detail;
+        if (detail.duplicates) {
+          // 显示重复记录的行号和关键字段(主键ID+所属河流)
+          const duplicateMsg = detail.duplicates
+            .map(
+              (item: any) =>
+                `第${item.row_number}行(主键ID=${item.duplicate_fields.id || "空"},所属河流=${item.duplicate_fields.river_name || "空"})`
+            )
+            .join(";");
+          errorMessage = `导入失败:${detail.message},重复记录:${duplicateMsg}`;
+        } else {
+          errorMessage = `导入失败:${detail.message || detail}`;
+        }
+      } else {
+        errorMessage += `: ${(error as unknown as Error).message}`;
+      }
+    }
+    ElMessage.error({
+      message: errorMessage,
+      duration: 5000,
+    });
+  }
+};
+
+// 分页大小改变
+const handleSizeChange = (val: number) => {
+  pageSize4.value = val;
+  currentPage4.value = 1;
+};
+
+// 当前页改变
+const handleCurrentChange = (val: number) => {
+  currentPage4.value = val;
+};
+
+//  表格值格式化:数字字段按精度显示,文字字段显示中文
+const formatTableValue = (row: any, column: any, cellValue: any) => {
+  const dataKey = column.prop;
+  // 文字字段(河流/位置/行政区):空值显示空字符串,非空显示中文
+  if (isTextField(dataKey)) {
+    return cellValue === undefined || cellValue === null ? "" : cellValue;
+  }
+  // 数字字段:按精度显示
+  if (isNumberField(dataKey)) {
+    if (isNaN(cellValue) || cellValue === undefined || cellValue === null) {
+      return 0;
+    }
+    // 经度/纬度:6位小数
+    if (dataKey === "longitude" || dataKey === "latitude") {
+      return typeof cellValue === "number" ? cellValue.toFixed(6) : cellValue;
+    }
+    // 镉浓度:4位小数(适配低浓度场景)
+    if (dataKey === "cd_concentration") {
+      return typeof cellValue === "number" ? cellValue.toFixed(4) : cellValue;
+    }
+    // 其他数字字段:默认1位小数
+    return typeof cellValue === "number" ? cellValue.toFixed(1) : cellValue;
+  }
+  // 其他字段:默认显示
+  return cellValue;
+};
+
+// 对话框标题:更新为断面数据相关
+const dialogTitle = computed(() => {
+  return dialogMode.value === "add"
+    ? `新增断面数据记录`
+    : "编辑断面数据记录";
+});
+
+// 对话框提交按钮文本
+const dialogSubmitButtonText = computed(() => {
+  return dialogMode.value === "add" ? "添加" : "保存";
+});
+</script>
+
+<style scoped>
+.app-container {
+  padding: 20px 40px;
+  min-height: 100vh;
+  background-color: #f0f2f5;
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.button-group {
+  display: flex;
+  gap: 12px;
+  justify-content: flex-end;
+  width: 100%;
+  margin-bottom: 20px;
+}
+
+.custom-button {
+  color: #fff;
+  border: none;
+  border-radius: 8px;
+  font-size: 14px;
+  padding: 10px 18px;
+  transition: transform 0.3s ease, background-color 0.3s ease;
+  min-width: 130px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.download-button,
+.export-button,
+.add-button,
+.import-button {
+  background-color: #67c23a;
+}
+
+.download-button:hover,
+.export-button:hover,
+.import-button:hover,
+.add-button:hover {
+  background-color: #85ce61;
+  transform: scale(1.05);
+}
+
+.custom-table {
+  width: 100%;
+  border-radius: 8px;
+  overflow: hidden;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+  background-color: #fff;
+  margin-top: 10px;
+}
+
+:deep(.el-table th) {
+  background: linear-gradient(180deg, #61e054, #4db944);
+  color: #fff;
+  font-weight: bold;
+  text-align: center;
+  padding: 14px 0;
+  font-size: 14px;
+}
+
+:deep(.el-table__row:nth-child(odd)) {
+  background-color: #e4fbe5;
+}
+
+:deep(.el-table__row:nth-child(even)) {
+  background-color: #ffffff;
+}
+
+:deep(.el-table td) {
+  padding: 12px 10px;
+  text-align: center;
+  border-bottom: 1px solid #ebeef5;
+}
+
+.action-button {
+  font-size: 16px;
+  margin-right: 5px;
+  border-radius: 50%;
+  transition: transform 0.3s ease, background-color 0.3s ease;
+}
+
+.edit-button {
+  background-color: #409eff;
+  color: #fff;
+}
+
+.edit-button:hover {
+  background-color: #66b1ff;
+  transform: scale(1.1);
+}
+
+.delete-button {
+  background-color: #f56c6c;
+  color: #fff;
+}
+
+.delete-button:hover {
+  background-color: #f78989;
+  transform: scale(1.1);
+}
+
+.el-form-item__label {
+  width: 150px;
+  font-weight: bold;
+}
+
+.el-form-item__content {
+  margin-left: 150px;
+}
+
+.pagination-container {
+  margin-top: 30px;
+  text-align: center;
+}
+
+.action-buttons {
+  display: flex;
+  justify-content: center;
+}
+
+/* ================= 弹框样式优化 ================= */
+:deep(.el-dialog) {
+  border-radius: 16px !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;
+}
+
+/* 弹框头部样式 */
+.dialog-header {
+  position: relative;
+  padding: 20px 24px 10px;
+  text-align: center;
+  background: linear-gradient(90deg, #4db944, #61e054);
+}
+
+.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, #3a9a32, #4fc747);
+  border-radius: 2px;
+  margin-top: 12px;
+  width: 60%;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+/* 表单容器样式 */
+.custom-dialog-form {
+  padding: 25px 40px 15px;
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  gap: 20px;
+}
+/* 适配小屏幕:单列显示 */
+@media (max-width: 1200px) {
+  .custom-dialog-form {
+    grid-template-columns: 1fr;
+  }
+}
+
+/* 表单项样式优化 */
+.custom-form-item {
+  margin-bottom: 0 !important;
+}
+
+:deep(.el-form-item__label) {
+  display: block;
+  text-align: left;
+  margin-bottom: 8px !important;
+  font-size: 15px;
+  font-weight: 600;
+  color: #4a5568;
+  padding: 0 !important;
+  line-height: 1.5;
+}
+
+/* 输入框样式优化 */
+:deep(.custom-dialog-input .el-input__inner) {
+  height: 44px !important;
+  padding: 0 16px !important;
+  font-size: 15px;
+  border: 1px solid #cbd5e0 !important;
+  border-radius: 10px !important;
+  background-color: #f8fafc;
+  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05);
+  transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
+}
+
+:deep(.custom-dialog-input .el-input__inner:hover) {
+  border-color: #a0aec0 !important;
+}
+
+:deep(.custom-dialog-input .el-input__inner:focus) {
+  border-color: #4db944 !important;
+  box-shadow: 0 0 0 3px rgba(77, 185, 68, 0.15) !important;
+  background-color: #fff;
+}
+
+/* 弹框底部按钮容器 */
+.dialog-footer {
+  display: flex;
+  gap: 16px;
+  justify-content: center;
+  padding: 15px 40px 25px;
+}
+
+/* 按钮基础样式 */
+.custom-cancel-button,
+.custom-submit-button {
+  min-width: 120px;
+  height: 44px;
+  border-radius: 10px;
+  font-size: 16px;
+  font-weight: 500;
+  transition: all 0.3s ease;
+  letter-spacing: 0.5px;
+  border: none;
+  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+}
+
+/* 取消按钮样式优化 */
+.custom-cancel-button {
+  background: linear-gradient(145deg, #f7fafc, #edf2f7);
+  color: #4a5568;
+}
+
+.custom-cancel-button:hover {
+  background: linear-gradient(145deg, #e2e8f0, #cbd5e0);
+  transform: translateY(-2px);
+  box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
+}
+
+/* 提交按钮样式优化 */
+.custom-submit-button {
+  background: linear-gradient(145deg, #4db944, #61e054);
+  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 {
+  transform: translateY(-2px);
+  box-shadow: 0 6px 15px rgba(77, 185, 68, 0.4);
+}
+
+/* 彻底隐藏文件信息相关元素 */
+:deep(.import-upload .el-upload-list) {
+  display: none !important;
+}
+</style>

+ 942 - 0
src/views/Admin/dataManagement/HeavyMetalInputFluxManager/heavyMetalEnterpriseData.vue

@@ -0,0 +1,942 @@
+<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
+        :auto-upload="false"
+        :on-change="handleFileSelect"
+        accept=".xlsx, .csv"
+        class="import-upload"
+      >
+        <el-button
+          :icon="Upload"
+          type="primary"
+          class="custom-button import-button"
+          >导入数据</el-button
+        >
+      </el-upload>
+    </div>
+
+    <!-- 数据表格:仅数字字段NaN显示0,文字字段不处理 -->
+    <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="formatTableValue"
+        align="center"
+        header-align="center"
+      ></el-table-column>
+      <el-table-column label="操作" width="120" align="center">
+        <template #default="scope">
+          <span class="action-buttons">
+            <el-tooltip
+              class="item"
+              effect="dark"
+              content="编辑"
+              placement="top"
+            >
+              <el-button
+                circle
+                :icon="EditPen"
+                @click.stop="openDialog('edit', scope.row)"
+                class="action-button edit-button"
+              ></el-button
+            ></el-tooltip>
+            <el-tooltip
+              class="item"
+              effect="dark"
+              content="删除"
+              placement="top"
+            >
+              <el-button
+                circle
+                :icon="DeleteFilled"
+                @click.stop="deleteItem(scope.row)"
+                class="action-button delete-button"
+              ></el-button
+            ></el-tooltip>
+          </span>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 分页控制 -->
+    <PaginationComponent
+      :total="tableData.length"
+      :currentPage="currentPage4"
+      :pageSize="pageSize4"
+      @update:currentPage="currentPage4 = $event"
+      @update:pageSize="pageSize4 = $event"
+      @size-change="handleSizeChange"
+      @current-change="handleCurrentChange"
+      class="pagination-container"
+    />
+
+    <!-- 新增/编辑对话框:优化样式 -->
+    <el-dialog
+      :title="dialogTitle"
+      v-model="dialogVisible"
+      width="50%"
+      :custom-class="['custom-dialog']"
+      :close-on-click-modal="false"
+      center
+    >
+      <div class="dialog-header">
+        <h2>{{ dialogTitle }}</h2>
+        <div class="header-decoration"></div>
+      </div>
+      <el-form
+        ref="formRef"
+        :model="formData"
+        label-position="top"
+        class="custom-dialog-form"
+      >
+        <el-form-item
+          v-for="col in editableColumns"
+          :key="col.key"
+          :label="col.title"
+          class="custom-form-item"
+          :rules="[
+            {
+              required: isTextField(col.dataKey),
+              message: `请输入${col.title}`,
+              trigger: 'blur',
+            },
+          ]"
+        >
+          <el-input
+            v-model="formData[col.dataKey]"
+            :type="col.inputType || 'text'"
+            class="custom-dialog-input"
+            :placeholder="`请输入${col.title}`"
+          ></el-input>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-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
+          >
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive, computed, onMounted } from "vue";
+import {
+  DeleteFilled,
+  Download,
+  Upload,
+  Plus,
+  EditPen,
+} from "@element-plus/icons-vue";
+import { ElMessage, ElForm } from "element-plus";
+import {
+  table,
+  updateItem,
+  addItem,
+  deleteItemApi,
+  downloadTemplate,
+  exportData,
+  importData,
+} from "@/API/admin";
+import PaginationComponent from "@/components/PaginationComponent.vue";
+
+// 核心:定义字段类型映射(区分文字/数字字段)
+const fieldTypeMap: Record<string, "text" | "number"> = {
+  longitude: "number",
+  latitude: "number",
+  company_name: "text",
+  company_type: "text",
+  county: "text",
+  particulate_emission: "number",
+};
+
+interface Column {
+  key: string;
+  dataKey: string;
+  title: string;
+  width: number;
+  inputType?: string;
+}
+
+// 表字段定义(与fieldTypeMap对应)
+const columns: Column[] = [
+  { key: "id", dataKey: "id", title: "ID", width: 100 },
+  {
+    key: "longitude",
+    dataKey: "longitude",
+    title: "经度坐标",
+    width: 150,
+    inputType: "number",
+  },
+  {
+    key: "latitude",
+    dataKey: "latitude",
+    title: "纬度坐标",
+    width: 150,
+    inputType: "number",
+  },
+  {
+    key: "company_name",
+    dataKey: "company_name",
+    title: "企业名称",
+    width: 200,
+    inputType: "text",
+  },
+  {
+    key: "company_type",
+    dataKey: "company_type",
+    title: "企业类型",
+    width: 150,
+    inputType: "text",
+  },
+  {
+    key: "county",
+    dataKey: "county",
+    title: "所属区县",
+    width: 250,
+    inputType: "text",
+  },
+  {
+    key: "particulate_emission",
+    dataKey: "particulate_emission",
+    title: "大气颗粒物排放量(吨/年)",
+    width: 150,
+    inputType: "number",
+  },
+];
+
+const editableColumns = columns.filter((col) => col.key !== "id");
+
+// 核心变量:统一表名
+const currentTableName = "atmo_company";
+
+// 表格数据相关
+const tableData = ref<any[]>([]);
+const selectedRow = ref<any | null>(null);
+const loading = ref(false);
+const formRef = ref<InstanceType<typeof ElForm> | null>(null); // 表单引用
+
+// 分页相关
+const currentPage4 = ref(1);
+const pageSize4 = ref(10);
+
+// 对话框相关
+const dialogVisible = ref(false);
+const formData = reactive<any>({});
+const dialogMode = ref<"add" | "edit">("add");
+
+// 辅助函数:判断是否为文字字段
+const isTextField = (dataKey: string): boolean => {
+  return fieldTypeMap[dataKey]?.toLowerCase() === "text";
+};
+
+const isNumberField = (dataKey: string): boolean => {
+  return fieldTypeMap[dataKey]?.toLowerCase() === "number";
+};
+
+// 分页和序号处理:仅数字字段处理NaN为0,文字字段保留空
+const pagedTableDataWithIndex = computed(() => {
+  const start = (currentPage4.value - 1) * pageSize4.value;
+  const end = start + pageSize4.value;
+  return tableData.value.slice(start, end).map((row, idx) => {
+    const processedRow = Object.entries(row).reduce((acc, [key, val]) => {
+      // 仅数字字段:NaN/undefined转为0;文字字段:保留原值(空/文字)
+      if (isNumberField(key)) {
+        acc[key] =
+          (typeof val === "number" && isNaN(val)) || val === undefined
+            ? 0
+            : val;
+      } else {
+        acc[key] = val === undefined || val === null ? "" : val; // 文字字段空值显示空字符串
+      }
+      return acc;
+    }, {} as any);
+    return {
+      ...processedRow,
+      displayIndex: start + idx + 1,
+    };
+  });
+});
+
+// 获取表格数据:仅数字字段处理NaN为0
+const fetchTable = async () => {
+  console.log("正在获取表格数据...");
+  try {
+    loading.value = true;
+    const response = await table({ table: currentTableName });
+    console.log("获取到的数据:", response);
+    
+    // 适配后端返回的直接数组格式
+    const rawData = Array.isArray(response.data) ? response.data : [];
+    
+    // 预处理数据:区分字段类型处理
+    tableData.value = rawData.map((row: any) =>
+      Object.entries(row).reduce((acc: any, [key, val]) => {
+        if (isNumberField(key)) {
+          acc[key] =
+            (typeof val === "number" && isNaN(val)) || val === undefined
+              ? 0
+              : val;
+        } else {
+          acc[key] = val === undefined || val === null ? "" : val; // 文字字段空值存空字符串
+        }
+        return acc;
+      }, {})
+    );
+  } catch (error) {
+    console.error("获取数据时出错:", error);
+    ElMessage.error("获取数据失败,请检查网络连接或服务器状态");
+  } finally {
+    loading.value = false;
+  }
+};
+
+onMounted(() => {
+  fetchTable();
+});
+
+// 表格行点击事件:文字字段保留原值,数字字段处理NaN为0
+const handleRowClick = (row: any) => {
+  console.log("点击行数据:", row);
+  selectedRow.value = row;
+  Object.assign(
+    formData,
+    Object.entries(row).reduce((acc: any, [key, val]) => {
+      if (isNumberField(key)) {
+        acc[key] =
+          (typeof val === "number" && isNaN(val)) || val === undefined
+            ? 0
+            : val;
+      } else {
+        acc[key] = val === undefined || val === null ? "" : val; // 文字字段空值显示空字符串
+      }
+      return acc;
+    }, {})
+  );
+};
+
+// 打开新增/编辑对话框:文字字段置空,数字字段可选置0
+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] = isTextField(col.dataKey) ? "" : "";
+    });
+  } else if (row) {
+    selectedRow.value = row;
+    // 编辑时:区分字段类型赋值
+    Object.assign(
+      formData,
+      Object.entries(row).reduce((acc: any, [key, val]) => {
+        if (isNumberField(key)) {
+          acc[key] =
+            (typeof val === "number" && isNaN(val)) || val === undefined
+              ? 0
+              : val;
+        } else {
+          acc[key] = val === undefined || val === null ? "" : val;
+        }
+        return acc;
+      }, {})
+    );
+  }
+};
+
+// 表单数据预处理:文字字段空字符串转为undefined(适配后端)
+function prepareFormData(
+  formData: { [x: string]: any },
+  excludeKeys = ["displayIndex"]
+): { [x: string]: any } {
+  const result: { [x: string]: any } = {};
+  for (const key in formData) {
+    if (!excludeKeys.includes(key)) {
+      let value = formData[key];
+      // 文字字段:空字符串转为undefined(后端可能需要NULL);数字字段:保留原值
+      if (isTextField(key)) {
+        result[key] = value === "" ? undefined : value;
+      } else {
+        result[key] = value === "" ? undefined : value; // 数字字段空值也转为undefined
+      }
+    }
+  }
+  return result;
+}
+
+// 表单提交:文字字段必填校验
+const submitForm = async () => {
+  // 表单校验:文字字段必填
+  if (!formRef.value) return;
+  
+  try {
+    const isValid = await formRef.value.validate();
+    if (!isValid) return;
+  } catch (error) {
+    console.error("表单验证失败:", error);
+    return;
+  }
+
+  try {
+    const dataToSubmit = prepareFormData(formData);
+    let response;
+
+    if (dialogMode.value === "add") {
+      response = await addItem({
+        table: currentTableName,
+        item: dataToSubmit,
+      });
+      
+      // 添加成功后,直接将新数据添加到本地数组
+      if (response.data && response.data.inserted) {
+        tableData.value.push(response.data.inserted);
+      }
+    } else {
+      // 更详细的错误检查和日志
+      console.log("编辑模式 - selectedRow:", selectedRow.value);
+      
+      if (!selectedRow.value) {
+        ElMessage.error("未选中任何记录,请先选择要编辑的记录");
+        return;
+      }
+      
+      // 检查记录是否有 ID 字段
+      if (!selectedRow.value.hasOwnProperty('id')) {
+        console.error("记录缺少ID字段:", selectedRow.value);
+        ElMessage.error("记录格式错误,缺少ID字段");
+        return;
+      }
+      
+      if (!selectedRow.value.id) {
+        console.error("记录ID为空:", selectedRow.value);
+        ElMessage.error("无法找到记录ID,请联系管理员");
+        return;
+      }
+      
+      response = await updateItem({
+        table: currentTableName,
+        id: selectedRow.value.id,
+        update_data: dataToSubmit,
+      });
+      
+      // 更新成功后,更新本地数据
+      const index = tableData.value.findIndex(item => item.id === selectedRow.value.id);
+      if (index > -1) {
+        tableData.value[index] = {...tableData.value[index], ...dataToSubmit};
+      } else {
+        // 如果本地找不到记录,重新获取数据
+        console.warn("本地未找到对应记录,重新获取数据");
+        fetchTable();
+      }
+    }
+
+    dialogVisible.value = false;
+    ElMessage.success(dialogMode.value === "add" ? "添加成功" : "修改成功");
+  } catch (error) {
+    console.error("提交表单时发生错误:", error);
+    let errorMessage = "未知错误";
+    
+    // 更详细的错误信息提取
+    if (error && typeof error === "object") {
+      const err = error as any;
+      if (err.response && err.response.data) {
+        errorMessage = err.response.data.detail || err.response.data.message || JSON.stringify(err.response.data);
+      } else if (err.message) {
+        errorMessage = err.message;
+      }
+    }    
+    ElMessage.error(`提交失败: ${errorMessage}`);
+  }
+};
+
+// 表单验证:文字字段必填,数字字段可选(根据业务调整)
+function validateFormData(data: { [x: string]: any }) {
+  return editableColumns.every((col) => {
+    const value = data[col.dataKey];
+    // 文字字段:必填(非空字符串);数字字段:可选(允许0或空)
+    if (isTextField(col.dataKey)) {
+      return value !== "" && value !== undefined && value !== null;
+    } else {
+      return true; // 数字字段可选,空值会被后端处理为NULL
+    }
+  });
+}
+
+// 删除记录
+const deleteItem = async (row: any) => {
+  if (!row) {
+    ElMessage.warning("请先选择一行记录");
+    return;
+  }
+
+  try {
+    await deleteItemApi({
+      table: currentTableName,
+      id: row.id,
+    });
+    
+    // 直接从本地数据中删除,避免重新获取全部数据
+    const index = tableData.value.findIndex((item) => item.id === row.id);
+    if (index > -1) {
+      tableData.value.splice(index, 1);
+    }
+    
+    ElMessage.success("记录删除成功");
+  } catch (error) {
+    console.error("删除记录时发生错误:", error);
+    ElMessage.error("删除失败,请重试");
+  }
+};
+
+// 下载模板
+const downloadTemplateAction = async () => {
+  try {
+    await downloadTemplate(currentTableName);
+    ElMessage.success("模板下载成功");
+  } catch (error) {
+    console.error("下载模板时发生错误:", error);
+    ElMessage.error("下载模板失败,请重试");
+  }
+};
+
+// 导出数据
+const exportDataAction = async () => {
+  try {
+    await exportData(currentTableName);
+    ElMessage.success("数据导出成功");
+  } catch (error) {
+    console.error("导出数据时发生错误:", error);
+    ElMessage.error("导出数据失败,请重试");
+  }
+};
+
+// 处理文件选择(导入数据)
+const handleFileSelect = async (uploadFile: any) => {
+  const file = uploadFile.raw;
+  if (!file) {
+    ElMessage.warning("请选择有效的 .xlsx 或 .csv 文件");
+    return;
+  }
+  await importDataAction(file);
+};
+
+// 导入数据:区分字段类型处理
+const importDataAction = async (file: File) => {
+  try {
+    const response = await importData(currentTableName, file);
+    if (response.success) {
+      const { total_data, new_data, duplicate_data } = response;
+      ElMessage({
+        message: `导入成功!共${total_data}条,新增${new_data}条,重复${duplicate_data}条`,
+        type: "success",
+        duration: 3000,
+      });
+      fetchTable(); // 刷新表格,应用字段类型处理逻辑
+    }
+  } catch (error) {
+    let errorMessage = "数据导入失败";
+    if (
+      error &&
+      typeof error === "object" &&
+      "message" in error &&
+      "response" in error &&
+      typeof (error as any).response === "object"
+    ) {
+      // 适配后端返回的重复记录详情
+      if ((error as any).response?.data?.detail) {
+        const detail = (error as any).response.data.detail;
+        if (detail.duplicates) {
+          // 显示重复记录的行号和文字字段内容
+          const duplicateMsg = detail.duplicates
+            .map(
+              (item: any) =>
+                `第${item.row_number}行(${Object.entries(item.duplicate_fields)
+                  .map(([k, v]) => `${k}=${v || "空"}`)
+                  .join(",")}`
+            )
+            .join(";");
+          errorMessage = `导入失败:${detail.message},重复记录:${duplicateMsg}`;
+        } else {
+          errorMessage = `导入失败:${detail.message || detail}`;
+        }
+      } else {
+        errorMessage += `: ${(error as unknown as Error).message}`;
+      }
+    }
+    ElMessage.error({
+      message: errorMessage,
+      duration: 5000, // 长一点的提示时间,让用户看清重复详情
+    });
+  }
+};
+
+// 分页大小改变
+const handleSizeChange = (val: number) => {
+  pageSize4.value = val;
+  currentPage4.value = 1;
+};
+
+// 当前页改变
+const handleCurrentChange = (val: number) => {
+  currentPage4.value = val;
+};
+
+// 表格值格式化:仅数字字段保留3位小数,文字字段显示原始内容
+const formatTableValue = (row: any, column: any, cellValue: any) => {
+  const dataKey = column.prop;
+  // 文字字段:空值显示空字符串,非空显示原始文字
+  if (isTextField(dataKey)) {
+    return cellValue === undefined || cellValue === null ? "" : cellValue;
+  }
+  // 数字字段:NaN/undefined显示0,保留3位小数
+  if (isNumberField(dataKey)) {
+    if (isNaN(cellValue) || cellValue === undefined || cellValue === null) {
+      return 0;
+    }
+    return typeof cellValue === "number" ? cellValue.toFixed(3) : cellValue;
+  }
+  // 其他字段:默认显示
+  return cellValue;
+};
+
+// 对话框标题
+const dialogTitle = computed(() => {
+  return dialogMode.value === "add"
+    ? `新增记录`
+    : "编辑记录";
+});
+
+// 对话框提交按钮文本
+const dialogSubmitButtonText = computed(() => {
+  return dialogMode.value === "add" ? "添加" : "保存";
+});
+</script>
+
+<style scoped>
+.app-container {
+  padding: 20px 40px;
+  min-height: 100vh;
+  background-color: #f0f2f5;
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.button-group {
+  display: flex;
+  gap: 12px;
+  justify-content: flex-end;
+  width: 100%;
+  margin-bottom: 20px;
+}
+
+.custom-button {
+  color: #fff;
+  border: none;
+  border-radius: 8px;
+  font-size: 14px;
+  padding: 10px 18px;
+  transition: transform 0.3s ease, background-color 0.3s ease;
+  min-width: 130px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.download-button,
+.export-button,
+.add-button,
+.import-button {
+  background-color: #67c23a;
+}
+
+.download-button:hover,
+.export-button:hover,
+.import-button:hover,
+.add-button:hover {
+  background-color: #85ce61;
+  transform: scale(1.05);
+}
+
+.custom-table {
+  width: 100%;
+  border-radius: 8px;
+  overflow: hidden;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+  background-color: #fff;
+  margin-top: 10px;
+}
+
+:deep(.el-table th) {
+  background: linear-gradient(180deg, #61e054, #4db944);
+  color: #fff;
+  font-weight: bold;
+  text-align: center;
+  padding: 14px 0;
+  font-size: 14px;
+}
+
+:deep(.el-table__row:nth-child(odd)) {
+  background-color: #e4fbe5;
+}
+
+:deep(.el-table__row:nth-child(even)) {
+  background-color: #ffffff;
+}
+
+:deep(.el-table td) {
+  padding: 12px 10px;
+  text-align: center;
+  border-bottom: 1px solid #ebeef5;
+}
+
+.action-button {
+  font-size: 16px;
+  margin-right: 5px;
+  border-radius: 50%;
+  transition: transform 0.3s ease, background-color 0.3s ease;
+}
+
+.edit-button {
+  background-color: #409eff;
+  color: #fff;
+}
+
+.edit-button:hover {
+  background-color: #66b1ff;
+  transform: scale(1.1);
+}
+
+.delete-button {
+  background-color: #f56c6c;
+  color: #fff;
+}
+
+.delete-button:hover {
+  background-color: #f78989;
+  transform: scale(1.1);
+}
+
+.el-form-item__label {
+  width: 150px;
+  font-weight: bold;
+}
+
+.el-form-item__content {
+  margin-left: 150px;
+}
+
+.pagination-container {
+  margin-top: 30px;
+  text-align: center;
+}
+
+.action-buttons {
+  display: flex;
+  justify-content: center;
+}
+
+/* ================= 弹框样式优化 ================= */
+:deep(.el-dialog) {
+  border-radius: 16px !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;
+}
+
+/* 弹框头部样式 */
+.dialog-header {
+  position: relative;
+  padding: 20px 24px 10px;
+  text-align: center;
+  background: linear-gradient(90deg, #4db944, #61e054);
+}
+
+.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, #3a9a32, #4fc747);
+  border-radius: 2px;
+  margin-top: 12px;
+  width: 60%;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+/* 表单容器样式 */
+.custom-dialog-form {
+  padding: 25px 40px 15px;
+}
+
+/* 表单项样式优化 */
+.custom-form-item {
+  margin-bottom: 22px !important;
+}
+
+:deep(.el-form-item__label) {
+  display: block;
+  text-align: left;
+  margin-bottom: 8px !important;
+  font-size: 15px;
+  font-weight: 600;
+  color: #4a5568;
+  padding: 0 !important;
+  line-height: 1.5;
+}
+
+/* 输入框样式优化 */
+:deep(.custom-dialog-input .el-input__inner) {
+  height: 44px !important;
+  padding: 0 16px !important;
+  font-size: 15px;
+  border: 1px solid #cbd5e0 !important;
+  border-radius: 10px !important;
+  background-color: #f8fafc;
+  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05);
+  transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
+}
+
+:deep(.custom-dialog-input .el-input__inner:hover) {
+  border-color: #a0aec0 !important;
+}
+
+:deep(.custom-dialog-input .el-input__inner:focus) {
+  border-color: #4db944 !important;
+  box-shadow: 0 0 0 3px rgba(77, 185, 68, 0.15) !important;
+  background-color: #fff;
+}
+
+/* 弹框底部按钮容器 */
+.dialog-footer {
+  display: flex;
+  gap: 16px;
+  justify-content: center;
+  padding: 15px 40px 25px;
+}
+
+/* 按钮基础样式 */
+.custom-cancel-button,
+.custom-submit-button {
+  min-width: 120px;
+  height: 44px;
+  border-radius: 10px;
+  font-size: 16px;
+  font-weight: 500;
+  transition: all 0.3s ease;
+  letter-spacing: 0.5px;
+  border: none;
+  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+}
+
+/* 取消按钮样式优化 */
+.custom-cancel-button {
+  background: linear-gradient(145deg, #f7fafc, #edf2f7);
+  color: #4a5568;
+}
+
+.custom-cancel-button:hover {
+  background: linear-gradient(145deg, #e2e8f0, #cbd5e0);
+  transform: translateY(-2px);
+  box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
+}
+
+/* 提交按钮样式优化 */
+.custom-submit-button {
+  background: linear-gradient(145deg, #4db944, #61e054);
+  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 {
+  transform: translateY(-2px);
+  box-shadow: 0 6px 15px rgba(77, 185, 68, 0.4);
+}
+
+/* 彻底隐藏文件信息相关元素 */
+:deep(.import-upload .el-upload-list) {
+  display: none !important;
+}
+</style>

+ 964 - 0
src/views/Admin/dataManagement/HeavyMetalInputFluxManager/irrigationWaterInputFluxData.vue

@@ -0,0 +1,964 @@
+<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
+        :auto-upload="false"
+        :on-change="handleFileSelect"
+        accept=".xlsx, .csv"
+        class="import-upload"
+      >
+        <el-button
+          :icon="Upload"
+          type="primary"
+          class="custom-button import-button"
+          >导入数据</el-button
+        >
+      </el-upload>
+    </div>
+
+    <!-- 数据表格:仅数字字段NaN显示0,文字字段不处理 -->
+    <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="formatTableValue"
+        align="center"
+        header-align="center"
+      ></el-table-column>
+      <el-table-column label="操作" width="120" align="center">
+        <template #default="scope">
+          <span class="action-buttons">
+            <el-tooltip
+              class="item"
+              effect="dark"
+              content="编辑"
+              placement="top"
+            >
+              <el-button
+                circle
+                :icon="EditPen"
+                @click.stop="openDialog('edit', scope.row)"
+                class="action-button edit-button"
+              ></el-button
+            ></el-tooltip>
+            <el-tooltip
+              class="item"
+              effect="dark"
+              content="删除"
+              placement="top"
+            >
+              <el-button
+                circle
+                :icon="DeleteFilled"
+                @click.stop="deleteItem(scope.row)"
+                class="action-button delete-button"
+              ></el-button
+            ></el-tooltip>
+          </span>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 分页控制 -->
+    <PaginationComponent
+      :total="tableData.length"
+      :currentPage="currentPage4"
+      :pageSize="pageSize4"
+      @update:currentPage="currentPage4 = $event"
+      @update:pageSize="pageSize4 = $event"
+      @size-change="handleSizeChange"
+      @current-change="handleCurrentChange"
+      class="pagination-container"
+    />
+
+    <!-- 新增/编辑对话框:优化样式 -->
+    <el-dialog
+      :title="dialogTitle"
+      v-model="dialogVisible"
+      width="50%"
+      :custom-class="['custom-dialog']"
+      :close-on-click-modal="false"
+      center
+    >
+      <div class="dialog-header">
+        <h2>{{ dialogTitle }}</h2>
+        <div class="header-decoration"></div>
+      </div>
+      <el-form
+        ref="formRef"
+        :model="formData"
+        label-position="top"
+        class="custom-dialog-form"
+      >
+        <el-form-item
+          v-for="col in editableColumns"
+          :key="col.key"
+          :label="col.title"
+          class="custom-form-item"
+          :rules="[
+            {
+              required: isTextField(col.dataKey),
+              message: `请输入${col.title}`,
+              trigger: 'blur',
+            },
+          ]"
+        >
+          <el-input
+            v-model="formData[col.dataKey]"
+            :type="col.inputType || 'text'"
+            class="custom-dialog-input"
+            :placeholder="`请输入${col.title}`"
+          ></el-input>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-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
+          >
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive, computed, onMounted } from "vue";
+import {
+  DeleteFilled,
+  Download,
+  Upload,
+  Plus,
+  EditPen,
+} from "@element-plus/icons-vue";
+import { ElMessage, ElForm } from "element-plus";
+import {
+  table,
+  updateItem,
+  addItem,
+  deleteItemApi,
+  downloadTemplate,
+  exportData,
+  importData,
+} from "@/API/admin";
+import PaginationComponent from "@/components/PaginationComponent.vue";
+
+//  核心:定义字段类型映射(区分文字/数字字段)
+const fieldTypeMap: Record<string, "text" | "number"> = {
+  id: "number", // 采样点ID(数字)
+  sample_code: "number", // 样品编码(文字)
+  sample_number: "text", // 样品编号(文字)
+  longitude: "number", // 经度坐标(数字)
+  latitude: "number", // 纬度坐标(数字)
+  sampling_location: "text", // 采样位置描述(文字)
+  sample_time: "text", // 采样时间(文字,建议后端接收时间格式)
+  weather: "text", // 天气状况(文字)
+  container_material: "text", // 储存容器材质(文字)
+  container_color: "text", // 储存容器颜色(文字)
+  container_capacity: "number", // 储存容器容量(mL)(数字)
+  sampling_volume: "number", // 采样体积(mL)(数字)
+  sample_description: "text", // 样品状态感官描述(文字)
+  water_quality: "text", // 断面水质表现(文字)
+  water_environment: "text", // 断面周边环境(文字)
+  cr_concentration: "number", // 铬(Cr)含量(μg/L)(数字)
+  as_concentration: "number", // 砷(As)含量(μg/L)(数字)
+  cd_concentration: "number", // 镉(Cd)含量(μg/L)(数字)
+  hg_concentration: "number", // 汞(Hg)含量(μg/L)(数字)
+  pb_concentration: "number", // 铅(Pb)含量(μg/L)(数字)
+  ph_value: "number" // 水样pH值(数字)
+};
+
+//  核心:定义必填数字字段(如容量、体积等必须填写的数字字段)
+const requiredNumberFields = [
+  "container_capacity", 
+  "sampling_volume", 
+  "cr_concentration", 
+  "as_concentration", 
+  "cd_concentration", 
+  "hg_concentration", 
+  "pb_concentration", 
+  "ph_value",
+  "longitude",
+  "latitude"
+];
+
+interface Column {
+  key: string;
+  dataKey: string;
+  title: string;
+  width: number;
+  inputType?: string;
+  step?: string; // 数字输入框步长(如小数点后6位)
+}
+
+//  核心:表字段定义(与新字段对应,包含步长配置)
+const columns: Column[] = [
+  { key: "id", dataKey: "id", title: "采样点ID", width: 120, inputType: "number" },
+  { key: "sample_code", dataKey: "sample_code", title: "样品编码", width: 160, inputType: "number" },
+  { key: "sample_number", dataKey: "sample_number", title: "样品编号", width: 160, inputType: "text" },
+  { 
+    key: "longitude", 
+    dataKey: "longitude", 
+    title: "经度坐标", 
+    width: 160, 
+    inputType: "number", 
+    step: "0.000001" // 经度精确到6位小数
+  },
+  { 
+    key: "latitude", 
+    dataKey: "latitude", 
+    title: "纬度坐标", 
+    width: 160, 
+    inputType: "number", 
+    step: "0.000001" // 纬度精确到6位小数
+  },
+  { key: "sampling_location", dataKey: "sampling_location", title: "采样位置描述", width: 220, inputType: "text" },
+  { key: "sample_time", dataKey: "sample_time", title: "采样时间", width: 200, inputType: "text" }, 
+  { key: "weather", dataKey: "weather", title: "天气状况", width: 140, inputType: "text" },
+  { key: "container_material", dataKey: "container_material", title: "储存容器材质", width: 180, inputType: "text" },
+  { key: "container_color", dataKey: "container_color", title: "储存容器颜色", width: 160, inputType: "text" },
+  { key: "container_capacity", dataKey: "container_capacity", title: "储存容器容量(mL)", width: 180, inputType: "number" },
+  { key: "sampling_volume", dataKey: "sampling_volume", title: "采样体积(mL)", width: 160, inputType: "number" },
+  { key: "sample_description", dataKey: "sample_description", title: "样品状态感官描述", width: 220, inputType: "text" },
+  { key: "water_quality", dataKey: "water_quality", title: "断面水质表现", width: 180, inputType: "text" },
+  { key: "water_environment", dataKey: "water_environment", title: "断面周边环境", width: 220, inputType: "text" },
+  { key: "cr_concentration", dataKey: "cr_concentration", title: "铬(Cr)含量(μg/L)", width: 180, inputType: "number" },
+  { key: "as_concentration", dataKey: "as_concentration", title: "砷(As)含量(μg/L)", width: 180, inputType: "number" },
+  { key: "cd_concentration", dataKey: "cd_concentration", title: "镉(Cd)含量(μg/L)", width: 180, inputType: "number" },
+  { key: "hg_concentration", dataKey: "hg_concentration", title: "汞(Hg)含量(μg/L)", width: 180, inputType: "number" },
+  { key: "pb_concentration", dataKey: "pb_concentration", title: "铅(Pb)含量(μg/L)", width: 180, inputType: "number" },
+  { key: "ph_value", dataKey: "ph_value", title: "水样pH值", width: 140, inputType: "number", step: "0.01" } // pH值精确到2位小数
+];
+
+const editableColumns = columns.filter((col) => col.key !== "id"); // 编辑时排除ID(通常自增)
+
+// 核心变量:统一表名(改为水质采样数据表名)
+const currentTableName = "water_sampling_data";
+
+// 表格数据相关
+const tableData = ref<any[]>([]);
+const selectedRow = ref<any | null>(null);
+const loading = ref(false);
+const formRef = ref<InstanceType<typeof ElForm> | null>(null); // 表单引用
+
+// 分页相关
+const currentPage4 = ref(1);
+const pageSize4 = ref(10);
+
+// 对话框相关
+const dialogVisible = ref(false);
+const formData = reactive<any>({});
+const dialogMode = ref<"add" | "edit">("add");
+
+// 辅助函数:判断是否为文字字段
+const isTextField = (dataKey: string): boolean => {
+  return fieldTypeMap[dataKey]?.toLowerCase() === "text";
+};
+
+const isNumberField = (dataKey: string): boolean => {
+  return fieldTypeMap[dataKey]?.toLowerCase() === "number";
+};
+
+// 分页和序号处理:仅数字字段处理NaN为0,文字字段保留空
+const pagedTableDataWithIndex = computed(() => {
+  const start = (currentPage4.value - 1) * pageSize4.value;
+  const end = start + pageSize4.value;
+  return tableData.value.slice(start, end).map((row, idx) => {
+    const processedRow = Object.entries(row).reduce((acc, [key, val]) => {
+      // 仅数字字段:NaN/undefined转为0;文字字段:保留原值(空/文字)
+      if (isNumberField(key)) {
+        acc[key] =
+          (typeof val === "number" && isNaN(val)) || val === undefined
+            ? 0
+            : val;
+      } else {
+        acc[key] = val === undefined || val === null ? "" : val; // 文字字段空值显示空字符串
+      }
+      return acc;
+    }, {} as any);
+    return {
+      ...processedRow,
+      displayIndex: start + idx + 1,
+    };
+  });
+});
+
+// 获取表格数据:仅数字字段处理NaN为0
+const fetchTable = async () => {
+  console.log("正在获取表格数据...");
+  try {
+    loading.value = true;
+    const response = await table({ table: currentTableName });
+    console.log("获取到的数据:", response);
+    
+    // 适配后端返回的直接数组格式
+    const rawData = Array.isArray(response.data) ? response.data : [];
+    
+    // 预处理数据:区分字段类型处理
+    tableData.value = rawData.map((row: any) =>
+      Object.entries(row).reduce((acc: any, [key, val]) => {
+        if (isNumberField(key)) {
+          acc[key] =
+            (typeof val === "number" && isNaN(val)) || val === undefined
+              ? 0
+              : val;
+        } else {
+          acc[key] = val === undefined || val === null ? "" : val; // 文字字段空值存空字符串
+        }
+        return acc;
+      }, {})
+    );
+  } catch (error) {
+    console.error("获取数据时出错:", error);
+    ElMessage.error("获取数据失败,请检查网络连接或服务器状态");
+  } finally {
+    loading.value = false;
+  }
+};
+
+onMounted(() => {
+  fetchTable();
+});
+
+// 表格行点击事件:文字字段保留原值,数字字段处理NaN为0
+const handleRowClick = (row: any) => {
+  console.log("点击行数据:", row);
+  selectedRow.value = row;
+  Object.assign(
+    formData,
+    Object.entries(row).reduce((acc: any, [key, val]) => {
+      if (isNumberField(key)) {
+        acc[key] =
+          (typeof val === "number" && isNaN(val)) || val === undefined
+            ? 0
+            : val;
+      } else {
+        acc[key] = val === undefined || val === null ? "" : val; // 文字字段空值显示空字符串
+      }
+      return acc;
+    }, {})
+  );
+};
+
+// 打开新增/编辑对话框:文字字段置空,数字字段可选置0
+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] = isTextField(col.dataKey) ? "" : "";
+    });
+  } else if (row) {
+    selectedRow.value = row;
+    // 编辑时:区分字段类型赋值
+    Object.assign(
+      formData,
+      Object.entries(row).reduce((acc: any, [key, val]) => {
+        if (isNumberField(key)) {
+          acc[key] =
+            (typeof val === "number" && isNaN(val)) || val === undefined
+              ? 0
+              : val;
+        } else {
+          acc[key] = val === undefined || val === null ? "" : val;
+        }
+        return acc;
+      }, {})
+    );
+  }
+};
+
+// 表单数据预处理:文字字段空字符串转为undefined(适配后端)
+function prepareFormData(
+  formData: { [x: string]: any },
+  excludeKeys = ["displayIndex"]
+): { [x: string]: any } {
+  const result: { [x: string]: any } = {};
+  for (const key in formData) {
+    if (!excludeKeys.includes(key)) {
+      let value = formData[key];
+      // 文字字段:空字符串转为undefined(后端可能需要NULL);数字字段:保留原值
+      if (isTextField(key)) {
+        result[key] = value === "" ? undefined : value;
+      } else {
+        result[key] = value === "" ? undefined : value; // 数字字段空值也转为undefined
+      }
+    }
+  }
+  return result;
+}
+
+// 表单提交:文字字段必填校验
+const submitForm = async () => {
+  // 表单校验:文字字段必填
+  if (!formRef.value) return;
+  
+  try {
+    const isValid = await formRef.value.validate();
+    if (!isValid) return;
+  } catch (error) {
+    console.error("表单验证失败:", error);
+    return;
+  }
+
+  try {
+    const dataToSubmit = prepareFormData(formData);
+    let response;
+
+    if (dialogMode.value === "add") {
+      response = await addItem({
+        table: currentTableName,
+        item: dataToSubmit,
+      });
+      
+      // 添加成功后,直接将新数据添加到本地数组
+      if (response.data && response.data.inserted) {
+        tableData.value.push(response.data.inserted);
+      }
+    } else {
+      // 更详细的错误检查和日志
+      console.log("编辑模式 - selectedRow:", selectedRow.value);
+      
+      if (!selectedRow.value) {
+        ElMessage.error("未选中任何记录,请先选择要编辑的记录");
+        return;
+      }
+      
+      // 检查记录是否有 ID 字段
+      if (!selectedRow.value.hasOwnProperty('id')) {
+        console.error("记录缺少ID字段:", selectedRow.value);
+        ElMessage.error("记录格式错误,缺少ID字段");
+        return;
+      }
+      
+      if (!selectedRow.value.id) {
+        console.error("记录ID为空:", selectedRow.value);
+        ElMessage.error("无法找到记录ID,请联系管理员");
+        return;
+      }
+      
+      response = await updateItem({
+        table: currentTableName,
+        id: selectedRow.value.id,
+        update_data: dataToSubmit,
+      });
+      
+      // 更新成功后,更新本地数据
+      const index = tableData.value.findIndex(item => item.id === selectedRow.value.id);
+      if (index > -1) {
+        tableData.value[index] = {...tableData.value[index], ...dataToSubmit};
+      } else {
+        // 如果本地找不到记录,重新获取数据
+        console.warn("本地未找到对应记录,重新获取数据");
+        fetchTable();
+      }
+    }
+
+    dialogVisible.value = false;
+    ElMessage.success(dialogMode.value === "add" ? "添加成功" : "修改成功");
+  } catch (error) {
+    console.error("提交表单时发生错误:", error);
+    let errorMessage = "未知错误";
+    
+    // 更详细的错误信息提取
+    if (error && typeof error === "object") {
+      const err = error as any;
+      if (err.response && err.response.data) {
+        errorMessage = err.response.data.detail || err.response.data.message || JSON.stringify(err.response.data);
+      } else if (err.message) {
+        errorMessage = err.message;
+      }
+    }    
+    ElMessage.error(`提交失败: ${errorMessage}`);
+  }
+};
+
+// 表单验证:文字字段必填,数字字段可选(根据业务调整)
+function validateFormData(data: { [x: string]: any }) {
+  return editableColumns.every((col) => {
+    const value = data[col.dataKey];
+    // 文字字段:必填(非空字符串);数字字段:可选(允许0或空)
+    if (isTextField(col.dataKey)) {
+      return value !== "" && value !== undefined && value !== null;
+    } else {
+      return true; // 数字字段可选,空值会被后端处理为NULL
+    }
+  });
+}
+
+// 删除记录
+const deleteItem = async (row: any) => {
+  if (!row) {
+    ElMessage.warning("请先选择一行记录");
+    return;
+  }
+
+  try {
+    await deleteItemApi({
+      table: currentTableName,
+      id: row.id,
+    });
+    
+    // 直接从本地数据中删除,避免重新获取全部数据
+    const index = tableData.value.findIndex((item) => item.id === row.id);
+    if (index > -1) {
+      tableData.value.splice(index, 1);
+    }
+    
+    ElMessage.success("记录删除成功");
+  } catch (error) {
+    console.error("删除记录时发生错误:", error);
+    ElMessage.error("删除失败,请重试");
+  }
+};
+
+// 下载模板
+const downloadTemplateAction = async () => {
+  try {
+    await downloadTemplate(currentTableName);
+    ElMessage.success("模板下载成功");
+  } catch (error) {
+    console.error("下载模板时发生错误:", error);
+    ElMessage.error("下载模板失败,请重试");
+  }
+};
+
+// 导出数据
+const exportDataAction = async () => {
+  try {
+    await exportData(currentTableName);
+    ElMessage.success("数据导出成功");
+  } catch (error) {
+    console.error("导出数据时发生错误:", error);
+    ElMessage.error("导出数据失败,请重试");
+  }
+};
+
+// 处理文件选择(导入数据)
+const handleFileSelect = async (uploadFile: any) => {
+  const file = uploadFile.raw;
+  if (!file) {
+    ElMessage.warning("请选择有效的 .xlsx 或 .csv 文件");
+    return;
+  }
+  await importDataAction(file);
+};
+
+// 导入数据:区分字段类型处理
+const importDataAction = async (file: File) => {
+  try {
+    const response = await importData(currentTableName, file);
+    if (response.success) {
+      const { total_data, new_data, duplicate_data } = response;
+      ElMessage({
+        message: `导入成功!共${total_data}条,新增${new_data}条,重复${duplicate_data}条`,
+        type: "success",
+        duration: 3000,
+      });
+      fetchTable(); // 刷新表格,应用字段类型处理逻辑
+    }
+  } catch (error) {
+    let errorMessage = "数据导入失败";
+    if (
+      error &&
+      typeof error === "object" &&
+      "message" in error &&
+      "response" in error &&
+      typeof (error as any).response === "object"
+    ) {
+      // 适配后端返回的重复记录详情
+      if ((error as any).response?.data?.detail) {
+        const detail = (error as any).response.data.detail;
+        if (detail.duplicates) {
+          // 显示重复记录的行号和文字字段内容
+          const duplicateMsg = detail.duplicates
+            .map(
+              (item: any) =>
+                `第${item.row_number}行(${Object.entries(item.duplicate_fields)
+                  .map(([k, v]) => `${k}=${v || "空"}`)
+                  .join(",")}`
+            )
+            .join(";");
+          errorMessage = `导入失败:${detail.message},重复记录:${duplicateMsg}`;
+        } else {
+          errorMessage = `导入失败:${detail.message || detail}`;
+        }
+      } else {
+        errorMessage += `: ${(error as unknown as Error).message}`;
+      }
+    }
+    ElMessage.error({
+      message: errorMessage,
+      duration: 5000, // 长一点的提示时间,让用户看清重复详情
+    });
+  }
+};
+
+// 分页大小改变
+const handleSizeChange = (val: number) => {
+  pageSize4.value = val;
+  currentPage4.value = 1;
+};
+
+// 当前页改变
+const handleCurrentChange = (val: number) => {
+  currentPage4.value = val;
+};
+
+// 表格值格式化:仅数字字段保留3位小数,文字字段显示原始内容
+const formatTableValue = (row: any, column: any, cellValue: any) => {
+  const dataKey = column.prop;
+  // 文字字段:空值显示空字符串,非空显示原始文字
+  if (isTextField(dataKey)) {
+    return cellValue === undefined || cellValue === null ? "" : cellValue;
+  }
+  // 数字字段:NaN/undefined显示0,保留3位小数
+  if (isNumberField(dataKey)) {
+    if (isNaN(cellValue) || cellValue === undefined || cellValue === null) {
+      return 0;
+    }
+    return typeof cellValue === "number" ? cellValue.toFixed(3) : cellValue;
+  }
+  // 其他字段:默认显示
+  return cellValue;
+};
+
+// 对话框标题
+const dialogTitle = computed(() => {
+  return dialogMode.value === "add"
+    ? `新增记录`
+    : "编辑记录";
+});
+
+// 对话框提交按钮文本
+const dialogSubmitButtonText = computed(() => {
+  return dialogMode.value === "add" ? "添加" : "保存";
+});
+</script>
+
+<style scoped>
+.app-container {
+  padding: 20px 40px;
+  min-height: 100vh;
+  background-color: #f0f2f5;
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.button-group {
+  display: flex;
+  gap: 12px;
+  justify-content: flex-end;
+  width: 100%;
+  margin-bottom: 20px;
+}
+
+.custom-button {
+  color: #fff;
+  border: none;
+  border-radius: 8px;
+  font-size: 14px;
+  padding: 10px 18px;
+  transition: transform 0.3s ease, background-color 0.3s ease;
+  min-width: 130px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.download-button,
+.export-button,
+.add-button,
+.import-button {
+  background-color: #67c23a;
+}
+
+.download-button:hover,
+.export-button:hover,
+.import-button:hover,
+.add-button:hover {
+  background-color: #85ce61;
+  transform: scale(1.05);
+}
+
+.custom-table {
+  width: 100%;
+  border-radius: 8px;
+  overflow: hidden;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+  background-color: #fff;
+  margin-top: 10px;
+}
+
+:deep(.el-table th) {
+  background: linear-gradient(180deg, #61e054, #4db944);
+  color: #fff;
+  font-weight: bold;
+  text-align: center;
+  padding: 14px 0;
+  font-size: 14px;
+}
+
+:deep(.el-table__row:nth-child(odd)) {
+  background-color: #e4fbe5;
+}
+
+:deep(.el-table__row:nth-child(even)) {
+  background-color: #ffffff;
+}
+
+:deep(.el-table td) {
+  padding: 12px 10px;
+  text-align: center;
+  border-bottom: 1px solid #ebeef5;
+}
+
+.action-button {
+  font-size: 16px;
+  margin-right: 5px;
+  border-radius: 50%;
+  transition: transform 0.3s ease, background-color 0.3s ease;
+}
+
+.edit-button {
+  background-color: #409eff;
+  color: #fff;
+}
+
+.edit-button:hover {
+  background-color: #66b1ff;
+  transform: scale(1.1);
+}
+
+.delete-button {
+  background-color: #f56c6c;
+  color: #fff;
+}
+
+.delete-button:hover {
+  background-color: #f78989;
+  transform: scale(1.1);
+}
+
+.el-form-item__label {
+  width: 150px;
+  font-weight: bold;
+}
+
+.el-form-item__content {
+  margin-left: 150px;
+}
+
+.pagination-container {
+  margin-top: 30px;
+  text-align: center;
+}
+
+.action-buttons {
+  display: flex;
+  justify-content: center;
+}
+
+/* ================= 弹框样式优化 ================= */
+:deep(.el-dialog) {
+  border-radius: 16px !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;
+}
+
+/* 弹框头部样式 */
+.dialog-header {
+  position: relative;
+  padding: 20px 24px 10px;
+  text-align: center;
+  background: linear-gradient(90deg, #4db944, #61e054);
+}
+
+.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, #3a9a32, #4fc747);
+  border-radius: 2px;
+  margin-top: 12px;
+  width: 60%;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+/* 表单容器样式 */
+.custom-dialog-form {
+  padding: 25px 40px 15px;
+}
+
+/* 表单项样式优化 */
+.custom-form-item {
+  margin-bottom: 22px !important;
+}
+
+:deep(.el-form-item__label) {
+  display: block;
+  text-align: left;
+  margin-bottom: 8px !important;
+  font-size: 15px;
+  font-weight: 600;
+  color: #4a5568;
+  padding: 0 !important;
+  line-height: 1.5;
+}
+
+/* 输入框样式优化 */
+:deep(.custom-dialog-input .el-input__inner) {
+  height: 44px !important;
+  padding: 0 16px !important;
+  font-size: 15px;
+  border: 1px solid #cbd5e0 !important;
+  border-radius: 10px !important;
+  background-color: #f8fafc;
+  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05);
+  transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
+}
+
+:deep(.custom-dialog-input .el-input__inner:hover) {
+  border-color: #a0aec0 !important;
+}
+
+:deep(.custom-dialog-input .el-input__inner:focus) {
+  border-color: #4db944 !important;
+  box-shadow: 0 0 0 3px rgba(77, 185, 68, 0.15) !important;
+  background-color: #fff;
+}
+
+/* 弹框底部按钮容器 */
+.dialog-footer {
+  display: flex;
+  gap: 16px;
+  justify-content: center;
+  padding: 15px 40px 25px;
+}
+
+/* 按钮基础样式 */
+.custom-cancel-button,
+.custom-submit-button {
+  min-width: 120px;
+  height: 44px;
+  border-radius: 10px;
+  font-size: 16px;
+  font-weight: 500;
+  transition: all 0.3s ease;
+  letter-spacing: 0.5px;
+  border: none;
+  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+}
+
+/* 取消按钮样式优化 */
+.custom-cancel-button {
+  background: linear-gradient(145deg, #f7fafc, #edf2f7);
+  color: #4a5568;
+}
+
+.custom-cancel-button:hover {
+  background: linear-gradient(145deg, #e2e8f0, #cbd5e0);
+  transform: translateY(-2px);
+  box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
+}
+
+/* 提交按钮样式优化 */
+.custom-submit-button {
+  background: linear-gradient(145deg, #4db944, #61e054);
+  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 {
+  transform: translateY(-2px);
+  box-shadow: 0 6px 15px rgba(77, 185, 68, 0.4);
+}
+
+/* 彻底隐藏文件信息相关元素 */
+:deep(.import-upload .el-upload-list) {
+  display: none !important;
+}
+</style>

+ 942 - 0
src/views/Admin/dataManagement/HeavyMetalInputFluxManager/irrigationWaterSampleData.vue

@@ -0,0 +1,942 @@
+<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
+        :auto-upload="false"
+        :on-change="handleFileSelect"
+        accept=".xlsx, .csv"
+        class="import-upload"
+      >
+        <el-button
+          :icon="Upload"
+          type="primary"
+          class="custom-button import-button"
+          >导入数据</el-button
+        >
+      </el-upload>
+    </div>
+
+    <!-- 数据表格:仅数字字段NaN显示0,文字字段不处理 -->
+    <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="formatTableValue"
+        align="center"
+        header-align="center"
+      ></el-table-column>
+      <el-table-column label="操作" width="120" align="center">
+        <template #default="scope">
+          <span class="action-buttons">
+            <el-tooltip
+              class="item"
+              effect="dark"
+              content="编辑"
+              placement="top"
+            >
+              <el-button
+                circle
+                :icon="EditPen"
+                @click.stop="openDialog('edit', scope.row)"
+                class="action-button edit-button"
+              ></el-button
+            ></el-tooltip>
+            <el-tooltip
+              class="item"
+              effect="dark"
+              content="删除"
+              placement="top"
+            >
+              <el-button
+                circle
+                :icon="DeleteFilled"
+                @click.stop="deleteItem(scope.row)"
+                class="action-button delete-button"
+              ></el-button
+            ></el-tooltip>
+          </span>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 分页控制 -->
+    <PaginationComponent
+      :total="tableData.length"
+      :currentPage="currentPage4"
+      :pageSize="pageSize4"
+      @update:currentPage="currentPage4 = $event"
+      @update:pageSize="pageSize4 = $event"
+      @size-change="handleSizeChange"
+      @current-change="handleCurrentChange"
+      class="pagination-container"
+    />
+
+    <!-- 新增/编辑对话框:优化样式 -->
+    <el-dialog
+      :title="dialogTitle"
+      v-model="dialogVisible"
+      width="50%"
+      :custom-class="['custom-dialog']"
+      :close-on-click-modal="false"
+      center
+    >
+      <div class="dialog-header">
+        <h2>{{ dialogTitle }}</h2>
+        <div class="header-decoration"></div>
+      </div>
+      <el-form
+        ref="formRef"
+        :model="formData"
+        label-position="top"
+        class="custom-dialog-form"
+      >
+        <el-form-item
+          v-for="col in editableColumns"
+          :key="col.key"
+          :label="col.title"
+          class="custom-form-item"
+          :rules="[
+            {
+              required: isTextField(col.dataKey),
+              message: `请输入${col.title}`,
+              trigger: 'blur',
+            },
+          ]"
+        >
+          <el-input
+            v-model="formData[col.dataKey]"
+            :type="col.inputType || 'text'"
+            class="custom-dialog-input"
+            :placeholder="`请输入${col.title}`"
+          ></el-input>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-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
+          >
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive, computed, onMounted } from "vue";
+import {
+  DeleteFilled,
+  Download,
+  Upload,
+  Plus,
+  EditPen,
+} from "@element-plus/icons-vue";
+import { ElMessage, ElForm } from "element-plus";
+import {
+  table,
+  updateItem,
+  addItem,
+  deleteItemApi,
+  downloadTemplate,
+  exportData,
+  importData,
+} from "@/API/admin";
+import PaginationComponent from "@/components/PaginationComponent.vue";
+
+// 核心:定义字段类型映射(区分文字/数字字段)
+const fieldTypeMap: Record<string, "text" | "number"> = {
+  longitude: "number",
+  latitude: "number",
+  company_name: "text",
+  company_type: "text",
+  county: "text",
+  particulate_emission: "number",
+};
+
+interface Column {
+  key: string;
+  dataKey: string;
+  title: string;
+  width: number;
+  inputType?: string;
+}
+
+// 表字段定义(与fieldTypeMap对应)
+const columns: Column[] = [
+  { key: "id", dataKey: "id", title: "ID", width: 80 },
+  {
+    key: "longitude",
+    dataKey: "longitude",
+    title: "经度坐标",
+    width: 150,
+    inputType: "number",
+  },
+  {
+    key: "latitude",
+    dataKey: "latitude",
+    title: "纬度坐标",
+    width: 150,
+    inputType: "number",
+  },
+  {
+    key: "company_name",
+    dataKey: "company_name",
+    title: "企业名称",
+    width: 200,
+    inputType: "text",
+  },
+  {
+    key: "company_type",
+    dataKey: "company_type",
+    title: "企业类型",
+    width: 150,
+    inputType: "text",
+  },
+  {
+    key: "county",
+    dataKey: "county",
+    title: "所属区县",
+    width: 200,
+    inputType: "text",
+  },
+  {
+    key: "particulate_emission",
+    dataKey: "particulate_emission",
+    title: "大气颗粒物排放量(吨/年)",
+    width: 150,
+    inputType: "number",
+  },
+];
+
+const editableColumns = columns.filter((col) => col.key !== "id");
+
+// 核心变量:统一表名
+const currentTableName = "atmo_company";
+
+// 表格数据相关
+const tableData = ref<any[]>([]);
+const selectedRow = ref<any | null>(null);
+const loading = ref(false);
+const formRef = ref<InstanceType<typeof ElForm> | null>(null); // 表单引用
+
+// 分页相关
+const currentPage4 = ref(1);
+const pageSize4 = ref(10);
+
+// 对话框相关
+const dialogVisible = ref(false);
+const formData = reactive<any>({});
+const dialogMode = ref<"add" | "edit">("add");
+
+// 辅助函数:判断是否为文字字段
+const isTextField = (dataKey: string): boolean => {
+  return fieldTypeMap[dataKey]?.toLowerCase() === "text";
+};
+
+const isNumberField = (dataKey: string): boolean => {
+  return fieldTypeMap[dataKey]?.toLowerCase() === "number";
+};
+
+// 分页和序号处理:仅数字字段处理NaN为0,文字字段保留空
+const pagedTableDataWithIndex = computed(() => {
+  const start = (currentPage4.value - 1) * pageSize4.value;
+  const end = start + pageSize4.value;
+  return tableData.value.slice(start, end).map((row, idx) => {
+    const processedRow = Object.entries(row).reduce((acc, [key, val]) => {
+      // 仅数字字段:NaN/undefined转为0;文字字段:保留原值(空/文字)
+      if (isNumberField(key)) {
+        acc[key] =
+          (typeof val === "number" && isNaN(val)) || val === undefined
+            ? 0
+            : val;
+      } else {
+        acc[key] = val === undefined || val === null ? "" : val; // 文字字段空值显示空字符串
+      }
+      return acc;
+    }, {} as any);
+    return {
+      ...processedRow,
+      displayIndex: start + idx + 1,
+    };
+  });
+});
+
+// 获取表格数据:仅数字字段处理NaN为0
+const fetchTable = async () => {
+  console.log("正在获取表格数据...");
+  try {
+    loading.value = true;
+    const response = await table({ table: currentTableName });
+    console.log("获取到的数据:", response);
+    
+    // 适配后端返回的直接数组格式
+    const rawData = Array.isArray(response.data) ? response.data : [];
+    
+    // 预处理数据:区分字段类型处理
+    tableData.value = rawData.map((row: any) =>
+      Object.entries(row).reduce((acc: any, [key, val]) => {
+        if (isNumberField(key)) {
+          acc[key] =
+            (typeof val === "number" && isNaN(val)) || val === undefined
+              ? 0
+              : val;
+        } else {
+          acc[key] = val === undefined || val === null ? "" : val; // 文字字段空值存空字符串
+        }
+        return acc;
+      }, {})
+    );
+  } catch (error) {
+    console.error("获取数据时出错:", error);
+    ElMessage.error("获取数据失败,请检查网络连接或服务器状态");
+  } finally {
+    loading.value = false;
+  }
+};
+
+onMounted(() => {
+  fetchTable();
+});
+
+// 表格行点击事件:文字字段保留原值,数字字段处理NaN为0
+const handleRowClick = (row: any) => {
+  console.log("点击行数据:", row);
+  selectedRow.value = row;
+  Object.assign(
+    formData,
+    Object.entries(row).reduce((acc: any, [key, val]) => {
+      if (isNumberField(key)) {
+        acc[key] =
+          (typeof val === "number" && isNaN(val)) || val === undefined
+            ? 0
+            : val;
+      } else {
+        acc[key] = val === undefined || val === null ? "" : val; // 文字字段空值显示空字符串
+      }
+      return acc;
+    }, {})
+  );
+};
+
+// 打开新增/编辑对话框:文字字段置空,数字字段可选置0
+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] = isTextField(col.dataKey) ? "" : "";
+    });
+  } else if (row) {
+    selectedRow.value = row;
+    // 编辑时:区分字段类型赋值
+    Object.assign(
+      formData,
+      Object.entries(row).reduce((acc: any, [key, val]) => {
+        if (isNumberField(key)) {
+          acc[key] =
+            (typeof val === "number" && isNaN(val)) || val === undefined
+              ? 0
+              : val;
+        } else {
+          acc[key] = val === undefined || val === null ? "" : val;
+        }
+        return acc;
+      }, {})
+    );
+  }
+};
+
+// 表单数据预处理:文字字段空字符串转为undefined(适配后端)
+function prepareFormData(
+  formData: { [x: string]: any },
+  excludeKeys = ["displayIndex"]
+): { [x: string]: any } {
+  const result: { [x: string]: any } = {};
+  for (const key in formData) {
+    if (!excludeKeys.includes(key)) {
+      let value = formData[key];
+      // 文字字段:空字符串转为undefined(后端可能需要NULL);数字字段:保留原值
+      if (isTextField(key)) {
+        result[key] = value === "" ? undefined : value;
+      } else {
+        result[key] = value === "" ? undefined : value; // 数字字段空值也转为undefined
+      }
+    }
+  }
+  return result;
+}
+
+// 表单提交:文字字段必填校验
+const submitForm = async () => {
+  // 表单校验:文字字段必填
+  if (!formRef.value) return;
+  
+  try {
+    const isValid = await formRef.value.validate();
+    if (!isValid) return;
+  } catch (error) {
+    console.error("表单验证失败:", error);
+    return;
+  }
+
+  try {
+    const dataToSubmit = prepareFormData(formData);
+    let response;
+
+    if (dialogMode.value === "add") {
+      response = await addItem({
+        table: currentTableName,
+        item: dataToSubmit,
+      });
+      
+      // 添加成功后,直接将新数据添加到本地数组
+      if (response.data && response.data.inserted) {
+        tableData.value.push(response.data.inserted);
+      }
+    } else {
+      // 更详细的错误检查和日志
+      console.log("编辑模式 - selectedRow:", selectedRow.value);
+      
+      if (!selectedRow.value) {
+        ElMessage.error("未选中任何记录,请先选择要编辑的记录");
+        return;
+      }
+      
+      // 检查记录是否有 ID 字段
+      if (!selectedRow.value.hasOwnProperty('id')) {
+        console.error("记录缺少ID字段:", selectedRow.value);
+        ElMessage.error("记录格式错误,缺少ID字段");
+        return;
+      }
+      
+      if (!selectedRow.value.id) {
+        console.error("记录ID为空:", selectedRow.value);
+        ElMessage.error("无法找到记录ID,请联系管理员");
+        return;
+      }
+      
+      response = await updateItem({
+        table: currentTableName,
+        id: selectedRow.value.id,
+        update_data: dataToSubmit,
+      });
+      
+      // 更新成功后,更新本地数据
+      const index = tableData.value.findIndex(item => item.id === selectedRow.value.id);
+      if (index > -1) {
+        tableData.value[index] = {...tableData.value[index], ...dataToSubmit};
+      } else {
+        // 如果本地找不到记录,重新获取数据
+        console.warn("本地未找到对应记录,重新获取数据");
+        fetchTable();
+      }
+    }
+
+    dialogVisible.value = false;
+    ElMessage.success(dialogMode.value === "add" ? "添加成功" : "修改成功");
+  } catch (error) {
+    console.error("提交表单时发生错误:", error);
+    let errorMessage = "未知错误";
+    
+    // 更详细的错误信息提取
+    if (error && typeof error === "object") {
+      const err = error as any;
+      if (err.response && err.response.data) {
+        errorMessage = err.response.data.detail || err.response.data.message || JSON.stringify(err.response.data);
+      } else if (err.message) {
+        errorMessage = err.message;
+      }
+    }    
+    ElMessage.error(`提交失败: ${errorMessage}`);
+  }
+};
+
+// 表单验证:文字字段必填,数字字段可选(根据业务调整)
+function validateFormData(data: { [x: string]: any }) {
+  return editableColumns.every((col) => {
+    const value = data[col.dataKey];
+    // 文字字段:必填(非空字符串);数字字段:可选(允许0或空)
+    if (isTextField(col.dataKey)) {
+      return value !== "" && value !== undefined && value !== null;
+    } else {
+      return true; // 数字字段可选,空值会被后端处理为NULL
+    }
+  });
+}
+
+// 删除记录
+const deleteItem = async (row: any) => {
+  if (!row) {
+    ElMessage.warning("请先选择一行记录");
+    return;
+  }
+
+  try {
+    await deleteItemApi({
+      table: currentTableName,
+      id: row.id,
+    });
+    
+    // 直接从本地数据中删除,避免重新获取全部数据
+    const index = tableData.value.findIndex((item) => item.id === row.id);
+    if (index > -1) {
+      tableData.value.splice(index, 1);
+    }
+    
+    ElMessage.success("记录删除成功");
+  } catch (error) {
+    console.error("删除记录时发生错误:", error);
+    ElMessage.error("删除失败,请重试");
+  }
+};
+
+// 下载模板
+const downloadTemplateAction = async () => {
+  try {
+    await downloadTemplate(currentTableName);
+    ElMessage.success("模板下载成功");
+  } catch (error) {
+    console.error("下载模板时发生错误:", error);
+    ElMessage.error("下载模板失败,请重试");
+  }
+};
+
+// 导出数据
+const exportDataAction = async () => {
+  try {
+    await exportData(currentTableName);
+    ElMessage.success("数据导出成功");
+  } catch (error) {
+    console.error("导出数据时发生错误:", error);
+    ElMessage.error("导出数据失败,请重试");
+  }
+};
+
+// 处理文件选择(导入数据)
+const handleFileSelect = async (uploadFile: any) => {
+  const file = uploadFile.raw;
+  if (!file) {
+    ElMessage.warning("请选择有效的 .xlsx 或 .csv 文件");
+    return;
+  }
+  await importDataAction(file);
+};
+
+// 导入数据:区分字段类型处理
+const importDataAction = async (file: File) => {
+  try {
+    const response = await importData(currentTableName, file);
+    if (response.success) {
+      const { total_data, new_data, duplicate_data } = response;
+      ElMessage({
+        message: `导入成功!共${total_data}条,新增${new_data}条,重复${duplicate_data}条`,
+        type: "success",
+        duration: 3000,
+      });
+      fetchTable(); // 刷新表格,应用字段类型处理逻辑
+    }
+  } catch (error) {
+    let errorMessage = "数据导入失败";
+    if (
+      error &&
+      typeof error === "object" &&
+      "message" in error &&
+      "response" in error &&
+      typeof (error as any).response === "object"
+    ) {
+      // 适配后端返回的重复记录详情
+      if ((error as any).response?.data?.detail) {
+        const detail = (error as any).response.data.detail;
+        if (detail.duplicates) {
+          // 显示重复记录的行号和文字字段内容
+          const duplicateMsg = detail.duplicates
+            .map(
+              (item: any) =>
+                `第${item.row_number}行(${Object.entries(item.duplicate_fields)
+                  .map(([k, v]) => `${k}=${v || "空"}`)
+                  .join(",")}`
+            )
+            .join(";");
+          errorMessage = `导入失败:${detail.message},重复记录:${duplicateMsg}`;
+        } else {
+          errorMessage = `导入失败:${detail.message || detail}`;
+        }
+      } else {
+        errorMessage += `: ${(error as unknown as Error).message}`;
+      }
+    }
+    ElMessage.error({
+      message: errorMessage,
+      duration: 5000, // 长一点的提示时间,让用户看清重复详情
+    });
+  }
+};
+
+// 分页大小改变
+const handleSizeChange = (val: number) => {
+  pageSize4.value = val;
+  currentPage4.value = 1;
+};
+
+// 当前页改变
+const handleCurrentChange = (val: number) => {
+  currentPage4.value = val;
+};
+
+// 表格值格式化:仅数字字段保留3位小数,文字字段显示原始内容
+const formatTableValue = (row: any, column: any, cellValue: any) => {
+  const dataKey = column.prop;
+  // 文字字段:空值显示空字符串,非空显示原始文字
+  if (isTextField(dataKey)) {
+    return cellValue === undefined || cellValue === null ? "" : cellValue;
+  }
+  // 数字字段:NaN/undefined显示0,保留3位小数
+  if (isNumberField(dataKey)) {
+    if (isNaN(cellValue) || cellValue === undefined || cellValue === null) {
+      return 0;
+    }
+    return typeof cellValue === "number" ? cellValue.toFixed(3) : cellValue;
+  }
+  // 其他字段:默认显示
+  return cellValue;
+};
+
+// 对话框标题
+const dialogTitle = computed(() => {
+  return dialogMode.value === "add"
+    ? `新增记录`
+    : "编辑记录";
+});
+
+// 对话框提交按钮文本
+const dialogSubmitButtonText = computed(() => {
+  return dialogMode.value === "add" ? "添加" : "保存";
+});
+</script>
+
+<style scoped>
+.app-container {
+  padding: 20px 40px;
+  min-height: 100vh;
+  background-color: #f0f2f5;
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.button-group {
+  display: flex;
+  gap: 12px;
+  justify-content: flex-end;
+  width: 100%;
+  margin-bottom: 20px;
+}
+
+.custom-button {
+  color: #fff;
+  border: none;
+  border-radius: 8px;
+  font-size: 14px;
+  padding: 10px 18px;
+  transition: transform 0.3s ease, background-color 0.3s ease;
+  min-width: 130px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.download-button,
+.export-button,
+.add-button,
+.import-button {
+  background-color: #67c23a;
+}
+
+.download-button:hover,
+.export-button:hover,
+.import-button:hover,
+.add-button:hover {
+  background-color: #85ce61;
+  transform: scale(1.05);
+}
+
+.custom-table {
+  width: 100%;
+  border-radius: 8px;
+  overflow: hidden;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+  background-color: #fff;
+  margin-top: 10px;
+}
+
+:deep(.el-table th) {
+  background: linear-gradient(180deg, #61e054, #4db944);
+  color: #fff;
+  font-weight: bold;
+  text-align: center;
+  padding: 14px 0;
+  font-size: 14px;
+}
+
+:deep(.el-table__row:nth-child(odd)) {
+  background-color: #e4fbe5;
+}
+
+:deep(.el-table__row:nth-child(even)) {
+  background-color: #ffffff;
+}
+
+:deep(.el-table td) {
+  padding: 12px 10px;
+  text-align: center;
+  border-bottom: 1px solid #ebeef5;
+}
+
+.action-button {
+  font-size: 16px;
+  margin-right: 5px;
+  border-radius: 50%;
+  transition: transform 0.3s ease, background-color 0.3s ease;
+}
+
+.edit-button {
+  background-color: #409eff;
+  color: #fff;
+}
+
+.edit-button:hover {
+  background-color: #66b1ff;
+  transform: scale(1.1);
+}
+
+.delete-button {
+  background-color: #f56c6c;
+  color: #fff;
+}
+
+.delete-button:hover {
+  background-color: #f78989;
+  transform: scale(1.1);
+}
+
+.el-form-item__label {
+  width: 150px;
+  font-weight: bold;
+}
+
+.el-form-item__content {
+  margin-left: 150px;
+}
+
+.pagination-container {
+  margin-top: 30px;
+  text-align: center;
+}
+
+.action-buttons {
+  display: flex;
+  justify-content: center;
+}
+
+/* ================= 弹框样式优化 ================= */
+:deep(.el-dialog) {
+  border-radius: 16px !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;
+}
+
+/* 弹框头部样式 */
+.dialog-header {
+  position: relative;
+  padding: 20px 24px 10px;
+  text-align: center;
+  background: linear-gradient(90deg, #4db944, #61e054);
+}
+
+.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, #3a9a32, #4fc747);
+  border-radius: 2px;
+  margin-top: 12px;
+  width: 60%;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+/* 表单容器样式 */
+.custom-dialog-form {
+  padding: 25px 40px 15px;
+}
+
+/* 表单项样式优化 */
+.custom-form-item {
+  margin-bottom: 22px !important;
+}
+
+:deep(.el-form-item__label) {
+  display: block;
+  text-align: left;
+  margin-bottom: 8px !important;
+  font-size: 15px;
+  font-weight: 600;
+  color: #4a5568;
+  padding: 0 !important;
+  line-height: 1.5;
+}
+
+/* 输入框样式优化 */
+:deep(.custom-dialog-input .el-input__inner) {
+  height: 44px !important;
+  padding: 0 16px !important;
+  font-size: 15px;
+  border: 1px solid #cbd5e0 !important;
+  border-radius: 10px !important;
+  background-color: #f8fafc;
+  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05);
+  transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
+}
+
+:deep(.custom-dialog-input .el-input__inner:hover) {
+  border-color: #a0aec0 !important;
+}
+
+:deep(.custom-dialog-input .el-input__inner:focus) {
+  border-color: #4db944 !important;
+  box-shadow: 0 0 0 3px rgba(77, 185, 68, 0.15) !important;
+  background-color: #fff;
+}
+
+/* 弹框底部按钮容器 */
+.dialog-footer {
+  display: flex;
+  gap: 16px;
+  justify-content: center;
+  padding: 15px 40px 25px;
+}
+
+/* 按钮基础样式 */
+.custom-cancel-button,
+.custom-submit-button {
+  min-width: 120px;
+  height: 44px;
+  border-radius: 10px;
+  font-size: 16px;
+  font-weight: 500;
+  transition: all 0.3s ease;
+  letter-spacing: 0.5px;
+  border: none;
+  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+}
+
+/* 取消按钮样式优化 */
+.custom-cancel-button {
+  background: linear-gradient(145deg, #f7fafc, #edf2f7);
+  color: #4a5568;
+}
+
+.custom-cancel-button:hover {
+  background: linear-gradient(145deg, #e2e8f0, #cbd5e0);
+  transform: translateY(-2px);
+  box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
+}
+
+/* 提交按钮样式优化 */
+.custom-submit-button {
+  background: linear-gradient(145deg, #4db944, #61e054);
+  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 {
+  transform: translateY(-2px);
+  box-shadow: 0 6px 15px rgba(77, 185, 68, 0.4);
+}
+
+/* 彻底隐藏文件信息相关元素 */
+:deep(.import-upload .el-upload-list) {
+  display: none !important;
+}
+</style>

+ 159 - 14
src/views/Admin/userManagement/UserManagement.vue

@@ -1,23 +1,168 @@
 <template>
-  <div class="">
-    12
+  <div class="user-management">
+    <el-card class="box-card">
+      <h2>用户管理</h2>
+
+      <el-table :data="users" style="width: 100%" border>
+        <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="注册时间" />
+        <el-table-column label="操作" width="200">
+          <template #default="scope">
+            <el-button size="small" type="primary" @click="onEdit(scope.row)">编辑</el-button>
+            <el-button size="small" type="danger" @click="onDelete(scope.row)">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-card>
+
+    <!-- 编辑用户 Dialog -->
+    <el-dialog
+      title="编辑用户"
+      v-model="showDialog"
+      :modal-append-to-body="false"
+      :style="{ top: '120px' }"
+    >
+      <el-form :model="form" ref="formRef" label-width="100px">
+        <el-form-item label="用户名">
+          <el-input v-model="form.name" />
+        </el-form-item>
+        <el-form-item label="密码">
+          <el-input type="password" v-model="form.password" placeholder="留空则不修改密码" />
+        </el-form-item>
+        <el-form-item label="用户类型">
+          <el-select v-model="form.userType" placeholder="请选择">
+            <el-option label="普通用户" value="user" />
+            <el-option label="管理员" value="admin" />
+          </el-select>
+        </el-form-item>
+      </el-form>
+
+      <template #footer>
+        <el-button @click="showDialog = false">取消</el-button>
+        <el-button type="primary" @click="onSubmit">确定</el-button>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
-<script>
-export default {
-  name: '',
-  data() {
-    return {
-      
-    };
-  },
-  methods: {
-    
+<script setup lang="ts">
+import { ref, reactive, onMounted } from "vue";
+import { ElMessage, ElForm } from "element-plus";
+import { getUsers, updateUser, deleteUser } from "@/API/users";
+
+interface User {
+  id: number;
+  name: string;
+  userType: string;
+  created_at: string;
+}
+
+const users = ref<User[]>([]);
+const showDialog = ref(false);
+const editUser = ref<User | null>(null);
+
+const formRef = ref<InstanceType<typeof ElForm> | null>(null);
+const form = reactive({
+  name: "",
+  password: "",
+  userType: "user",
+});
+
+// 用户类型映射函数
+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();
+  const MM = String(date.getMonth() + 1).padStart(2, "0");
+  const dd = String(date.getDate()).padStart(2, "0");
+  const hh = String(date.getHours()).padStart(2, "0");
+  const mm = String(date.getMinutes()).padStart(2, "0");
+  const ss = String(date.getSeconds()).padStart(2, "0");
+  return `${yyyy}-${MM}-${dd} ${hh}:${mm}:${ss}`;
+};
+
+// 加载用户列表
+const loadUsers = async () => {
+  try {
+    const res = await getUsers();
+    users.value = res.data.users.map((u: User) => ({
+      ...u,
+      created_at: formatTime(u.created_at),
+    }));
+  } catch (err: any) {
+    ElMessage.error("加载用户失败");
+    console.error(err);
+  }
+};
+
+// 编辑用户
+const onEdit = (user: User) => {
+  editUser.value = user;
+  form.name = user.name;
+  form.userType = user.userType;
+  form.password = "";
+  showDialog.value = true;
+};
+
+// 删除用户
+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);
   }
 };
+
+// 提交编辑(三选一校验)
+const onSubmit = async () => {
+  if (!editUser.value) return;
+
+  // 检查是否至少修改一项
+  const changed = form.name !== editUser.value.name ||
+                  form.password ||
+                  form.userType !== editUser.value.userType;
+
+  if (!changed) {
+    ElMessage.warning("请至少修改用户名、密码或用户类型中的一项");
+    return;
+  }
+
+  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);
+  }
+};
+
+onMounted(loadUsers);
 </script>
 
 <style scoped>
-  
-</style>
+.user-management .card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+}
+</style>

+ 144 - 37
src/views/Admin/userManagement/UserRegistration.vue

@@ -1,51 +1,158 @@
 <template>
-  <div id="app">
-    <el-card class="box-card">
-      <h2>User Data</h2>
-      <el-table :data="userData" style="width: 100%">
-        <el-table-column prop="username" label="Username" width="180"></el-table-column>
-        <el-table-column prop="action" label="Action" width="180"></el-table-column>
-        <el-table-column prop="timestamp" label="Timestamp"></el-table-column>
-      </el-table>
-    </el-card>
+  <div class="register-wrapper">
+    <el-form
+      ref="registerFormRef"
+      :model="registerForm"
+      :rules="registerRules"
+      label-width="116px"
+      class="register-form"
+    >
+      <!-- 输入账号 -->
+      <div class="input-frame">
+        <el-form-item label="账号:" prop="name">
+          <el-input v-model="registerForm.name" />
+        </el-form-item>
+      </div>
+
+      <!-- 输入密码 -->
+      <div class="input-frame">
+        <el-form-item label="密码:" prop="password">
+          <el-input type="password" v-model="registerForm.password" />
+        </el-form-item>
+      </div>
+
+      <!-- 确认密码 -->
+      <div class="input-frame">
+        <el-form-item label="确认密码:" prop="confirmPassword">
+          <el-input type="password" v-model="registerForm.confirmPassword" />
+        </el-form-item>
+      </div>
+
+      <!-- 注册按钮 -->
+      <el-form-item>
+        <div class="button-group">
+          <el-button
+            type="primary"
+            @click="onRegister"
+            :loading="loading"
+            class="register-button"
+          >
+            注册
+          </el-button>
+        </div>
+      </el-form-item>
+    </el-form>
   </div>
 </template>
 
-<script>
-import { ref, onMounted } from 'vue';
-import { ElCard, ElTable, ElTableColumn, ElMessage } from 'element-plus';
-
-export default {
-  name: 'UserAuth',
-  components: {
-    ElCard,
-    ElTable,
-    ElTableColumn
-  },
-  setup() {
-    const userData = ref([]);
-
-    // 模拟一些初始数据
-    const initialData = [
-      { username: 'user1', action: 'Registered', timestamp: new Date().toLocaleString() },
-      { username: 'user2', action: 'Logged In', timestamp: new Date().toLocaleString() }
-    ];
-
-    onMounted(() => {
-      // 加载初始数据
-      userData.value = initialData;
+<script setup lang="ts">
+import { reactive, ref } from "vue";
+import { ElForm, ElMessage } from "element-plus";
+import { register } from "@/API/users";
+
+const loading = ref(false);
+
+// 注册表单数据
+const registerForm = reactive({ name: "", password: "", confirmPassword: "" });
+const registerFormRef = ref<InstanceType<typeof ElForm> | null>(null);
+
+// 表单校验规则
+const registerRules = {
+  name: [{ required: true, message: "请输入账号", trigger: "blur" }],
+  password: [
+    { required: true, message: "请输入密码", trigger: "blur" },
+    { min: 3, max: 16, message: "密码长度应为3-16位", trigger: "blur" }
+  ],
+  confirmPassword: [
+    { required: true, message: "请确认密码", trigger: "blur" },
+    {
+      validator: (_rule: any, value: string, callback: (error?: Error) => void) => {
+        if (value !== registerForm.password) callback(new Error("两次密码输入不一致"));
+        else callback();
+      },
+      trigger: "blur"
+    }
+  ]
+};
+
+// 注册逻辑,默认用户类型为普通用户
+const onRegister = async () => {
+  if (!registerFormRef.value) return;
+  try {
+    await registerFormRef.value.validate();
+    loading.value = true;
+
+    const res = await register({
+      name: registerForm.name,
+      password: registerForm.password,
+      userType: "user", // 强制普通用户
     });
 
-    return {
-      userData
-    };
+    if (res.data?.message) {
+      ElMessage.success(res.data.message);
+      registerForm.name = "";
+      registerForm.password = "";
+      registerForm.confirmPassword = "";
+    } else {
+      ElMessage.error(res.data?.message || "注册失败");
+    }
+  } catch (error: any) {
+    ElMessage.error(error?.response?.data?.detail || "注册异常");
+  } finally {
+    loading.value = false;
   }
 };
 </script>
 
-<style>
+<style scoped>
+.register-wrapper {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  height: 70vh;
+  background-color: #f6f6f6;
+}
 
-</style>
+.register-form {
+  width: 100%;
+  max-width: 500px;
+  padding: 40px 30px;
+  background: rgba(255, 255, 255, 0.95);
+  border-radius: 15px;
+}
+
+.input-frame {
+  margin-bottom: 20px;
+}
+
+:deep(.el-form-item__label) {
+  font-size: 20px;
+  color: #7e7878;
+}
 
+:deep(.el-input .el-input__inner) {
+  height: 50px;
+  font-size: 18px;
+  border-radius: 5px;
+}
 
+.register-button {
+  width: 300px;
+  max-width: 3000px; /* 最大宽度 */
+  height: 56px;     /* 高度加大 */
+  font-size: 20px;
+  border-radius: 15px;
+  background: linear-gradient(to right, #8df9f0, #26b046);
+  color: #fff;
+}
+
+.register-button:hover {
+  opacity: 0.9;
+}
+
+.button-group {
+  display: flex;
+  justify-content: center;
+}
+</style>
 

+ 215 - 285
src/views/login/loginView.vue

@@ -1,119 +1,124 @@
 <template>
   <div class="auth-wrapper">
-    <!-- 左侧背景图部分 -->
+    <!-- 左侧背景图区域 -->
     <div class="auth-left"></div>
 
-    <!-- 登录/注册表单部分 -->
+    <!-- 右侧表单容器 -->
     <div class="auth-form-container">
       <!-- 登录表单 -->
-      <el-form v-if="isLogin" ref="formRef" :model="form" :rules="rules" label-width="116px" class="login-form">
+      <el-form
+        v-if="isLogin"
+        ref="formRef"
+        :model="form"
+        :rules="rules"
+        label-width="116px"
+        class="login-form"
+      >
         <div class="form-header">
-          <!-- 根据用户类型显示不同的标题 -->
           <h2 class="form-title">
             {{
-              userType === "user"
-                ? $t("login.userTitle")
-                : $t("login.adminTitle")
+              userType === "user" ? t("login.userTitle") : t("login.adminTitle")
             }}
           </h2>
-          <!-- 切换用户类型的按钮 -->
           <el-button class="user-type-toggle" @click="toggleUserType" link>
-            <el-icon>
-              <User />
-            </el-icon>
+            <el-icon><User /></el-icon>
             <span>{{ currentUserTypeName }}</span>
           </el-button>
         </div>
 
-        <!-- 用户名输入框 -->
         <div class="input-frame">
           <el-form-item label="账号:" prop="name">
-            <el-input v-model="form.name"></el-input>
+            <el-input v-model="form.name" />
           </el-form-item>
         </div>
 
-        <!-- 密码输入框 -->
         <div class="input-frame">
           <el-form-item label="密码:" prop="password">
-            <el-input type="password" v-model="form.password"></el-input>
+            <el-input type="password" v-model="form.password" />
           </el-form-item>
         </div>
 
-        <!-- 语言切换按钮 -->
         <div class="language-toggle-wrapper">
           <span class="text-toggle" @click="toggleLanguage">{{
             currentLanguageName
           }}</span>
         </div>
 
-        <!-- 提交登录按钮和注册链接 -->
         <el-form-item>
           <div class="button-group">
-            <el-button type="primary" @click="onSubmit" :loading="loading" class="login-button">
-              {{ $t("login.loginButton") }}
+            <el-button
+              type="primary"
+              @click="onSubmit"
+              :loading="loading"
+              class="login-button"
+            >
+              {{ t("login.loginButton") }}
             </el-button>
           </div>
           <div class="text-link-wrapper">
             <span class="text-toggle" @click="toggleForm">{{
-              $t("login.registerLink")
+              t("login.registerLink")
             }}</span>
           </div>
         </el-form-item>
       </el-form>
 
       <!-- 注册表单 -->
-      <el-form v-else ref="registerFormRef" :model="registerForm" :rules="registerRules" label-width="116px"
-        class="login-form">
+      <el-form
+        v-else
+        ref="registerFormRef"
+        :model="registerForm"
+        :rules="registerRules"
+        label-width="116px"
+        class="login-form"
+      >
         <div class="form-header">
-          <!-- 注册表单标题 -->
-          <h2 class="form-title">{{ $t("register.title") }}</h2>
-          <!-- 切换用户类型的按钮 -->
+          <h2 class="form-title">{{ t("register.title") }}</h2>
           <el-button class="user-type-toggle" @click="toggleUserType" link>
-            <el-icon>
-              <User />
-            </el-icon>
+            <el-icon><User /></el-icon>
             <span>{{ currentUserTypeName }}</span>
           </el-button>
         </div>
 
-        <!-- 用户名输入框 -->
         <div class="input-frame">
           <el-form-item label="账号:" prop="name">
-            <el-input v-model="registerForm.name"></el-input>
+            <el-input v-model="registerForm.name" />
           </el-form-item>
         </div>
 
-        <!-- 密码输入框 -->
         <div class="input-frame">
           <el-form-item label="密码:" prop="password">
-            <el-input type="password" v-model="registerForm.password"></el-input>
+            <el-input type="password" v-model="registerForm.password" />
           </el-form-item>
         </div>
 
-        <!-- 确认密码输入框 -->
         <div class="input-frame">
           <el-form-item label="确认密码:" prop="confirmPassword">
-            <el-input type="password" v-model="registerForm.confirmPassword"></el-input>
+            <el-input type="password" v-model="registerForm.confirmPassword" />
           </el-form-item>
         </div>
 
-        <!-- 语言切换按钮 -->
         <div class="language-toggle-wrapper">
           <span class="text-toggle" @click="toggleLanguage">{{
             currentLanguageName
           }}</span>
         </div>
-        <!-- 提交注册按钮和返回登录链接 -->
+
         <el-form-item>
           <div class="button-group">
-            <el-button type="primary" @click="onRegister" :loading="loading" class="login-button">
-              {{ $t("register.registerButton") }}
+            <el-button
+              type="primary"
+              @click="onRegister"
+              :loading="loading"
+              class="login-button"
+            >
+              {{ t("register.registerButton") }}
             </el-button>
           </div>
           <div class="button-group register-link-container">
-            <span @click="toggleForm" class="register-button">
-              {{ $t("register.backToLoginButton") }}
-            </span>
+            <span @click="toggleForm" class="register-button">{{
+              t("register.backToLoginButton")
+            }}</span>
           </div>
         </el-form-item>
       </el-form>
@@ -121,227 +126,205 @@
   </div>
 </template>
 
-<script lang="ts" setup>
-import { reactive, ref, computed } from "vue";
-import { ElMessage, ElForm } from "element-plus";
-import { User } from "@element-plus/icons-vue";
-import axios from "axios";
+<script setup lang="ts">
+import { reactive, ref, computed, watch, onMounted } from "vue";
+import { ElForm, ElMessage } from "element-plus";
+import type { FormRules } from "element-plus";
 import { login, register } from "@/API/users";
-import { useRouter, useRoute } from "vue-router";
 import { useTokenStore } from "@/stores/mytoken";
 import { useI18n } from "vue-i18n";
-import i18n from "@/i18n";
+import { User } from "@element-plus/icons-vue";
+import { useRouter } from "vue-router";
+
+// ============ 类型定义 ============
+interface LoginForm {
+  name: string;
+  password: string;
+}
+interface RegisterForm {
+  name: string;
+  password: string;
+  confirmPassword: string;
+}
 
-// 获取状态管理实例
+// ============ 核心实例 ============
 const store = useTokenStore();
+const { t, locale } = useI18n();
 const router = useRouter();
-const route = useRoute();
 
-// 控制是否显示登录表单
+// ============ 状态 ============
 const isLogin = ref(true);
-// 当前用户类型(用户或管理员)
-const userType = ref("user");
-
-// 登录表单数据模型
-const form = reactive({
-  name: "",
-  password: "",
-});
+const userType = ref<"user" | "admin">("user");
+const loading = ref(false);
 
-// 注册表单数据模型
-const registerForm = reactive({
+const form = reactive<LoginForm>({ name: "", password: "" });
+const registerForm = reactive<RegisterForm>({
   name: "",
   password: "",
   confirmPassword: "",
 });
 
-// 表单引用
 const formRef = ref<InstanceType<typeof ElForm> | null>(null);
 const registerFormRef = ref<InstanceType<typeof ElForm> | null>(null);
 
-// 加载状态控制
-const loading = ref(false);
-
-// 国际化相关
-const { t, locale } = useI18n();
-
-// 当前语言名称
-const currentLanguageName = computed(() => {
-  return locale.value === "zh" ? "English" : "中文";
-});
-
-// 切换语言
-const toggleLanguage = () => {
-  locale.value = locale.value === "zh" ? "en" : "zh";
-  localStorage.setItem("lang", locale.value);
+// ============ 表单切换 ============
+const toggleForm = () => {
+  isLogin.value = !isLogin.value;
 };
-
-// 当前用户类型名称
-const currentUserTypeName = computed(() => {
-  return userType.value === "user"
-    ? t("login.switchToAdmin")
-    : t("login.switchToUser");
-});
-
-// 切换用户类型
 const toggleUserType = () => {
   userType.value = userType.value === "user" ? "admin" : "user";
 };
-
-// 切换登录/注册表单
-const toggleForm = () => {
-  isLogin.value = !isLogin.value;
+const toggleLanguage = () => {
+  locale.value = locale.value === "zh" ? "en" : "zh";
+  localStorage.setItem("lang", locale.value);
 };
 
-// 提交登录请求
+// ============ 计算属性 ============
+const currentLanguageName = computed(() =>
+  locale.value === "zh" ? "English" : "中文"
+);
+const currentUserTypeName = computed(() =>
+  userType.value === "user" ? t("login.switchToAdmin") : t("login.switchToUser")
+);
+
+// ============ 登录逻辑 ============
 const onSubmit = async () => {
   if (!formRef.value) return;
-  formRef.value.validate(async (valid: boolean) => {
-    if (!valid) return;
+  try {
+    await formRef.value.validate();
     loading.value = true;
-    try {
-      const res = await login({
-        name: form.name,
-        password: form.password,
-        userType: userType.value,
-      });
-      if (res.data.success) {
-        const userInfo = {
-          userId: parseInt(res.data.userId),
-          name: res.data.name,
-          loginType: userType.value === "admin" ? "admin" : "user",
-        };
-        store.saveToken(userInfo);
-        ElMessage.success(i18n.global.t("login.loginSuccess"));
-        // 根据用户类型跳转到不同页面
-        if (userType.value === "user") {
-          router.push({ name: "selectCityAndCounty" });
-        } else {
-          const redirect =
-            typeof route.query.redirect === "string"
-              ? route.query.redirect
-              : "/select-city";
-          router.push(redirect);
-        }
-      } else {
-        ElMessage.error(res.data.message || i18n.global.t("login.loginFailed"));
-      }
-    } catch (error) {
-      if (axios.isAxiosError(error)) {
-        ElMessage.error(`HTTP Error: ${error.response?.statusText}`);
-      } else {
-        ElMessage.error(i18n.global.t("login.loginFailed"));
-      }
-    } finally {
-      loading.value = false;
+
+    const res = await login({
+      username: form.name,
+      password: form.password,
+      usertype: userType.value,
+    });
+
+    const user = res.data?.user;
+    if (!user) {
+      ElMessage.error(res.data?.message || t("login.loginFailed"));
+      return;
     }
-  });
+
+    // 普通用户选择 admin → 强制按真实类型登录
+    if (user.userType !== "admin" && userType.value === "admin") {
+      ElMessage.warning(t("login.userTypeMismatch"));
+      userType.value = "user";
+    }
+
+    // 管理员自由选择登录类型
+    store.saveToken({
+      userId: user.id,
+      name: user.name,
+      loginType: user.userType === "admin" ? userType.value : "user",
+    });
+
+    ElMessage.success(res.data?.message || t("login.loginSuccess"));
+    router.push({ name: "selectCityAndCounty" });
+  } catch (error: any) {
+    console.error("登录异常:", error);
+    ElMessage.error(error?.response?.data?.detail || t("login.loginFailed"));
+  } finally {
+    loading.value = false;
+  }
 };
 
-// 提交注册请求
+// ============ 注册逻辑 ============
 const onRegister = async () => {
   if (!registerFormRef.value) return;
-  registerFormRef.value.validate(async (valid: boolean) => {
-    if (!valid) return;
+  try {
+    await registerFormRef.value.validate();
+    loading.value = true;
 
-    // 检查两次输入的密码是否一致
-    if (registerForm.password !== registerForm.confirmPassword) {
-      ElMessage.error(i18n.global.t("register.passwordMismatch"));
-      return;
+    const res = await register({
+      name: registerForm.name,
+      password: registerForm.password,
+      userType: userType.value,
+    });
+
+    if (res.data?.message) {
+      ElMessage.success(res.data.message);
+      toggleForm();
+    } else {
+      ElMessage.error(res.data?.message || t("register.registerFailed"));
     }
+  } catch (error: any) {
+    console.error("注册异常:", error);
+    ElMessage.error(
+      error?.response?.data?.detail || t("register.registerFailed")
+    );
+  } finally {
+    loading.value = false;
+  }
+};
 
-    try {
-      const res = await register({
-        name: registerForm.name,
-        password: registerForm.password,
-      });
-      if (res.data.success) {
-        ElMessage.success(i18n.global.t("register.registerSuccess"));
-        toggleForm(); // 注册成功后切换回登录表单
-      } else {
-        ElMessage.error(
-          res.data.message || i18n.global.t("register.registerFailed")
-        );
-      }
-    } catch (error) {
-      if (axios.isAxiosError(error)) {
-        ElMessage.error(`HTTP Error: ${error.response?.statusText}`);
-      } else {
-        ElMessage.error(i18n.global.t("register.registerFailed"));
-      }
-    } finally {
-      loading.value = false;
-    }
-  });
+// ============ 校验规则 ============
+const createRules = (): { rules: FormRules; registerRules: FormRules } => {
+  const rules: FormRules = {
+    name: [
+      {
+        required: true,
+        message: t("validation.usernameRequired"),
+        trigger: "blur",
+      },
+    ],
+    password: [
+      {
+        required: true,
+        message: t("validation.passwordRequired"),
+        trigger: "blur",
+      },
+      {
+        min: 3,
+        max: 16,
+        message: t("validation.passwordLength"),
+        trigger: "blur",
+      },
+    ],
+  };
+  const registerRules: FormRules = {
+    name: rules.name,
+    password: rules.password,
+    confirmPassword: [
+      {
+        required: true,
+        message: t("validation.confirmPasswordRequired"),
+        trigger: "blur",
+      },
+      {
+        validator: (_rule, value: string, callback) => {
+          if (value !== registerForm.password)
+            callback(new Error(t("validation.passwordMismatch")));
+          else callback();
+        },
+        trigger: "blur",
+      },
+    ],
+  };
+  return { rules, registerRules };
 };
 
-// 登录表单验证规则
-const rules = reactive({
-  name: [
-    {
-      required: true,
-      message: i18n.global.t("validation.usernameRequired"),
-      trigger: "blur",
-    },
-  ],
-  password: [
-    {
-      required: true,
-      message: i18n.global.t("validation.passwordRequired"),
-      trigger: "blur",
-    },
-    {
-      min: 3,
-      max: 16,
-      message: i18n.global.t("validation.passwordLength"),
-      trigger: "blur",
-    },
-  ],
-});
+let { rules, registerRules } = createRules();
 
-// 注册表单验证规则
-const registerRules = reactive({
-  name: [
-    {
-      required: true,
-      message: i18n.global.t("validation.usernameRequired"),
-      trigger: "blur",
-    },
-  ],
-  password: [
-    {
-      required: true,
-      message: i18n.global.t("validation.passwordRequired"),
-      trigger: "blur",
-    },
-    {
-      min: 3,
-      max: 16,
-      message: i18n.global.t("validation.passwordLength"),
-      trigger: "blur",
-    },
-  ],
-  confirmPassword: [
-    {
-      required: true,
-      message: i18n.global.t("validation.confirmPasswordRequired"),
-      trigger: "blur",
-    },
-    {
-      validator: (
-        rule: any,
-        value: string,
-        callback: (error?: Error) => void
-      ) => {
-        if (value !== registerForm.password) {
-          callback(new Error(i18n.global.t("validation.passwordMismatch")));
-        } else {
-          callback();
-        }
-      },
-      trigger: "blur",
-    },
-  ],
+watch(locale, () => {
+  const newRules = createRules();
+  rules = newRules.rules;
+  registerRules = newRules.registerRules;
+});
+watch(
+  () => registerForm.password,
+  () => {
+    registerFormRef.value?.validateField("confirmPassword");
+  }
+);
+
+onMounted(() => {
+  console.log("登录/注册页初始化", {
+    form,
+    registerForm,
+    userType: userType.value,
+    locale: locale.value,
+  });
 });
 </script>
 
@@ -351,14 +334,10 @@ const registerRules = reactive({
   height: 100vh;
   background-color: #f6f6f6;
 }
-
 .auth-left {
   width: 35%;
-  background: url("@/assets/login-bg.png") no-repeat center center;
-  background-size: cover;
+  background: url("@/assets/login-bg.png") no-repeat center/cover;
 }
-
-/* 调整表单容器大小 */
 .auth-form-container {
   width: 55%;
   padding: 0 40px 0 60px;
@@ -367,8 +346,6 @@ const registerRules = reactive({
   align-items: center;
   flex-direction: column;
 }
-
-/* 调整表单整体大小 */
 .login-form {
   width: 100%;
   max-width: 700px;
@@ -377,8 +354,6 @@ const registerRules = reactive({
   background: rgba(255, 255, 255, 0.9);
   border-radius: 15px;
 }
-
-/* 表单头部 */
 .form-header {
   display: flex;
   justify-content: space-between;
@@ -386,65 +361,39 @@ const registerRules = reactive({
   margin-bottom: 40px;
   margin-top: 20px;
 }
-
 .form-title {
   font-size: 32px;
   font-weight: 600;
   color: #333;
 }
-
 .user-type-toggle {
-  position: relative;
-  top: -50px;
-  right: 20px;
-  margin-left: 10px;
   font-size: 36px;
   color: #333;
 }
-
 .user-type-toggle span {
   margin-left: 6px;
-  color: #333;
   font-size: 36px;
 }
-
-/* 语言切换按钮 */
 .language-toggle-wrapper {
   text-align: right;
-  margin: 5px 0 10px;
+  margin: 15px 0 20px;
 }
-
-/* 表单项样式 */
 :deep(.el-form-item__label) {
-  float: none !important;
+  float: none;
   display: block;
   text-align: left;
   font-size: 24px;
   padding-bottom: 8px;
-  color: #7E7878;
-}
-
-:deep(.el-form-item) {
-  display: flex;
-  flex-direction: column;
-  margin-bottom: 0;
-}
-
-/* 输入框样式 - 改为长方形 */
-:deep(.el-input) {
-  width: 100%;
+  color: #7e7878;
 }
-
 :deep(.el-input .el-input__inner) {
   height: 50px;
   font-size: 20px;
-  border-radius: 0; /* 将圆角改为0,变成直角 */
+  border-radius: 0;
   border: 1px solid #dcdfe6;
-  background-color: #ffffff;
+  background-color: #fff;
   padding: 0 15px;
 }
-
-/* 登录按钮 */
 .login-button {
   background: linear-gradient(to right, #8df9f0, #26b046);
   width: 100%;
@@ -457,16 +406,9 @@ const registerRules = reactive({
   cursor: pointer;
   margin-top: 10px;
 }
-
 .login-button:hover {
   opacity: 0.9;
 }
-
-/* 注册按钮容器 */
-.register-link-container {
-  margin-top: 20px;
-}
-
 .register-button {
   display: block;
   text-align: center;
@@ -476,44 +418,32 @@ const registerRules = reactive({
   padding: 10px 0;
   width: 100%;
 }
-
 .register-button:hover {
   color: #357ae8;
   text-decoration: underline;
 }
-
-/* 按钮容器 */
 .button-group {
   display: flex;
   justify-content: center;
   width: 100%;
 }
-
 .text-toggle {
   color: #478bf0;
   font-size: 16px;
   cursor: pointer;
 }
-
 .text-toggle:hover {
   color: #357ae8;
   text-decoration: underline;
 }
-
-.language-toggle-wrapper {
-  text-align: right;
-  margin: 15px 0 20px;
-}
-
 .text-link-wrapper {
   text-align: center;
   margin-top: 20px;
 }
-
 .input-frame {
   background-color: #fff;
   width: 100%;
   padding: 15px 10px;
   margin-bottom: 20px;
 }
-</style>
+</style>