grainRemovalInputFlux.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. <template>
  2. <div class="agricultural-input-management">
  3. <!-- 直接展示结果页面 -->
  4. <div class="page-container">
  5. <el-card class="results-card">
  6. <div class="results-content">
  7. <!-- 地图区域 -->
  8. <div class="map-section">
  9. <h3>籽粒移除Cd通量分布图</h3>
  10. <div v-if="loading" class="loading-container">
  11. <el-icon class="loading-icon"><Loading /></el-icon>
  12. <span>数据加载中...</span>
  13. </div>
  14. <div v-else-if="error" class="error-container">
  15. <el-icon class="error-icon"><Warning /></el-icon>
  16. <span>{{ error }}</span>
  17. </div>
  18. <div v-else class="image-container">
  19. <img :src="visualizationImage" class="result-image"></img>
  20. </div>
  21. </div>
  22. <!-- 统计图表区域 -->
  23. <div class="stats-area">
  24. <h3>籽粒移除Cd通量统计信息</h3>
  25. <div class="model-info">
  26. <el-tag type="info">籽粒移除Cd通量模型</el-tag>
  27. <span class="update-time">
  28. 最后更新: {{ reportTime }}
  29. </span>
  30. </div>
  31. <div v-if="loading" class="loading-container">
  32. <el-icon class="loading-icon"><Loading /></el-icon>
  33. <span>统计数据加载中...</span>
  34. </div>
  35. <div v-else-if="error" class="error-container">
  36. <el-icon class="error-icon"><Warning /></el-icon>
  37. <span>{{ error }}</span>
  38. </div>
  39. <div v-else class="stats-container">
  40. <div class="stats-grid">
  41. <div class="stat-card">
  42. <div class="stat-value">{{ statistics.total_samples.toLocaleString() }}</div>
  43. <div class="stat-label">总样本数</div>
  44. </div>
  45. <div class="stat-card">
  46. <div class="stat-value">{{ statistics.mean_flux.toFixed(4) }}</div>
  47. <div class="stat-label">平均通量</div>
  48. </div>
  49. <div class="stat-card">
  50. <div class="stat-value">{{ statistics.max_flux.toFixed(4) }}</div>
  51. <div class="stat-label">最大通量</div>
  52. </div>
  53. <div class="stat-card">
  54. <div class="stat-value">{{ statistics.min_flux.toFixed(4) }}</div>
  55. <div class="stat-label">最小通量</div>
  56. </div>
  57. </div>
  58. <div v-if="resultDetails.length > 0" class="details-table">
  59. <el-table
  60. :data="resultDetails"
  61. style="width: 100%; margin-top: 20px;"
  62. border
  63. stripe
  64. >
  65. <el-table-column prop="type" label="区域类型" align="center" min-width="120" />
  66. <el-table-column prop="flux" label="Cd通量(g/ha/a)" align="center" :formatter="formatNumber" min-width="150" />
  67. </el-table>
  68. </div>
  69. </div>
  70. </div>
  71. </div>
  72. </el-card>
  73. </div>
  74. </div>
  75. </template>
  76. <script>
  77. import { ref, onMounted, onBeforeUnmount } from 'vue';
  78. import {
  79. ElButton,
  80. ElCard,
  81. ElTable,
  82. ElTableColumn,
  83. ElTag,
  84. ElIcon
  85. } from 'element-plus';
  86. import {
  87. Loading,
  88. Picture,
  89. Warning
  90. } from '@element-plus/icons-vue';
  91. import { api8000 } from '@/utils/request';
  92. export default {
  93. components: {
  94. ElButton,
  95. ElCard,
  96. ElTable,
  97. ElTableColumn,
  98. ElTag,
  99. ElIcon,
  100. Loading,
  101. Picture,
  102. Warning
  103. },
  104. props: {
  105. area: {
  106. type: String,
  107. default: '乐昌市'
  108. }
  109. },
  110. setup(props) {
  111. // 响应式数据
  112. const results = ref([]);
  113. const statistics = ref({
  114. total_samples: 0,
  115. mean_flux: 0,
  116. max_flux: 0,
  117. min_flux: 0
  118. });
  119. const visualizationImage = ref("");
  120. const reportTime = ref("");
  121. const loading = ref(true);
  122. const error = ref(null);
  123. // 格式化数字显示(保留4位小数)
  124. const formatNumber = (row, column, cellValue) => {
  125. if (typeof cellValue === 'number') {
  126. return cellValue.toFixed(4);
  127. }
  128. return cellValue;
  129. };
  130. // 计算详情数据
  131. const resultDetails = ref([]);
  132. // 从API获取数据
  133. const fetchData = async () => {
  134. try {
  135. loading.value = true;
  136. error.value = null;
  137. // 获取计算结果数据
  138. const dataResponse = await api8000.get(
  139. `/api/cd-flux-removal/grain-removal?area=${encodeURIComponent(props.area)}`
  140. );
  141. if (!dataResponse.data.success) {
  142. throw new Error(`API错误: ${dataResponse.data.message}`);
  143. }
  144. // 设置数据
  145. results.value = dataResponse.data.data.results;
  146. statistics.value = dataResponse.data.data.statistics;
  147. // 更新详情数据
  148. if (dataResponse.data.data.details) {
  149. resultDetails.value = Object.entries(dataResponse.data.data.details).map(([type, flux]) => ({
  150. type: getTypeName(type),
  151. flux: flux
  152. }));
  153. }
  154. // 获取可视化图片
  155. try {
  156. const imageResponse = await api8000.get(
  157. `/api/cd-flux-removal/grain-removal/latest-map/${encodeURIComponent(props.area)}`,
  158. { responseType: 'blob' }
  159. );
  160. // 创建图片Blob URL
  161. const blob = new Blob([imageResponse.data], { type: imageResponse.headers['content-type'] });
  162. visualizationImage.value = URL.createObjectURL(blob);
  163. } catch (imgErr) {
  164. console.error('图片加载错误:', imgErr);
  165. }
  166. // 设置报告时间
  167. reportTime.value = new Date().toLocaleString('zh-CN', {
  168. year: 'numeric',
  169. month: 'long',
  170. day: 'numeric',
  171. hour: '2-digit',
  172. minute: '2-digit'
  173. });
  174. } catch (err) {
  175. console.error('数据获取错误:', err);
  176. error.value = err.message || '获取数据时发生错误';
  177. } finally {
  178. loading.value = false;
  179. }
  180. };
  181. // 组件挂载时获取数据
  182. onMounted(() => {
  183. fetchData();
  184. });
  185. onBeforeUnmount(() => {
  186. if (visualizationImage.value) {
  187. URL.revokeObjectURL(visualizationImage.value);
  188. }
  189. });
  190. return {
  191. results,
  192. statistics,
  193. visualizationImage,
  194. reportTime,
  195. loading,
  196. error,
  197. resultDetails,
  198. formatNumber
  199. };
  200. }
  201. };
  202. </script>
  203. <style scoped>
  204. /* 整体布局优化 - 与农业投入品保持一致 */
  205. .agricultural-input-management {
  206. padding: 20px;
  207. background: linear-gradient(
  208. 135deg,
  209. rgba(230, 247, 255, 0.7) 0%,
  210. rgba(240, 248, 255, 0.7) 100%
  211. );
  212. min-height: 100vh;
  213. box-sizing: border-box;
  214. }
  215. .page-container {
  216. width: 100%;
  217. height: 100%;
  218. }
  219. /* 工具栏样式 */
  220. .toolbar {
  221. display: flex;
  222. justify-content: flex-start;
  223. gap: 15px;
  224. margin-bottom: 20px;
  225. padding: 15px;
  226. background-color: rgba(255, 255, 255, 0.8);
  227. border-radius: 8px;
  228. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
  229. backdrop-filter: blur(5px);
  230. }
  231. .custom-button {
  232. background-color: #47C3B9 !important;
  233. color: #DCFFFA !important;
  234. border: none;
  235. border-radius: 155px;
  236. padding: 10px 20px;
  237. font-weight: bold;
  238. display: flex;
  239. align-items: center;
  240. }
  241. /* 结果卡片样式 */
  242. .results-card {
  243. background-color: rgba(255, 255, 255, 0.8);
  244. border-radius: 8px;
  245. padding: 20px;
  246. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
  247. backdrop-filter: blur(5px);
  248. height: 100%;
  249. box-sizing: border-box;
  250. }
  251. .results-content {
  252. height: 100%;
  253. display: flex;
  254. flex-direction: column;
  255. }
  256. .map-section, .stats-area {
  257. background-color: rgba(255, 255, 255, 0.8);
  258. border-radius: 8px;
  259. padding: 20px;
  260. margin-bottom: 20px;
  261. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
  262. }
  263. h3 {
  264. margin-bottom: 15px;
  265. color: #333;
  266. font-size: 18px;
  267. font-weight: 600;
  268. }
  269. .model-info {
  270. display: flex;
  271. align-items: center;
  272. gap: 15px;
  273. margin-bottom: 15px;
  274. }
  275. .update-time {
  276. color: #666;
  277. font-size: 14px;
  278. }
  279. /* 统计卡片样式 */
  280. .stats-grid {
  281. display: grid;
  282. grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  283. gap: 20px;
  284. margin-bottom: 20px;
  285. }
  286. .stat-card {
  287. background: rgba(178, 235, 242, 0.3);
  288. border-radius: 15px;
  289. padding: 20px;
  290. text-align: center;
  291. box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08);
  292. transition: all 0.3s ease;
  293. }
  294. .stat-card:hover {
  295. transform: translateY(-5px);
  296. background: rgba(178, 235, 242, 0.5);
  297. }
  298. .stat-value {
  299. font-size: 1.8rem;
  300. font-weight: 700;
  301. color: #006064;
  302. margin-bottom: 8px;
  303. }
  304. .stat-label {
  305. font-size: 1.1rem;
  306. color: #00838f;
  307. }
  308. /* 图片容器限定大小 */
  309. .image-container {
  310. width: 100%;
  311. height: 500px;
  312. display: flex;
  313. justify-content: center;
  314. align-items: center;
  315. background-color: #f9f9f9;
  316. border-radius: 8px;
  317. overflow: hidden;
  318. }
  319. .result-image {
  320. max-width: 100%;
  321. max-height: 100%;
  322. object-fit: contain;
  323. }
  324. .loading-container, .error-container {
  325. display: flex;
  326. flex-direction: column;
  327. align-items: center;
  328. justify-content: center;
  329. height: 300px;
  330. gap: 15px;
  331. }
  332. .loading-container {
  333. color: #47C3B9;
  334. }
  335. .error-container {
  336. color: #F56C6C;
  337. }
  338. .loading-icon {
  339. font-size: 36px;
  340. margin-bottom: 10px;
  341. animation: rotate 2s linear infinite;
  342. }
  343. .error-icon {
  344. font-size: 36px;
  345. margin-bottom: 10px;
  346. }
  347. .no-data {
  348. display: flex;
  349. flex-direction: column;
  350. align-items: center;
  351. justify-content: center;
  352. height: 300px;
  353. color: #999;
  354. font-size: 16px;
  355. }
  356. .no-data .el-icon {
  357. font-size: 48px;
  358. margin-bottom: 10px;
  359. }
  360. .details-table {
  361. margin-top: 20px;
  362. }
  363. @keyframes rotate {
  364. from { transform: rotate(0deg); }
  365. to { transform: rotate(360deg); }
  366. }
  367. /* 响应式设计 */
  368. @media (max-width: 768px) {
  369. .toolbar {
  370. flex-direction: column;
  371. align-items: stretch;
  372. }
  373. .stats-grid {
  374. grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  375. }
  376. .image-container {
  377. height: 300px;
  378. }
  379. }
  380. @media (max-width: 480px) {
  381. .agricultural-input-management {
  382. padding: 10px;
  383. }
  384. .results-card {
  385. padding: 15px;
  386. }
  387. .stats-grid {
  388. grid-template-columns: 1fr;
  389. }
  390. .stat-card {
  391. padding: 15px;
  392. }
  393. .image-container {
  394. height: 250px;
  395. }
  396. .stat-value {
  397. font-size: 1.5rem;
  398. }
  399. }
  400. </style>