Pārlūkot izejas kodu

已把功能集成进来

qw 19 stundas atpakaļ
vecāks
revīzija
79e7f551a2

+ 1 - 1
.env

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

+ 2 - 0
components.d.ts

@@ -16,6 +16,8 @@ declare module 'vue' {
     ElAvatar: typeof import('element-plus/es')['ElAvatar']
     ElButton: typeof import('element-plus/es')['ElButton']
     ElCard: typeof import('element-plus/es')['ElCard']
+    ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
+    ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
     ElCol: typeof import('element-plus/es')['ElCol']
     ElContainer: typeof import('element-plus/es')['ElContainer']
     ElDialog: typeof import('element-plus/es')['ElDialog']

+ 72 - 230
src/components/layout/AppAside.vue

@@ -5,290 +5,132 @@
       router
       unique-opened
       :default-active="activeMenuItem.index"
+      class="professional-menu"
     >
-      <!-- 根据 activeTab 动态渲染菜单项 -->
-      <template v-if="activeTab === 'shuJuKanBan'">
-        <el-menu-item
-          index="/shuJuKanBan"
-          @click="handleMenuClick('/shuJuKanBan')"
-          >数据看板</el-menu-item
-        >
-      </template>
-
-      <template v-else-if="activeTab === 'introduction'">
-        <el-menu-item index="/SoilPro" @click="handleMenuClick('/SoilPro')"
-          >软件简介</el-menu-item
-        >
-        <el-menu-item index="/Overview" @click="handleMenuClick('/Overview')"
-          >项目简介</el-menu-item
-        >
-        <el-menu-item
-          index="/ResearchFindings"
-          @click="handleMenuClick('/ResearchFindings')"
-          >研究成果</el-menu-item
-        >
-        <el-menu-item index="/Unit" @click="handleMenuClick('/Unit')"
-          >团队信息</el-menu-item
-        >
-      </template>
-
-      <template v-else-if="activeTab === 'heavyMetalFluxCalculation'">
-        <el-sub-menu index="1">
+      <template v-for="item in filteredMenuItems" :key="item.index">
+        <el-sub-menu v-if="item.children" :index="item.index">
           <template #title>
-            <span>输入通量计算</span>
+            <el-icon><component :is="item.icon" /></el-icon>
+            <span>{{ item.label }}</span>
           </template>
           <el-menu-item
-            index="/irrigationWater"
-            @click="handleMenuClick('/irrigationWater')"
-            >灌溉水</el-menu-item
-          >
-          <el-menu-item
-            index="/agriculturalProductInput"
-            @click="handleMenuClick('/agriculturalProductInput')"
-            >农产品投入</el-menu-item
-          >
-          <el-menu-item
-            index="/atmosphericDryWetDeposition"
-            @click="handleMenuClick('/atmosphericDryWetDeposition')"
-            >大气干湿沉降</el-menu-item
+            v-for="child in item.children"
+            :key="child.index"
+            :index="child.index"
+            @click="handleMenuClick(child.index)"
           >
+            <el-icon><component :is="child.icon" /></el-icon>
+            <span>{{ child.label }}</span>
+          </el-menu-item>
         </el-sub-menu>
 
-        <el-sub-menu index="2">
-          <template #title>
-            <span>输出通量计算</span>
-          </template>
-          <el-menu-item
-            index="/surfaceRunoff"
-            @click="handleMenuClick('/surfaceRunoff')"
-            >地表径流</el-menu-item
-          >
-          <el-menu-item
-            index="/cropRemoval"
-            @click="handleMenuClick('/cropRemoval')"
-            >农作物移除</el-menu-item
-          >
-          <el-menu-item
-            index="/subsurfaceFlow"
-            @click="handleMenuClick('/subsurfaceFlow')"
-            >地下渗流</el-menu-item
-          >
-        </el-sub-menu>
-      </template>
-
-      <template v-else-if="activeTab === 'mapView'">
-        <el-menu-item index="/mapView" @click="handleMenuClick('/mapView')"
-          >地图展示</el-menu-item
-        >
-      </template>
-
-      <template v-else-if="activeTab === 'cadmiumPrediction'">
-        <el-menu-item
-          index="/TotalCadmiumPrediction"
-          @click="handleMenuClick('/TotalCadmiumPrediction')"
-          >土壤镉的总含量预测</el-menu-item
-        >
-        <el-menu-item
-          index="/EffectiveCadmiumPrediction"
-          @click="handleMenuClick('/EffectiveCadmiumPrediction')"
-          >土壤镉有效态含量预测</el-menu-item
-        >
-      </template>
-
-      <template v-else-if="activeTab === 'cropRiskAssessment'">
-        <el-menu-item
-          index="/cropRiskAssessment"
-          @click="handleMenuClick('/cropRiskAssessment')"
-          >水稻镉污染风险</el-menu-item
-        >
-      </template>
-
-      <template v-else-if="activeTab === 'farmlandQualityAssessment'">
-        <el-menu-item
-          index="/farmlandQualityAssessment"
-          @click="handleMenuClick('/farmlandQualityAssessment')"
-          >韶关</el-menu-item
-        >
-      </template>
-
-      <template v-else-if="activeTab === 'soilAcidificationPrediction'">
-        <el-menu-item
-          index="/Calculation"
-          @click="handleMenuClick('/Calculation')"
-          >土壤反酸预测</el-menu-item
-        >
         <el-menu-item
-          index="/AcidNeutralizationModel"
-          @click="handleMenuClick('/AcidNeutralizationModel')"
-          >土壤降酸预测</el-menu-item
+          v-else
+          :index="item.index"
+          @click="handleMenuClick(item.index)"
         >
-      </template>
-
-      <template v-else-if="activeTab === 'scenarioSimulation'">
-        <el-menu-item
-          index="/TraditionalFarmingRisk"
-          @click="handleMenuClick('/TraditionalFarmingRisk')"
-          >传统耕种习惯风险趋势</el-menu-item
-        >
-        <el-menu-item
-          index="/HeavyMetalCadmiumControl"
-          @click="handleMenuClick('/HeavyMetalCadmiumControl')"
-          >重金属镉污染治理</el-menu-item
-        >
-        <el-menu-item
-          index="/SoilAcidificationControl"
-          @click="handleMenuClick('/SoilAcidificationControl')"
-          >土壤酸化治理</el-menu-item
-        >
-      </template>
-
-      <template v-else-if="activeTab === 'dataStatistics'">
-        <el-menu-item
-          index="/DetectionStatistics"
-          @click="handleMenuClick('/DetectionStatistics')"
-          >检测信息统计</el-menu-item
-        >
-        <el-menu-item
-          index="/FarmlandPollutionStatistics"
-          @click="handleMenuClick('/FarmlandPollutionStatistics')"
-          >耕地污染信息统计</el-menu-item
-        >
-        <el-menu-item
-          index="/PlantingRiskStatistics"
-          @click="handleMenuClick('/PlantingRiskStatistics')"
-          >种植风险信息统计</el-menu-item
-        >
-      </template>
-
-      <template v-else>
-        <el-menu-item disabled>未知的 Tab:{{ activeTab }}</el-menu-item>
+          <el-icon><component :is="item.icon" /></el-icon>
+          <span>{{ item.label }}</span>
+        </el-menu-item>
       </template>
     </el-menu>
   </el-scrollbar>
 </template>
 
 <script setup lang="ts">
-import { inject, reactive, watch, toRefs } from "vue";
-import { ElMessage } from "element-plus";
-import { useRouter } from "vue-router";
+import { reactive, computed, inject, toRefs, watch } from 'vue';
+import { useRouter } from 'vue-router';
+import { ElMessage } from 'element-plus';
+import { menuItems } from './menuItems'; // 建议你将 menuItems 独立维护在该文件中
 
 const props = defineProps({
   activeTab: {
     type: String,
     required: true,
-    default: "introduction",
+    default: 'introduction'
   },
   showTabs: {
     type: Boolean,
     required: false,
-    default: true,
-  },
+    default: true
+  }
 });
 
 const { activeTab } = toRefs(props);
-const isCollapse = inject("isCollapse", false);
-
-const activeMenuItem = reactive({ index: "" });
+const isCollapse = inject('isCollapse', false);
+const userPermissions = inject('userPermissions', [] as string[]);
 
 const router = useRouter();
+const activeMenuItem = reactive({ index: '' });
+
+const filteredMenuItems = computed(() =>
+  menuItems
+    .filter(item => item.tab === activeTab.value)
+    .map(item => {
+      if (!item.children) return item;
+      return {
+        ...item,
+        children: item.children.filter(
+          child => !child.permission || userPermissions.includes(child.permission)
+        )
+      };
+    })
+);
 
