Prechádzať zdrojové kódy

fix(cockpit): 票46 复验修复 —— 详情状态标签去掉顶部已废弃的形状,与一张图图例完全一致

- 维护者复验反馈:详情卡状态标签「□ ≡ 无设备」多出顶部已废弃的形状设计(形状 SVG 与图标并存),最左空出一节
- 根因:顶部图例定稿用 :deep(.status-tag__shape){display:none} 隐形状只留图标+文字;票46 把图标做进 StatusTag 内部时仍渲染形状
- 修复:StatusTag 图标优先 —— 传 icon 只画图标、形状/圆点挪进 template v-else(互斥);data-shape 保留(语义数据);图标尺寸 12u→14u 对齐顶部状态行;顶部图例不传 icon 零改动
- 守卫补牙:E10 只判"有没有 icon"未判"形状还在不在"(守卫过宽第六次)→ 新增 E10c(icon 与形状互斥 + 14u + 顶部 :deep 隐形状存在)与探针 F4/F5(两侧形状可见性逐行对照);变异 M11 验穿
- 离线闸门 t46 54/54、变异 12/12;既有闸门 t05/t09/t10/t11/t12/t41/t43/t44/t15/t50 回归全绿
- 真实页面复验 29/29 全过(CBWS202610150001/21119740):顶部 8 行形状可见数=0、详情标签形状可见数=0,视觉与顶部「🔔 报警」完全一致
ChenJZ 5 dní pred
rodič
commit
80da342145

+ 4 - 0
.scratch/cockpit-real-data-wayfinding/issues/46-untitled.md

@@ -117,5 +117,9 @@ Labels: ready-for-agent, wayfinder:task
 ### local — 2026-10-15T09:49:14.842Z
 浏览器走查完成并关闭(2026-10-15 第三轮,维护者授权,真实后端 real)。方式:一次性 CDP 探针(本仓票 26/33/36 手法;地图标记是 Cesium 实体,交互式会话点不到,且并行会话写 src 会触发 vite 整页重载——进入路径做成可重入、交互+读数合并进单个页内 evaluate 原子块)。目标 CBWS202610150001(唯一报警点,顶部状态栏「报警」筛选孤立出来 → setCameraFocus 飞近 → 真鼠标点票 47 DOM overlay 标记内部 SVG)/ 设备 21119740(真实 5 预警/66 报警/7 工单)。**27/27 全过**:① 点位摘要=状态卡+KPI 计数条(ALARM=1:lv1…真 0 照画),t41 钉住的 dd/b 形状原样;② 五页签默认档案、图片并入档案页(sections=archive,latestImage)、页签条数 5/66/7;③ 报警第 1/7 页·共 66 条、每页 10 行、末页余 6 行、到边按钮禁用、66 条默认全收起;④ chips all=66/4级=66、点分类后页码回第 1 页、回显「当前分类:4级(再点同一项取消)」、再点取消恢复全量;⑤ 展开一条报警见 数据时间/状态/来源/关联设备/编码/等级/量值/位置,值全非空白;⑥ 顶部状态栏 8 行全带图标、详情内 3 个状态标签全带图标且色键 lv1 与顶部报警行同源;⑦ 返回再进同一设备零重复取数(detail-aggregate 0→0)、视图回默认页签;⑧ 480px 窄面板 tabs/aside/body/chips 溢出量全 0;Z 应用侧零 pageerror、零 console error。证据 verify/artifacts/t46/(探针脚本 + 5 张截图 + walkthrough-result.json)。走查中修掉探针自身两处误判(顶部图例图标类名选择器错、C3 空集合 every 恒真需连行数一起判),非应用缺陷。源码提交 2e27b29 已推送 origin/master(ChenJZ)。本票关闭。
 
