Prechádzať zdrojové kódy

修复供水首页入口路由跳转

Kazerin 3 týždňov pred
rodič
commit
feb44bdf27

+ 9 - 0
memory/sessions/2026-09-28.md

@@ -40,3 +40,12 @@
 - 后端默认查询在无采样时仍返回供水设备基础档案,监测值与时间保持空值;有采集时间或数值筛选时仍只保留匹配采样的设备。前端将无监测值显示为“暂无数据”,不计入异常点位统计。
 - 定向测试先红后绿:`MonitoringServiceImplTest` 模拟四台已约定供水设备及排水设备,核对三页归属、无采样空值与筛选行为;`waterSupplyMonitoring.test.mjs` 锁定无数据的前端状态。
 - 后端 `zksy-system` 及依赖全量测试通过;整仓 Maven 在并行新增的 cockpit 测试编译中失败(`CockpitMonitoringPointDetailSnapshotContractTest` 调用旧 `load` 形参),未触碰该文件。前端构建通过,Node 全量 179 项中 176 通过、3 项为既有旧设备页测试失败。真实 KingbaseES 与浏览器登录态验收仍由项目侧复核。
+
+## Gogs #68 / T04 首页入口路由跳转修复
+
+- 公共 seam 为首页语义入口解析到当前用户动态菜单路由;未修改数据库菜单或权限配置。
+- 新增 `src/router/waterHomeRouteResolver.js`,支持供水专属监测报警路径和已部署的共享报警菜单路径,排除其他子系统/未知入口;首页统一通过解析器跳转。
+- 修复审核快捷入口及实时报警回落审核时丢失 `rootTypeId=1` 的问题,确保共享审核页仍按供水范围加载;保留 alarmId/deviceCode 深链参数。
+- 新增路由解析器测试,扩展 `npm test` 项目入口;Home 模型测试覆盖审核入口供水范围。
+- `npm test` 6/6 通过;首页/模型及路由目标回归 25 项通过;生产构建通过;`git diff --check` 通过。全量 183 项中 180 项通过,3 项仍是旧设备台账/责任人测试引用已删除 `WaterDevicePage.vue` 的既有失败。
+- Code Review:Standards 无硬违规,Spec 复核无阻断;将路由评分数字收敛为具名常量。保留已有 `.gitignore` 用户修改,不纳入本票。

+ 1 - 1
package.json

