Ver código fonte

feat(water): add statistics dashboard filters (#7)

Kazerin 4 dias atrás
pai
commit
8f9a740f95

+ 11 - 0
src/utils/waterFacilityStatisticsModel.js

@@ -0,0 +1,11 @@
+export function buildPipeNetworkStatisticsQuery({ facilityTypes = [], status = '' } = {}) {
+  const query = {}
+  const types = Array.isArray(facilityTypes)
+    ? facilityTypes.map(type => String(type).trim()).filter(Boolean)
+    : []
+  const normalizedStatus = String(status ?? '').trim()
+
+  if (types.length) query.facilityTypes = types.join(',')
+  if (normalizedStatus) query.status = normalizedStatus
+  return query
+}

+ 33 - 4
src/views/subSystem/waterSupply/components/WaterFacilityDashboard.vue

@@ -3,10 +3,18 @@
     <div class="page-heading">
       <div class="page-title"><span class="page-title-icon"><el-icon><component :is="pageIcon" /></el-icon></span><div><h2>{{ title }}</h2><span class="muted">{{ view === 'gis' ? '设施分类图层、空间分布与详情联动' : '设施规模、状态与分类统计分析' }}</span></div></div>
       <div class="heading-actions">
+        <template v-if="view === 'stats'">
+          <el-select v-model="statusFilter" clearable placeholder="全部状态" style="width: 130px" @change="loadData">
+            <el-option label="正常" value="0" /><el-option label="异常" value="1" />
+          </el-select>
+          <el-checkbox-group v-model="selectedTypes" @change="loadData">
+            <el-checkbox-button v-for="item in facilityTypes" :key="item.value" :value="item.value">{{ item.label }}</el-checkbox-button>
+          </el-checkbox-group>
+        </template>
         <el-checkbox-group v-if="view === 'gis'" v-model="selectedTypes" @change="loadData">
           <el-checkbox-button v-for="item in facilityTypes" :key="item.value" :value="item.value">{{ item.label }}</el-checkbox-button>
         </el-checkbox-group>
-        <el-button v-if="view === 'stats'" type="success" plain icon="Download" @click="exportData">导出</el-button>
+        <el-button v-if="view === 'stats'" type="success" plain icon="Download" :disabled="!selectedTypes.length" @click="exportData">导出</el-button>
         <el-button :icon="Refresh" @click="loadData">刷新</el-button>
       </div>
     </div>
@@ -37,7 +45,7 @@
         <el-card shadow="never"><div class="card-title">设施数量与状态</div><div ref="barChartRef" class="chart"></div></el-card>
         <el-card shadow="never"><div class="card-title">设施类型占比</div><div ref="pieChartRef" class="chart"></div></el-card>
       </div>
-      <el-card shadow="never"><el-table :data="rows" stripe border v-loading="loading"><el-table-column prop="name" label="统计项" min-width="180"/><el-table-column prop="value" label="数量" width="130"/><el-table-column prop="unit" label="单位" width="100"/><el-table-column prop="remark" label="说明" min-width="240"/></el-table><el-empty v-if="!rows.length" description="暂无统计数据"/></el-card>
+      <el-card shadow="never"><el-table :data="rows" stripe border v-loading="loading"><el-table-column prop="name" label="统计项" min-width="180"/><el-table-column prop="value" label="数量" width="130"/><el-table-column prop="unit" label="单位" width="100"/><el-table-column prop="remark" label="说明" min-width="240"/></el-table><el-empty v-if="!rows.length" :description="selectedTypes.length ? '暂无统计数据' : '请至少选择一类设施'"/></el-card>
     </template>
 
     <el-drawer v-model="detailVisible" class="detail-drawer" title="供水设施详情" direction="rtl" size="560px" append-to-body><el-descriptions :column="1" border><el-descriptions-item v-for="(value, key) in detail" :key="key" :label="key">{{ value ?? '-' }}</el-descriptions-item></el-descriptions></el-drawer>
@@ -51,6 +59,7 @@ import { DataAnalysis, Location, MapLocation, Refresh, Setting } from '@element-
 import { saveAs } from 'file-saver'
 import * as echarts from 'echarts'
 import { exportPipeNetworkStatistics, getPipeNetworkDashboard, getWaterFacilityDetail, getWaterFacilityFeatures, getWaterFacilityOverview } from '@/api/pipeNetwork/waterSupply'
+import { buildPipeNetworkStatisticsQuery } from '@/utils/waterFacilityStatisticsModel'
 import { normalizeMapPoints, unwrapResponse } from '@/utils/waterSupplyModel'
 
 const props = defineProps({ title: { type: String, required: true }, view: { type: String, default: 'stats' } })
@@ -70,6 +79,7 @@ const mapPoints = ref([])
 const mapLines = ref([])
 const routeTypes = String(route.query.type || '').split(',').map(item => item.trim()).filter(Boolean)
 const selectedTypes = ref(routeTypes.length ? facilityTypes.map(item => item.value).filter(item => routeTypes.includes(item)) : facilityTypes.map(item => item.value))
+const statusFilter = ref(String(route.query.status || ''))
 const loading = ref(false)
 const detailVisible = ref(false)
 const detail = ref({})
@@ -89,6 +99,12 @@ const flattenMetrics = (section, value) => {
   })
 }
 
