Browse Source

农业投入品,地下渗漏和地表径流绘图,输出总通量

yangtaodemon 2 days ago
parent
commit
a76aa10b44

+ 1 - 10
components.d.ts

@@ -23,14 +23,12 @@ declare module 'vue' {
     CrossSetionData1: typeof import('./src/components/irrpollution/crossSetionData1.vue')['default']
     CrossSetionData2: typeof import('./src/components/irrpollution/crossSetionData2.vue')['default']
     CrossSetionTencentmap: typeof import('./src/components/irrpollution/crossSetionTencentmap.vue')['default']
-    ElAlert: typeof import('element-plus/es')['ElAlert']
     ElAside: typeof import('element-plus/es')['ElAside']
     ElAvatar: typeof import('element-plus/es')['ElAvatar']
     ElButton: typeof import('element-plus/es')['ElButton']
     ElCard: typeof import('element-plus/es')['ElCard']
     ElCol: typeof import('element-plus/es')['ElCol']
     ElContainer: typeof import('element-plus/es')['ElContainer']
-    ElDialog: typeof import('element-plus/es')['ElDialog']
     ElDropdown: typeof import('element-plus/es')['ElDropdown']
     ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
     ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
@@ -43,21 +41,17 @@ declare module 'vue' {
     ElMain: typeof import('element-plus/es')['ElMain']
     ElMenu: typeof import('element-plus/es')['ElMenu']
     ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
-    ElOption: typeof import('element-plus/es')['ElOption']
-    ElPagination: typeof import('element-plus/es')['ElPagination']
+    ElProgress: typeof import('element-plus/es')['ElProgress']
     ElRadio: typeof import('element-plus/es')['ElRadio']
     ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
     ElRow: typeof import('element-plus/es')['ElRow']
     ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
-    ElSelect: typeof import('element-plus/es')['ElSelect']
     ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
     ElTable: typeof import('element-plus/es')['ElTable']
     ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
     ElTabPane: typeof import('element-plus/es')['ElTabPane']
     ElTabs: typeof import('element-plus/es')['ElTabs']
     ElTag: typeof import('element-plus/es')['ElTag']
-    ElTooltip: typeof import('element-plus/es')['ElTooltip']
-    ElUpload: typeof import('element-plus/es')['ElUpload']
     HeavyMetalEnterprisechart: typeof import('./src/components/atmpollution/heavyMetalEnterprisechart.vue')['default']
     HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
     IconCommunity: typeof import('./src/components/icons/IconCommunity.vue')['default']
@@ -79,7 +73,4 @@ declare module 'vue' {
     Waterdataline: typeof import('./src/components/irrpollution/waterdataline.vue')['default']
     WelcomeItem: typeof import('./src/components/WelcomeItem.vue')['default']
   }
-  export interface ComponentCustomProperties {
-    vLoading: typeof import('element-plus/es')['ElLoadingDirective']
-  }
 }

+ 163 - 58
src/views/User/HmOutFlux/agriInput/prodInputFlux.vue

@@ -6,8 +6,8 @@
         <div class="input-section">
           <el-form label-width="250px" label-position="top">
             <div class="form-section">
-                      <div class="input-group">
-                <el-form-item label="氮肥镉含量平均值 (mg/kg)" class="form-item">
+              <div class="input-group">
+                <el-form-item label="氮肥镉含量平均值 (mg/kg)" class="form-item">
                   <el-input v-model="formData.f3_nitrogen_cd_content" placeholder="0.05"></el-input>
                 </el-form-item>
                 <el-form-item label="氮肥单位面积使用量 (t/ha/a)" class="form-item">
@@ -15,7 +15,7 @@
                 </el-form-item>
               </div>
               <div class="input-group">
-                <el-form-item label="磷肥镉含量平均值 (mg/kg)" class="form-item">
+                <el-form-item label="磷肥镉含量平均值 (mg/kg)" class="form-item">
                   <el-input v-model="formData.f4_phosphorus_cd_content" placeholder="0.158"></el-input>
                 </el-form-item>
                 <el-form-item label="磷肥单位面积使用量 (t/ha/a)" class="form-item">
@@ -23,7 +23,7 @@
                 </el-form-item>
               </div>
               <div class="input-group">
-                <el-form-item label="钾肥镉含量平均值 (mg/kg)" class="form-item">
+                <el-form-item label="钾肥镉含量平均值 (mg/kg)" class="form-item">
                   <el-input v-model="formData.f5_potassium_cd_content" placeholder="0.06"></el-input>
                 </el-form-item>
                 <el-form-item label="钾肥单位面积使用量 (t/ha/a)" class="form-item">
@@ -31,7 +31,7 @@
                 </el-form-item>
               </div>
               <div class="input-group">
-                <el-form-item label="复合肥镉含量平均值 (mg/kg)" class="form-item">
+                <el-form-item label="复合肥镉含量平均值 (mg/kg)" class="form-item">
                   <el-input v-model="formData.f6_compound_cd_content" placeholder="0.065"></el-input>
                 </el-form-item>
                 <el-form-item label="复合肥单位面积使用量 (t/ha/a)" class="form-item">
@@ -39,7 +39,7 @@
                 </el-form-item>
               </div>
               <div class="input-group">
-                <el-form-item label="有机肥镉含量平均值 (mg/kg)" class="form-item">
+                <el-form-item label="有机肥镉含量平均值 (mg/kg)" class="form-item">
                   <el-input v-model="formData.f7_organic_cd_content" placeholder="0.6"></el-input>
                 </el-form-item>
                 <el-form-item label="有机肥单位面积使用量 (t/ha/a)" class="form-item">
@@ -47,7 +47,7 @@
                 </el-form-item>
               </div>
               <div class="input-group">
-                <el-form-item label="农药镉含量 (mg/kg)" class="form-item">
+                <el-form-item label="农药镉含量 (mg/kg)" class="form-item">
                   <el-input v-model="formData.f8_pesticide_cd_content" placeholder="0.25"></el-input>
                 </el-form-item>
                 <el-form-item label="农药单位面积使用量 (t/ha/a)" class="form-item">
@@ -55,7 +55,7 @@
                 </el-form-item>
               </div>
               <div class="input-group">
-                <el-form-item label="农家肥镉含量 (mg/kg)" class="form-item">
+                <el-form-item label="农家肥镉含量 (mg/kg)" class="form-item">
                   <el-input v-model="formData.f9_farmyard_cd_content" placeholder="0.35"></el-input>
                 </el-form-item>
                 <el-form-item label="农家肥单位面积使用量 (t/ha/a)" class="form-item">
@@ -63,7 +63,7 @@
                 </el-form-item>
               </div>
               <div class="input-group">
-                <el-form-item label="农膜镉含量 (mg/kg)" class="form-item">
+                <el-form-item label="农膜镉含量 (mg/kg)" class="form-item">
                   <el-input v-model="formData.f10_film_cd_content" placeholder="0.25"></el-input>
                 </el-form-item>
                 <el-form-item label="农膜(存留)单位面积使用量 (t/ha/a)" class="form-item">
@@ -112,22 +112,20 @@
         <div class="chart-container">
             <div ref="customPieChart" style="width: 100%; height: 400px;"></div>
           </div>
-      </el-card>
-      
-      <!-- 所有地区统计结果卡片 -->
-      <el-card class="result-card" v-if="allAreasResult.success">
-        <h3>所有地区农业投入Cd通量统计结果</h3>
-        <p>平均通量: {{ allAreasResult.data.summary.average_cd_flux }} g/ha/a</p>
-        <p>最高通量: {{ allAreasResult.data.summary.max_cd_flux.total_cd_flux }} g/ha/a ({{ allAreasResult.data.summary.max_cd_flux.area }})</p>
-        <p>最低通量: {{ allAreasResult.data.summary.min_cd_flux.total_cd_flux }} g/ha/a ({{ allAreasResult.data.summary.min_cd_flux.area }})</p>
-        
-        <el-table :data="allAreasList" border>
-          <el-table-column prop="area" label="地区"></el-table-column>
-          <el-table-column prop="total_cd_flux" label="Cd通量(g/ha/a)"></el-table-column>
-        </el-table>
-        <div class="chart-container">
-            <div ref="allAreasPieChart" style="width: 100%; height: 400px;"></div>
+          
+        <!-- 添加地图展示区域 -->
+        <div class="map-container">
+          <h3>空间分布图</h3>
+          <div class="map-wrapper">
+            <div v-if="mapImageUrl" class="map-image-container">
+              <img :src=mapImageUrl alt="农业投入品输入通量分布图" class="map-image">
+            </div>
+            <div v-else class="map-placeholder">
+              <p>地图生成中...</p>
+              <el-progress :percentage="mapProgress" :status="mapStatus"></el-progress>
+            </div>
           </div>
+        </div>
       </el-card>
     </div>
   </div>
@@ -136,6 +134,7 @@
 <script>
 import axios from 'axios';
 import * as echarts from 'echarts'; 
+import { ElNotification } from 'element-plus';
 
 export default {
   data() {
@@ -171,7 +170,13 @@ export default {
 
       // ECharts实例
       customPieChart: null,
-      allAreasPieChart: null
+      allAreasPieChart: null,
+      
+      // 地图相关数据
+      mapImageUrl: null,
+      mapProgress: 0,
+      mapStatus: 'success',
+      mapInterval: null
     };
   },
   computed: {
@@ -244,14 +249,11 @@ export default {
         };
         
         // 同时调用两个API
-        const [customResponse, allAreasResponse] = await Promise.all([
+        const [customResponse] = await Promise.all([
           axios.post(
             'http://localhost:8000/api/agricultural-input/calculate-with-custom-data', 
             requestBody
           ),
-          axios.get(
-            'http://localhost:8000/api/agricultural-input/calculate-all-areas'
-          )
         ]);
         
         // 处理自定义数据结果
@@ -260,18 +262,15 @@ export default {
           this.$message.error(this.customResult.message || '自定义数据计算失败');
         }
         
-        // 处理所有地区结果
-        this.allAreasResult = allAreasResponse.data;
-        if (!this.allAreasResult.success) {
-          this.$message.error(this.allAreasResult.message || '计算所有地区失败');
-        }
-        
         // 切换到结果页面
         this.showInputForm = false;
 
-         // 等待DOM更新
+        // 等待DOM更新
         this.$nextTick(() => {
           this.initCharts();
+          
+          // 调用地图生成方法
+          this.generateMap();
         });
       } catch (error) {
         console.error('API调用错误:', error);
@@ -295,7 +294,8 @@ export default {
       };
       return typeNames[type] || type;
     },
-     // 初始化图表
+    
+    // 初始化图表
     initCharts() {
       // 销毁已有实例
       if (this.customPieChart) {
@@ -307,14 +307,9 @@ export default {
       
       // 创建新的图表实例
       this.customPieChart = echarts.init(this.$refs.customPieChart);
-      this.allAreasPieChart = echarts.init(this.$refs.allAreasPieChart);
       
       // 设置图表选项
       this.customPieChart.setOption(this.getPieChartOption('当前地区各项投入通量占比', this.customPieData));
-      this.allAreasPieChart.setOption(this.getPieChartOption('各地区通量占比', this.allAreasPieData));
-      
-      // 响应窗口大小变化
-      window.addEventListener('resize', this.onResize);
     },
     
     // 获取饼图配置
@@ -376,9 +371,68 @@ export default {
       if (this.customPieChart) {
         this.customPieChart.resize();
       }
-      if (this.allAreasPieChart) {
-        this.allAreasPieChart.resize();
+    },
+    
+    // 新增:生成地图的方法
+    async generateMap() {
+      try {
+        // 重置地图状态
+        this.mapImageUrl = null;
+        this.mapProgress = 0;
+        this.mapStatus = 'success';
+        
+        // 启动进度条模拟
+        this.startProgressSimulation();
+        
+        // 调用后端地图生成接口
+        const response = await axios.get(
+          'http://localhost:8000/api/agricultural-input/visualize',
+          {
+            params: {
+              area: '韶关', // 使用固定地区
+              level: 'county' // 使用县级行政层级
+            },
+            responseType: 'blob' // 重要:指定响应类型为blob
+          }
+        );
+        
+        // 停止进度条模拟
+        this.stopProgressSimulation();
+        
+        // 创建Blob URL
+        const blob = new Blob([response.data], { type: 'image/jpeg' });
+        this.mapImageUrl = URL.createObjectURL(blob);
+        
+      } catch (error) {
+        console.error('地图生成失败:', error);
+        this.stopProgressSimulation();
+        this.mapStatus = 'exception';
+        ElNotification.error({
+          title: '地图生成失败',
+          message: '无法生成空间分布图,请稍后再试',
+          duration: 5000
+        });
+      }
+    },
+    
+    // 启动进度条模拟
+    startProgressSimulation() {
+      this.mapInterval = setInterval(() => {
+        if (this.mapProgress < 90) {
+          this.mapProgress += 10;
+        } else if (this.mapProgress < 99) {
+          this.mapProgress += 1;
+        }
+      }, 500);
+    },
+    
+    // 停止进度条模拟
+    stopProgressSimulation() {
+      if (this.mapInterval) {
+        clearInterval(this.mapInterval);
+        this.mapInterval = null;
       }
+      this.mapProgress = 100;
     }
   },
   watch: {
@@ -387,11 +441,6 @@ export default {
       if (this.customPieChart) {
         this.customPieChart.setOption(this.getPieChartOption('当前地区各项投入通量占比', this.customPieData));
       }
-    },
-    allAreasPieData() {
-      if (this.allAreasPieChart) {
-        this.allAreasPieChart.setOption(this.getPieChartOption('各地区通量占比', this.allAreasPieData));
-      }
     }
   },
   beforeUnmount() {
@@ -402,8 +451,11 @@ export default {
     if (this.customPieChart) {
       this.customPieChart.dispose();
     }
-    if (this.allAreasPieChart) {
-      this.allAreasPieChart.dispose();
+    
+    // 清理地图相关资源
+    this.stopProgressSimulation();
+    if (this.mapImageUrl) {
+      URL.revokeObjectURL(this.mapImageUrl);
     }
   }
 };
@@ -424,7 +476,7 @@ export default {
   max-width: 1200px;
   margin: 0 auto;
   background: linear-gradient(135deg, #FAFDFF, #FFFAA2);
-  border: 1px solid #e6e6e6;
+  border: 1极端的 solid #e6e6e6;
   border-radius: 12px;
   overflow: hidden;
   box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
@@ -577,6 +629,7 @@ export default {
   background: linear-gradient(135deg, #FAFDFF, #FFFAA2);
   border-radius: 12px;
   box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+  position: relative;
 }
 
 .results-title {
@@ -588,14 +641,12 @@ export default {
 
 /* 结果卡片样式 */
 .result-card {
-  width: 90%;
-  max-width: 1200px;
+  width: 100%;
   margin: 0 auto;
-  background: linear-gradient(135deg, #FAFDFF, #FFFAA2);
-  border: 1px solid #e6e6e6;
+  padding: 20px;
+  background: white;
   border-radius: 12px;
-  overflow: hidden;
-  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
 }
 
 .result-card h3 {
@@ -607,6 +658,9 @@ export default {
 .result-card p {
   margin: 10px 0;
   font-size: 16px;
+  text-align: center;
+  font-weight: bold;
+  color: #26B046;
 }
 
 /* 表格样式 */
@@ -629,6 +683,57 @@ export default {
   background: #f9f9f9;
 }
 
+/* 地图容器样式 */
+.map-container {
+  margin-top: 30px;
+  padding: 20px;
+  background-color: #f9f9f9;
+  border-radius: 8px;
+  border: 1px solid #eee;
+}
+
+.map-container h3 {
+  text-align: center;
+  margin-bottom: 15px;
+  color: #333;
+}
+
+.map-wrapper {
+  position: relative;
+  min-height: 400px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background-color: white;
+  border-radius: 6px;
+  overflow: hidden;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.map-image-container {
+  width: 100%;
+  display: flex;
+  justify-content: center;
+}
+
+.map-image {
+  max-width: 100%;
+  max-height: 500px;
+  object-fit: contain;
+}
+
+.map-placeholder {
+  text-align: center;
+  padding: 20px;
+  width: 80%;
+}
+
+.map-placeholder p {
+  margin-bottom: 15px;
+  font-size: 16px;
+  color: #666;
+}
+
 /* 返回按钮样式 */
 .back-button {
   position: absolute;

+ 5 - 7
src/views/User/cadmiumPrediction/totalInputFlux.vue

@@ -190,7 +190,7 @@ export default {
     async fetchLatestMap() {
       try {
         const response = await axios.get(
-          `http://localhost:8000/api/cd-flux/map`,
+          `http://localhost:8000/api/cd-flux/input/map`,
           { responseType: 'blob' }
         );
         
@@ -206,7 +206,7 @@ export default {
     async fetchLatestHistogram() {
       try {
         const response = await axios.get(
-          `http://localhost:8000/api/cd-flux/histogram`,
+          `http://localhost:8000/api/cd-flux/input/histogram`,
           { responseType: 'blob' }
         );
         
@@ -227,8 +227,6 @@ export default {
         { name: '最大值', value: stats.max.toFixed(4), unit: 'g/ha/year', description: '样本中的最大Cd通量' },
         { name: '平均值', value: stats.mean.toFixed(4), unit: 'g/ha/year', description: '所有样本的平均Cd通量' },
         { name: '标准差', value: stats.std.toFixed(4), unit: 'g/ha/year', description: 'Cd通量的标准差' },
-        { name: '有效像元数', value: stats.valid_pixels, unit: '个', description: '有效数据点的数量' },
-        { name: '总像元数', value: stats.total_pixels, unit: '个', description: '总像元数量' }
       ];
     },
 
@@ -238,7 +236,7 @@ export default {
         this.loadingStats = true;
         
         const response = await axios.get(
-          `http://localhost:8000/api/cd-flux/statistics`
+          `http://localhost:8000/api/cd-flux/input/statistics`
         );
         
         if (response.data) {
@@ -282,7 +280,7 @@ export default {
         
         // 调用Cd通量计算接口
         await axios.post(
-          'http://localhost:8000/api/cd-flux/calculate',
+          'http://localhost:8000/api/cd-flux/input/calculate',
           formData,
           {
             headers: {
@@ -352,7 +350,7 @@ export default {
         this.$message.info('正在获取Cd输入通量数据...');
         
         const response = await axios.get(
-          `http://localhost:8000/api/cd-flux/export-csv`,
+          `http://localhost:8000/api/cd-flux/input/export-csv`,
           { responseType: 'blob' }
         );
         

+ 498 - 113
src/views/User/cadmiumPrediction/totalOutputFlux.vue

@@ -1,147 +1,532 @@
 <template>
-  <div class="cd-input-output-container">
-    <el-card class="gradient-card" shadow="hover">
-      <el-row :gutter="20">
-        <el-col :span="12">
-          <p class="label">地表径流 (g/ha/a) 输出</p>
-          <el-input
-            v-model="runoffOutput"
-            placeholder="请输入内容"
-            class="custom-input"
-            readonly
-          />
-        </el-col>
-        <el-col :span="12">
-          <p class="label">籽粒移除 (g/ha/a) 输出</p>
-          <el-input
-            v-model="grainRemovalOutput"
-            placeholder="请输入内容"
-            class="custom-input"
-            readonly
-          />
-        </el-col>
-        <el-col :span="12" style="margin-top: 20px;">
-          <p class="label">地下渗漏 (g/ha/a) 输出</p>
-          <el-input
-            v-model="leakageOutput"
-            placeholder="请输入内容"
-            class="custom-input"
-            readonly
-          />
-        </el-col>
-        <el-col :span="12" style="margin-top: 20px;">
-          <p class="label">籽粒移除 (g/ha/a) 输出</p>
-          <el-input
-            v-model="anotherGrainRemovalOutput"
-            placeholder="请输入内容"
-            class="custom-input"
-            readonly
-          />
-        </el-col>
-        <el-col :span="24" style="margin-top: 20px;">
-          <el-button class="calculate-btn" @click="onCalculate">计算</el-button>
-        </el-col>
-      </el-row>
-    </el-card>
+  <div class="container">
+    <!-- 顶部操作栏 -->
+    <div class="toolbar">
+      <!-- 文件上传区域 -->
+      <div class="upload-section">
+        <input type="file" ref="fileInput" accept=".csv" @change="handleFileUpload" style="display: none">
+        <el-button class="custom-button" @click="triggerFileUpload">
+          <el-icon class="upload-icon"><Upload /></el-icon>
+          选择CSV文件
+        </el-button>
+        <span v-if="selectedFile" class="file-name">{{ selectedFile.name }}</span>
+        <el-button 
+          class="custom-button" 
+          :loading="isCalculating" 
+          :disabled="!selectedFile" 
+          @click="calculate"
+        >
+        <el-icon class="upload-icon"><Document /></el-icon>  
+        上传并计算
+        </el-button>
+      </div>
+      <!-- 操作按钮 -->
+      <div class="action-buttons">
+        <el-button class="custom-button" :disabled="!mapBlob" @click="exportMap">
+          <el-icon class="upload-icon"><Download /></el-icon>
+          导出地图</el-button>
+        <el-button class="custom-button" :disabled="!histogramBlob" @click="exportHistogram">
+          <el-icon class="upload-icon"><Download /></el-icon>
+          导出直方图</el-button>
+        <el-button class="custom-button" :disabled="!statisticsData.length" @click="exportData">
+          <el-icon class="upload-icon"><Download /></el-icon>
+          导出数据</el-button>
+      </div>
+    </div>
+
+    <!-- 主体内容区 -->
+    <div class="content-area">
+      <!-- 地图区域 - 修改为横向布局 -->
+      <div class="horizontal-container">
+        <!-- 地图展示 -->
+        <div class="map-section">
+          <h3>Cd输出通量空间分布图</h3>
+          <div v-if="loadingMap" class="loading-container">
+            <el-icon class="loading-icon"><Loading /></el-icon>
+            <span>地图加载中...</span>
+          </div>
+          <img v-if="mapImageUrl && !loadingMap" :src="mapImageUrl" alt="Cd输出通量空间分布图" class="map-image">
+          <div v-if="!mapImageUrl && !loadingMap" class="no-data">
+            <el-icon><Picture /></el-icon>
+            <p>暂无地图数据</p>
+          </div>
+        </div>
+        
+        <!-- 直方图展示 -->
+        <div class="histogram-section">
+          <h3>Cd输出通量直方图</h3>
+          <div v-if="loadingHistogram" class="loading-container">
+            <el-icon class="loading-icon"><Loading /></el-icon>
+            <span>直方图加载中...</span>
+          </div>
+          <img v-if="histogramImageUrl && !loadingHistogram" :src="histogramImageUrl" alt="Cd输出通量直方图" class="histogram-image">
+          <div v-if="!histogramImageUrl && !loadingHistogram" class="no-data">
+            <el-icon><Histogram /></el-icon>
+            <p>暂无直方图数据</p>
+          </div>
+        </div>
+      </div>
+
+      <!-- 统计图表区域 -->
+      <div class="stats-area">
+        <h3>Cd输出通量统计信息</h3>
+        <div class="model-info">
+          <el-tag type="info">Cd通量模型</el-tag>
+          <span class="update-time">
+            最后更新: {{ updateTime ? new Date(updateTime).toLocaleString() : '未知' }}
+          </span>
+        </div>
+        
+        <div v-if="loadingStats" class="loading-container">
+          <el-icon class="loading-icon"><Loading /></el-icon>
+          <span>统计数据加载中...</span>
+        </div>
+        
+        <div v-if="!loadingStats && statisticsData.length" class="stats-container">
+          <!-- 统计表格 -->
+         <el-table 
+            :data="statisticsData" 
+            style="width: 100%; margin-bottom: 20px;"
+            border
+            stripe
+          >
+            <el-table-column prop="name" label="统计项" min-width="180" />
+            <el-table-column prop="value" label="值" min-width="150" />
+            <el-table-column prop="unit" label="单位" min-width="100" />
+            <el-table-column prop="description" label="描述" min-width="200" />
+          </el-table>
+          
+        
+        <div v-if="!loadingStats && !statisticsData.length" class="no-data">
+          <el-icon><DataAnalysis /></el-icon>
+          <p>暂无统计数据</p>
+        </div>
+        </div>
+      </div>
+    </div>
   </div>
 </template>
 
-<script setup>
-import { ref } from 'vue';
-import { ElCard, ElRow, ElCol, ElInput, ElButton } from 'element-plus';
+<script>
+import * as XLSX from 'xlsx';
+import { saveAs } from 'file-saver';
+import axios from 'axios';
+import * as echarts from 'echarts';
+import { 
+  Loading, Upload, Picture, Histogram, Download, Document, DataAnalysis 
+} from '@element-plus/icons-vue';
+
+export default {
+  name: 'CdFluxVisualization',
+  components: { 
+    Loading, Upload, Picture, Histogram, Download, Document, DataAnalysis 
+  },
+  data() {
+    return {
+      isCalculating: false,
+      loadingMap: false,
+      loadingHistogram: false,
+      loadingStats: false,
+      statisticsData: [],
+      mapImageUrl: null,
+      histogramImageUrl: null,
+      mapBlob: null,
+      histogramBlob: null,
+      selectedFile: null,
+      distributionChart: null,
+      updateTime: null
+    };
+  },
+
+  mounted() {
+    // 组件挂载时获取最新数据
+    this.fetchLatestResults();
+    this.fetchStatistics();
+  },
 
-const runoffOutput = ref('');
-const grainRemovalOutput = ref('');
-const leakageOutput = ref('');
-const anotherGrainRemovalOutput = ref('');
+  beforeDestroy() {
+    if (this.mapImageUrl) URL.revokeObjectURL(this.mapImageUrl);
+    if (this.histogramImageUrl) URL.revokeObjectURL(this.histogramImageUrl);
+    if (this.distributionChart) this.distributionChart.dispose();
+  },
+  methods: {
+    // 触发文件选择
+    triggerFileUpload() {
+      this.$refs.fileInput.click();
+    },
+    
+    // 处理文件上传
+    handleFileUpload(event) {
+      const files = event.target.files;
+      if (files && files.length > 0) {
+        this.selectedFile = files[0];
+      } else {
+        this.selectedFile = null;
+      }
+    },
+    
+    // 获取最新结果
+    async fetchLatestResults() {
+      try {
+        this.loadingMap = true;
+        this.loadingHistogram = true;
+        
+        // 获取最新地图
+        await this.fetchLatestMap();
+        
+        // 获取最新直方图
+        await this.fetchLatestHistogram();
+        
+      } catch (error) {
+        console.error('获取最新结果失败:', error);
+        this.$message.error('获取最新结果失败');
+      } finally {
+        this.loadingMap = false;
+        this.loadingHistogram = false;
+      }
+    },
+    
+    // 获取最新地图
+    async fetchLatestMap() {
+      try {
+        const response = await axios.get(
+          `http://localhost:8000/api/cd-flux/output/map`,
+          { responseType: 'blob' }
+        );
+        
+        this.mapBlob = response.data;
+        this.mapImageUrl = URL.createObjectURL(this.mapBlob);
+      } catch (error) {
+        console.error('获取最新地图失败:', error);
+        this.$message.warning('获取最新地图失败,请先执行预测');
+      }
+    },
+    
+    // 获取最新直方图
+    async fetchLatestHistogram() {
+      try {
+        const response = await axios.get(
+          `http://localhost:8000/api/cd-flux/output/histogram`,
+          { responseType: 'blob' }
+        );
+        
+        this.histogramBlob = response.data;
+        this.histogramImageUrl = URL.createObjectURL(this.histogramBlob);
+      } catch (error) {
+        console.error('获取最新直方图失败:', error);
+        this.$message.warning('获取最新直方图失败,请先执行预测');
+      }
+    },
+    
+    // 格式化统计数据
+    formatStatisticsData(stats) {
+      if (!stats) return [];
+      
+      return [
+        { name: '最小值', value: stats.min.toFixed(4), unit: 'g/ha/year', description: '样本中的最小Cd通量' },
+        { name: '最大值', value: stats.max.toFixed(4), unit: 'g/ha/year', description: '样本中的最大Cd通量' },
+        { name: '平均值', value: stats.mean.toFixed(4), unit: 'g/ha/year', description: '所有样本的平均Cd通量' },
+        { name: '标准差', value: stats.std.toFixed(4), unit: 'g/ha/year', description: 'Cd通量的标准差' },
+      ];
+    },
 
-const onCalculate = () => {
-  // 暂无计算逻辑,仅作展示
-  alert('计算按钮已点击');
+    // 修改fetchStatistics方法
+    async fetchStatistics() {
+      try {
+        this.loadingStats = true;
+        
+        const response = await axios.get(
+          `http://localhost:8000/api/cd-flux/output/statistics`
+        );
+        
+        if (response.data) {
+          const stats = response.data;
+          this.statisticsData = this.formatStatisticsData(stats);
+          this.updateTime = new Date().toISOString();
+          
+          this.$nextTick(() => {
+            this.initCharts(stats);
+          });
+        }
+      } catch (error) {
+        console.error('获取统计信息失败:', error);
+        this.$message.warning('获取统计信息失败');
+      } finally {
+        this.loadingStats = false;
+      }
+    },
+    
+    // 处理窗口大小变化
+    handleResize() {
+      if (this.distributionChart) this.distributionChart.resize();
+    },
+    
+    // 上传并计算
+    async calculate() {
+      if (!this.selectedFile) {
+        this.$message.warning('请先选择CSV文件');
+        return;
+      }
+      
+      try {
+        this.isCalculating = true;
+        this.loadingMap = true;
+        this.loadingHistogram = true;
+        this.loadingStats = true;
+        
+        // 创建FormData
+        const formData = new FormData();
+        formData.append('csv_file', this.selectedFile);
+        
+        // 调用Cd通量计算接口
+        await axios.post(
+          'http://localhost:8000/api/cd-flux/output/calculate',
+          formData,
+          {
+            headers: {
+              'Content-Type': 'multipart/form-data'
+            }
+          }
+        );
+        
+        // 更新后重新获取地图、直方图和统计数据
+        await this.fetchLatestResults();
+        await this.fetchStatistics();
+        
+        this.$message.success('计算完成!');
+        
+      } catch (error) {
+        console.error('计算失败:', error);
+        let errorMessage = '计算失败,请重试';
+        
+        if (error.response) {
+          if (error.response.status === 400) {
+            errorMessage = '文件格式错误:' + (error.response.data.detail || '请上传正确的CSV文件');
+          } else if (error.response.status === 500) {
+            errorMessage = '服务器错误:' + (error.response.data.detail || '请稍后重试');
+          }
+        }
+        
+        this.$message.error(errorMessage);
+      } finally {
+        this.isCalculating = false;
+        this.loadingMap = false;
+        this.loadingHistogram = false;
+        this.loadingStats = false;
+      }
+    },
+    
+    // 导出地图
+    exportMap() {
+      if (!this.mapBlob) {
+        this.$message.warning('请先计算生成地图');
+        return;
+      }
+      
+      const link = document.createElement('a');
+      link.href = URL.createObjectURL(this.mapBlob);
+      link.download = `Cd输出通量空间分布图.jpg`;
+      link.click();
+      URL.revokeObjectURL(link.href);
+    },
+    
+    // 导出直方图
+    exportHistogram() {
+      if (!this.histogramBlob) {
+        this.$message.warning('请先计算生成直方图');
+        return;
+      }
+      
+      const link = document.createElement('a');
+      link.href = URL.createObjectURL(this.histogramBlob);
+      link.download = `Cd输出通量直方图.jpg`;
+      link.click();
+      URL.revokeObjectURL(link.href);
+    },
+    
+    // 导出数据
+    async exportData() {
+      try {
+        this.$message.info('正在获取Cd输出通量数据...');
+        
+        const response = await axios.get(
+          `http://localhost:8000/api/cd-flux/output/export-csv`,
+          { responseType: 'blob' }
+        );
+        
+        const blob = new Blob([response.data], { type: 'text/csv' });
+        const link = document.createElement('a');
+        link.href = URL.createObjectURL(blob);
+        link.download = `Cd输出通量数据.csv`;
+        link.click();
+        URL.revokeObjectURL(link.href);
+        
+        this.$message.success('数据导出成功');
+      } catch (error) {
+        console.error('导出数据失败:', error);
+        this.$message.error('导出数据失败: ' + (error.response?.data?.detail || '请稍后重试'));
+      }
+    }
+  }
 };
 </script>
 
 <style scoped>
-.cd-input-output-container {
-  display: flex;
-  justify-content: center;
-  align-items: center;
+/* 保持原有样式不变 */
+.container {
   padding: 20px;
-}
-
-.gradient-card {
-  /* 半透明渐变背景 */
+  /* 添加70%透明度的渐变背景 */
   background: linear-gradient(
     135deg, 
-    rgba(250, 253, 255, 0.8), 
-    rgba(137, 223, 252, 0.8)
+    rgba(230, 247, 255, 0.7) 0%, 
+    rgba(240, 248, 255, 0.7) 100%
   );
-  border-radius: 12px;
-  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
-  padding: 30px;
-  text-align: left; /* 改为左对齐 */
-  width: 600px;
+  min-height: 100vh;
+  box-sizing: border-box;
+}
+
+.toolbar {
+  display: flex;
+  flex-direction: column;
+  gap: 15px;
+  margin-bottom: 20px;
+  padding: 15px;
+  background-color: rgba(255, 255, 255, 0.8); /* 调整为半透明白色 */
+  border-radius: 8px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
   backdrop-filter: blur(5px); /* 添加模糊效果增强半透明感 */
-  border: none;
 }
 
-.label {
+.upload-section {
+  display: flex;
+  align-items: center;
+  gap: 15px;
+  padding-bottom: 15px;
+  border-bottom: 1px solid rgba(0, 0, 0, 0.1); /* 调整边框透明度 */
+}
+
+.file-name {
+  flex: 1;
+  padding: 0 10px;
+  color: #666;
+  font-size: 14px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.action-buttons {
+  display: flex;
+  gap: 10px;
+}
+
+.custom-button {
+  background-color: #47C3B9 !important;
+  color: #DCFFFA !important;
+  border: none;
+  border-radius: 155px;
+  padding: 10px 20px;
   font-weight: bold;
-  font-size: 18px;
-  margin-bottom: 10px; /* 减少底部外边距 */
-  color: #333;
+  display: flex;
+  align-items: center;
+}
+
+.upload-icon {
+  margin-right: 5px;
 }
 
-.custom-input {
+.content-area {
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+
+/* 横向布局容器 */
+.horizontal-container {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 20px;
   width: 100%;
-  max-width: 200px;
-  margin-left: 0; /* 确保输入框靠左对齐 */
 }
 
-/* 自定义输入框样式 */
-:deep(.custom-input .el-input__inner) {
-  background: rgba(255, 255, 255, 0.7);
+.map-section, .histogram-section {
+  flex: 1;
+  min-width: 300px;
+  background-color: rgba(255, 255, 255, 0.8); /* 调整为半透明白色 */
   border-radius: 8px;
-  border: 1px solid #dcdfe6;
-  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
-  padding: 10px 15px;
-  font-size: 16px;
-  color: #333;
+  padding: 15px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+  position: relative;
+  min-height: 400px;
+  backdrop-filter: blur(5px); /* 添加模糊效果增强半透明感 */
 }
 
-:deep(.custom-input .el-input__inner:focus) {
-  border-color: #409EFF;
-  box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
+.map-image, .histogram-image {
+  width: 100%;
+  height: 100%;
+  max-height: 600px;
+  object-fit: contain;
+  border-radius: 4px;
 }
 
-.calculate-btn {
+.table-area {
   width: 100%;
-  max-width: 200px;
-  height: 50px;
-  border: none;
-  border-radius: 25px !important;
-  font-size: 18px;
-  font-weight: bold;
-  transition: all 0.4s ease;
-  
-  /* 渐变背景色 */
-  background: linear-gradient(to right, #8DF9F0, #26B046);
-  color: white !important;
-  /* 按钮整体阴影 */
-  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15),
-              0 2px 6px rgba(38, 176, 70, 0.3) inset;
+  background-color: rgba(255, 255, 255, 0.8); /* 调整为半透明白色 */
+  border-radius: 8px;
+  padding: 15px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+  margin-top: 20px;
+  backdrop-filter: blur(5px); /* 添加模糊效果增强半透明感 */
+}
+
+.loading-container {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  height: 300px;
+  color: #47C3B9;
+}
+
+.no-data {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  height: 300px;
+  color: #999;
+  font-size: 16px;
+}
+
+.no-data .el-icon {
+  font-size: 48px;
+  margin-bottom: 10px;
 }
 
-.calculate-btn:hover {
-  transform: scale(1.03);
-  box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2),
-              0 2px 8px rgba(38, 176, 70, 0.4) inset;
-  background: linear-gradient(to right, #7de8df, #20a03d);
+.loading-icon {
+  font-size: 36px;
+  margin-bottom: 10px;
+  animation: rotate 2s linear infinite;
 }
 
-.calculate-btn:active {
-  transform: scale(0.98);
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1),
-              0 1px 6px rgba(38, 176, 70, 0.4) inset;
+@keyframes rotate {
+  from {
+    transform: rotate(0deg);
+  }
+  to {
+    transform: rotate(360deg);
+  }
+}
+
+/* 响应式布局调整 */
+@media (max-width: 992px) {
+  .horizontal-container {
+    flex-direction: column;
+  }
+  
+  .map-section, .histogram-section {
+    width: 100%;
+    flex: none;
+  }
 }
-</style>
+</style>

+ 267 - 184
src/views/User/hmInFlux/subsurfaceLeakage/subsurfaceLeakageInputFlux.vue

@@ -1,266 +1,349 @@
 <template>
-  <div class="container">
-    <div class="gradient-card">
-      <div class="card-header">
-        <h1>地下渗漏计算器</h1>
-        <p>评估地下水渗漏量及其环境影响</p>
-      </div>
-
-      <div class="card-content">
-        <div class="input-section">
-          <div class="input-group">
-            <label class="label">地下渗漏 (g/ha/a)</label>
-            <input v-model="leakage" placeholder="请输入地表径流" class="custom-input" />
-          </div>
-
-          <div class="info-panel">
-            <div class="info-item">
-              <i class="fas fa-info-circle"></i>
-              <span>当前值: {{ leakage || '0.023' }}</span>
-            </div>
-            <div class="info-item">
-              <i class="fas fa-chart-line"></i>
-              <span>建议范围: 0.01 - 0.05</span>
-            </div>
-          </div>
+  <div class="leakage-flux-container">
+    <div class="map-title">地下渗漏Cd通量分布图</div>
+    <div class="map-content">
+      <div class="map-image-container">
+        <!-- 加载状态 -->
+        <div v-if="isLoading" class="loading-overlay">
+          <div class="spinner"></div>
+          <p>地图加载中...</p>
         </div>
-
-        <div class="button-section">
-          <button class="calculate-btn" @click="onCalculate">
-            <i class="fas fa-calculator"></i> 计算地表径流
+        
+        <!-- 错误状态 -->
+        <div v-if="isError" class="error-overlay">
+          <i class="fas fa-exclamation-triangle"></i>
+          <p>{{ errorMessage }}</p>
+          <button class="retry-btn" @click="fetchMap">
+            <i class="fas fa-redo"></i> 重新加载
           </button>
         </div>
-      </div>
-
-      <div class="card-footer">
-        <p><i class="fas fa-lightbulb"></i> 提示: 输入地表径流后点击计算按钮获取详细分析</p>
+        
+        <!-- 地图图片 -->
+        <img 
+          v-if="mapImageUrl"
+          :src="mapImageUrl" 
+          alt="地下渗漏Cd通量分布图" 
+          class="map-image"
+          @load="handleImageLoad"
+        >
+        
+        <!-- 默认占位图 -->
+        <div v-else class="placeholder">
+          <i class="fas fa-map"></i>
+          <p>准备加载地下渗漏Cd通量分布图</p>
+        </div>
       </div>
     </div>
   </div>
 </template>
 
 <script>
-import { ref } from 'vue';
+import axios from 'axios';
 
 export default {
-  name: 'LeakageCalculator',
-  setup() {
-    const leakage = ref('0.023');
-
-    const onCalculate = () => {
-      alert(`计算完成!当前地表径流: ${leakage.value || '0.023'} g/ha/a`);
-    };
-
+  name: 'LeakageFluxMap',
+  data() {
     return {
-      leakage,
-      onCalculate
+      mapImageUrl: null,
+      isLoading: true,
+      isError: false,
+      errorMessage: '',
+      area: '乐昌市',
+      level: 'county',
+      colormap: 'blues',
+      avgFlux: '0.023',
+      lastUpdated: '2025年8月21日',
     };
+  },
+  mounted() {
+    this.fetchMap();
+  },
+  methods: {
+    async fetchMap() {
+      this.isLoading = true;
+      this.isError = false;
+      
+      try {
+        // 构建API URL
+        const apiUrl = `http://localhost:8000/api/cd-flux-removal/groundwater_leaching/visualize`;
+        const params = {
+          area: this.area,
+          level: this.level,
+          colormap: this.colormap
+        };
+        
+        // 发送API请求
+        const response = await axios.get(apiUrl, {
+          params,
+          responseType: 'blob' // 接收二进制数据
+        });
+        
+        // 创建图片URL
+        const blob = new Blob([response.data], { type: 'image/jpeg' });
+        this.mapImageUrl = URL.createObjectURL(blob);
+        
+      } catch (error) {
+        console.error('加载地图失败:', error);
+        this.isError = true;
+        this.errorMessage = '地图加载失败,请稍后重试';
+      } finally {
+        this.isLoading = false;
+      }
+    },
+    handleImageLoad() {
+      // 图片加载完成后的处理
+      console.log('地图图片加载完成');
+    }
   }
 };
 </script>
 
 <style scoped>
-body, html {
-  margin: 0;
-  padding: 0;
-  height: 100%;
-  overflow: hidden; /* 禁止页面滚动 */
-}
-
-.gradient-card {
-  background: linear-gradient(135deg,
-      rgba(250, 253, 255, 0.8),
-      rgba(137, 223, 252, 0.8));
-  border-radius: 20px;
-  box-shadow: 0 15px 40px rgba(0, 0, 0, 0.15);
-  padding: 30px;
+.leakage-flux-container {
   width: 100%;
-  max-width: 500px;
-  margin-left: 350px; /* 左侧留出空间 */
-  backdrop-filter: blur(8px);
-  border: none;
-  display: flex;
-  flex-direction: column;
+  max-width: 900px;
+  margin: 0 auto;
+  background: white;
+  border-radius: 12px;
+  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15);
   overflow: hidden;
+  font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
 }
 
-.card-header {
+.map-title {
+  background: linear-gradient(to right, #1a5fad, #2c8fd1);
+  color: white;
   text-align: center;
-  margin-bottom: 20px;
+  padding: 18px 0;
+  font-size: 1.8rem;
+  font-weight: 600;
+  letter-spacing: 1px;
+  text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
 }
 
-.card-header h1 {
-  font-size: 2rem;
-  color: #006064;
-  margin-bottom: 10px;
-  font-weight: 700;
-  text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.1);
+.map-content {
+  display: flex;
+  flex-direction: column;
+  padding: 20px;
+  background: #f8fafc;
 }
 
-.card-header p {
-  font-size: 1.1rem;
-  color: #00838f;
-  opacity: 0.9;
+.map-image-container {
+  flex: 1;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  padding: 10px;
+  background: white;
+  border-radius: 12px;
+  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
+  margin-bottom: 20px;
+  min-height: 600px;
+  position: relative;
 }
 
-.input-section {
-  margin: 20px 0;
+.map-image {
+  max-width: 100%;
+  max-height: 100%;
+  object-fit: contain;
+  border-radius: 8px;
+  box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
+  transition: transform 0.3s ease;
+  height: 600px;
+  width: 100%;
 }
 
-.input-group {
-  margin-bottom: 20px;
+.map-image:hover {
+  transform: scale(1.02);
 }
 
-.label {
-  display: block;
-  font-weight: 600;
-  font-size: 1.2rem;
-  margin-bottom: 10px;
-  color: #006064;
+/* 修改后的map-info样式 - 居中显示 */
+.map-info {
+  display: flex;
+  justify-content: center; /* 水平居中 */
+  align-items: center; /* 垂直居中 */
+  background: rgba(26, 95, 173, 0.05);
+  border-radius: 10px;
+  padding: 20px;
 }
 
-.custom-input {
-  width: 100%;
-  padding: 15px 10px;
-  border-radius: 12px;
-  border: 2px solid #80deea;
-  font-size: 1rem;
-  background: rgba(255, 255, 255, 0.7);
-  transition: all 0.3s ease;
-  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05);
+.info-item {
+  display: flex;
+  align-items: center;
+  padding: 10px 20px; /* 增加左右内边距 */
+  background: white;
+  border-radius: 8px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+  width: fit-content; /* 使宽度适应内容 */
 }
 
-.custom-input:focus {
-  outline: none;
-  border-color: #26c6da;
-  box-shadow: 0 0 0 4px rgba(38, 198, 218, 0.3);
+.info-item i {
+  font-size: 1.2rem;
+  color: #1a5fad;
+  margin-right: 10px;
 }
 
-.info-panel {
-  background: rgba(178, 235, 242, 0.4);
-  border-radius: 12px;
+.map-footer {
+  background: rgba(26, 95, 173, 0.1);
   padding: 15px;
-  margin-top: 15px;
+  text-align: center;
+  font-size: 0.9rem;
+  color: #1a5fad;
 }
 
-.info-item {
+/* 加载状态样式 */
+.loading-overlay {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
   display: flex;
+  flex-direction: column;
+  justify-content: center;
   align-items: center;
-  margin-bottom: 10px;
-  font-size: 1rem;
-  color: #006064;
+  background: rgba(255, 255, 255, 0.8);
+  z-index: 10;
 }
 
-.info-item i {
-  margin-right: 10px;
+.spinner {
+  width: 50px;
+  height: 50px;
+  border: 5px solid rgba(26, 95, 173, 0.2);
+  border-top: 5px solid #1a5fad;
+  border-radius: 50%;
+  animation: spin 1s linear infinite;
+  margin-bottom: 15px;
+}
+
+@keyframes spin {
+  0% { transform: rotate(0deg); }
+  100% { transform: rotate(360deg); }
+}
+
+.loading-overlay p {
   font-size: 1.2rem;
-  color: #00838f;
+  color: #1a5fad;
+  font-weight: 500;
 }
 
-.button-section {
-  margin: 25px 0;
-  text-align: center;
-  flex-grow: 1;
+/* 错误状态样式 */
+.error-overlay {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
   display: flex;
-  align-items: flex-end;
+  flex-direction: column;
   justify-content: center;
+  align-items: center;
+  background: rgba(255, 255, 255, 0.9);
+  z-index: 10;
+  padding: 20px;
+  text-align: center;
+}
+
+.error-overlay i {
+  font-size: 3rem;
+  color: #e74c3c;
+  margin-bottom: 15px;
+}
+
+.error-overlay p {
+  font-size: 1.2rem;
+  color: #333;
+  margin-bottom: 20px;
 }
 
-.calculate-btn {
-  background: linear-gradient(to right, #8DF9F0, #26B046);
+.retry-btn {
+  background: #1a5fad;
   color: white;
   border: none;
-  border-radius: 50px;
-  padding: 15px 30px;
-  font-size: 1.2rem;
-  font-weight: 600;
+  border-radius: 30px;
+  padding: 10px 25px;
+  font-size: 1rem;
   cursor: pointer;
-  transition: all 0.4s ease;
-  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);
-  display: inline-flex;
+  display: flex;
   align-items: center;
-  justify-content: center;
-  min-width: 250px;
-  position: relative;
-  overflow: hidden;
-}
-
-.calculate-btn i {
-  margin-right: 10px;
-  font-size: 1.4rem;
+  transition: background 0.3s;
 }
 
-.calculate-btn:hover {
-  transform: translateY(-5px);
-  box-shadow: 0 12px 25px rgba(0, 0, 0, 0.3);
-  background: linear-gradient(to right, #7de8df, #20a03d);
+.retry-btn:hover {
+  background: #154a8a;
 }
 
-.calculate-btn:active {
-  transform: translateY(-2px);
+.retry-btn i {
+  font-size: 1rem;
+  margin-right: 8px;
+  color: white;
 }
 
-.card-footer {
-  text-align: center;
-  font-size: 1rem;
-  color: #00838f;
-  font-style: italic;
-  padding-top: 15px;
-  border-top: 1px solid rgba(0, 150, 136, 0.2);
+/* 占位符样式 */
+.placeholder {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  height: 100%;
+  color: #1a5fad;
 }
 
-.card-footer i {
-  margin-right: 10px;
+.placeholder i {
+  font-size: 4rem;
+  margin-bottom: 20px;
 }
 
-/* 水波纹效果 */
-.calculate-btn::after {
-  content: "";
-  position: absolute;
-  top: 50%;
-  left: 50%;
-  width: 0;
-  height: 0;
-  background: rgba(255, 255, 255, 0.3);
-  border-radius: 50%;
-  transform: translate(-50%, -50%);
-  transition: width 0.6s, height 0.6s;
+.placeholder p {
+  font-size: 1.2rem;
+  font-weight: 500;
 }
 
-.calculate-btn:active::after {
-  width: 200px;
-  height: 200px;
+@media (max-width: 768px) {
+  .map-title {
+    font-size: 1.5rem;
+    padding: 15px 0;
+  }
+  
+  .map-image-container {
+    min-height: 450px;
+  }
+  
+  .map-image {
+    height: 450px;
+  }
+  
+  /* 移动端调整信息项样式 */
+  .map-info {
+    padding: 15px;
+  }
+  
+  .info-item {
+    padding: 10px 15px;
+  }
 }
 
-/* 响应式设计 */
 @media (max-width: 480px) {
-  .gradient-card {
-    padding: 20px;
-    width: 100%;
-    max-width: 100%;
+  .map-title {
+    font-size: 1.3rem;
   }
-
-  .card-header h1 {
-    font-size: 1.8rem;
+  
+  .map-image-container {
+    min-height: 350px;
   }
-
-  .label {
-    font-size: 1rem;
+  
+  .map-image {
+    height: 350px;
   }
-
-  .custom-input {
-    padding: 12px;
+  
+  .info-item {
     font-size: 0.9rem;
+    padding: 8px 12px;
   }
-
-  .calculate-btn {
-    padding: 12px 25px;
+  
+  .info-item i {
     font-size: 1rem;
-    min-width: 200px;
-  }
-
-  .info-item span {
-    font-size: 0.9rem;
   }
 }
 </style>

+ 268 - 188
src/views/User/hmInFlux/surfaceRunoff/surfaceRunoffInputFlux.vue

@@ -1,269 +1,349 @@
 <template>
-  <div class="container">
-    <div class="gradient-card">
-      <div class="card-header">
-        <h1>地表径流计算器</h1>
-        <p>评估地表径流量及其环境影响</p>
-      </div>
-
-      <div class="card-content">
-        <div class="input-section">
-          <div class="input-group">
-            <label class="label">地表径流 (g/ha/a)</label>
-            <input v-model="leakage" placeholder="请输入地表径流" class="custom-input" />
-          </div>
-
-          <div class="info-panel">
-            <div class="info-item">
-              <i class="fas fa-info-circle"></i>
-              <span>当前值: {{ leakage || '0.368' }}</span>
-            </div>
-            <div class="info-item">
-              <i class="fas fa-chart-line"></i>
-              <span>建议范围: 0.01 - 0.05</span>
-            </div>
-          </div>
+  <div class="runoff-flux-container">
+    <div class="map-title">地表径流Cd通量分布图</div>
+    <div class="map-content">
+      <div class="map-image-container">
+        <!-- 加载状态 -->
+        <div v-if="isLoading" class="loading-overlay">
+          <div class="spinner"></div>
+          <p>地图加载中...</p>
         </div>
-
-        <div class="button-section">
-          <button class="calculate-btn" @click="onCalculate">
-            <i class="fas fa-calculator"></i> 计算地表径流
+        
+        <!-- 错误状态 -->
+        <div v-if="isError" class="error-overlay">
+          <i class="fas fa-exclamation-triangle"></i>
+          <p>{{ errorMessage }}</p>
+          <button class="retry-btn" @click="fetchMap">
+            <i class="fas fa-redo"></i> 重新加载
           </button>
         </div>
-      </div>
-
-      <div class="card-footer">
-        <p><i class="fas fa-lightbulb"></i> 提示: 输入地表径流后点击计算按钮获取详细分析</p>
+        
+        <!-- 地图图片 -->
+        <img 
+          v-if="mapImageUrl"
+          :src="mapImageUrl" 
+          alt="地表径流Cd通量分布图" 
+          class="map-image"
+          @load="handleImageLoad"
+        >
+        
+        <!-- 默认占位图 -->
+        <div v-else class="placeholder">
+          <i class="fas fa-map"></i>
+          <p>准备加载地表径流Cd通量分布图</p>
+        </div>
       </div>
     </div>
   </div>
 </template>
 
 <script>
-import { ref } from 'vue';
+import axios from 'axios';
 
 export default {
-  name: 'LeakageCalculator',
-  setup() {
-    const leakage = ref('0.368');
-
-    const onCalculate = () => {
-      alert(`计算完成!当前地表径流: ${leakage.value || '0.368'} g/ha/a`);
-    };
-
+  name: 'RunoffFluxMap',
+  data() {
     return {
-      leakage,
-      onCalculate
+      mapImageUrl: null,
+      isLoading: true,
+      isError: false,
+      errorMessage: '',
+      area: '乐昌市',
+      level: 'county',
+      colormap: 'blues',
+      
+      lastUpdated: '2025年8月21日',
     };
+  },
+  mounted() {
+    this.fetchMap();
+  },
+  methods: {
+    async fetchMap() {
+      this.isLoading = true;
+      this.isError = false;
+      
+      try {
+        // 构建API URL
+        const apiUrl = `http://localhost:8000/api/cd-flux-removal/surface_runoff/visualize`;
+        const params = {
+          area: this.area,
+          level: this.level,
+          colormap: this.colormap
+        };
+        
+        // 发送API请求
+        const response = await axios.get(apiUrl, {
+          params,
+          responseType: 'blob' // 接收二进制数据
+        });
+        
+        // 创建图片URL
+        const blob = new Blob([response.data], { type: 'image/jpeg' });
+        this.mapImageUrl = URL.createObjectURL(blob);
+        
+      } catch (error) {
+        console.error('加载地图失败:', error);
+        this.isError = true;
+        this.errorMessage = '地图加载失败,请稍后重试';
+      } finally {
+        this.isLoading = false;
+      }
+    },
+    handleImageLoad() {
+      // 图片加载完成后的处理
+      console.log('地图图片加载完成');
+    }
   }
 };
 </script>
 
 <style scoped>
-body, html {
-  margin: 0;
-  padding: 0;
-  height: 100%;
-  overflow: hidden; /* 禁止页面滚动 */
-}
-
-.gradient-card {
-  background: linear-gradient(135deg,
-      rgba(250, 253, 255, 0.8),
-      rgba(137, 223, 252, 0.8));
-  border-radius: 20px;
-  box-shadow: 0 15px 40px rgba(0, 0, 0, 0.15);
-  padding: 30px;
+.runoff-flux-container {
   width: 100%;
-  max-width: 500px;
-  margin-left: 350px; /* 左侧留出空间 */
-  backdrop-filter: blur(8px);
-  border: none;
-  display: flex;
-  flex-direction: column;
+  max-width: 900px;
+  margin: 0 auto;
+  background: white;
+  border-radius: 12px;
+  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15);
   overflow: hidden;
+  font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
 }
 
-.card-header {
+.map-title {
+  background: linear-gradient(to right, #1a5fad, #2c8fd1);
+  color: white;
   text-align: center;
-  margin-bottom: 20px;
+  padding: 18px 0;
+  font-size: 1.8rem;
+  font-weight: 600;
+  letter-spacing: 1px;
+  text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
 }
 
-.card-header h1 {
-  font-size: 2rem;
-  color: #006064;
-  margin-bottom: 10px;
-  font-weight: 700;
-  text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.1);
+.map-content {
+  display: flex;
+  flex-direction: column;
+  padding: 20px;
+  background: #f8fafc;
 }
 
-.card-header p {
-  font-size: 1.1rem;
-  color: #00838f;
-  opacity: 0.9;
+.map-image-container {
+  flex: 1;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  padding: 10px;
+  background: white;
+  border-radius: 12px;
+  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
+  margin-bottom: 20px;
+  min-height: 600px;
+  position: relative;
 }
 
-.input-section {
-  margin: 20px 0;
+.map-image {
+  max-width: 100%;
+  max-height: 100%;
+  object-fit: contain;
+  border-radius: 8px;
+  box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
+  transition: transform 0.3s ease;
+  height: 600px;
+  width: 100%;
 }
 
-.input-group {
-  margin-bottom: 20px;
+.map-image:hover {
+  transform: scale(1.02);
 }
 
-.label {
-  display: block;
-  font-weight: 600;
-  font-size: 1.2rem;
-  margin-bottom: 10px;
-  color: #006064;
+/* 修改后的map-info样式 - 居中显示 */
+.map-info {
+  display: flex;
+  justify-content: center; /* 水平居中 */
+  align-items: center; /* 垂直居中 */
+  background: rgba(26, 95, 173, 0.05);
+  border-radius: 10px;
+  padding: 20px;
 }
 
-.custom-input {
-  width: 100%;
-  padding: 15px 10px;
-  border-radius: 12px;
-  border: 2px solid #80deea;
-  font-size: 1rem;
-  background: rgba(255, 255, 255, 0.7);
-  transition: all 0.3s ease;
-  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05);
+.info-item {
+  display: flex;
+  align-items: center;
+  padding: 10px 20px; /* 增加左右内边距 */
+  background: white;
+  border-radius: 8px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+  width: fit-content; /* 使宽度适应内容 */
 }
 
-.custom-input:focus {
-  outline: none;
-  border-color: #26c6da;
-  box-shadow: 0 0 0 4px rgba(38, 198, 218, 0.3);
+.info-item i {
+  font-size: 1.2rem;
+  color: #1a5fad;
+  margin-right: 10px;
 }
 
-.info-panel {
-  background: rgba(178, 235, 242, 0.4);
-  border-radius: 12px;
+.map-footer {
+  background: rgba(26, 95, 173, 0.1);
   padding: 15px;
-  margin-top: 15px;
+  text-align: center;
+  font-size: 0.9rem;
+  color: #1a5fad;
 }
 
-.info-item {
+/* 加载状态样式 */
+.loading-overlay {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
   display: flex;
+  flex-direction: column;
+  justify-content: center;
   align-items: center;
-  margin-bottom: 10px;
-  font-size: 1rem;
-  color: #006064;
+  background: rgba(255, 255, 255, 0.8);
+  z-index: 10;
 }
 
-.info-item i {
-  margin-right: 10px;
+.spinner {
+  width: 50px;
+  height: 50px;
+  border: 5px solid rgba(26, 95, 173, 0.2);
+  border-top: 5px solid #1a5fad;
+  border-radius: 50%;
+  animation: spin 1s linear infinite;
+  margin-bottom: 15px;
+}
+
+@keyframes spin {
+  0% { transform: rotate(0deg); }
+  100% { transform: rotate(360deg); }
+}
+
+.loading-overlay p {
   font-size: 1.2rem;
-  color: #00838f;
+  color: #1a5fad;
+  font-weight: 500;
 }
 
-.button-section {
-  margin: 25px 0;
-  text-align: center;
-  flex-grow: 1;
+/* 错误状态样式 */
+.error-overlay {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
   display: flex;
-  align-items: flex-end;
+  flex-direction: column;
   justify-content: center;
+  align-items: center;
+  background: rgba(255, 255, 255, 0.9);
+  z-index: 10;
+  padding: 20px;
+  text-align: center;
 }
 
-.calculate-btn {
-  background: linear-gradient(to right, #8DF9F0, #26B046);
+.error-overlay i {
+  font-size: 3rem;
+  color: #e74c3c;
+  margin-bottom: 15px;
+}
+
+.error-overlay p {
+  font-size: 1.2rem;
+  color: #333;
+  margin-bottom: 20px;
+}
+
+.retry-btn {
+  background: #1a5fad;
   color: white;
   border: none;
-  border-radius: 50px;
-  padding: 15px 30px;
-  font-size: 1.2rem;
-  font-weight: 600;
+  border-radius: 30px;
+  padding: 10px 25px;
+  font-size: 1rem;
   cursor: pointer;
-  transition: all 0.4s ease;
-  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);
-  display: inline-flex;
+  display: flex;
   align-items: center;
-  justify-content: center;
-  min-width: 250px;
-  position: relative;
-  overflow: hidden;
+  transition: background 0.3s;
 }
 
-.calculate-btn i {
-  margin-right: 10px;
-  font-size: 1.4rem;
+.retry-btn:hover {
+  background: #154a8a;
 }
 
-.calculate-btn:hover {
-  transform: translateY(-5px);
-  box-shadow: 0 12px 25px rgba(0, 0, 0, 0.3);
-  background: linear-gradient(to right, #7de8df, #20a03d);
-}
-
-.calculate-btn:active {
-  transform: translateY(-2px);
+.retry-btn i {
+  font-size: 1rem;
+  margin-right: 8px;
+  color: white;
 }
 
-.card-footer {
-  text-align: center;
-  font-size: 1rem;
-  color: #00838f;
-  font-style: italic;
-  padding-top: 15px;
-  border-top: 1px solid rgba(0, 150, 136, 0.2);
+/* 占位符样式 */
+.placeholder {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  height: 100%;
+  color: #1a5fad;
 }
 
-.card-footer i {
-  margin-right: 10px;
+.placeholder i {
+  font-size: 4rem;
+  margin-bottom: 20px;
 }
 
-/* 水波纹效果 */
-.calculate-btn::after {
-  content: "";
-  position: absolute;
-  top: 50%;
-  left: 50%;
-  width: 0;
-  height: 0;
-  background: rgba(255, 255, 255, 0.3);
-  border-radius: 50%;
-  transform: translate(-50%, -50%);
-  transition: width 0.6s, height 0.6s;
+.placeholder p {
+  font-size: 1.2rem;
+  font-weight: 500;
 }
 
-.calculate-btn:active::after {
-  width: 200px;
-  height: 200px;
+@media (max-width: 768px) {
+  .map-title {
+    font-size: 1.5rem;
+    padding: 15px 0;
+  }
+  
+  .map-image-container {
+    min-height: 450px;
+  }
+  
+  .map-image {
+    height: 450px;
+  }
+  
+  /* 移动端调整信息项样式 */
+  .map-info {
+    padding: 15px;
+  }
+  
+  .info-item {
+    padding: 10px 15px;
+  }
 }
 
-/* 响应式设计 */
 @media (max-width: 480px) {
-  .gradient-card {
-    padding: 20px;
-    width: 100%;
-    max-width: 100%;
+  .map-title {
+    font-size: 1.3rem;
   }
-
-  .card-header h1 {
-    font-size: 1.8rem;
+  
+  .map-image-container {
+    min-height: 350px;
   }
-
-  .label {
-    font-size: 1rem;
+  
+  .map-image {
+    height: 350px;
   }
-
-  .custom-input {
-    padding: 12px;
+  
+  .info-item {
     font-size: 0.9rem;
+    padding: 8px 12px;
   }
-
-  .calculate-btn {
-    padding: 12px 25px;
+  
+  .info-item i {
     font-size: 1rem;
-    min-width: 200px;
-  }
-
-  .info-item span {
-    font-size: 0.9rem;
   }
 }
-</style>
-
-
-
+</style>