| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017 |
- <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>
- <!-- 添加从数据库计算按钮 -->
- <el-button
- class="custom-button"
- :loading="isCalculatingFromDB"
- @click="calculateFromDatabase"
- >
- <el-icon class="upload-icon"><Box /></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="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 v-if="loadingTownshipMap" class="loading-container">
- <el-icon class="loading-icon"><Loading /></el-icon>
- <span>乡镇地图加载中...</span>
- </div>
- <!-- ECharts地图容器 -->
- <div
- v-show="!loadingTownshipMap"
- ref="townshipMapRef"
- class="township-map-container"
- ></div>
- <div v-if="!loadingTownshipMap && !townshipMapInstance" class="no-data">
- <el-icon><Location /></el-icon>
- <p>暂无乡镇边界数据</p>
- </div>
- </div>
- <!-- 统计图表区域 -->
- <div class="stats-area">
- <h3>作物态Cd预测统计信息</h3>
- <div class="model-info">
- <el-tag type="info">{{ modelInfo.modelType || 'Cd预测模型' }}</el-tag>
- <span class="update-time">
- 最后更新: {{ modelInfo.updateTime ? new Date(modelInfo.updateTime).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">
- <!-- 基础统计表格 -->
- <h4>基础统计信息</h4>
- <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>
- <!-- 分布统计表格 -->
- <h4>水稻镉含量分布统计</h4>
- <el-table
- :data="distributionData"
- style="width: 100%; margin-bottom: 20px;"
- border
- stripe
- >
- <el-table-column prop="序号" label="序号" width="80" />
- <el-table-column prop="分布区间" label="分布区间 (mg/kg)" min-width="150" />
- <el-table-column prop="区间说明" label="区间说明" min-width="120" />
- <el-table-column prop="数据点数量" label="数据点数量" min-width="120" />
- <el-table-column prop="占比" label="占比 (%)" min-width="100">
- <template #default="{ row }">
- {{ row.占比 }}%
- </template>
- </el-table-column>
- </el-table>
- </div>
-
- <div v-if="!loadingStats && !statisticsData.length" class="no-data">
- <el-icon><DataAnalysis /></el-icon>
- <p>暂无统计数据</p>
- </div>
- </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>
- </template>
- <script>
- import * as XLSX from 'xlsx';
- import * as echarts from 'echarts';
- import { saveAs } from 'file-saver';
- import { api8000 } from '@/utils/request';
- import {
- Location,Loading, Upload, Picture, Histogram, Download, Document, Box, DataAnalysis
- } from '@element-plus/icons-vue';
- export default {
- name: 'CropCadmiumPrediction',
- components: {
- Loading, Upload, Picture, Histogram, Download, Document, Box, DataAnalysis
- },
- data() {
- return {
- isCalculating: false,
- isCalculatingFromDB: false,
- loadingMap: false,
- loadingHistogram: false,
- loadingStats: false,
- statisticsData: [],
- distributionData: [],
- distributionSummary: null,
- distributionTotal: 0,
- modelInfo: {
- modelType: '',
- unit: '',
- updateTime: null,
- dataSource: ''
- },
- mapImageUrl: null,
- histogramImageUrl: null,
- mapBlob: null,
- histogramBlob: null,
- selectedFile: null,
- // 新增:乡镇地图相关数据
- loadingTownshipMap: false, // 乡镇地图加载状态
- townshipMapInstance: null, // ECharts实例
- townshipGeoJson: null, // 本地边界数据(TopoJSON/GeoJSON)
- townshipData: [], // 接口获取的乡镇Cd数据
- countyName: '乐昌市', // 默认县市名称
- townshipMapRef :null,
- currentTooltipTown: '', // 当前悬浮的乡镇
- currentTooltipData: null, // 当前乡镇的详情数据
- isTooltipLoading: false, // tooltip 是否在加载中
- };
- },
- mounted() {
- // 组件挂载时获取最新数据
- this.fetchLatestResults();
- this.fetchStatistics();
- this.initTownshipMap();
- },
- beforeDestroy() {
- if (this.mapImageUrl) URL.revokeObjectURL(this.mapImageUrl);
- if (this.histogramImageUrl) URL.revokeObjectURL(this.histogramImageUrl);
- // 新增:销毁ECharts实例,避免内存泄漏
- if (this.townshipMapInstance) {
- this.townshipMapInstance.dispose();
- this.townshipMapInstance = null;
- }
- },
- 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 api8000.get(
- `/api/cd-prediction/crop-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 api8000.get(
- `/api/cd-prediction/crop-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(statsData) {
- if (!statsData || !statsData.data) return [];
-
- const { 基础统计, 数据单位 } = statsData.data;
-
- return [
- {
- name: '数据点总数',
- value: 基础统计.数据点总数,
- unit: '-',
- description: '样本总数'
- },
- {
- name: '均值',
- value: 基础统计.均值.toFixed(4),
- unit: 数据单位,
- description: '所有样本的平均值'
- },
- {
- name: '中位数',
- value: 基础统计.中位数.toFixed(4),
- unit: 数据单位,
- description: '样本的中位数值'
- },
- {
- name: '标准差',
- value: 基础统计.标准差.toFixed(4),
- unit: 数据单位,
- description: '数据的离散程度'
- },
- {
- name: '最小值',
- value: 基础统计.最小值.toFixed(4),
- unit: 数据单位,
- description: '样本中的最小值'
- },
- {
- name: '最大值',
- value: 基础统计.最大值.toFixed(4),
- unit: 数据单位,
- description: '样本中的最大值'
- },
- {
- name: '25%分位数',
- value: 基础统计['25%分位数'].toFixed(4),
- unit: 数据单位,
- description: '第一四分位数'
- },
- {
- name: '75%分位数',
- value: 基础统计['75%分位数'].toFixed(4),
- unit: 数据单位,
- description: '第三四分位数'
- }
- ];
- },
- // 获取统计信息
- async fetchStatistics() {
- try {
- this.loadingStats = true;
-
- const response = await api8000.get(
- `/api/cd-prediction/crop-cd/statistics`
- );
-
- if (response.data && response.data.success) {
- const statsData = response.data;
- this.statisticsData = this.formatStatisticsData(statsData);
-
- // 设置分布统计表格数据
- if (statsData.data.分布统计表格 && statsData.data.分布统计表格.表格数据) {
- this.distributionData = statsData.data.分布统计表格.表格数据;
- this.distributionSummary = statsData.data.分布统计表格.汇总;
- this.distributionTotal = statsData.data.分布统计表格.总数据点数;
- }
-
- // 设置模型信息
- this.modelInfo = {
- modelType: statsData.data.模型类型,
- unit: statsData.data.数据单位,
- updateTime: statsData.data.数据更新时间,
- dataSource: statsData.data.数据来源
- };
- }
- } catch (error) {
- console.error('获取统计信息失败:', error);
- this.$message.warning('获取统计信息失败');
- } finally {
- this.loadingStats = false;
- }
- },
- // 新增方法:根据乡镇名请求接口
- async fetchTownshipDetailByName(townName) {
- try {
- // 调用对应乡镇的接口,假设接口地址为 /api/township-details/{townName}
- const encodedTownName = encodeURIComponent(townName);
- const response = await api8000.get(`/api/cd-prediction/crop-cd/statistics/town/${encodedTownName}`);
- if (response.data && response.data.success) {
- return response.data.data;
- }
- return null;
- } catch (error) {
- console.error(`获取${townName}详情失败:`, error);
- return null;
- }
- },
- // 新增1:初始化乡镇边界地图(核心逻辑,仅加载边界)
- async initTownshipMap() {
- try {
- this.loadingTownshipMap = true;
- // 步骤1:加载本地 GeoJSON 边界文件
- await this.loadLocalGeoJson();
- // 步骤2:渲染边界地图(不关联接口数据)
- this.renderTownshipMap();
- } catch (error) {
- console.error('乡镇边界加载失败:', error);
- this.townshipMapInstance = null; // 标记加载失败
- } finally {
- this.loadingTownshipMap = false;
- }
- },
- // 新增2:加载本地 GeoJSON 文件(关键:路径必须正确)
- async loadLocalGeoJson() {
- try {
- // 1. 确认文件路径:public/data/韶关市乡镇划分图5.geojson
- const geoJsonPath = '/data/韶关市乡镇划分图5.geojson';
-
- // 2. 发送请求加载 GeoJSON
- const response = await fetch(geoJsonPath);
- // 3. 检查请求是否成功(状态码 200-299 为成功)
- if (!response.ok) {
- throw new Error(`文件加载失败:状态码 ${response.status}(路径:${geoJsonPath})`);
- }
-
- // 4. 解析 GeoJSON 数据
- let originalGeoJson = await response.json();
- // 关键修改:过滤只保留 FXZQMC 为"乐昌市"的特征数据
- this.townshipGeoJson = {
- ...originalGeoJson, // 保留原有属性(如 type、crs 等)
- features: originalGeoJson.features// 过滤出乐昌市的乡镇
- .filter(feature => feature.properties?.FXZQMC === '乐昌市')
- // 为每个乡镇添加name字段,值为TXZQMC(ECharts默认读取name字段)
- .map(feature => ({
- ...feature,
- properties: {
- ...feature.properties,
- name: feature.properties?.TXZQMC || '未知乡镇' // 核心:映射TXZQMC到name
- }
- }))
- };
-
- // 5. 验证 GeoJSON 格式(必须包含 features 数组,否则是无效格式)
- if (!this.townshipGeoJson.features || !Array.isArray(this.townshipGeoJson.features)) {
- throw new Error('GeoJSON 格式错误:缺少 features 数组');
- }
- console.log('GeoJSON 加载成功,包含乡镇数量:', this.townshipGeoJson.features.length);
- } catch (error) {
- console.error('GeoJSON 加载异常:', error);
- throw error; // 抛出错误,让 initTownshipMap 捕获
- }
- },
- // 新增3:渲染乡镇边界(仅显示边界和乡镇名,不关联数据)
- renderTownshipMap() {
- // 1. 获取地图容器 DOM(必须存在)
- const mapContainer = this.$refs.townshipMapRef;
- if (!mapContainer) {
- throw new Error('ECharts 容器不存在:请检查 ref="townshipMapRef" 是否正确');
- }
- // 2. 初始化 ECharts 实例
- this.townshipMapInstance = echarts.init(mapContainer);
- // 3. 注册地图:将 GeoJSON 数据注册到 ECharts(名称用 countyName:乐昌市)
- echarts.registerMap(this.countyName, this.townshipGeoJson);
- // 4. ECharts 配置项(仅显示边界和乡镇名,无数据关联)
- const option = {
- // 标题(可选,显示在地图上方)
- title: {
- text: '乐昌市乡镇边界',
- left: 'center',
- textStyle: { fontSize: 16, fontWeight: 'bold' }
- },
- // 提示框(鼠标悬浮时显示乡镇名)
- tooltip: {
- trigger: 'item', // 按乡镇区域触发
- formatter: () => {
- if (this.isTooltipLoading) {
- return '<div style="padding: 5px;">加载中...</div>';
- } else if (this.currentTooltipData) {
- const detail = this.currentTooltipData;
- let content = `
- <div class="town-tooltip">
- <h3 style="margin: 0 0 5px; color: #0066CC; text-align : center;">${this.currentTooltipTown}</h3>
- <div style="height: 1px; background-color: #0066CC; margin-bottom: 8px;"></div>
- <p>采样点数量: ${detail.基础统计.采样点数量}</p>
- <p>平均值: ${detail.基础统计.平均值.toFixed(4)} mg/kg</p>
- <p>最小值: ${detail.基础统计.最小值.toFixed(4)} mg/kg</p>
- <p>最大值: ${detail.基础统计.最大值.toFixed(4)} mg/kg</p>
- <p>数据更新时间: ${new Date(detail.数据更新时间).toLocaleString()}</p>
- <div style="height: 1px; background-color: #0066CC; margin-bottom: 8px;"></div>
- <p style="color:black; font-size:16px;">分布统计:</p>
- <p>安全区间占比: ${detail.分布统计表格.汇总.安全区间占比}</p>
- <p>预警区间占比: ${detail.分布统计表格.汇总.预警区间占比}</p>
- <p>超标区间占比: ${detail.分布统计表格.汇总.超标区间占比}</p>
- </div>
- `;
- return content;
- }
- },
- },
- // 地图系列(核心:渲染边界)
- series: [
- {
- type: 'map',
- map: this.countyName, // 对应注册的地图名称(乐昌市)
- roam: true, // 允许鼠标缩放、平移地图(方便查看)
- label: {
- show: true, // 显示乡镇名称标签
- fontSize: 10, // 标签字体大小(避免重叠)
- color: '#333' // 标签颜色
- },
- itemStyle: {
- color: 'transparent', // 乡镇区域填充色(透明,仅显示边界)
- borderColor: '#000000', // 边界颜色(青色,醒目)
- borderWidth: 1.5 // 边界宽度
- },
- emphasis: {
- // 鼠标悬浮时的样式(高亮边界和标签)
- label: { color: '#fff', fontSize: 11 },
- itemStyle: { color: 'rgba(71, 195, 185, 0.3)' } // 悬浮区域填充色
- }
- }
- ]
- };
- this.townshipMapInstance.setOption(option);
- // 监听鼠标悬浮事件
- this.townshipMapInstance.on('mouseover', async (params) => {
- if (params.componentType === 'series' && params.seriesType === 'map') {
- const townName = params.name;
- this.currentTooltipTown = townName;
- this.isTooltipLoading = true;
- this.currentTooltipData = null;
- // 手动触发 tooltip 更新
- this.townshipMapInstance.setOption({ tooltip: {} });
- const detail = await this.fetchTownshipDetailByName(townName);
- this.isTooltipLoading = false;
- this.currentTooltipData = detail;
- // 再次手动触发 tooltip 更新,显示获取到的数据
- this.townshipMapInstance.setOption({ tooltip: {} });
- }
- });
- // 监听鼠标移出事件,重置状态
- this.townshipMapInstance.on('mouseout', () => {
- this.currentTooltipTown = '';
- this.currentTooltipData = null;
- this.isTooltipLoading = false;
- });
- // 5. 渲染地图
- this.townshipMapInstance.setOption(option);
- // 6. 监听窗口 resize(地图自适应)
- window.addEventListener('resize', () => {
- this.townshipMapInstance && this.townshipMapInstance.resize();
- });
- },
-
- // 上传并计算
- async calculate() {
- if (!this.selectedFile) {
- this.$message.warning('请先选择CSV文件');
- return;
- }
-
- try {
- this.isCalculating = true;
- this.loadingMap = true;
- this.loadingHistogram = true;
-
- // 创建FormData
- const formData = new FormData();
- formData.append('area', this.countyName);
- formData.append('data_file', this.selectedFile);
- formData.append('use_database', 'false'); // 使用上传的文件
-
- // 调用作物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();
- await this.fetchStatistics();
-
- this.$message.success('计算完成!');
- await this.initTownshipMap();
-
- } 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;
- }
- },
-
- // 从数据库计算
- 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();
- await this.fetchStatistics();
-
- this.$message.success('数据库计算完成!');
- await this.initTownshipMap();
-
- } 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;
- }
- },
-
- // 导出地图
- 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);
- },
-
- // 导出数据
- async exportData() {
- try {
- this.$message.info('正在获取作物态Cd预测数据...');
-
- const response = await api8000.get(
- `/api/cd-prediction/crop-cd/export-csv`,
- { responseType: 'blob' }
- );
-
- const blob = new Blob([response.data], { type: 'text/csv' });
- const link = document.createElement('a');
- link.href = URL.createObjectURL(blob);
- link.download = `作物态Cd预测数据.csv`;
- link.click();
- URL.revokeObjectURL(link.href);
-
- this.$message.success('数据导出成功');
- } catch (error) {
- console.error('导出数据失败:', error);
- this.$message.error('导出数据失败: ' + (error.response?.data?.detail || '请稍后重试'));
- }
- },
-
- // 处理窗口大小变化
- handleResize() {
- if (this.distributionChart) this.distributionChart.resize();
- },
- }
- };
- </script>
- <style scoped>
- .container {
- padding: 20px;
- 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;
- }
- /* 新增:乡镇地图样式 */
- .township-map-section {
- 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: 500px; /* 与原有地图高度一致 */
- backdrop-filter: blur(5px);
- margin-bottom: 20px; /* 与下方地图间距 */
- }
- .township-map-container {
- width: 90%; /* 使用百分比宽度 */
- max-width: 1000px; /* 最大宽度限制 */
- height: 500px;
- border-radius: 4px;
- background-color: #fff;
- margin: 15px auto; /* 上下15px,水平自动居中 */
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
- }
- .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;
- flex-wrap: wrap;
- }
- .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;
- }
- /* 地图区域 */
- .map-section {
- 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: 500px;
- backdrop-filter: blur(5px);
- }
- .map-image {
- width: 100%;
- height: 100%;
- max-height: 500px;
- object-fit: contain;
- border-radius: 4px;
- }
- /* 直方图区域 */
- .histogram-section {
- 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: 500px;
- backdrop-filter: blur(5px);
- }
- .histogram-image {
- width: 100%;
- height: 100%;
- max-height: 600px;
- object-fit: contain;
- border-radius: 4px;
- }
- /* 统计区域 */
- .stats-area {
- 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;
- backdrop-filter: blur(5px);
- }
- .model-info {
- display: flex;
- align-items: center;
- gap: 15px;
- margin-bottom: 15px;
- padding-bottom: 15px;
- border-bottom: 1px solid rgba(0, 0, 0, 0.1);
- }
- .update-time {
- color: #666;
- font-size: 14px;
- }
- .data-source {
- display: flex;
- align-items: center;
- gap: 10px;
- margin-top: 15px;
- padding-top: 15px;
- border-top: 1px solid rgba(0, 0, 0, 0.1);
- color: #666;
- }
- .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;
- }
- /* 新增样式 */
- .summary-info {
- margin-top: 20px;
- }
- .card-header {
- font-weight: bold;
- color: #409EFF;
- }
- .summary-items {
- display: flex;
- flex-direction: column;
- gap: 10px;
- }
- .summary-item {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 8px 0;
- border-bottom: 1px solid #ebeef5;
- }
- .summary-item:last-child {
- border-bottom: none;
- }
- .summary-item .label {
- font-weight: bold;
- color: #606266;
- }
- .summary-item .value {
- font-weight: bold;
- }
- .summary-item .value.safe {
- color: #67C23A;
- }
- .summary-item .value.warning {
- color: #E6A23C;
- }
- .summary-item .value.danger {
- color: #F56C6C;
- }
- @keyframes rotate {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
- }
- /* 响应式布局调整 */
- @media (max-width: 992px) {
- .content-area {
- flex-direction: column;
- }
-
- .action-buttons {
- flex-direction: column;
- align-items: stretch;
- }
-
- .custom-button {
- justify-content: center;
- }
-
- .upload-section {
- flex-direction: column;
- align-items: stretch;
- }
-
- .file-name {
- text-align: center;
- }
- }
- </style>
|