EffectiveCadmiumPrediction.vue 14 KB

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