|
@@ -0,0 +1,264 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="container">
|
|
|
|
|
+ <!-- 顶部操作栏 -->
|
|
|
|
|
+ <div class="forecast-controls">
|
|
|
|
|
+ <div class="year-selector">
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model.number="forecastYear"
|
|
|
|
|
+ type="number"
|
|
|
|
|
+ placeholder="输入预测年份(1-100)"
|
|
|
|
|
+ :min="1"
|
|
|
|
|
+ :max="100"
|
|
|
|
|
+ @keypress.enter="generateForecast"
|
|
|
|
|
+ :class="{ 'is-invalid': !isValidYear }"
|
|
|
|
|
+ ></el-input>
|
|
|
|
|
+ <el-button
|
|
|
|
|
+ class="custom-button"
|
|
|
|
|
+ :loading="isGenerating"
|
|
|
|
|
+ :disabled="!canGenerate"
|
|
|
|
|
+ @click="generateForecast"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-icon class="play-icon"></el-icon>
|
|
|
|
|
+ 开始预测
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 主体内容区 -->
|
|
|
|
|
+ <div class="content-area">
|
|
|
|
|
+ <!-- 预测结果展示区 -->
|
|
|
|
|
+ <div class="result-display">
|
|
|
|
|
+ <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 :src=mapImageUrl class="result-img"></img>
|
|
|
|
|
+ <div v-if="!loadingMap && !mapImageUrl" class="no-data">
|
|
|
|
|
+ <el-icon><Picture /></el-icon>
|
|
|
|
|
+ <p>暂无地图数据</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="histogram-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>
|
|
|
|
|
+ <p>暂无直方图数据</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script>
|
|
|
|
|
+import { SaveAs } from 'file-saver';
|
|
|
|
|
+import { api8000 } from '@/utils/request';
|
|
|
|
|
+import {
|
|
|
|
|
+ Loading, Picture, Histogram
|
|
|
|
|
+} from '@element-plus/icons-vue';
|
|
|
|
|
+
|
|
|
|
|
+export default {
|
|
|
|
|
+ name: 'FutureCdPrediction',
|
|
|
|
|
+ props: {
|
|
|
|
|
+ countyName: {
|
|
|
|
|
+ type: String,
|
|
|
|
|
+ required: true,
|
|
|
|
|
+ default: '乐昌市'
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ data() {
|
|
|
|
|
+ return {
|
|
|
|
|
+ forecastYear: null,
|
|
|
|
|
+ isGenerating: false,
|
|
|
|
|
+ generateVisualization: true,
|
|
|
|
|
+ loadingMap: false,
|
|
|
|
|
+ loadingHistogram: false,
|
|
|
|
|
+ mapBlob: null,
|
|
|
|
|
+ histogramBlob: null,
|
|
|
|
|
+ mapImageUrl: '',
|
|
|
|
|
+ histogramImageUrl: ''
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ computed: {
|
|
|
|
|
+ isValidYear() {
|
|
|
|
|
+ return this.forecastYear >= 1 && this.forecastYear <= 100;
|
|
|
|
|
+ },
|
|
|
|
|
+ canGenerate() {
|
|
|
|
|
+ return this.isValidYear && !this.isGenerating;
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ watch: {
|
|
|
|
|
+ forecastYear(newVal) {
|
|
|
|
|
+ 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)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 加载结果(保持原有逻辑)
|
|
|
|
|
+ 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 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)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.histogramBlob = response.data
|
|
|
|
|
+ this.histogramImageUrl = URL.createObjectURL(this.histogramBlob)
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ this.$message.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>
|
|
|
|
|
+.container {
|
|
|
|
|
+ max-width: 1400px;
|
|
|
|
|
+ margin: 20px auto;
|
|
|
|
|
+ padding: 0 20px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.toolbar {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ padding: 15px;
|
|
|
|
|
+ background-color: #f5f7fa;
|
|
|
|
|
+ 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;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.action-buttons {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.result-display {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: 1fr 1fr;
|
|
|
|
|
+ gap: 20px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.map-section, .histogram-section {
|
|
|
|
|
+ background-color: #fff;
|
|
|
|
|
+ padding: 20px;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+h3 {
|
|
|
|
|
+ margin-top: 0;
|
|
|
|
|
+ margin-bottom: 15px;
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.loading-container {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.no-data {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ color: #999;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.result-img {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: auto;
|
|
|
|
|
+ object-fit: contain;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|