EffectiveCadmiumPrediction.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698
  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="!statisticsData.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="stats-area">
  69. <h3>{{countyName}} - 有效Cd预测统计信息</h3>
  70. <div class="model-info">
  71. <el-tag type="info">{{currentStats?.['模型类型'] || '有效Cd模型'}}</el-tag>
  72. <span class="update-time">
  73. 最后更新: {{currentStats?.['数据更新时间'] ? new Date(currentStats['数据更新时间']).toLocaleString() : '未知'}}
  74. </span>
  75. </div>
  76. <div v-if="loadingStats" class="loading-container">
  77. <el-icon class="loading-icon"><Loading /></el-icon>
  78. <span>统计数据加载中...</span>
  79. </div>
  80. <div v-if="!loadingStats && statisticsData.length" class="stats-container">
  81. <!-- 统计表格 -->
  82. <el-table
  83. :data="statisticsData"
  84. style="width: 100%; margin-bottom: 20px;"
  85. border
  86. stripe
  87. >
  88. <el-table-column prop="name" label="统计项" min-width="180" />
  89. <el-table-column prop="value" label="值" min-width="150" />
  90. <el-table-column prop="unit" label="单位" min-width="100" />
  91. <el-table-column prop="description" label="描述" min-width="200" />
  92. </el-table>
  93. <!-- 统计图表 -->
  94. <div class="charts-container">
  95. <div class="chart-item">
  96. <div ref="distributionChart" style="width: 100%; height: 400px;"></div>
  97. </div>
  98. <div class="chart-item">
  99. <div ref="exceedanceChart" style="width: 100%; height: 400px;"></div>
  100. </div>
  101. </div>
  102. </div>
  103. <div v-if="!loadingStats && !statisticsData.length" class="no-data">
  104. <el-icon><DataAnalysis /></el-icon>
  105. <p>暂无统计数据</p>
  106. </div>
  107. </div>
  108. </div>
  109. </div>
  110. </template>
  111. <script>
  112. import * as XLSX from 'xlsx';
  113. import { saveAs } from 'file-saver';
  114. import axios from 'axios';
  115. import * as echarts from 'echarts';
  116. import {
  117. Loading, Upload, Picture, Histogram, Download, Document, DataAnalysis
  118. } from '@element-plus/icons-vue';
  119. export default {
  120. name: 'CropCadmiumPrediction',
  121. components: {
  122. Loading, Upload, Picture, Histogram, Download, Document, DataAnalysis
  123. },
  124. data() {
  125. return {
  126. isCalculating: false,
  127. loadingMap: false,
  128. loadingHistogram: false,
  129. loadingStats: false,
  130. statisticsData: [],
  131. mapImageUrl: null,
  132. histogramImageUrl: null,
  133. mapBlob: null,
  134. histogramBlob: null,
  135. selectedFile: null,
  136. countyName: '乐昌市', // 默认县市名称
  137. distributionChart: null,
  138. exceedanceChart: null
  139. };
  140. },
  141. mounted() {
  142. // 组件挂载时获取最新数据
  143. this.fetchLatestResults();
  144. this.fetchStatistics();
  145. },
  146. beforeDestroy() {
  147. if (this.mapImageUrl) URL.revokeObjectURL(this.mapImageUrl);
  148. if (this.histogramImageUrl) URL.revokeObjectURL(this.histogramImageUrl);
  149. if (this.distributionChart) this.distributionChart.dispose();
  150. if (this.exceedanceChart) this.exceedanceChart.dispose();
  151. },
  152. methods: {
  153. // 触发文件选择
  154. triggerFileUpload() {
  155. this.$refs.fileInput.click();
  156. },
  157. // 处理文件上传
  158. handleFileUpload(event) {
  159. const files = event.target.files;
  160. if (files && files.length > 0) {
  161. this.selectedFile = files[0];
  162. } else {
  163. this.selectedFile = null;
  164. }
  165. },
  166. // 获取最新结果
  167. async fetchLatestResults() {
  168. try {
  169. this.loadingMap = true;
  170. this.loadingHistogram = true;
  171. // 获取最新地图
  172. await this.fetchLatestMap();
  173. // 获取最新直方图
  174. await this.fetchLatestHistogram();
  175. } catch (error) {
  176. console.error('获取最新结果失败:', error);
  177. this.$message.error('获取最新结果失败');
  178. } finally {
  179. this.loadingMap = false;
  180. this.loadingHistogram = false;
  181. }
  182. },
  183. // 获取最新地图
  184. async fetchLatestMap() {
  185. try {
  186. const response = await axios.get(
  187. `http://localhost:8000/api/cd-prediction/effective-cd/latest-map/${this.countyName}`,
  188. { responseType: 'blob' }
  189. );
  190. this.mapBlob = response.data;
  191. this.mapImageUrl = URL.createObjectURL(this.mapBlob);
  192. } catch (error) {
  193. console.error('获取最新地图失败:', error);
  194. this.$message.warning('获取最新地图失败,请先执行预测');
  195. }
  196. },
  197. // 获取最新直方图
  198. async fetchLatestHistogram() {
  199. try {
  200. const response = await axios.get(
  201. `http://localhost:8000/api/cd-prediction/effective-cd/latest-histogram/${this.countyName}`,
  202. { responseType: 'blob' }
  203. );
  204. this.histogramBlob = response.data;
  205. this.histogramImageUrl = URL.createObjectURL(this.histogramBlob);
  206. } catch (error) {
  207. console.error('获取最新直方图失败:', error);
  208. this.$message.warning('获取最新直方图失败,请先执行预测');
  209. }
  210. },
  211. // 格式化统计数据
  212. formatStatisticsData(stats) {
  213. return [
  214. { name: '数据点总数', value: stats['基础统计']['数据点总数'], unit: '个', description: '总样本数量' },
  215. { name: '平均值', value: stats['基础统计']['均值'].toFixed(4), unit: '(mg/kg)', description: '所有样本的平均Cd含量' },
  216. { name: '中位数', value: stats['基础统计']['中位数'].toFixed(4), unit: '(mg/kg)', description: '样本的中位Cd含量' },
  217. { name: '标准差', value: stats['基础统计']['标准差'].toFixed(4), unit: '(mg/kg)', description: 'Cd含量的标准差' },
  218. { name: '最小值', value: stats['基础统计']['最小值'].toFixed(4), unit: '(mg/kg)', description: '样本中的最小Cd含量' },
  219. { name: '最大值', value: stats['基础统计']['最大值'].toFixed(4), unit: '(mg/kg)', description: '样本中的最大Cd含量' },
  220. { name: '偏度', value: stats['基础统计']['偏度'].toFixed(4), unit: '', description: '数据分布偏斜程度' },
  221. { name: '峰度', value: stats['基础统计']['峰度'].toFixed(4), unit: '', description: '数据分布峰态' },
  222. {
  223. name: '经度范围',
  224. value: `${stats['空间统计']['经度范围']['最小值'].toFixed(6)} - ${stats['空间统计']['经度范围']['最大值'].toFixed(6)}`,
  225. unit: '度',
  226. description: `跨度: ${stats['空间统计']['经度范围']['跨度'].toFixed(6)}度`
  227. },
  228. {
  229. name: '纬度范围',
  230. value: `${stats['空间统计']['纬度范围']['最小值'].toFixed(6)} - ${stats['空间统计']['纬度范围']['最大值'].toFixed(6)}`,
  231. unit: '度',
  232. description: `跨度: ${stats['空间统计']['纬度范围']['跨度'].toFixed(6)}度`
  233. }
  234. ];
  235. },
  236. // 初始化图表 - 根据实际数据更新
  237. initCharts() {
  238. if (!this.statisticsData.length || !this.currentStats) return;
  239. // 销毁旧图表
  240. if (this.distributionChart) this.distributionChart.dispose();
  241. if (this.exceedanceChart) this.exceedanceChart.dispose();
  242. const histData = this.currentStats['分布直方图'];
  243. // 1. 分布直方图
  244. this.distributionChart = echarts.init(this.$refs.distributionChart);
  245. this.distributionChart.setOption({
  246. title: {
  247. text: 'Cd含量分布直方图',
  248. left: 'center'
  249. },
  250. tooltip: {
  251. trigger: 'item',
  252. formatter: params => {
  253. const index = params.dataIndex;
  254. const lowerBound = histData['区间边界'][index].toFixed(4);
  255. const upperBound = histData['区间边界'][index + 1].toFixed(4);
  256. return `区间: ${lowerBound} ~ ${upperBound}<br/>频次: ${params.value}`;
  257. }
  258. },
  259. xAxis: {
  260. type: 'category',
  261. data: histData['区间中心'].map(v => v.toFixed(4)),
  262. name: 'Cd含量',
  263. axisLabel: {
  264. rotate: 45
  265. }
  266. },
  267. yAxis: {
  268. type: 'value',
  269. name: '频次'
  270. },
  271. series: [{
  272. name: '样本分布',
  273. type: 'bar',
  274. data: histData['频次'],
  275. itemStyle: {
  276. color: '#47C3B9'
  277. },
  278. barWidth: '80%'
  279. }],
  280. grid: {
  281. bottom: '20%'
  282. }
  283. });
  284. // 2. 箱线图/统计图表
  285. this.exceedanceChart = echarts.init(this.$refs.exceedanceChart);
  286. // 准备箱线图数据
  287. const boxData = [
  288. [
  289. this.currentStats['基础统计']['最小值'],
  290. this.currentStats['基础统计']['25%分位数'],
  291. this.currentStats['基础统计']['中位数'],
  292. this.currentStats['基础统计']['75%分位数'],
  293. this.currentStats['基础统计']['最大值'],
  294. // 还可以添加离群点数据(如果有)
  295. ]
  296. ];
  297. this.exceedanceChart.setOption({
  298. title: {
  299. text: 'Cd含量统计指标',
  300. left: 'center'
  301. },
  302. tooltip: {
  303. trigger: 'item',
  304. axisPointer: {
  305. type: 'shadow'
  306. },
  307. formatter: params => {
  308. const data = boxData[0];
  309. return [
  310. '最大值: ' + data[4].toFixed(4),
  311. '75%分位数: ' + data[3].toFixed(4),
  312. '中位数: ' + data[2].toFixed(4),
  313. '25%分位数: ' + data[1].toFixed(4),
  314. '最小值: ' + data[0].toFixed(4)
  315. ].join('<br/>');
  316. }
  317. },
  318. xAxis: {
  319. type: 'category',
  320. data: ['Cd含量统计'],
  321. axisLabel: {
  322. rotate: 45
  323. }
  324. },
  325. yAxis: {
  326. type: 'value',
  327. name: '(Cd含量)'
  328. },
  329. series: [{
  330. name: '统计值',
  331. type: 'boxplot',
  332. data: boxData,
  333. itemStyle: {
  334. color: '#47C3B9',
  335. borderColor: '#2F4554'
  336. },
  337. emphasis: {
  338. itemStyle: {
  339. color: '#FF6B6B',
  340. borderColor: '#C23531'
  341. }
  342. },
  343. tooltip: {
  344. formatter: param => {
  345. const data = boxData[0];
  346. return [
  347. '最大值: ' + data[4].toFixed(4),
  348. '75%分位数: ' + data[3].toFixed(4),
  349. '中位数: ' + data[2].toFixed(4),
  350. '25%分位数: ' + data[1].toFixed(4),
  351. '最小值: ' + data[0].toFixed(4)
  352. ].join('<br/>');
  353. }
  354. }
  355. }],
  356. grid: {
  357. bottom: '15%'
  358. }
  359. });
  360. // 响应式调整
  361. window.addEventListener('resize', this.handleResize);
  362. },
  363. // 修改fetchStatistics方法
  364. async fetchStatistics() {
  365. try {
  366. this.loadingStats = true;
  367. const response = await axios.get(
  368. `http://localhost:8000/api/cd-prediction/effective-cd/statistics/${this.countyName}`
  369. );
  370. if (response.data.success && response.data.data) {
  371. this.currentStats = response.data.data; // 保存原始统计数据
  372. this.statisticsData = this.formatStatisticsData(response.data.data);
  373. this.$nextTick(() => {
  374. this.initCharts();
  375. });
  376. }
  377. } catch (error) {
  378. console.error('获取统计信息失败:', error);
  379. this.$message.warning('获取统计信息失败');
  380. } finally {
  381. this.loadingStats = false;
  382. }
  383. },
  384. // 处理窗口大小变化
  385. handleResize() {
  386. if (this.distributionChart) this.distributionChart.resize();
  387. if (this.exceedanceChart) this.exceedanceChart.resize();
  388. },
  389. // 上传并计算
  390. async calculate() {
  391. if (!this.selectedFile) {
  392. this.$message.warning('请先选择CSV文件');
  393. return;
  394. }
  395. try {
  396. this.isCalculating = true;
  397. this.loadingMap = true;
  398. this.loadingHistogram = true;
  399. this.loadingStats = true;
  400. // 创建FormData
  401. const formData = new FormData();
  402. formData.append('county_name', this.countyName);
  403. formData.append('data_file', this.selectedFile);
  404. // 调用有效Cd地图接口
  405. const mapResponse = await axios.post(
  406. 'http://localhost:8000/api/cd-prediction/effective-cd/generate-and-get-map',
  407. formData,
  408. {
  409. headers: {
  410. 'Content-Type': 'multipart/form-data'
  411. },
  412. responseType: 'blob'
  413. }
  414. );
  415. // 保存地图数据
  416. this.mapBlob = mapResponse.data;
  417. this.mapImageUrl = URL.createObjectURL(this.mapBlob);
  418. // 更新后重新获取直方图和统计数据
  419. await this.fetchLatestHistogram();
  420. await this.fetchStatistics();
  421. this.$message.success('计算完成!');
  422. } catch (error) {
  423. console.error('计算失败:', error);
  424. let errorMessage = '计算失败,请重试';
  425. if (error.response) {
  426. if (error.response.status === 400) {
  427. errorMessage = '文件格式错误:' + (error.response.data.detail || '请上传正确的CSV文件');
  428. } else if (error.response.status === 404) {
  429. errorMessage = '不支持的县市:' + this.countyName;
  430. } else if (error.response.status === 500) {
  431. errorMessage = '服务器错误:' + (error.response.data.detail || '请稍后重试');
  432. }
  433. }
  434. this.$message.error(errorMessage);
  435. } finally {
  436. this.isCalculating = false;
  437. this.loadingMap = false;
  438. this.loadingHistogram = false;
  439. this.loadingStats = false;
  440. }
  441. },
  442. // 导出地图
  443. exportMap() {
  444. if (!this.mapBlob) {
  445. this.$message.warning('请先计算生成地图');
  446. return;
  447. }
  448. const link = document.createElement('a');
  449. link.href = URL.createObjectURL(this.mapBlob);
  450. link.download = `${this.countyName}_有效态Cd预测地图.jpg`;
  451. link.click();
  452. URL.revokeObjectURL(link.href);
  453. },
  454. // 导出直方图
  455. exportHistogram() {
  456. if (!this.histogramBlob) {
  457. this.$message.warning('请先计算生成直方图');
  458. return;
  459. }
  460. const link = document.createElement('a');
  461. link.href = URL.createObjectURL(this.histogramBlob);
  462. link.download = `${this.countyName}_有效态Cd预测直方图.jpg`;
  463. link.click();
  464. URL.revokeObjectURL(link.href);
  465. },
  466. // 导出数据 - 修改为获取有效Cd的CSV文件
  467. async exportData() {
  468. try {
  469. this.$message.info('正在获取有效Cd预测数据...');
  470. const response = await axios.get(
  471. `http://localhost:8000/api/cd-prediction/download-final-effective-cd-csv`,
  472. { responseType: 'blob' }
  473. );
  474. const blob = new Blob([response.data], { type: 'text/csv' });
  475. const link = document.createElement('a');
  476. link.href = URL.createObjectURL(blob);
  477. link.download = `${this.countyName}_有效Cd预测数据.csv`;
  478. link.click();
  479. URL.revokeObjectURL(link.href);
  480. this.$message.success('数据导出成功');
  481. } catch (error) {
  482. console.error('导出数据失败:', error);
  483. this.$message.error('导出数据失败: ' + (error.response?.data?.detail || '请稍后重试'));
  484. }
  485. }
  486. }
  487. };
  488. </script>
  489. <style scoped>
  490. .container {
  491. padding: 20px;
  492. /* 添加70%透明度的渐变背景 */
  493. background: linear-gradient(
  494. 135deg,
  495. rgba(230, 247, 255, 0.7) 0%,
  496. rgba(240, 248, 255, 0.7) 100%
  497. );
  498. min-height: 100vh;
  499. box-sizing: border-box;
  500. }
  501. .toolbar {
  502. display: flex;
  503. flex-direction: column;
  504. gap: 15px;
  505. margin-bottom: 20px;
  506. padding: 15px;
  507. background-color: rgba(255, 255, 255, 0.8); /* 调整为半透明白色 */
  508. border-radius: 8px;
  509. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
  510. backdrop-filter: blur(5px); /* 添加模糊效果增强半透明感 */
  511. }
  512. .upload-section {
  513. display: flex;
  514. align-items: center;
  515. gap: 15px;
  516. padding-bottom: 15px;
  517. border-bottom: 1px solid rgba(0, 0, 0, 0.1); /* 调整边框透明度 */
  518. }
  519. .file-name {
  520. flex: 1;
  521. padding: 0 10px;
  522. color: #666;
  523. font-size: 14px;
  524. overflow: hidden;
  525. text-overflow: ellipsis;
  526. white-space: nowrap;
  527. }
  528. .action-buttons {
  529. display: flex;
  530. gap: 10px;
  531. }
  532. .custom-button {
  533. background-color: #47C3B9 !important;
  534. color: #DCFFFA !important;
  535. border: none;
  536. border-radius: 155px;
  537. padding: 10px 20px;
  538. font-weight: bold;
  539. display: flex;
  540. align-items: center;
  541. }
  542. .upload-icon {
  543. margin-right: 5px;
  544. }
  545. .content-area {
  546. display: flex;
  547. flex-direction: column;
  548. gap: 20px;
  549. }
  550. /* 横向布局容器 */
  551. .horizontal-container {
  552. display: flex;
  553. flex-wrap: wrap;
  554. gap: 20px;
  555. width: 100%;
  556. }
  557. .map-section, .histogram-section {
  558. flex: 1;
  559. min-width: 300px;
  560. background-color: rgba(255, 255, 255, 0.8); /* 调整为半透明白色 */
  561. border-radius: 8px;
  562. padding: 15px;
  563. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
  564. position: relative;
  565. min-height: 400px;
  566. backdrop-filter: blur(5px); /* 添加模糊效果增强半透明感 */
  567. }
  568. .map-image, .histogram-image {
  569. width: 100%;
  570. height: 100%;
  571. max-height: 600px;
  572. object-fit: contain;
  573. border-radius: 4px;
  574. }
  575. .table-area {
  576. width: 100%;
  577. background-color: rgba(255, 255, 255, 0.8); /* 调整为半透明白色 */
  578. border-radius: 8px;
  579. padding: 15px;
  580. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
  581. margin-top: 20px;
  582. backdrop-filter: blur(5px); /* 添加模糊效果增强半透明感 */
  583. }
  584. .loading-container {
  585. display: flex;
  586. flex-direction: column;
  587. align-items: center;
  588. justify-content: center;
  589. height: 300px;
  590. color: #47C3B9;
  591. }
  592. .no-data {
  593. display: flex;
  594. flex-direction: column;
  595. align-items: center;
  596. justify-content: center;
  597. height: 300px;
  598. color: #999;
  599. font-size: 16px;
  600. }
  601. .no-data .el-icon {
  602. font-size: 48px;
  603. margin-bottom: 10px;
  604. }
  605. .loading-icon {
  606. font-size: 36px;
  607. margin-bottom: 10px;
  608. animation: rotate 2s linear infinite;
  609. }
  610. @keyframes rotate {
  611. from {
  612. transform: rotate(0deg);
  613. }
  614. to {
  615. transform: rotate(360deg);
  616. }
  617. }
  618. /* 响应式布局调整 */
  619. @media (max-width: 992px) {
  620. .horizontal-container {
  621. flex-direction: column;
  622. }
  623. .map-section, .histogram-section {
  624. width: 100%;
  625. flex: none;
  626. }
  627. }
  628. </style>