totalOutputFlux.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. <template>
  2. <div class="container">
  3. <!-- 顶部操作栏 -->
  4. <div class="toolbar">
  5. <!-- 文件上传区域 -->
  6. <div class="upload-section">
  7. <input type="file" ref="fileInput" accept=".csv" @change="handleFileUpload" style="display: none">
  8. <el-button class="custom-button" @click="triggerFileUpload">
  9. <el-icon class="upload-icon"><Upload /></el-icon>
  10. 选择CSV文件
  11. </el-button>
  12. <span v-if="selectedFile" class="file-name">{{ selectedFile.name }}</span>
  13. <el-button
  14. class="custom-button"
  15. :loading="isCalculating"
  16. :disabled="!selectedFile"
  17. @click="calculate"
  18. >
  19. <el-icon class="upload-icon"><Document /></el-icon>
  20. 上传并计算
  21. </el-button>
  22. </div>
  23. <!-- 操作按钮 -->
  24. <div class="action-buttons">
  25. <el-button class="custom-button" :disabled="!mapBlob" @click="exportMap">
  26. <el-icon class="upload-icon"><Download /></el-icon>
  27. 导出地图</el-button>
  28. <el-button class="custom-button" :disabled="!histogramBlob" @click="exportHistogram">
  29. <el-icon class="upload-icon"><Download /></el-icon>
  30. 导出直方图</el-button>
  31. <el-button class="custom-button" :disabled="!statisticsData.length" @click="exportData">
  32. <el-icon class="upload-icon"><Download /></el-icon>
  33. 导出数据</el-button>
  34. </div>
  35. </div>
  36. <!-- 主体内容区 -->
  37. <div class="content-area">
  38. <!-- 地图区域 - 修改为横向布局 -->
  39. <div class="horizontal-container">
  40. <!-- 地图展示 -->
  41. <div class="map-section">
  42. <h3>Cd输出通量空间分布图</h3>
  43. <div v-if="loadingMap" class="loading-container">
  44. <el-icon class="loading-icon"><Loading /></el-icon>
  45. <span>地图加载中...</span>
  46. </div>
  47. <img v-if="mapImageUrl && !loadingMap" :src="mapImageUrl" alt="Cd输出通量空间分布图" class="map-image">
  48. <div v-if="!mapImageUrl && !loadingMap" class="no-data">
  49. <el-icon><Picture /></el-icon>
  50. <p>暂无地图数据</p>
  51. </div>
  52. </div>
  53. <!-- 直方图展示 -->
  54. <div class="histogram-section">
  55. <h3>Cd输出通量直方图</h3>
  56. <div v-if="loadingHistogram" class="loading-container">
  57. <el-icon class="loading-icon"><Loading /></el-icon>
  58. <span>直方图加载中...</span>
  59. </div>
  60. <img v-if="histogramImageUrl && !loadingHistogram" :src="histogramImageUrl" alt="Cd输出通量直方图" class="histogram-image">
  61. <div v-if="!histogramImageUrl && !loadingHistogram" class="no-data">
  62. <el-icon><Histogram /></el-icon>
  63. <p>暂无直方图数据</p>
  64. </div>
  65. </div>
  66. </div>
  67. <!-- 统计图表区域 -->
  68. <div class="stats-area">
  69. <h3>Cd输出通量统计信息</h3>
  70. <div class="model-info">
  71. <el-tag type="info">Cd通量模型</el-tag>
  72. <span class="update-time">
  73. 最后更新: {{ updateTime ? new Date(updateTime).toLocaleString() : '未知' }}
  74. </span>
  75. </div>
  76. <div v-if="loadingStats" class="loading-container">
  77. <el-icon class="loading-icon"><Loading /></el-icon>
  78. <span>统计数据加载中...</span>
  79. </div>
  80. <div v-if="!loadingStats && statisticsData.length" class="stats-container">
  81. <!-- 统计表格 -->
  82. <el-table
  83. :data="statisticsData"
  84. style="width: 100%; margin-bottom: 20px;"
  85. border
  86. stripe
  87. >
  88. <el-table-column prop="name" label="统计项" min-width="180" />
  89. <el-table-column prop="value" label="值" min-width="150" />
  90. <el-table-column prop="unit" label="单位" min-width="100" />
  91. <el-table-column prop="description" label="描述" min-width="200" />
  92. </el-table>
  93. <div v-if="!loadingStats && !statisticsData.length" class="no-data">
  94. <el-icon><DataAnalysis /></el-icon>
  95. <p>暂无统计数据</p>
  96. </div>
  97. </div>
  98. </div>
  99. </div>
  100. </div>
  101. </template>
  102. <script>
  103. import * as XLSX from 'xlsx';
  104. import { saveAs } from 'file-saver';
  105. import axios from 'axios';
  106. import * as echarts from 'echarts';
  107. import {
  108. Loading, Upload, Picture, Histogram, Download, Document, DataAnalysis
  109. } from '@element-plus/icons-vue';
  110. export default {
  111. name: 'CdFluxVisualization',
  112. components: {
  113. Loading, Upload, Picture, Histogram, Download, Document, DataAnalysis
  114. },
  115. data() {
  116. return {
  117. isCalculating: false,
  118. loadingMap: false,
  119. loadingHistogram: false,
  120. loadingStats: false,
  121. statisticsData: [],
  122. mapImageUrl: null,
  123. histogramImageUrl: null,
  124. mapBlob: null,
  125. histogramBlob: null,
  126. selectedFile: null,
  127. distributionChart: null,
  128. updateTime: null
  129. };
  130. },
  131. mounted() {
  132. // 组件挂载时获取最新数据
  133. this.fetchLatestResults();
  134. this.fetchStatistics();
  135. },
  136. beforeDestroy() {
  137. if (this.mapImageUrl) URL.revokeObjectURL(this.mapImageUrl);
  138. if (this.histogramImageUrl) URL.revokeObjectURL(this.histogramImageUrl);
  139. if (this.distributionChart) this.distributionChart.dispose();
  140. },
  141. methods: {
  142. // 触发文件选择
  143. triggerFileUpload() {
  144. this.$refs.fileInput.click();
  145. },
  146. // 处理文件上传
  147. handleFileUpload(event) {
  148. const files = event.target.files;
  149. if (files && files.length > 0) {
  150. this.selectedFile = files[0];
  151. } else {
  152. this.selectedFile = null;
  153. }
  154. },
  155. // 获取最新结果
  156. async fetchLatestResults() {
  157. try {
  158. this.loadingMap = true;
  159. this.loadingHistogram = true;
  160. // 获取最新地图
  161. await this.fetchLatestMap();
  162. // 获取最新直方图
  163. await this.fetchLatestHistogram();
  164. } catch (error) {
  165. console.error('获取最新结果失败:', error);
  166. this.$message.error('获取最新结果失败');
  167. } finally {
  168. this.loadingMap = false;
  169. this.loadingHistogram = false;
  170. }
  171. },
  172. // 获取最新地图
  173. async fetchLatestMap() {
  174. try {
  175. const response = await axios.get(
  176. `http://localhost:8000/api/cd-flux/output/map`,
  177. { responseType: 'blob' }
  178. );
  179. this.mapBlob = response.data;
  180. this.mapImageUrl = URL.createObjectURL(this.mapBlob);
  181. } catch (error) {
  182. console.error('获取最新地图失败:', error);
  183. this.$message.warning('获取最新地图失败,请先执行预测');
  184. }
  185. },
  186. // 获取最新直方图
  187. async fetchLatestHistogram() {
  188. try {
  189. const response = await axios.get(
  190. `http://localhost:8000/api/cd-flux/output/histogram`,
  191. { responseType: 'blob' }
  192. );
  193. this.histogramBlob = response.data;
  194. this.histogramImageUrl = URL.createObjectURL(this.histogramBlob);
  195. } catch (error) {
  196. console.error('获取最新直方图失败:', error);
  197. this.$message.warning('获取最新直方图失败,请先执行预测');
  198. }
  199. },
  200. // 格式化统计数据
  201. formatStatisticsData(stats) {
  202. if (!stats) return [];
  203. return [
  204. { name: '最小值', value: stats.min.toFixed(4), unit: 'g/ha/year', description: '样本中的最小Cd通量' },
  205. { name: '最大值', value: stats.max.toFixed(4), unit: 'g/ha/year', description: '样本中的最大Cd通量' },
  206. { name: '平均值', value: stats.mean.toFixed(4), unit: 'g/ha/year', description: '所有样本的平均Cd通量' },
  207. { name: '标准差', value: stats.std.toFixed(4), unit: 'g/ha/year', description: 'Cd通量的标准差' },
  208. ];
  209. },
  210. // 修改fetchStatistics方法
  211. async fetchStatistics() {
  212. try {
  213. this.loadingStats = true;
  214. const response = await axios.get(
  215. `http://localhost:8000/api/cd-flux/output/statistics`
  216. );
  217. if (response.data) {
  218. const stats = response.data;
  219. this.statisticsData = this.formatStatisticsData(stats);
  220. this.updateTime = new Date().toISOString();
  221. this.$nextTick(() => {
  222. this.initCharts(stats);
  223. });
  224. }
  225. } catch (error) {
  226. console.error('获取统计信息失败:', error);
  227. this.$message.warning('获取统计信息失败');
  228. } finally {
  229. this.loadingStats = false;
  230. }
  231. },
  232. // 处理窗口大小变化
  233. handleResize() {
  234. if (this.distributionChart) this.distributionChart.resize();
  235. },
  236. // 上传并计算
  237. async calculate() {
  238. if (!this.selectedFile) {
  239. this.$message.warning('请先选择CSV文件');
  240. return;
  241. }
  242. try {
  243. this.isCalculating = true;
  244. this.loadingMap = true;
  245. this.loadingHistogram = true;
  246. this.loadingStats = true;
  247. // 创建FormData
  248. const formData = new FormData();
  249. formData.append('csv_file', this.selectedFile);
  250. // 调用Cd通量计算接口
  251. await axios.post(
  252. 'http://localhost:8000/api/cd-flux/output/calculate',
  253. formData,
  254. {
  255. headers: {
  256. 'Content-Type': 'multipart/form-data'
  257. }
  258. }
  259. );
  260. // 更新后重新获取地图、直方图和统计数据
  261. await this.fetchLatestResults();
  262. await this.fetchStatistics();
  263. this.$message.success('计算完成!');
  264. } catch (error) {
  265. console.error('计算失败:', error);
  266. let errorMessage = '计算失败,请重试';
  267. if (error.response) {
  268. if (error.response.status === 400) {
  269. errorMessage = '文件格式错误:' + (error.response.data.detail || '请上传正确的CSV文件');
  270. } else if (error.response.status === 500) {
  271. errorMessage = '服务器错误:' + (error.response.data.detail || '请稍后重试');
  272. }
  273. }
  274. this.$message.error(errorMessage);
  275. } finally {
  276. this.isCalculating = false;
  277. this.loadingMap = false;
  278. this.loadingHistogram = false;
  279. this.loadingStats = false;
  280. }
  281. },
  282. // 导出地图
  283. exportMap() {
  284. if (!this.mapBlob) {
  285. this.$message.warning('请先计算生成地图');
  286. return;
  287. }
  288. const link = document.createElement('a');
  289. link.href = URL.createObjectURL(this.mapBlob);
  290. link.download = `Cd输出通量空间分布图.jpg`;
  291. link.click();
  292. URL.revokeObjectURL(link.href);
  293. },
  294. // 导出直方图
  295. exportHistogram() {
  296. if (!this.histogramBlob) {
  297. this.$message.warning('请先计算生成直方图');
  298. return;
  299. }
  300. const link = document.createElement('a');
  301. link.href = URL.createObjectURL(this.histogramBlob);
  302. link.download = `Cd输出通量直方图.jpg`;
  303. link.click();
  304. URL.revokeObjectURL(link.href);
  305. },
  306. // 导出数据
  307. async exportData() {
  308. try {
  309. this.$message.info('正在获取Cd输出通量数据...');
  310. const response = await axios.get(
  311. `http://localhost:8000/api/cd-flux/output/export-csv`,
  312. { responseType: 'blob' }
  313. );
  314. const blob = new Blob([response.data], { type: 'text/csv' });
  315. const link = document.createElement('a');
  316. link.href = URL.createObjectURL(blob);
  317. link.download = `Cd输出通量数据.csv`;
  318. link.click();
  319. URL.revokeObjectURL(link.href);
  320. this.$message.success('数据导出成功');
  321. } catch (error) {
  322. console.error('导出数据失败:', error);
  323. this.$message.error('导出数据失败: ' + (error.response?.data?.detail || '请稍后重试'));
  324. }
  325. }
  326. }
  327. };
  328. </script>
  329. <style scoped>
  330. /* 保持原有样式不变 */
  331. .container {
  332. padding: 20px;
  333. /* 添加70%透明度的渐变背景 */
  334. background: linear-gradient(
  335. 135deg,
  336. rgba(230, 247, 255, 0.7) 0%,
  337. rgba(240, 248, 255, 0.7) 100%
  338. );
  339. min-height: 100vh;
  340. box-sizing: border-box;
  341. }
  342. .toolbar {
  343. display: flex;
  344. flex-direction: column;
  345. gap: 15px;
  346. margin-bottom: 20px;
  347. padding: 15px;
  348. background-color: rgba(255, 255, 255, 0.8); /* 调整为半透明白色 */
  349. border-radius: 8px;
  350. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
  351. backdrop-filter: blur(5px); /* 添加模糊效果增强半透明感 */
  352. }
  353. .upload-section {
  354. display: flex;
  355. align-items: center;
  356. gap: 15px;
  357. padding-bottom: 15px;
  358. border-bottom: 1px solid rgba(0, 0, 0, 0.1); /* 调整边框透明度 */
  359. }
  360. .file-name {
  361. flex: 1;
  362. padding: 0 10px;
  363. color: #666;
  364. font-size: 14px;
  365. overflow: hidden;
  366. text-overflow: ellipsis;
  367. white-space: nowrap;
  368. }
  369. .action-buttons {
  370. display: flex;
  371. gap: 10px;
  372. }
  373. .custom-button {
  374. background-color: #47C3B9 !important;
  375. color: #DCFFFA !important;
  376. border: none;
  377. border-radius: 155px;
  378. padding: 10px 20px;
  379. font-weight: bold;
  380. display: flex;
  381. align-items: center;
  382. }
  383. .upload-icon {
  384. margin-right: 5px;
  385. }
  386. .content-area {
  387. display: flex;
  388. flex-direction: column;
  389. gap: 20px;
  390. }
  391. /* 横向布局容器 */
  392. .horizontal-container {
  393. display: flex;
  394. flex-wrap: wrap;
  395. gap: 20px;
  396. width: 100%;
  397. }
  398. .map-section, .histogram-section {
  399. flex: 1;
  400. min-width: 300px;
  401. background-color: rgba(255, 255, 255, 0.8); /* 调整为半透明白色 */
  402. border-radius: 8px;
  403. padding: 15px;
  404. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
  405. position: relative;
  406. min-height: 400px;
  407. backdrop-filter: blur(5px); /* 添加模糊效果增强半透明感 */
  408. }
  409. .map-image, .histogram-image {
  410. width: 100%;
  411. height: 100%;
  412. max-height: 600px;
  413. object-fit: contain;
  414. border-radius: 4px;
  415. }
  416. .table-area {
  417. width: 100%;
  418. background-color: rgba(255, 255, 255, 0.8); /* 调整为半透明白色 */
  419. border-radius: 8px;
  420. padding: 15px;
  421. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
  422. margin-top: 20px;
  423. backdrop-filter: blur(5px); /* 添加模糊效果增强半透明感 */
  424. }
  425. .loading-container {
  426. display: flex;
  427. flex-direction: column;
  428. align-items: center;
  429. justify-content: center;
  430. height: 300px;
  431. color: #47C3B9;
  432. }
  433. .no-data {
  434. display: flex;
  435. flex-direction: column;
  436. align-items: center;
  437. justify-content: center;
  438. height: 300px;
  439. color: #999;
  440. font-size: 16px;
  441. }
  442. .no-data .el-icon {
  443. font-size: 48px;
  444. margin-bottom: 10px;
  445. }
  446. .loading-icon {
  447. font-size: 36px;
  448. margin-bottom: 10px;
  449. animation: rotate 2s linear infinite;
  450. }
  451. @keyframes rotate {
  452. from {
  453. transform: rotate(0deg);
  454. }
  455. to {
  456. transform: rotate(360deg);
  457. }
  458. }
  459. /* 响应式布局调整 */
  460. @media (max-width: 992px) {
  461. .horizontal-container {
  462. flex-direction: column;
  463. }
  464. .map-section, .histogram-section {
  465. width: 100%;
  466. flex: none;
  467. }
  468. }
  469. </style>