-function logAction(action: string) {
-  console.log(`[AppAside] ${action}`);
-}
-
-function showError(message: string) {
-  ElMessage.error(message);
-}
-
-async function handleMenuClick(index: string) {
+function handleMenuClick(index: string) {
   try {
-    logAction(`Menu item clicked: ${index}`);
     if (router.currentRoute.value.path !== index) {
-      // 避免重复导航
-      await router.push(index);
+      router.push(index);
       activeMenuItem.index = index;
     }
   } catch (error) {
-    console.error(`[AppAside] Error navigating to ${index}:`, error);
-    if ((error as any).code === "ERR_NETWORK") {
-      showError("网络错误,请检查您的网络连接。");
-    } else {
-      showError("导航失败,请检查您的网络连接或联系管理员。");
-    }
-  }
-}
-
-// 统一设置默认激活菜单项的函数
-function getDefaultMenuIndex(tab: string) {
-  switch (tab) {
-    case "shuJuKanBan":
-      return "/shuJuKanBan";
-    case "introduction":
-      return "/SoilPro";
-    case "heavyMetalFluxCalculation":
-      return "/inputFluxCalculation/irrigationWater";
-    case "mapView":
-      return "/mapView";
-    case "cadmiumPrediction":
-      return "/TotalCadmiumPrediction";
-    case "cropRiskAssessment":
-      return "/cropRiskAssessment";
-    case "farmlandQualityAssessment":
-      return "/farmlandQualityAssessment";
-    case "soilAcidificationPrediction":
-      return "/Calculation";
-    case "scenarioSimulation":
-      return "/TraditionalFarmingRisk";
-    case "dataStatistics":
-      return "/DetectionStatistics";
-    case "selectCityAndCounty":
-      return "";
-    default:
-      return "";
+    ElMessage.error('导航失败,请检查网络或联系管理员');
   }
 }
 
 watch(
   activeTab,
-  (newVal) => {
-    console.log(`[AppAside] activeTab changed to: ${newVal}`);
-    activeMenuItem.index = getDefaultMenuIndex(newVal);
-  },
-  { immediate: true }
-);
-
-watch(
-  () => isCollapse,
-  (newVal) => {
-    console.log(`[AppAside] isCollapse changed to: ${newVal}`);
+  newVal => {
+    const current = filteredMenuItems.value?.[0];
+    activeMenuItem.index = current?.children?.[0]?.index || current?.index || '';
   },
   { immediate: true }
 );
 </script>
 
 <style scoped>
-.el-menu {
-  background-color: #ffffff;
+.professional-menu .el-menu {
+  background-color: #f4f6f9;
   border-right: none;
+  margin-top: 20px;
+  box-shadow: 2px 0 10px rgba(0, 0, 0, 0.05);
+  font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
+  border-radius: 0 12px 12px 0;
 }
 
-.el-menu-item {
-  color: #000000;
-  font-size: 14px;
-  padding: 10px 20px;
-  border-radius: 6px;
+.professional-menu .el-menu-item,
+.professional-menu .el-submenu__title {
+  color: #333;
+  font-size: 15px;
+  padding: 14px 20px;
   transition: background-color 0.3s ease, color 0.3s ease;
+  border-radius: 8px;
 }
 
-.el-menu-item:hover {
-  background-color: #f3f4f6;
-  color: #2563eb;
+.professional-menu .el-menu-item:hover,
+.professional-menu .el-submenu__title:hover {
+  background-color: #e6f0ff;
+  color: #007bff;
 }
 
-.el-menu-item.is-active {
-  background-color: #2563eb;
-  color: #ffffff;
+.professional-menu .el-menu-item.is-active {
+  background-color: #007bff;
+  color: #fff;
   font-weight: bold;
-  box-shadow: inset 0 0 10px rgba(37, 99, 235, 0.5);
+  box-shadow: inset 0 0 8px rgba(0, 123, 255, 0.4);
+}
+
+.professional-menu .el-icon {
+  margin-right: 8px;
 }
 </style>

+ 89 - 99
src/components/layout/AppAsideForTab2.vue

@@ -1,39 +1,34 @@
 <template>
   <el-scrollbar v-if="showTabs">
-    <!-- 菜单 -->
-    <el-menu :collapse="isCollapse" router unique-opened :default-active="activeMenuItem.index">
-      <!-- 根据 activeTab 动态渲染菜单项 -->
-      <template v-if="activeTab === 'parameterConfig'">
-        <el-menu-item index="/ModelSelection" @click="handleMenuClick('/ModelSelection')">模型选择</el-menu-item>
-        <el-menu-item index="/thres" @click="handleMenuClick('/thres')">阈值选择</el-menu-item>
-        <el-menu-item index="/ModelTrain" @click="handleMenuClick('/ModelTrain')">模型训练</el-menu-item>
-      </template>
-      <template v-else-if="activeTab === 'dataManagement'">
-        <el-menu-item index="/Visualizatio" @click="handleMenuClick('/Visualizatio')">降酸数据管理</el-menu-item>
-        <el-menu-item index="/Visualization" @click="handleMenuClick('/Visualization')">反酸数据管理</el-menu-item>
-        <el-menu-item index="/AdminRegionData" @click="handleMenuClick('/AdminRegionData')">行政区域数据</el-menu-item>
-        <el-menu-item index="/SoilAssessmentUnitData" @click="handleMenuClick('/SoilAssessmentUnitData')">土壤评估单元格数据</el-menu-item>
-        <el-menu-item index="/SoilHeavyMetalData" @click="handleMenuClick('/SoilHeavyMetalData')">土壤重金属采集数据</el-menu-item>
-        <el-menu-item index="/CropHeavyMetalData" @click="handleMenuClick('/CropHeavyMetalData')">农作物重金属采集样数据</el-menu-item>
-        <el-menu-item index="/LandUseTypeData" @click="handleMenuClick('/LandUseTypeData')">用地类型数据</el-menu-item>
-        <el-menu-item index="/SoilAcidificationData" @click="handleMenuClick('/SoilAcidificationData')">土壤酸化采样数据</el-menu-item>
-        <el-menu-item index="/ClimateInfoData" @click="handleMenuClick('/ClimateInfoData')">气候信息数据</el-menu-item>
-        <el-menu-item index="/GeographicEnvInfoData" @click="handleMenuClick('/GeographicEnvInfoData')">地理环境信息</el-menu-item>
-      </template>
-      <template v-else-if="activeTab === 'infoManagement'">
-        <el-menu-item index="/IntroductionUpdate" @click="handleMenuClick('/IntroductionUpdate')">介绍信息管理</el-menu-item>
-      </template>
-      <template v-else-if="activeTab === 'modelManagement'">
-        <el-menu-item index="/CadmiumPredictionModel" @click="handleMenuClick('/CadmiumPredictionModel')">土壤镉含量预测模型</el-menu-item>
-        <el-menu-item index="/EffectiveCadmiumModel" @click="handleMenuClick('/EffectiveCadmiumModel')">土壤有效态镉预测模型</el-menu-item>
-        <el-menu-item index="/RiceRiskModel" @click="handleMenuClick('/RiceRiskModel')">水稻镉污染风险模型</el-menu-item>
-        <el-menu-item index="/AcidReductionModel" @click="handleMenuClick('/AcidReductionModel')">反酸及降酸模型</el-menu-item>
-        <el-menu-item index="/WheatRiskModel" @click="handleMenuClick('/WheatRiskModel')">小麦镉污染风险模型</el-menu-item>
-        <el-menu-item index="/VegetableRiskModel" @click="handleMenuClick('/VegetableRiskModel')">蔬菜镉污染风险模型</el-menu-item>
-      </template>
-      <template v-else-if="activeTab === 'userManagement'">
-        <el-menu-item index="/UserRegistration" @click="handleMenuClick('/UserRegistration')">普通用户</el-menu-item>
-        <el-menu-item index="/UserManagement" @click="handleMenuClick('/UserManagement')">用户信息</el-menu-item>
+    <el-menu
+      :collapse="isCollapse"
+      router
+      unique-opened
+      :default-active="activeMenuItem.index"
+      :default-openeds="openKeys"
+      @select="handleMenuClick"
+    >
+      <template v-if="menuList.length">
+        <template v-for="item in menuList" :key="item.index">
+          <el-sub-menu v-if="item.children && item.children.length" :index="item.index">
+            <template #title>
+              <el-icon v-if="item.icon"><component :is="item.icon" /></el-icon>
+              <span>{{ item.label }}</span>
+            </template>
+            <el-menu-item
+              v-for="child in item.children"
+              :key="child.index"
+              :index="child.index"
+            >
+              <el-icon v-if="child.icon"><component :is="child.icon" /></el-icon>
+              <span>{{ child.label }}</span>
+            </el-menu-item>
+          </el-sub-menu>
+          <el-menu-item v-else :index="item.index">
+            <el-icon v-if="item.icon"><component :is="item.icon" /></el-icon>
+            <span>{{ item.label }}</span>
+          </el-menu-item>
+        </template>
       </template>
       <template v-else>
         <el-menu-item disabled>未知的 Tab:{{ activeTab }}</el-menu-item>
@@ -43,110 +38,105 @@
 </template>
 
 <script setup lang="ts">
-import { inject, reactive, watch, toRefs } from "vue";
+import { inject, reactive, watch, toRefs, computed } from "vue";
+import { useRouter, useRoute, type RouteLocationAsPathGeneric, type RouteLocationAsRelativeGeneric } from "vue-router";
 import { ElMessage } from "element-plus";
-import { useRouter } from "vue-router";
+import { tabMenuMap } from "./menuItems2";
 
-// 接收来自父组件的 activeTab 和 showTabs 属性
 const props = defineProps({
   activeTab: {
-    type: String, // 确保 activeTab 是字符串
+    type: String,
     required: true,
-    default: "introduction", // 默认值为 "introduction"
+    default: "introduction",
   },
   showTabs: {
-    type: Boolean, // 确保 showTabs 是布尔值
+    type: Boolean,
     required: true,
-    default: true, // 默认值为 true
+    default: true,
   },
 });
 
-// 将 activeTab 转为响应式引用
 const { activeTab } = toRefs(props);
+const isCollapse = inject("isCollapse", false);
+const router = useRouter();
+const route = useRoute();
 
-// 注入 isCollapse 值,用于控制侧边栏的缩放
-const isCollapse = inject("isCollapse", false); // 默认值为 false,确保注入失败时有默认值
-
-// 新增用于高亮的状态
-const activeMenuItem = reactive({ index: "" });
-
-// 日志记录函数
-function logAction(action: string) {
-  console.log(`[AppAsideForTab2] ${action}`);
-}
+const activeMenuItem = reactive({ index: route.path });
+const openKeys = reactive<string[]>([]);
 
-// 错误提示函数
-function showError(message: string) {
-  ElMessage.error(message);
-}
-
-const router = useRouter();
+const menuList = computed(() => {
+  return tabMenuMap[activeTab.value] || [];
+});
 
-// 在菜单项点击时记录日志并导航
-async function handleMenuClick(index: string) {
-  try {
-    logAction(`Menu item clicked: ${index}`);
-    await router.push(index); // 确保导航到对应的路由
-    activeMenuItem.index = index; // 更新高亮状态
-  } catch (error) {
-    console.error(`[AppAsideForTab2] Error navigating to ${index}:`, error);
-    showError("导航失败,请检查您的网络连接或联系管理员。");
+// 查找当前路由对应的父级菜单index,用于默认展开子菜单
+function findOpenKeys(menuItems: any[], currentPath: string, parents: string[] = []): string[] {
+  for (const item of menuItems) {
+    if (item.index === currentPath) {
+      return parents;
+    } else if (item.children) {
+      const found = findOpenKeys(item.children, currentPath, [...parents, item.index]);
+      if (found.length) return found;
+    }
   }
+  return [];
 }
 
-// 调试日志:记录 activeTab 和 isCollapse 的值
 watch(
-  activeTab, // 使用响应式引用
-  (newVal) => {
-    console.log(`[AppAside] activeTab changed to: ${newVal}`);
-    // 默认高亮第一个菜单项
-    if (newVal === "parameterConfig") {
-      activeMenuItem.index = "/ModelSelection";
-    } else if (newVal === "dataManagement") {
-      activeMenuItem.index = "/Visualizatio";
-    } else if (newVal === "infoManagement") {
-      activeMenuItem.index = "/IntroductionUpdate";
-    } else if (newVal === "modelManagement") {
-      activeMenuItem.index = "/CadmiumPredictionModel";
-    } else if (newVal === "userManagement") {
-      activeMenuItem.index = "/UserRegistration";
-    }
+  () => route.path,
+  (newPath) => {
+    activeMenuItem.index = newPath;
+    openKeys.splice(0, openKeys.length, ...findOpenKeys(menuList.value, newPath));
   },
   { immediate: true }
 );
 
 watch(
-  () => isCollapse,
+  activeTab,
   (newVal) => {
-    console.log(`[AppAside] isCollapse changed to: ${newVal}`);
+    const first = tabMenuMap[newVal]?.[0];
+    if (first) activeMenuItem.index = first.index;
+    else activeMenuItem.index = "";
+    openKeys.splice(0, openKeys.length, ...findOpenKeys(menuList.value, activeMenuItem.index));
   },
   { immediate: true }
 );
+
+async function handleMenuClick(index: string | RouteLocationAsRelativeGeneric | RouteLocationAsPathGeneric) {
+  try {
+    if (index === route.path) return;
+    await router.push(index);
+    activeMenuItem.index = index as string;
+  } catch (error) {
+    console.error(`[AppAsideForTab2] 路由跳转失败: ${index}`, error);
+    ElMessage.error("导航失败,请检查网络连接或联系管理员。");
+  }
+}
 </script>
 
 <style scoped>
 .el-menu {
-  background-color: #1f2937; /* 深色背景 */
-  border-right: none; /* 移除边框 */
+  background-color: #ffffff;
+  border-right: none;
+  margin-top: 10px;
 }
 
 .el-menu-item {
-  color: #d1d5db; /* 浅灰色字体 */
-  font-size: 14px; /* 调整字体大小 */
-  padding: 10px 20px; /* 增加内边距 */
-  border-radius: 6px; /* 添加圆角 */
-  transition: background-color 0.3s ease, color 0.3s ease; /* 平滑过渡 */
+  color: #000000;
+  font-size: 14px;
+  padding: 10px 20px;
+  border-radius: 6px;
+  transition: background-color 0.3s ease, color 0.3s ease;
 }
 
 .el-menu-item:hover {
-  background-color: #374151; /* 悬停时背景色 */
-  color: #60a5fa; /* 悬停时字体颜色 */
+  background-color: #f3f4f6;
+  color: #2563eb;
 }
 
 .el-menu-item.is-active {
-  background-color: #2563eb; /* 激活状态背景色 */
-  color: #ffffff; /* 激活状态字体颜色 */
-  font-weight: bold; /* 激活状态加粗字体 */
-  box-shadow: inset 0 0 10px rgba(37, 99, 235, 0.5); /* 添加内阴影 */
+  background-color: #2563eb;
+  color: #ffffff;
+  font-weight: bold;
+  box-shadow: inset 0 0 10px rgba(37, 99, 235, 0.5);
 }
-</style>
+</style>

+ 156 - 218
src/components/layout/AppLayout.vue

@@ -9,98 +9,79 @@ const router = useRouter();
 const route = useRoute();
 const tokenStore = useTokenStore();
 
-// 新增:判断当前路由是否全屏模式(无头部和侧边栏)
+// 是否为全屏页面
 const isFullScreen = computed(() => route.meta.fullScreen === true);
 
-// 根据用户类型动态定义 Tab 数据
+// Tab 配置
 const tabs = computed(() => {
   if (tokenStore.token.loginType === "admin") {
     return [
-      { name: "parameterConfig", label: "参数配置", routes: ["/ModelSelection", "/thres", "/ModelTrain"] },
-      { name: "dataManagement", label: "数据管理", routes: ["/Visualization", "/Visualizatio", "/AdminRegionData", "/SoilAssessmentUnitData", "/SoilHeavyMetalData", "/CropHeavyMetalData", "/LandUseTypeData", "/SoilAcidificationData", "/ClimateInfoData", "/GeographicEnvInfoData"] },
-      { name: "infoManagement", label: "信息管理", routes: ["/IntroductionUpdate"] },
-      { name: "modelManagement", label: "模型管理及配置", routes: ["/Admin/CadmiumPredictionModel", "/Admin/EffectiveCadmiumModel", "/Admin/RiceRiskModel", "/Admin/AcidReductionModel", "/Admin/WheatRiskModel", "/Admin/VegetableRiskModel"] },
-      { name: "userManagement", label: "用户管理", routes: ["/Admin/UserRegistration", "/Admin/UserManagement"] },
+      { name: "dataManagement", label: "数据管理", icon: "el-icon-folder", routes: ["/Visualizatio","/Visualization", "/AdminRegionData", "/SoilAssessmentUnitData", "/SoilHeavyMetalData", "/CropHeavyMetalData", "/LandUseTypeData", "/SoilAcidificationData", "/ClimateInfoData", "/GeographicEnvInfoData"] },
+      { 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"] },
+      { name: "userManagement", label: "用户管理", icon: "el-icon-user", routes: ["/UserManagement", "/UserRegistration"] },
     ];
   } else {
     return [
-      { name: "shuJuKanBan", label: "数据看板", routes: ["/shuJuKanBan"] },
-      { name: "introduction", label: "软件简介", routes: ["/SoilPro", "/Overview", "/ResearchFindings", "/Unit"] },
-      { name: "heavyMetalFluxCalculation", label: "重金属输入输出通量计算", routes: ["/irrigationWater", "/agriculturalProductInput", "/atmosphericDryWetDeposition", "/surfaceRunoff", "/cropRemoval", "/subsurfaceFlow"] },
-      { name: "mapView", label: "地图展示", routes: ["/mapView"] },
-      { name: "cadmiumPrediction", label: "土壤镉含量预测", routes: ["/TotalCadmiumPrediction", "/EffectiveCadmiumPrediction"] },
-      { name: "cropRiskAssessment", label: "作物安全生产风险阈值评估", routes: ["/cropRiskAssessment"] },
-      { name: "farmlandQualityAssessment", label: "耕地质量评估", routes: ["/farmlandQualityAssessment"] },
-      { name: "soilAcidificationPrediction", label: "土壤酸化预测", routes: ["/Calculation", "/AcidNeutralizationModel"] },
-      { name: "scenarioSimulation", label: "情景模拟", routes: ["/TraditionalFarmingRisk", "/HeavyMetalCadmiumControl", "/SoilAcidificationControl"] },
-      { name: "dataStatistics", label: "数据统计报表", routes: ["/DetectionStatistics", "/FarmlandPollutionStatistics", "/PlantingRiskStatistics"] },
+      { name: "shuJuKanBan", label: "数据看板", icon: "el-icon-data-analysis", routes: ["/shuJuKanBan"] },
+      { name: "introduction", label: "软件简介", icon: "el-icon-info-filled", routes: ["/SoilPro", "/Overview", "/ResearchFindings", "/Unit"] },
+      { name: "heavyMetalFluxCalculation", label: "重金属输入输出通量", icon: "el-icon-refresh", routes: ["/irrigationWater", "/agriculturalProductInput", "/atmosphericDryWetDeposition", "/surfaceRunoff", "/cropRemoval", "/subsurfaceFlow"] },
+      { name: "mapView", label: "地图展示", icon: "el-icon-map-location", routes: ["/mapView"] },
+      { name: "cadmiumPrediction", label: "土壤镉预测", icon: "el-icon-c-scale-to-original", routes: ["/TotalCadmiumPrediction", "/EffectiveCadmiumPrediction"] },
+      { name: "cropRiskAssessment", label: "作物风险评估", icon: "el-icon-warning", routes: ["/cropRiskAssessment"] },
+      { name: "farmlandQualityAssessment", label: "耕地质量评估", icon: "el-icon-rank", routes: ["/farmlandQualityAssessment"] },
+      { name: "soilAcidificationPrediction", label: "土壤酸化预测", icon: "el-icon-magic-stick", routes: ["/Calculation", "/AcidNeutralizationModel"] },
+      { name: "scenarioSimulation", label: "情景模拟", icon: "el-icon-s-operation", routes: ["/TraditionalFarmingRisk", "/HeavyMetalCadmiumControl", "/SoilAcidificationControl"] },
+      { name: "dataStatistics", label: "数据统计", icon: "el-icon-pie-chart", routes: ["/DetectionStatistics", "/FarmlandPollutionStatistics", "/PlantingRiskStatistics"] },
     ];
   }
 });
 
-// 当前激活的 Tab
-const activeName = ref(tabs.value[0]?.name || ""); // 确保默认激活的是第一个 Tab
-
-// 控制是否显示 Tabs
+// 当前激活 tab
+const activeName = ref(tabs.value[0]?.name || "");
 const showTabs = computed(() => tabs.value.length > 1);
-
-// 控制 Tabs 样式
-const tabStyle = computed(() => {
-  return tabs.value.length === 1 ? { width: "100%", justifyContent: "center" } : {};
-});
-
-// 用于控制首次是否跳转
+const tabStyle = computed(() => tabs.value.length === 1 ? { width: "100%", justifyContent: "center" } : {});
 let hasNavigated = false;
 
-// 监听 activeName 的变化,加载对应数据,且首次不自动跳转
-watch(
-  () => activeName.value,
-  (newTab) => {
-    const tab = tabs.value.find(t => t.name === newTab);
-    const targetPath = tab?.routes?.[0];
-
-    if (!hasNavigated) {
-      hasNavigated = true;
-      return;
-    }
-
-    if (tab && targetPath && router.currentRoute.value.path !== targetPath) {
-      console.log("跳转目标:", targetPath);
-      router.push({ path: targetPath });
-    }
-  },
-  { immediate: true }
-);
+watch(() => activeName.value, (newTab) => {
+  const tab = tabs.value.find(t => t.name === newTab);
+  const targetPath = tab?.routes?.[0];
+  if (!hasNavigated) {
+    hasNavigated = true;
+    return;
+  }
+  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 AsideComponent = computed(() => {
   if (["parameterConfig", "dataManagement", "infoManagement", "modelManagement", "userManagement"].includes(activeName.value)) {
     return defineAsyncComponent(() => import("./AppAsideForTab2.vue"));
-  } else if (["introduction", "acidModel", "neutralizationModel", "mapView", "cadmiumPrediction", "cropRiskAssessment", "farmlandQualityAssessment", "soilAcidificationPrediction", "scenarioSimulation", "dataStatistics", "heavyMetalFluxCalculation"].includes(activeName.value)) {
+  } else {
     return defineAsyncComponent(() => import("./AppAside.vue"));
   }
-  return null;
 });
 
-// 控制侧边栏显示(全屏时隐藏)
+// 是否显示侧边栏
 const showAside = computed(() => {
-  return !isFullScreen.value && activeName.value !== "mapView" && activeName.value !== "shuJuKanBan" && activeName.value !== "cropRiskAssessment" && activeName.value !== "farmlandQualityAssessment"; // 数据看板不显示侧边栏
+  return (
+    !isFullScreen.value &&
+    activeName.value !== "mapView" &&
+    activeName.value !== "shuJuKanBan" &&
+    activeName.value !== "cropRiskAssessment" &&
+    activeName.value !== "farmlandQualityAssessment"
+  );
 });
 
-// 固定选中侧边栏选项
-const activeAsideTab = ref(activeName.value || "shuJuKanBan");
-
-// 用户信息
-const userInfo = reactive({
-  name: tokenStore.token.name || "未登录",
-});
+const activeAsideTab = ref(activeName.value || "");
+const userInfo = reactive({ name: tokenStore.token.name || "未登录" });
 
-// 处理退出逻辑
 const handleLogout = async () => {
   try {
     await ElMessageBox.confirm("确定要退出登录吗?", "提示", {
@@ -112,54 +93,36 @@ const handleLogout = async () => {
     tokenStore.clearToken();
     ElMessage.success("退出成功");
     router.push("/login");
-  } catch (error) {
+  } catch {
     ElMessage.info("已取消退出");
   }
 };
 
-// 添加全局错误处理逻辑(如果需要)
-watch(
-  () => route,
-  (newRoute) => {
-    console.log(`[AppLayout] Route changed to: ${newRoute.path}`);
-  },
-  { immediate: true }
-);
+const mainPaddingStyle = computed(() => {
+  return {
+    padding: ["mapView", "infoManagement"].includes(activeName.value) ? "0" : "20px",
+  };
+});
+
+const scrollbarStyle = computed(() => {
+  return isFullScreen.value ? { height: "100vh" } : { height: "calc(100vh - 128px)" };
+});
 </script>
 
 <template>
   <div class="common-layout" :class="{ 'full-screen': isFullScreen }">
-    <el-container style="height: 100vh">
-      <!-- 只在非全屏模式显示顶部导航栏 -->
-      <el-header v-if="!isFullScreen" class="header">
-        <div class="header-content">
-          <!-- 添加 logo 图片 -->
-          <div class="logo-container">
-            <img src="@/assets/logo.png" alt="Logo 1" class="logo" />
-          </div>
-          <span class="project-name">酸性土壤精准治酸智能专家系统</span>
-          <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"
-              :label="tab.label"
-              :name="tab.name"
-            />
-          </el-tabs>
-          <div v-else class="single-tab" @click="handleClick(tabs[0], $event)">
-            {{ tabs[0]?.label }}
-          </div>
-          <el-dropdown>
+    <el-container class="layout-container">
+      <!-- 顶部 Header -->
+      <el-header class="layout-header" v-if="!isFullScreen">
+        <div class="logo-title-row">
+          <img src="@/assets/logo.png" alt="Logo" class="logo" />
+          <span class="project-name">区域土壤重金属污染风险评估</span>
+        </div>
+        <el-dropdown>
             <span class="el-dropdown-link">
               <el-avatar
                 :size="40"
-                :src="'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'"
+                src="https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png"
               />
             </span>
             <template #dropdown>
@@ -169,173 +132,148 @@ watch(
               </el-dropdown-menu>
             </template>
           </el-dropdown>
-        </div>
       </el-header>
 
-      <el-container>
-        <!-- 非全屏且满足条件时显示侧边栏 -->
-        <el-aside
-          v-if="!isFullScreen && showAside && showTabs"
-          :width="'200px'"
-          class="aside"
-        >
-          <component
-            :is="AsideComponent"
-            :activeTab="activeName"
-            :showTabs="showTabs"
-          />
+      <!-- Tab 区域 -->
+      <div class="tabs-row" v-if="!isFullScreen">
+        <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>
+              <span class="tab-label-text">{{ tab.label }}</span>
+            </template>
+          </el-tab-pane>
+        </el-tabs>
+        <div v-else class="single-tab" @click="handleClick(tabs[0], $event)">
+          <i :class="['tab-icon', tabs[0].icon]"></i>
+          <span class="tab-label-text">{{ tabs[0].label }}</span>
+        </div>
+      </div>
+
+      <!-- 主体区域 -->
+      <el-container class="layout-main-container">
+        <el-aside v-if="showAside && showTabs" class="layout-aside">
+          <component :is="AsideComponent" :activeTab="activeName" :showTabs="showTabs" />
         </el-aside>
 
-        <el-container class="header-and-main" :style="isFullScreen ? { height: '100vh' } : {}">
-          <el-main
-            :style="isFullScreen ? { padding: 0 } : { padding: activeName === 'mapView' || activeName === 'infoManagement' ? '0' : '20px', backgroundColor: '#ecf0f1' }"
-          >
-            <el-scrollbar :style="isFullScreen ? { height: '100vh' } : { height: 'auto' }">
-              <RouterView />
-            </el-scrollbar>
-          </el-main>
-        </el-container>
+        <el-main class="layout-content-wrapper" :style="mainPaddingStyle">
+          <el-scrollbar :style="scrollbarStyle">
+            <RouterView />
+          </el-scrollbar>
+        </el-main>
       </el-container>
     </el-container>
   </div>
 </template>
 
-<style scoped>
-.common-layout {
-  display: flex;
-  height: 100%;
-  font-family: 'Roboto', sans-serif;
-  background: linear-gradient(135deg, #f3f4f6, #e5e7eb);
-}
-
-/* 新增:全屏布局,移除间距,充满视图 */
-.full-screen {
+<style>
+.layout-container {
   height: 100vh;
-  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+  background-color: #f5f7fa;
 }
 
-.full-screen .el-main,
-.full-screen .el-scrollbar {
-  height: 100vh !important;
-  padding: 0 !important;
-  background-color: transparent !important;
+.layout-header {
+  height: 160px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0 50px;
+  background: linear-gradient(to bottom, #6c8598cc 0%, #5b7083cc 100%); /* 更浅色调 */
+  backdrop-filter: blur(12px);
+  -webkit-backdrop-filter: blur(12px);
+  border-bottom: none;
+  color: #f0f3f7;
 }
 
-.header {
+.tabs-row {
   display: flex;
-  justify-content: space-between;
+  justify-content: center;
   align-items: center;
-  padding: 15px 30px;
-  background-color: #ffffff;
-  border-bottom: 1px solid #e5e7eb;
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
-  border-radius: 10px;
+  padding: 5px 24px;
+  background: linear-gradient(to bottom, #5b7083cc 0%, #4a5e70cc 100%); /* 更浅色调 */
+  backdrop-filter: blur(10px);
+  -webkit-backdrop-filter: blur(10px);
+  border-bottom: 1px solid rgba(255, 255, 255, 0.05);
 }
 
-.header-content {
+.logo-title-row {
   display: flex;
   align-items: center;
-  justify-content: space-between;
-  width: 100%;
+  gap: 24px;
+}
+
+.logo {
+  height: 80px;
 }
 
 .project-name {
-  font-size: 1.2rem;
+  font-size: 72px;
   font-weight: bold;
-  color: #1f2937;
-  margin-right: 20px;
+  color: #f0f3f7;
 }
 
 .demo-tabs {
-  flex-grow: 1;
-  margin-right: 20px;
+  height: 64px;
   display: flex;
   align-items: center;
-  background-color: #f9fafb;
-  border-radius: 8px;
-  padding: 5px;
-  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
-}
-
-.demo-tabs > .el-tabs__header {
-  border-bottom: none;
 }
 
-.demo-tabs > .el-tabs__item {
-  color: #374151;
-  font-size: 14px;
-  font-weight: 500;
-  padding: 8px 16px;
-  border-radius: 6px;
-  transition: background-color 0.3s ease, color 0.3s ease;
+.el-tabs__item {
+  font-size: 40px;
+  font-weight: 600;
+  padding: 25px 36px !important;
+  margin: 5px;
+  color: #cfd8dc; /* 默认浅灰文字 */
+  display: flex;
+  align-items: center;
+  border-radius: 10px;
+  transition: all 0.2s ease-in-out;
 }
 
-.demo-tabs > .el-tabs__item:hover {
-  background-color: #e5e7eb;
-  color: #2563eb;
+.el-tabs__item:hover {
+  background-color: #455a64; /* 悬停深色背景 */
+  color: #ffffff; /* 悬停白色文字 */
 }
 
-.demo-tabs > .el-tabs__item.is-active {
-  background-color: #2563eb;
-  color: #ffffff;
-  font-weight: bold;
-  box-shadow: inset 0 0 6px rgba(37, 99, 235, 0.5);
+.el-tabs__item.is-active {
+  background-color: #1abc9c; /* 激活亮色背景 */
+  color: #ffffff; /* 激活白色文字 */
+  box-shadow: 0 4px 16px rgba(26, 188, 156, 0.4);
+  font-weight: 700;
 }
 
-.single-tab {
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  width: 100%;
-  height: 40px;
-  background-color: #2563eb;
-  color: #ffffff;
-  font-size: 16px;
-  font-weight: bold;
-  border-radius: 8px;
-  cursor: pointer;
-  transition: background-color 0.3s ease;
-  user-select: none;
+.tab-icon {
+  font-size: 36px; /* 原 20px */
+  margin-right: 8px;
+  color: inherit; /* 跟随文字颜色 */
 }
 
-.single-tab:hover {
-  background-color: #1d4ed8;
+.tab-label-text {
+  font-size: 20px;
+  color: inherit;
 }
 
-.aside {
-  padding: 0;
-  background-color: #f9fafb;
-  border-radius: 10px;
-  margin-left: 10px;
-  box-shadow: 0 0 10px rgb(32 33 36 / 8%);
-}
-
-.header-and-main {
-  border-radius: 10px;
-  background-color: #ffffff;
-  margin-left: 10px;
-  height: 100%;
-  overflow: hidden;
+.layout-main-container {
+  flex: 1;
   display: flex;
-  flex-direction: column;
+  overflow: hidden;
+  min-height: 0;
 }
 
-.el-main {
+.layout-aside {
+  width: 200px;
+  background-color: #fff;
+  border-right: 1px solid #dcdfe6;
   overflow-y: auto;
 }
 
-.el-scrollbar {
-  height: 100%;
-}
-
-.logo-container {
+.layout-content-wrapper {
+  flex: 1;
   display: flex;
-  align-items: center;
-  gap: 10px;
-}
-
-.logo {
-  height: 40px;
-  width: auto;
+  flex-direction: column;
+  background-color: #fff;
+  overflow: hidden;
 }
 </style>

+ 221 - 0
src/components/layout/menuItems.ts

@@ -0,0 +1,221 @@
+// src/config/menuItems.ts
+import {
+    Menu as MenuIcon,
+    Monitor,
+    InfoFilled,
+    DataLine,
+    Histogram,
+    Cloudy,
+    Watermelon,
+    WindPower,
+    Sunny,
+    List,
+    Location,
+    PieChart,
+    Compass,
+    Collection,
+    MagicStick,
+    HelpFilled,
+    Coin
+  } from '@element-plus/icons-vue';
+  
+  export interface MenuItem {
+    index: string;
+    label: string;
+    icon?: any;
+    tab: string;
+    permission?: string;
+    children?: MenuItem[];
+  }
+  
+  export const menuItems: MenuItem[] = [
+    {
+      index: '/shuJuKanBan',
+      label: '数据看板',
+      icon: Monitor,
+      tab: 'shuJuKanBan'
+    },
+    {
+      index: '/SoilPro',
+      label: '软件简介',
+      icon: InfoFilled,
+      tab: 'introduction'
+    },
+    {
+      index: '/Overview',
+      label: '项目简介',
+      icon: Collection,
+      tab: 'introduction'
+    },
+    {
+      index: '/ResearchFindings',
+      label: '研究成果',
+      icon: Histogram,
+      tab: 'introduction'
+    },
+    {
+      index: '/Unit',
+      label: '团队信息',
+      icon: HelpFilled,
+      tab: 'introduction'
+    },
+    {
+      index: 'inputFlux',
+      label: '输入通量计算',
+      icon: Watermelon,
+      tab: 'heavyMetalFluxCalculation',
+      children: [
+        {
+          index: '/irrigationWater',
+          label: '灌溉水',
+          icon: Sunny,
+          tab: 'heavyMetalFluxCalculation'
+        },
+        {
+          index: '/agriculturalProductInput',
+          label: '农产品投入',
+          icon: Coin,
+          tab: 'heavyMetalFluxCalculation'
+        },
+        {
+          index: '/atmosphericDryWetDeposition',
+          label: '大气干湿沉降',
+          icon: Cloudy,
+          tab: 'heavyMetalFluxCalculation'
+        }
+      ]
+    },
+    {
+      index: 'outputFlux',
+      label: '输出通量计算',
+      icon: WindPower,
+      tab: 'heavyMetalFluxCalculation',
+      children: [
+        {
+          index: '/surfaceRunoff',
+          label: '地表径流',
+          icon: Watermelon,
+          tab: 'heavyMetalFluxCalculation'
+        },
+        {
+          index: '/cropRemoval',
+          label: '农作物移除',
+          icon: List,
+          tab: 'heavyMetalFluxCalculation'
+        },
+        {
+          index: '/subsurfaceFlow',
+          label: '地下渗流',
+          icon: DataLine,
+          tab: 'heavyMetalFluxCalculation'
+        }
+      ]
+    },
+    {
+      index: '/mapView',
+      label: '地图展示',
+      icon: Location,
+      tab: 'mapView'
+    },
+    {
+      index: '/TotalCadmiumPrediction',
+      label: '土壤镉的总含量预测',
+      icon: PieChart,
+      tab: 'cadmiumPrediction'
+    },
+    {
+      index: '/EffectiveCadmiumPrediction',
+      label: '土壤镉有效态含量预测',
+      icon: PieChart,
+      tab: 'cadmiumPrediction'
+    },
+    {
+      index: '/cropRiskAssessment',
+      label: '水稻镉污染风险',
+      icon: Compass,
+      tab: 'cropRiskAssessment'
+    },
+    {
+      index: '/farmlandQualityAssessment',
+      label: '韶关',
+      icon: DataLine,
+      tab: 'farmlandQualityAssessment'
+    },
+    {
+      index: '/acidModel',
+      label: '土壤反酸',
+      icon: MagicStick,
+      tab: 'soilAcidificationPrediction',
+      children: [
+        {
+          index: '/Calculation',
+          label: '土壤反酸预测',
+          icon: Sunny,
+          tab: 'heavyMetalFluxCalculation'
+        },
+        {
+          index: '/SoilAcidReductionIterativeEvolution',
+          label: '反酸模型迭代可视化',
+          icon: Coin,
+          tab: 'heavyMetalFluxCalculation'
+        }
+      ]
+    },
+    {
+      index: '/neutralizationModel',
+      label: '土壤降酸',
+      icon: MagicStick,
+      tab: 'soilAcidificationPrediction',
+      children: [
+        {
+          index: '/AcidNeutralizationModel',
+          label: '土壤降酸预测',
+          icon: Sunny,
+          tab: 'heavyMetalFluxCalculation'
+        },
+        {
+          index: '/SoilAcidificationIterativeEvolution',
+          label: '土壤降酸预测',
+          icon: Coin,
+          tab: 'heavyMetalFluxCalculation'
+        }
+      ]
+    },
+    {
+      index: '/TraditionalFarmingRisk',
+      label: '传统耕种习惯风险趋势',
+      icon: MenuIcon,
+      tab: 'scenarioSimulation'
+    },
+    {
+      index: '/HeavyMetalCadmiumControl',
+      label: '重金属镉污染治理',
+      icon: MenuIcon,
+      tab: 'scenarioSimulation'
+    },
+    {
+      index: '/SoilAcidificationControl',
+      label: '土壤酸化治理',
+      icon: MenuIcon,
+      tab: 'scenarioSimulation'
+    },
+    {
+      index: '/DetectionStatistics',
+      label: '检测信息统计',
+      icon: List,
+      tab: 'dataStatistics'
+    },
+    {
+      index: '/FarmlandPollutionStatistics',
+      label: '耕地污染信息统计',
+      icon: List,
+      tab: 'dataStatistics'
+    },
+    {
+      index: '/PlantingRiskStatistics',
+      label: '种植风险信息统计',
+      icon: List,
+      tab: 'dataStatistics'
+    }
+  ];
+  

+ 86 - 0
src/components/layout/menuItems2.ts

@@ -0,0 +1,86 @@
+import {
+    Setting,
+    Menu,
+    Folder,
+    Document,
+    User,
+    Grid,
+    Reading,
+    Location,
+    TrendCharts,
+    Collection,
+    Postcard,
+    Star,
+    Warning,
+  } from "@element-plus/icons-vue";
+  
+  export const tabMenuMap: Record<string, any[]> = {
+    dataManagement: [
+      { index: "/Visualizatio", label: "降酸数据管理", icon: Document },
+      { index: "/Visualization", label: "反酸数据管理", icon: Document },
+      { index: "/AdminRegionData", label: "行政区域数据", icon: Location },
+      {
+        index: "/SoilAssessmentUnitData",
+        label: "土壤评估单元格数据",
+        icon: Grid,
+      },
+      {
+        index: "/SoilHeavyMetalData",
+        label: "土壤重金属采集数据",
+        icon: Collection,
+      },
+      {
+        index: "/CropHeavyMetalData",
+        label: "农作物重金属采集样数据",
+        icon: Postcard,
+      },
+      { index: "/LandUseTypeData", label: "用地类型数据", icon: Menu },
+      {
+        index: "/SoilAcidificationData",
+        label: "土壤酸化采样数据",
+        icon: Warning,
+      },
+      { index: "/ClimateInfoData", label: "气候信息数据", icon: TrendCharts },
+      { index: "/GeographicEnvInfoData", label: "地理环境信息", icon: Reading },
+    ],
+    infoManagement: [
+      { index: "/IntroductionUpdate", label: "介绍信息管理", icon: Document },
+    ],
+    modelManagement: [
+      {
+        index: "/CadmiumPredictionModel",
+        label: "土壤镉含量预测模型",
+        icon: Star,
+      },
+      {
+        index: "/AcidReductionModel",
+        label: "反酸及降酸模型",
+        icon: Star,
+        children: [
+          {
+            index: '/ModelSelection',
+            label: '模型选择',
+            icon: Setting
+          },
+          {
+            index: '/thres',
+            label: '阈值选择',
+            icon: Menu
+          },
+          {
+            index: '/ModelTrain',
+            label: '模型训练',
+            icon: Folder,
+          }
+        ]
+      },      
+      { index: "/RiceRiskModel", label: "水稻镉污染风险模型", icon: Star },
+      { index: "/WheatRiskModel", label: "小麦镉污染风险模型", icon: Star },
+      { index: "/VegetableRiskModel", label: "蔬菜镉污染风险模型", icon: Star },
+    ],
+    userManagement: [
+      { index: "/UserManagement", label: "用户信息", icon: User },
+      { index: "/UserRegistration", label: "普通用户", icon: User },
+    ],
+  };
+  

+ 10 - 19
src/router/index.ts

@@ -7,15 +7,14 @@ const routes = [
   {
     path: "/login",
     name: "login",
-    component: () => import("@/views/menu/loginView.vue"), // 修复路径
+    component: () => import("@/views/login/loginView.vue"), // 修复路径
   },
   {
     path: "/",
     name: "home",
     component: AppLayout,
     meta: { requiresAuth: true, title: "模型" },
-    redirect: { name: "login" }, // 修改默认重定向为 loginView
-
+    redirect: { name: "login" }, // 修改默认重定向为 login
     children: [
       {
         path: "/:catchAll(.*)",
@@ -276,19 +275,19 @@ const routes = [
         path: "ModelSelection",
         name: "ModelSelection",
         component: () =>
-          import("@/views/Admin/parameterConfig/ModelSelection.vue"),
+          import("@/views/Admin/modelManagement/AcidReductionModel/ModelSelection.vue"),
         meta: { title: "模型选择" },
       },
       {
         path: "thres",
         name: "thres",
-        component: () => import("@/views/Admin/parameterConfig/thres.vue"),
+        component: () => import("@/views/Admin/modelManagement/AcidReductionModel/thres.vue"),
         meta: { title: "阈值选择" },
       },
       {
         path: "ModelTrain",
         name: "ModelTrain",
-        component: () => import("@/views/Admin/parameterConfig/ModelTrain.vue"),
+        component: () => import("@/views/Admin/modelManagement/AcidReductionModel/ModelTrain.vue"),
         meta: { title: "模型训练" },
       },
       {
@@ -326,13 +325,6 @@ const routes = [
           import("@/views/Admin/modelManagement/RiceRiskModel.vue"),
         meta: { title: "水稻镉污染风险模型" },
       },
-      {
-        path: "AcidReductionModel",
-        name: "AcidReductionModel",
-        component: () =>
-          import("@/views/Admin/modelManagement/AcidReductionModel.vue"),
-        meta: { title: "反酸及降酸模型" },
-      },
       {
         path: "WheatRiskModel",
         name: "WheatRiskModel",
@@ -415,21 +407,20 @@ const routes = [
   },
 ];
 
-// 创建 router 实例
 const router = createRouter({
   history: createWebHistory(import.meta.env.BASE_URL),
-  routes, // 使用路由配置数组
+  routes,
 });
 
-// 导航守卫
-// 导航守卫:只做鉴权,不做跳转
 router.beforeEach((to, from, next) => {
   const tokenStore = useTokenStore();
-  if (to.matched.some((r) => r.meta.requiresAuth)) {
+  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();
   }
 });
 
-export default router;
+export default router;

+ 0 - 23
src/views/Admin/modelManagement/AcidReductionModel.vue

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

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


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


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


+ 0 - 0
src/views/Admin/selectCityAndCounty.vue


+ 1 - 1
src/views/User/introduction/Introduce.vue

@@ -52,7 +52,7 @@ const processParagraph = (paragraph: string) => {
 onMounted(async () => {
   try {
     // 从后端获取介绍数据
-    const response = await axios.get(`https://127.0.0.1:5000/software-intro/${props.targetId}`);
+    const response = await axios.get(`https://soilgd.com:5000/software-intro/${props.targetId}`);
     const { title, intro } = response.data;
     // 保留 \r\n 换行符
     const processedIntro = intro.replace(/<p>/g, '').replace(/<\/p>/g, '\r\n').replace(/<br>/g, '');

+ 63 - 29
src/views/User/selectCityAndCounty.vue

@@ -18,28 +18,40 @@
 
     <h2 class="subtitle" v-if="selectedCity">请选择县或区</h2>
 
-    <div class="districts" v-if="selectedCity">
-      <div
-        class="district-card"
+   <!-- 1. 使用 el-checkbox-group 进行多选绑定 -->
+   <el-checkbox-group
+      v-if="selectedCity"
+      v-model="selectedDistricts"
+      class="districts"
+    >
+      <!-- 2. 每个 el-checkbox 包裹整个 卡片 -->
+      <el-checkbox
         v-for="district in districts[selectedCity]"
         :key="district.name"
-        @click="goToKanBan(district.name)"
+        :value="district.name"
+        class="district-checkbox"
       >
-        <!-- 左侧图标 -->
-        <img :src="district.image" class="district-image" />
-
-        <!-- 中间文字 -->
-        <div class="district-content">
-          <span class="district-name">{{ district.name }}</span>
-          <p class="district-desc">{{ district.description }}</p>
+        <div class="district-card">
+          <img :src="district.image" class="district-image" />
+          <div class="district-content">
+            <span class="district-name">{{ district.name }}</span>
+            <p class="district-desc">{{ district.description }}</p>
+          </div>
+          <el-icon class="arrow-icon">
+            <arrow-right />
+          </el-icon>
         </div>
+      </el-checkbox>
+    </el-checkbox-group>
 
-        <!-- 右侧箭头图标 -->
-        <el-icon class="arrow-icon">
-          <arrow-right />
-        </el-icon>
-      </div>
-    </div>
+    <el-button
+      type="primary"
+      class="go-button"
+      :disabled="selectedDistricts.length === 0"
+      @click="goToKanBan"
+    >
+      跳转到数据看板
+    </el-button>
   </div>
 </template>
 
@@ -56,6 +68,7 @@ export default {
     const router = useRouter();
 
     const selectedCity = ref("韶关");
+    const selectedDistricts = ref([]);
     const cities = [{ name: "韶关" }, { name: "河池" }, { name: "腾冲" }];
 
     const districts = {
@@ -83,7 +96,7 @@ export default {
         { name: "巴马瑶族自治县", description: "巴马长寿之乡,河池健康之地", image: "/images/巴马瑶族自治县.png" },
         { name: "都安瑶族自治县", description: "都安瑶族风情,河池山水画卷", image: "/images/都安瑶族自治县.jpg" },
         { name: "大化瑶族自治县", description: "大化瑶族文化,河池民族风采", image: "/images/大化瑶族自治县.jpg" }
-],
+      ],
       腾冲: [
         { name: "腾冲市区", description: "腾冲火山热海,云南旅游胜地", image: "/images/腾冲市区.jpg" },
         { name: "和顺古镇", description: "和顺古镇,腾冲文化名片", image: "/images/和顺古镇.jpg" },
@@ -93,7 +106,6 @@ export default {
         { name: "明光镇", description: "明光古村,腾冲历史遗迹", image: "/images/明光镇.jpg" },
         { name: "清水乡", description: "清水河畔,腾冲自然风光", image: "/images/清水乡.jpg" },
         { name: "猴桥镇", description: "猴桥边境风情,腾冲异域风光", image: "/images/猴桥镇.jpg" }
-
       ],
     };
 
@@ -103,17 +115,27 @@ export default {
 
     const selectCity = (cityName) => {
       selectedCity.value = cityName;
+      selectedDistricts.value = []; // 切换城市时清空所选项
     };
 
-    const goToKanBan = (districtName) => {
-      localStorage.setItem("selectedDistrict", districtName);
-      router.push({ name: "shuJuKanBan", query: { district: districtName } });
+    const goToKanBan = () => {
+      localStorage.setItem("selectedDistricts", JSON.stringify(selectedDistricts.value));
+      const userType = localStorage.getItem("userType"); // 获取用户类型
+      if (userType === "admin") {
+        router.push({ name: "parameterConfig" }); // 管理员跳转到 parameterConfig
+      } else {
+        router.push({
+          name: "shuJuKanBan", // 普通用户跳转到 shuJuKanBan
+          query: { districts: selectedDistricts.value.join(",") },
+        });
+      }
     };
 
     return {
       selectedCity,
       cities,
       districts,
+      selectedDistricts,
       selectCity,
       goToKanBan,
     };
@@ -129,7 +151,7 @@ export default {
 }
 
 .city-selection.full-page {
-  min-height: 100vh; /* 改成 min-height 使内容多时撑开 */
+  min-height: 100vh;
   display: flex;
   flex-direction: column;
   align-items: center;
@@ -182,12 +204,21 @@ export default {
   text-align: left;
 }
 
+/* 将 el-checkbox-group 当作 flex 容器,使卡片并排排列 */
 .districts {
-  margin-top: 88px;
+  margin-top: 40px;
   display: flex;
   flex-wrap: wrap;
   justify-content: center;
-  gap: 20px;
+  gap: 150px;
+  width: 100%;
+  padding: 0 20px;
+}
+
+/* 让每个 el-checkbox 占用固定宽度,并且把卡片当成 label 显示 */
+.district-checkbox {
+  display: block;
+  width: 374px;
 }
 
 .district-card {
@@ -233,12 +264,15 @@ export default {
   color: #444;
 }
 
-.arrow-icon {
-  font-size: 24px;
-  color: #333;
+.go-button {
+  margin-top: 180px;
+  width: 240px;
+  height: 50px;
+  font-size: 18px;
+  border-radius: 25px;
 }
 
-/* 以下是 body 和 html 的全局样式,去掉白边 */
+/* 全局样式去除白边 */
 :global(html), :global(body) {
   height: 100%;
   margin: 0;

+ 111 - 42
src/views/login/loginView.vue

@@ -11,12 +11,18 @@
     >
       <div class="form-header">
         <!-- 切换语言按钮 -->
-        <el-button class="language-toggle" @click="toggleLanguage">{{ currentLanguageName }}</el-button>
+        <el-button class="language-toggle" @click="toggleLanguage">{{
+          currentLanguageName
+        }}</el-button>
         <!-- 切换用户类型按钮 -->
-        <el-button class="user-type-toggle" @click="toggleUserType">{{ currentUserTypeName }}</el-button>
+        <el-button class="user-type-toggle" @click="toggleUserType">{{
+          currentUserTypeName
+        }}</el-button>
       </div>
       <h2 class="form-title">
-        {{ userType === 'user' ? $t('login.userTitle') : $t('login.adminTitle') }}
+        {{
+          userType === "user" ? $t("login.userTitle") : $t("login.adminTitle")
+        }}
       </h2>
       <el-form-item :label="$t('login.username')" prop="name">
         <el-input v-model="form.name"></el-input>
@@ -27,11 +33,11 @@
       <el-form-item>
         <div class="button-group">
           <el-button type="primary" @click="onSubmit" :loading="loading">
-            {{ $t('login.loginButton') }}
+            {{ $t("login.loginButton") }}
           </el-button>
         </div>
-        <div class="register-link" style="margin-top: 20px; text-align: center;">
-          <a @click="toggleForm">{{ $t('login.registerLink') }}</a>
+        <div class="register-link" style="margin-top: 20px; text-align: center">
+          <a @click="toggleForm">{{ $t("login.registerLink") }}</a>
         </div>
       </el-form-item>
     </el-form>
@@ -47,18 +53,25 @@
     >
       <div class="form-header">
         <!-- 切换语言按钮 -->
-        <el-button class="language-toggle" @click="toggleLanguage">{{ currentLanguageName }}</el-button>
+        <el-button class="language-toggle" @click="toggleLanguage">{{
+          currentLanguageName
+        }}</el-button>
         <!-- 切换用户类型按钮 -->
-        <el-button class="user-type-toggle" @click="toggleUserType">{{ currentUserTypeName }}</el-button>
+        <el-button class="user-type-toggle" @click="toggleUserType">{{
+          currentUserTypeName
+        }}</el-button>
       </div>
-      <h2 class="form-title">{{ $t('register.title') }}</h2>
+      <h2 class="form-title">{{ $t("register.title") }}</h2>
       <el-form-item :label="$t('register.username')" prop="name">
         <el-input v-model="registerForm.name"></el-input>
       </el-form-item>
       <el-form-item :label="$t('register.password')" prop="password">
         <el-input type="password" v-model="registerForm.password"></el-input>
       </el-form-item>
-      <el-form-item :label="$t('register.confirmPassword')" prop="confirmPassword">
+      <el-form-item
+        :label="$t('register.confirmPassword')"
+        prop="confirmPassword"
+      >
         <el-input
           type="password"
           v-model="registerForm.confirmPassword"
@@ -68,13 +81,13 @@
         <div class="button-group">
           <!-- 注册按钮 -->
           <el-button type="primary" @click="onRegister" :loading="loading">
-            {{ $t('register.registerButton') }}
+            {{ $t("register.registerButton") }}
           </el-button>
         </div>
         <div class="button-group">
           <!-- 返回登录按钮 -->
           <el-button @click="toggleForm">
-            {{ $t('register.backToLoginButton') }}
+            {{ $t("register.backToLoginButton") }}
           </el-button>
         </div>
       </el-form-item>
@@ -96,13 +109,14 @@ const store = useTokenStore();
 const router = useRouter();
 const route = useRoute();
 
-const isLogin = ref(true); // 控制显示登录或注册表单
+const isLogin = ref(true); // 确保默认值为 true,显示登录表单
 const userType = ref("user"); // 当前用户类型,默认为普通用户
 
 const form = reactive({
   name: "",
   password: "",
 });
+console.log("form 的类型:", typeof form);
 
 const registerForm = reactive({
   name: "",
@@ -114,7 +128,6 @@ const registerFormRef = ref<InstanceType<typeof ElForm> | null>(null);
 const loading = ref(false);
 const { t, locale } = useI18n(); // 使用 Composition API 模式
 
-
 // 获取当前语言名称
 const currentLanguageName = computed(() => {
   return locale.value === "zh" ? "English" : "中文";
@@ -128,7 +141,9 @@ const toggleLanguage = () => {
 
 // 获取当前用户类型名称
 const currentUserTypeName = computed(() => {
-  return userType.value === "user" ? t("login.switchToAdmin") : t("login.switchToUser");
+  return userType.value === "user"
+    ? t("login.switchToAdmin")
+    : t("login.switchToUser");
 });
 
 // 切换用户类型
@@ -148,22 +163,41 @@ const onSubmit = async () => {
     if (!valid) return;
     loading.value = true;
     try {
-      const res = await login({ name: form.name, password: form.password, userType: userType.value });
+      const res = await login({
+        name: form.name,
+        password: form.password,
+        userType: userType.value,
+      });
       if (res.data.success) {
         console.log("登录成功返回的信息:", res.data);
         const userInfo = {
           userId: parseInt(res.data.userId),
           name: res.data.name,
-          loginType: userType.value === 'admin' ? 'admin' : 'user', // 根据用户类型设置 loginType
+          loginType: userType.value === "admin" ? "admin" : "user", // 根据用户类型设置 loginType
         };
         store.saveToken(userInfo);
         console.log("保存的Token信息:", store.token);
-        ElMessage.success(i18n.global.t('login.loginSuccess'));
-        const redirectPath =
-          typeof route.query.redirect === "string" ? route.query.redirect : "/";
-        router.push(redirectPath);
+        ElMessage.success(i18n.global.t("login.loginSuccess"));
+        if (res.data.success) {
+          let redirect = "/";
+          if (userType.value === "user") {
+            console.log("普通用户登录成功,准备跳转到 selectCityAndCounty");
+            router.push({ name: "selectCityAndCounty" }).then(() => {
+              console.log("loginView.vue:185 成功跳转到 selectCityAndCounty");
+            }).catch((err) => {
+              console.error("跳转到 selectCityAndCounty 失败:", err);
+            });
+          } else {
+            redirect =
+              typeof route.query.redirect === "string"
+                ? route.query.redirect
+                : "/select-city";
+            router.push(redirect);
+          }
+          console.log("用户类型:", userType.value, "跳转地址:", redirect);
+        }
       } else {
-        ElMessage.error(res.data.message || i18n.global.t('login.loginFailed'));
+        ElMessage.error(res.data.message || i18n.global.t("login.loginFailed"));
       }
     } catch (error) {
       if (axios.isAxiosError(error)) {
@@ -173,7 +207,7 @@ const onSubmit = async () => {
         ElMessage.error(`HTTP Error: ${error.response?.statusText}`);
       } else {
         console.error(error);
-        ElMessage.error(i18n.global.t('login.loginFailed'));
+        ElMessage.error(i18n.global.t("login.loginFailed"));
       }
     } finally {
       loading.value = false;
@@ -188,7 +222,7 @@ const onRegister = async () => {
     if (!valid) return;
 
     if (registerForm.password !== registerForm.confirmPassword) {
-      ElMessage.error(i18n.global.t('register.passwordMismatch'));
+      ElMessage.error(i18n.global.t("register.passwordMismatch"));
       return;
     }
 
@@ -198,10 +232,12 @@ const onRegister = async () => {
         password: registerForm.password,
       });
       if (res.data.success) {
-        ElMessage.success(i18n.global.t('register.registerSuccess'));
+        ElMessage.success(i18n.global.t("register.registerSuccess"));
         toggleForm(); // 切换回登录表单
       } else {
-        ElMessage.error(res.data.message || i18n.global.t('register.registerFailed'));
+        ElMessage.error(
+          res.data.message || i18n.global.t("register.registerFailed")
+        );
       }
     } catch (error) {
       if (axios.isAxiosError(error)) {
@@ -211,7 +247,7 @@ const onRegister = async () => {
         ElMessage.error(`HTTP Error: ${error.response?.statusText}`);
       } else {
         console.error(error);
-        ElMessage.error(i18n.global.t('register.registerFailed'));
+        ElMessage.error(i18n.global.t("register.registerFailed"));
       }
     } finally {
       loading.value = false;
@@ -222,35 +258,69 @@ const onRegister = async () => {
 // 验证规则
 const rules = reactive({
   name: [
-    { required: true, message: i18n.global.t('validation.usernameRequired'), trigger: 'blur' }
+    {
+      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' }
-  ]
+    {
+      required: true,
+      message: i18n.global.t("validation.passwordRequired"),
+      trigger: "blur",
+    },
+    {
+      min: 3,
+      max: 16,
+      message: i18n.global.t("validation.passwordLength"),
+      trigger: "blur",
+    },
+  ],
 });
 
 const registerRules = reactive({
   name: [
-    { required: true, message: i18n.global.t('validation.usernameRequired'), trigger: 'blur' }
+    {
+      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' }
+    {
+      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) => {
+      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')));
+          callback(new Error(i18n.global.t("validation.passwordMismatch")));
         } else {
           callback();
         }
       },
-      trigger: 'blur'
-    }
-  ]
+      trigger: "blur",
+    },
+  ],
 });
 </script>
 
@@ -261,7 +331,7 @@ const registerRules = reactive({
   display: flex;
   justify-content: center;
   align-items: center;
-  font-family: 'Arial', sans-serif; /* 更现代的字体 */
+  font-family: "Arial", sans-serif; /* 更现代的字体 */
 }
 
 .login-form {
@@ -404,4 +474,3 @@ const registerRules = reactive({
   }
 }
 </style>
-

+ 1 - 1
tsconfig.app.json

@@ -9,7 +9,7 @@
     "src/views/User/introduction/IntroUpdateModal.vue", // Exclude missing file
     "src/views/Admin/dataManagement/Visualizatio.vue", // Exclude missing file
     "src/views/Admin/dataManagement/Visualization.vue", // Exclude missing file
-    "src/views/Admin/parameterConfig/thres.vue", // Exclude missing file
+    "src/views/Admin/modelManagement/AcidReductionModel/thres.vue", // Exclude missing file
     "src/views/User/heavyMetalFluxCalculation/outputFluxCalculation/outputFluxCalculation.vue" // Exclude missing file
   ],
   "compilerOptions": {