|
@@ -2,35 +2,67 @@
|
|
|
<div class="container">
|
|
|
<!-- 顶部操作栏 -->
|
|
|
<div class="toolbar">
|
|
|
- <el-button class="custom-button" :loading="isCalculating" @click="calculate">计算</el-button>
|
|
|
- <el-button class="custom-button" :disabled="isCalculating || !mapBlob" @click="exportMap">导出地图</el-button>
|
|
|
- <el-button class="custom-button" :disabled="isCalculating || !histogramBlob" @click="exportHistogram">导出直方图</el-button>
|
|
|
- <el-button class="custom-button" :disabled="isCalculating || !tableData.length" @click="exportData">导出数据</el-button>
|
|
|
+ <!-- 文件上传区域 -->
|
|
|
+ <div class="upload-section">
|
|
|
+ <input type="file" ref="fileInput" accept=".csv" @change="handleFileUpload" style="display: none">
|
|
|
+ <el-button class="custom-button" @click="triggerFileUpload">
|
|
|
+ <el-icon class="upload-icon"><Upload /></el-icon>
|
|
|
+ 选择CSV文件
|
|
|
+ </el-button>
|
|
|
+ <span v-if="selectedFile" class="file-name">{{ selectedFile.name }}</span>
|
|
|
+ <el-button
|
|
|
+ class="custom-button"
|
|
|
+ :loading="isCalculating"
|
|
|
+ :disabled="!selectedFile"
|
|
|
+ @click="calculate"
|
|
|
+ >
|
|
|
+ <el-icon class="upload-icon"><Document /></el-icon>
|
|
|
+ 上传并计算
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ <!-- 操作按钮 -->
|
|
|
+ <div class="action-buttons">
|
|
|
+ <el-button class="custom-button" :disabled="!mapBlob" @click="exportMap">
|
|
|
+ <el-icon class="upload-icon"><Download /></el-icon>
|
|
|
+ 导出地图</el-button>
|
|
|
+ <el-button class="custom-button" :disabled="!histogramBlob" @click="exportHistogram">
|
|
|
+ <el-icon class="upload-icon"><Download /></el-icon>
|
|
|
+ 导出直方图</el-button>
|
|
|
+ <el-button class="custom-button" :disabled="!tableData.length" @click="exportData">
|
|
|
+ <el-icon class="upload-icon"><Download /></el-icon>
|
|
|
+ 导出数据</el-button>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
|
|
|
- <!-- 主体内容区,计算后显示 -->
|
|
|
- <div v-if="result" class="content-area">
|
|
|
- <!-- 地图区域 - 现在包含两个图片展示区 -->
|
|
|
- <div class="map-area">
|
|
|
- <div class="map-container">
|
|
|
- <!-- 地图展示 -->
|
|
|
- <div class="map-section">
|
|
|
- <h3>有效态Cd预测地图</h3>
|
|
|
- <img v-if="mapImageUrl" :src="mapImageUrl" alt="有效态Cd预测地图" class="map-image">
|
|
|
- <div v-if="loadingMap" class="loading-container">
|
|
|
- <el-icon class="loading-icon"><Loading /></el-icon>
|
|
|
- <span>地图加载中...</span>
|
|
|
- </div>
|
|
|
+ <!-- 主体内容区 -->
|
|
|
+ <div class="content-area">
|
|
|
+ <!-- 地图区域 - 修改为横向布局 -->
|
|
|
+ <div class="horizontal-container">
|
|
|
+ <!-- 地图展示 -->
|
|
|
+ <div class="map-section">
|
|
|
+ <h3>有效态Cd预测地图</h3>
|
|
|
+ <div v-if="loadingMap" class="loading-container">
|
|
|
+ <el-icon class="loading-icon"><Loading /></el-icon>
|
|
|
+ <span>地图加载中...</span>
|
|
|
</div>
|
|
|
-
|
|
|
- <!-- 直方图展示 -->
|
|
|
- <div class="histogram-section">
|
|
|
- <h3>有效态Cd预测直方图</h3>
|
|
|
- <img v-if="histogramImageUrl" :src="histogramImageUrl" alt="有效态Cd预测直方图" class="histogram-image">
|
|
|
- <div v-if="loadingMap" class="loading-container">
|
|
|
- <el-icon class="loading-icon"><Loading /></el-icon>
|
|
|
- <span>直方图加载中...</span>
|
|
|
- </div>
|
|
|
+ <img v-if="mapImageUrl && !loadingMap" :src="mapImageUrl" alt="有效态Cd预测地图" class="map-image">
|
|
|
+ <div v-if="!mapImageUrl && !loadingMap" class="no-data">
|
|
|
+ <el-icon><Picture /></el-icon>
|
|
|
+ <p>暂无地图数据</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 直方图展示 -->
|
|
|
+ <div class="histogram-section">
|
|
|
+ <h3>有效态Cd预测直方图</h3>
|
|
|
+ <div v-if="loadingHistogram" class="loading-container">
|
|
|
+ <el-icon class="loading-icon"><Loading /></el-icon>
|
|
|
+ <span>直方图加载中...</span>
|
|
|
+ </div>
|
|
|
+ <img v-if="histogramImageUrl && !loadingHistogram" :src="histogramImageUrl" alt="有效态Cd预测直方图" class="histogram-image">
|
|
|
+ <div v-if="!histogramImageUrl && !loadingHistogram" class="no-data">
|
|
|
+ <el-icon><Histogram /></el-icon>
|
|
|
+ <p>暂无直方图数据</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
@@ -53,143 +85,209 @@
|
|
|
import * as XLSX from 'xlsx';
|
|
|
import { saveAs } from 'file-saver';
|
|
|
import axios from 'axios';
|
|
|
-import { Loading } from '@element-plus/icons-vue';
|
|
|
+import { Loading, Upload, Picture, Histogram, Download, Document } from '@element-plus/icons-vue';
|
|
|
|
|
|
export default {
|
|
|
name: 'EffectiveCadmiumPrediction',
|
|
|
- components: { Loading },
|
|
|
+ components: { Loading, Upload, Picture, Histogram, Download, Document },
|
|
|
data() {
|
|
|
return {
|
|
|
isCalculating: false,
|
|
|
loadingMap: false,
|
|
|
loadingHistogram: false,
|
|
|
- result: false,
|
|
|
tableData: [],
|
|
|
- mapImageUrl: null, // 存储地图图片 URL
|
|
|
- histogramImageUrl: null, // 存储直方图图片 URL
|
|
|
- mapBlob: null, // 存储地图原始Blob数据
|
|
|
- histogramBlob: null // 存储直方图原始Blob数据
|
|
|
+ mapImageUrl: null,
|
|
|
+ histogramImageUrl: null,
|
|
|
+ mapBlob: null,
|
|
|
+ histogramBlob: null,
|
|
|
+ selectedFile: null,
|
|
|
+ countyName: '乐昌市' // 默认县市名称
|
|
|
};
|
|
|
},
|
|
|
+ mounted() {
|
|
|
+ // 组件挂载时获取最新数据
|
|
|
+ this.fetchLatestResults();
|
|
|
+ },
|
|
|
methods: {
|
|
|
+ // 触发文件选择
|
|
|
+ triggerFileUpload() {
|
|
|
+ this.$refs.fileInput.click();
|
|
|
+ },
|
|
|
+
|
|
|
+ // 处理文件上传
|
|
|
+ handleFileUpload(event) {
|
|
|
+ const files = event.target.files;
|
|
|
+ if (files && files.length > 0) {
|
|
|
+ this.selectedFile = files[0];
|
|
|
+ } else {
|
|
|
+ this.selectedFile = null;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 获取最新结果
|
|
|
+ async fetchLatestResults() {
|
|
|
+ try {
|
|
|
+ this.loadingMap = true;
|
|
|
+ this.loadingHistogram = true;
|
|
|
+
|
|
|
+ // 获取最新地图
|
|
|
+ await this.fetchLatestMap();
|
|
|
+
|
|
|
+ // 获取最新直方图
|
|
|
+ await this.fetchLatestHistogram();
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取最新结果失败:', error);
|
|
|
+ this.$message.error('获取最新结果失败');
|
|
|
+ } finally {
|
|
|
+ this.loadingMap = false;
|
|
|
+ this.loadingHistogram = false;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 获取最新地图
|
|
|
+ async fetchLatestMap() {
|
|
|
+ try {
|
|
|
+ const response = await axios.get(
|
|
|
+ `https://soilgd.com:8000/api/cd-prediction/effective-cd/latest-map/${this.countyName}`,
|
|
|
+ { responseType: 'blob' }
|
|
|
+ );
|
|
|
+
|
|
|
+ this.mapBlob = response.data;
|
|
|
+ this.mapImageUrl = URL.createObjectURL(this.mapBlob);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取最新地图失败:', error);
|
|
|
+ this.$message.warning('获取最新地图失败,请先执行预测');
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 获取最新直方图
|
|
|
+ async fetchLatestHistogram() {
|
|
|
+ try {
|
|
|
+ const response = await axios.get(
|
|
|
+ `https://soilgd.com:8000/api/cd-prediction/effective-cd/latest-histogram/${this.countyName}`,
|
|
|
+ { responseType: 'blob' }
|
|
|
+ );
|
|
|
+
|
|
|
+ this.histogramBlob = response.data;
|
|
|
+ this.histogramImageUrl = URL.createObjectURL(this.histogramBlob);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取最新直方图失败:', error);
|
|
|
+ this.$message.warning('获取最新直方图失败,请先执行预测');
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 上传并计算
|
|
|
async calculate() {
|
|
|
+ if (!this.selectedFile) {
|
|
|
+ this.$message.warning('请先选择CSV文件');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
try {
|
|
|
- // 重置状态
|
|
|
this.isCalculating = true;
|
|
|
this.loadingMap = true;
|
|
|
this.loadingHistogram = true;
|
|
|
- this.result = false;
|
|
|
- this.mapImageUrl = null;
|
|
|
- this.histogramImageUrl = null;
|
|
|
- this.mapBlob = null;
|
|
|
- this.histogramBlob = null;
|
|
|
- this.tableData = [];
|
|
|
|
|
|
- // 调用有效态Cd地图接口
|
|
|
- const mapResponse = await axios.post('https://soilgd.com:8000/api/cd-prediction/effective-cd/generate-and-get-map', {}, {
|
|
|
- responseType: 'blob'
|
|
|
- });
|
|
|
+ // 创建FormData
|
|
|
+ const formData = new FormData();
|
|
|
+ formData.append('county_name', this.countyName);
|
|
|
+ formData.append('data_file', this.selectedFile);
|
|
|
|
|
|
- // 调用有效态Cd直方图接口
|
|
|
- const histogramResponse = await axios.post('https://soilgd.com:8000/api/cd-prediction/effective-cd/generate-and-get-histogram', {}, {
|
|
|
- responseType: 'blob'
|
|
|
- });
|
|
|
+ // 调用有效态Cd地图接口
|
|
|
+ const mapResponse = await axios.post(
|
|
|
+ 'https://soilgd.com:8000/api/cd-prediction/effective-cd/generate-and-get-map',
|
|
|
+ formData,
|
|
|
+ {
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'multipart/form-data'
|
|
|
+ },
|
|
|
+ responseType: 'blob'
|
|
|
+ }
|
|
|
+ );
|
|
|
|
|
|
- // 保存原始Blob数据用于导出
|
|
|
+ // 保存地图数据
|
|
|
this.mapBlob = mapResponse.data;
|
|
|
- this.histogramBlob = histogramResponse.data;
|
|
|
-
|
|
|
- // 为图片数据创建 URL
|
|
|
this.mapImageUrl = URL.createObjectURL(this.mapBlob);
|
|
|
- this.histogramImageUrl = URL.createObjectURL(this.histogramBlob);
|
|
|
|
|
|
- // 更新表格数据
|
|
|
- this.result = true;
|
|
|
+ // 更新后重新获取直方图(因为生成新数据后直方图也会更新)
|
|
|
+ await this.fetchLatestHistogram();
|
|
|
+
|
|
|
+ // 更新表格数据(示例)
|
|
|
this.tableData = [
|
|
|
{ name: '样本1', value: 10, unit: 'mg/L', description: '描述1' },
|
|
|
{ name: '样本2', value: 20, unit: 'mg/L', description: '描述2' }
|
|
|
];
|
|
|
|
|
|
+ this.$message.success('计算完成!');
|
|
|
+
|
|
|
} catch (error) {
|
|
|
- console.error('获取地图或直方图失败:', error);
|
|
|
- this.$message.error('获取预测结果失败,请重试');
|
|
|
+ console.error('计算失败:', error);
|
|
|
+ let errorMessage = '计算失败,请重试';
|
|
|
+
|
|
|
+ if (error.response) {
|
|
|
+ // 处理不同错误状态码
|
|
|
+ if (error.response.status === 400) {
|
|
|
+ errorMessage = '文件格式错误:' + (error.response.data.detail || '请上传正确的CSV文件');
|
|
|
+ } else if (error.response.status === 404) {
|
|
|
+ errorMessage = '不支持的县市:' + this.countyName;
|
|
|
+ } else if (error.response.status === 500) {
|
|
|
+ errorMessage = '服务器错误:' + (error.response.data.detail || '请稍后重试');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ this.$message.error(errorMessage);
|
|
|
} finally {
|
|
|
- // 无论成功失败都重置加载状态
|
|
|
this.isCalculating = false;
|
|
|
this.loadingMap = false;
|
|
|
this.loadingHistogram = false;
|
|
|
}
|
|
|
},
|
|
|
|
|
|
- // 导出地图方法
|
|
|
+ // 导出地图
|
|
|
exportMap() {
|
|
|
if (!this.mapBlob) {
|
|
|
this.$message.warning('请先计算生成地图');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- // 创建下载链接并添加到文档中
|
|
|
const link = document.createElement('a');
|
|
|
link.href = URL.createObjectURL(this.mapBlob);
|
|
|
- link.download = '有效态Cd预测地图.jpg';
|
|
|
- link.style.display = 'none';
|
|
|
- document.body.appendChild(link);
|
|
|
-
|
|
|
- // 触发点击事件
|
|
|
+ link.download = `${this.countyName}_有效态Cd预测地图.jpg`;
|
|
|
link.click();
|
|
|
-
|
|
|
- // 清理临时URL和链接元素
|
|
|
- setTimeout(() => {
|
|
|
- document.body.removeChild(link);
|
|
|
- URL.revokeObjectURL(link.href);
|
|
|
- }, 100);
|
|
|
+ URL.revokeObjectURL(link.href);
|
|
|
},
|
|
|
|
|
|
- // 导出直方图方法 - 修复:确保链接元素被添加到文档中
|
|
|
+ // 导出直方图
|
|
|
exportHistogram() {
|
|
|
if (!this.histogramBlob) {
|
|
|
this.$message.warning('请先计算生成直方图');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- // 创建下载链接并添加到文档中
|
|
|
const link = document.createElement('a');
|
|
|
link.href = URL.createObjectURL(this.histogramBlob);
|
|
|
- link.download = '有效态Cd预测直方图.jpg';
|
|
|
- link.style.display = 'none';
|
|
|
- document.body.appendChild(link);
|
|
|
-
|
|
|
- // 触发点击事件
|
|
|
+ link.download = `${this.countyName}_有效态Cd预测直方图.jpg`;
|
|
|
link.click();
|
|
|
-
|
|
|
- // 清理临时URL和链接元素
|
|
|
- setTimeout(() => {
|
|
|
- document.body.removeChild(link);
|
|
|
- URL.revokeObjectURL(link.href);
|
|
|
- }, 100);
|
|
|
+ URL.revokeObjectURL(link.href);
|
|
|
},
|
|
|
|
|
|
- // 导出数据方法
|
|
|
+ // 导出数据
|
|
|
exportData() {
|
|
|
- // 创建工作簿
|
|
|
- const workbook = XLSX.utils.book_new();
|
|
|
+ if (!this.tableData.length) {
|
|
|
+ this.$message.warning('暂无数据可导出');
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- // 创建数据表
|
|
|
+ const workbook = XLSX.utils.book_new();
|
|
|
const worksheet = XLSX.utils.json_to_sheet(this.tableData);
|
|
|
-
|
|
|
- // 将工作表添加到工作簿
|
|
|
XLSX.utils.book_append_sheet(workbook, worksheet, '有效态Cd数据');
|
|
|
-
|
|
|
- // 生成Excel文件
|
|
|
const excelBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
|
|
|
const excelData = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
|
|
-
|
|
|
- // 保存文件
|
|
|
- saveAs(excelData, 'effective-cd-data.xlsx');
|
|
|
+ saveAs(excelData, `${this.countyName}_有效态Cd数据.xlsx`);
|
|
|
}
|
|
|
},
|
|
|
beforeDestroy() {
|
|
|
- // 组件销毁时释放图片 URL 防止内存泄漏
|
|
|
if (this.mapImageUrl) URL.revokeObjectURL(this.mapImageUrl);
|
|
|
if (this.histogramImageUrl) URL.revokeObjectURL(this.histogramImageUrl);
|
|
|
}
|
|
@@ -197,7 +295,6 @@ export default {
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
-/* 样式保持不变 */
|
|
|
.container {
|
|
|
padding: 20px;
|
|
|
background-color: #f5f7fa;
|
|
@@ -206,10 +303,37 @@ export default {
|
|
|
}
|
|
|
|
|
|
.toolbar {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 15px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ padding: 15px;
|
|
|
+ background-color: white;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.upload-section {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
+ gap: 15px;
|
|
|
+ padding-bottom: 15px;
|
|
|
+ border-bottom: 1px solid #eee;
|
|
|
+}
|
|
|
+
|
|
|
+.file-name {
|
|
|
+ flex: 1;
|
|
|
+ padding: 0 10px;
|
|
|
+ color: #666;
|
|
|
+ font-size: 14px;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
+.action-buttons {
|
|
|
+ display: flex;
|
|
|
gap: 10px;
|
|
|
- margin-bottom: 20px;
|
|
|
}
|
|
|
|
|
|
.custom-button {
|
|
@@ -219,57 +343,54 @@ export default {
|
|
|
border-radius: 155px;
|
|
|
padding: 10px 20px;
|
|
|
font-weight: bold;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.upload-icon {
|
|
|
+ margin-right: 5px;
|
|
|
}
|
|
|
|
|
|
.content-area {
|
|
|
display: flex;
|
|
|
+ flex-direction: column;
|
|
|
gap: 20px;
|
|
|
}
|
|
|
|
|
|
-.map-area {
|
|
|
- flex: 1;
|
|
|
- min-width: 300px;
|
|
|
-}
|
|
|
-
|
|
|
-.map-container {
|
|
|
+/* 横向布局容器 */
|
|
|
+.horizontal-container {
|
|
|
display: flex;
|
|
|
- flex-direction: column;
|
|
|
+ flex-wrap: wrap;
|
|
|
gap: 20px;
|
|
|
+ width: 100%;
|
|
|
}
|
|
|
|
|
|
.map-section, .histogram-section {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 300px; /* 最小宽度,确保在小屏幕上也能正常显示 */
|
|
|
background-color: white;
|
|
|
border-radius: 8px;
|
|
|
padding: 15px;
|
|
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
|
|
position: relative;
|
|
|
+ min-height: 400px;
|
|
|
}
|
|
|
|
|
|
.map-image, .histogram-image {
|
|
|
width: 100%;
|
|
|
+ height: 100%;
|
|
|
max-height: 600px;
|
|
|
object-fit: contain;
|
|
|
border-radius: 4px;
|
|
|
}
|
|
|
|
|
|
-.map-placeholder {
|
|
|
- background-color: #cce5ff;
|
|
|
- height: 200px;
|
|
|
- border-radius: 8px;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- font-weight: bold;
|
|
|
- font-size: 16px;
|
|
|
- color: #003366;
|
|
|
-}
|
|
|
-
|
|
|
.table-area {
|
|
|
- flex: 1;
|
|
|
+ width: 100%;
|
|
|
background-color: white;
|
|
|
border-radius: 8px;
|
|
|
padding: 15px;
|
|
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
|
|
+ margin-top: 20px;
|
|
|
}
|
|
|
|
|
|
.loading-container {
|
|
@@ -277,10 +398,25 @@ export default {
|
|
|
flex-direction: column;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
- height: 200px;
|
|
|
+ height: 300px;
|
|
|
color: #47C3B9;
|
|
|
}
|
|
|
|
|
|
+.no-data {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ height: 300px;
|
|
|
+ color: #999;
|
|
|
+ font-size: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.no-data .el-icon {
|
|
|
+ font-size: 48px;
|
|
|
+ margin-bottom: 10px;
|
|
|
+}
|
|
|
+
|
|
|
.loading-icon {
|
|
|
font-size: 36px;
|
|
|
margin-bottom: 10px;
|
|
@@ -295,4 +431,16 @@ export default {
|
|
|
transform: rotate(360deg);
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+/* 响应式布局调整 */
|
|
|
+@media (max-width: 992px) {
|
|
|
+ .horizontal-container {
|
|
|
+ flex-direction: column;
|
|
|
+ }
|
|
|
+
|
|
|
+ .map-section, .histogram-section {
|
|
|
+ width: 100%;
|
|
|
+ flex: none;
|
|
|
+ }
|
|
|
+}
|
|
|
</style>
|