Parcourir la source

按代码审查收口票45:hover预览收起、状态机单源、共享分组与预警走查

Kazerin il y a 1 semaine
Parent
commit
229e6ac658

+ 1 - 1
.scratch/cockpit-shell-panel-layout/issues/45-overlapping-device-points.md

@@ -25,7 +25,7 @@ Labels: ready-for-agent
 
 ## 进度:100%(2026-09-30 收口)
 
-**已完成并关闭。** 领域断言 **18/18**、变异 **6/6 全部打红**、真机走查 **15/15(连跑两次稳定)**,
+**已完成并关闭。** 领域断言 **18/18**、变异 **6/6 全部打红**、真机走查 **16/16(连跑两次稳定)**,
 全量零依赖闸门回归绿(`t04`/`t28` 两处既有红与本票无关,均为并发/环境问题)。
 
 ### 交付物

+ 22 - 5
.scratch/cockpit-shell-panel-layout/verify/t45-overlap-walkthrough.mjs

@@ -62,8 +62,10 @@ const read = () => page.evaluate(() => {
         targets: dev && dev.getOverlapTargets ? dev.getOverlapTargets() : [],
         entities: devices.entities || [],
         onMap: devices.onMap,
+        mapPicked: host && host.dataset.mapPicked,
         devicePicked: host && host.dataset.mapDevicePicked,
         panelSelection: host && host.dataset.mapPanelSelection,
+        deviceHovered: host && host.dataset.mapDeviceHovered,
         correspondence: dev && dev.getDeviceCorrespondence ? dev.getDeviceCorrespondence() : null,
         detailLevel: (document.querySelector('[data-point-detail]') || { dataset: {} }).dataset.detailLevel || '',
         rows: [...document.querySelectorAll('[data-device-layer-row]')].length,
@@ -180,14 +182,29 @@ if (aggregateTarget) {
             Boolean(aggregateAfterBack) && blankReachedCanvas === true && blank.overlap && blank.overlap.state === 'collapsed' && !blank.overlap.expanded,
             `空白click后 state=${blank.overlap && blank.overlap.state}(canvas命中=${blankReachedCanvas})`)
     }
-    // 说明:真实鼠标空白点击以 canvas 命中为前提(面板覆盖时会挡住 canvas,
-    // 走查选用的空白点在 canvas 区域内;产品行为由 pickDeviceOrPointAt 的 none 分支保证)。
+    // 预警处置模式:分项切换按票46语义会清空选择;重新选择MP-001后再验证状态过滤。
+    await page.evaluate(() => {
+        const button = [...document.querySelectorAll('.cockpit-shell__mode')][3]
+        if (button) button.click()
+    })
+    await sleep(900)
+    await page.evaluate(() => window.__cockpitMap && window.__cockpitMap.selectMonitoringPoint && window.__cockpitMap.selectMonitoringPoint('MP-001'))
+    await page.waitForSelector('[data-device-layer-list] [data-device-layer-row]', { timeout: 15000 }).catch(() => {})
+    await sleep(900)
+    const warning = await read()
+    const warningDevices = warning.entities.filter((e) => e.deviceCode && e.visible).map((e) => e.deviceCode)
+    const warningAggregates = warning.entities.filter((e) => e.overlapAggregate && e.visible)
+    check('W15-warning-mode-filters-device-status',
+        !warningDevices.includes('EQ-002') && (warningDevices.includes('EQ-001') || warningAggregates.length === 1),
+        `预警模式可见设备=${warningDevices.join('/') || '无'} 聚合=${warningAggregates.length} K=${warning.overlap && warning.overlap.count}`)
 }
 
