Pārlūkot izejas kodu

Merge branch 'lili' of qw/soilgd12 into master

Ding 1 nedēļu atpakaļ
vecāks
revīzija
a977f2c5a5

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

@@ -5,6 +5,7 @@
     "ComputedRef": true,
     "DirectiveBinding": true,
     "EffectScope": true,
+    "ElMessage": true,
     "ExtractDefaultPropTypes": true,
     "ExtractPropTypes": true,
     "ExtractPublicPropTypes": true,

+ 12 - 2
src/components/layout/AppLayout.vue

@@ -34,7 +34,7 @@
               :title="currentLang === 'zh' ? 'Switch to English' : '切换到中文'"
             >
               <i class="el-icon-globe"></i>
-              <span class="lang-label">{{ currentLang === 'zh' ? '中文' : 'English' }}</span>
+              <span class="lang-label">{{ currentLang === 'zh' ? '英文' : 'Chinese' }}</span>
             </button>
          </div>
 
@@ -218,6 +218,12 @@ const tabs = computed(() => {
     ];
   } else {
     return [
+      {
+        name:"totalIntroduction",
+        label: t('Menu.totalIntroduction'),
+        icon: "el-icon-info-filled",
+        routes: ["/totalIntroduction"],
+      },
       {
         name: "introduction",
         label: t('Menu.swIntroduce'),
@@ -232,7 +238,7 @@ const tabs = computed(() => {
       },
        {
         name: "agentDialog",
-        label: "土壤酸化AI智能体",
+        label: t('Menu.soilAcidAIAssistant'),
         icon: "el-icon-help-filled",
         routes: ["/AgentDialog"],
       },
@@ -547,6 +553,10 @@ const handleLogout = async () => {
   color: inherit;
   line-height: 1;
   display: inline-block;
+  max-width: 200px; /* 限制最大宽度,防止文本过长 */
+  overflow: hidden; /* 隐藏超出部分 */
+  text-overflow: ellipsis; /* 显示省略号 */
+  white-space: nowrap; /* 不换行 */
 }
 
 .logo {

+ 8 - 4
src/components/soilcdStatistics/fluxcdStatictics.vue

@@ -106,6 +106,14 @@ const fetchData = async () => {
     if (!processedData) {
       throw new Error('无法解析API返回的数据结构');
     }
+
+    // 从任意字段中获取样本数量(所有字段的 count 应该相同)
+    const firstFieldKey = Object.keys(processedData)[0];
+    if (firstFieldKey && processedData[firstFieldKey].count !== undefined) {
+      stats.value.samples = processedData[firstFieldKey].count;
+    } else {
+      stats.value.samples = 0;
+    }
     
     // console.log('处理后的数据:', processedData);
     return processedData;
@@ -158,9 +166,6 @@ const calculateAllStats = (data) => {
     calculateFieldStats(data, indicator.key, indicator.name)
   );
   
-  // 更新样本数
-  const firstStat = initialCdStats.value[0] || otherIndicatorsStats.value[0];
-  stats.value.samples = firstStat?.count || 0;
 };
 
 
@@ -192,7 +197,6 @@ const initInitialCdChart = () => {
     const boxData = buildBoxplotData(initialCdStats.value);
     
     const option = {
-      title: { text: t('SoilCdStatistics.initialCdDistribution'), left: 'center' },
       tooltip: {
         trigger: "item",
         formatter: (params) => {

+ 8 - 2
src/locales/en.json

@@ -33,7 +33,8 @@
     "registerFailed": "Registration failed",
     "autoLoginPrompt": "Registration successful, redirecting to login page..."
   },
-  "validation": {"account": "Account",
+  "validation": {
+    "account": "Account",
     "password": "Password",
     "confirmPassword": "Confirm Password",
     "usernameRequired": "Please enter your username",
@@ -63,7 +64,9 @@
     "detectionStatistics": "Detection Statistics",
     "soilCadmiumStatistics": "Soil Cadmium Content Statistics",
     "cropRiskAssessment": "Crop Risk Assessment System",
-    "soilAcidificationStatistics": "Soil Acidification Statistics"
+    "soilAcidificationStatistics": "Soil Acidification Statistics",
+    "soilAcidAIAssistant": "Soil Acidification AI Assistant",
+    "totalIntroduction":"Total Introduction"
   },
   "SoilacidificationStatistics": {
     "Title": "Soil acidification data statistics"
@@ -89,6 +92,9 @@
     "nitrate": "Nitrate",
     "ammonium": "Ammonium",
     "ferricOxide": "FerricOxide",
+    "ferricOxidePlaceholder": "Enter ferric oxide 0~50(g/kg)",
+    "dryLand": "Dry Land",
+    "paddyField": "Paddy Field",
     "cancel": "Cancel",
     "confirm": "Start Prediction",
     "acidInversionParams": "Acid Reflux Parameters",

+ 6 - 1
src/locales/zh.json

@@ -64,7 +64,9 @@
     "detectionStatistics": "检测信息统计",
     "soilCadmiumStatistics": "土壤镉含量统计",
     "cropRiskAssessment": "作物风险评估系统",
-    "soilAcidificationStatistics": "土壤酸化统计"
+    "soilAcidificationStatistics": "土壤酸化统计",
+    "soilAcidAIAssistant": "土壤酸化 AI 智能体",
+    "totalIntroduction": "数据看板"
   },
   "SoilacidificationStatistics": {
     "Title": "土壤酸化数据统计"
@@ -90,6 +92,9 @@
     "nitrate": "硝酸盐",
     "ammonium": "铵盐",
     "ferricOxide": "氧化铁",
+    "ferricOxidePlaceholder": "请输入氧化铁 0~50(g/kg)",
+    "dryLand": "旱地",
+    "paddyField": "水田",
     "cancel": "取消",
     "confirm": "开始预测",
     "acidInversionParams": "反酸预测参数",

+ 7 - 0
src/router/index.ts

@@ -55,6 +55,13 @@ const routes = [
           import("@/views/User/introduction/IntroductionUpdate.vue"), // 修复路径
         meta: { title: "更新介绍" },
       },
+      {
+        path: "totalIntroduction",
+        name: "totalIntroduction",
+        component: () =>
+          import("@/views/User/introduction/TotalIntroduction.vue"),
+        meta: { title: "总体介绍" },
+      },
       {
         path: "Calculation",
         name: "Calculation",

+ 31 - 28
src/views/User/acidModel/Calculation.vue

@@ -2,12 +2,12 @@
   <el-card class="box-card">
     <template #header>
       <div class="card-header">
-        <span>反酸模型</span>
+        <span>{{ t('Calculation.refluxTitle') }}</span>
         <div class="mode-switch">
           <el-button type="primary" :class="['mode-button', 'dry-mode', currentMode === 'dry' ? 'active' : '']"
-            @click="setMode('dry')" icon="el-icon-crop" round>旱地</el-button>
+            @click="setMode('dry')" icon="el-icon-crop" round>{{ t('AcidModelMap.dryLand') || '旱地' }}</el-button>
           <el-button type="primary" :class="['mode-button', 'paddy-mode', currentMode === 'paddy' ? 'active' : '']"
-            @click="setMode('paddy')" icon="el-icon-watermelon" round>水田</el-button>
+            @click="setMode('paddy')" icon="el-icon-watermelon" round>{{ t('AcidModelMap.paddyField') || '水田' }}</el-button>
         </div>
       </div>
     </template>
@@ -15,43 +15,43 @@
     <el-form :model="form" ref="predictForm" label-width="240px" label-position="left" class="form-container"
       :class="currentMode === 'dry' ? 'dry-form' : 'paddy-form'">
       <!-- 旱地模式下显示交换性氢 -->
-      <el-form-item v-if="currentMode === 'dry'" label="交换性氢(cmol/kg)" prop="H" :error="errorMessages.H" required>
-        <el-input v-model="form.H" size="large" placeholder="请输入交换性氢0~5(cmol/kg)"
+      <el-form-item v-if="currentMode === 'dry'" :label="t('Calculation.exchangeableHydrogen')" prop="H" :error="errorMessages.H" required>
+        <el-input v-model="form.H" size="large" :placeholder="t('Calculation.reflux.exchangeableHydrogenPlaceholder')"
           @input="handleInput('H', $event, 0, 5)"></el-input>
       </el-form-item>
-      <el-form-item label="交换性铝(cmol/kg)" prop="Al" :error="errorMessages.Al" required>
-        <el-input v-model="form.Al" size="large" placeholder="请输入交换性铝0~10(cmol/kg)"
+      <el-form-item :label="t('Calculation.exchangeableAluminum')" prop="Al" :error="errorMessages.Al" required>
+        <el-input v-model="form.Al" size="large" :placeholder="t('Calculation.reflux.exchangeableAluminumPlaceholder')"
           @input="handleInput('Al', $event, 0, 10)"></el-input>
       </el-form-item>
-      <el-form-item label="土壤有机质(g/kg)" prop="OM" :error="errorMessages.OM" required>
-        <el-input v-model="form.OM" size="large" placeholder="请输入土壤有机质0~35(g/kg)"
+      <el-form-item :label="t('Calculation.soilOrganicMatter')" prop="OM" :error="errorMessages.OM" required>
+        <el-input v-model="form.OM" size="large" :placeholder="t('Calculation.reflux.soilOrganicMatterPlaceholder')"
           @input="handleInput('OM', $event, 0, 35)"></el-input>
       </el-form-item>
-      <el-form-item label="硝酸盐(mg/kg)" prop="NO3" :error="errorMessages.NO3" required>
-        <el-input v-model="form.NO3" size="large" placeholder="请输入硝酸盐0~70(mg/kg)"
+      <el-form-item :label="t('Calculation.nitrate')" prop="NO3" :error="errorMessages.NO3" required>
+        <el-input v-model="form.NO3" size="large" :placeholder="t('Calculation.reflux.nitratePlaceholder')"
           @input="handleInput('NO3', $event, 0, 70)"></el-input>
       </el-form-item>
-      <el-form-item label="铵盐(mg/kg)" prop="NH4" :error="errorMessages.NH4" required>
-        <el-input v-model="form.NH4" size="large" placeholder="请输入铵盐0~20(mg/kg)"
+      <el-form-item :label="t('Calculation.ammoniumSalt')" prop="NH4" :error="errorMessages.NH4" required>
+        <el-input v-model="form.NH4" size="large" :placeholder="t('Calculation.reflux.ammoniumSaltPlaceholder')"
           @input="handleInput('NH4', $event, 0, 20)"></el-input>
       </el-form-item>
-      <el-form-item label="阳离子交换量(cmol/kg)" prop="CEC" :error="errorMessages.CEC" required>
-        <el-input v-model="form.CEC" size="large" placeholder="请输入阳离子交换量0~20(cmol/kg)"
+      <el-form-item :label="t('Calculation.cationExchangeCapacity')" prop="CEC" :error="errorMessages.CEC" required>
+        <el-input v-model="form.CEC" size="large" :placeholder="t('Calculation.reflux.cationExchangeCapacityPlaceholder')"
           @input="handleInput('CEC', $event, 0, 20)"></el-input>
       </el-form-item>
-      <!-- 水田模式下显示FeO -->
-      <el-form-item v-if="currentMode === 'paddy'" label="氧化铁(g/kg)" prop="FeO" :error="errorMessages.FeO" required>
-        <el-input v-model="form.FeO" size="large" placeholder="请输入氧化铁 0~50(g/kg)"
+      <!-- 水田模式下显示 FeO -->
+      <el-form-item v-if="currentMode === 'paddy'" :label="t('AcidModelMap.ferricOxide')" prop="FeO" :error="errorMessages.FeO" required>
+        <el-input v-model="form.FeO" size="large" :placeholder="t('AcidModelMap.ferricOxidePlaceholder') || '请输入氧化铁 0~50(g/kg)'"
           @input="handleInput('FeO', $event, 0, 50)"></el-input>
       </el-form-item>
 
-      <el-button type="primary" @click="onSubmit" class="onSubmit">计算</el-button>
+      <el-button type="primary" @click="onSubmit" class="onSubmit">{{ t('Calculation.calculateButton') }}</el-button>
 
       <el-dialog v-model="dialogVisible" @close="onDialogClose" :close-on-click-modal="false" width="500px" align-center
-        title="计算结果">
+        :title="t('Calculation.resultTitle')">
         <span class="dialog-class">ΔpH: {{ result }}</span>
         <template #footer>
-          <el-button @click="dialogVisible = false">关闭</el-button>
+          <el-button @click="dialogVisible = false">{{ t('Calculation.closeButton') }}</el-button>
         </template>
       </el-dialog>
     </el-form>
@@ -62,6 +62,9 @@
 import { reactive, ref, nextTick, onMounted } from 'vue';
 import { ElMessage } from 'element-plus';
 import { api5000 } from '../../../utils/request'; // 使用api5000
+import { useI18n } from 'vue-i18n';
+
+const { t } = useI18n();
 
 // 计算模式类型
 type CalculationMode = 'dry' | 'paddy';
@@ -187,14 +190,14 @@ const onSubmit = async () => {
     const value = form[field];
     if (!validateInput(value, min, max)) {
       isValid = false;
-      errorMessages[field] = `输入值应在 ${min} 到 ${max} 之间且为有效数字`;
+      errorMessages[field] = t('Calculation.validationRange', { min, max })
     } else {
       errorMessages[field] = '';
     }
   }
 
   if (!isValid) {
-    ElMessage.error('输入值不符合要求,请检查输入');
+    ElMessage.error(t('Calculation.validationError'))
     return;
   }
 
@@ -231,19 +234,19 @@ const onSubmit = async () => {
       result.value = parseFloat(response.data.result[0].toFixed(2));
       dialogVisible.value = true;
     } else {
-      ElMessage.error('未获取到有效的预测结果');
+      ElMessage.error(t('Calculation.invalidResult'))
     }
   } catch (error: any) {
     console.error('请求失败:', error);
     if (error.response) {
-      ElMessage.error(`请求失败,状态码: ${error.response.status}`);
+      ElMessage.error(t('Calculation.requestFailed') + error.response.status);
     } else if (error.request) {
-      ElMessage.error('请求发送成功,但没有收到响应');
+      ElMessage.error(t('Calculation.noResponse'));
     } else {
-      ElMessage.error('请求过程中发生错误: ' + error.message);
+      ElMessage.error(t('Calculation.requestError') + error.message);
     }
   }
-};
+}
 
 // 弹框关闭后重置表单
 const onDialogClose = () => {

+ 20 - 17
src/views/User/acidModel/ModelIterationVisualization.vue

@@ -3,6 +3,9 @@ import { ref, onMounted, nextTick, onUnmounted, watch } from 'vue';
 import VueEcharts from 'vue-echarts';
 import 'echarts';
 import { api5000 } from '../../../utils/request';
+import { useI18n } from 'vue-i18n';
+
+const { t } = useI18n();
 
 interface HistoryDataItem {
   dataset_id: number;
@@ -86,10 +89,10 @@ const selectedLandType = ref('dryland'); // 默认选择旱地
 
 // 模型类型选项
 const modelTypeOptions = [
-  { label: '随机森林', value: 'rf' },
-  { label: 'XGBoost', value: 'xgbr' },
-  { label: '梯度提升', value: 'gbst' },
-];
+  { label: t('ModelIteration.randomForest'), value: 'rf' },
+  { label: t('ModelIteration.xgboost'), value: 'xgbr' },
+  { label: t('ModelIteration.gradientBoosting'), value: 'gbst' },
+]
 
 // 土地类型验证
 const validateLandType = (landType: string): boolean => {
@@ -145,10 +148,10 @@ const fetchLineData = async () => {
         containLabel: true,
       },
       xAxis: {
-        name: '模型迭代',
+        name: t('ModelIteration.generations'),
         type: 'category',
         boundaryGap: false,
-        data: timestamps.map((_, index) => `${index + 1}`),
+        data: timestamps.map((_, index) => `${index + 1}${t('ModelIteration.generations')}`),
       },
       yAxis: {
         name: 'Score (R^2)',
@@ -158,7 +161,7 @@ const fetchLineData = async () => {
     };
     console.log('ecLineOption updated:', ecLineOption.value);
   } catch (error) {
-    console.error('获取折线图数据失败:', error);
+    console.error(t('SoilCdStatistics.dataLoadFailed') + ':', error)
   }
 };
 
@@ -358,15 +361,15 @@ onUnmounted(() => {
   <div class="container">
     <!-- 主标题区域,包含标题和土地类型按钮组 -->
     <div class="main-title-row">
-      <h1 class="main-title">模型迭代可视化</h1>
+      <h1 class="main-title">{{ t('ModelIteration.modelIteration') }}</h1>
       <div class="land-type-buttons">
         <button :class="['land-button', 'dryland-button', selectedLandType === 'dryland' ? 'active' : '']"
           @click="selectedLandType = 'dryland'">
-          旱地
+          {{ t('AcidModelMap.dryLand') || '旱地' }}
         </button>
         <button :class="['land-button', 'paddy-button', selectedLandType === 'paddy_field' ? 'active' : '']"
           @click="selectedLandType = 'paddy_field'">
-          水田
+          {{ t('AcidModelMap.paddyField') || '水田' }}
         </button>
       </div>
     </div>
@@ -375,7 +378,7 @@ onUnmounted(() => {
     <div class="chart-module">
       <!-- 学习曲线标题和模型选择器在同一行 -->
       <div class="chart-title-row">
-        <h2 class="chart-header">学习曲线</h2>
+        <h2 class="chart-header">{{ t('ModelIteration.learningCurve') }}</h2>
         <div class="model-selector-wrapper">
           <select v-model="selectedModelType" class="model-select">
             <option v-for="option in modelTypeOptions" :key="option.value" :value="option.value">
@@ -386,19 +389,19 @@ onUnmounted(() => {
       </div>
       <div class="chart-container">
         <div class="image-container">
-          <img v-if="learningCurveImageUrl" :src="learningCurveImageUrl" alt="学习曲线" />
-          <div v-else class="image-placeholder">加载中...</div>
+          <img v-if="learningCurveImageUrl" :src="learningCurveImageUrl" :alt="t('ModelIteration.learningCurve')" />
+          <div v-else class="image-placeholder">{{ t('ModelIteration.loading') }}</div>
         </div>
       </div>
     </div>
 
     <!-- 数据增长曲线模块 - 移到中间 -->
     <div class="chart-module">
-      <h2 class="chart-header">数据增长曲线</h2>
+      <h2 class="chart-header">{{ t('ModelIteration.dataIncreaseCurve') }}</h2>
       <div class="chart-container">
         <div class="image-container">
-          <img v-if="dataIncreaseCurveImageUrl" :src="dataIncreaseCurveImageUrl" alt="数据增长曲线" />
-          <div v-else class="image-placeholder">加载中...</div>
+          <img v-if="dataIncreaseCurveImageUrl" :src="dataIncreaseCurveImageUrl" :alt="t('ModelIteration.dataIncreaseCurve')" />
+          <div v-else class="image-placeholder">{{ t('ModelIteration.loading') }}</div>
         </div>
       </div>
     </div>
@@ -406,7 +409,7 @@ onUnmounted(() => {
     <!-- 散点图模块 - 移到最下方 -->
     <template v-if="showInitScatterChart">
       <div class="chart-module">
-        <h2 class="chart-header">散点图</h2>
+        <h2 class="chart-header">{{ t('ModelIteration.scatterPlotTitle') }}</h2>
         <div class="chart-container">
           <VueEcharts :option="ecInitScatterOption" ref="ecInitScatterOptionRef" />
         </div>

+ 94 - 51
src/views/User/introduction/Introduce.vue

@@ -6,8 +6,8 @@
     <div v-else-if="error" class="error-message">{{ error }}</div>
     <!-- 正常展示内容 -->
     <div v-else class="content-wrapper">
-      <h1 class="title">{{ softwareIntro.title }}</h1>
-      <div v-for="(paragraph, index) in softwareIntro.introParagraphs" :key="index" class="paragraph">
+      <h1 class="title">{{ currentTitle }}</h1>
+      <div v-for="(paragraph, index) in currentIntroParagraphs" :key="index" class="paragraph">
         <p v-html="paragraph"></p>
       </div>
     </div>
@@ -15,8 +15,11 @@
 </template>
 
 <script setup lang="ts">
-import { ref, onMounted } from 'vue';
-import { api5000 } from '@/utils/request'; // 导入封装的api5000
+import { ref, onMounted, onUnmounted, computed } from 'vue';
+import { api5000 } from '@/utils/request';
+import { useI18n } from 'vue-i18n';
+
+const { locale } = useI18n();
 
 // 定义 targetId 为 prop
 const props = defineProps({
@@ -26,36 +29,94 @@ const props = defineProps({
   }
 });
 
-// 存储介绍内容
+// 存储介绍内容(支持中英文)
 const softwareIntro = ref({
   title: '',
-  introParagraphs: []
+  title_en: '',
+  intro: '',
+  intro_en: ''
+});
+
+// 当前语言状态(与全局同步)
+const language = ref(locale.value || localStorage.getItem('language') || 'zh');
+
+// 当前显示的标题
+const currentTitle = computed(() => {
+  return language.value === 'zh' ? softwareIntro.value.title : softwareIntro.value.title_en;
+});
+
+// 当前显示的段落内容
+const currentIntroParagraphs = computed(() => {
+  const intro = language.value === 'zh' ? softwareIntro.value.intro : softwareIntro.value.intro_en;
+  return intro
+    .replace(/<p>/g, '')
+    .replace(/<\/p>/g, '\r\n')
+    .replace(/<br>/g, '')
+    .split('\r\n')
+    .filter((p: string) => p.trim() !== '');
 });
+
 // 加载状态
 const isLoading = ref(true);
 // 错误信息
 const error = ref('');
 
-// 判断是否为标题段落
-
-// 处理段落,将图片路径转换为  标签
-
-onMounted(async () => {
+// 获取数据
+const fetchSoftwareIntro = async () => {
   try {
-    // 使用 api5000 调用接口
     const response = await api5000.get(`/admin/software-intro/${props.targetId}`);
-    const { title, intro } = response.data;
-    // 保留 \r\n 换行符
-    const processedIntro = intro.replace(/<p>/g, '').replace(/<\/p>/g, '\r\n').replace(/<br>/g, '');
-    softwareIntro.value.introParagraphs = processedIntro.split('\r\n').filter((paragraph: string) => paragraph.trim()!== '');
-    softwareIntro.value.title = title;
+    const { title, title_en, intro, intro_en } = response.data;
+
+    // 设置多语言内容
+    softwareIntro.value.title = title || '';
+    softwareIntro.value.title_en = title_en || '';
+    softwareIntro.value.intro = intro || '';
+    softwareIntro.value.intro_en = intro_en || '';
   } catch (err: any) {
     error.value = err.message || '请求数据时发生错误';
   } finally {
     isLoading.value = false;
   }
+};
+
+// 监听全局语言变化事件
+const handleLanguageChange = (event: Event) => {
+ const newLang = (event as CustomEvent<{ lang: string }>).detail.lang;
+  language.value = newLang;
+  // 重新获取数据以刷新内容
+  fetchSoftwareIntro();
+};
+
+// 页面挂载后初始化
+onMounted(async () => {
+  await fetchSoftwareIntro();
+
+  // 添加全局事件监听器
+  window.addEventListener('languageChanged', handleLanguageChange);
+});
+
+// 卸载时移除监听器
+onUnmounted(() => {
+  window.removeEventListener('languageChanged', handleLanguageChange);
+});
+
+// 封装设置视频样式的函数(保持原逻辑)
+const setVideoStyle = (video: HTMLVideoElement) => {
+  const containerWidth = (document.querySelector('.software-intro-container') as HTMLElement).offsetWidth;
+  const maxWidth = containerWidth * 0.8;
+  if (video.videoWidth > maxWidth) {
+    video.style.width = `${maxWidth}px`;
+    video.style.height = 'auto';
+  } else {
+    video.style.width = 'auto';
+    video.style.height = 'auto';
+  }
+  video.style.maxWidth = '80%';
+  video.style.objectFit = 'contain';
+};
 
-  // 页面加载完成后,监听视频播放事件
+// 页面加载完成后,监听视频播放事件
+onMounted(() => {
   const container = document.querySelector('.software-intro-container');
   if (container) {
     container.addEventListener('play', (event) => {
@@ -65,7 +126,6 @@ onMounted(async () => {
       }
     }, true);
 
-    // 监听窗口大小变化事件,确保视频在窗口大小改变时也能适应
     window.addEventListener('resize', () => {
       const videos = container.querySelectorAll('video');
       videos.forEach((video) => {
@@ -76,35 +136,20 @@ onMounted(async () => {
     });
   }
 });
-
-// 封装设置视频样式的函数
-const setVideoStyle = (video: HTMLVideoElement) => {
-  const containerWidth = (document.querySelector('.software-intro-container') as HTMLElement).offsetWidth;
-  const maxWidth = containerWidth * 0.8; // 最大宽度为容器宽度的 80%
-  if (video.videoWidth > maxWidth) {
-    video.style.width = `${maxWidth}px`;
-    video.style.height = 'auto';
-  } else {
-    video.style.width = 'auto';
-    video.style.height = 'auto';
-  }
-  video.style.maxWidth = '80%';
-  video.style.objectFit = 'contain';
-};
 </script>
 
 <style scoped lang="scss">
 .software-intro-container {
   width: 95%;
-  margin: 0 auto; 
+  margin: 0 auto;
   padding: 20px;
-  background-color: #fff; 
-  border-radius: 8px; /* 圆角 */
-  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); /* 阴影 */
-  height: 80vh; /* 设置容器高度为视口高度的 80% */
-  overflow-y: auto; /* 当内容超出容器高度时,显示垂直滚动条 */
-  scrollbar-width: none; /* Firefox */
-  -ms-overflow-style: none; /* Internet Explorer 10+ */
+  background-color: #fff;
+  border-radius: 8px;
+  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+  height: 80vh;
+  overflow-y: auto;
+  scrollbar-width: none;
+  -ms-overflow-style: none;
 }
 
 .loading-message {
@@ -117,7 +162,7 @@ const setVideoStyle = (video: HTMLVideoElement) => {
 .error-message {
   text-align: center;
   font-size: 18px;
-  color: #ff0000; /* 红色错误信息 */
+  color: #ff0000;
   padding: 20px;
 }
 
@@ -125,7 +170,7 @@ const setVideoStyle = (video: HTMLVideoElement) => {
   font-size: 36px;
   text-align: center;
   margin-bottom: 20px;
-  color: #333; /* 深灰色标题 */
+  color: #333;
 }
 
 .paragraph {
@@ -134,19 +179,18 @@ const setVideoStyle = (video: HTMLVideoElement) => {
 
 p {
   font-size: 16px;
-  line-height: 1.8; /* 增大行高 */
-  color: #666; /* 灰色文字 */
+  line-height: 1.8;
+  color: #666;
 }
 
 .intro-image {
   max-width: 100%;
   height: auto;
-  border-radius: 4px; /* 图片圆角 */
-  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* 图片阴影 */
+  border-radius: 4px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
   margin-bottom: 10px;
 }
 
-/* 控制视频尺寸 */
 video {
   width: 80%;
   max-width: 80%;
@@ -154,11 +198,10 @@ video {
   object-fit: contain;
 }
 
-/* 响应式设计 */
 @media (max-width: 768px) {
   .software-intro-container {
     padding: 15px;
-    height: 90vh; /* 在小屏幕上,调整容器高度为视口高度的 90% */
+    height: 90vh;
   }
 
   .title {

+ 260 - 0
src/views/User/introduction/TotalIntroduction.vue

@@ -0,0 +1,260 @@
+<template>
+  <div class="total-introduction">
+    <!-- 背景层 -->
+    <div class="background-layer"></div>
+
+    <!-- 顶部标题 -->
+    <!-- <div class="header-title">
+      <h1>数据看板</h1>
+    </div> -->
+
+    <!-- 主要内容区域 -->
+    <div class="main-content">
+      <!-- 左侧区域 -->
+      <div class="left-section">
+        <CropcdStatictics/>
+      </div>
+
+      <!-- 中间区域 -->
+      <div class="center-section">
+        <!-- 地图区域 -->
+       
+        <div id="map-container" class="map-containter" ref="mapContainer">
+          <div v-if="mapLoading" class="map-loading">
+            <div class="spinner"></div>
+            <p>地图加载中...</p>
+          </div>
+          <div v-if="mapError" class="map-error">
+            <p>地图加载失败,请刷新重试</p>
+            <button @click="reloadMap" class="reload-btn">重新加载</button>
+          </div>
+
+        </div>
+
+        <FluxcdStatictics/>
+
+      </div>
+
+      <!-- 右侧区域 -->
+      <div class="right-section">
+       <EffcdStatistics/>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, nextTick } from 'vue';
+import FluxcdStatictics from "@/components/soilcdStatistics/fluxcdStatictics.vue";
+import { ElMessage } from 'element-plus';
+import { useI18n } from 'vue-i18n';
+import CropcdStatictics from '@/components/soilcdStatistics/cropcdStatictics.vue';
+
+const { t } = useI18n();
+
+// 地图相关
+let L = null;
+const map = ref(null);
+const mapContainer = ref(null);
+const mapLoading = ref(false);
+const mapError = ref(false);
+const initMap = async () => {
+  mapLoading.value = true;
+  mapError.value = false;
+
+  try {
+    // 动态导入 Leaflet
+    if (!L) {
+      L = await import('leaflet');
+      await import('leaflet/dist/leaflet.css');
+      
+      delete (L.Icon.Default.prototype)._getIconUrl;
+      L.Icon.Default.mergeOptions({
+        iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
+        iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
+        shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
+      });
+    }
+
+    // 清除现有地图
+   const container = document.getElementById('map-container');
+
+    // 创建地图实例
+    map.value = L.map(container, {
+      zoomControl: true,
+      attributionControl: false,
+      center: [25.202903, 113.25383],
+      zoom: 10
+    });
+
+    // WMS 配置
+    const GEOSERVER_CONFIG = {
+      url: "/geoserver/wms",
+      workspace: "acidmap",
+      layerGroup: "mapwithboundary", 
+    };
+
+    // WMS 图层配置
+    const wmsLayer = L.tileLayer.wms(GEOSERVER_CONFIG.url, {
+      layers: `${GEOSERVER_CONFIG.workspace}:${GEOSERVER_CONFIG.layerGroup}`,
+      format: "image/png",
+      transparent: true,
+      version: "1.1.1",
+      crs: L.CRS.EPSG4326,
+      attribution: "Data from GeoServer"
+    });
+
+    // 添加图层到地图
+    wmsLayer.addTo(map.value);
+    // 等待地图渲染完成后调整大小
+    setTimeout(() => {
+      if (map.value) {
+        map.value.invalidateSize();
+      }
+    }, 100);
+
+    mapLoading.value = false;
+
+  } catch (error) {
+    console.error('地图初始化失败:', error);
+    mapError.value = true;
+    mapLoading.value = false;
+    
+    let errorMessage = t('AcidModelMap.mapInitError');
+    if (error instanceof Error) {
+      errorMessage += ': ' + error.message;
+    }
+    ElMessage.error(errorMessage);
+  }
+};
+
+const reloadMap = () => {
+  initMap();
+};
+
+onMounted(() => {
+  // 等待地图渲染完成后调整大小
+    setTimeout(() => {
+      initMap()
+    }, 200);
+});
+
+
+
+</script>
+
+<style scoped>
+.total-introduction {
+  width: 100%;
+  height: 100%;
+  background-color: #0a0a2a;
+  color: white;
+  font-family: 'Arial', sans-serif;
+  position: relative;
+}
+
+.background-layer {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background-image: url('https://via.placeholder.com/1920x1080/0a0a2a/0a0a2a');
+  background-size: cover;
+  z-index: -1;
+}
+
+.header-title {
+  text-align: center;
+  padding: 20px 20px 0 20px;
+}
+
+.header-title h1 {
+  font-size: 32px;
+  color: #00bfff;
+  text-shadow: 0 0 10px rgba(0, 187, 255, 0.8);
+  border-bottom: 2px solid #00bfff;
+  padding-bottom: 10px;
+}
+
+.main-content {
+  display: flex;
+  gap: 20px;
+  padding: 20px;
+  overflow-x: auto;
+}
+
+.left-section,
+
+.right-section {
+  flex: 1;
+  width: 200px;
+}
+.center-section {
+  flex: 2;
+  width: 500px;
+}
+
+.map-containter {
+  width: 100%;
+  height: 600px;
+  border: 2px solid #00bfff;
+  border-radius: 10px;
+  overflow: hidden;
+  position: relative;
+  background-color: rgba(10, 10, 42, 0.8);
+}
+
+.map-loading,
+.map-error {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  background-color: rgba(0, 0, 0, 0.7);
+  z-index: 1000;
+  color: white;
+}
+
+.spinner {
+  width: 40px;
+  height: 40px;
+  border: 4px solid rgba(0, 187, 255, 0.3);
+  border-radius: 50%;
+  border-top-color: #00bfff;
+  animation: spin 1s linear infinite;
+  margin-bottom: 15px;
+}
+
+@keyframes spin {
+  to { transform: rotate(360deg); }
+}
+
+.map-error p {
+  color: #ff6b6b;
+  font-size: 16px;
+  margin-bottom: 15px;
+}
+
+.reload-btn {
+  padding: 10px 20px;
+  background-color: #00bfff;
+  color: white;
+  border: none;
+  border-radius: 5px;
+  cursor: pointer;
+  font-size: 14px;
+  transition: all 0.3s;
+}
+
+.reload-btn:hover {
+  background-color: #009acd;
+  transform: translateY(-2px);
+}
+
+</style>