Просмотр исходного кода

feat(waterSupply): 供水设施GIS一张图统一按设施状态着色并补充状态标识

Kazerin 1 месяц назад
Родитель
Сommit
c0bcedbb3a

+ 33 - 8
src/utils/gisMapColors.js

@@ -16,16 +16,41 @@ export const gisPointStatusIcons = {
   offline: new URL('../assets/gis/T_MarkPoint_Grey.png', import.meta.url).href
 }
 
-export const facilityPointIcons = {
-  source: new URL('../assets/gis/T_MarkPoint_Blue.png', import.meta.url).href,
-  plant: new URL('../assets/gis/T_MarkPoint_Green.png', import.meta.url).href,
-  pumpStation: new URL('../assets/gis/T_MarkPoint_Red.png', import.meta.url).href,
-  pipe: new URL('../assets/gis/T_MarkPoint_Yellow.png', import.meta.url).href,
-  user: new URL('../assets/gis/T_MarkPoint_Black.png', import.meta.url).href
+/**
+ * 设施状态语义:与监测设备共用同一套状态色(绿-正常、红-报警/异常、蓝-工单/检修、黄-预警/欠费)。
+ * 供水设施 GIS 一张图与首页全域态势都从这里取状态,保证设施颜色只有一处定义。
+ * 状态缺失或未维护时按“正常”处理,不臆造异常。
+ */
+const facilityStatusMeta = {
+  source: {
+    0: { key: 'normal', text: '正常' },
+    1: { key: 'alarm', text: '异常' }
+  },
+  plant: {
+    0: { key: 'normal', text: '正常' },
+    1: { key: 'alarm', text: '停产' }
+  },
+  pumpStation: {
+    0: { key: 'normal', text: '正常' },
+    1: { key: 'alarm', text: '故障停运' },
+    2: { key: 'workOrder', text: '检修中' }
+  },
+  // 管网保持现状:只构造管线,不落点位图标,因此不参与状态色映射
+  user: {
+    0: { key: 'normal', text: '正常' },
+    1: { key: 'warning', text: '欠费' },
+    2: { key: 'alarm', text: '停用' }
+  }
 }
 
-export function getFacilityPointIcon(type = '') {
-  return facilityPointIcons[type] || facilityPointIcons.plant
+export function getFacilityStatusMeta(type = '', status = null) {
+  return facilityStatusMeta[type]?.[String(status)] || { key: 'normal', text: '正常' }
+}
+
+/** 设施点位图标:按状态取图标,管网不落点位图标。 */
+export function getFacilityStatusIcon(type = '', status = null) {
+  if (type === 'pipe') return null
+  return getMonitoringPointIcon({ statusKey: getFacilityStatusMeta(type, status).key })
 }
 
 const clusterPointIcon = new URL('../assets/gis/T_MarkPoint_Colorful.png', import.meta.url).href

+ 3 - 25
src/utils/waterHomeModel.js

@@ -1,5 +1,5 @@
 import { normalizeDeviceRow } from './waterSupplyUiModel.js'
-import { getMonitoringPointIcon, getMonitoringPointStatus } from './gisMapColors.js'
+import { getFacilityStatusMeta, getMonitoringPointIcon, getMonitoringPointStatus } from './gisMapColors.js'
 
 /**
  * 供水首页工作台数据模型(T05 #48)。
@@ -283,29 +283,6 @@ function hasCoordinates(point = {}) {
   return lng !== null && lat !== null
 }
 
-// 首页设施点位与设备共用状态色语义:绿-正常、红-报警/异常、蓝-工单/检修、黄-预警/欠费。
-// 管网不落点位,仅以管线呈现;状态缺失时按正常处理。
-const HOME_FACILITY_STATUS_META = {
-  source: {
-    0: { key: 'normal', text: '正常' },
-    1: { key: 'alarm', text: '异常' }
-  },
-  plant: {
-    0: { key: 'normal', text: '正常' },
-    1: { key: 'alarm', text: '停产' }
-  },
-  pumpStation: {
-    0: { key: 'normal', text: '正常' },
-    1: { key: 'alarm', text: '故障停运' },
-    2: { key: 'workOrder', text: '检修中' }
-  },
-  user: {
-    0: { key: 'normal', text: '正常' },
-    1: { key: 'warning', text: '欠费' },
-    2: { key: 'alarm', text: '停用' }
-  }
-}
-
 function normalizeHomeOverviewPoint(point, kind) {
   const lng = finiteCoordinate(point?.lng ?? point?.longitude)
   const lat = finiteCoordinate(point?.lat ?? point?.latitude)
@@ -313,7 +290,8 @@ function normalizeHomeOverviewPoint(point, kind) {
 
   if (kind === 'facility') {
     if (point?.type === 'pipe') return null
-    const statusMeta = HOME_FACILITY_STATUS_META[point?.type]?.[String(point?.status)] || { key: 'normal', text: '正常' }
+    // 设施状态语义统一由共享色板定义,首页与设施 GIS 一张图同源
+    const statusMeta = getFacilityStatusMeta(point?.type, point?.status)
     return {
       ...point,
       id: point?.id ?? point?.featureId,

+ 54 - 8
src/views/subSystem/waterSupply/facility/gis.vue

@@ -102,10 +102,11 @@
 
     <template #legend>
       <div class="panel-section legend-panel">
-          <h3>图例</h3>
-          <div class="legend-row" v-for="item in facilityTypes" :key="item.value">
+          <h3>设施状态图例</h3>
+          <div class="legend-row" v-for="item in facilityStatusLegend" :key="item.label">
             <i class="legend-dot" :style="{ background: item.color }"></i>{{ item.label }}
           </div>
+          <div class="legend-row"><i class="legend-dot" :style="{ background: colorMap.pipe }"></i>供水管网(仅管线)</div>
       </div>
     </template>
 
@@ -122,9 +123,17 @@
           />
           <div v-else class="device-result-list">
             <button v-for="feature in filteredFeatures" :key="feature.id || feature.name" type="button" @click="locateFeature(feature)">
-              <span class="device-name">{{ feature.name }}</span>
+              <span class="device-name">
+                <i class="status-dot" :style="{ background: featureColor(feature) }"></i>
+                {{ feature.name }}
+              </span>
               <span class="device-meta">{{ labelMap[feature.type] || '供水设施' }} · {{ feature.code || '-' }}</span>
-              <el-tag size="small" type="primary">{{ labelMap[feature.type] || '供水设施' }}</el-tag>
+              <!-- 管网只有管线、没有状态语义,这里回退展示设施类型 -->
+              <el-tag v-if="featureStatusMeta(feature)" size="small"
+                      :type="gisPointStatusTagTypes[featureStatusMeta(feature).key] || 'info'">
+                {{ featureStatusMeta(feature).text }}
+              </el-tag>
+              <el-tag v-else size="small" type="primary">{{ labelMap[feature.type] || '供水设施' }}</el-tag>
             </button>
           </div>
       </div>
@@ -167,6 +176,7 @@
             <p>{{ labelMap[detailTarget?.type] || '供水设施' }} · {{ detailTarget?.code || '-' }}</p>
             <div class="status-tags">
               <el-tag type="primary">{{ labelMap[detailTarget?.type] || '供水设施' }}</el-tag>
+              <el-tag :type="gisPointStatusTagTypes[detailStatusMeta.key] || 'info'">{{ detailStatusMeta.text }}</el-tag>
             </div>
           </section>
           <section class="detail-section">
@@ -198,7 +208,7 @@ import { getWaterFacilityOverview, getWaterFacilityFeatures, getWaterFacilityDet
 import { normalizeGeoJson, unwrapResponse } from '@/utils/waterSupplyModel'
 import { getRequestErrorMessage } from '@/utils/waterFacilityQuery'
 import { buildFacilityMapModel, filterFacilityMapModel, findFacilityFeature, formatFacilityDetail } from '@/utils/waterFacilityGisModel'
-import { getFacilityPointIcon } from '@/utils/gisMapColors'
+import { getFacilityStatusIcon, getFacilityStatusMeta, gisPointStatusColors, gisPointStatusTagTypes } from '@/utils/gisMapColors'
 
 const centerPoint = { lng: 110.3965, lat: 28.4638 }
 
@@ -234,6 +244,14 @@ const labelMap = {
   user: '用水户'
 }
 
+// 顶部状态标识:与点位同源,绿-正常、红-报警/异常、蓝-检修中、黄-欠费
+const facilityStatusLegend = [
+  { label: '正常', color: gisPointStatusColors.normal },
+  { label: '异常 / 停产 / 故障停运 / 停用', color: gisPointStatusColors.alarm },
+  { label: '检修中', color: gisPointStatusColors.workOrder },
+  { label: '欠费', color: gisPointStatusColors.warning }
+]
+
 const gisMapRef = ref(null)
 const searchKeyword = ref('')
 const selectedFacilityType = ref('')
@@ -285,20 +303,36 @@ const filteredFeatures = computed(() => {
 
 const mapPoints = computed(() => visibleMapModel.value.points.map(point => ({
   ...point,
-  color: colorMap[point.type] || '#457b9d',
-  iconUrl: getFacilityPointIcon(point.type),
+  color: gisPointStatusColors[getFacilityStatusMeta(point.type, point.status).key],
+  iconUrl: getFacilityStatusIcon(point.type, point.status),
+  statusText: getFacilityStatusMeta(point.type, point.status).text,
   name: point.name || point.code || '供水设施'
 })))
 
 const mapLines = computed(() => visibleMapModel.value.lines.map(line => ({
   ...line,
   color: colorMap[line.type] || '#fcbf49',
-  iconUrl: getFacilityPointIcon(line.type),
   name: line.name || '供水管网'
 })))
 
 const detailEntries = computed(() => formatFacilityDetail(detailRecord.value, detailTarget.value?.type))
 
+// 管网只有管线、不参与设施状态语义;其余设施取状态元数据
+function featureStatusMeta(feature) {
+  return feature?.type === 'pipe' ? null : getFacilityStatusMeta(feature?.type, feature?.status)
+}
+
+function featureColor(feature) {
+  const meta = featureStatusMeta(feature)
+  return meta ? gisPointStatusColors[meta.key] : colorMap.pipe
+}
+
+// 详情顶部标签展示设施业务状态(异常/停产/故障停运/检修中/欠费/停用),与点位颜色同一口径
+const detailStatusMeta = computed(() => getFacilityStatusMeta(
+  detailTarget.value?.type,
+  detailRecord.value?.status ?? detailTarget.value?.status
+))
+
 watch(layerState, () => {
   if (mapReady.value) nextTick(() => gisMapRef.value?.fitBounds())
 }, { deep: true })
@@ -500,6 +534,18 @@ async function refreshScene() {
   border-radius: 50%;
   flex-shrink: 0;
 }
+.device-name {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+}
+.status-dot {
+  display: inline-block;
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  flex: none;
+}
 .result-section {
   flex: 1;
   min-height: 0;

+ 35 - 9
tests/gisMapCanvas.test.mjs

@@ -1,7 +1,8 @@
 import test from 'node:test'
 import assert from 'node:assert/strict'
 import { readFile } from 'node:fs/promises'
-import { getFacilityPointIcon, getMapClusterIcon, getMonitoringPointColor, getMonitoringPointIcon, getMonitoringPointLabel, getMonitoringPointStatus, getMonitoringPointTagType } from '../src/utils/gisMapColors.js'
+import { getMapClusterIcon, getMonitoringPointColor, getMonitoringPointIcon, getMonitoringPointLabel, getMonitoringPointStatus, getMonitoringPointTagType } from '../src/utils/gisMapColors.js'
+import { getFacilityStatusIcon, getFacilityStatusMeta } from '../src/utils/gisMapColors.js'
 
 test('shared gis map canvas converts WGS84 coordinates before BMapGL rendering', async () => {
   const source = await readFile('src/components/GisMapCanvas.vue', 'utf8')
@@ -69,13 +70,38 @@ test('shared gis map canvas renders every point as an official icon with a label
   assert.ok(source.includes("emit('point-click', point)"))
 })
 
-test('facility point icons follow the five-facility-type contract', () => {
-  assert.match(getFacilityPointIcon('source'), /T_MarkPoint_Blue\.png/u)
-  assert.match(getFacilityPointIcon('plant'), /T_MarkPoint_Green\.png/u)
-  assert.match(getFacilityPointIcon('pumpStation'), /T_MarkPoint_Red\.png/u)
-  assert.match(getFacilityPointIcon('pipe'), /T_MarkPoint_Yellow\.png/u)
-  assert.match(getFacilityPointIcon('user'), /T_MarkPoint_Black\.png/u)
-  assert.match(getFacilityPointIcon('unknown'), /T_MarkPoint_Green\.png/u)
+test('facility point colors follow the facility status semantics shared with monitoring devices', () => {
+  // 供水设施一张图与首页统一:绿-正常、红-报警/异常、蓝-工单/检修、黄-预警/欠费
+  assert.deepEqual(getFacilityStatusMeta('source', 0), { key: 'normal', text: '正常' })
+  assert.deepEqual(getFacilityStatusMeta('source', 1), { key: 'alarm', text: '异常' })
+  assert.deepEqual(getFacilityStatusMeta('plant', 1), { key: 'alarm', text: '停产' })
+  assert.deepEqual(getFacilityStatusMeta('pumpStation', 1), { key: 'alarm', text: '故障停运' })
+  assert.deepEqual(getFacilityStatusMeta('pumpStation', 2), { key: 'workOrder', text: '检修中' })
+  assert.deepEqual(getFacilityStatusMeta('user', 1), { key: 'warning', text: '欠费' })
+  assert.deepEqual(getFacilityStatusMeta('user', 2), { key: 'alarm', text: '停用' })
+  // 未维护状态的设施按正常处理,不臆造异常
+  assert.deepEqual(getFacilityStatusMeta('source', null), { key: 'normal', text: '正常' })
+  assert.deepEqual(getFacilityStatusMeta('plant', undefined), { key: 'normal', text: '正常' })
+  assert.deepEqual(getFacilityStatusMeta('source', 9), { key: 'normal', text: '正常' })
+  assert.deepEqual(getFacilityStatusMeta('unknown', 1), { key: 'normal', text: '正常' })
+})
+
+test('facility gis page colors points by facility status instead of facility type', async () => {
+  const source = await readFile('src/views/subSystem/waterSupply/facility/gis.vue', 'utf8')
+
+  assert.match(source, /getFacilityStatusMeta/u)
+  assert.match(source, /getFacilityStatusIcon/u)
+  assert.doesNotMatch(source, /iconUrl: getFacilityPointIcon\(point\.type\)/u)
+  assert.match(source, /设施状态图例/u)
+})
+
+test('pipelines keep no point icon while facilities map status to the shared palette', () => {
+  // 管网保持现状:只构造管线,不添加图标
+  assert.equal(getFacilityStatusIcon('pipe', 0), null)
+  assert.match(getFacilityStatusIcon('source', 0), /T_MarkPoint_Green\.png/u)
+  assert.match(getFacilityStatusIcon('source', 1), /T_MarkPoint_Red\.png/u)
+  assert.match(getFacilityStatusIcon('pumpStation', 2), /T_MarkPoint_Blue\.png/u)
+  assert.match(getFacilityStatusIcon('user', 1), /T_MarkPoint_Yellow\.png/u)
 })
 
 test('cluster icons reuse the single member icon or fall back to the colorful marker', () => {
@@ -140,7 +166,7 @@ test('water supply GIS pages share the common canvas component', async () => {
   assert.match(facilitySource, /<GisMapCanvas/u)
   assert.match(facilitySource, /:points="mapPoints"/u)
   assert.match(facilitySource, /:lines="mapLines"/u)
-  assert.match(facilitySource, /iconUrl: getFacilityPointIcon\(point\.type\)/u)
+  assert.match(facilitySource, /getFacilityStatusIcon\(point\.type, point\.status\)/u)
   assert.match(facilitySource, /cluster-label="设施集合"/u)
   assert.match(facilitySource, /@point-click="locateFeature"/u)
   const alarmSource = await readFile('src/views/subSystem/waterSupply/components/WaterAlarmPage.vue', 'utf8')