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

feat(cockpit): 管理信息四项指标接入排水雨污二级分项

维护者要的「污水管段 / 雨水管段」此前是占位符(后端无拆分)。现端点排水行下发
networkLevelCounts(0-污水 1-雨水,来自 pipe_network_base.network_level):
· toManagementInformation 归一分项(缺字段→null,脏码剔除),「有没有」与「是多少」分开;
· drainageLevelSplit 判可用性并按码位求和:无排水行→真实 0,任一行缺分项→null(旧后端);
· managementFacilityCards 四项全真值,hint/gap 措辞随可用性切换,降级路径保留缺口句;
· mock 管理载荷同步补分项,与真实端点同形。
码位语义归前端排水字典解释(NETWORK_LEVEL_TEXT/pipeVisual 同源):该列是共享字典,
燃气侧为压力等级,后端不给跨行业名字。
ChenJZ 16 часов назад
Родитель
Сommit
ba3bcfc3c4

+ 65 - 9
src/modules/cockpit/domain/infrastructurePanels.js

@@ -228,6 +228,27 @@ export function toOperatingInformation(raw) {
     }
 }
 
+/**
+ * 排水行的雨污二级分项(维护者 2026-11-02):`networkLevelCounts[]` 只挂在 `industry=DRAINAGE`
+ * 的行上,每项 `{ networkLevel, facilityCount }`。码位语义归前端的排水字典解释
+ * (0-污水 1-雨水,与 `entityDetail.js` 的 NETWORK_LEVEL_TEXT、`pipeVisual.js` 的管线配色同源)——
+ * 后端**不给名字**:`pipe_network_base.network_level` 存的是共享字典 `pipe_network_level`,
+ * 对燃气是压力等级、对排水才是雨污,同一列跨行业语义不同,名字只能按行业给。
+ *
+ * 「有没有」分得开:旧后端没有该字段 → 回 `null`(未知,画占位符);
+ * 新后端给了空数组 → 真实 0(该行确实没有可解释等级的设施)。
+ */
+function toNetworkLevelCounts(raw) {
+    if (!Array.isArray(raw)) return null
+    return raw
+        .filter((item) => item && typeof item === 'object')
+        .map((item) => ({
+            networkLevel: item.networkLevel == null ? '' : String(item.networkLevel).trim(),
+            facilityCount: numberish(item.facilityCount) || 0,
+        }))
+        .filter((row) => row.networkLevel !== '')
+}
+
 /** 管理信息读模型:片区编码与行业计数,不消费已退役的机构/权属维度。 */
 export function toManagementInformation(raw) {
     const reader = fieldReader(raw, 'managementInformation')
@@ -242,6 +263,7 @@ export function toManagementInformation(raw) {
             industry: typeof item.industry === 'string' ? item.industry : '',
             facilityCount: numberish(item.facilityCount),
             equipmentCount: numberish(item.equipmentCount),
+            networkLevelCounts: toNetworkLevelCounts(item.networkLevelCounts),
         })),
         missing: reader.missing,
     }
