t38-infrastructure-panel-adjustments.mjs 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. /**
  2. * T38 基础设施面板调整 纯逻辑闸门(票 38,前端执行票)。
  3. *
  4. * 零依赖 node 断言脚本(仓库没配测试运行器,约定见 t01-panel-catalog.mjs)。
  5. * 运行:node .scratch/cockpit-real-data-wayfinding/verify/t38-infrastructure-panel-adjustments.mjs
  6. *
  7. * 六段,对号票面四条范围 + 验收三句:
  8. * A 规模读数已删除(archivePanel / infrastructureSummaryPanel / 领域层不再透传 scale),
  9. * 而 `lengthKm` 与既有读数不受影响(验收第 1 条前半)。
  10. * B 定位三种「点不动」各有自己的文案、分得开(「没反应」本身是缺陷)。
  11. * C 管理信息按片区/行业两维度生成正确图表块 + 下方保留紧凑清单(验收第 1 条中段)。
  12. * D 真实后端 `scale` 恒 null 的形状灌进来,不再画空读数;`"km"` 不当规模展示。
  13. * E 静态守卫:反馈可见性搬出折叠面板;相机走完整视角路径;同行呈现无 key 特判。
  14. * F store 行为:成功走 flyPoint、失败不动相机、三种 result 各自落到 locate、异步 false 被接住。
  15. *
  16. * 变异归 t38-mutations.mjs(本脚本自己只跑绿)。
  17. */
  18. import { readFileSync } from 'node:fs'
  19. import { createPinia, setActivePinia } from 'pinia'
  20. import {
  21. archivePanel,
  22. managementPanel,
  23. toBaseArchives,
  24. toManagementInformation,
  25. } from '../../../src/modules/cockpit/domain/infrastructurePanels.js'
  26. import { infrastructureSummaryPanel, toOverviewRegions } from '../../../src/modules/cockpit/domain/overviewPanels.js'
  27. import { LOCATE_RESULT, LOCATE_TEXT, planLocate } from '../../../src/modules/cockpit/domain/mapLocate.js'
  28. import { DETAIL_PLACEHOLDER } from '../../../src/modules/cockpit/domain/detailFlow.js'
  29. import { SECTION_STATE, section } from '../../../src/modules/cockpit/domain/sectionState.js'
  30. import { useInfrastructureStore } from '../../../src/modules/cockpit/stores/infrastructureStore.js'
  31. const moduleRoot = new URL('../../../src/modules/cockpit/', import.meta.url)
  32. const read = (relative) => readFileSync(new URL(relative, moduleRoot), 'utf8')
  33. /** 去注释(注释会解释被禁的东西,全文正则会拿假阳性换通过率)。 */
  34. const code = (text) => text.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:])\/\/.*$/gm, '$1')
  35. const codeVue = (text) => code(text.replace(/<!--[\s\S]*?-->/g, ''))
  36. let passed = 0
  37. let failed = 0
  38. function check(name, condition, message) {
  39. if (condition) {
  40. passed += 1
  41. console.log(`PASS ${name}`)
  42. } else {
  43. failed += 1
  44. console.error(`FAIL ${name} — ${message}`)
  45. }
  46. }
  47. const sv = (data) => section(SECTION_STATE.SUCCESS, { data, loaded: true })
  48. setActivePinia(createPinia())
  49. // ================================================================ A 规模读数已删除
  50. {
  51. const data = toBaseArchives({
  52. updatedAt: 'T',
  53. range: { type: 'CURRENT_BASELINE' },
  54. // 故意把 scale 给成有值(真实后端是 null):界面仍不得出现「规模」读数。
  55. totals: { facilityCount: 5, lengthKm: '12.40', scale: 'medium' },
  56. industryDistribution: [{ code: 'GAS', count: 2 }],
  57. districtDistribution: [{ district: 0, count: 3 }],
  58. ageDistribution: [{ code: 'UNDER_5_YEARS', count: 1 }],
  59. facilityTypeDistribution: [{ code: 'GAS_PIPE', name: '燃气管线', count: 2, lengthKm: 3.1, scale: 'km' }],
  60. ageReference: { asOfDate: 'D', commissionedCount: 1, builtCount: 1, unknownCount: 0 },
  61. excluded: { unknownTypeCount: 0, typeKindMismatchCount: 0 },
  62. })
  63. check('A1-domain-drops-scale', data.scale === undefined, `领域对象不再带出 scale(实测:${String(data.scale)})`)
  64. check('A1b-domain-keeps-length', data.lengthKm === 12.4, `lengthKm 保留(BigDecimal 字符串被认下:${data.lengthKm})`)
  65. const view = archivePanel(data, sv(data))
  66. const kpi = view.blocks[0]
  67. const labels = kpi.items.map((item) => item.label)
  68. const keys = kpi.items.map((item) => item.key)
  69. check(
  70. 'A2-archive-kpi-has-no-scale-reading',
  71. !keys.includes('scale') && !labels.includes('规模') && !JSON.stringify(view).includes('中型'),
  72. `基础档案 KPI 只剩 ${keys.join('/')}(规模读数与其中文翻译都不再上屏)`,
  73. )
  74. check(
  75. 'A2b-archive-keeps-facility-count-and-length',
  76. 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',
  77. '设施总数与管线里程读数不变(票面「不改变其它读数」)',
  78. )
  79. check(
  80. 'A3-two-readings-share-one-kpi-block',
  81. kpi.items.length === 2 && view.blocks.filter((b) => b.kind === 'kpi').length === 1,
  82. `两项读数在同一条 KPI 网格(auto-fit → 同行呈现),没有第三块抢行`,
  83. )
  84. check('A3b-no-per-key-wide-class', !/item\.key === 'lengthKm'/.test(codeVue(read('components/PanelBlocks.vue'))), '渲染层不按 key 特判整行独占(第二轮:`wide` 改由 domain 声明)')
  85. // 票 38 第二轮(反馈第 1 条):概览的管线里程独占整行(数值最长,并排挤压),
  86. // 但「谁独占」的决定权在 domain —— 渲染层只认通用 `item.wide`。
  87. const ovWide = infrastructureSummaryPanel(
  88. { infrastructure: { facilityCount: 5, smartDeviceCount: 9, industries: [], excluded: {} }, regions: { regions: [], lengthKm: 12.4 } },
  89. sv(null),
  90. )
  91. check(
  92. 'A3c-overview-length-is-wide',
  93. ovWide.blocks[0].items.find((i) => i.key === 'lengthKm').wide === true &&
  94. ovWide.blocks[0].items.filter((i) => i.wide === true).length === 1,
  95. '概览基础设施:只有管线里程声明 wide(独占整行排版更好看)',
  96. )
  97. check(
  98. 'A3d-archive-row-not-wide',
  99. kpi.items.every((item) => item.wide === undefined),
  100. '基础档案两项都不声明 wide → 仍同行(第一轮的第 4 条不被撤销)',
  101. )
  102. // 总体概览「基础设施」面板同样删规模。
  103. const overviewRegions = toOverviewRegions({
  104. totals: { facilityCount: 9, lengthKm: 12.4, scale: 'medium' },
  105. districtDistribution: [{ district: 0, count: 3 }],
  106. ageReference: {},
  107. excluded: {},
  108. })
  109. check('A4-overview-regions-drop-scale', overviewRegions.scale === undefined && overviewRegions.lengthKm === 12.4, 'toOverviewRegions 不再带 scale,lengthKm 保留')
  110. const ov = infrastructureSummaryPanel(
  111. { infrastructure: { facilityCount: 5, smartDeviceCount: 9, industries: [], excluded: {} }, regions: overviewRegions },
  112. sv(null),
  113. )
  114. const ovKeys = ov.blocks[0].items.map((item) => item.key)
  115. check(
  116. 'A5-overview-kpi-has-no-scale-reading',
  117. !ovKeys.includes('scale') && ovKeys.includes('lengthKm'),
  118. `概览基础设施 KPI:${ovKeys.join('/')}(规模已删、里程保留)`,
  119. )
  120. check('A6-both-panels-clean', !JSON.stringify(ov).includes('规模') && !JSON.stringify(view.blocks[0]).includes('规模'), '两块面板的 KPI 里零「规模」字样')
  121. }
  122. // ================================================================ B 三种「点不动」各有文案
  123. {
  124. const objects = [
  125. { kind: 'monitoring-point', code: 'MP-001', name: '甲', longitude: 110.1, latitude: 34.2, coordinate: { renderable: true, validated: true } },
  126. { kind: 'monitoring-point', code: 'MP-003', name: '丙', longitude: 110.3, latitude: 34.4, coordinate: { renderable: false, validated: true, reason: 'ZERO_COORDINATE' } },
  127. ]
  128. const missing = planLocate({ objects, pointCode: 'NOPE', mapReady: true })
  129. const blocked = planLocate({ objects, pointCode: 'MP-003', mapReady: true })
  130. const unavailable = planLocate({ objects, pointCode: 'MP-001', mapReady: false })
  131. const ok = planLocate({ objects, pointCode: 'MP-001', mapReady: true })
  132. 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 四态分得开')
  133. check(
  134. 'B2-each-failure-has-visible-text',
  135. [missing, blocked, unavailable].every((plan) => typeof plan.reasonText === 'string' && plan.reasonText.trim().length > 0),
  136. `三种失败都有可读文案:${missing.reasonText} / ${blocked.reasonText} / ${unavailable.reasonText}`,
  137. )
  138. check('B2b-texts-differ', new Set([missing.reasonText, blocked.reasonText, unavailable.reasonText]).size === 3, '三句文案互不相同(读者能分清是自己点错还是数据问题)')
  139. check('B3-blocked-carries-backend-reason', blocked.reasonText === '零坐标(经纬度均为 0)' && !/ZERO_COORDINATE/.test(blocked.reasonText), `不可上图说具体原因且不泄原因码:${blocked.reasonText}`)
  140. check('B4-locate-text-constants', LOCATE_TEXT.MISSING.includes('已加载') && LOCATE_TEXT.UNAVAILABLE.includes('就绪'), '两种自带文案的语义锚点(范围 / 就绪)在 domain 一处定义')
  141. // 票 38 第二轮(反馈第 2 条):**关联设备**(真实数据 `21119740`)不以自身编码上图,
  142. // 它是宿主监测点 `deviceCodes` 的成员。点它的定位必须回联宿主点位并成功,
  143. // 而不是误报「该点位不在已加载范围」——维护者:「实际上该点位是已加载在图中的真实设备」。
  144. const deviceObjects = [
  145. {
  146. kind: 'monitoring-point',
  147. code: 'CBWS202610150001',
  148. name: '城北排水监测点01',
  149. longitude: 110.399905,
  150. latitude: 28.459147,
  151. deviceCodes: ['21119740', '21119741'],
  152. coordinate: { renderable: true, validated: true },
  153. },
  154. ]
  155. const viaDevice = planLocate({ objects: deviceObjects, pointCode: '21119740', mapReady: true })
  156. check(
  157. 'B5-associated-device-resolves-to-host-point',
  158. viaDevice.result === LOCATE_RESULT.OK && viaDevice.object.code === 'CBWS202610150001' && viaDevice.deviceCode === '21119740',
  159. `点关联设备回联宿主监测点并定位成功(${viaDevice.object && viaDevice.object.code},设备码带出 ${viaDevice.deviceCode})`,
  160. )
  161. check('B5b-point-code-leaves-device-code-empty', planLocate({ objects: deviceObjects, pointCode: 'CBWS202610150001', mapReady: true }).deviceCode === '', '直接点位编码时 deviceCode 为空(不误开设备级)')
  162. const stillMissing = planLocate({ objects: deviceObjects, pointCode: 'TCRQ202609080001', mapReady: true })
  163. check('B5c-unknown-code-still-missing', stillMissing.result === LOCATE_RESULT.MISSING && stillMissing.object === null, '既非对象编码也非任何点位的设备编码 → 仍如实 MISSING(回联不吞真缺失)')
  164. const blockedDevice = planLocate({ objects: [{ ...deviceObjects[0], coordinate: { renderable: false, validated: true, reason: 'ZERO_COORDINATE' } }], pointCode: '21119740', mapReady: true })
  165. check('B5d-host-blocked-reports-blocked', blockedDevice.result === LOCATE_RESULT.BLOCKED, '宿主点位不可上图 → BLOCKED(准入判据照宿主点位自己,不因为换了入口就放行)')
  166. 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', '编码若真是对象自身编码,直接命中(回联只在找不到时兜底,不改既有语义)')
  167. }
  168. // ================================================================ C 管理信息:片区 × 行业图表化
  169. {
  170. const data = toManagementInformation({
  171. totals: { facilityCount: 12, equipmentCount: 8 },
  172. districtIndustry: [
  173. { district: 0, districtName: '城南', industry: 'GAS', facilityCount: 5, equipmentCount: 2 },
  174. { district: 0, districtName: '城南', industry: 'WATER_SUPPLY', facilityCount: 4, equipmentCount: 3 },
  175. { district: 1, districtName: '城北', industry: 'GAS', facilityCount: 2, equipmentCount: 1 },
  176. { district: null, districtName: '未分区', industry: 'DRAINAGE', facilityCount: 1, equipmentCount: 2 },
  177. ],
  178. // 已退役字段:给了也不许被消费。
  179. organizations: [{ organizationName: 'FORBIDDEN-ORG' }],
  180. ownershipDistribution: [{ ownershipType: 'OWNED' }],
  181. unmaintained: [{ code: 'FORBIDDEN-UNMAINTAINED' }],
  182. })
  183. const view = managementPanel(data, sv(data))
  184. const dim = view.blocks.find((b) => b.kind === 'dimbars')
  185. check('C1-dimbars-block-exists', !!dim, `管理信息有可切换维度的柱状图块(块类型:${view.blocks.map((b) => b.kind).join('/')})`)
  186. check(
  187. 'C2-two-dimensions',
  188. dim && dim.dimensions.length === 2 && dim.dimensions.map((d) => d.key).join(',') === 'district,industry',
  189. `两维度都在:${dim ? dim.dimensions.map((d) => `${d.label}(${d.key})`).join(' / ') : '—'}`,
  190. )
  191. const districtDim = dim.dimensions.find((d) => d.key === 'district')
  192. const industryDim = dim.dimensions.find((d) => d.key === 'industry')
  193. check(
  194. 'C3-district-rows-aggregate-and-rank',
  195. districtDim.rows.map((r) => r.count).join(',') === '9,2,1' && districtDim.rows.map((r) => r.label).join('、') === '城南、城北、未分区',
  196. `按片区聚合城南=5+4=9 并降序:${districtDim.rows.map((r) => `${r.label}${r.count}`).join(' ')}`,
  197. )
  198. check(
  199. 'C4-industry-rows-aggregate',
  200. industryDim.rows.map((r) => r.count).join(',') === '7,4,1' && industryDim.rows.map((r) => r.label).join('、') === '燃气、供水、排水',
  201. `按行业聚合燃气=5+2=7:${industryDim.rows.map((r) => `${r.label}${r.count}`).join(' ')}`,
  202. )
  203. check('C5-ratio-normalized-per-dimension', districtDim.rows[0].ratio === 1 && industryDim.rows[0].ratio === 1, '条长按该维度最大项归一(各维度独立)')
  204. check('C5b-equipment-count-in-hint', districtDim.rows[0].hint.includes('设备'), `设备计数进悬停说明不占柱:${districtDim.rows[0].hint}`)
  205. check('C6-default-active-district', dim.active === 'district', `默认维度=片区(图面先回答「分布在哪」):${dim.active}`)
  206. const list = view.blocks.find((b) => b.kind === 'list')
  207. check('C7-compact-list-kept', !!list && list.rows.length === 4 && list.rows[0].meta.join(' ').includes('设备'), '下方保留紧凑卡片清单(逐 片区×行业 组合)')
  208. check('C7b-list-label', list.label.includes('片区') && list.label.includes('行业'), `清单标题仍是片区×行业口径:${list.label}`)
  209. const dumped = JSON.stringify(view)
  210. check('C8-retired-fields-not-consumed', !/FORBIDDEN-ORG|FORBIDDEN-UNMAINTAINED|OWNED|管理单位|权属/.test(dumped), '退役的机构/权属/未维护维度零消费')
  211. check('C8b-no-responsibility-area-concept', !/责任区域/.test(dumped), '「责任区域」不作为独立概念出现(决策 06:它就是片区别名)')
  212. // 空范围:不假装有数据。
  213. const empty = managementPanel(toManagementInformation({ totals: { facilityCount: 0, equipmentCount: 0 }, districtIndustry: [] }), sv(null))
  214. const emptyDim = empty.blocks.find((b) => b.kind === 'dimbars')
  215. check('C9-empty-says-empty', emptyDim.dimensions.every((d) => d.rows.length === 0), '空计数 → 两维度都没有行(界面走 emptyText,不画假柱)')
  216. // 行业未知 / 全 0 计数保留行(0 是真实值)。
  217. const zero = managementPanel(
  218. { facilityCount: 0, equipmentCount: 1, districtIndustry: [{ district: 1, industry: 'GAS', facilityCount: 0, equipmentCount: 1 }] },
  219. sv(null),
  220. )
  221. const zeroDim = zero.blocks.find((b) => b.kind === 'dimbars')
  222. const gasRow = zeroDim.dimensions.find((d) => d.key === 'district').rows[0]
  223. check('C10-zero-count-row-kept', gasRow.count === 0 && gasRow.ratio === 0 && Number.isFinite(gasRow.ratio), '0 计数保留行且条长 0(不 NaN、不隐藏)')
  224. const unk = managementPanel(toManagementInformation({ totals: { facilityCount: 0, equipmentCount: 0 }, districtIndustry: [{ district: 9, industry: '', facilityCount: 0, equipmentCount: 0 }] }), sv(null))
  225. const unkInd = unk.blocks.find((b) => b.kind === 'dimbars').dimensions.find((d) => d.key === 'industry').rows[0]
  226. check('C10b-unknown-bucket-labeled', unkInd.key === 'UNASSIGNED' && unkInd.label === '行业未知', `认不出的行业进显式「行业未知」桶:${unkInd.label}`)
  227. const unkDist = unk.blocks.find((b) => b.kind === 'dimbars').dimensions.find((d) => d.key === 'district').rows[0]
  228. check('C10c-invalid-district-unknown', unkDist.key === 'UNASSIGNED' && unkDist.label === '未分区', `认不出的片区经适配层归一后进「未分区」桶:${unkDist.label}`)
  229. }
  230. // ================================================================ D 真实后端形状(scale 恒 null)
  231. {
  232. // 照抄 t18 真实审计载荷:totals.scale = null、facilityTypes[].scale = "km"。
  233. const real = toBaseArchives({
  234. totals: { facilityCount: 5283, lengthKm: 67.60944, scale: null },
  235. industryDistribution: [{ code: 'GAS', count: 537 }],
  236. districtDistribution: [{ district: 0, count: 500 }],
  237. facilityTypeDistribution: [{ code: 'GAS_PIPE', name: '燃气管线', count: 537, lengthKm: 3.118, scale: 'km' }],
  238. ageReference: {},
  239. excluded: {},
  240. })
  241. const view = archivePanel(real, sv(real))
  242. check('D1-null-scale-not-rendered', !JSON.stringify(view.blocks[0]).includes('规模'), `真实 null 规模不再画成一行空读数(KPI:${view.blocks[0].items.map((i) => i.key).join('/')})`)
  243. check('D2-km-unit-not-shown-as-scale', JSON.stringify(view).indexOf('规模') === -1, `facilityTypes[].scale 的 "km" 不被当规模展示(长度归 lengthKm)`)
  244. check('D3-facility-count-unchanged', view.blocks[0].items.find((i) => i.key === 'facilityCount').value === '5283', '设施总数读数不受影响')
  245. check('D3b-length-unchanged', view.blocks[0].items.find((i) => i.key === 'lengthKm').value === '67.60944', '管线里程读数不受影响')
  246. }
  247. // ================================================================ E 静态守卫
  248. {
  249. const panelVue = codeVue(read('components/PanelBlocks.vue'))
  250. const shellVue = codeVue(read('CockpitShell.vue'))
  251. const storeCode = code(read('stores/infrastructureStore.js'))
  252. const domainCode = code(read('domain/infrastructurePanels.js'))
  253. check('E1-dimbars-rendered', /block\.kind === 'dimbars'/.test(panelVue) && /data-dimbar=/.test(panelVue) && /data-dim-tab=/.test(panelVue), 'dimbars 块有画法与走查断言面')
  254. check('E2-switch-is-display-only', /setDimension\(/.test(panelVue) && !/emit\('panel-action'[\s\S]{0,120}dimbar/.test(panelVue), '维度切换是组件本地展示状态,不冒充 panel-action 筛选接缝')
  255. check('E2b-switch-uses-semantic-button', /<button[\s\S]{0,500}?data-dim-tab=/.test(panelVue) && /:aria-pressed=/.test(panelVue), '切换标签是真 button + aria-pressed(状态读得出,不靠颜色)')
  256. check('E3-dimbars-labels-ellipsis', (panelVue.match(/<EllipsisText class="panel-bar__label" :text="row\.label" \/>/g) || []).length >= 2, 'dimbars 行标签复用票 37 共用省略方法(bars 与 dimbars 两处同写法)')
  257. check('E3b-single-ellipsis-declaration', !/text-overflow/.test(panelVue), 'PanelBlocks.vue 仍零 text-overflow 声明(省略只有一份)')
  258. check('E4-domain-does-not-fetch-or-import-service', !/\bfetch\s*\(|XMLHttpRequest/.test(domainCode) && !/from '\.\.\/services\//.test(domainCode), 'domain 纯函数层不发请求、不 import service')
  259. check('E5-locate-seam-still-shared', /planLocate/.test(code(read('domain/mapLocate.js'))) && /isRenderable/.test(code(read('domain/mapLocate.js'))), '定位判定仍收在 domain 一处(复用票 07 坐标准入)')
  260. // 反馈可见性:不得再藏在默认收起的状态详情里。
  261. const noticeAt = shellVue.indexOf('data-locate-notice')
  262. check('E6-visible-notice-exists', noticeAt >= 0, '定位反馈有独立可见浮层(data-locate-notice)')
  263. const foldRegion = shellVue.slice(shellVue.indexOf('v-show="statusPanelExpanded"'), noticeAt)
  264. check('E6b-feedback-not-behind-collapse', noticeAt >= 0 && foldRegion.indexOf('data-locate-result') < 0, '失败反馈不再挂在折叠的状态详情区(折叠=「没反应」的来源)')
  265. const noticeTag = shellVue.slice(shellVue.lastIndexOf('<div', noticeAt), noticeAt + 60)
  266. 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), '浮层显隐只由反馈结果决定,不被折叠状态二次门控(门回折叠=「没反应」换写法复发)')
  267. check('E7-result-attribute-surface', /:data-locate-result="locateFeedback\.result"/.test(shellVue), 'data-locate-result 断言面随浮层保留')
  268. check('E8-auto-dismiss', /setTimeout\(\(\) => \{[\s\S]{0,120}?clearLocate\(\)/.test(shellVue) && /dismissLocateFeedback/.test(shellVue) && /clearTimeout\(locateDismissTimer\)/.test(shellVue), '浮层自动收起 + 可手动关 + 卸载拆定时器(票 15 的漏取消坑)')
  269. check('E9-locate-flies-full-view', /locateMonitoringPoint/.test(storeCode) && /flyPoint\(plan\.object\)/.test(storeCode) && /await flyPoint/.test(storeCode), '监测点走完整视角定位(不是只换中心的 setCameraFocus)')
  270. check('E10-store-clear-exports', /clearLocate/.test(storeCode), 'store 暴露 clearLocate(反馈唯一持有者仍是 store,壳不自己抹状态)')
  271. check('E11-shell-forwards-locate', (shellVue.match(/@locate="locateOnMap"/g) || []).length === 2, '左右两栏的 @locate 仍接到外壳(data-locate-point → @locate → store)')
  272. check('E12-no-backend-names-in-ui', !/districtIndustry|ownershipDistribution|organizations/.test(panelVue), '渲染层不认识后端字段名(维度语义归 domain)')
  273. // 票 38 第二轮(反馈第 1 条):概览的管线里程与基础档案对不上 —— 根因是 overviewStore
  274. // 明明取了 base-archives(里程唯一来源)却在合并时硬写 `regions: null`。
  275. // 静态守卫钉住「真的并进数据」;行为守卫见 t20 的 D3(loadPanels 端到端)。
  276. const overviewStoreCode = code(read('stores/overviewStore.js'))
  277. 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)')
  278. }
  279. // ================================================================ F store 行为
  280. {
  281. const store = useInfrastructureStore()
  282. store.reset()
  283. const objects = [
  284. { kind: 'monitoring-point', code: 'MP-001', name: '甲', longitude: 110.1, latitude: 34.2, coordinate: { renderable: true, validated: true } },
  285. { kind: 'monitoring-point', code: 'MP-003', name: '丙', longitude: 110.3, latitude: 34.4, coordinate: { renderable: false, validated: true, reason: 'ZERO_COORDINATE' } },
  286. ]
  287. const flyCalls = []
  288. const openCalls = []
  289. const deps = {
  290. objects,
  291. mapReady: true,
  292. focus: () => {
  293. throw new Error('监测点定位不该再走中心换焦')
  294. },
  295. flyPoint: (object) => {
  296. flyCalls.push(object.code)
  297. return true
  298. },
  299. openPoint: (codeArg) => {
  300. openCalls.push(codeArg)
  301. },
  302. }
  303. const ok = await store.locatePoint('MP-001', deps)
  304. const blocked = await store.locatePoint('MP-003', deps)
  305. const missing = await store.locatePoint('NOPE', deps)
  306. const notReady = await store.locatePoint('MP-001', { ...deps, mapReady: false })
  307. check('F1-success-flies-once', ok.result === LOCATE_RESULT.OK && flyCalls.join(',') === 'MP-001' && openCalls.join(',') === 'MP-001', `成功:飞行 1 次 + 详情 1 次(${flyCalls.join(',')})`)
  308. check('F2-failures-dont-fly', blocked.result === LOCATE_RESULT.BLOCKED && missing.result === LOCATE_RESULT.MISSING && notReady.result === LOCATE_RESULT.UNAVAILABLE && flyCalls.length === 1, '三种失败都不动相机(不可上图/找不到/未就绪各归各)')
  309. check('F3-each-failure-has-text', [blocked, missing, notReady].every((r) => r.reasonText.trim().length > 0), '三种失败结果都带着要显示的文案(外壳据此出浮层,不存在「无反馈」状态)')
  310. check('F4-camera-refusal-degrades-honestly', (await store.locatePoint('MP-001', { ...deps, flyPoint: () => false })).result === LOCATE_RESULT.UNAVAILABLE, '相机没吃下指令 → 如实「地图尚未就绪」,不假装成功')
  311. check('F5-async-fly-false-awaited', (await store.locatePoint('MP-001', { ...deps, flyPoint: async () => false })).result === LOCATE_RESULT.UNAVAILABLE && openCalls.length === 1, '异步下发的 false 也被接住(详情不误开)')
  312. store.clearLocate()
  313. check('F6-clear-locate-resets', store.locate.result === '', 'clearLocate 收起反馈(浮层到点/手动关的落点)')
  314. await store.locatePoint('NOPE', deps)
  315. store.reset()
  316. check('F7-reset-clears-locate', store.locate.result === '', 'reset 一并清掉定位状态')
  317. // 未关联设备(非监测点)仍走中心换焦,不误用点位级完整视角。
  318. const deviceObjects = [{ kind: 'unassociated-equipment', code: 'EQ-1', longitude: 110.9, latitude: 34.9, coordinate: { renderable: true, validated: true } }]
  319. let focusHits = 0
  320. const device = await store.locatePoint('EQ-1', {
  321. objects: deviceObjects,
  322. mapReady: true,
  323. focus: () => (focusHits += 1, true),
  324. flyPoint: () => {
  325. throw new Error('未关联设备不该走监测点视角')
  326. },
  327. openPoint: () => {},
  328. })
  329. check('F8-non-point-keeps-center-focus', device.result === LOCATE_RESULT.OK && focusHits === 1, `非监测点仍走中心换焦(${focusHits} 次)`)
  330. // 票 38 第二轮(反馈第 2 条):点**关联设备** → 飞宿主点位 + 开宿主详情 + 进设备级弹窗。
  331. // 维护者的要求原话是「直接调用对应点位的点击事件,移动到目标位置并触发弹窗」。
  332. const hostObjects = [
  333. {
  334. kind: 'monitoring-point',
  335. code: 'CBWS202610150001',
  336. name: '城北排水监测点01',
  337. longitude: 110.399905,
  338. latitude: 28.459147,
  339. deviceCodes: ['21119740'],
  340. coordinate: { renderable: true, validated: true },
  341. },
  342. ]
  343. const hostFlights = []
  344. const hostOpened = []
  345. const deviceOpened = []
  346. const fromDevice = await store.locatePoint('21119740', {
  347. objects: hostObjects,
  348. mapReady: true,
  349. focus: () => {
  350. throw new Error('回联到监测点后不该走中心换焦')
  351. },
  352. flyPoint: (object) => {
  353. hostFlights.push(object.code)
  354. return true
  355. },
  356. openPoint: (codeArg) => {
  357. hostOpened.push(codeArg)
  358. },
  359. openDevice: (codeArg) => {
  360. deviceOpened.push(codeArg)
  361. },
  362. })
  363. check(
  364. 'F9-device-locates-host-and-opens-device',
  365. fromDevice.result === LOCATE_RESULT.OK &&
  366. hostFlights.join(',') === 'CBWS202610150001' &&
  367. hostOpened.join(',') === 'CBWS202610150001' &&
  368. deviceOpened.join(',') === '21119740',
  369. `设备定位=飞宿主点位 + 开宿主详情 + 进设备级(飞行 ${hostFlights.join(',')} / 详情 ${deviceOpened.join(',')})`,
  370. )
  371. check('F9b-notice-keeps-clicked-code', fromDevice.pointCode === '21119740', '反馈里显示的仍是用户点的那个编码(回联是内部动作,不改写用户看到的对象)')
  372. store.clearLocate()
  373. }
  374. console.log('')
  375. console.log(`T38 infrastructure panel adjustments: ${failed ? `${failed} FAILED` : `${passed} passed`}`)
  376. if (failed) process.exit(1)