+### local — 2026-10-16T01:27:31.891Z
+复验修复(2026-10-15,维护者反馈第 4 条):点位摘要/设备详情的状态标签最右侧多出顶部已废弃的形状设计(「□ ≡ 无设备」:hollowSquare 形状 SVG 与图标并存),最左空出一节、与一张图顶部「≡ 无设备」对不齐。**根因**:批次 1 第三轮的顶部定稿用 `.map-holo__legend-row :deep(.status-tag__shape){display:none}` 把形状隐去、只留图标+文字;票 46 返工把图标做进 StatusTag 内部(icon prop)时仍渲染形状 SVG,于是「形状+图标+文字」三件并存。**修复**:StatusTag 改**图标优先**——传 icon 只画图标、不渲染形状/圆点(二者互斥,形状挪进 `<template v-else>`);`data-shape` 保留(语义数据,走查断言在读,不是可见元素);图标尺寸 12u→**14u** 对齐顶部状态行;顶部图例不传 icon(走 v-else 分支)零改动。**守卫补牙**:上一版 E10 只判"有没有 icon"、没判"形状还在不在"(守卫比它要守的东西宽第六次)——新增 E10c(icon 与形状互斥 + 14u 尺寸 + 顶部 :deep 隐形状仍存在)与探针 F4/F5(两侧形状**可见性**逐行对照,非仅 existence);变异 M11(形状照画→E10c 红)验穿,t46 54/54、变异 12/12。**真实页面复验 29/29 全过**(新增 F4/F5:顶部 8 行形状可见数=0、详情形状可见数=0),截图确认「🔔 报警」与顶部一致。修复连同证据将随本票收口提交推送。
+
+
 
 ## Answer

BIN
.scratch/cockpit-real-data-wayfinding/verify/artifacts/t46/01-point-level.png


+ 90 - 0
.scratch/cockpit-real-data-wayfinding/verify/artifacts/t46/probe-legend-diff.mjs

