|
|
@@ -15,10 +15,10 @@
|
|
|
<el-button
|
|
|
class="custom-button"
|
|
|
:loading="isGenerating"
|
|
|
- :disabled="!canGenerate"
|
|
|
+ :disabled="(!isValidYear || isGenerating)"
|
|
|
@click="generateForecast"
|
|
|
>
|
|
|
- <el-icon class="play-icon"></el-icon>
|
|
|
+ <el-icon><Right /></el-icon>
|
|
|
开始预测
|
|
|
</el-button>
|
|
|
</div>
|
|
|
@@ -26,31 +26,34 @@
|
|
|
|
|
|
<!-- 主体内容区 -->
|
|
|
<div class="content-area">
|
|
|
- <!-- 预测结果展示区 -->
|
|
|
+ <!-- 预测结果展示区 - 垂直排列 -->
|
|
|
<div class="result-display">
|
|
|
- <div class="map-section">
|
|
|
+ <!-- 地图部分 -->
|
|
|
+ <div class="visualization-section">
|
|
|
<h3>未来Cd浓度预测地图</h3>
|
|
|
<div v-if="loadingMap" class="loading-container">
|
|
|
<el-icon class="loading-icon"><Loading /></el-icon>
|
|
|
<span>地图加载中...</span>
|
|
|
</div>
|
|
|
- <img :src=mapImageUrl class="result-img"></img>
|
|
|
- <div v-if="!loadingMap && !mapImageUrl" class="no-data">
|
|
|
+ <img v-if="mapImageUrl" :src="mapImageUrl" class="result-img"></img>
|
|
|
+ <div v-else class="no-data">
|
|
|
<el-icon><Picture /></el-icon>
|
|
|
<p>暂无地图数据</p>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
- <div class="histogram-section">
|
|
|
+
|
|
|
+ <!-- 直方图部分 -->
|
|
|
+ <div class="visualization-section">
|
|
|
<h3>预测直方图</h3>
|
|
|
<div v-if="loadingHistogram" class="loading-container">
|
|
|
<el-icon class="loading-icon"><Loading /></el-icon>
|
|
|
<span>直方图加载中...</span>
|
|
|
</div>
|
|
|
- <img :src=histogramImageUrl class="result-img"></img>
|
|
|
- <div v-if="!loadingHistogram && !histogramImageUrl" class="no-data">
|
|
|
- <el-icon><Picture /></el-icon>
|
|
|
+ <img v-if="histogramImageUrl" :src="histogramImageUrl" class="result-img"></img>
|
|
|
+ <div v-else class="no-data">
|
|
|
+ <el-icon><Histogram /></el-icon>
|
|
|
<p>暂无直方图数据</p>
|
|
|
- </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -58,14 +61,17 @@
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
-import { SaveAs } from 'file-saver';
|
|
|
import { api8000 } from '@/utils/request';
|
|
|
-import {
|
|
|
- Loading, Picture, Histogram
|
|
|
-} from '@element-plus/icons-vue';
|
|
|
+import { Loading, Picture, Histogram, Right } from '@element-plus/icons-vue';
|
|
|
|
|
|
export default {
|
|
|
name: 'FutureCdPrediction',
|
|
|
+ components: {
|
|
|
+ Loading,
|
|
|
+ Picture,
|
|
|
+ Histogram,
|
|
|
+ Right
|
|
|
+ },
|
|
|
props: {
|
|
|
countyName: {
|
|
|
type: String,
|
|
|
@@ -84,7 +90,7 @@ export default {
|
|
|
histogramBlob: null,
|
|
|
mapImageUrl: '',
|
|
|
histogramImageUrl: ''
|
|
|
- }
|
|
|
+ };
|
|
|
},
|
|
|
computed: {
|
|
|
isValidYear() {
|
|
|
@@ -96,169 +102,283 @@ export default {
|
|
|
},
|
|
|
watch: {
|
|
|
forecastYear(newVal) {
|
|
|
- newVal = parseInt(newVal)
|
|
|
- if (isNaN(newVal)) this.forecastYear = null
|
|
|
- if (newVal < 1) this.forecastYear = 1
|
|
|
- if (newVal > 100) this.forecastYear = 100
|
|
|
+ newVal = parseInt(newVal);
|
|
|
+ if (isNaN(newVal)) this.forecastYear = null;
|
|
|
+ if (newVal < 1) this.forecastYear = 1;
|
|
|
+ if (newVal > 100) this.forecastYear = 100;
|
|
|
}
|
|
|
},
|
|
|
methods: {
|
|
|
// 预测控制逻辑
|
|
|
- async generateForecast() {
|
|
|
- if (this.isGenerating) return
|
|
|
-
|
|
|
- this.isGenerating = true
|
|
|
- this.mapBlob = null
|
|
|
- this.histogramBlob = null
|
|
|
- this.mapImageUrl = ''
|
|
|
- this.histogramImageUrl = ''
|
|
|
-
|
|
|
- try {
|
|
|
- // 1. 构建正确URL(使用GET方法+查询参数)
|
|
|
- let url = `/api/cd-flux/predict-future-cd?years=${encodeURIComponent(this.forecastYear)}`
|
|
|
- if (this.countyName) url += `&area=${encodeURIComponent(this.countyName)}`
|
|
|
- if (this.generateVisualization) url += '&generate_visualization=true'
|
|
|
-
|
|
|
- // 2. 发送GET请求(移除FormData)
|
|
|
- const predictResponse = await api8000.get(url, { responseType: 'json' })
|
|
|
-
|
|
|
- if (!predictResponse.data.success) {
|
|
|
- throw new Error(predictResponse.data.message)
|
|
|
+ async generateForecast() {
|
|
|
+ if (this.isGenerating) return;
|
|
|
+
|
|
|
+ this.isGenerating = true;
|
|
|
+ this.mapBlob = null;
|
|
|
+ this.histogramBlob = null;
|
|
|
+ this.mapImageUrl = '';
|
|
|
+ this.histogramImageUrl = '';
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 1. 尝试加载现有预测数据
|
|
|
+ await this.checkExistingData();
|
|
|
+
|
|
|
+ // 2. 如果存在历史数据,直接显示
|
|
|
+ this.$message.success(`成功加载${this.forecastYear}年预测数据`);
|
|
|
+ this.isGenerating = false;
|
|
|
+ return;
|
|
|
+ } catch (loadError) {
|
|
|
+ console.log('未找到历史数据,开始生成预测...', loadError);
|
|
|
}
|
|
|
|
|
|
- // 3. 加载结果(保持原有逻辑)
|
|
|
+ try {
|
|
|
+ // 3. 生成新预测
|
|
|
+ await this.generateNewPrediction();
|
|
|
+
|
|
|
+ // 4. 加载新生成的数据
|
|
|
+ await Promise.all([
|
|
|
+ this.loadVisualization('map', this.forecastYear),
|
|
|
+ this.loadVisualization('histogram', this.forecastYear)
|
|
|
+ ]);
|
|
|
+
|
|
|
+ this.$message.success(`预测生成成功(年份:${this.forecastYear})`);
|
|
|
+ } catch (generateError) {
|
|
|
+ this.$message.error(`预测失败:${generateError.message}`);
|
|
|
+ } finally {
|
|
|
+ this.isGenerating = false;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 检查历史数据是否存在
|
|
|
+ async checkExistingData() {
|
|
|
await Promise.all([
|
|
|
this.loadVisualization('map', this.forecastYear),
|
|
|
this.loadVisualization('histogram', this.forecastYear)
|
|
|
- ])
|
|
|
+ ]);
|
|
|
+ },
|
|
|
|
|
|
- this.$message.success(`预测生成成功(年份:${this.forecastYear})`)
|
|
|
- } catch (error) {
|
|
|
- this.$message.error(`预测失败:${error.message}`)
|
|
|
- } finally {
|
|
|
- this.isGenerating = false
|
|
|
- }
|
|
|
- },
|
|
|
-
|
|
|
- // 结果加载逻辑
|
|
|
+ // 生成新预测
|
|
|
+ async generateNewPrediction() {
|
|
|
+ let url = `/api/cd-flux/predict-future-cd?years=${encodeURIComponent(
|
|
|
+ this.forecastYear
|
|
|
+ )}`;
|
|
|
+ if (this.countyName) {
|
|
|
+ url += `&area=${encodeURIComponent(this.countyName)}`;
|
|
|
+ }
|
|
|
+ if (this.generateVisualization) {
|
|
|
+ url += '&generate_visualization=true';
|
|
|
+ }
|
|
|
+
|
|
|
+ const response = await api8000.get(url, { responseType: 'json' });
|
|
|
+ if (!response.data.success) {
|
|
|
+ throw new Error(response.data.message || '生成预测失败');
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 加载可视化数据
|
|
|
async loadVisualization(type, year) {
|
|
|
try {
|
|
|
const response = await api8000.get(
|
|
|
`/api/cd-flux/predict-future-cd/${type}/${year}`,
|
|
|
{ responseType: 'blob' }
|
|
|
- )
|
|
|
+ );
|
|
|
|
|
|
if (type === 'map') {
|
|
|
- this.mapBlob = response.data
|
|
|
- this.mapImageUrl = URL.createObjectURL(this.mapBlob)
|
|
|
+ this.mapBlob = response.data;
|
|
|
+ this.mapImageUrl = URL.createObjectURL(this.mapBlob);
|
|
|
} else {
|
|
|
- this.histogramBlob = response.data
|
|
|
- this.histogramImageUrl = URL.createObjectURL(this.histogramBlob)
|
|
|
+ this.histogramBlob = response.data;
|
|
|
+ this.histogramImageUrl = URL.createObjectURL(this.histogramBlob);
|
|
|
}
|
|
|
} catch (error) {
|
|
|
- this.$message.error(`加载${type}失败:${error.message}`)
|
|
|
+ // 显式抛出错误供外部捕获
|
|
|
+ throw new Error(`加载${type}失败: ${error.message}`);
|
|
|
}
|
|
|
},
|
|
|
-
|
|
|
- // 结果导出逻辑
|
|
|
- exportAllResults() {
|
|
|
- if (this.mapBlob) SaveAs(this.mapBlob, `future_cd_map_${this.forecastYear}.jpg`)
|
|
|
- if (this.histogramBlob) SaveAs(this.histogramBlob, `future_cd_histogram_${this.forecastYear}.jpg`)
|
|
|
- }
|
|
|
}
|
|
|
-}
|
|
|
+};
|
|
|
</script>
|
|
|
|
|
|
-
|
|
|
<style scoped>
|
|
|
+/* 全局颜色变量 */
|
|
|
+:root {
|
|
|
+ --primary-color: #47C3B9;
|
|
|
+ --secondary-color: #36A897;
|
|
|
+ --text-color: #333;
|
|
|
+ --bg-color: #F5F7FA;
|
|
|
+ --border-color: #E0E0E0;
|
|
|
+ --loading-color: #47C3B9;
|
|
|
+ --error-color: #FF4D4F;
|
|
|
+}
|
|
|
+
|
|
|
+/* 容器样式 */
|
|
|
.container {
|
|
|
max-width: 1400px;
|
|
|
margin: 20px auto;
|
|
|
padding: 0 20px;
|
|
|
+ background: var(--bg-color);
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 2px 12px rgba(0,0,0,0.1);
|
|
|
+ overflow: hidden;
|
|
|
}
|
|
|
|
|
|
-.toolbar {
|
|
|
+/* 顶部工具栏 */
|
|
|
+.forecast-controls {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: center;
|
|
|
- padding: 15px;
|
|
|
- background-color: #f5f7fa;
|
|
|
+ padding: 15px 20px;
|
|
|
+ background-color: rgba(230, 247, 255, 0.7);
|
|
|
border-radius: 8px;
|
|
|
margin-bottom: 20px;
|
|
|
}
|
|
|
|
|
|
-.upload-section {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 15px;
|
|
|
-}
|
|
|
-
|
|
|
-.forecast-controls {
|
|
|
- display: flex;
|
|
|
- gap: 15px;
|
|
|
- align-items: center;
|
|
|
-}
|
|
|
-
|
|
|
+/* 预测控制区 */
|
|
|
.year-selector {
|
|
|
display: flex;
|
|
|
gap: 10px;
|
|
|
align-items: center;
|
|
|
}
|
|
|
|
|
|
-.file-name {
|
|
|
- font-size: 14px;
|
|
|
- color: #666;
|
|
|
-}
|
|
|
-
|
|
|
+/* 自定义按钮 */
|
|
|
.custom-button {
|
|
|
padding: 8px 16px;
|
|
|
border-radius: 4px;
|
|
|
font-size: 14px;
|
|
|
+ background-color: var(--primary-color) !important;
|
|
|
+ color: white !important;
|
|
|
+ border: none;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.3s;
|
|
|
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 5px;
|
|
|
}
|
|
|
|
|
|
-.action-buttons {
|
|
|
- display: flex;
|
|
|
- gap: 10px;
|
|
|
+.custom-button:hover {
|
|
|
+ background-color: #36a897;
|
|
|
}
|
|
|
|
|
|
+.custom-button:active {
|
|
|
+ transform: translateY(0);
|
|
|
+}
|
|
|
+
|
|
|
+.custom-button:disabled {
|
|
|
+ background-color: #cccccc !important;
|
|
|
+ color: rgba(255,255,255,0.5) !important;
|
|
|
+ cursor: not-allowed;
|
|
|
+}
|
|
|
+
|
|
|
+/* 结果展示区 - 垂直布局 */
|
|
|
.result-display {
|
|
|
- display: grid;
|
|
|
- grid-template-columns: 1fr 1fr;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
gap: 20px;
|
|
|
+ margin-top: 10px;
|
|
|
}
|
|
|
|
|
|
-.map-section, .histogram-section {
|
|
|
- background-color: #fff;
|
|
|
+/* 可视化部分通用样式 */
|
|
|
+.visualization-section {
|
|
|
+ background-color: white;
|
|
|
padding: 20px;
|
|
|
border-radius: 8px;
|
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
|
|
+ min-height: 400px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
}
|
|
|
|
|
|
-h3 {
|
|
|
+.visualization-section h3 {
|
|
|
margin-top: 0;
|
|
|
margin-bottom: 15px;
|
|
|
font-size: 16px;
|
|
|
- color: #333;
|
|
|
+ color: var(--text-color);
|
|
|
+ border-bottom: 2px solid var(--secondary-color);
|
|
|
+ padding-bottom: 5px;
|
|
|
+ flex-shrink: 0;
|
|
|
}
|
|
|
|
|
|
+/* 加载状态 */
|
|
|
.loading-container {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 10px;
|
|
|
- color: #666;
|
|
|
+ color: var(--loading-color);
|
|
|
+ min-height: 300px;
|
|
|
+ justify-content: center;
|
|
|
+ flex-grow: 1;
|
|
|
}
|
|
|
|
|
|
+/* 无数据提示 */
|
|
|
.no-data {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
align-items: center;
|
|
|
gap: 10px;
|
|
|
color: #999;
|
|
|
+ min-height: 300px;
|
|
|
+ justify-content: center;
|
|
|
+ flex-grow: 1;
|
|
|
}
|
|
|
|
|
|
+/* 结果图片 */
|
|
|
.result-img {
|
|
|
width: 100%;
|
|
|
- height: auto;
|
|
|
+ height: 350px;
|
|
|
object-fit: contain;
|
|
|
+ border-radius: 4px;
|
|
|
+ margin-top: 10px;
|
|
|
+ flex-grow: 1;
|
|
|
+ background-color: #f9f9f9;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+
|
|
|
+.result-img:empty {
|
|
|
+ display: none;
|
|
|
+}
|
|
|
+
|
|
|
+/* 响应式设计 */
|
|
|
+@media (max-width: 992px) {
|
|
|
+ .forecast-controls {
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 15px;
|
|
|
+ align-items: stretch;
|
|
|
+ }
|
|
|
+
|
|
|
+ .year-selector {
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: stretch;
|
|
|
+ }
|
|
|
+
|
|
|
+ .custom-button {
|
|
|
+ width: 100%;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 图标样式 */
|
|
|
+.el-icon {
|
|
|
+ color: var(--primary-color);
|
|
|
+ font-size: 18px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 输入框样式 */
|
|
|
+.el-input {
|
|
|
+ border: 1px solid var(--border-color);
|
|
|
+ border-radius: 4px;
|
|
|
+ transition: border-color 0.3s;
|
|
|
+}
|
|
|
+
|
|
|
+.el-input:focus {
|
|
|
+ border-color: var(--primary-color);
|
|
|
+ box-shadow: 0 0 5px var(--primary-color);
|
|
|
+}
|
|
|
+
|
|
|
+.el-input__inner {
|
|
|
+ padding: 8px 12px;
|
|
|
+ font-size: 14px;
|
|
|
+ color: var(--text-color);
|
|
|
}
|
|
|
</style>
|