|
|
@@ -0,0 +1,408 @@
|
|
|
+/**
|
|
|
+ * 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(/<!--[\s\S]*?-->/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', /<button[\s\S]{0,500}?data-dim-tab=/.test(panelVue) && /:aria-pressed=/.test(panelVue), '切换标签是真 button + aria-pressed(状态读得出,不靠颜色)')
|
|
|
+ check('E3-dimbars-labels-ellipsis', (panelVue.match(/<EllipsisText class="panel-bar__label" :text="row\.label" \/>/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('<div', noticeAt), noticeAt + 60)
|
|
|
+ check('E6c-notice-not-gated-on-collapse', /v-if="locateFeedback"\s+class="cockpit-shell__notice"/.test(shellVue) && !/locateFeedback[\s\S]{0,40}statusPanelExpanded/.test(noticeTag), '浮层显隐只由反馈结果决定,不被折叠状态二次门控(门回折叠=「没反应」换写法复发)')
|
|
|
+ check('E7-result-attribute-surface', /:data-locate-result="locateFeedback\.result"/.test(shellVue), 'data-locate-result 断言面随浮层保留')
|
|
|
+ check('E8-auto-dismiss', /setTimeout\(\(\) => \{[\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)
|