@@ -0,0 +1,90 @@
+/**
+ * 诊断:详情卡状态标签 与 一张图顶部状态栏 的 DOM 结构逐一对比(维护者反馈"多出一个废弃设计")。
+ * 只读探针:打开点位详情 → dump 两侧 outerHTML + 计算样式。
+ */
+import { mkdir, writeFile } from 'node:fs/promises'
+import path from 'node:path'
+import { cdpLaunch } from '../../../../cockpit-shell-panel-layout/verify/cdp-driver.mjs'
+const CHROME = process.env.CHROME_PATH || 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'
+const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
+const POINT = 'CBWS202610150001'
+
+const browser = await cdpLaunch({ executablePath: CHROME, headless: true, width: 1680, height: 950 })
+const page = await browser.newPage()
+await page.goto('http://127.0.0.1:8000/#/cockpit', { timeout: 60000 })
+await sleep(2500)
+await page.click('[data-cockpit-intro]').catch(() => {})
+await page.waitForFunction(() => ['ready', 'failed', 'disabled'].includes(document.querySelector('.cockpit-shell__map .map-canvas')?.dataset.mapEngine || ''), { timeout: 60000 })
+
+// 报警筛选 + 飞近 + 真鼠标点标记(与走查同一进入路径)
+await page.click('[data-legend-key="ALARM"]').catch(() => {})
+await sleep(4000)
+const coord = await page.evaluate(async (code) => {
+    const r = await fetch('http://192.168.110.96:8301/api/cockpit/v1/map-snapshot?page=1&pageSize=200&pointStatus=ALARM', { cache: 'no-store' })
+    const j = await r.json()
+    const found = ((j.data || j).monitoringPoints || []).find((p) => (p.pointCode || p.code) === code)
+    return found ? { lon: found.longitude, lat: found.latitude } : null
+}, POINT)
+await page.evaluate((c) => globalThis.__cockpitMap?.setCameraFocus({ lon: c.lon, lat: c.lat, height: 900 }), coord)
+await sleep(4000)
+const box = await page.evaluate((code) => {
+    const node = document.querySelector(`[data-marker-overlay-code="${code}"]`)
+    const inner = node?.querySelector('svg')
+    if (!inner) return null
+    const r = inner.getBoundingClientRect()
+    return { cx: r.x + r.width / 2, cy: r.y + r.height / 2 }
+}, POINT)
+if (box) {
+    await page._dispatchMouse('mouseMoved', box.cx, box.cy)
+    await page._dispatchMouse('mousePressed', box.cx, box.cy, { button: 'left', buttons: 1, clickCount: 1 })
+    await page._dispatchMouse('mouseReleased', box.cx, box.cy, { button: 'left', buttons: 0, clickCount: 1 })
+}
+await sleep(3000)
+
+const dump = await page.evaluate(() => {
+    const strip = (html) => (html || '').replace(/\s+/g, ' ').slice(0, 900)
+    const topAlarm = document.querySelector('[data-map-legend] [data-legend-key="ALARM"]')
+    const topEmpty = document.querySelector('[data-map-legend] [data-legend-key="EMPTY"]')
+    const cardStatus = document.querySelector('[data-point-status]')
+    const deviceCardTag = document.querySelector('[data-device-card] .status-tag')
+    const shapeOf = (el) => {
+        const svg = el?.querySelector('svg.status-tag__shape')
+        if (!svg) return { shape: false }
+        const r = svg.getBoundingClientRect()
+        return { shape: true, w: Math.round(r.width), h: Math.round(r.height), display: getComputedStyle(svg).display }
+    }
+    const iconOf = (el) => {
+        const icon = el?.querySelector('.map-holo__legend-icon, .status-tag__icon')
+        if (!icon) return { icon: false }
+        const r = icon.getBoundingClientRect()
+        return { icon: true, w: Math.round(r.width), h: Math.round(r.height), cls: icon.className.baseVal !== undefined ? String(icon.getAttribute('class')) : icon.className }
+    }
+    return {
+        topAlarmHTML: strip(topAlarm?.outerHTML),
+        topEmptyHTML: strip(topEmpty?.outerHTML),
+        cardStatusHTML: strip(cardStatus?.outerHTML),
+        deviceCardHTML: strip(deviceCardTag?.outerHTML),
+        topAlarmShape: shapeOf(topAlarm?.querySelector('.status-tag')),
+        topAlarmIcon: iconOf(topAlarm),
+        cardShape: shapeOf(cardStatus),
+        cardIcon: iconOf(cardStatus),
+    }
+})
+console.log(JSON.stringify(dump, null, 2))
+
+// 放大截图:状态卡区域 + 顶部图例区域
+const clipDump = await page.evaluate(() => {
+    const pick = (el) => { const r = el.getBoundingClientRect(); return { x: r.x, y: r.y, width: r.width, height: r.height } }
+    return {
+        card: pick(document.querySelector('[data-point-status-card]')),
+        legend: pick(document.querySelector('[data-map-legend]')),
+    }
+})
+const OUT = path.resolve('.scratch/cockpit-real-data-wayfinding/verify/artifacts/t46')
+await mkdir(OUT, { recursive: true })
+for (const [tag, clip] of Object.entries(clipDump)) {
+    const b64 = await page.screenshot({ clip: { ...clip, scale: 3 } })
+    await writeFile(path.join(OUT, `zoom-${tag}.png`), Buffer.from(b64, 'base64'))
+}
+console.log('zoom shots written')
+await browser.close()

+ 16 - 2
.scratch/cockpit-real-data-wayfinding/verify/artifacts/t46/probe-walkthrough.mjs