+const clearStatistics = async () => {
+  overview.value = []
+  rows.value = []
+  await renderCharts([])
+}
+
 const renderCharts = async facilityOverview => {
   await nextTick()
   barChart?.dispose(); pieChart?.dispose()
@@ -124,7 +140,15 @@ const loadData = async () => {
       overview.value = summary.filter(item => item.facilityType !== 'total').map(item => ({ label: item.facilityTypeName, value: item.geoCompleteCount ?? 0, unit: '个点位' }))
       buildMap(unwrapResponse(featureResponse)?.features || [])
     } else {
-      const data = unwrapResponse(await getPipeNetworkDashboard()) || {}
+      if (!selectedTypes.value.length) {
+        await clearStatistics()
+        return
+      }
+      const params = buildPipeNetworkStatisticsQuery({
+        facilityTypes: selectedTypes.value,
+        status: statusFilter.value
+      })
+      const data = unwrapResponse(await getPipeNetworkDashboard(params)) || {}
       const facilityOverview = Array.isArray(data.facilityOverview) ? data.facilityOverview : []
       overview.value = facilityOverview.map(item => ({ label: item.facilityTypeName || item.facilityType, value: item.totalCount ?? 0, unit: '项' }))
       rows.value = Object.entries(data).filter(([section]) => section !== 'facilityOverview').flatMap(([section, value]) => flattenMetrics(section, value)).map(item => ({ name: item.name || item.label || item.section || '统计项', value: item.value ?? item.totalCount ?? 0, unit: item.unit || '项', remark: item.remark || item.section || '' }))
@@ -133,8 +157,13 @@ const loadData = async () => {
   } catch (error) { overview.value = []; rows.value = []; mapPoints.value = []; mapLines.value = []; console.warn(`[${props.title}] 数据加载失败`, error) } finally { loading.value = false }
 }
 const exportData = async () => {
+  if (props.view === 'stats' && !selectedTypes.value.length) return
   try {
-    const blob = await exportPipeNetworkStatistics()
+    const params = buildPipeNetworkStatisticsQuery({
+      facilityTypes: selectedTypes.value,
+      status: statusFilter.value
+    })
+    const blob = await exportPipeNetworkStatistics(params)
     saveAs(new Blob([blob]), `管网数据统计分析_${new Date().toISOString().slice(0, 10)}.xlsx`)
   } catch (error) {
     console.warn('管网统计导出失败', error)

+ 42 - 0
tests/waterFacilityStatistics.test.mjs

@@ -0,0 +1,42 @@
+import test from 'node:test'
+import assert from 'node:assert/strict'
+import fs from 'node:fs'
+
+import { buildPipeNetworkStatisticsQuery } from '../src/utils/waterFacilityStatisticsModel.js'
+
+test('statistics query mirrors dashboard and export filters', () => {
+  assert.deepEqual(
+    buildPipeNetworkStatisticsQuery({
+      facilityTypes: ['plant', 'pipe', ''],
+      status: '1'
+    }),
+    { facilityTypes: 'plant,pipe', status: '1' }
+  )
+})
+
+test('statistics query omits empty filters instead of requesting empty datasets', () => {
+  assert.deepEqual(buildPipeNetworkStatisticsQuery(), {})
+  assert.deepEqual(buildPipeNetworkStatisticsQuery({ facilityTypes: ['', ' '], status: '' }), {})
+})
+
+test('statistics page prevents empty facility-type requests and export', () => {
+  const source = fs.readFileSync(
+    'src/views/subSystem/waterSupply/components/WaterFacilityDashboard.vue',
+    'utf8'
+  )
+
+  assert.match(source, /buildPipeNetworkStatisticsQuery/u)
+  assert.match(source, /:disabled="!selectedTypes\.length"/u)
+  assert.match(source, /请至少选择一类设施/u)
+  assert.match(source, /if \(!selectedTypes\.value\.length\)/u)
+})
+
+test('statistics status filter follows the facility status contract', () => {
+  const source = fs.readFileSync(
+    'src/views/subSystem/waterSupply/components/WaterFacilityDashboard.vue',
+    'utf8'
+  )
+
+  assert.match(source, /<el-option label="正常" value="0" \/><el-option label="异常" value="1" \/>/u)
+  assert.doesNotMatch(source, /value="2"/u)
+})