#4 添加中英文切换功能

已關閉
lirong 請求將 9 次代碼提交從 qw/lili 合併至 qw/ding

+ 18 - 3
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>
 
@@ -175,6 +175,10 @@ const backgroundStyle = computed(() => ({
 const isFullScreen = computed(() => route.meta.fullScreen === true);
 const isSelectCity = computed(() => route.path === "/select-city");
 
+const isDataDashboard = computed(() => {
+  return route.path === '/totalIntroduction'
+})
+
 // 当前用户信息
 const userInfo = reactive({
   name: tokenStore.userName,
@@ -218,6 +222,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 +242,7 @@ const tabs = computed(() => {
       },
        {
         name: "agentDialog",
-        label: "土壤酸化AI智能体",
+        label: t('Menu.soilAcidAIAssistant'),
         icon: "el-icon-help-filled",
         routes: ["/AgentDialog"],
       },
@@ -303,7 +313,7 @@ const AsideComponent = computed(() => {
 
 const showAside = computed(
   () =>
-    !isFullScreen.value && !["cropRiskAssessment"].includes(activeName.value)
+    !isFullScreen.value && !["cropRiskAssessment"].includes(activeName.value) && !isDataDashboard.value
 );
 
 const mainStyle = computed(() => ({
@@ -311,6 +321,7 @@ const mainStyle = computed(() => ({
     ? "0"
     : "20px",
   overflow: "hidden",
+  width:isDataDashboard.value?'100%':'auto'   //数据看板页占满宽度
 }));
 
 // 登出逻辑 - 支持多语言
@@ -547,6 +558,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 {

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

@@ -0,0 +1,910 @@
+<script setup>
+   import { ref, watch, onMounted, onBeforeUnmount, onUnmounted ,computed,nextTick} from 'vue'
+   import L from 'leaflet'
+   import 'leaflet/dist/leaflet.css'
+   import { api8000 } from '@/utils/request'
+
+   const samplePointsData = ref([])
+   const mapRef = ref(null)
+   let map = null
+   let wmsLayer = null
+   let geoJsonLayer = null
+
+
+   const statistics = ref({
+     totalBlocks: 0,
+     avgPH: 0,
+     strongAcidCount: 0,
+     mildAcidCount: 0,
+     normalCount: 0,
+     maxPH: 0,
+     minPH: 0
+   })
+
+  //  
+   const phDistribution = ref({
+     range1: 0,
+     range2: 0,
+     range3: 0,
+   })
+
+   const selectedPoint = ref(null)
+
+   const CONFIG = {
+    center:[25.202903, 113.25383],
+    zoom:11,
+    getPoint:'/api/vector/export/all?table_name=le_soil_data&format=geojson',
+    geoserver:{
+      url:'/geoserver',
+      workspace:'acidmap',
+      layerGroup:'leshujukanbanmap',
+      dataLayer:'le_soil_data',
+      wmsUrl:'/geoserver/acidmap/wms'
+    }
+   }
+
+
+   // 获取 pH 等级对应的 CSS 类
+function getPHLevelClass(ph) {
+    // ✅ 先转换为数字
+    const numericPh = typeof ph === 'string' ? parseFloat(ph) : ph
+    if (!numericPh || numericPh === 0) return ''
+    if (numericPh <= 5.2) return 'danger'
+    if (numericPh < 6.0) return 'warning'
+    return 'success'
+  }
+
+
+// 获取综合评语
+function getPHComment(avgPH) {
+  if (avgPH <= 4.5) return '🔴 土壤严重酸化,需立即治理!'
+  if (avgPH <= 5.2) return '🟠 土壤酸化明显,建议尽快改良'
+  if (avgPH < 6.0) return '🟡 土壤微酸性,注意保持'
+  return '🟢 土壤酸碱度适宜,状态良好'
+}
+
+  
+  function getPHRangeLabel(range) {
+  const labels = {
+    range1: '< 5.2',
+    range2: '5.2 - 6.0',
+    range3: ' >6.0 ',
+  }
+  return labels[range] || range
+  }
+
+  // 添加分页加载支持
+   const batchSize = 1000;
+   let allFeatures = [];
+
+  async function initMap(){
+    await nextTick()
+
+    if(!mapRef.value) return 
+    map = L.map(mapRef.value).setView(CONFIG.center,CONFIG.zoom)
+
+    wmsLayer = L.tileLayer.wms(CONFIG.geoserver.wmsUrl, {
+      layers: `${CONFIG.geoserver.workspace}:${CONFIG.geoserver.layerGroup}`,
+      format: 'image/png',
+      transparent: true,
+      version: '1.1.0',
+      srs:'EPSG:4326',
+      attribution: '© GeoServer - Acidmap'
+    }).addTo(map)
+
+  }
+
+  function createPointIcon(feature) {
+    const ph = feature.properties.ph || feature.properties.value
+    let color = '#22c55e'
+    if (ph <= 5.2) color = '#ef4444'
+    else if (ph < 6.0) color = '#f59e0b'
+
+    return L.divIcon({
+      className: 'custom-marker',
+      html: `<div style="
+        background-color: ${color};
+        width: 12px;
+        height: 12px;
+        border-radius: 50%;
+        border: 2px solid white;
+        box-shadow: 0 2px 4px rgba(0,0,0,0.3);
+      "></div>`,
+      iconSize: [12, 12],
+      iconAnchor: [6, 6]
+    })
+  }
+
+  function parsePHValue(phValue) {
+    if (!phValue && phValue !== 0) return null
+    const numericPh = typeof phValue === 'string' ? parseFloat(phValue) : phValue
+    return !isNaN(numericPh) && numericPh > 0 ? numericPh : null
+  }
+
+
+  async function loadStatistics() {
+  try {
+    // 等待数据加载完成
+    if (samplePointsData.value.length === 0) return;
+
+    let phCount = 0
+    let avgPH = 0
+    let strongAcidCount = 0
+    let mildAcidCount = 0
+    let normalCount = 0
+    let maxPH = -Infinity
+    let minPH = Infinity
+
+    samplePointsData.value.forEach(feature => {
+      const numericPh = parsePHValue(feature.properties.ph || feature.properties.value)
+      
+      if (numericPh && numericPh > 0 && !isNaN(numericPh)) {
+        phCount++
+        const delta = numericPh - avgPH;
+        avgPH = avgPH + delta / phCount;
+        maxPH = Math.max(maxPH, numericPh)
+        minPH = Math.min(minPH, numericPh)
+        
+        if (numericPh <= 5.2) strongAcidCount++
+        else if (numericPh < 6.0) mildAcidCount++
+        else normalCount++
+      }
+    });
+
+    statistics.value = {
+      totalBlocks: samplePointsData.value.length,
+      avgPH:  phCount > 0 ? parseFloat(avgPH.toFixed(2)) : 0,  // ✅ 确保是数字
+      strongAcidCount,
+      mildAcidCount,
+      normalCount,
+      maxPH: maxPH === -Infinity ? 0 : parseFloat(maxPH.toFixed(2)),
+      minPH: minPH === Infinity ? 0 : parseFloat(minPH.toFixed(2))
+    };
+
+    console.log('✅ 统计数据加载完成:', statistics.value);
+  } catch (err) {
+    console.error('加载统计数据失败:', err);
+  }
+}
+
+// 修改为只获取数据用于统计,不渲染到地图
+  async function fetchDataForStatistics() {
+    try {
+      const response = await fetch(
+        `${CONFIG.geoserver.url}/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=${CONFIG.geoserver.workspace}:${CONFIG.geoserver.dataLayer}&outputFormat=application/json`
+      );
+      
+      if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
+      
+      const geoJsonData = await response.json();
+      
+      console.log('✅ 获取到数据:', geoJsonData.features.length, '条记录');
+
+      // 保存数据用于统计和交互
+      allFeatures = geoJsonData.features;
+      samplePointsData.value = geoJsonData.features;
+      
+      // 计算统计信息
+      calculatePHDistribution(allFeatures);
+      loadStatistics();
+      
+      // 添加点击事件监听(不显示标记点)
+      map.on('click', function(e) {
+        const latlng = e.latlng;
+        // 查找最近的采样点
+        findNearestPoint(latlng);
+      });
+      
+    } catch (err) {
+      console.error('❌ 加载统计数据失败:', err);
+    }
+  }
+
+  // 计算 pH 分布
+  function calculatePHDistribution(features) {
+    const distribution = {
+      range1: 0,
+      range2: 0,
+      range3: 0
+    }
+
+    features.forEach(feature => {
+      const phValue = feature.properties.ph || feature.properties.value
+      const numericPh = typeof phValue === 'string' ? parseFloat(phValue) : phValue
+      if (numericPh && numericPh > 0) {
+        if (numericPh <= 5.2) distribution.range1++
+        else if (numericPh <= 6) distribution.range2++
+        else  distribution.range3++
+      }
+    })
+
+    phDistribution.value = distribution
+  }
+
+  // 查找最近的采样点
+  function findNearestPoint(latlng) {
+    let nearestPoint = null;
+    let minDistance = Infinity;
+    
+    allFeatures.forEach(feature => {
+      const coords = feature.geometry.coordinates;
+      if (coords && coords.length >= 2) {
+        const pointLat = coords[1];
+        const pointLng = coords[0];
+        const distance = Math.sqrt(
+          Math.pow(pointLat - latlng.lat, 2) + 
+          Math.pow(pointLng - latlng.lng, 2)
+        );
+        
+        if (distance < minDistance && distance < 0.01) { // 10 米范围内
+          minDistance = distance;
+          nearestPoint = feature;
+        }
+      }
+    });
+    
+    if (nearestPoint) {
+      const ph = parsePHValue(nearestPoint.properties.ph || nearestPoint.properties.value);
+      
+      selectedPoint.value = {
+        ph: ph,
+        properties: nearestPoint.properties
+      };
+    }
+  }
+
+
+onUnmounted(()=>{
+    if(map) {
+      map.remove()
+      map = null
+    }
+    if(wmsLayer) {
+      wmsLayer = null
+    }
+    if(markersLayer) {
+      markersLayer = null
+    }
+    if(geoJsonLayer) { // 原 markersLayer 改为 geoJsonLayer(代码中实际定义的是 geoJsonLayer)
+      geoJsonLayer = null
+    }
+    samplePointsData.value = []
+    selectedPoint.value = null
+  })
+
+  onMounted(async()=>{
+    await initMap()
+    await fetchDataForStatistics()
+    await loadStatistics()
+  })
+
+  onUnmounted(()=>{
+    if(map) map.remove()
+  })
+
+</script>
+
+<template>
+  <div class="map-container">
+    <div class="ph-map" ref="mapRef"></div>
+
+    <!-- 计算刷新按钮 -->
+     <div class="compute">
+      <button class="combtn">实施降酸措施一周期后</button>
+      <button class="combtn">反酸一周期后</button>
+     </div>
+    <!-- pH 统计 -->
+    <div class="statistics-panel">
+      <h4>📊 乐昌县土壤 pH 统计</h4>
+      
+      <div class="stat-row">
+        <span class="stat-label">采样点总数:</span>
+        <span class="stat-value">{{ statistics.totalBlocks }} 个</span>
+      </div>
+      
+      <div class="stat-row highlight">
+        <span class="stat-label">平均 pH 值:</span>
+        <span class="stat-value" :class="getPHLevelClass(parseFloat(statistics.avgPH))">
+          {{ statistics.avgPH || '-' }}
+        </span>
+      </div>
+      
+      <div class="stat-row">
+        <span class="stat-label">强酸性 (pH≤5.2):</span>
+        <span class="stat-value danger">{{ statistics.strongAcidCount }} 个</span>
+      </div>
+      
+      <div class="stat-row">
+        <span class="stat-label">弱酸性 (pH 5.2~6.0):  </span>
+        <span class="stat-value warning">{{ statistics.mildAcidCount }} 个</span>
+      </div>
+      
+      <div class="stat-row">
+        <span class="stat-label">正常 (pH≥6.0):</span>
+        <span class="stat-value success">{{ statistics.normalCount }} 个</span>
+      </div>
+      
+      <div class="stat-divider"></div>
+      
+      <div class="stat-row small">
+        <span class="stat-label">最高 pH:</span>
+        <span class="stat-value success">{{ statistics.maxPH || '-' }}</span>
+      </div>
+      
+      <div class="stat-row small">
+        <span class="stat-label">最低 pH:</span>
+        <span class="stat-value danger">{{ statistics.minPH || '-' }}</span>
+      </div>
+      
+      <div class="stat-comment" v-if="statistics.avgPH > 0">
+        <div class="comment-title">📝 综合评估:</div>
+        <div class="comment-text" :class="getPHLevelClass(statistics.avgPH)">
+          {{ getPHComment(statistics.avgPH) }}
+        </div>
+      </div>
+    </div>
+
+    <!-- ph分布 -->
+    <div class="distribution-chart">
+      <h4>📈 pH 分布</h4>
+      <div class="bar-chart">
+        <div 
+          v-for="(value, key) in phDistribution" 
+          :key="key"
+          class="bar-item"
+        >
+          <div class="bar-label">{{ getPHRangeLabel(key) }}</div>
+          <div class="bar-container">
+            <div 
+              class="bar-fill" 
+              :style="{ width: `${(value / statistics.totalBlocks) * 100}%` }"
+              :class="
+              {
+               danger: key === 'range1',
+               warning: key === 'range2',
+               success: key === 'range3'
+              }
+              "
+            ></div>
+            <span class="bar-value">{{ value }}</span>
+          </div>
+        </div>
+      </div>
+    </div>
+    
+    <!-- 图例 -->
+    <div class="legend">
+      <h4>pH 值图例</h4>
+      <div class="legend-item">
+        <span class="legend-color" style="background: #ef4444;"></span>
+        <span>≤ 5.2 (强酸性)</span>
+      </div>
+      <div class="legend-item">
+        <span class="legend-color" style="background: #eab308;"></span>
+        <span>5.2 - 6.0 (弱酸性)</span>
+      </div>
+      <div class="legend-item">
+        <span class="legend-color" style="background: #22c55e;"></span>
+        <span>≥ 6.0 (中性/碱性)</span>
+      </div>
+      <div class="legend-item">
+        <span class="legend-color" style="background: #cccccc;"></span>
+        <span>无数据</span>
+      </div>
+    </div>
+
+    <!-- 采样点详情 -->
+    <div class="point-detail-modal" v-if="selectedPoint">
+      <div class="detail-content">
+        <div class="detail-header">
+          <h4>📍 采样点详情</h4>
+          <button @click="selectedPoint = null" class="close-btn">×</button>
+        </div>
+        <div class="detail-row">
+          <span class="detail-label">pH 值:</span>
+          <span class="detail-value" :class="getPHLevelClass(selectedPoint.ph)">
+            {{ selectedPoint.ph?.toFixed(2) || '-' }}
+          </span>
+        </div>
+        <div class="detail-row">
+          <span class="detail-label">酸化程度:</span>
+          <span :class="['detail-value', getPHLevelClass(selectedPoint.ph)]">
+            {{ selectedPoint.ph <= 5.2 ? '强酸性' : selectedPoint.ph < 6.0 ? '弱酸性' : '正常' }}
+          </span>
+        </div>
+        <div class="detail-row">
+          <span class="detail-label">建议:</span>
+          <span class="detail-suggestion">
+            {{ selectedPoint.ph <= 5.2 ? '立即治理,施用石灰改良' : selectedPoint.ph < 6.0 ? '注意保持,适量施用有机肥' : '继续保持当前管理措施' }}
+          </span>
+        </div>
+      </div>
+    </div>
+
+   </div>
+</template>
+
+<style  scoped>
+.map-container{
+  width: 100%;
+  height: 100vh;
+  position: absolute;
+  left:0;
+  top:0;
+  z-index: 1000;
+}
+
+.ph-map{
+  width: 65%;
+  height: 95%;
+  min-height: 500px;
+  border-radius: 16px; /* 圆角大小,可根据需要调整,比如 10px、20px */
+  border: 3px solid #1092d8; /* 蓝色边框,宽度和颜色可自定义 */
+  overflow: hidden; /* 关键:防止地图内容溢出圆角区域 */
+}
+
+/* ✅ 统计面板样式 */
+.statistics-panel {
+  position: absolute;
+  top: 100px;
+  right: 10px;
+  background: rgba(255, 255, 255, 0.95);
+  padding: 20px;
+  border-radius: 12px;
+  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
+  z-index: 1000;
+  min-width: 350px;
+  backdrop-filter: blur(10px);
+  border: 2px solid rgba(16, 146, 216, 0.2);
+}
+
+.compute {
+  position: absolute;
+  display: flex;
+  gap: 15px;
+  top:5px;
+  right: 20px;
+  padding: 20px;
+  z-index: 1000;
+}
+
+.combtn {
+  padding: 15px 20px;
+  font-size: 14px;
+  font-weight: 600;
+  color: #fff;
+  background: linear-gradient(135deg, #1092d8 0%, #0d7bb8 100%);
+  border: none;
+  border-radius: 8px;
+  cursor: pointer;
+  box-shadow: 0 4px 12px rgba(16, 146, 216, 0.3);
+  transition: all 0.3s ease;
+  white-space: nowrap;
+}
+
+.combtn:hover {
+  background: linear-gradient(135deg, #0d7bb8 0%, #0a6598 100%);
+  box-shadow: 0 6px 16px rgba(16, 146, 216, 0.4);
+  transform: translateY(-2px);
+}
+
+.combtn:active {
+  transform: translateY(0);
+  box-shadow: 0 2px 8px rgba(16, 146, 216, 0.3);
+}
+
+
+.statistics-panel h4 {
+  margin: 0 0 15px 0;
+  font-size: 18px;
+  color: #1092d8;
+  border-bottom: 2px solid #1092d8;
+  padding-bottom: 10px;
+  font-weight: bold;
+}
+
+.stat-row {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 12px;
+  font-size: 14px;
+  line-height: 1.4; /* 新增行高,提升可读性 */
+}
+
+.stat-row.highlight {
+  background: linear-gradient(to right, rgba(16, 146, 216, 0.1), transparent);
+  padding: 8px 12px;
+  border-radius: 6px;
+  margin-bottom: 15px;
+}
+
+.stat-row.small {
+  font-size: 12px;
+  margin-bottom: 6px;
+}
+
+.stat-label {
+  color: #666;
+  font-weight: 500;
+}
+
+.stat-value {
+  font-weight: 600;
+  font-size: 16px;
+  color: #333;
+  letter-spacing: 0.5px; /* 新增字间距 */
+}
+
+.stat-value.danger {
+  color: #ef4444;
+}
+
+.stat-value.warning {
+  color: #f59e0b;
+}
+
+.stat-value.success {
+  color: #22c55e;
+}
+
+.stat-divider {
+  height: 1px;
+  background: linear-gradient(to right, transparent, #ddd, transparent);
+  margin: 15px 0;
+}
+
+/* 综合评语 */
+.stat-comment {
+  margin-top: 15px;
+  padding: 12px;
+  background: rgba(243, 244, 246, 0.8);
+  border-radius: 8px;
+  border-left: 4px solid #1092d8;
+}
+
+.comment-title {
+  font-size: 12px;
+  color: #666;
+  margin-bottom: 6px;
+  font-weight: bold;
+}
+
+.comment-text {
+  font-size: 14px;
+  font-weight: bold;
+  color: #333;
+}
+
+.comment-text.danger {
+  color: #ef4444;
+}
+
+.comment-text.warning {
+  color: #f59e0b;
+}
+
+.comment-text.success {
+  color: #22c55e;
+}
+
+.distribution-chart {
+  position: absolute;
+  top:600px;
+  right: 10px;
+  background: rgba(255, 255, 255, 0.95);
+  padding: 15px;
+  border-radius: 12px;
+  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
+  z-index: 1000;
+  min-width: 350px;
+  backdrop-filter: blur(10px);
+}
+
+.distribution-chart h4 {
+  margin: 0 0 12px 0;
+  font-size: 16px;
+  color: #1092d8;
+  border-bottom: 2px solid #1092d8;
+  padding-bottom: 8px;
+}
+
+.bar-chart {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.bar-item {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.bar-label {
+  font-size: 11px;
+  color: #666;
+  min-width: 60px;
+}
+
+.bar-container {
+  flex: 1;
+  position: relative;
+  height: 20px;
+  background: #f0f0f0;
+  border-radius: 4px;
+  overflow: hidden;
+  border: 1px solid #e5e7eb;
+}
+
+.bar-fill {
+  height: 100%;
+  transition: width 0.3s ease-in-out; /* 缓动动画更丝滑 */
+  border-radius: 3px; /* 圆角匹配容器 */
+}
+
+.bar-fill.danger {
+  background: #ef4444;
+}
+
+.bar-fill.warning {
+  background: #f59e0b;
+}
+
+.bar-fill.success {
+  background: #22c55e;
+}
+
+.bar-value {
+  position: absolute;
+  right: 8px;
+  top: 50%;
+  transform: translateY(-50%);
+  font-size: 11px;
+  font-weight: bold;
+  color: black; 
+  text-shadow: 0 1px 2px rgba(0,0,0,0.2); /* 新增文字阴影 */
+}
+
+.alert-panel {
+  position: absolute;
+  top: 20px;
+  left: 10px;
+  background: rgba(255, 255, 255, 0.95);
+  padding: 15px;
+  border-radius: 12px;
+  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
+  z-index: 1000;
+  min-width: 250px;
+  max-height: 300px;
+  overflow-y: auto;
+  backdrop-filter: blur(10px);
+}
+
+.alert-panel h4 {
+  margin: 0 0 12px 0;
+  font-size: 16px;
+  color: #ef4444;
+  border-bottom: 2px solid #ef4444;
+  padding-bottom: 8px;
+}
+
+.alert-list {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.alert-item {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 8px;
+  background: rgba(239, 68, 68, 0.05);
+  border-radius: 6px;
+  cursor: pointer;
+  transition: all 0.3s;
+}
+
+.alert-item:hover {
+  background: rgba(239, 68, 68, 0.15);
+  transform: translateX(4px);
+}
+
+.alert-rank {
+  background: #ef4444;
+  color: white;
+  width: 20px;
+  height: 20px;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 12px;
+  font-weight: bold;
+  flex-shrink: 0;
+}
+
+.alert-name {
+  flex: 1;
+  font-size: 13px;
+  color: #333;
+}
+
+.alert-ph {
+  font-size: 12px;
+  font-weight: bold;
+}
+
+.alert-ph.danger {
+  color: #ef4444;
+}
+
+/* 图例样式 */
+.legend {
+  position: absolute;
+  top: 30px;
+  left: 30px;
+  background: rgba(255, 255, 255, 0.95);
+  padding: 15px;
+  border-radius: 8px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+  z-index: 1000;
+  min-width: 180px;
+  backdrop-filter: blur(4px);
+  z-index: 0;
+}
+
+.legend h4 {
+  margin: 0 0 10px 0;
+  font-size: 14px;
+  color: #333;
+  border-bottom: 2px solid #1092d8;
+  padding-bottom: 6px;
+}
+
+.legend-item {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  margin-bottom: 6px;
+  font-size: 12px;
+  color: #555;
+}
+
+.legend-item:last-child {
+  margin-bottom: 0;
+}
+
+.legend-color {
+  width: 20px;
+  height: 20px;
+  border-radius: 4px;
+  border: 1px solid #ddd;
+  flex-shrink: 0;
+}
+
+.point-detail-modal {
+  position: fixed;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  background: white;
+  padding: 20px;
+  border-radius: 12px;
+  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
+  z-index: 2000;
+  min-width: 300px;
+  border: 1px solid rgba(16, 146, 216, 0.2); /* 新增边框 */
+  animation: fadeIn 0.3s ease; /* 新增淡入动画 */
+}
+
+/* 新增弹窗动画 */
+@keyframes fadeIn {
+  from {
+    opacity: 0;
+    transform: translate(-50%, -45%);
+  }
+  to {
+    opacity: 1;
+    transform: translate(-50%, -50%);
+  }
+}
+
+.detail-content {
+  position: relative;
+}
+
+.detail-content h4 {
+  margin: 0 0 15px 0;
+  color: #1092d8;
+  border-bottom: 2px solid #1092d8;
+  padding-bottom: 10px;
+}
+
+.detail-row {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 12px;
+  padding: 8px;
+  background: #f8f9fa;
+  border-radius: 6px;
+}
+
+.detail-label {
+  font-weight: 500;
+  color: #666;
+}
+
+.detail-value {
+  font-weight: bold;
+  color: #333;
+}
+
+.detail-value.danger {
+  color: #ef4444;
+}
+
+.detail-value.warning {
+  color: #f59e0b;
+}
+
+.detail-value.success {
+  color: #22c55e;
+}
+
+.detail-suggestion {
+  color: #1092d8;
+  font-size: 13px;
+  line-height: 1.5;
+}
+
+/* 自定义 Tooltip 样式 */
+:deep(.custom-tooltip) {
+  background: rgba(255, 255, 255, 0.95);
+  border: 2px solid #1092d8;
+  border-radius: 8px;
+  padding: 8px 12px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+  font-family: 'Microsoft YaHei', sans-serif;
+  backdrop-filter: blur(4px);
+}
+
+.custom-marker {
+  background: transparent !important;
+  border: none !important;
+}
+
+.detail-content {
+  position: relative;
+}
+
+.detail-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 15px;
+}
+
+
+.close-btn {
+  background: none;
+  border: none;
+  font-size: 20px;
+  cursor: pointer;
+  color: #909399;
+  padding: 0;
+  width: 20px;
+  height: 20px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border-radius: 50%; /* 新增圆形背景 */
+  transition: all 0.2s; /* 新增过渡 */
+}
+
+.close-btn:hover {
+  color: #ef4444; /* 改为红色更醒目 */
+  background: #fef2f2; /* 新增背景色 */
+}
+</style>