|
|
@@ -19,6 +19,15 @@
|
|
|
<el-icon class="upload-icon"><Document /></el-icon>
|
|
|
上传并计算
|
|
|
</el-button>
|
|
|
+ <!-- 添加从数据库计算按钮 -->
|
|
|
+ <el-button
|
|
|
+ class="custom-button"
|
|
|
+ :loading="isCalculatingFromDB"
|
|
|
+ @click="calculateFromDatabase"
|
|
|
+ >
|
|
|
+ <el-icon class="upload-icon"><Box /></el-icon>
|
|
|
+ 从数据库计算
|
|
|
+ </el-button>
|
|
|
</div>
|
|
|
<!-- 操作按钮 -->
|
|
|
<div class="action-buttons">
|
|
|
@@ -28,9 +37,6 @@
|
|
|
<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>
|
|
|
|
|
|
@@ -50,53 +56,7 @@
|
|
|
</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 class="histogram-section">
|
|
|
<h3>作物态Cd预测直方图</h3>
|
|
|
<div v-if="loadingHistogram" class="loading-container">
|
|
|
@@ -116,47 +76,39 @@
|
|
|
<script>
|
|
|
import * as XLSX from 'xlsx';
|
|
|
import { saveAs } from 'file-saver';
|
|
|
-import { api8000 } from '@/utils/request'; // 导入api8000
|
|
|
-import * as echarts from 'echarts';
|
|
|
+import { api8000 } from '@/utils/request';
|
|
|
import {
|
|
|
- Loading, Upload, Picture, Histogram, Download, Document, DataAnalysis
|
|
|
+ Loading, Upload, Picture, Histogram, Download, Document, Box
|
|
|
} from '@element-plus/icons-vue';
|
|
|
|
|
|
export default {
|
|
|
name: 'CropCadmiumPrediction',
|
|
|
components: {
|
|
|
- Loading, Upload, Picture, Histogram, Download, Document, DataAnalysis
|
|
|
+ Loading, Upload, Picture, Histogram, Download, Document, Box
|
|
|
},
|
|
|
data() {
|
|
|
return {
|
|
|
isCalculating: false,
|
|
|
+ isCalculatingFromDB: false,
|
|
|
loadingMap: false,
|
|
|
loadingHistogram: false,
|
|
|
- loadingStats: false,
|
|
|
- statisticsData: [],
|
|
|
mapImageUrl: null,
|
|
|
histogramImageUrl: null,
|
|
|
mapBlob: null,
|
|
|
histogramBlob: null,
|
|
|
selectedFile: null,
|
|
|
- countyName: '乐昌市', // 默认县市名称
|
|
|
- distributionChart: null,
|
|
|
- exceedanceChart: null,
|
|
|
- currentStats: null
|
|
|
+ countyName: '乐昌市' // 默认县市名称
|
|
|
};
|
|
|
},
|
|
|
|
|
|
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: {
|
|
|
// 触发文件选择
|
|
|
@@ -227,197 +179,6 @@ export default {
|
|
|
}
|
|
|
},
|
|
|
|
|
|
- // 格式化统计数据
|
|
|
- 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 api8000.get(
|
|
|
- `/api/cd-prediction/crop-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) {
|
|
|
@@ -429,12 +190,12 @@ export default {
|
|
|
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('area', this.countyName);
|
|
|
formData.append('data_file', this.selectedFile);
|
|
|
+ formData.append('use_database', 'false'); // 使用上传的文件
|
|
|
|
|
|
// 调用作物Cd地图接口
|
|
|
const mapResponse = await api8000.post(
|
|
|
@@ -452,9 +213,8 @@ export default {
|
|
|
this.mapBlob = mapResponse.data;
|
|
|
this.mapImageUrl = URL.createObjectURL(this.mapBlob);
|
|
|
|
|
|
- // 更新后重新获取直方图和统计数据
|
|
|
+ // 更新后重新获取直方图
|
|
|
await this.fetchLatestHistogram();
|
|
|
- await this.fetchStatistics();
|
|
|
|
|
|
this.$message.success('计算完成!');
|
|
|
|
|
|
@@ -477,7 +237,61 @@ export default {
|
|
|
this.isCalculating = false;
|
|
|
this.loadingMap = false;
|
|
|
this.loadingHistogram = false;
|
|
|
- this.loadingStats = false;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 从数据库计算
|
|
|
+ async calculateFromDatabase() {
|
|
|
+ try {
|
|
|
+ this.isCalculatingFromDB = true;
|
|
|
+ this.loadingMap = true;
|
|
|
+ this.loadingHistogram = true;
|
|
|
+
|
|
|
+ // 创建FormData
|
|
|
+ const formData = new FormData();
|
|
|
+ formData.append('area', this.countyName);
|
|
|
+ formData.append('use_database', 'true'); // 使用数据库数据
|
|
|
+
|
|
|
+ // 调用作物Cd地图接口
|
|
|
+ const mapResponse = await api8000.post(
|
|
|
+ '/api/cd-prediction/crop-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();
|
|
|
+
|
|
|
+ this.$message.success('数据库计算完成!');
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('从数据库计算失败:', error);
|
|
|
+ let errorMessage = '数据库计算失败,请重试';
|
|
|
+
|
|
|
+ if (error.response) {
|
|
|
+ if (error.response.status === 400) {
|
|
|
+ errorMessage = '参数错误:' + (error.response.data.detail || '请检查县市名称');
|
|
|
+ } 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.isCalculatingFromDB = false;
|
|
|
+ this.loadingMap = false;
|
|
|
+ this.loadingHistogram = false;
|
|
|
}
|
|
|
},
|
|
|
|
|
|
@@ -508,30 +322,6 @@ export default {
|
|
|
link.click();
|
|
|
URL.revokeObjectURL(link.href);
|
|
|
},
|
|
|
-
|
|
|
- // 导出数据 - 修改为获取作物Cd的CSV文件
|
|
|
- async exportData() {
|
|
|
- try {
|
|
|
- this.$message.info('正在获取作物Cd预测数据...');
|
|
|
-
|
|
|
- const response = await api8000.get(
|
|
|
- `/api/cd-prediction/download-final-crop-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>
|