@@ -195,15 +195,29 @@ async function main() {
 
     // ---------------- F 图例对齐(点位级:状态卡 + 设备卡片 >=2 个标签)----------------
     // 顶部图例的图标类是 .map-holo__legend-icon(CockpitIcon),详情标签内是 .status-tag__icon。
+    // 维护者复验补判据:顶部定稿是「图标 + 文字」(形状被 CSS 隐去),详情侧曾画出
+    // 「□ ≡ 无设备」多一个已废弃形状 —— 所以两侧都必须测**形状可见性**,不能只测有没有图标。
     const legend = await page.evaluate(() => {
-        const top = [...document.querySelectorAll('[data-map-legend] [data-legend-key]')].map((n) => ({ key: n.dataset.legendKey, tone: n.querySelector('.status-tag')?.dataset.tone || '', icon: !!n.querySelector('.map-holo__legend-icon') }))
-        const card = [...document.querySelectorAll('[data-point-detail] .status-tag')].map((n) => ({ tone: n.dataset.tone, shape: n.dataset.shape, icon: !!n.querySelector('.status-tag__icon') }))
+        const shapeVisible = (tag) => {
+            const svg = tag?.querySelector('.status-tag__shape')
+            if (!svg) return false
+            const r = svg.getBoundingClientRect()
+            return r.width > 0 && r.height > 0 && getComputedStyle(svg).display !== 'none'
+        }
+        const top = [...document.querySelectorAll('[data-map-legend] [data-legend-key]')].map((n) => {
+            const tag = n.querySelector('.status-tag')
+            return { key: n.dataset.legendKey, tone: tag?.dataset.tone || '', icon: !!n.querySelector('.map-holo__legend-icon'), shapeVisible: shapeVisible(tag) }
+        })
+        const card = [...document.querySelectorAll('[data-point-detail] .status-tag')].map((n) => ({ tone: n.dataset.tone, shape: n.dataset.shape, icon: !!n.querySelector('.status-tag__icon'), shapeVisible: shapeVisible(n) }))
         return { top, card }
     })
     const alarmRow = legend.top.find((r) => r.key === 'ALARM')
     check('F1-top-legend-has-icon-rows', legend.top.length >= 8 && alarmRow?.icon === true, `顶部状态栏 ${legend.top.length} 行,报警行带图标=${alarmRow?.icon}`)
     check('F2-detail-tags-carry-icon', legend.card.length >= 2 && legend.card.every((c) => c.icon === true), `详情内 ${legend.card.length} 个状态标签全部带图标=${legend.card.every((c) => c.icon)}`)
     check('F3-tone-shared', legend.card.some((c) => c.tone === alarmRow?.tone), `详情标签色键 ${legend.card.map((c) => c.tone).join('/')} 与顶部报警行 ${alarmRow?.tone} 同源`)
+    // F4/F5 形状可见性两侧一致(顶部定稿隐形状 → 详情也必须隐;这就是"多出一个废弃设计"的判据)
+    check('F4-top-legend-shapes-hidden', legend.top.every((r) => r.shapeVisible === false), `顶部 ${legend.top.length} 行形状可见数=${legend.top.filter((r) => r.shapeVisible).length}`)
+    check('F5-detail-shapes-hidden-matching-top', legend.card.every((c) => c.shapeVisible === false), `详情标签形状可见数=${legend.card.filter((c) => c.shapeVisible).length}(应为 0,与顶部一致)`)
 
     // ---------------- B 进设备详情:标签页 ----------------
     await ensureDeviceTab('archive')

+ 30 - 11
.scratch/cockpit-real-data-wayfinding/verify/artifacts/t46/walkthrough-result.json

@@ -28,6 +28,14 @@
       "name": "F3-tone-shared",
       "pass": true
     },
