/** * T38 基础设施面板调整 纯逻辑闸门(票 38,前端执行票)。 * * 零依赖 node 断言脚本(仓库没配测试运行器,约定见 t01-panel-catalog.mjs)。 * 运行:node .scratch/cockpit-real-data-wayfinding/verify/t38-infrastructure-panel-adjustments.mjs * * 六段,对号票面四条范围 + 验收三句: * A 规模读数已删除(archivePanel / infrastructureSummaryPanel / 领域层不再透传 scale), * 而 `lengthKm` 与既有读数不受影响(验收第 1 条前半)。 * B 定位三种「点不动」各有自己的文案、分得开(「没反应」本身是缺陷)。 * C 管理信息按片区/行业两维度生成正确图表块 + 下方保留紧凑清单(验收第 1 条中段)。 * D 真实后端 `scale` 恒 null 的形状灌进来,不再画空读数;`"km"` 不当规模展示。 * E 静态守卫:反馈可见性搬出折叠面板;相机走完整视角路径;同行呈现无 key 特判。 * F store 行为:成功走 flyPoint、失败不动相机、三种 result 各自落到 locate、异步 false 被接住。 * * 变异归 t38-mutations.mjs(本脚本自己只跑绿)。 */ import { readFileSync } from 'node:fs' import { createPinia, setActivePinia } from 'pinia' import { archivePanel, managementPanel, toBaseArchives, toManagementInformation, } from '../../../src/modules/cockpit/domain/infrastructurePanels.js' import { infrastructureSummaryPanel, toOverviewRegions } from '../../../src/modules/cockpit/domain/overviewPanels.js' import { LOCATE_RESULT, LOCATE_TEXT, planLocate } from '../../../src/modules/cockpit/domain/mapLocate.js' import { DETAIL_PLACEHOLDER } from '../../../src/modules/cockpit/domain/detailFlow.js' import { SECTION_STATE, section } from '../../../src/modules/cockpit/domain/sectionState.js' import { useInfrastructureStore } from '../../../src/modules/cockpit/stores/infrastructureStore.js' const moduleRoot = new URL('../../../src/modules/cockpit/', import.meta.url) const read = (relative) => readFileSync(new URL(relative, moduleRoot), 'utf8') /** 去注释(注释会解释被禁的东西,全文正则会拿假阳性换通过率)。 */ const code = (text) => text.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:])\/\/.*$/gm, '$1') const codeVue = (text) => code(text.replace(//g, '')) let passed = 0 let failed = 0 function check(name, condition, message) { if (condition) { passed += 1 console.log(`PASS ${name}`) } else { failed += 1 console.error(`FAIL ${name} — ${message}`) } } const sv = (data) => section(SECTION_STATE.SUCCESS, { data, loaded: true }) setActivePinia(createPinia()) // ================================================================ A 规模读数已删除 { const data = toBaseArchives({ updatedAt: 'T', range: { type: 'CURRENT_BASELINE' }, // 故意把 scale 给成有值(真实后端是 null):界面仍不得出现「规模」读数。 totals: { facilityCount: 5, lengthKm: '12.40', scale: 'medium' }, industryDistribution: [{ code: 'GAS', count: 2 }], districtDistribution: [{ district: 0, count: 3 }], ageDistribution: [{ code: 'UNDER_5_YEARS', count: 1 }], facilityTypeDistribution: [{ code: 'GAS_PIPE', name: '燃气管线', count: 2, lengthKm: 3.1, scale: 'km' }], ageReference: { asOfDate: 'D', commissionedCount: 1, builtCount: 1, unknownCount: 0 }, excluded: { unknownTypeCount: 0, typeKindMismatchCount: 0 }, }) check('A1-domain-drops-scale', data.scale === undefined, `领域对象不再带出 scale(实测:${String(data.scale)})`) check('A1b-domain-keeps-length', data.lengthKm === 12.4, `lengthKm 保留(BigDecimal 字符串被认下:${data.lengthKm})`) const view = archivePanel(data, sv(data)) const kpi = view.blocks[0] const labels = kpi.items.map((item) => item.label) const keys = kpi.items.map((item) => item.key) check( 'A2-archive-kpi-has-no-scale-reading', !keys.includes('scale') && !labels.includes('规模') && !JSON.stringify(view).includes('中型'), `基础档案 KPI 只剩 ${keys.join('/')}(规模读数与其中文翻译都不再上屏)`, ) check( 'A2b-archive-keeps-facility-count-and-length', keys.includes('facilityCount') && keys.includes('lengthKm') && kpi.items.find((i) => i.key === 'facilityCount').value === '5' && kpi.items.find((i) => i.key === 'lengthKm').value === '12.4', '设施总数与管线里程读数不变(票面「不改变其它读数」)', ) check( 'A3-two-readings-share-one-kpi-block', kpi.items.length === 2 && view.blocks.filter((b) => b.kind === 'kpi').length === 1, `两项读数在同一条 KPI 网格(auto-fit → 同行呈现),没有第三块抢行`, ) check('A3b-no-per-key-wide-class', !/item\.key === 'lengthKm'/.test(codeVue(read('components/PanelBlocks.vue'))), '渲染层不按 key 特判整行独占(第二轮:`wide` 改由 domain 声明)') // 票 38 第二轮(反馈第 1 条):概览的管线里程独占整行(数值最长,并排挤压), // 但「谁独占」的决定权在 domain —— 渲染层只认通用 `item.wide`。 const ovWide = infrastructureSummaryPanel( { infrastructure: { facilityCount: 5, smartDeviceCount: 9, industries: [], excluded: {} }, regions: { regions: [], lengthKm: 12.4 } }, sv(null), ) check( 'A3c-overview-length-is-wide', ovWide.blocks[0].items.find((i) => i.key === 'lengthKm').wide === true && ovWide.blocks[0].items.filter((i) => i.wide === true).length === 1, '概览基础设施:只有管线里程声明 wide(独占整行排版更好看)', ) check( 'A3d-archive-row-not-wide', kpi.items.every((item) => item.wide === undefined), '基础档案两项都不声明 wide → 仍同行(第一轮的第 4 条不被撤销)', ) // 总体概览「基础设施」面板同样删规模。 const overviewRegions = toOverviewRegions({ totals: { facilityCount: 9, lengthKm: 12.4, scale: 'medium' }, districtDistribution: [{ district: 0, count: 3 }], ageReference: {}, excluded: {}, }) check('A4-overview-regions-drop-scale', overviewRegions.scale === undefined && overviewRegions.lengthKm === 12.4, 'toOverviewRegions 不再带 scale,lengthKm 保留') const ov = infrastructureSummaryPanel( { infrastructure: { facilityCount: 5, smartDeviceCount: 9, industries: [], excluded: {} }, regions: overviewRegions }, sv(null), ) const ovKeys = ov.blocks[0].items.map((item) => item.key) check( 'A5-overview-kpi-has-no-scale-reading', !ovKeys.includes('scale') && ovKeys.includes('lengthKm'), `概览基础设施 KPI:${ovKeys.join('/')}(规模已删、里程保留)`, ) check('A6-both-panels-clean', !JSON.stringify(ov).includes('规模') && !JSON.stringify(view.blocks[0]).includes('规模'), '两块面板的 KPI 里零「规模」字样') } // ================================================================ B 三种「点不动」各有文案 { const objects = [ { kind: 'monitoring-point', code: 'MP-001', name: '甲', longitude: 110.1, latitude: 34.2, coordinate: { renderable: true, validated: true } }, { kind: 'monitoring-point', code: 'MP-003', name: '丙', longitude: 110.3, latitude: 34.4, coordinate: { renderable: false, validated: true, reason: 'ZERO_COORDINATE' } }, ] const missing = planLocate({ objects, pointCode: 'NOPE', mapReady: true }) const blocked = planLocate({ objects, pointCode: 'MP-003', mapReady: true }) const unavailable = planLocate({ objects, pointCode: 'MP-001', mapReady: false }) const ok = planLocate({ objects, pointCode: 'MP-001', mapReady: true }) check('B1-three-failures-distinct', missing.result === LOCATE_RESULT.MISSING && blocked.result === LOCATE_RESULT.BLOCKED && unavailable.result === LOCATE_RESULT.UNAVAILABLE && ok.result === LOCATE_RESULT.OK, 'missing/blocked/unavailable/ok 四态分得开') check( 'B2-each-failure-has-visible-text', [missing, blocked, unavailable].every((plan) => typeof plan.reasonText === 'string' && plan.reasonText.trim().length > 0), `三种失败都有可读文案:${missing.reasonText} / ${blocked.reasonText} / ${unavailable.reasonText}`, ) check('B2b-texts-differ', new Set([missing.reasonText, blocked.reasonText, unavailable.reasonText]).size === 3, '三句文案互不相同(读者能分清是自己点错还是数据问题)') check('B3-blocked-carries-backend-reason', blocked.reasonText === '零坐标(经纬度均为 0)' && !/ZERO_COORDINATE/.test(blocked.reasonText), `不可上图说具体原因且不泄原因码:${blocked.reasonText}`) check('B4-locate-text-constants', LOCATE_TEXT.MISSING.includes('已加载') && LOCATE_TEXT.UNAVAILABLE.includes('就绪'), '两种自带文案的语义锚点(范围 / 就绪)在 domain 一处定义') // 票 38 第二轮(反馈第 2 条):**关联设备**(真实数据 `21119740`)不以自身编码上图, // 它是宿主监测点 `deviceCodes` 的成员。点它的定位必须回联宿主点位并成功, // 而不是误报「该点位不在已加载范围」——维护者:「实际上该点位是已加载在图中的真实设备」。 const deviceObjects = [ { kind: 'monitoring-point', code: 'CBWS202610150001', name: '城北排水监测点01', longitude: 110.399905, latitude: 28.459147, deviceCodes: ['21119740', '21119741'], coordinate: { renderable: true, validated: true }, }, ] const viaDevice = planLocate({ objects: deviceObjects, pointCode: '21119740', mapReady: true }) check( 'B5-associated-device-resolves-to-host-point', viaDevice.result === LOCATE_RESULT.OK && viaDevice.object.code === 'CBWS202610150001' && viaDevice.deviceCode === '21119740', `点关联设备回联宿主监测点并定位成功(${viaDevice.object && viaDevice.object.code},设备码带出 ${viaDevice.deviceCode})`, ) check('B5b-point-code-leaves-device-code-empty', planLocate({ objects: deviceObjects, pointCode: 'CBWS202610150001', mapReady: true }).deviceCode === '', '直接点位编码时 deviceCode 为空(不误开设备级)') const stillMissing = planLocate({ objects: deviceObjects, pointCode: 'TCRQ202609080001', mapReady: true }) check('B5c-unknown-code-still-missing', stillMissing.result === LOCATE_RESULT.MISSING && stillMissing.object === null, '既非对象编码也非任何点位的设备编码 → 仍如实 MISSING(回联不吞真缺失)') const blockedDevice = planLocate({ objects: [{ ...deviceObjects[0], coordinate: { renderable: false, validated: true, reason: 'ZERO_COORDINATE' } }], pointCode: '21119740', mapReady: true }) check('B5d-host-blocked-reports-blocked', blockedDevice.result === LOCATE_RESULT.BLOCKED, '宿主点位不可上图 → BLOCKED(准入判据照宿主点位自己,不因为换了入口就放行)') check('B5e-device-code-not-matched-as-object', planLocate({ objects: [{ kind: 'monitoring-point', code: '21119740', coordinate: { renderable: true } }], pointCode: '21119740', mapReady: true }).object.code === '21119740', '编码若真是对象自身编码,直接命中(回联只在找不到时兜底,不改既有语义)') } // ================================================================ C 管理信息:片区 × 行业图表化 { const data = toManagementInformation({ totals: { facilityCount: 12, equipmentCount: 8 }, districtIndustry: [ { district: 0, districtName: '城南', industry: 'GAS', facilityCount: 5, equipmentCount: 2 }, { district: 0, districtName: '城南', industry: 'WATER_SUPPLY', facilityCount: 4, equipmentCount: 3 }, { district: 1, districtName: '城北', industry: 'GAS', facilityCount: 2, equipmentCount: 1 }, { district: null, districtName: '未分区', industry: 'DRAINAGE', facilityCount: 1, equipmentCount: 2 }, ], // 已退役字段:给了也不许被消费。 organizations: [{ organizationName: 'FORBIDDEN-ORG' }], ownershipDistribution: [{ ownershipType: 'OWNED' }], unmaintained: [{ code: 'FORBIDDEN-UNMAINTAINED' }], }) const view = managementPanel(data, sv(data)) const dim = view.blocks.find((b) => b.kind === 'dimbars') check('C1-dimbars-block-exists', !!dim, `管理信息有可切换维度的柱状图块(块类型:${view.blocks.map((b) => b.kind).join('/')})`) check( 'C2-two-dimensions', dim && dim.dimensions.length === 2 && dim.dimensions.map((d) => d.key).join(',') === 'district,industry', `两维度都在:${dim ? dim.dimensions.map((d) => `${d.label}(${d.key})`).join(' / ') : '—'}`, ) const districtDim = dim.dimensions.find((d) => d.key === 'district') const industryDim = dim.dimensions.find((d) => d.key === 'industry') check( 'C3-district-rows-aggregate-and-rank', districtDim.rows.map((r) => r.count).join(',') === '9,2,1' && districtDim.rows.map((r) => r.label).join('、') === '城南、城北、未分区', `按片区聚合城南=5+4=9 并降序:${districtDim.rows.map((r) => `${r.label}${r.count}`).join(' ')}`, ) check( 'C4-industry-rows-aggregate', industryDim.rows.map((r) => r.count).join(',') === '7,4,1' && industryDim.rows.map((r) => r.label).join('、') === '燃气、供水、排水', `按行业聚合燃气=5+2=7:${industryDim.rows.map((r) => `${r.label}${r.count}`).join(' ')}`, ) check('C5-ratio-normalized-per-dimension', districtDim.rows[0].ratio === 1 && industryDim.rows[0].ratio === 1, '条长按该维度最大项归一(各维度独立)') check('C5b-equipment-count-in-hint', districtDim.rows[0].hint.includes('设备'), `设备计数进悬停说明不占柱:${districtDim.rows[0].hint}`) check('C6-default-active-district', dim.active === 'district', `默认维度=片区(图面先回答「分布在哪」):${dim.active}`) const list = view.blocks.find((b) => b.kind === 'list') check('C7-compact-list-kept', !!list && list.rows.length === 4 && list.rows[0].meta.join(' ').includes('设备'), '下方保留紧凑卡片清单(逐 片区×行业 组合)') check('C7b-list-label', list.label.includes('片区') && list.label.includes('行业'), `清单标题仍是片区×行业口径:${list.label}`) const dumped = JSON.stringify(view) check('C8-retired-fields-not-consumed', !/FORBIDDEN-ORG|FORBIDDEN-UNMAINTAINED|OWNED|管理单位|权属/.test(dumped), '退役的机构/权属/未维护维度零消费') check('C8b-no-responsibility-area-concept', !/责任区域/.test(dumped), '「责任区域」不作为独立概念出现(决策 06:它就是片区别名)') // 空范围:不假装有数据。 const empty = managementPanel(toManagementInformation({ totals: { facilityCount: 0, equipmentCount: 0 }, districtIndustry: [] }), sv(null)) const emptyDim = empty.blocks.find((b) => b.kind === 'dimbars') check('C9-empty-says-empty', emptyDim.dimensions.every((d) => d.rows.length === 0), '空计数 → 两维度都没有行(界面走 emptyText,不画假柱)') // 行业未知 / 全 0 计数保留行(0 是真实值)。 const zero = managementPanel( { facilityCount: 0, equipmentCount: 1, districtIndustry: [{ district: 1, industry: 'GAS', facilityCount: 0, equipmentCount: 1 }] }, sv(null), ) const zeroDim = zero.blocks.find((b) => b.kind === 'dimbars') const gasRow = zeroDim.dimensions.find((d) => d.key === 'district').rows[0] check('C10-zero-count-row-kept', gasRow.count === 0 && gasRow.ratio === 0 && Number.isFinite(gasRow.ratio), '0 计数保留行且条长 0(不 NaN、不隐藏)') const unk = managementPanel(toManagementInformation({ totals: { facilityCount: 0, equipmentCount: 0 }, districtIndustry: [{ district: 9, industry: '', facilityCount: 0, equipmentCount: 0 }] }), sv(null)) const unkInd = unk.blocks.find((b) => b.kind === 'dimbars').dimensions.find((d) => d.key === 'industry').rows[0] check('C10b-unknown-bucket-labeled', unkInd.key === 'UNASSIGNED' && unkInd.label === '行业未知', `认不出的行业进显式「行业未知」桶:${unkInd.label}`) const unkDist = unk.blocks.find((b) => b.kind === 'dimbars').dimensions.find((d) => d.key === 'district').rows[0] check('C10c-invalid-district-unknown', unkDist.key === 'UNASSIGNED' && unkDist.label === '未分区', `认不出的片区经适配层归一后进「未分区」桶:${unkDist.label}`) } // ================================================================ D 真实后端形状(scale 恒 null) { // 照抄 t18 真实审计载荷:totals.scale = null、facilityTypes[].scale = "km"。 const real = toBaseArchives({ totals: { facilityCount: 5283, lengthKm: 67.60944, scale: null }, industryDistribution: [{ code: 'GAS', count: 537 }], districtDistribution: [{ district: 0, count: 500 }], facilityTypeDistribution: [{ code: 'GAS_PIPE', name: '燃气管线', count: 537, lengthKm: 3.118, scale: 'km' }], ageReference: {}, excluded: {}, }) const view = archivePanel(real, sv(real)) check('D1-null-scale-not-rendered', !JSON.stringify(view.blocks[0]).includes('规模'), `真实 null 规模不再画成一行空读数(KPI:${view.blocks[0].items.map((i) => i.key).join('/')})`) check('D2-km-unit-not-shown-as-scale', JSON.stringify(view).indexOf('规模') === -1, `facilityTypes[].scale 的 "km" 不被当规模展示(长度归 lengthKm)`) check('D3-facility-count-unchanged', view.blocks[0].items.find((i) => i.key === 'facilityCount').value === '5283', '设施总数读数不受影响') check('D3b-length-unchanged', view.blocks[0].items.find((i) => i.key === 'lengthKm').value === '67.60944', '管线里程读数不受影响') } // ================================================================ E 静态守卫 { const panelVue = codeVue(read('components/PanelBlocks.vue')) const shellVue = codeVue(read('CockpitShell.vue')) const storeCode = code(read('stores/infrastructureStore.js')) const domainCode = code(read('domain/infrastructurePanels.js')) check('E1-dimbars-rendered', /block\.kind === 'dimbars'/.test(panelVue) && /data-dimbar=/.test(panelVue) && /data-dim-tab=/.test(panelVue), 'dimbars 块有画法与走查断言面') check('E2-switch-is-display-only', /setDimension\(/.test(panelVue) && !/emit\('panel-action'[\s\S]{0,120}dimbar/.test(panelVue), '维度切换是组件本地展示状态,不冒充 panel-action 筛选接缝') check('E2b-switch-uses-semantic-button', //g) || []).length >= 2, 'dimbars 行标签复用票 37 共用省略方法(bars 与 dimbars 两处同写法)') check('E3b-single-ellipsis-declaration', !/text-overflow/.test(panelVue), 'PanelBlocks.vue 仍零 text-overflow 声明(省略只有一份)') check('E4-domain-does-not-fetch-or-import-service', !/\bfetch\s*\(|XMLHttpRequest/.test(domainCode) && !/from '\.\.\/services\//.test(domainCode), 'domain 纯函数层不发请求、不 import service') check('E5-locate-seam-still-shared', /planLocate/.test(code(read('domain/mapLocate.js'))) && /isRenderable/.test(code(read('domain/mapLocate.js'))), '定位判定仍收在 domain 一处(复用票 07 坐标准入)') // 反馈可见性:不得再藏在默认收起的状态详情里。 const noticeAt = shellVue.indexOf('data-locate-notice') check('E6-visible-notice-exists', noticeAt >= 0, '定位反馈有独立可见浮层(data-locate-notice)') const foldRegion = shellVue.slice(shellVue.indexOf('v-show="statusPanelExpanded"'), noticeAt) check('E6b-feedback-not-behind-collapse', noticeAt >= 0 && foldRegion.indexOf('data-locate-result') < 0, '失败反馈不再挂在折叠的状态详情区(折叠=「没反应」的来源)') const noticeTag = shellVue.slice(shellVue.lastIndexOf(' \{[\s\S]{0,120}?clearLocate\(\)/.test(shellVue) && /dismissLocateFeedback/.test(shellVue) && /clearTimeout\(locateDismissTimer\)/.test(shellVue), '浮层自动收起 + 可手动关 + 卸载拆定时器(票 15 的漏取消坑)') check('E9-locate-flies-full-view', /locateMonitoringPoint/.test(storeCode) && /flyPoint\(plan\.object\)/.test(storeCode) && /await flyPoint/.test(storeCode), '监测点走完整视角定位(不是只换中心的 setCameraFocus)') check('E10-store-clear-exports', /clearLocate/.test(storeCode), 'store 暴露 clearLocate(反馈唯一持有者仍是 store,壳不自己抹状态)') check('E11-shell-forwards-locate', (shellVue.match(/@locate="locateOnMap"/g) || []).length === 2, '左右两栏的 @locate 仍接到外壳(data-locate-point → @locate → store)') check('E12-no-backend-names-in-ui', !/districtIndustry|ownershipDistribution|organizations/.test(panelVue), '渲染层不认识后端字段名(维度语义归 domain)') // 票 38 第二轮(反馈第 1 条):概览的管线里程与基础档案对不上 —— 根因是 overviewStore // 明明取了 base-archives(里程唯一来源)却在合并时硬写 `regions: null`。 // 静态守卫钉住「真的并进数据」;行为守卫见 t20 的 D3(loadPanels 端到端)。 const overviewStoreCode = code(read('stores/overviewStore.js')) check('E13-regions-merged-not-nulled', /regions: regionsLoaded \? regions\.data : null/.test(overviewStoreCode) && !/regions: null,\s*\n\s*regionsState: SECTION_STATE\.FAILED/.test(overviewStoreCode), '区域分布真的并进基础设施分区(里程两处同源,不再硬写 null)') } // ================================================================ F store 行为 { const store = useInfrastructureStore() store.reset() const objects = [ { kind: 'monitoring-point', code: 'MP-001', name: '甲', longitude: 110.1, latitude: 34.2, coordinate: { renderable: true, validated: true } }, { kind: 'monitoring-point', code: 'MP-003', name: '丙', longitude: 110.3, latitude: 34.4, coordinate: { renderable: false, validated: true, reason: 'ZERO_COORDINATE' } }, ] const flyCalls = [] const openCalls = [] const deps = { objects, mapReady: true, focus: () => { throw new Error('监测点定位不该再走中心换焦') }, flyPoint: (object) => { flyCalls.push(object.code) return true }, openPoint: (codeArg) => { openCalls.push(codeArg) }, } const ok = await store.locatePoint('MP-001', deps) const blocked = await store.locatePoint('MP-003', deps) const missing = await store.locatePoint('NOPE', deps) const notReady = await store.locatePoint('MP-001', { ...deps, mapReady: false }) check('F1-success-flies-once', ok.result === LOCATE_RESULT.OK && flyCalls.join(',') === 'MP-001' && openCalls.join(',') === 'MP-001', `成功:飞行 1 次 + 详情 1 次(${flyCalls.join(',')})`) check('F2-failures-dont-fly', blocked.result === LOCATE_RESULT.BLOCKED && missing.result === LOCATE_RESULT.MISSING && notReady.result === LOCATE_RESULT.UNAVAILABLE && flyCalls.length === 1, '三种失败都不动相机(不可上图/找不到/未就绪各归各)') check('F3-each-failure-has-text', [blocked, missing, notReady].every((r) => r.reasonText.trim().length > 0), '三种失败结果都带着要显示的文案(外壳据此出浮层,不存在「无反馈」状态)') check('F4-camera-refusal-degrades-honestly', (await store.locatePoint('MP-001', { ...deps, flyPoint: () => false })).result === LOCATE_RESULT.UNAVAILABLE, '相机没吃下指令 → 如实「地图尚未就绪」,不假装成功') check('F5-async-fly-false-awaited', (await store.locatePoint('MP-001', { ...deps, flyPoint: async () => false })).result === LOCATE_RESULT.UNAVAILABLE && openCalls.length === 1, '异步下发的 false 也被接住(详情不误开)') store.clearLocate() check('F6-clear-locate-resets', store.locate.result === '', 'clearLocate 收起反馈(浮层到点/手动关的落点)') await store.locatePoint('NOPE', deps) store.reset() check('F7-reset-clears-locate', store.locate.result === '', 'reset 一并清掉定位状态') // 未关联设备(非监测点)仍走中心换焦,不误用点位级完整视角。 const deviceObjects = [{ kind: 'unassociated-equipment', code: 'EQ-1', longitude: 110.9, latitude: 34.9, coordinate: { renderable: true, validated: true } }] let focusHits = 0 const device = await store.locatePoint('EQ-1', { objects: deviceObjects, mapReady: true, focus: () => (focusHits += 1, true), flyPoint: () => { throw new Error('未关联设备不该走监测点视角') }, openPoint: () => {}, }) check('F8-non-point-keeps-center-focus', device.result === LOCATE_RESULT.OK && focusHits === 1, `非监测点仍走中心换焦(${focusHits} 次)`) // 票 38 第二轮(反馈第 2 条):点**关联设备** → 飞宿主点位 + 开宿主详情 + 进设备级弹窗。 // 维护者的要求原话是「直接调用对应点位的点击事件,移动到目标位置并触发弹窗」。 const hostObjects = [ { kind: 'monitoring-point', code: 'CBWS202610150001', name: '城北排水监测点01', longitude: 110.399905, latitude: 28.459147, deviceCodes: ['21119740'], coordinate: { renderable: true, validated: true }, }, ] const hostFlights = [] const hostOpened = [] const deviceOpened = [] const fromDevice = await store.locatePoint('21119740', { objects: hostObjects, mapReady: true, focus: () => { throw new Error('回联到监测点后不该走中心换焦') }, flyPoint: (object) => { hostFlights.push(object.code) return true }, openPoint: (codeArg) => { hostOpened.push(codeArg) }, openDevice: (codeArg) => { deviceOpened.push(codeArg) }, }) check( 'F9-device-locates-host-and-opens-device', fromDevice.result === LOCATE_RESULT.OK && hostFlights.join(',') === 'CBWS202610150001' && hostOpened.join(',') === 'CBWS202610150001' && deviceOpened.join(',') === '21119740', `设备定位=飞宿主点位 + 开宿主详情 + 进设备级(飞行 ${hostFlights.join(',')} / 详情 ${deviceOpened.join(',')})`, ) check('F9b-notice-keeps-clicked-code', fromDevice.pointCode === '21119740', '反馈里显示的仍是用户点的那个编码(回联是内部动作,不改写用户看到的对象)') store.clearLocate() } console.log('') console.log(`T38 infrastructure panel adjustments: ${failed ? `${failed} FAILED` : `${passed} passed`}`) if (failed) process.exit(1)