123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698 |
- <template>
- <div class="container">
- <!-- 顶部操作栏 -->
- <div class="toolbar">
- <!-- 文件上传区域 -->
- <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="!statisticsData.length" @click="exportData">
- <el-icon class="upload-icon"><Download /></el-icon>
- 导出数据</el-button>
- </div>
- </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>
- <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>
- <!-- 统计图表区域 -->
- <div class="stats-area">
- <h3>{{countyName}} - 有效Cd预测统计信息</h3>
- <div class="model-info">
- <el-tag type="info">{{currentStats?.['模型类型'] || '有效Cd模型'}}</el-tag>
- <span class="update-time">
- 最后更新: {{currentStats?.['数据更新时间'] ? new Date(currentStats['数据更新时间']).toLocaleString() : '未知'}}
- </span>
- </div>
-
- <div v-if="loadingStats" class="loading-container">
- <el-icon class="loading-icon"><Loading /></el-icon>
- <span>统计数据加载中...</span>
- </div>
-
- <div v-if="!loadingStats && statisticsData.length" class="stats-container">
- <!-- 统计表格 -->
- <el-table
- :data="statisticsData"
- style="width: 100%; margin-bottom: 20px;"
- border
- stripe
- >
- <el-table-column prop="name" label="统计项" min-width="180" />
- <el-table-column prop="value" label="值" min-width="150" />
- <el-table-column prop="unit" label="单位" min-width="100" />
- <el-table-column prop="description" label="描述" min-width="200" />
- </el-table>
-
- <!-- 统计图表 -->
- <div class="charts-container">
- <div class="chart-item">
- <div ref="distributionChart" style="width: 100%; height: 400px;"></div>
- </div>
- <div class="chart-item">
- <div ref="exceedanceChart" style="width: 100%; height: 400px;"></div>
- </div>
- </div>
- </div>
-
- <div v-if="!loadingStats && !statisticsData.length" class="no-data">
- <el-icon><DataAnalysis /></el-icon>
- <p>暂无统计数据</p>
- </div>
- </div>
- </div>
- </div>
- </template>
- <script>
- import * as XLSX from 'xlsx';
- import { saveAs } from 'file-saver';
- import axios from 'axios';
- import * as echarts from 'echarts';
- import {
- Loading, Upload, Picture, Histogram, Download, Document, DataAnalysis
- } from '@element-plus/icons-vue';
- export default {
- name: 'CropCadmiumPrediction',
- components: {
- Loading, Upload, Picture, Histogram, Download, Document, DataAnalysis
- },
- data() {
- return {
- isCalculating: false,
- loadingMap: false,
- loadingHistogram: false,
- loadingStats: false,
- statisticsData: [],
- mapImageUrl: null,
- histogramImageUrl: null,
- mapBlob: null,
- histogramBlob: null,
- selectedFile: null,
- countyName: '乐昌市', // 默认县市名称
- distributionChart: null,
- exceedanceChart: null
- };
- },
- mounted() {
- // 组件挂载时获取最新数据
- this.fetchLatestResults();
- this.fetchStatistics();
- },
- beforeDestroy() {
- if (this.mapImageUrl) URL.revokeObjectURL(this.mapImageUrl);
- if (this.histogramImageUrl) URL.revokeObjectURL(this.histogramImageUrl);
- if (this.distributionChart) this.distributionChart.dispose();
- if (this.exceedanceChart) this.exceedanceChart.dispose();
- },
- 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(
- `http://localhost: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(
- `http://localhost: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('获取最新直方图失败,请先执行预测');
- }
- },
-
- // 格式化统计数据
- formatStatisticsData(stats) {
- return [
- { name: '数据点总数', value: stats['基础统计']['数据点总数'], unit: '个', description: '总样本数量' },
- { name: '平均值', value: stats['基础统计']['均值'].toFixed(4), unit: '(mg/kg)', description: '所有样本的平均Cd含量' },
- { name: '中位数', value: stats['基础统计']['中位数'].toFixed(4), unit: '(mg/kg)', description: '样本的中位Cd含量' },
- { name: '标准差', value: stats['基础统计']['标准差'].toFixed(4), unit: '(mg/kg)', description: 'Cd含量的标准差' },
- { name: '最小值', value: stats['基础统计']['最小值'].toFixed(4), unit: '(mg/kg)', description: '样本中的最小Cd含量' },
- { name: '最大值', value: stats['基础统计']['最大值'].toFixed(4), unit: '(mg/kg)', description: '样本中的最大Cd含量' },
- { name: '偏度', value: stats['基础统计']['偏度'].toFixed(4), unit: '', description: '数据分布偏斜程度' },
- { name: '峰度', value: stats['基础统计']['峰度'].toFixed(4), unit: '', description: '数据分布峰态' },
- {
- name: '经度范围',
- value: `${stats['空间统计']['经度范围']['最小值'].toFixed(6)} - ${stats['空间统计']['经度范围']['最大值'].toFixed(6)}`,
- unit: '度',
- description: `跨度: ${stats['空间统计']['经度范围']['跨度'].toFixed(6)}度`
- },
- {
- name: '纬度范围',
- value: `${stats['空间统计']['纬度范围']['最小值'].toFixed(6)} - ${stats['空间统计']['纬度范围']['最大值'].toFixed(6)}`,
- unit: '度',
- description: `跨度: ${stats['空间统计']['纬度范围']['跨度'].toFixed(6)}度`
- }
- ];
- },
- // 初始化图表 - 根据实际数据更新
- initCharts() {
- if (!this.statisticsData.length || !this.currentStats) return;
-
- // 销毁旧图表
- if (this.distributionChart) this.distributionChart.dispose();
- if (this.exceedanceChart) this.exceedanceChart.dispose();
-
- const histData = this.currentStats['分布直方图'];
-
- // 1. 分布直方图
- this.distributionChart = echarts.init(this.$refs.distributionChart);
- this.distributionChart.setOption({
- title: {
- text: 'Cd含量分布直方图',
- left: 'center'
- },
- tooltip: {
- trigger: 'item',
- formatter: params => {
- const index = params.dataIndex;
- const lowerBound = histData['区间边界'][index].toFixed(4);
- const upperBound = histData['区间边界'][index + 1].toFixed(4);
- return `区间: ${lowerBound} ~ ${upperBound}<br/>频次: ${params.value}`;
- }
- },
- xAxis: {
- type: 'category',
- data: histData['区间中心'].map(v => v.toFixed(4)),
- name: 'Cd含量',
- axisLabel: {
- rotate: 45
- }
- },
- yAxis: {
- type: 'value',
- name: '频次'
- },
- series: [{
- name: '样本分布',
- type: 'bar',
- data: histData['频次'],
- itemStyle: {
- color: '#47C3B9'
- },
- barWidth: '80%'
- }],
- grid: {
- bottom: '20%'
- }
- });
-
- // 2. 箱线图/统计图表
- this.exceedanceChart = echarts.init(this.$refs.exceedanceChart);
-
- // 准备箱线图数据
- const boxData = [
- [
- this.currentStats['基础统计']['最小值'],
- this.currentStats['基础统计']['25%分位数'],
- this.currentStats['基础统计']['中位数'],
- this.currentStats['基础统计']['75%分位数'],
- this.currentStats['基础统计']['最大值'],
- // 还可以添加离群点数据(如果有)
- ]
- ];
-
- this.exceedanceChart.setOption({
- title: {
- text: 'Cd含量统计指标',
- left: 'center'
- },
- tooltip: {
- trigger: 'item',
- axisPointer: {
- type: 'shadow'
- },
- formatter: params => {
- const data = boxData[0];
- return [
- '最大值: ' + data[4].toFixed(4),
- '75%分位数: ' + data[3].toFixed(4),
- '中位数: ' + data[2].toFixed(4),
- '25%分位数: ' + data[1].toFixed(4),
- '最小值: ' + data[0].toFixed(4)
- ].join('<br/>');
- }
- },
- xAxis: {
- type: 'category',
- data: ['Cd含量统计'],
- axisLabel: {
- rotate: 45
- }
- },
- yAxis: {
- type: 'value',
- name: '(Cd含量)'
- },
- series: [{
- name: '统计值',
- type: 'boxplot',
- data: boxData,
- itemStyle: {
- color: '#47C3B9',
- borderColor: '#2F4554'
- },
- emphasis: {
- itemStyle: {
- color: '#FF6B6B',
- borderColor: '#C23531'
- }
- },
- tooltip: {
- formatter: param => {
- const data = boxData[0];
- return [
- '最大值: ' + data[4].toFixed(4),
- '75%分位数: ' + data[3].toFixed(4),
- '中位数: ' + data[2].toFixed(4),
- '25%分位数: ' + data[1].toFixed(4),
- '最小值: ' + data[0].toFixed(4)
- ].join('<br/>');
- }
- }
- }],
- grid: {
- bottom: '15%'
- }
- });
-
- // 响应式调整
- window.addEventListener('resize', this.handleResize);
- },
- // 修改fetchStatistics方法
- async fetchStatistics() {
- try {
- this.loadingStats = true;
-
- const response = await axios.get(
- `http://localhost:8000/api/cd-prediction/effective-cd/statistics/${this.countyName}`
- );
-
- if (response.data.success && response.data.data) {
- this.currentStats = response.data.data; // 保存原始统计数据
- this.statisticsData = this.formatStatisticsData(response.data.data);
- this.$nextTick(() => {
- this.initCharts();
- });
- }
- } catch (error) {
- console.error('获取统计信息失败:', error);
- this.$message.warning('获取统计信息失败');
- } finally {
- this.loadingStats = false;
- }
- },
-
- // 处理窗口大小变化
- handleResize() {
- if (this.distributionChart) this.distributionChart.resize();
- if (this.exceedanceChart) this.exceedanceChart.resize();
- },
-
- // 上传并计算
- async calculate() {
- if (!this.selectedFile) {
- this.$message.warning('请先选择CSV文件');
- return;
- }
-
- try {
- this.isCalculating = true;
- this.loadingMap = true;
- this.loadingHistogram = true;
- this.loadingStats = true;
-
- // 创建FormData
- const formData = new FormData();
- formData.append('county_name', this.countyName);
- formData.append('data_file', this.selectedFile);
-
- // 调用有效Cd地图接口
- const mapResponse = await axios.post(
- 'http://localhost:8000/api/cd-prediction/effective-cd/generate-and-get-map',
- formData,
- {
- headers: {
- 'Content-Type': 'multipart/form-data'
- },
- responseType: 'blob'
- }
- );
-
- // 保存地图数据
- this.mapBlob = mapResponse.data;
- this.mapImageUrl = URL.createObjectURL(this.mapBlob);
-
- // 更新后重新获取直方图和统计数据
- await this.fetchLatestHistogram();
- await this.fetchStatistics();
-
- this.$message.success('计算完成!');
-
- } catch (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;
- this.loadingStats = false;
- }
- },
-
- // 导出地图
- exportMap() {
- if (!this.mapBlob) {
- this.$message.warning('请先计算生成地图');
- return;
- }
-
- const link = document.createElement('a');
- link.href = URL.createObjectURL(this.mapBlob);
- link.download = `${this.countyName}_有效态Cd预测地图.jpg`;
- link.click();
- 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 = `${this.countyName}_有效态Cd预测直方图.jpg`;
- link.click();
- URL.revokeObjectURL(link.href);
- },
-
- // 导出数据 - 修改为获取有效Cd的CSV文件
- async exportData() {
- try {
- this.$message.info('正在获取有效Cd预测数据...');
-
- const response = await axios.get(
- `http://localhost:8000/api/cd-prediction/download-final-effective-cd-csv`,
- { responseType: 'blob' }
- );
-
- const blob = new Blob([response.data], { type: 'text/csv' });
- const link = document.createElement('a');
- link.href = URL.createObjectURL(blob);
- link.download = `${this.countyName}_有效Cd预测数据.csv`;
- link.click();
- URL.revokeObjectURL(link.href);
-
- this.$message.success('数据导出成功');
- } catch (error) {
- console.error('导出数据失败:', error);
- this.$message.error('导出数据失败: ' + (error.response?.data?.detail || '请稍后重试'));
- }
- }
- }
- };
- </script>
- <style scoped>
- .container {
- padding: 20px;
- /* 添加70%透明度的渐变背景 */
- background: linear-gradient(
- 135deg,
- rgba(230, 247, 255, 0.7) 0%,
- rgba(240, 248, 255, 0.7) 100%
- );
- min-height: 100vh;
- box-sizing: border-box;
- }
- .toolbar {
- display: flex;
- flex-direction: column;
- gap: 15px;
- margin-bottom: 20px;
- padding: 15px;
- background-color: rgba(255, 255, 255, 0.8); /* 调整为半透明白色 */
- border-radius: 8px;
- box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
- backdrop-filter: blur(5px); /* 添加模糊效果增强半透明感 */
- }
- .upload-section {
- display: flex;
- align-items: center;
- gap: 15px;
- padding-bottom: 15px;
- border-bottom: 1px solid rgba(0, 0, 0, 0.1); /* 调整边框透明度 */
- }
- .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;
- }
- .custom-button {
- background-color: #47C3B9 !important;
- color: #DCFFFA !important;
- border: none;
- 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;
- }
- /* 横向布局容器 */
- .horizontal-container {
- display: flex;
- flex-wrap: wrap;
- gap: 20px;
- width: 100%;
- }
- .map-section, .histogram-section {
- flex: 1;
- min-width: 300px;
- background-color: rgba(255, 255, 255, 0.8); /* 调整为半透明白色 */
- border-radius: 8px;
- padding: 15px;
- box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
- position: relative;
- min-height: 400px;
- backdrop-filter: blur(5px); /* 添加模糊效果增强半透明感 */
- }
- .map-image, .histogram-image {
- width: 100%;
- height: 100%;
- max-height: 600px;
- object-fit: contain;
- border-radius: 4px;
- }
- .table-area {
- width: 100%;
- background-color: rgba(255, 255, 255, 0.8); /* 调整为半透明白色 */
- border-radius: 8px;
- padding: 15px;
- box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
- margin-top: 20px;
- backdrop-filter: blur(5px); /* 添加模糊效果增强半透明感 */
- }
- .loading-container {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- 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;
- animation: rotate 2s linear infinite;
- }
- @keyframes rotate {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
- }
- /* 响应式布局调整 */
- @media (max-width: 992px) {
- .horizontal-container {
- flex-direction: column;
- }
-
- .map-section, .histogram-section {
- width: 100%;
- flex: none;
- }
- }
- </style>
|