@@ -7,7 +7,7 @@
   "type": "module",
   "scripts": {
     "dev": "vite",
-    "test": "node --test tests/waterSupplyMonitoring.test.mjs tests/waterSupplyModel.test.mjs",
+    "test": "node --test tests/waterHomeRouteResolver.test.mjs tests/waterSupplyMonitoring.test.mjs tests/waterSupplyModel.test.mjs",
     "build:prod": "vite build",
     "build:stage": "vite build --mode staging",
     "preview": "vite preview"

+ 79 - 0
src/router/waterHomeRouteResolver.js

@@ -0,0 +1,79 @@
+const SCORE = {
+  SUFFIX_BASE: 1000,
+  WATER_PATH: 500,
+  ROOT_TYPE: 400,
+  SHARED_ALARM_PATH: 200,
+  TITLE: 100
+}
+
+const WATER_ALARM_ENTRY_ALIASES = {
+  '/monitorAlarm/index': {
+    title: '报警管理',
+    suffixes: ['/monitorAlarm/index', '/alarm/common/list', '/alarm/list', '/alarm/alarmList', '/alarm/index']
+  },
+  '/monitorAlarm/audit': {
+    title: '报警审核',
+    suffixes: ['/monitorAlarm/audit', '/alarm/common/audit', '/alarm/audit', '/alarm/alarmAudit']
+  }
+}
+
+function normalizePath(value) {
+  const path = String(value || '').trim()
+  if (!path) return ''
+  const normalized = path.startsWith('/') ? path : `/${path}`
+  return normalized.replace(/\/+/gu, '/').replace(/\/$/u, '') || '/'
+}
+
+function parseQuery(value) {
+  if (!value) return {}
+  if (typeof value === 'object') return value
+  try { return JSON.parse(value) || {} } catch { return {} }
+}
+
+function routeRootTypeId(route) {
+  const query = parseQuery(route?.query || route?.meta?.query)
+  return query.rootTypeId == null ? null : Number(query.rootTypeId)
+}
+
+function routeScore(route, candidates, title) {
+  const path = normalizePath(route?.path)
+  if (!path) return Number.NEGATIVE_INFINITY
+  const rootTypeId = routeRootTypeId(route)
+  if (rootTypeId != null && rootTypeId !== 1) return Number.NEGATIVE_INFINITY
+  if (title && !path.includes('/subSystem/waterSupply/') && !path.startsWith('/subSystem/alarm/')) {
+    return Number.NEGATIVE_INFINITY
+  }
+
+  const suffixIndex = candidates.findIndex(candidate => path.endsWith(normalizePath(candidate)))
+  const titleMatch = route?.meta?.title === title
+  if (suffixIndex < 0 && !titleMatch) return Number.NEGATIVE_INFINITY
+  let score = suffixIndex < 0 ? 0 : SCORE.SUFFIX_BASE - suffixIndex
+  if (titleMatch) score += SCORE.TITLE
+  if (path.includes('/subSystem/waterSupply/')) score += SCORE.WATER_PATH
+  if (rootTypeId === 1) score += SCORE.ROOT_TYPE
+  if (path.startsWith('/subSystem/alarm/')) score += SCORE.SHARED_ALARM_PATH
+  return score
+}
+
+/**
+ * Resolve a semantic water-home entry against the routes currently registered by
+ * the permission store. Shared alarm menus and dedicated water menus both occur
+ * in deployed environments, so this boundary must not assume a database path.
+ */
+export function resolveWaterHomeRoute(routes, target) {
+  const list = Array.isArray(routes) ? routes : []
+  const canonicalPath = typeof target === 'string' ? target : target?.path
+  const definition = WATER_ALARM_ENTRY_ALIASES[canonicalPath]
+  const candidates = definition?.suffixes || [canonicalPath]
+  const title = definition?.title
+  let best = null
+  let bestScore = Number.NEGATIVE_INFINITY
+  for (const route of list) {
+    const score = routeScore(route, candidates, title)
+    if (score > bestScore) {
+      best = route
+      bestScore = score
+    }
+  }
+  return bestScore === Number.NEGATIVE_INFINITY ? null : best
+}

+ 2 - 1
src/utils/waterHomeModel.js

@@ -42,7 +42,7 @@ export const WATER_DEVICE_ROOT_TYPE_ID = 1
 export const HOME_QUICK_ENTRIES = [
   { key: 'abnormal', label: '设备异常管理', suffix: '/device/abnormal', icon: 'WarningFilled' },
   { key: 'alarm', label: '报警管理', suffix: '/monitorAlarm/index', icon: 'BellFilled' },
-  { key: 'audit', label: '报警审核', suffix: '/monitorAlarm/audit', icon: 'Finished' },
+  { key: 'audit', label: '报警审核', suffix: '/monitorAlarm/audit', query: { rootTypeId: WATER_DEVICE_ROOT_TYPE_ID }, icon: 'Finished' },
   { key: 'map', label: '监测报警一张图', suffix: '/waterSupplyAlarmGIS', query: { rootTypeId: WATER_DEVICE_ROOT_TYPE_ID }, icon: 'MapLocation' }
 ]
 
@@ -244,6 +244,7 @@ export function realtimeAlarmJumpTarget(row = {}) {
   return {
     path: '/monitorAlarm/audit',
     query: {
+      rootTypeId: WATER_DEVICE_ROOT_TYPE_ID,
       alarmId: row?.id ?? row?.alarmId ?? undefined,
       deviceCode: row?.deviceCode || undefined
     }

+ 2 - 1
src/views/subSystem/waterSupply/components/WaterSupplyHome.vue

@@ -293,6 +293,7 @@ import { normalizeGeoJson, normalizePage, unwrapResponse } from '@/utils/waterSu
 import { normalizeDeviceGisPoints } from '@/utils/waterSupplyUiModel'
 import { gisPointStatusColors, gisPointStatusLabels, gisPointStatusTagTypes } from '@/utils/gisMapColors'
 import { buildFacilityMapModel } from '@/utils/waterFacilityGisModel'
+import { resolveWaterHomeRoute } from '@/router/waterHomeRouteResolver'
 import GisMapCanvas from '@/components/GisMapCanvas.vue'
 import {
   HOME_QUICK_ENTRIES, WATER_DEVICE_ROOT_TYPE_ID, buildHomeGisOverview, buildHomeWorkOrderQuery,
@@ -494,7 +495,7 @@ function openRealtimeAlarm(item) {
 }
 
 function resolveRoutePath(suffix) {
-  const matched = router.getRoutes().find(item => item.path.endsWith(suffix))
+  const matched = resolveWaterHomeRoute(router.getRoutes(), suffix)
   return matched ? matched.path : ''
 }
 

+ 3 - 1
tests/waterHomeModel.test.mjs

@@ -211,7 +211,7 @@ test('realtime alarm jumps split between audit and work order by display status'
     displayStatus: 1
   }), {
     path: '/monitorAlarm/audit',
-    query: { alarmId: 'alarm-1', deviceCode: 'CN-GS-202609020001' }
+    query: { rootTypeId: 1, alarmId: 'alarm-1', deviceCode: 'CN-GS-202609020001' }
   })
   assert.deepEqual(realtimeAlarmJumpTarget({
     id: 'alarm-2',
@@ -225,6 +225,7 @@ test('realtime alarm jumps split between audit and work order by display status'
   // 已转工单但缺少工单号时回落到报警审核,避免无效跳转
   const fallback = realtimeAlarmJumpTarget({ id: 'alarm-3', displayStatus: 2 })
   assert.equal(fallback.path, '/monitorAlarm/audit')
+  assert.equal(fallback.query.rootTypeId, 1)
 })
 
 test('abnormal todo rows reuse the shared device normalization contract', () => {
@@ -354,6 +355,7 @@ test('home GIS facility points share device status color semantics by facility s
 test('home quick entries cover the four workbench jumps', () => {
   assert.deepEqual(HOME_QUICK_ENTRIES.map(item => item.label), ['设备异常管理', '报警管理', '报警审核', '监测报警一张图'])
   assert.ok(HOME_QUICK_ENTRIES.every(item => item.suffix.startsWith('/')))
+  assert.equal(HOME_QUICK_ENTRIES.find(item => item.key === 'audit').query.rootTypeId, 1)
 })
 
 test('home device statistics derive online rate from the unified equipment endpoint', () => {

+ 35 - 0
tests/waterHomeRouteResolver.test.mjs

@@ -0,0 +1,35 @@
+import test from 'node:test'
+import assert from 'node:assert/strict'
+import { resolveWaterHomeRoute } from '../src/router/waterHomeRouteResolver.js'
+
+test('resolves dedicated water routes by canonical suffix', () => {
+  const route = resolveWaterHomeRoute([
+    { path: '/subSystem/waterSupply/monitorAlarm/index', meta: { title: '报警管理' } },
+    { path: '/subSystem/waterSupply/monitorAlarm/audit', meta: { title: '报警审核' } }
+  ], '/monitorAlarm/index')
+
+  assert.equal(route.path, '/subSystem/waterSupply/monitorAlarm/index')
+})
+
+test('resolves legacy shared alarm menus without changing database routes', () => {
+  const routes = [
+    { path: '/subSystem/alarm/alarmList', meta: { title: '报警管理' }, query: { rootTypeId: 1 } },
+    { path: '/subSystem/alarm/alarmAudit', meta: { title: '报警审核' }, query: { rootTypeId: 1 } }
+  ]
+
+  assert.equal(resolveWaterHomeRoute(routes, '/monitorAlarm/index').path, '/subSystem/alarm/alarmList')
+  assert.equal(resolveWaterHomeRoute(routes, '/monitorAlarm/audit').path, '/subSystem/alarm/alarmAudit')
+})
+
+test('does not select an unrelated subsystem route with the same title', () => {
+  const routes = [
+    { path: '/subSystem/drainage/monitorAlarm/index', meta: { title: '报警管理' } },
+    { path: '/subSystem/waterSupply/monitorAlarm/index', meta: { title: '报警管理' }, query: { rootTypeId: 1 } }
+  ]
+
+  assert.equal(resolveWaterHomeRoute(routes, '/monitorAlarm/index').path, '/subSystem/waterSupply/monitorAlarm/index')
+})
+
+test('returns null when the requested entry is not registered', () => {
+  assert.equal(resolveWaterHomeRoute([{ path: '/404', meta: { title: '页面不存在' } }], '/monitorAlarm/index'), null)
+})