@@ -759,15 +781,41 @@ export function districtIndustrySummary(rows) {
  * 只消费 `districtIndustry[]`;已退役的 `organizations`/`ownershipDistribution`/`unmaintained` 不再读取。
  * 「责任区域」=片区别名(决策 06),不新增独立维度。
  */
+/**
+ * 排水行的雨污拆分读数(维护者 2026-11-02)。
+ *
+ * 「有没有」与「是多少」分得开,沿用面板取数的老纪律(不把「后端没给」画成 0):
+ *   · 范围里**没有排水行**(行业筛到别处)→ 真实 0,与燃气/供水的 `sumOf` 同一语义;
+ *   · 有排水行但**任一行缺二级分项**(旧后端没下发该字段)→ 回 `null`=未知,
+ *     卡片画占位符、面板 gap 照旧交代缺口;
+ *   · 全部排水行都带分项 → 按码位求和(0-污水 1-雨水,语义归前端排水字典)。
+ *
+ * @param {Array} rows `districtIndustry[]`(领域行,`networkLevelCounts` 已归一为数组或 null)
+ * @returns {{available: boolean, sewage: number, rainwater: number}|null} null=分项不可得
+ */
+export function drainageLevelSplit(rows) {
+    // 输入不是数组(分区还没就绪 / 脏输入)=未知,与「空范围」分得开。
+    if (!Array.isArray(rows)) return null
+    const drainageRows = rows
+        .filter((row) => String((row && row.industry) || '') === 'DRAINAGE')
+    if (!drainageRows.length) return { available: true, sewage: 0, rainwater: 0 }
+    if (!drainageRows.every((row) => Array.isArray(row.networkLevelCounts))) return null
+    const sumOf = (level) => drainageRows.reduce((total, row) => {
+        const hit = row.networkLevelCounts.find((item) => item.networkLevel === level)
+        return total + (hit ? hit.facilityCount || 0 : 0)
+    }, 0)
+    return { available: true, sewage: sumOf('0'), rainwater: sumOf('1') }
+}
+
 /**
  * 管理信息「设施」读数(维护者 2026-10-29):只统计设施,不再出现设备口径。
  *
  * 四类管段来自 `districtIndustry[]` 的行业分项 —— 燃气/供水/排水/窨井各自的设施数。
- * 维护者要的四项是「燃气管段 / 供水管段 / 污水管段 / 雨水管段」;后端本端点只有
- * 行业级(排水不拆雨污)与设施类型级两种粒度,合计口径见下面的说明块:
+ * 维护者要的四项是「燃气管段 / 供水管段 / 污水管段 / 雨水管段」(维护者 2026-11-02 起四项全真值):
  *   · 燃气管段、供水管段 ← GAS / WATER_SUPPLY 两个行业的设施数(与基础档案同源);
- *   · 污水/雨水管段 ← 本端点**没有**雨污拆分,故回占位符,并把缺口写进面板 gap;
- *     能给出的是「排水管段(雨污合计)」,作为说明条附在卡片 caption 上。
+ *   · 污水/雨水管段 ← 排水行自带的雨污二级分项(`networkLevelCounts`,见 `drainageLevelSplit`);
+ *     分项只覆盖能认出等级的排水管线,与「排水设施合计」的差值(泵站等非管线设施)在
+ *     雨水卡说明里如实交代,不强行补齐。
  *
  * @param {Array} rows `districtIndustry[]`
  * @returns {Array} 四张 KPI 卡(2×2)
@@ -780,17 +828,25 @@ export function managementFacilityCards(rows) {
     const gas = sumOf('GAS')
     const water = sumOf('WATER_SUPPLY')
     const drainage = sumOf('DRAINAGE')
+    const split = drainageLevelSplit(list)
     return [
         { key: 'gasFacilityCount', label: '燃气管段', value: valueText(gas), unit: '处' },
         { key: 'waterFacilityCount', label: '供水管段', value: valueText(water), unit: '处' },
-        { key: 'sewageFacilityCount', label: '污水管段', value: DETAIL_PLACEHOLDER, unit: '处', hint: '本端点排水未按雨污拆分,雨污合计见下方说明' },
-        { key: 'rainwaterFacilityCount', label: '雨水管段', value: DETAIL_PLACEHOLDER, unit: '处', hint: '本端点排水未按雨污拆分;排水设施合计 ' + valueText(drainage) + ' 处' },
+        { key: 'sewageFacilityCount', label: '污水管段', value: valueText(split ? split.sewage : null), unit: '处',
+          hint: split ? '按排水管线的雨污二级分类计数' : '本端点暂未提供雨污分项' },
+        { key: 'rainwaterFacilityCount', label: '雨水管段', value: valueText(split ? split.rainwater : null), unit: '处',
+          hint: split
+              ? '按排水管线的雨污二级分类计数;排水设施合计 ' + valueText(drainage) + ' 处(含泵站等非管线设施)'
+              : '本端点暂未提供雨污分项;排水设施合计 ' + valueText(drainage) + ' 处' },
     ]
 }
 export function managementPanel(data, section) {
     const current = data || {}
     const rows = current.districtIndustry || []
     const summary = districtIndustrySummary(rows)
+    // 雨污分项可用性决定 gap 措辞:拿得到就不再喊「后端没有拆分」(那是撒谎),
+    // 拿不到(旧后端)才如实交代缺口 —— 措辞纪律:只说业务含义,不出现接口字段名。
+    const split = drainageLevelSplit(rows)
 
     const blocks = [
         // 维护者 2026-10-29:管理信息**只统计设施**(设备口径归监测模式),
@@ -830,9 +886,9 @@ export function managementPanel(data, section) {
         // 票 68(维护者 2026-10-19 反馈第 3 条):公开「这些数从哪来、覆盖什么范围、与别处为何对得上」。
         // 票 66 随形态一并改写:旧的「一行一个组合」清单说明随清单一起去掉,不留旧措辞。
         // 措辞纪律:只说业务含义与范围,**不出现接口字段名** —— 读者是值班人员,不是接口调用方。
-        sourceNote: '设施计数取自基础设施档案目录(与「基础档案」是同一份目录、同一批设施实体);片区取档案里登记的片区,没有登记的落「未分区」,如实列出、不隐藏。',
-        // 维护者 2026-10-29:雨污拆分在后端还没有 —— 如实说明缺口,不拿「排水合计」冒充其中一项。
-        gap: '本端点的排水设施未按雨污拆分,因此「污水管段 / 雨水管段」暂不给出单项读数;排水设施合计可在「片区 × 行业」块按行业维度读到。',
+        sourceNote: '设施计数取自基础设施档案目录(与「基础档案」是同一份目录、同一批设施实体);片区取档案里登记的片区,没有登记的落「未分区」,如实列出、不隐藏。污水/雨水管段按排水管线的雨污二级分类计,泵站等非管线排水设施不参与拆分。',
+        // 维护者 2026-11-02:雨污分项已由端点下发 —— 只有旧后端(没下发)才保留缺口句。
+        gap: split ? '' : '当前服务端未提供排水的雨污分项,因此「污水管段 / 雨水管段」暂不给出单项读数;排水设施合计可在「片区 × 行业」块按行业维度读到。',
         locate: [],
         locateNote: '片区与行业计数为聚合口径,本面板没有可定位对象',
         missing: panelMissingNote(current),

+ 3 - 1
src/modules/cockpit/mocks/contractMocks.js

@@ -1901,7 +1901,9 @@ function managementInformationPayload(scenario, query = {}) {
             { district: 0, districtName: '城南', industry: 'WATER_SUPPLY', facilityCount: 1, equipmentCount: 1 },
             { district: 1, districtName: '城北', industry: 'MANHOLE', facilityCount: 0, equipmentCount: 1 },
             { district: 2, districtName: '太常', industry: 'GAS', facilityCount: 1, equipmentCount: 1 },
-            { district: null, districtName: '未分区', industry: 'DRAINAGE', facilityCount: 1, equipmentCount: 0 },
+            // 排水行带雨污二级分项(维护者 2026-11-02):与真实端点同形,码位 0-污水 1-雨水。
+            { district: null, districtName: '未分区', industry: 'DRAINAGE', facilityCount: 1, equipmentCount: 0,
+              networkLevelCounts: [{ networkLevel: '0', facilityCount: 1 }] },
     ].filter((row) => (!query.district || normalizedDistrict(row.district) === query.district) && (!query.industry || row.industry === query.industry))
     const selectedFilters = {
         industry: query.industry || null,