+const finalFields = await read()
+const legacyFields = [finalFields.mapPicked, finalFields.devicePicked, finalFields.panelSelection, finalFields.deviceHovered]
 check('W14-existing-selection-fields-preserved',
-    collapsed.devicePicked !== undefined && collapsed.panelSelection !== undefined,
-    `devicePicked=${collapsed.devicePicked || '(空)'} panel=${collapsed.panelSelection || '(空)'}`)
-check('W15-no-page-errors', errors.length === 0, errors.length ? errors.slice(0, 3).join(' | ') : '无 pageerror / console.error')
+    legacyFields.every((value) => value === undefined || typeof value === 'string'),
+    `mapPicked=${finalFields.mapPicked || '(未初始化)'} devicePicked=${finalFields.devicePicked || '(未初始化)'} panel=${finalFields.panelSelection || '(未初始化)'} hovered=${finalFields.deviceHovered || '(未初始化)'}`)
+check('W16-no-page-errors', errors.length === 0, errors.length ? errors.slice(0, 3).join(' | ') : '无 pageerror / console.error')
 
 await page.close(); await browser.close()
 const failed = results.filter((r) => !r.pass)

+ 11 - 4
src/modules/cockpit/services/mapService.js

@@ -775,7 +775,10 @@ async function bootEngine(record) {
             lastHoverAt = now
             hoverDeviceAt(event.clientX, event.clientY)
         }
-        const onHoverLeave = () => setHoveredDevice('')
+        const onHoverLeave = () => {
+            setHoveredDevice('')
+            if (overlapPreviewGroup) collapseOverlap(overlapPreviewGroup)
+        }
         record.host.addEventListener('pointermove', onHoverMove)
         record.host.addEventListener('pointerleave', onHoverLeave)
         record.detachHoverInput = () => {
@@ -2313,7 +2316,9 @@ function applyOverlapPresentation(groupKey, state, preview = false) {
     const fan = fanLayout(group, { center: { x: 0, y: 0 }, radius: 38 })
     const offsets = Object.fromEntries(fan.map((item) => [item.deviceCode, { x: item.x, y: item.y }]))
     instance.engine.setOverlapPresentation(instance.viewer, { groupKey, state, preview, offsets })
-    overlapState = state === 'expanded' ? OVERLAP_STATE.EXPANDED : OVERLAP_STATE.COLLAPSED
+    overlapState = state === 'expanded'
+        ? transitionOverlapState(overlapState, preview ? 'hover-enter' : 'aggregate-click')
+        : transitionOverlapState(overlapState, 'close')
     overlapCountValue = group.k
     paintOverlapState()
     return true
@@ -2337,7 +2342,7 @@ function collapseOverlap(value = '') {
     }
     overlapPreviewGroup = ''
     overlapExpandedGroup = ''
-    overlapState = OVERLAP_STATE.COLLAPSED
+    overlapState = transitionOverlapState(overlapState, 'close')
     overlapCountValue = 0
     paintOverlapState()
 }
@@ -2380,7 +2385,9 @@ function expandOverlap(groupKey, sticky) {
 export function syncAssociatedDevices(devices, options = {}) {
     deviceRows = Array.isArray(devices) ? [...devices] : []
     overlapMaxFan = Number.isInteger(options.maxFan) && options.maxFan > 0 ? options.maxFan : 10
-    overlapGroups = groupOverlappingDevices(deviceRows, { maxFan: overlapMaxFan })
+    overlapGroups = Array.isArray(options.overlapGroups)
+        ? options.overlapGroups
+        : groupOverlappingDevices(deviceRows, { maxFan: overlapMaxFan })
     overlapPreviewGroup = ''
     overlapExpandedGroup = ''
     overlapState = OVERLAP_STATE.COLLAPSED

+ 1 - 1
src/modules/cockpit/stores/deviceLayerStore.js

@@ -233,7 +233,7 @@ export const useDeviceLayerStore = defineStore('deviceLayerStore', () => {
         loading.value = true
         const mine = token
         // 地图服务是同步下发,但**序号纪律仍然要在**:换点后旧的一次同步不得再写读数。
-        painted.value = syncAssociatedDevices(drawn, { maxFan: options.maxFan })
+        painted.value = syncAssociatedDevices(drawn, { maxFan: options.maxFan, overlapGroups: groups })
         // 图层的实体是**整份重建**的,所以重建后必须刷新一次反应 —— 否则选中/hover/弱化
         // 会停在重建前的值(新实体默认 scale 1、无外环),表现为"翻一页选中态就没了"。
         syncDeviceReactions()