| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437 |
- <template>
- <div class="container">
- <!-- 顶部操作栏 -->
- <div class="toolbar">
- <div class="forecast-controls">
- <div class="year-selector">
- <!-- 新增:年份输入标题 -->
- <span class="input-label">预测年份:</span>
- <el-input
- v-model.number="forecastYear"
- type="number"
- placeholder="输入预测年份(1-100)"
- :min="1"
- :max="100"
- @keypress.enter="generateForecast"
- :class="{ 'is-invalid': !isValidYear }"
- class="custom-input"
- ></el-input>
- <el-button
- class="custom-button"
- :loading="isGenerating"
- :disabled="(!isValidYear || isGenerating)"
- @click="generateForecast"
- >
- 开始预测
- </el-button>
- </div>
- </div>
- </div>
- <!-- 主体内容区 -->
- <div class="content-area">
- <!-- 预测结果展示区 - 垂直排列 -->
- <!-- 地图部分 -->
- <div class="visualization-section">
- <h3>土壤Cd未来浓度预测地图</h3>
- <div class="image-container">
- <div v-if="loadingMap" class="loading-container">
- <el-icon class="loading-icon"><Loading /></el-icon>
- <span>地图加载中...</span>
- </div>
- <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="visualization-section">
- <h3>预测直方图</h3>
- <div class="image-container">
- <div v-if="loadingHistogram" class="loading-container">
- <el-icon class="loading-icon"><Loading /></el-icon>
- <span>直方图加载中...</span>
- </div>
- <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>
- </template>
- <script>
- import { api8000 } from '@/utils/request';
- import { Loading, Picture, Histogram, Right } from '@element-plus/icons-vue';
- export default {
- name: 'FutureCdPrediction',
- components: {
- Loading,
- Picture,
- Histogram,
- Right
- },
- 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. 尝试加载现有预测数据
- await this.checkExistingData();
- // 2. 如果存在历史数据,直接显示
- this.$message.success(`成功加载${this.forecastYear}年预测数据`);
- this.isGenerating = false;
- return;
- } catch (loadError) {
- console.log('未找到历史数据,开始生成预测...', loadError);
- }
- 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)
- ]);
- },
- // 生成新预测
- 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);
- } else {
- this.histogramBlob = response.data;
- this.histogramImageUrl = URL.createObjectURL(this.histogramBlob);
- }
- } catch (error) {
- // 显式抛出错误供外部捕获
- throw new Error(`加载${type}失败: ${error.message}`);
- }
- },
- }
- };
- </script>
- <style scoped>
- /* 自定义覆盖样式 */
- .container {
- padding: 20px;
- background: linear-gradient(
- 135deg,
- rgba(230, 247, 255, 0.7) 0%,
- rgba(240, 248, 255, 0.7) 100%
- );
- min-height: 100vh;
- box-sizing: border-box;
- }
- /* 结果展示区 - 垂直排列 */
- .content-area {
- display: flex;
- flex-direction: column;
- gap: 25px;
- margin-top: 15px;
- }
- /* 可视化区域通用样式 */
- .visualization-section {
- background-color: rgba(255, 255, 255, 0.85);
- border-radius: 10px;
- padding: 20px;
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
- position: relative;
- backdrop-filter: blur(5px);
- overflow: hidden;
- transition: all 0.3s ease;
- border: 1px solid rgba(100, 180, 255, 0.2);
- }
- .visualization-section:hover {
- box-shadow: 0 6px 20px rgba(0, 100, 200, 0.12);
- transform: translateY(-3px);
- }
- .visualization-section h3 {
- margin-top: 0;
- margin-bottom: 15px;
- color: #2c3e50;
- font-size: 1.3rem;
- padding-bottom: 10px;
- border-bottom: 1px solid #eaeaea;
- display: flex;
- align-items: center;
- }
- .visualization-section h3 i {
- margin-right: 8px;
- color: #47C3B9;
- }
- /* 图片容器样式 */
- .image-container {
- position: relative;
- min-height: 350px;
- border-radius: 6px;
- overflow: hidden;
- background-color: #f8fafc;
- border: 1px dashed #cbd5e0;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .result-img {
- width: 100%;
- max-height: 500px;
- object-fit: contain;
- border-radius: 4px;
- transition: opacity 0.3s;
- }
- /* 加载状态样式 */
- .loading-container {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- height: 100%;
- color: #4a5568;
- }
- .loading-icon {
- font-size: 2rem;
- margin-bottom: 10px;
- color: #47C3B9;
- animation: spin 1.5s linear infinite;
- }
- @keyframes spin {
- 0% { transform: rotate(0deg); }
- 100% { transform: rotate(360deg); }
- }
- /* 无数据状态样式 */
- .no-data {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- height: 100%;
- color: #a0aec0;
- text-align: center;
- padding: 20px;
- }
- .no-data p {
- margin-top: 10px;
- font-size: 1.1rem;
- }
- /* 工具栏样式增强 */
- .toolbar {
- display: flex;
- flex-direction: column;
- gap: 15px;
- margin-bottom: 20px;
- padding: 20px;
- background-color: rgba(255, 255, 255, 0.9);
- border-radius: 10px;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
- backdrop-filter: blur(5px);
- border: 1px solid rgba(100, 180, 255, 0.2);
- }
- .forecast-controls {
- display: flex;
- flex-wrap: wrap;
- gap: 15px;
- align-items: center;
- }
- .year-selector {
- display: flex;
- flex-wrap: wrap;
- gap: 15px;
- align-items: center;
- flex: 1;
- }
- .custom-input {
- flex: 1;
- min-width: 200px;
- }
- .custom-input.is-invalid {
- border-color: #fc8181;
- }
- .custom-button {
- background: linear-gradient(135deg, #47C3B9 0%, #3ba0a0 100%) !important;
- color: white !important;
- border: none;
- border-radius: 15px;
- padding: 10px 25px;
- font-weight: bold;
- transition: all 0.3s ease;
- box-shadow: 0 4px 6px rgba(71, 195, 185, 0.2);
- }
- .custom-button:hover:not(:disabled) {
- transform: translateY(-2px);
- box-shadow: 0 6px 8px rgba(71, 195, 185, 0.3);
- }
- .custom-button:disabled {
- opacity: 0.7;
- cursor: not-allowed;
- }
- /* 响应式设计 */
- @media (min-width: 992px) {
- .content-area {
- flex-direction: row;
- flex-wrap: wrap;
- }
-
- .visualization-section {
- flex: 1;
- min-width: calc(50% - 15px);
- }
- }
- @media (max-width: 768px) {
- .toolbar {
- padding: 15px;
- }
-
- .visualization-section {
- padding: 15px;
- }
-
- .image-container {
- min-height: 300px;
- }
- }
- /* 结果提示动画 */
- .fade-enter-active, .fade-leave-active {
- transition: opacity 0.5s;
- }
- .fade-enter, .fade-leave-to {
- opacity: 0;
- }
- /* 新增:输入栏标题样式 */
- .input-label {
- font-size: 14px;
- color: #666;
- white-space: nowrap; /* 禁止换行,保持标签与输入框对齐 */
- margin-right: 8px; /* 与输入框的间距 */
- }
- /* 新增:限定输入框长度 */
- .custom-input {
- flex: 1;
- min-width: 200px; /* 最小宽度,避免过窄 */
- max-width: 320px; /* 最大宽度,限制输入栏长度 */
- }
- /* 调整按钮与输入框的间距(可选) */
- .custom-button {
- margin-left: auto; /* 让按钮靠右(若需要) */
- /* 原有按钮样式不变 */
- }
- </style>
|