+    {
+      "name": "F4-top-legend-shapes-hidden",
+      "pass": true
+    },
+    {
+      "name": "F5-detail-shapes-hidden-matching-top",
+      "pass": true
+    },
     {
       "name": "B1-five-tabs-default-archive",
       "pass": true
@@ -137,59 +145,70 @@
       {
         "key": "ALARM",
         "tone": "lv1",
-        "icon": true
+        "icon": true,
+        "shapeVisible": false
       },
       {
         "key": "WARNING",
         "tone": "lv2",
-        "icon": true
+        "icon": true,
+        "shapeVisible": false
       },
       {
         "key": "WORK_ORDER",
         "tone": "lv3",
-        "icon": true
+        "icon": true,
+        "shapeVisible": false
       },
       {
         "key": "NORMAL",
         "tone": "lv4",
-        "icon": true
+        "icon": true,
+        "shapeVisible": false
       },
       {
         "key": "OFFLINE",
         "tone": "point-offline",
-        "icon": true
+        "icon": true,
+        "shapeVisible": false
       },
       {
         "key": "EMPTY",
         "tone": "point-empty",
-        "icon": true
+        "icon": true,
+        "shapeVisible": false
       },
       {
         "key": "UNKNOWN",
         "tone": "point-unknown",
-        "icon": true
+        "icon": true,
+        "shapeVisible": false
       },
       {
         "key": "UNASSOCIATED",
         "tone": "point-unassociated",
-        "icon": true
+        "icon": true,
+        "shapeVisible": false
       }
     ],
     "card": [
       {
         "tone": "lv1",
         "shape": "triangle",
-        "icon": true
+        "icon": true,
+        "shapeVisible": false
       },
       {
         "tone": "lv1",
         "shape": "triangle",
-        "icon": true
+        "icon": true,
+        "shapeVisible": false
       },
       {
         "tone": "lv1",
         "shape": "triangle",
-        "icon": true
+        "icon": true,
+        "shapeVisible": false
       }
     ]
   },

BIN
.scratch/cockpit-real-data-wayfinding/verify/artifacts/t46/zoom-card.png


BIN
.scratch/cockpit-real-data-wayfinding/verify/artifacts/t46/zoom-legend.png


+ 15 - 0
.scratch/cockpit-real-data-wayfinding/verify/t46-detail-cards-history.mjs

