| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411 |
- /**
- * T44 设备位置符号与可替换资产接缝 纯逻辑检查(票 44;规格 16 / 决策票 10)。
- *
- * 零依赖 node 断言脚本(约定见 t01-panel-catalog.mjs)。
- * 运行:node .scratch/cockpit-shell-panel-layout/verify/t44-device-assets.mjs
- *
- * 覆盖验收第 8 条点名的四件事:**资产接缝与降级分类**、**尺寸层级**、**状态来源独立**、
- * **地形/坐标两种降级分得开**;外加"不显示经纬度数字""不散落形状字符串/资源地址"等静态守卫。
- */
- import { readFileSync } from 'node:fs'
- import {
- ASSET_MODE,
- ASSET_REASON,
- MARKER_SIZE,
- assetReasonText,
- deviceLabel,
- markerSizeFor,
- planDeviceAsset,
- planDevicePlacement,
- sizeHierarchyHolds,
- } from '../../../src/modules/cockpit/domain/deviceAssets.js'
- import {
- DEVICE_STATUS,
- DEVICE_STATUS_VISUALS,
- DEVICE_UNKNOWN_VISUAL,
- deviceLegend,
- deviceVisualForStatus,
- } from '../../../src/modules/cockpit/domain/deviceStatus.js'
- import { POINT_STATUS, POINT_STATUS_VISUALS } from '../../../src/modules/cockpit/domain/pointStatus.js'
- import { deviceCardView, statusView, deviceStatusView } from '../../../src/modules/cockpit/domain/detailFlow.js'
- const checks = []
- function check(name, pass, detail = '') {
- checks.push({ name, pass, detail })
- }
- const device = (overrides = {}) => ({ code: 'EQ-1', pointCode: 'MP-1', name: '供水压力计-001', status: 'NORMAL', ...overrides })
- // ================================================================ A 独立设备状态词表
- {
- // 五态齐全,且**不含**点位专有的 EMPTY(那是点位聚合态)。
- const keys = Object.keys(DEVICE_STATUS_VISUALS)
- check('A1-five-statuses-no-empty',
- keys.length === 5 && !keys.includes('EMPTY') &&
- ['ALARM', 'WARNING', 'WORK_ORDER', 'NORMAL', 'OFFLINE'].every((k) => keys.includes(k)),
- `设备五态:${keys.join('/')}(**无 EMPTY** —— 那是点位聚合态专有)`)
- // 六态(五态 + UNKNOWN)各有色键、形状、文字(颜色不是唯一线索)。
- const all = [...keys.map((k) => DEVICE_STATUS_VISUALS[k]), DEVICE_UNKNOWN_VISUAL]
- check('A2-each-has-tone-shape-label',
- all.every((v) => v.tone && v.shape && v.label),
- all.map((v) => `${v.key}:${v.tone}/${v.shape}/${v.label}`).join(' '))
- // **状态未知不猜正常**。
- const unknown = deviceVisualForStatus('SOMETHING_NEW')
- check('A3-unknown-not-guessed-as-normal',
- unknown.key === 'UNKNOWN' && unknown.key !== 'NORMAL',
- `认不出的状态 → ${unknown.key}「${unknown.label}」(**不猜成正常**)`)
- const missing = deviceVisualForStatus('')
- check('A4-missing-status-is-unknown', missing.key === 'UNKNOWN', '状态缺失 → UNKNOWN')
- // **与点位表分开**:同一字符串在两表里是各自独立的词条,且点位专有键在设备表里查不到。
- check('A5-parallel-not-shared',
- deviceVisualForStatus('EMPTY').key === 'UNKNOWN' && POINT_STATUS_VISUALS.EMPTY.key === 'EMPTY',
- '设备表里 `EMPTY` 认不出(落 UNKNOWN)—— 证明没有借用点位表')
- // 独立图例:行数 == 五态 + UNKNOWN,且与点位图例分开。
- const legend = deviceLegend({ ALARM: 2, NORMAL: 1 })
- check('A6-device-legend-independent',
- legend.length === 6 && legend.find((r) => r.key === 'ALARM').count === 2 &&
- legend.find((r) => r.key === 'OFFLINE').count === 0,
- `设备图例 ${legend.length} 行、按优先级排、0 不隐藏行`)
- // 设备卡片视图走设备表(本票修掉的越界)。
- const card = deviceCardView({ code: 'EQ-1', name: '设备', status: 'ALARM', statusLabel: '', statusReasons: [] })
- const pointStyle = statusView('ALARM')
- check('A7-device-card-uses-device-table',
- card.status.key === 'ALARM' && card.status.tone === DEVICE_STATUS_VISUALS.ALARM.tone,
- `设备卡片状态走设备表(${card.status.key}/${card.status.tone})`)
- // 关键反例:设备拿到一个"点位专有"的状态时,两张表的答案必须不同 ——
- // 若相同就说明设备卡片仍在借点位表(本票修正的越界)。
- const deviceOnEmpty = deviceStatusView('EMPTY')
- const pointOnEmpty = statusView('EMPTY')
- check('A8-device-vs-point-diverge-on-empty',
- deviceOnEmpty.key === 'UNKNOWN' && pointOnEmpty.key === 'EMPTY' && deviceOnEmpty.key !== pointOnEmpty.key,
- `同一个 'EMPTY':设备表给 ${deviceOnEmpty.key}、点位表给 ${pointOnEmpty.key}(两表**确实分开**)`)
- }
- // ================================================================ B 资产接缝与降级链
- {
- // 本期常态:正式素材未提供 → 设备专属图标,并记明原因(不是真模型、不算成功)。
- const icon = planDeviceAsset({ device: device(), color: '#35e39a' })
- check('B1-default-is-icon-not-model',
- icon.mode === ASSET_MODE.ICON && !!icon.image && icon.reason === ASSET_REASON.ASSET_MISSING,
- `默认:模式 ${icon.mode}、原因「${icon.reasonText}」(**不是**真模型)`)
- // 正式图片可用 → 用正式素材。
- const image = planDeviceAsset({ device: device(), imageUrl: '/cockpit/devices/pump.png', color: '#35e39a' })
- check('B2-image-when-available',
- image.mode === ASSET_MODE.IMAGE && image.image === '/cockpit/devices/pump.png',
- `正式素材可用 → 模式 ${image.mode}`)
- // 正式素材加载失败 → 回退图标并如实说明。
- const imageFailed = planDeviceAsset({ device: device(), imageUrl: '/x.png', imageFailed: true, color: '#35e39a' })
- check('B3-image-failure-falls-back-to-icon',
- imageFailed.mode === ASSET_MODE.ICON && imageFailed.reason === ASSET_REASON.ASSET_FAILED,
- `正式素材失败 → 回退图标并记「${imageFailed.reasonText}」`)
- // 未来真模型的四种不可用各记各的原因(不能混成一句话)。
- const modelCases = [
- ['missing', ASSET_REASON.MODEL_MISSING],
- ['invalid', ASSET_REASON.MODEL_INVALID],
- ['failed', ASSET_REASON.MODEL_FAILED],
- ['over-budget', ASSET_REASON.MODEL_OVER_BUDGET],
- ]
- check('B4-model-degrades-are-distinct',
- modelCases.every(([state, reason]) => planDeviceAsset({ device: device(), modelState: state }).reason === reason),
- `真模型四种不可用各自可分:${modelCases.map(([s]) => s).join('/')}`)
- // **图标资源自身失败 → 通用位置符号**(最后兜底),并记「视觉资源异常」。
- // 用**注入的渲染实现**真实驱动这条分支(本仓反对"传一个实现不读的键"那种恒真断言)。
- const iconFailed = planDeviceAsset({
- // 用 `ALARM`(形状 triangle)而不是 `NORMAL`(形状本身就是 circle)——
- // 否则"第一次调用必须抛错"的判据会被"形状恰好是 circle"蒙混过去(本票实测栽过)。
- device: device({ status: 'ALARM' }),
- color: '#ff4d5e',
- renderIconImpl: (shape) => {
- if (shape !== 'circle') throw new Error('icon asset render failed')
- return 'data:image/svg+xml,fallback'
- },
- })
- check('B5-icon-failure-falls-back-to-generic-symbol',
- iconFailed.mode === ASSET_MODE.FALLBACK &&
- iconFailed.shape === 'circle' &&
- iconFailed.reason === ASSET_REASON.ICON_FAILED &&
- iconFailed.image === 'data:image/svg+xml,fallback',
- `图标渲染抛错 → 模式 ${iconFailed.mode}、形状 ${iconFailed.shape}、原因「${iconFailed.reasonText}」`)
- // 反过来:图标渲染正常时**不**走兜底(对照组,证明上面那条不是恒真)。
- const iconOk = planDeviceAsset({ device: device(), color: '#35e39a', renderIconImpl: () => 'data:image/svg+xml,ok' })
- check('B5b-icon-ok-does-not-fall-back',
- iconOk.mode === ASSET_MODE.ICON && iconOk.reason !== ASSET_REASON.ICON_FAILED,
- `图标渲染正常 → 模式 ${iconOk.mode}(对照:这才说明兜底只在失败时出现)`)
- // 兜底符号仍是可画的(`circle`),且形状在形状表里存在。
- check('B6-fallback-shape-is-renderable', planDeviceAsset({ device: device() }).shape === 'circle',
- '正常设备的状态形状来自设备词表(NORMAL → circle)')
- // 真模型不可用时,资产模式**不得**声称 model(本票不加载真模型)。
- // 正面判据:模式取值集合里根本没有 `model`,且四种真模型不可用都落到 icon。
- const modeValues = Object.values(ASSET_MODE)
- check('B7-no-model-mode-exists',
- !modeValues.includes('model') && modeValues.length === 3 &&
- modelCases.every(([state]) => planDeviceAsset({ device: device(), modelState: state }).mode === ASSET_MODE.ICON),
- `模式集合 ${modeValues.join('/')} 里没有 model(本期不加载真模型)`)
- }
- // ================================================================ C 尺寸层级(点始终大于设备)
- {
- check('C1-device-smaller-than-point', MARKER_SIZE.device < MARKER_SIZE.point,
- `设备 ${MARKER_SIZE.device} < 监测点 ${MARKER_SIZE.point}`)
- // **任何允许的缩放**下层级都不反转(规格验收第 2 条)。
- const scales = [MARKER_SIZE.minScale, 1, 1.3, 2, MARKER_SIZE.maxScale, 99, 0.01]
- check('C2-hierarchy-holds-at-any-scale', scales.every((s) => sizeHierarchyHolds(s)),
- `缩放 ${scales.join('/')} 下层级都成立(夹取到 [${MARKER_SIZE.minScale}, ${MARKER_SIZE.maxScale}])`)
- // 越界缩放被夹取(不放大到离谱/缩到看不见)。
- check('C3-scale-is-clamped',
- markerSizeFor('monitoring-device', 999) === Math.round(MARKER_SIZE.device * MARKER_SIZE.maxScale * 100) / 100 &&
- markerSizeFor('monitoring-device', 0) === Math.round(MARKER_SIZE.device * MARKER_SIZE.minScale * 100) / 100,
- `越界缩放夹到区间内(999 → ×${MARKER_SIZE.maxScale}、0 → ×${MARKER_SIZE.minScale})`)
- // 非法缩放退回 1。
- check('C4-illegal-scale-falls-back', markerSizeFor('monitoring-device', NaN) === MARKER_SIZE.device,
- 'NaN 缩放退回基准尺寸')
- }
- // ================================================================ D 标签(默认不显示、不出现经纬度)
- {
- const hidden = deviceLabel(device(), { visible: false })
- check('D1-hidden-by-default', hidden.visible === false && hidden.name === '' && hidden.code === '',
- '默认不产出长文本(由界面按 visible 决定画不画)')
- const shown = deviceLabel(device(), { visible: true })
- check('D2-name-first-code-second',
- shown.name === '供水压力计-001' && shown.code === 'EQ-1',
- `hover/选中:名称为主「${shown.name}」、编码为辅「${shown.code}」`)
- // 没有名字时用编码顶上(不留空标签)。
- const noName = deviceLabel(device({ name: '' }), { visible: true })
- check('D3-code-fills-when-no-name', noName.name === 'EQ-1' && noName.code === '',
- '没有名称时用编码作主文案(不出现空标签)')
- // 标签内容**不含经纬度数字**(规格:「坐标图标」只代表空间位置)。
- // 判据覆盖**返回对象的全部字段**,不只 name/code —— 变异 M5 实测:
- // 只查那两个字段时,新增一个 `coord` 字段把经纬度带出去仍然全绿。
- const withCoord = deviceLabel(device({ longitude: 110.3991, latitude: 28.4351 }), { visible: true })
- const serialized = JSON.stringify(withCoord)
- check('D4-label-has-no-coordinates',
- !/110\.|28\.|经度|纬度/.test(serialized),
- `标签对象 ${serialized} 里不含经纬度数字(坐标不当作标签内容)`)
- }
- // ================================================================ E 地形降级 vs 坐标无效(两件事)
- {
- const ok = planDevicePlacement({ coordinateRenderable: true, terrainReady: true })
- check('E1-renderable-only-when-both-ok', ok.placement === 'renderable', `坐标有效 + 地形就绪 → ${ok.placement}`)
- const noCoord = planDevicePlacement({ coordinateRenderable: false, terrainReady: true })
- check('E2-invalid-coordinate-is-no-location', noCoord.placement === 'no-location',
- `后端判不可上图 → ${noCoord.placement}`)
- // **本票的关键区分**:坐标有效但地形未就绪,必须是"位置待确认"而**不是**"坐标无效"。
- const terrain = planDevicePlacement({ coordinateRenderable: true, terrainReady: false })
- check('E3-terrain-pending-is-not-no-location',
- terrain.placement === 'terrain-pending' && terrain.reason === ASSET_REASON.TERRAIN_PENDING &&
- terrain.placement !== 'no-location',
- `坐标有效 + 地形未就绪 → ${terrain.placement}(**不是** no-location:那是"等地图资源"不是"数据有问题")`)
- check('E4-terrain-reason-text-is-human',
- assetReasonText(terrain.reason) === '地形未就绪,位置待确认',
- `原因文案「${assetReasonText(terrain.reason)}」`)
- }
- // ================================================================ F 静态守卫(接缝单一、不散落)
- {
- const root = new URL('../../../src/modules/cockpit/', import.meta.url)
- const read = (rel) => readFileSync(new URL(rel, root), 'utf8')
- const engine = read('services/cesiumEngine.js')
- const mapService = read('services/mapService.js')
- const assets = read('domain/deviceAssets.js')
- const deviceStatus = read('domain/deviceStatus.js')
- const detailFlow = read('domain/detailFlow.js')
- const card = read('components/PointDetailCard.vue')
- // ① 资产接缝**只调用一次** `markerSvg`(形状/资源地址不散落)。
- check('F1-single-asset-seam',
- (assets.match(/markerSvg\(/g) || []).length === 1,
- '形状渲染只经 `deviceAssets.js` 一处调用 markerSvg(单一接缝)')
- // ② 引擎不认识形状字符串(它只画调用方给的形状)——设备层不得内联 `'circle'` 之类的**业务默认**。
- const deviceSyncBody = (/export function syncAssociatedDevices[\s\S]*?\n\}/.exec(engine) || [''])[0]
- check('F2-engine-takes-shape-from-caller',
- /imageOf\(device\.shape/.test(deviceSyncBody) && !/shape === 'triangle'|SHAPES\[/.test(deviceSyncBody),
- '引擎只画调用方给的 shape,不含业务形状判断')
- // ③ **不开启穿透例外**(规格 16 明写遵守正常深度关系)。
- // 判据只看**赋值语句**:注释里会出现这个字样(解释"故意不设"),被注释满足就是守卫比它要守的宽。
- check('F3-no-depth-test-exception',
- !/disableDepthTestDistance:/.test(deviceSyncBody),
- '设备位置符号遵守正常遮挡(设备图层里没有 disableDepthTestDistance 赋值)')
- // ④ 尺寸来自接缝,不写死字面量。
- check('F4-sizes-come-from-seam',
- /planDeviceAsset\(/.test(mapService) && /MARKER_SIZE\.point/.test(mapService) && !/markerSvg\(shape, color, 18\)/.test(mapService),
- '监测点与设备的尺寸都取自 `deviceAssets` 的 MARKER_SIZE(不写死 18/14)')
- // ⑤ 设备状态表**不 import** 点位表的状态词表(只可共用 SHAPES 片段)。
- check('F5-device-status-independent',
- !/POINT_STATUS_VISUALS|visualForStatus/.test(deviceStatus) && /SHAPES/.test(deviceStatus),
- '设备词表只共用 SHAPES 片段,不借用点位状态词表')
- // ⑥ 设备卡片视图走设备表(越界已修)。
- // 守卫必须**只截 `deviceCardView` 的函数体**:`pointSummary`(点位摘要)用 `statusView`
- // 是**正确**的,对全文匹配会被它满足而看不出越界(变异/实测都确认过)。
- const cardViewBody = (/export function deviceCardView[\s\S]*?\n\}/.exec(detailFlow) || [''])[0]
- check('F6-device-card-uses-device-table',
- /status: deviceStatusView\(/.test(cardViewBody) && !/status: statusView\(/.test(cardViewBody),
- '`deviceCardView` 的函数体里用 `deviceStatusView`、不用点位表的 `statusView`')
- // ⑦ 详情卡片显示资产模式与原因(验收第 5 条)。
- // 守卫要**连绑定一起**判:只查 `row.assetReasonText` 出现在文件里,会被 `v-if="false"`
- // 之类的写法满足(变异 M16 实测全绿)。
- check('F7-detail-shows-asset-reason',
- /v-if="row\.assetReasonText"/.test(card) &&
- /:data-device-asset-mode="row\.assetMode"/.test(card) &&
- /:data-device-asset-reason="row\.assetReason"/.test(card) &&
- /\{\{ row\.assetReasonText \}\}/.test(card),
- '详情卡片**绑定**了资产模式、原因码与原因文案(不是仅出现字样)')
- // ⑧ 设备状态词表里**没有** EMPTY(那是点位专有)。
- check('F8-no-empty-in-device-status',
- !/EMPTY:/.test(deviceStatus) && !/EMPTY:/.test(JSON.stringify(DEVICE_STATUS)),
- '设备词表不含 EMPTY(点位聚合态专有)')
- // ⑨ 点位词表未被本票改动语义(仍是六态 + UNKNOWN + UNASSOCIATED)。
- check('F9-point-table-untouched',
- Object.keys(POINT_STATUS_VISUALS).length === 6 && POINT_STATUS.EMPTY === 'EMPTY',
- '点位词表仍是六态(本票不改它的语义)')
- // ⑩ 设备长文本有**稳定截断边界**(验收第 3 条:不撑破布局)。
- // 只判"名称"这一处:它是唯一会变长的自由文本。`min-width: 0` 是与
- // `grid-template-columns: minmax(0, 1fr)` 配对生效的那一半,缺了它
- // `text-overflow` 不会触发(长名字直接把列撑宽)——两者必须一起判。
- const nameRule = (/\.point-detail__device-name\s*\{[\s\S]*?\}/.exec(card) || [''])[0]
- check('F10-long-text-has-truncation-boundary',
- /min-width:\s*0/.test(nameRule) && /overflow:\s*hidden/.test(nameRule) &&
- /white-space:\s*nowrap/.test(nameRule) && /text-overflow:\s*ellipsis/.test(nameRule),
- '设备名称列有 min-width:0 + overflow:hidden + nowrap + ellipsis(长名字截断而非撑破布局)')
- // ⑪ 设备词条**只含静态描述**(本票边界:动效归票 47)。
- // 判据是"键的集合"而非"是否出现某个词":只查动画字样的话,
- // 将来给词条加个 `effect: 'pulse'` 仍然全绿。
- const staticKeys = ['key', 'tone', 'shape', 'label', 'priority']
- const visualEntries = [...Object.values(DEVICE_STATUS_VISUALS), DEVICE_UNKNOWN_VISUAL]
- const offenders = visualEntries
- .flatMap((v) => Object.keys(v).filter((k) => !staticKeys.includes(k)))
- check('F11-status-visuals-are-static-only',
- offenders.length === 0,
- `六个词条只含静态描述 ${staticKeys.join('/')}(动效留给票 47)${offenders.length ? `;越界键:${offenders.join('/')}` : ''}`)
- // ⑫ 标签口径**确有生产调用点**(验收第 3 条:名称为主、编码为辅要真的用上)。
- // 守卫只查"文件里出现过 deviceLabel"会被 import 语句满足(本仓栽过多次),
- // 所以判据是**模板里真的用它的结论渲染**:既有 `row.label.name`,也有 `row.label.code`。
- check('F12-label-seam-has-production-caller',
- /import \{[^}]*deviceLabel[^}]*\} from '\.\.\/domain\/deviceAssets\.js'/.test(card) &&
- /row\.nameLabel\.name/.test(card) &&
- /row\.nameLabel\.code/.test(card),
- '详情清单用 `deviceLabel(row, { visible: true })` 的结论渲染(名称为主、编码为辅)')
- // ⑬ 接缝结论**写进了实体**(不然"地图上那台设备的形状/资产模式"根本无从断言)。
- // 只截 `syncAssociatedDevices` 的函数体:属性名在别处出现不算数。
- const engineDeviceBody = (/export function syncAssociatedDevices[\s\S]*?\n\}/.exec(engine) || [''])[0]
- check('F13-entity-carries-seam-conclusion',
- /deviceShape:\s*device\.shape/.test(engineDeviceBody) &&
- /assetMode:\s*device\.assetMode/.test(engineDeviceBody) &&
- /assetReason:\s*device\.assetReason/.test(engineDeviceBody) &&
- /width:\s*size/.test(engineDeviceBody),
- '设备实体带 `deviceShape`/`assetMode`/`assetReason`,billboard 宽高取自接缝的 size')
- // ⑭ 读取器**暴露**上面那些字段(否则走查只能读计划行 —— 那是"打算画什么",
- // 不是"画成了什么";本票实测正是栽在这里)。
- const readerBody = (/export function readAssociatedDevices[\s\S]*?\n\}/.exec(engine) || [''])[0]
- check('F14-reader-exposes-seam-conclusion',
- /shape:\s*get\('deviceShape'\)/.test(readerBody) &&
- /assetMode:\s*get\('assetMode'\)/.test(readerBody) &&
- /assetReason:\s*get\('assetReason'\)/.test(readerBody) &&
- /billboard[\s\S]{0,80}width[\s\S]{0,40}getValue\(\)/.test(readerBody),
- '`readAssociatedDevices` 回形状/资产模式/降级原因,尺寸读 billboard 真实宽高')
- }
- // ================================================================ G store 级端到端(替代浏览器走查的可观察证据)
- //
- // 本沙箱下 Chrome 无法启动(mojo IPC 被文件沙箱拒绝,实测 `--single-process` 也不行),
- // 所以"外壳走查"这一层改用**既有成例**(t15/t20 都在 Node 里用 pinia 驱动 store):
- // 直接走 `deviceLayerStore.syncForSelection()` 这条**真实用户路径**,断言资产接缝真的作用到产出的行上。
- await (async () => {
- const { createPinia, setActivePinia } = await import('pinia')
- setActivePinia(createPinia())
- const { useDeviceLayerStore } = await import('../../../src/modules/cockpit/stores/deviceLayerStore.js')
- const { useDetailStore } = await import('../../../src/modules/cockpit/stores/detailStore.js')
- const { useMapStore } = await import('../../../src/modules/cockpit/stores/mapStore.js')
- const mapStore = useMapStore()
- const detailStore = useDetailStore()
- const layerStore = useDeviceLayerStore()
- // 造一个"当前快照里有这个点位"的合并态(走 store 的公开字段,不碰私有)。
- mapStore.summary = {
- ...mapStore.summary,
- objects: [
- { kind: 'monitoring-point', code: 'MP-1', name: '测试点', deviceCodes: ['EQ-A', 'EQ-B'] },
- ],
- }
- // 详情给两台设备:一台坐标有效(可上图)、一台后端判不可上图。
- detailStore.setSection('pointDetail', {
- state: 'success',
- data: {
- code: 'MP-1',
- devices: [
- { code: 'EQ-A', name: '可上图设备', status: 'ALARM', statusLabel: '', statusReasons: [], longitude: 110.4, latitude: 28.44, coordinate: { state: 'VALID', reason: '', renderable: true, validated: true } },
- { code: 'EQ-B', name: '无位置设备', status: 'NORMAL', statusLabel: '', statusReasons: [], longitude: null, latitude: null, coordinate: { state: 'UNVERIFIED', reason: 'UNVERIFIED_COORDINATE', renderable: false, validated: true } },
- ],
- },
- })
- detailStore.selection = { level: 'point', pointCode: 'MP-1', deviceCode: '' }
- layerStore.syncForSelection({ force: true, mode: 'overview' })
- const rows = layerStore.rows
- const drawn = rows.find((r) => r.code === 'EQ-A')
- const blocked = rows.find((r) => r.code === 'EQ-B')
- check('G1-store-produces-both-rows', rows.length === 2,
- `store 产出 ${rows.length} 行(快照去重 deviceCodes 两台都在清单里)`)
- check('G2-drawn-row-carries-asset-plan',
- !!drawn && drawn.shape === 'triangle' && drawn.assetMode === ASSET_MODE.ICON &&
- drawn.assetReason === ASSET_REASON.ASSET_MISSING && !!drawn.assetReasonText,
- `可上图设备的行带资产计划:形状 ${drawn && drawn.shape}、模式 ${drawn && drawn.assetMode}、原因「${drawn && drawn.assetReasonText}」`)
- check('G3-blocked-row-not-drawn',
- !!blocked && blocked.placement === 'no-location',
- `后端判不可上图 → ${blocked && blocked.placement}(保留在清单里)`)
- // 尺寸层级落到行上(可断言"点比设备大"这条关系在真实数据路径上成立)。
- check('G4-size-hierarchy-on-real-rows',
- drawn.size === MARKER_SIZE.device && sizeHierarchyHolds(1),
- `设备行尺寸 ${drawn.size} < 监测点 ${MARKER_SIZE.point}(层级成立)`)
- // 设备状态词表独立:ALARM 的色键来自设备表。
- check('G5-device-tone-from-device-table',
- drawn.visualKey === 'ALARM',
- `设备行视觉键 ${drawn.visualKey}(来自设备词表,非点位表)`)
- layerStore.reset()
- })()
- const failed = checks.filter((item) => !item.pass)
- console.log('')
- for (const item of checks) {
- if (!item.pass) console.log(` ✗ ${item.name}${item.detail ? ` — ${item.detail}` : ''}`)
- }
- console.log(`T44 设备位置符号与资产接缝检查:${checks.length - failed.length}/${checks.length} 项通过。`)
- if (failed.length) process.exit(1)
- console.log('T44 设备位置符号与资产接缝全部通过。')
|