FutureCadmiumPrediction.vue 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. <template>
  2. <div class="container">
  3. <!-- 顶部操作栏 -->
  4. <div class="forecast-controls">
  5. <div class="year-selector">
  6. <el-input
  7. v-model.number="forecastYear"
  8. type="number"
  9. placeholder="输入预测年份(1-100)"
  10. :min="1"
  11. :max="100"
  12. @keypress.enter="generateForecast"
  13. :class="{ 'is-invalid': !isValidYear }"
  14. ></el-input>
  15. <el-button
  16. class="custom-button"
  17. :loading="isGenerating"
  18. :disabled="!canGenerate"
  19. @click="generateForecast"
  20. >
  21. <el-icon class="play-icon"></el-icon>
  22. 开始预测
  23. </el-button>
  24. </div>
  25. </div>
  26. <!-- 主体内容区 -->
  27. <div class="content-area">
  28. <!-- 预测结果展示区 -->
  29. <div class="result-display">
  30. <div class="map-section">
  31. <h3>未来Cd浓度预测地图</h3>
  32. <div v-if="loadingMap" class="loading-container">
  33. <el-icon class="loading-icon"><Loading /></el-icon>
  34. <span>地图加载中...</span>
  35. </div>
  36. <img :src=mapImageUrl class="result-img"></img>
  37. <div v-if="!loadingMap && !mapImageUrl" class="no-data">
  38. <el-icon><Picture /></el-icon>
  39. <p>暂无地图数据</p>
  40. </div>
  41. </div>
  42. <div class="histogram-section">
  43. <h3>预测直方图</h3>
  44. <div v-if="loadingHistogram" class="loading-container">
  45. <el-icon class="loading-icon"><Loading /></el-icon>
  46. <span>直方图加载中...</span>
  47. </div>
  48. <img :src=histogramImageUrl class="result-img"></img>
  49. <div v-if="!loadingHistogram && !histogramImageUrl" class="no-data">
  50. <el-icon><Picture /></el-icon>
  51. <p>暂无直方图数据</p>
  52. </div>
  53. </div>
  54. </div>
  55. </div>
  56. </div>
  57. </template>
  58. <script>
  59. import { SaveAs } from 'file-saver';
  60. import { api8000 } from '@/utils/request';
  61. import {
  62. Loading, Picture, Histogram
  63. } from '@element-plus/icons-vue';
  64. export default {
  65. name: 'FutureCdPrediction',
  66. props: {
  67. countyName: {
  68. type: String,
  69. required: true,
  70. default: '乐昌市'
  71. }
  72. },
  73. data() {
  74. return {
  75. forecastYear: null,
  76. isGenerating: false,
  77. generateVisualization: true,
  78. loadingMap: false,
  79. loadingHistogram: false,
  80. mapBlob: null,
  81. histogramBlob: null,
  82. mapImageUrl: '',
  83. histogramImageUrl: ''
  84. }
  85. },
  86. computed: {
  87. isValidYear() {
  88. return this.forecastYear >= 1 && this.forecastYear <= 100;
  89. },
  90. canGenerate() {
  91. return this.isValidYear && !this.isGenerating;
  92. }
  93. },
  94. watch: {
  95. forecastYear(newVal) {
  96. newVal = parseInt(newVal)
  97. if (isNaN(newVal)) this.forecastYear = null
  98. if (newVal < 1) this.forecastYear = 1
  99. if (newVal > 100) this.forecastYear = 100
  100. }
  101. },
  102. methods: {
  103. // 预测控制逻辑
  104. async generateForecast() {
  105. if (this.isGenerating) return
  106. this.isGenerating = true
  107. this.mapBlob = null
  108. this.histogramBlob = null
  109. this.mapImageUrl = ''
  110. this.histogramImageUrl = ''
  111. try {
  112. // 1. 构建正确URL(使用GET方法+查询参数)
  113. let url = `/api/cd-flux/predict-future-cd?years=${encodeURIComponent(this.forecastYear)}`
  114. if (this.countyName) url += `&area=${encodeURIComponent(this.countyName)}`
  115. if (this.generateVisualization) url += '&generate_visualization=true'
  116. // 2. 发送GET请求(移除FormData)
  117. const predictResponse = await api8000.get(url, { responseType: 'json' })
  118. if (!predictResponse.data.success) {
  119. throw new Error(predictResponse.data.message)
  120. }
  121. // 3. 加载结果(保持原有逻辑)
  122. await Promise.all([
  123. this.loadVisualization('map', this.forecastYear),
  124. this.loadVisualization('histogram', this.forecastYear)
  125. ])
  126. this.$message.success(`预测生成成功(年份:${this.forecastYear})`)
  127. } catch (error) {
  128. this.$message.error(`预测失败:${error.message}`)
  129. } finally {
  130. this.isGenerating = false
  131. }
  132. },
  133. // 结果加载逻辑
  134. async loadVisualization(type, year) {
  135. try {
  136. const response = await api8000.get(
  137. `/api/cd-flux/predict-future-cd/${type}/${year}`,
  138. { responseType: 'blob' }
  139. )
  140. if (type === 'map') {
  141. this.mapBlob = response.data
  142. this.mapImageUrl = URL.createObjectURL(this.mapBlob)
  143. } else {
  144. this.histogramBlob = response.data
  145. this.histogramImageUrl = URL.createObjectURL(this.histogramBlob)
  146. }
  147. } catch (error) {
  148. this.$message.error(`加载${type}失败:${error.message}`)
  149. }
  150. },
  151. // 结果导出逻辑
  152. exportAllResults() {
  153. if (this.mapBlob) SaveAs(this.mapBlob, `future_cd_map_${this.forecastYear}.jpg`)
  154. if (this.histogramBlob) SaveAs(this.histogramBlob, `future_cd_histogram_${this.forecastYear}.jpg`)
  155. }
  156. }
  157. }
  158. </script>
  159. <style scoped>
  160. .container {
  161. max-width: 1400px;
  162. margin: 20px auto;
  163. padding: 0 20px;
  164. }
  165. .toolbar {
  166. display: flex;
  167. justify-content: space-between;
  168. align-items: center;
  169. padding: 15px;
  170. background-color: #f5f7fa;
  171. border-radius: 8px;
  172. margin-bottom: 20px;
  173. }
  174. .upload-section {
  175. display: flex;
  176. align-items: center;
  177. gap: 15px;
  178. }
  179. .forecast-controls {
  180. display: flex;
  181. gap: 15px;
  182. align-items: center;
  183. }
  184. .year-selector {
  185. display: flex;
  186. gap: 10px;
  187. align-items: center;
  188. }
  189. .file-name {
  190. font-size: 14px;
  191. color: #666;
  192. }
  193. .custom-button {
  194. padding: 8px 16px;
  195. border-radius: 4px;
  196. font-size: 14px;
  197. }
  198. .action-buttons {
  199. display: flex;
  200. gap: 10px;
  201. }
  202. .result-display {
  203. display: grid;
  204. grid-template-columns: 1fr 1fr;
  205. gap: 20px;
  206. }
  207. .map-section, .histogram-section {
  208. background-color: #fff;
  209. padding: 20px;
  210. border-radius: 8px;
  211. box-shadow: 0 2px 4px rgba(0,0,0,0.05);
  212. }
  213. h3 {
  214. margin-top: 0;
  215. margin-bottom: 15px;
  216. font-size: 16px;
  217. color: #333;
  218. }
  219. .loading-container {
  220. display: flex;
  221. align-items: center;
  222. gap: 10px;
  223. color: #666;
  224. }
  225. .no-data {
  226. display: flex;
  227. flex-direction: column;
  228. align-items: center;
  229. gap: 10px;
  230. color: #999;
  231. }
  232. .result-img {
  233. width: 100%;
  234. height: auto;
  235. object-fit: contain;
  236. }
  237. </style>