StationYearCompare.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. <!--
  2. StationYearCompare.vue - 单站多年水质对比组件
  3. 功能说明:
  4. 一河三湖专题下的子组件。同一站点不同年份的水质数据对比。
  5. 1. 站点选择 + 多年份选择(2020-2026)
  6. 2. ECharts 折线图:各年份月度水温(wt) 均值对比
  7. 3. 数据表格 + 分页
  8. 4. 数据来源:mock 生成(generateMockData)
  9. -->
  10. <script setup>
  11. import { ref, onMounted, nextTick, watch } from 'vue'
  12. import * as echarts from 'echarts'
  13. import { ElSelect, ElOption, ElButton, ElTable, ElTableColumn, ElPagination } from 'element-plus'
  14. // 站点列表
  15. const stations = ref([
  16. { value: 'TPL001', label: '太浦河水保区' },
  17. { value: 'JSS001', label: '吴江水质站' },
  18. { value: 'QPS001', label: '青浦水质站' },
  19. { value: 'JXS001', label: '嘉善水质站' },
  20. { value: 'SZS001', label: '苏州水质站' }
  21. ])
  22. // 年份列表
  23. const years = ref([
  24. { value: '2020', label: '2020年' },
  25. { value: '2021', label: '2021年' },
  26. { value: '2022', label: '2022年' },
  27. { value: '2023', label: '2023年' },
  28. { value: '2024', label: '2024年' },
  29. { value: '2025', label: '2025年' },
  30. { value: '2026', label: '2026年' }
  31. ])
  32. // 查询条件
  33. const selectedStation = ref('TPL001')
  34. const selectedYears = ref(['2024', '2025', '2026'])
  35. // 图表实例
  36. const chartRef = ref(null)
  37. let chartInstance = null
  38. // 表格数据
  39. const tableData = ref([])
  40. const total = ref(0)
  41. const pageSize = ref(10)
  42. const currentPage = ref(1)
  43. // 模拟数据生成函数
  44. function generateMockData(stationCode, year) {
  45. const baseData = {
  46. 'TPL001': { name: '太浦河水保区', unit: '浙江省生态环境厅', baseTemp: 17, basePh: 8, baseCond: 9.3, baseDo: 7.2 },
  47. 'JSS001': { name: '吴江水质站', unit: '江苏省生态环境厅', baseTemp: 18, basePh: 7.8, baseCond: 8.5, baseDo: 6.8 },
  48. 'QPS001': { name: '青浦水质站', unit: '上海市生态环境局', baseTemp: 16.5, basePh: 8.2, baseCond: 9.0, baseDo: 7.5 },
  49. 'JXS001': { name: '嘉善水质站', unit: '浙江省生态环境厅', baseTemp: 17.5, basePh: 7.9, baseCond: 8.8, baseDo: 7.0 },
  50. 'SZS001': { name: '苏州水质站', unit: '江苏省生态环境厅', baseTemp: 18.2, basePh: 7.7, baseCond: 8.6, baseDo: 6.5 }
  51. }
  52. const stationInfo = baseData[stationCode] || baseData['TPL001']
  53. const data = []
  54. // 每年生成12个月的数据
  55. for (let month = 1; month <= 12; month++) {
  56. const monthStr = `${year}-${String(month).padStart(2, '0')}-15`
  57. // 添加季节性波动
  58. const seasonFactor = Math.sin((month - 3) * Math.PI / 6) * 2
  59. const temp = stationInfo.baseTemp + seasonFactor + (Math.random() - 0.5) * 2
  60. const ph = stationInfo.basePh + (Math.random() - 0.5) * 0.3
  61. const cond = stationInfo.baseCond + seasonFactor * 0.3 + (Math.random() - 0.5) * 1
  62. const doVal = stationInfo.baseDo - seasonFactor * 0.3 + (Math.random() - 0.5) * 0.8
  63. const codmn = 3.5 + Math.random() * 1.5
  64. const mn = 0.05 + Math.random() * 0.2
  65. const tn = 1.2 + Math.random() * 0.8
  66. const tp = 0.03 + Math.random() * 0.08
  67. data.push({
  68. year: year,
  69. month: month,
  70. monthStr: `${year}年${month}月`,
  71. stnm: stationInfo.name,
  72. unit: stationInfo.unit,
  73. tm: monthStr,
  74. wt: temp.toFixed(1),
  75. ph: ph.toFixed(1),
  76. cond: cond.toFixed(1),
  77. dox: doVal.toFixed(1),
  78. codmn: codmn.toFixed(2),
  79. mn: mn.toFixed(2),
  80. tn: tn.toFixed(2),
  81. tp: tp.toFixed(2)
  82. })
  83. }
  84. return {
  85. stationName: stationInfo.name,
  86. unit: stationInfo.unit,
  87. year: year,
  88. rows: data
  89. }
  90. }
  91. // 初始化图表
  92. function initChart() {
  93. if (!chartRef.value) return
  94. chartInstance = echarts.init(chartRef.value)
  95. }
  96. // 更新图表
  97. function updateChart(allYearData) {
  98. if (!chartInstance || !allYearData || allYearData.length === 0) return
  99. const months = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
  100. const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#98D8C8', '#F7DC6F']
  101. const series = []
  102. selectedYears.value.forEach((year, idx) => {
  103. const yearData = allYearData.find(d => d.year === year)
  104. if (yearData && yearData.rows) {
  105. const codmnData = yearData.rows.map(row => parseFloat(row.codmn))
  106. series.push({
  107. name: `${year}年`,
  108. type: 'line',
  109. data: codmnData,
  110. smooth: true,
  111. symbol: 'circle',
  112. symbolSize: 6,
  113. itemStyle: { color: colors[idx % colors.length] },
  114. lineStyle: { width: 2 }
  115. })
  116. }
  117. })
  118. const option = {
  119. tooltip: {
  120. trigger: 'axis',
  121. axisPointer: {
  122. type: 'cross'
  123. }
  124. },
  125. legend: {
  126. data: selectedYears.value.map(y => `${y}年`),
  127. top: 10,
  128. textStyle: { fontSize: 11 },
  129. itemWidth: 15,
  130. itemHeight: 10
  131. },
  132. grid: {
  133. left: '3%',
  134. right: '4%',
  135. bottom: '3%',
  136. top: 60,
  137. containLabel: true
  138. },
  139. xAxis: {
  140. type: 'category',
  141. boundaryGap: false,
  142. data: months,
  143. axisLabel: { fontSize: 10 }
  144. },
  145. yAxis: {
  146. type: 'value',
  147. name: '高锰酸盐指数(mg/L)',
  148. axisLabel: { fontSize: 10 }
  149. },
  150. series: series
  151. }
  152. chartInstance.setOption(option)
  153. }
  154. // 查询数据
  155. function queryData() {
  156. const allYearData = []
  157. let allRows = []
  158. selectedYears.value.forEach(year => {
  159. const yearData = generateMockData(selectedStation.value, year)
  160. allYearData.push(yearData)
  161. allRows = allRows.concat(yearData.rows)
  162. })
  163. total.value = allRows.length
  164. // 分页处理
  165. const start = (currentPage.value - 1) * pageSize.value
  166. const end = start + pageSize.value
  167. tableData.value = allRows.slice(start, end)
  168. // 更新图表
  169. nextTick(() => {
  170. if (!chartInstance) {
  171. initChart()
  172. }
  173. updateChart(allYearData)
  174. })
  175. }
  176. // 分页变化
  177. function handlePageChange(page) {
  178. currentPage.value = page
  179. queryData()
  180. }
  181. // 处理窗口resize
  182. function handleResize() {
  183. chartInstance && chartInstance.resize()
  184. }
  185. watch([selectedStation, selectedYears], () => {
  186. currentPage.value = 1
  187. })
  188. onMounted(() => {
  189. nextTick(() => {
  190. initChart()
  191. queryData()
  192. })
  193. window.addEventListener('resize', handleResize)
  194. })
  195. </script>
  196. <template>
  197. <div class="year-compare-container">
  198. <!-- 查询栏 -->
  199. <div class="query-bar">
  200. <div class="query-item">
  201. <span class="query-label">站点选择:</span>
  202. <ElSelect v-model="selectedStation" class="query-select" style="width: 180px;">
  203. <ElOption v-for="station in stations" :key="station.value" :label="station.label" :value="station.value" />
  204. </ElSelect>
  205. </div>
  206. <div class="query-item">
  207. <span class="query-label">年份选择:</span>
  208. <ElSelect v-model="selectedYears" multiple class="query-select" style="width: 280px;">
  209. <ElOption v-for="year in years" :key="year.value" :label="year.label" :value="year.value" />
  210. </ElSelect>
  211. </div>
  212. <ElButton type="primary" @click="queryData" class="query-btn">查询</ElButton>
  213. </div>
  214. <!-- 图表区域 -->
  215. <div class="chart-section">
  216. <div class="chart-header">
  217. <span class="chart-title">[{{ stations.find(s => s.value === selectedStation)?.label || '' }}] 多年度水质数据对比(高锰酸盐指数)</span>
  218. </div>
  219. <div ref="chartRef" class="chart-container"></div>
  220. </div>
  221. <!-- 表格区域 -->
  222. <div class="table-section">
  223. <ElTable :data="tableData" border class="data-table" :show-header="true">
  224. <ElTableColumn prop="year" label="年份" align="center" />
  225. <ElTableColumn prop="month" label="月份" align="center" />
  226. <ElTableColumn prop="stnm" label="站名" align="center" />
  227. <ElTableColumn prop="unit" label="共享单位" align="center" />
  228. <ElTableColumn prop="wt" label="水温(℃)" align="center" />
  229. <ElTableColumn prop="ph" label="PH" align="center" />
  230. <ElTableColumn prop="cond" label="电导率" align="center" />
  231. <ElTableColumn prop="dox" label="溶解氧(mg/L)" align="center" />
  232. <ElTableColumn prop="codmn" label="高锰酸盐(mg/L)" align="center" />
  233. <ElTableColumn prop="mn" label="氨氮(mg/L)" align="center" />
  234. <ElTableColumn prop="tn" label="总氮(mg/L)" align="center" />
  235. <ElTableColumn prop="tp" label="总磷(mg/L)" align="center" />
  236. </ElTable>
  237. <!-- 分页 -->
  238. <div class="pagination-wrapper">
  239. <span class="pagination-info">共 {{ total }} 条</span>
  240. <ElPagination
  241. :current-page="currentPage"
  242. :page-size="pageSize"
  243. :total="total"
  244. @current-change="handlePageChange"
  245. layout="prev, pager, next, jumper"
  246. />
  247. </div>
  248. </div>
  249. </div>
  250. </template>
  251. <style scoped>
  252. .year-compare-container {
  253. width: 100%;
  254. height: 100%;
  255. display: flex;
  256. flex-direction: column;
  257. padding: 15px;
  258. background: #fff;
  259. overflow: hidden;
  260. }
  261. .query-bar {
  262. display: flex;
  263. align-items: center;
  264. gap: 15px;
  265. padding: 15px 20px;
  266. background: #f8f9fa;
  267. border-radius: 4px;
  268. margin-bottom: 15px;
  269. flex-wrap: wrap;
  270. }
  271. .query-item {
  272. display: flex;
  273. align-items: center;
  274. gap: 8px;
  275. }
  276. .query-label {
  277. font-size: 14px;
  278. color: #333;
  279. }
  280. .query-select {
  281. font-size: 14px;
  282. }
  283. .query-btn {
  284. margin-left: auto;
  285. }
  286. .chart-section {
  287. flex: 1;
  288. min-height: 300px;
  289. margin-bottom: 15px;
  290. display: flex;
  291. flex-direction: column;
  292. }
  293. .chart-header {
  294. padding: 10px 15px;
  295. background: linear-gradient(90deg, #1E9FFF 0%, #4DA3FF 100%);
  296. border-radius: 4px 4px 0 0;
  297. }
  298. .chart-title {
  299. font-size: 14px;
  300. font-weight: bold;
  301. color: #fff;
  302. }
  303. .chart-container {
  304. flex: 1;
  305. border: 1px solid #e8e8e8;
  306. border-top: none;
  307. border-radius: 0 0 4px 4px;
  308. min-height: 280px;
  309. }
  310. .table-section {
  311. flex-shrink: 0;
  312. max-height: 250px;
  313. display: flex;
  314. flex-direction: column;
  315. }
  316. .data-table {
  317. flex: 1;
  318. overflow-y: auto;
  319. }
  320. .data-table th {
  321. background: #f5f5f5;
  322. font-weight: bold;
  323. font-size: 13px;
  324. }
  325. .data-table td {
  326. font-size: 12px;
  327. padding: 8px 5px;
  328. }
  329. .pagination-wrapper {
  330. display: flex;
  331. align-items: center;
  332. justify-content: flex-end;
  333. gap: 15px;
  334. padding: 10px 15px;
  335. background: #f8f9fa;
  336. border-top: 1px solid #e8e8e8;
  337. }
  338. .pagination-info {
  339. font-size: 13px;
  340. color: #666;
  341. }
  342. </style>