TotalCadmiumPrediction.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  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="!tableData.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="table-area">
  69. <h3>表格数据</h3>
  70. <el-table :data="tableData" style="width: 100%;">
  71. <el-table-column prop="name" label="名称" width="180" />
  72. <el-table-column prop="value" label="值" width="100" />
  73. <el-table-column prop="unit" label="单位" width="100" />
  74. <el-table-column prop="description" label="描述" />
  75. </el-table>
  76. </div>
  77. </div>
  78. </div>
  79. </template>
  80. <script>
  81. import * as XLSX from 'xlsx';
  82. import { saveAs } from 'file-saver';
  83. import axios from 'axios';
  84. import { Loading, Upload, Picture, Histogram, Download, Document } from '@element-plus/icons-vue';
  85. export default {
  86. name: 'EffectiveCadmiumPrediction',
  87. components: { Loading, Upload, Picture, Histogram, Download, Document },
  88. data() {
  89. return {
  90. isCalculating: false,
  91. loadingMap: false,
  92. loadingHistogram: false,
  93. tableData: [],
  94. mapImageUrl: null,
  95. histogramImageUrl: null,
  96. mapBlob: null,
  97. histogramBlob: null,
  98. selectedFile: null,
  99. countyName: '乐昌市' // 默认县市名称
  100. };
  101. },
  102. mounted() {
  103. // 组件挂载时获取最新数据
  104. this.fetchLatestResults();
  105. },
  106. methods: {
  107. // 触发文件选择
  108. triggerFileUpload() {
  109. this.$refs.fileInput.click();
  110. },
  111. // 处理文件上传
  112. handleFileUpload(event) {
  113. const files = event.target.files;
  114. if (files && files.length > 0) {
  115. this.selectedFile = files[0];
  116. } else {
  117. this.selectedFile = null;
  118. }
  119. },
  120. // 获取最新结果
  121. async fetchLatestResults() {
  122. try {
  123. this.loadingMap = true;
  124. this.loadingHistogram = true;
  125. // 获取最新地图
  126. await this.fetchLatestMap();
  127. // 获取最新直方图
  128. await this.fetchLatestHistogram();
  129. } catch (error) {
  130. console.error('获取最新结果失败:', error);
  131. this.$message.error('获取最新结果失败');
  132. } finally {
  133. this.loadingMap = false;
  134. this.loadingHistogram = false;
  135. }
  136. },
  137. // 获取最新地图
  138. async fetchLatestMap() {
  139. try {
  140. const response = await axios.get(
  141. `https://soilgd.com:8000/api/cd-prediction/effective-cd/latest-map/${this.countyName}`,
  142. { responseType: 'blob' }
  143. );
  144. this.mapBlob = response.data;
  145. this.mapImageUrl = URL.createObjectURL(this.mapBlob);
  146. } catch (error) {
  147. console.error('获取最新地图失败:', error);
  148. this.$message.warning('获取最新地图失败,请先执行预测');
  149. }
  150. },
  151. // 获取最新直方图
  152. async fetchLatestHistogram() {
  153. try {
  154. const response = await axios.get(
  155. `https://soilgd.com:8000/api/cd-prediction/effective-cd/latest-histogram/${this.countyName}`,
  156. { responseType: 'blob' }
  157. );
  158. this.histogramBlob = response.data;
  159. this.histogramImageUrl = URL.createObjectURL(this.histogramBlob);
  160. } catch (error) {
  161. console.error('获取最新直方图失败:', error);
  162. this.$message.warning('获取最新直方图失败,请先执行预测');
  163. }
  164. },
  165. // 上传并计算
  166. async calculate() {
  167. if (!this.selectedFile) {
  168. this.$message.warning('请先选择CSV文件');
  169. return;
  170. }
  171. try {
  172. this.isCalculating = true;
  173. this.loadingMap = true;
  174. this.loadingHistogram = true;
  175. // 创建FormData
  176. const formData = new FormData();
  177. formData.append('county_name', this.countyName);
  178. formData.append('data_file', this.selectedFile);
  179. // 调用有效态Cd地图接口
  180. const mapResponse = await axios.post(
  181. 'https://soilgd.com:8000/api/cd-prediction/effective-cd/generate-and-get-map',
  182. formData,
  183. {
  184. headers: {
  185. 'Content-Type': 'multipart/form-data'
  186. },
  187. responseType: 'blob'
  188. }
  189. );
  190. // 保存地图数据
  191. this.mapBlob = mapResponse.data;
  192. this.mapImageUrl = URL.createObjectURL(this.mapBlob);
  193. // 更新后重新获取直方图(因为生成新数据后直方图也会更新)
  194. await this.fetchLatestHistogram();
  195. // 更新表格数据(示例)
  196. this.tableData = [
  197. { name: '样本1', value: 10, unit: 'mg/L', description: '描述1' },
  198. { name: '样本2', value: 20, unit: 'mg/L', description: '描述2' }
  199. ];
  200. this.$message.success('计算完成!');
  201. } catch (error) {
  202. console.error('计算失败:', error);
  203. let errorMessage = '计算失败,请重试';
  204. if (error.response) {
  205. // 处理不同错误状态码
  206. if (error.response.status === 400) {
  207. errorMessage = '文件格式错误:' + (error.response.data.detail || '请上传正确的CSV文件');
  208. } else if (error.response.status === 404) {
  209. errorMessage = '不支持的县市:' + this.countyName;
  210. } else if (error.response.status === 500) {
  211. errorMessage = '服务器错误:' + (error.response.data.detail || '请稍后重试');
  212. }
  213. }
  214. this.$message.error(errorMessage);
  215. } finally {
  216. this.isCalculating = false;
  217. this.loadingMap = false;
  218. this.loadingHistogram = false;
  219. }
  220. },
  221. // 导出地图
  222. exportMap() {
  223. if (!this.mapBlob) {
  224. this.$message.warning('请先计算生成地图');
  225. return;
  226. }
  227. const link = document.createElement('a');
  228. link.href = URL.createObjectURL(this.mapBlob);
  229. link.download = `${this.countyName}_有效态Cd预测地图.jpg`;
  230. link.click();
  231. URL.revokeObjectURL(link.href);
  232. },
  233. // 导出直方图
  234. exportHistogram() {
  235. if (!this.histogramBlob) {
  236. this.$message.warning('请先计算生成直方图');
  237. return;
  238. }
  239. const link = document.createElement('a');
  240. link.href = URL.createObjectURL(this.histogramBlob);
  241. link.download = `${this.countyName}_有效态Cd预测直方图.jpg`;
  242. link.click();
  243. URL.revokeObjectURL(link.href);
  244. },
  245. // 导出数据
  246. exportData() {
  247. if (!this.tableData.length) {
  248. this.$message.warning('暂无数据可导出');
  249. return;
  250. }
  251. const workbook = XLSX.utils.book_new();
  252. const worksheet = XLSX.utils.json_to_sheet(this.tableData);
  253. XLSX.utils.book_append_sheet(workbook, worksheet, '有效态Cd数据');
  254. const excelBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
  255. const excelData = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
  256. saveAs(excelData, `${this.countyName}_有效态Cd数据.xlsx`);
  257. }
  258. },
  259. beforeDestroy() {
  260. if (this.mapImageUrl) URL.revokeObjectURL(this.mapImageUrl);
  261. if (this.histogramImageUrl) URL.revokeObjectURL(this.histogramImageUrl);
  262. }
  263. };
  264. </script>
  265. <style scoped>
  266. .container {
  267. padding: 20px;
  268. /* 添加70%透明度的渐变背景 */
  269. background: linear-gradient(
  270. 135deg,
  271. rgba(230, 247, 255, 0.7) 0%,
  272. rgba(240, 248, 255, 0.7) 100%
  273. );
  274. min-height: 100vh;
  275. box-sizing: border-box;
  276. }
  277. .toolbar {
  278. display: flex;
  279. flex-direction: column;
  280. gap: 15px;
  281. margin-bottom: 20px;
  282. padding: 15px;
  283. background-color: rgba(255, 255, 255, 0.8); /* 调整为半透明白色 */
  284. border-radius: 8px;
  285. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
  286. backdrop-filter: blur(5px); /* 添加模糊效果增强半透明感 */
  287. }
  288. .upload-section {
  289. display: flex;
  290. align-items: center;
  291. gap: 15px;
  292. padding-bottom: 15px;
  293. border-bottom: 1px solid rgba(0, 0, 0, 0.1); /* 调整边框透明度 */
  294. }
  295. .file-name {
  296. flex: 1;
  297. padding: 0 10px;
  298. color: #666;
  299. font-size: 14px;
  300. overflow: hidden;
  301. text-overflow: ellipsis;
  302. white-space: nowrap;
  303. }
  304. .action-buttons {
  305. display: flex;
  306. gap: 10px;
  307. }
  308. .custom-button {
  309. background-color: #47C3B9 !important;
  310. color: #DCFFFA !important;
  311. border: none;
  312. border-radius: 155px;
  313. padding: 10px 20px;
  314. font-weight: bold;
  315. display: flex;
  316. align-items: center;
  317. }
  318. .upload-icon {
  319. margin-right: 5px;
  320. }
  321. .content-area {
  322. display: flex;
  323. flex-direction: column;
  324. gap: 20px;
  325. }
  326. /* 横向布局容器 */
  327. .horizontal-container {
  328. display: flex;
  329. flex-wrap: wrap;
  330. gap: 20px;
  331. width: 100%;
  332. }
  333. .map-section, .histogram-section {
  334. flex: 1;
  335. min-width: 300px;
  336. background-color: rgba(255, 255, 255, 0.8); /* 调整为半透明白色 */
  337. border-radius: 8px;
  338. padding: 15px;
  339. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
  340. position: relative;
  341. min-height: 400px;
  342. backdrop-filter: blur(5px); /* 添加模糊效果增强半透明感 */
  343. }
  344. .map-image, .histogram-image {
  345. width: 100%;
  346. height: 100%;
  347. max-height: 600px;
  348. object-fit: contain;
  349. border-radius: 4px;
  350. }
  351. .table-area {
  352. width: 100%;
  353. background-color: rgba(255, 255, 255, 0.8); /* 调整为半透明白色 */
  354. border-radius: 8px;
  355. padding: 15px;
  356. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
  357. margin-top: 20px;
  358. backdrop-filter: blur(5px); /* 添加模糊效果增强半透明感 */
  359. }
  360. .loading-container {
  361. display: flex;
  362. flex-direction: column;
  363. align-items: center;
  364. justify-content: center;
  365. height: 300px;
  366. color: #47C3B9;
  367. }
  368. .no-data {
  369. display: flex;
  370. flex-direction: column;
  371. align-items: center;
  372. justify-content: center;
  373. height: 300px;
  374. color: #999;
  375. font-size: 16px;
  376. }
  377. .no-data .el-icon {
  378. font-size: 48px;
  379. margin-bottom: 10px;
  380. }
  381. .loading-icon {
  382. font-size: 36px;
  383. margin-bottom: 10px;
  384. animation: rotate 2s linear infinite;
  385. }
  386. @keyframes rotate {
  387. from {
  388. transform: rotate(0deg);
  389. }
  390. to {
  391. transform: rotate(360deg);
  392. }
  393. }
  394. /* 响应式布局调整 */
  395. @media (max-width: 992px) {
  396. .horizontal-container {
  397. flex-direction: column;
  398. }
  399. .map-section, .histogram-section {
  400. width: 100%;
  401. flex: none;
  402. }
  403. }
  404. </style>