@@ -358,6 +358,21 @@ function check(name, condition, message) {
         cardTags.length >= 3 && cardTags.every((tag) => /:icon="/.test(tag)),
         `详情卡内 ${cardTags.length} 个状态标签全部绑定 icon(与一张图顶部状态栏同一套视觉)`,
     )
+    // E10c(收口后维护者复验补):StatusTag 传 icon 时**不渲染形状/圆点** —— 图标分支与
+    //     形状分支必须互斥(形状在 `<template v-else>` 里)。上一版只判"有没有 icon"、
+    //     没判"形状还在不在",于是详情标签画出「□ ≡ 无设备」(形状是顶部已废弃的设计),
+    //     与顶部「≡ 无设备」对不齐 —— 守卫比它要守的东西宽的第六次现身。
+    //     图标尺寸也钉成顶部的 14u(MapCanvas 状态行同款),两处各写一档迟早漂移。
+    const statusTagCode = read('components/StatusTag.vue')
+    const mapCanvasSource = read('components/MapCanvas.vue')
+    check(
+        'E10c-icon-suppresses-deprecated-shape',
+        /<CockpitIcon v-if="icon"/.test(statusTagCode) &&
+            /<template v-else>[\s\S]*?<svg[\s\S]*?v-if="shapeKey"[\s\S]*?class="status-tag__shape"[\s\S]*?<\/template>/.test(statusTagCode) &&
+            /size="calc\(14 \* var\(--u, 1px\)\)"/.test(statusTagCode) &&
+            /:deep\(\.status-tag__shape\)\s*\{\s*display:\s*none/.test(mapCanvasSource),
+        'StatusTag 传 icon 只画图标(形状/圆点在 v-else 分支)、尺寸 14u 与顶部一致;顶部图例仍以 CSS 隐形状',
+    )
     // E10b 图标登记的单源在 pointStatus(顶部状态栏与详情共用一个函数,不各存一份表)。
     //     判据:登记只在 pointStatus 一处定义;MapCanvas 经 legendVisuals 消费(不自建图标表)。
     const pointStatus = read('domain/pointStatus.js')

+ 10 - 0
.scratch/cockpit-real-data-wayfinding/verify/t46-mutations.mjs

@@ -187,6 +187,16 @@ mutate(
     'H2-point-status-matches-top-legend',
 )
 
+// M11(收口后维护者复验补):把"形状与图标互斥"改回"形状照画" —— 即本次修掉的回归
+// (详情标签画出「□ ≡ 无设备」多一个顶部已废弃的形状)。E10c 必须转红。
+mutate(
+    'M11-shape-renders-alongside-icon',
+    'components/StatusTag.vue',
+    '        <template v-else>',
+    '        <template v-if="true">',
+    'E10c-icon-suppresses-deprecated-shape',
+)
+
 console.log('')
 console.log(`T46 变异测试:${proved}/${injected} 处变异被对应断言抓住。`)
 rmSync(reportDir, { recursive: true, force: true })

+ 24 - 19
src/modules/cockpit/components/StatusTag.vue

@@ -12,27 +12,32 @@
 -->
 <template>
     <span class="status-tag" :class="`is-${toneKey}`" :data-tone="toneKey" :data-shape="shapeKey || ''">
-        <!-- 形状:与地图标记同一份片段(domain/pointStatus.js 的 SHAPES) -->
-        <svg
-            v-if="shapeKey"
-            class="status-tag__shape"
-            viewBox="0 0 12 12"
-            width="1em"
-            height="1em"
-            fill="none"
-            aria-hidden="true"
-            focusable="false"
-            v-html="shapeMarkup"
-        />
-        <i v-else class="status-tag__dot" aria-hidden="true"></i>
         <!--
-            票 46 返工第 2 条:可选的图例图标位(`icon` = CockpitIcon 语义名,
-            值来自 domain/pointStatus.js 的**单源登记** `legendIconFor`)。
-            详情面板传入它之后,状态标签与一张图顶部状态栏就是同一套视觉:
-            图标 + 形状 + 语义色 + 文字,词条同源,不再各画各的。
-            不传 = 保持票 06 的原外观(事件列表等既有调用点零变化)。
+            票 46 返工第 2 条(收口后维护者复验修正):传了 `icon` 就**只画图标**,
+            不再画形状/圆点 —— 与一张图顶部状态栏**完全一致**。
+            顶部状态栏用 `.map-holo__legend-row :deep(.status-tag__shape){display:none}`
+            把形状隐去、只留图标 + 文字(批次 1 第三轮的定稿);详情侧上一版把图标做进
+            StatusTag 内部却仍渲染形状,于是「□ ≡ 无设备」多出一个已废弃的形状、
+            与顶部「≡ 无设备」对不齐。图标优先即消除该形状,`data-shape` 仍保留
+            (它是语义数据,走查断言读它,不是可见元素)。
+            不传 icon = 保持票 06 原外观(形状/圆点 + 文字,事件列表等既有调用点零变化)。
         -->
-        <CockpitIcon v-if="icon" monochrome :name="icon" size="calc(12 * var(--u, 1px))" class="status-tag__icon" />
+        <CockpitIcon v-if="icon" monochrome :name="icon" size="calc(14 * var(--u, 1px))" class="status-tag__icon" />
+        <template v-else>
+            <!-- 形状:与地图标记同一份片段(domain/pointStatus.js 的 SHAPES) -->
+            <svg
+                v-if="shapeKey"
+                class="status-tag__shape"
+                viewBox="0 0 12 12"
+                width="1em"
+                height="1em"
+                fill="none"
+                aria-hidden="true"
+                focusable="false"
+                v-html="shapeMarkup"
+            />
+            <i v-else class="status-tag__dot" aria-hidden="true"></i>
+        </template>
         <span class="status-tag__label">{{ label }}</span>
     </span>
 </template>