DetectionStatistics.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  1. <template>
  2. <div class="metal-tables-container">
  3. <!-- 灌溉水表格 -->
  4. <div class="table-section">
  5. <el-row :gutter="10" align="middle">
  6. <el-col :span="20">
  7. <div class="title">灌溉水采样点各重金属平均值</div>
  8. </el-col>
  9. <el-col :span="4" style="text-align: right;">
  10. <div class="sample-count" v-if="waterValidSamples > 0">
  11. 有效样本:{{ waterValidSamples }}个
  12. </div>
  13. </el-col>
  14. </el-row>
  15. <el-table
  16. v-if="waterTableData.length && !waterLoading && !waterError"
  17. :data="waterTableData"
  18. border
  19. style="width: 100%"
  20. >
  21. <el-table-column label="指标" prop="indicator" width="80" />
  22. <el-table-column
  23. v-for="(metal, field) in waterMetals"
  24. :key="field"
  25. :label="metal.label"
  26. >
  27. <template #default="scope">
  28. {{ scope.row[field] }}
  29. </template>
  30. </el-table-column>
  31. </el-table>
  32. <div v-else class="empty-state" v-if="!waterLoading && !waterError">
  33. <el-empty description="灌溉水数据为空,请刷新" />
  34. </div>
  35. <el-alert
  36. v-if="waterError"
  37. type="error"
  38. :description="waterError"
  39. show-icon
  40. class="error-alert"
  41. />
  42. </div>
  43. <!-- 断面数据表格 -->
  44. <div class="table-section">
  45. <el-row :gutter="10" align="middle">
  46. <el-col :span="20">
  47. <div class="title">断面采样点镉含量平均值</div>
  48. </el-col>
  49. <el-col :span="4" style="text-align: right;">
  50. <div class="sample-count" v-if="sectionValidSamples > 0">
  51. 有效样本:{{ sectionValidSamples }}个
  52. </div>
  53. </el-col>
  54. </el-row>
  55. <el-table
  56. v-if="sectionTableData.length && !sectionLoading && !sectionError"
  57. :data="sectionTableData"
  58. border
  59. style="width: 100%"
  60. >
  61. <el-table-column label="指标" prop="indicator" width="80" />
  62. <el-table-column
  63. v-for="(metal, field) in sectionMetals"
  64. :key="field"
  65. :label="metal.label"
  66. >
  67. <template #default="scope">
  68. {{ scope.row[field] }}
  69. </template>
  70. </el-table-column>
  71. </el-table>
  72. <div v-else class="empty-state" v-if="!sectionLoading && !sectionError">
  73. <el-empty description="断面数据为空,请刷新" />
  74. </div>
  75. <el-alert
  76. v-if="sectionError"
  77. type="error"
  78. :description="sectionError"
  79. show-icon
  80. class="error-alert"
  81. />
  82. </div>
  83. <!-- 大气企业表格 -->
  84. <div class="table-section">
  85. <el-row :gutter="10" align="middle">
  86. <el-col :span="20">
  87. <div class="title">大气企业颗粒物排放平均值</div>
  88. </el-col>
  89. <el-col :span="4" style="text-align: right;">
  90. <div class="sample-count" v-if="atmoCompanyValidSamples > 0">
  91. 有效样本:{{ atmoCompanyValidSamples }}个
  92. </div>
  93. </el-col>
  94. </el-row>
  95. <el-table
  96. v-if="atmoCompanyTableData.length && !atmoCompanyLoading && !atmoCompanyError"
  97. :data="atmoCompanyTableData"
  98. border
  99. style="width: 100%"
  100. >
  101. <el-table-column label="指标" prop="indicator" width="80" />
  102. <el-table-column
  103. v-for="(metric, field) in atmoCompanyMetrics"
  104. :key="field"
  105. :label="metric.label"
  106. >
  107. <template #default="scope">
  108. {{ scope.row[field]}}
  109. </template>
  110. </el-table-column>
  111. </el-table>
  112. <div v-else class="empty-state" v-if="!atmoCompanyLoading && !atmoCompanyError">
  113. <el-empty description="大气企业数据为空,请刷新" />
  114. </div>
  115. <el-alert
  116. v-if="atmoCompanyError"
  117. type="error"
  118. :description="atmoCompanyError"
  119. show-icon
  120. class="error-alert"
  121. />
  122. </div>
  123. <!-- 大气样本表格 -->
  124. <div class="table-section">
  125. <el-row :gutter="10" align="middle">
  126. <el-col :span="20">
  127. <div class="title">大气样本重金属平均值</div>
  128. </el-col>
  129. <el-col :span="4" style="text-align: right;">
  130. <div class="sample-count" v-if="atmoSampleValidSamples > 0">
  131. 有效样本:{{ atmoSampleValidSamples }}个
  132. </div>
  133. </el-col>
  134. </el-row>
  135. <el-table
  136. v-if="atmoSampleTableData.length && !atmoSampleLoading && !atmoSampleError"
  137. :data="atmoSampleTableData"
  138. border
  139. style="width: 100%"
  140. >
  141. <el-table-column label="指标" prop="indicator" width="80" />
  142. <el-table-column
  143. v-for="(metric, field) in atmoSampleMetrics"
  144. :key="field"
  145. :label="metric.label"
  146. :width="getColWidth(field)"
  147. >
  148. <template #default="scope">
  149. {{ scope.row[field] }}
  150. </template>
  151. </el-table-column>
  152. </el-table>
  153. <div v-else class="empty-state" v-if="!atmoSampleLoading && !atmoSampleError">
  154. <el-empty description="大气样本数据为空,请刷新" />
  155. </div>
  156. <el-alert
  157. v-if="atmoSampleError"
  158. type="error"
  159. :description="atmoSampleError"
  160. show-icon
  161. class="error-alert"
  162. />
  163. </div>
  164. <!-- 统一刷新按钮 -->
  165. <div class="refresh-btn">
  166. <el-button
  167. type="primary"
  168. @click="refreshAll"
  169. :loading="waterLoading || sectionLoading || atmoCompanyLoading || atmoSampleLoading"
  170. icon="el-icon-refresh"
  171. >
  172. 刷新所有数据
  173. </el-button>
  174. </div>
  175. </div>
  176. </template>
  177. <script setup lang="ts">
  178. import { ref, computed, onMounted } from 'vue';
  179. import axios from 'axios';
  180. // ====================== 通用工具类型 ======================
  181. type MetricsMap<T extends string> = { [K in T]: { label: string } };
  182. type TableRow<T extends string> = { indicator: string } & { [K in T]: number };
  183. // ====================== 灌溉水模块(严格类型) ======================
  184. const waterMetals = {
  185. cr_concentration: { label: '铬含量(ug/L)' },
  186. as_concentration: { label: '砷含量(ug/L)' },
  187. cd_concentration: { label: '镉含量(ug/L)' },
  188. hg_concentration: { label: '汞含量(ug/L)' },
  189. pb_concentration: { label: '铅含量(ug/L)' },
  190. };
  191. type WaterMetalKey = keyof typeof waterMetals;
  192. type WaterRow = TableRow<WaterMetalKey>;
  193. const waterLoading = ref(false);
  194. const waterError = ref('');
  195. const waterData = ref<{ [K in WaterMetalKey]?: number | string }[]>([]);
  196. const waterTableData = ref<WaterRow[]>([]);
  197. const waterValidSamples = ref(0); // 有效样本数
  198. const calculateWaterAverage = () => {
  199. const stats: Record<WaterMetalKey, { sum: number; count: number }> = {} as any;
  200. (Object.keys(waterMetals) as WaterMetalKey[]).forEach(key => {
  201. stats[key] = { sum: 0, count: 0 };
  202. });
  203. let validSamples = 0;
  204. waterData.value.forEach(item => {
  205. let hasValidMetric = false;
  206. (Object.keys(waterMetals) as WaterMetalKey[]).forEach(key => {
  207. const value = Number(item[key]);
  208. if (!isNaN(value)) {
  209. stats[key].sum += value;
  210. stats[key].count += 1;
  211. hasValidMetric = true;
  212. }
  213. });
  214. if (hasValidMetric) validSamples++;
  215. });
  216. waterTableData.value = [(Object.keys(waterMetals) as WaterMetalKey[]).reduce((row, key) => {
  217. row.indicator = '平均值';
  218. row[key] = stats[key].count > 0 ? stats[key].sum / stats[key].count : 0;
  219. return row;
  220. }, {} as WaterRow)];
  221. waterValidSamples.value = validSamples;
  222. };
  223. const fetchWaterData = async () => {
  224. try {
  225. waterLoading.value = true;
  226. waterError.value = '';
  227. const res = await axios.get(
  228. 'http://localhost:8000/api/vector/export/all?table_name=water_sampling_data'
  229. );
  230. waterData.value = res.data.features.map((f: { properties: any }) => f.properties);
  231. calculateWaterAverage();
  232. } catch (err: any) {
  233. waterError.value = err.message;
  234. waterTableData.value = [];
  235. } finally {
  236. waterLoading.value = false;
  237. }
  238. };
  239. // ====================== 断面模块(严格类型) ======================
  240. const sectionMetals = { cd_concentration: { label: '镉含量(ug/L)' } };
  241. type SectionMetalKey = keyof typeof sectionMetals;
  242. type SectionRow = TableRow<SectionMetalKey>;
  243. const sectionLoading = ref(false);
  244. const sectionError = ref('');
  245. const sectionData = ref<{ [K in SectionMetalKey]?: number | string }[]>([]);
  246. const sectionTableData = ref<SectionRow[]>([]);
  247. const sectionValidSamples = ref(0); // 有效样本数
  248. const calculateSectionAverage = () => {
  249. const stats: Record<SectionMetalKey, { sum: number; count: number }> = {} as any;
  250. (Object.keys(sectionMetals) as SectionMetalKey[]).forEach(key => {
  251. stats[key] = { sum: 0, count: 0 };
  252. });
  253. let validSamples = 0;
  254. sectionData.value.forEach(item => {
  255. let hasValidMetric = false;
  256. (Object.keys(sectionMetals) as SectionMetalKey[]).forEach(key => {
  257. const value = Number(item[key]);
  258. if (!isNaN(value)) {
  259. stats[key].sum += value;
  260. stats[key].count += 1;
  261. hasValidMetric = true;
  262. }
  263. });
  264. if (hasValidMetric) validSamples++;
  265. });
  266. sectionTableData.value = [(Object.keys(sectionMetals) as SectionMetalKey[]).reduce((row, key) => {
  267. row.indicator = '平均值';
  268. row[key] = stats[key].count > 0 ? stats[key].sum / stats[key].count : 0;
  269. return row;
  270. }, {} as SectionRow)];
  271. sectionValidSamples.value = validSamples;
  272. };
  273. const fetchSectionData = async () => {
  274. try {
  275. sectionLoading.value = true;
  276. sectionError.value = '';
  277. const res = await fetch('http://localhost:8000/api/vector/export/all?table_name=cross_section');
  278. if (!res.ok) throw new Error(`HTTP 错误:${res.status}`);
  279. let rawText = await res.text();
  280. rawText = rawText.replace(/:\s*NaN/g, ': null'); // 修复 NaN
  281. const geoJSON = JSON.parse(rawText);
  282. sectionData.value = geoJSON.features.map((f: { properties: any }) => f.properties);
  283. calculateSectionAverage();
  284. } catch (err: any) {
  285. sectionError.value = err.message;
  286. sectionTableData.value = [];
  287. } finally {
  288. sectionLoading.value = false;
  289. }
  290. };
  291. // ====================== 大气企业模块(严格类型) ======================
  292. const atmoCompanyMetrics = {
  293. particulate_emission: { label: '颗粒物排放量' },
  294. };
  295. type AtmoCompanyKey = keyof typeof atmoCompanyMetrics;
  296. type AtmoCompanyRow = TableRow<AtmoCompanyKey>;
  297. const atmoCompanyLoading = ref(false);
  298. const atmoCompanyError = ref('');
  299. const atmoCompanyData = ref<{ [K in AtmoCompanyKey]?: number | string }[]>([]);
  300. const atmoCompanyTableData = ref<AtmoCompanyRow[]>([]);
  301. const atmoCompanyValidSamples = ref(0); // 有效样本数
  302. const calculateAtmoCompanyAverage = () => {
  303. const stats: Record<AtmoCompanyKey, { sum: number; count: number }> = {} as any;
  304. (Object.keys(atmoCompanyMetrics) as AtmoCompanyKey[]).forEach(key => {
  305. stats[key] = { sum: 0, count: 0 };
  306. });
  307. let validSamples = 0;
  308. atmoCompanyData.value.forEach(item => {
  309. let hasValidMetric = false;
  310. (Object.keys(atmoCompanyMetrics) as AtmoCompanyKey[]).forEach(key => {
  311. const value = Number(item[key]);
  312. if (!isNaN(value)) {
  313. stats[key].sum += value;
  314. stats[key].count += 1;
  315. hasValidMetric = true;
  316. }
  317. });
  318. if (hasValidMetric) validSamples++;
  319. });
  320. atmoCompanyTableData.value = [(Object.keys(atmoCompanyMetrics) as AtmoCompanyKey[]).reduce((row, key) => {
  321. row.indicator = '平均值';
  322. row[key] = stats[key].count > 0 ? stats[key].sum / stats[key].count : 0;
  323. return row;
  324. }, {} as AtmoCompanyRow)];
  325. atmoCompanyValidSamples.value = validSamples;
  326. };
  327. const fetchAtmoCompanyData = async () => {
  328. try {
  329. atmoCompanyLoading.value = true;
  330. atmoCompanyError.value = '';
  331. const res = await fetch('http://localhost:8000/api/vector/export/all?table_name=atmo_company');
  332. if (!res.ok) throw new Error(`HTTP 错误:${res.status}`);
  333. let rawText = await res.text();
  334. rawText = rawText.replace(/:\s*NaN/g, ': null'); // 修复 NaN
  335. const geoJSON = JSON.parse(rawText);
  336. atmoCompanyData.value = geoJSON.features.map((f: { properties: any }) => f.properties);
  337. calculateAtmoCompanyAverage();
  338. } catch (err: any) {
  339. atmoCompanyError.value = err.message;
  340. atmoCompanyTableData.value = [];
  341. } finally {
  342. atmoCompanyLoading.value = false;
  343. }
  344. };
  345. // ====================== 大气样本模块(严格类型) ======================
  346. const weightColumns = [
  347. { key: 'Cr_particulate', label: 'Cr mg/kg' },
  348. { key: 'As_particulate', label: 'As mg/kg' },
  349. { key: 'Cd_particulate', label: 'Cd mg/kg' },
  350. { key: 'Hg_particulate', label: 'Hg mg/kg' },
  351. { key: 'Pb_particulate', label: 'Pb mg/kg' },
  352. { key: 'particle_weight', label: '颗粒物重量 mg' },
  353. ];
  354. const atmoSampleMetrics = weightColumns.reduce((obj, col) => {
  355. obj[col.key] = { label: col.label };
  356. return obj;
  357. }, {} as Record<string, { label: string }>);
  358. type AtmoSampleKey = keyof typeof atmoSampleMetrics;
  359. type AtmoSampleRow = TableRow<AtmoSampleKey>;
  360. const atmoSampleLoading = ref(false);
  361. const atmoSampleError = ref('');
  362. const atmoSampleData = ref<{ [K in AtmoSampleKey]?: number | string }[]>([]);
  363. const atmoSampleTableData = ref<AtmoSampleRow[]>([]);
  364. const atmoSampleValidSamples = ref(0); // 有效样本数
  365. const getColWidth = computed(() => (field: AtmoSampleKey) => {
  366. const col = weightColumns.find(c => c.key === field);
  367. return col ;
  368. });
  369. const calculateAtmoSampleAverage = () => {
  370. const stats: Record<AtmoSampleKey, { sum: number; count: number }> = {} as any;
  371. (Object.keys(atmoSampleMetrics) as AtmoSampleKey[]).forEach(key => {
  372. stats[key] = { sum: 0, count: 0 };
  373. });
  374. let validSamples = 0;
  375. atmoSampleData.value.forEach(item => {
  376. let hasValidMetric = false;
  377. (Object.keys(atmoSampleMetrics) as AtmoSampleKey[]).forEach(key => {
  378. const value = Number(item[key]);
  379. if (!isNaN(value)) {
  380. stats[key].sum += value;
  381. stats[key].count += 1;
  382. hasValidMetric = true;
  383. }
  384. });
  385. if (hasValidMetric) validSamples++;
  386. });
  387. atmoSampleTableData.value = [(Object.keys(atmoSampleMetrics) as AtmoSampleKey[]).reduce((row, key) => {
  388. row.indicator = '平均值';
  389. row[key] = stats[key].count > 0 ? stats[key].sum / stats[key].count : 0;
  390. return row;
  391. }, {} as AtmoSampleRow)];
  392. atmoSampleValidSamples.value = validSamples;
  393. };
  394. const fetchAtmoSampleData = async () => {
  395. try {
  396. atmoSampleLoading.value = true;
  397. atmoSampleError.value = '';
  398. const res = await fetch('http://localhost:8000/api/vector/export/all?table_name=Atmo_sample_data');
  399. if (!res.ok) throw new Error(`HTTP 错误:${res.status}`);
  400. let rawText = await res.text();
  401. rawText = rawText.replace(/:\s*NaN/g, ': null'); // 修复 NaN
  402. const geoJSON = JSON.parse(rawText);
  403. atmoSampleData.value = geoJSON.features.map((f: { properties: any }) => f.properties);
  404. calculateAtmoSampleAverage();
  405. } catch (err: any) {
  406. atmoSampleError.value = err.message;
  407. atmoSampleTableData.value = [];
  408. } finally {
  409. atmoSampleLoading.value = false;
  410. }
  411. };
  412. // ====================== 统一刷新 ======================
  413. const refreshAll = () => {
  414. fetchWaterData();
  415. fetchSectionData();
  416. fetchAtmoCompanyData();
  417. fetchAtmoSampleData();
  418. };
  419. onMounted(() => {
  420. refreshAll();
  421. });
  422. </script>
  423. <style scoped>
  424. .metal-tables-container {
  425. padding: 20px;
  426. background: #fff;
  427. border-radius: 8px;
  428. box-shadow: 0 2px 12px rgba(0,0,0,0.1);
  429. }
  430. .table-section {
  431. margin-bottom: 30px;
  432. padding-bottom: 20px;
  433. border-bottom: 1px solid #eee;
  434. }
  435. .table-section:last-child {
  436. border-bottom: none;
  437. margin-bottom: 0;
  438. padding-bottom: 0;
  439. }
  440. .title {
  441. font-size: 20px;
  442. font-weight: 500;
  443. margin-bottom: 8px;
  444. color: #333;
  445. }
  446. .sample-count {
  447. font-size: 14px;
  448. color: #666;
  449. font-style: italic;
  450. }
  451. .error-alert {
  452. margin: 16px 0;
  453. }
  454. .empty-state {
  455. padding: 40px 0;
  456. text-align: center;
  457. }
  458. .refresh-btn {
  459. text-align: center;
  460. margin-top: 10px;
  461. }
  462. /* 关键样式:自适应列宽 + 内容不换行 */
  463. .el-table {
  464. table-layout: auto; /* 列宽自适应内容 */
  465. min-width: 100%; /* 确保容器宽度不足时触发滚动 */
  466. }
  467. /* 覆盖Element UI的单元格样式(提高优先级) */
  468. .el-table td,
  469. .el-table th {
  470. white-space: nowrap !important; /* 强制不换行 */
  471. overflow: hidden !important; /* 溢出隐藏 */
  472. text-overflow: ellipsis !important; /* 溢出省略号 */
  473. word-break: normal !important; /* 禁止自动断词 */
  474. }
  475. .el-table__cell {
  476. white-space: nowrap !important; /* 强制不换行 */
  477. overflow: hidden; /* 溢出隐藏 */
  478. text-overflow: ellipsis; /* 溢出显示省略号 */
  479. }
  480. .table-section {
  481. overflow-x: auto; /* 横向滚动 */
  482. }
  483. </style>