| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547 |
- /**
- * T14 事件详情与设备定位 纯逻辑检查(票 14)。
- *
- * 零依赖 node 断言脚本(仓库没配测试运行器,约定见 t01-panel-catalog.mjs)。
- * 运行:node .scratch/cockpit-shell-panel-layout/verify/t14-event-detail.mjs
- *
- * 八段(每条都对着票 14 的验收原句):
- * A 验收 1:详情字段齐全(编码/设备编码/指标/阈值/实际值/等级/时间/状态)
- * B 等级与状态复用**同一张表**(票 13 的词表,不在这里长出第二份映射)
- * C 验收 2+3:定位按**设备编码**回联点位;高亮的编码同时含点位码与设备码(管线匹配键)
- * D 验收 5:三种"点不动"分得开(不在已加载范围 / 后端判不可上图 / 地图未就绪),
- * 且原因用的是票 07 的同一张中文词表
- * E 验收 5:无有效坐标时详情**仍完整可看**,并在"位置"一栏给出无地图位置原因
- * F 验收 3:高亮**不新建地图实体**(事件不变成独立图层)—— 静态守卫 + 计划不变
- * G 验收 4:store 的打开/返回不动列表上下文(行、族、分区一个字节没变),定位不重取;
- * 高亮返回 0 时如实报「尚未出现在地图上」
- * H 静态守卫:组件/store 零后端裸字段、零凭据、domain 不 import service
- *
- * 契约依据:`CockpitCurrentEventsResponse.EventItem`(票 13 的 `toEventItem`)与
- * CONTEXT.md 的「两级详情 / 无地图位置 / 管线→设备的关联方向」。
- */
- import { readFileSync } from 'node:fs'
- import { createPinia, setActivePinia } from 'pinia'
- import { SECTION_STATE } from '../../../src/modules/cockpit/domain/sectionState.js'
- import { LEVEL_TAGS } from '../../../src/modules/cockpit/domain/statusTones.js'
- import { COORDINATE_REASON_TEXT, isRenderable } from '../../../src/modules/cockpit/domain/coordinate.js'
- import { LOCATE_RESULT, LOCATE_TEXT } from '../../../src/modules/cockpit/domain/mapLocate.js'
- import { toEventItem, eventRowView, latestEventRows } from '../../../src/modules/cockpit/domain/eventList.js'
- import {
- EVENT_DETAIL_RESULT,
- eventDetailView,
- eventHighlightCodes,
- eventLocationView,
- planEventLocate,
- } from '../../../src/modules/cockpit/domain/eventDetail.js'
- import { planPointMarkers } from '../../../src/modules/cockpit/domain/pointStatus.js'
- import { registerMockTransport, resetMockTransportsForTest } from '../../../src/modules/cockpit/services/cockpitApi.js'
- import { registerCockpitMocks } from '../../../src/modules/cockpit/mocks/contractMocks.js'
- import { fetchCurrentEvents } from '../../../src/modules/cockpit/services/eventApi.js'
- import { EMPHASIS_STYLE, shouldEmphasize } from '../../../src/modules/cockpit/domain/mapEmphasis.js'
- import { useEventStore } from '../../../src/modules/cockpit/stores/eventStore.js'
- const checks = []
- function check(name, pass, evidence) {
- checks.push({ name, pass })
- console.log(`${pass ? 'PASS' : 'FAIL'} ${name}${evidence ? ` — ${evidence}` : ''}`)
- }
- const mockConfig = { dataSource: 'mock', api: { baseUrl: '', token: '' } }
- 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 readCode = (relative) => code(read(relative))
- /** `.vue` 的注释是 `<!-- -->`,要单独剥。 */
- const codeVue = (text) => code(text.replace(/<!--[\s\S]*?-->/g, ''))
- const readVueCode = (relative) => codeVue(read(relative))
- /** 取一个具名函数的**正文**(用于「这个接缝里有没有干某件事」的静态守卫)。 */
- function functionBody(text, name) {
- const start = text.indexOf(`function ${name}(`)
- if (start < 0) return ''
- let depth = 0
- for (let i = text.indexOf('{', start); i < text.length; i += 1) {
- if (text[i] === '{') depth += 1
- if (text[i] === '}') {
- depth -= 1
- if (depth === 0) return text.slice(start, i + 1)
- }
- }
- return text.slice(start)
- }
- setActivePinia(createPinia())
- /** 后端 `/events/current` 的一条 `EventItem`(字段照契约)。 */
- function rawItem({
- no = 'WRN-2001',
- name = '供水压力预警',
- type = 'WATER_PRESSURE',
- level = 2,
- status = 'PENDING',
- value = 0.28,
- unit = 'MPa',
- deviceCode = 'EQ-001',
- monitoringPointCode = 'MP-001',
- min = 0.3,
- max = 0.6,
- validation = { state: 'VALID', reason: null, renderable: true, crs: 'WGS84' },
- } = {}) {
- return {
- eventNo: no,
- eventName: name,
- eventType: type,
- level,
- levelColor: { 1: 'RED', 2: 'YELLOW', 3: 'BLUE', 4: 'GREEN' }[level] || 'UNKNOWN',
- status,
- eventTime: '2026-09-02T09:55:00+08:00',
- updatedTime: '2026-09-02T09:56:00+08:00',
- value,
- unit,
- deviceCode,
- deviceName: '供水压力计-001',
- monitoringPointCode,
- monitoringPointName: '供水干管监测点',
- location: '城区B',
- industry: 'WATER_SUPPLY',
- longitude: 110.141,
- latitude: 28.452,
- coordinateValidation: validation,
- thresholdMinValue: min,
- thresholdMaxValue: max,
- workOrderSummary: null,
- }
- }
- /** 领域行(票 13 的 `toEventItem` → 列表视图行,票 14 的详情读它)。 */
- const rowOf = (raw) => eventRowView(toEventItem(raw))
- /** 一张图已有的对象(形状同 `domain/mapSummary.js`)。 */
- const mapObject = ({ code: objectCode = 'MP-001', deviceCodes = ['EQ-001'], renderable = true, reason = null } = {}) => ({
- kind: 'monitoring-point',
- code: objectCode,
- name: '供水干管监测点',
- status: 'NORMAL',
- statusReasons: [],
- deviceCodes,
- longitude: 110.141,
- latitude: 28.452,
- coordinate: { state: renderable ? 'VALID' : 'UNVERIFIED', reason, crs: 'WGS84', renderable, validated: true },
- })
- // ================================================================ A 详情字段齐全
- {
- const view = eventDetailView(rowOf(rawItem()))
- const wanted = [
- ['事件编码', view.code, 'WRN-2001'],
- ['设备编码', view.deviceCode, 'EQ-001'],
- ['指标', view.metric, 'WATER_PRESSURE'],
- ['阈值', view.threshold, '0.3 ~ 0.6 MPa'],
- ['实际值', view.value, '0.28 MPa'],
- ['等级', view.level.label, '2级'],
- ['时间', view.eventTime, '2026-09-02T09:55:00+08:00'],
- ['状态', view.status, '待处理'],
- ]
- const bad = wanted.filter(([, actual, expected]) => actual !== expected)
- check(
- 'A1-detail-lists-all-required-fields',
- bad.length === 0 && view.level.tone === LEVEL_TAGS[1].tone,
- bad.length ? `不符:${bad.map(([label, actual]) => `${label}=${actual}`).join(' / ')}` : '8 项字段齐全(含等级色键 + 文字)',
- )
- // 缺失值不伪装零值:null 走占位符,0 是**有效**数据(票 16 的纪律)。
- const sparse = eventDetailView(rowOf(rawItem({ value: null, min: null, max: null, type: '' })))
- const zero = eventDetailView(rowOf(rawItem({ value: 0 })))
- check(
- 'A2-missing-not-zero',
- sparse.value === '—' && sparse.threshold === '—' && sparse.metric === '—' && zero.value === '0 MPa',
- `缺失 value=${sparse.value}(占位符)、真实的 0 值=${zero.value}(照实显示,不当缺失)`,
- )
- // 只给最小阈值(报警常见形状):≥ 而不是编一个上界。
- const minOnly = eventDetailView(rowOf(rawItem({ max: null })))
- check('A3-threshold-half-open', minOnly.threshold === '≥ 0.3 MPa', `只有下界时显示 ${minOnly.threshold}`)
- }
- // ================================================================ B 等级/状态复用同一张表
- {
- const row = rowOf(rawItem({ level: 4, status: 'WEIRD_NEW_CODE' }))
- const view = eventDetailView(row)
- check(
- 'B1-same-tables-as-list',
- view.level.label === row.level.label &&
- view.level.tone === row.level.tone &&
- view.status === row.statusText &&
- view.status === 'WEIRD_NEW_CODE',
- `详情等级/状态与列表行逐个相等(${view.level.label} / ${view.status});认不出的状态回原码`,
- )
- }
- // ================================================================ C 定位:按设备编码回联
- {
- const objects = [mapObject({ objectCode: 'MP-001', deviceCodes: ['EQ-001'] }), mapObject({ objectCode: 'MP-009', deviceCodes: ['EQ-009'] })]
- const row = rowOf(rawItem({ deviceCode: 'EQ-001', monitoringPointCode: 'MP-001' }))
- const plan = planEventLocate(row, objects, true)
- check(
- 'C1-locates-by-device-code',
- plan.result === EVENT_DETAIL_RESULT.OK &&
- plan.object.code === 'MP-001' &&
- plan.target &&
- plan.target.lon === 110.141 &&
- plan.cameraHeight === undefined &&
- plan.object.code !== 'MP-009',
- `按设备编码 EQ-001 找到点位 ${plan.object && plan.object.code},相机目标 (${plan.target.lon}, ${plan.target.lat}, ${plan.target.height})`,
- )
- check(
- 'C2-highlight-codes-include-device-and-point',
- plan.codes.includes('EQ-001') && plan.codes.includes('MP-001'),
- `高亮编码 = ${JSON.stringify(plan.codes)}(点位码强调标记、设备码匹配关联管线 —— 票 22 的管线按设备关联)`,
- )
- // 设备只带编码、没有点位码时也能回联(事件列表可以只有 deviceCode)。
- const deviceOnly = planEventLocate(rowOf(rawItem({ monitoringPointCode: null })), objects, true)
- check(
- 'C3-locates-without-point-code',
- deviceOnly.result === EVENT_DETAIL_RESULT.OK && deviceOnly.object.code === 'MP-001',
- '只有设备编码(无点位码)时仍按 deviceCodes 命中点位',
- )
- check(
- 'C4-same-seat-as-ticket-17',
- LOCATE_RESULT.OK === EVENT_DETAIL_RESULT.OK &&
- LOCATE_RESULT.MISSING === EVENT_DETAIL_RESULT.MISSING &&
- LOCATE_RESULT.BLOCKED === EVENT_DETAIL_RESULT.BLOCKED &&
- LOCATE_RESULT.UNAVAILABLE === EVENT_DETAIL_RESULT.UNAVAILABLE,
- '四种定位结果沿用票 17/18 的 mapLocate 词表(不新造一套结果码)',
- )
- }
- // ================================================================ D 三种「点不动」分得开
- {
- const row = rowOf(rawItem())
- const missing = planEventLocate(row, [], true)
- check(
- 'D1-missing-keeps-list-data',
- missing.result === EVENT_DETAIL_RESULT.MISSING && missing.reasonText === LOCATE_TEXT.MISSING,
- `不在已加载范围 → ${missing.result}(「${missing.reasonText}」,数据可能还没翻到,不是错误)`,
- )
- const blocked = planEventLocate(row, [mapObject({ renderable: false, reason: 'UNVERIFIED_COORDINATE' })], true)
- check(
- 'D2-blocked-reuses-ticket-07-wording',
- blocked.result === EVENT_DETAIL_RESULT.BLOCKED &&
- blocked.reasonText === COORDINATE_REASON_TEXT.UNVERIFIED_COORDINATE &&
- isRenderable(blocked.object) === false,
- `后端判不可上图 → ${blocked.result},文案「${blocked.reasonText}」(与票 07 的「无地图位置」同一张词表)`,
- )
- const notReady = planEventLocate(row, [mapObject()], false)
- check(
- 'D3-unavailable-is-not-an-error',
- notReady.result === EVENT_DETAIL_RESULT.UNAVAILABLE && notReady.reasonText === LOCATE_TEXT.UNAVAILABLE,
- `地图未就绪 → ${notReady.result}(「${notReady.reasonText}」,与「数据没有」分开说)`,
- )
- // 定位的是关联设备:已加载地图对象可上图时不被事件旧坐标拦住。
- const invalidEvent = rowOf(rawItem({ validation: { state: 'INVALID', reason: 'ZERO_COORDINATE', renderable: false, crs: 'WGS84' } }))
- const badEvent = planEventLocate(invalidEvent, [mapObject()], true)
- const noDevice = planEventLocate(invalidEvent, [], true)
- check(
- 'D4-map-device-coordinate-is-authoritative',
- badEvent.result === EVENT_DETAIL_RESULT.OK &&
- noDevice.result === EVENT_DETAIL_RESULT.BLOCKED && noDevice.reasonText === COORDINATE_REASON_TEXT.ZERO_COORDINATE,
- '地图中设备有效可定位;没有设备且事件坐标非法仍如实拦下',
- )
- }
- // ================================================================ E 无坐标仍可查看
- {
- const blockedRow = rowOf(rawItem({ validation: { state: 'INVALID', reason: 'LONGITUDE_OUT_OF_RANGE', renderable: false, crs: 'WGS84' } }))
- const view = eventDetailView(blockedRow)
- const plan = planEventLocate(blockedRow, [mapObject()], true)
- check(
- 'E1-detail-complete-when-no-location',
- view.code === 'WRN-2001' &&
- view.deviceCode === 'EQ-001' &&
- view.value === '0.28 MPa' &&
- view.eventTime === '2026-09-02T09:55:00+08:00' &&
- view.status === '待处理' &&
- view.hasLocation === false,
- `无有效坐标时 8 项字段仍齐全,hasLocation=${view.hasLocation}`,
- )
- check(
- 'E2-no-location-reason-is-shown',
- view.location.includes(COORDINATE_REASON_TEXT.LONGITUDE_OUT_OF_RANGE) &&
- plan.result === EVENT_DETAIL_RESULT.OK &&
- planEventLocate(blockedRow, [], true).reasonText === COORDINATE_REASON_TEXT.LONGITUDE_OUT_OF_RANGE,
- `事件自身位置注明「${view.location}」;关联设备有效可定位,无设备时仍报告事件坐标原因`,
- )
- // 有坐标但后端没给文本位置:仍说得出「可定位」。
- const textless = eventLocationView(rowOf(rawItem({ location: null })))
- check(
- 'E3-locatable-without-text',
- textless.hasLocation === true && !!textless.location,
- `后端可上图但没给文本位置 → hasLocation=${textless.hasLocation}、显示「${textless.location}」`,
- )
- }
- // 走查用的「无坐标」样例必须真的能让整条路径复现(否则真机 D 组只是走过场)。
- {
- resetMockTransportsForTest()
- registerCockpitMocks({
- scenario: 'event-detail-no-coordinate',
- onlyPath: '/events/current',
- handlers: { registerMockTransport, resetMockTransportsForTest },
- })
- const payload = await fetchCurrentEvents({ mode: 'WARNING', config: mockConfig })
- resetMockTransportsForTest()
- registerCockpitMocks({ scenario: 'success', handlers: { registerMockTransport, resetMockTransportsForTest } })
- const rows = latestEventRows(payload.data.sections.events.data).map(eventRowView)
- const view = eventDetailView(rows[0])
- const plan = planEventLocate(rows[0], [mapObject()], true)
- check(
- 'E4-mock-scenario-covers-no-location',
- rows.length >= 12 &&
- rows[0].code.startsWith('WRN-') &&
- view.code === rows[0].code &&
- view.hasLocation === false &&
- view.location.includes(COORDINATE_REASON_TEXT.UNVERIFIED_COORDINATE) &&
- plan.result === EVENT_DETAIL_RESULT.BLOCKED,
- `样例 event-detail-no-coordinate:${rows.length} 行事件仍可看,详情 hasLocation=${view.hasLocation}、位置「${view.location}」、定位 ${plan.result} —— 真机 D 组不是走过场`,
- )
- }
- // ================================================================ F 高亮不新建实体
- {
- const objects = [mapObject(), mapObject({ objectCode: 'MP-009', deviceCodes: ['EQ-009'] })]
- const before = planPointMarkers(objects)
- const after = planPointMarkers(objects)
- check(
- 'F1-marker-plan-unchanged',
- before.markers.length === after.markers.length &&
- JSON.stringify(before.byStatus) === JSON.stringify(after.byStatus) &&
- before.total === 2,
- `定位前后标记计划逐位相同(共 ${before.total} 个),事件不进标记计划 —— 验收第 3 条是结构性的`,
- )
- const engine = readCode('services/cesiumEngine.js')
- const highlight = functionBody(engine, 'highlightEntities')
- check(
- 'F2-highlight-adds-no-entity',
- !!highlight && !/entities\.add\(/.test(highlight) && /entity\.billboard\.scale/.test(highlight),
- 'highlightEntities 只改已存在实体的显示参数(billboard.scale / polyline.width),零 entities.add',
- )
- const pipeline = functionBody(engine, 'pipelineSource')
- check(
- 'F3-pipeline-seat-is-ready',
- /cockpit-pipelines/.test(engine) && !!pipeline,
- '管线数据源(票 22)已经在高亮的扫描范围内:落地当天按设备码自动一起高亮,不需要再改本票',
- )
- const service = readCode('services/mapService.js')
- const serviceHighlight = functionBody(service, 'highlightMapObject')
- check(
- 'F4-service-highlight-is-read-only',
- !!serviceHighlight && !/markerPlan\s*=/.test(serviceHighlight) && /markerPlan\.markers\.find/.test(serviceHighlight),
- 'highlightMapObject 只读 markerPlan(把设备码解析成点位码),不改计划、不重画标记',
- )
- // ---- 真实行为:强调判定是纯函数,直接断言(不是读源码、也不是自己跟自己比)----
- const markerCandidates = ['MP-002', '', 'EQ-002', ['EQ-002']]
- const otherCandidates = ['MP-001', '', 'EQ-001', ['EQ-001']]
- const pipeCandidates = ['', 'PIPE-9', 'EQ-002', []]
- check(
- 'F5-emphasis-hits-point-and-pipeline-by-device',
- shouldEmphasize(markerCandidates, new Set(['EQ-002'])) === true &&
- shouldEmphasize(otherCandidates, new Set(['EQ-002'])) === false &&
- shouldEmphasize(pipeCandidates, new Set(['EQ-002'])) === true &&
- EMPHASIS_STYLE.markerScale > 1 &&
- EMPHASIS_STYLE.pipelineWidth > 0,
- '事件按设备码一次命中**两个**实体:点位标记(属性里的设备码)+ 关联管线(pipelineCode/deviceCode)—— 验收第 3 条的两半',
- )
- check(
- 'F6-empty-target-restores-everything',
- shouldEmphasize(markerCandidates, new Set()) === false &&
- shouldEmphasize(pipeCandidates, []) === false &&
- shouldEmphasize(markerCandidates, null) === false,
- '目标为空 = 全部还原(判 false);引擎对**每个实体**都写回上一次记下的原值,收起详情后标记不会一直胖着(早先版本在这里提前 return,一趟都没走)',
- )
- check(
- 'F7-engine-applies-the-verdict',
- /shouldEmphasize/.test(highlight) &&
- /EMPHASIS_STYLE\.markerScale/.test(highlight) &&
- /base\.polylineWidth/.test(highlight) &&
- !/wanted\.size\)\s*return/.test(highlight),
- '引擎只把判定写成显示参数:命中写强调值、未命中写回记下的原值,且空目标时**不提前返回**(还原路径走得通)',
- )
- check(
- 'F8-emphasis-is-observable',
- /dataset\.mapEmphasis = String\(matched\)/.test(service) &&
- /delete record\.host\.dataset\.mapEmphasis/.test(service),
- '服务层把强调计数写进宿主 `data-map-emphasis`、随地图销毁一并 delete:真机走查据此断言"收起后回到 0",不是读内部变量',
- )
- }
- // ================================================================ G store:上下文与结果
- {
- const store = useEventStore()
- store.reset()
- store.configure({ config: mockConfig })
- resetMockTransportsForTest()
- registerCockpitMocks({ scenario: 'success', handlers: { registerMockTransport, resetMockTransportsForTest } })
- await store.loadCurrentEvents({ mode: 'WARNING' })
- const rowsBefore = store.view.rows.map((row) => row.key)
- const sectionBefore = store.sections.currentEvents
- const domainBefore = store.currentDomain
- store.openEvent(store.view.rows[0])
- const sectionOpen = store.sections.currentEvents
- check(
- 'G1-open-keeps-list-context',
- !!store.selectedEvent &&
- store.mode === 'WARNING' &&
- store.sections.currentEvents === sectionBefore &&
- store.currentDomain === domainBefore &&
- JSON.stringify(store.view.rows.map((row) => row.key)) === JSON.stringify(rowsBefore),
- `打开详情后列表逐位不变(${rowsBefore.length} 行、族 ${store.mode}、分区对象同一引用)—— 列表在 store,收起/展开详情不触发任何重取`,
- )
- check(
- 'G2-open-keeps-section-identity',
- store.sections.currentEvents.loaded === true &&
- store.sections.currentEvents.state === SECTION_STATE.SUCCESS &&
- sectionOpen === sectionBefore,
- '详情打开不新建分区、不改分区状态',
- )
- const calls = { focus: [], highlight: [] }
- // 用**选中那一行的**编码造地图对象:定位是按设备编码回联,样例写死编码就会点空。
- const selected = store.selectedEvent
- const objects = [mapObject({ objectCode: selected.monitoringPointCode, deviceCodes: [selected.deviceCode] })]
- const ok = await store.locateEvent(store.selectedEvent, {
- objects,
- mapReady: true,
- focus: (target) => {
- calls.focus.push(target)
- return true
- },
- highlight: (codes) => {
- calls.highlight.push(codes)
- return 1
- },
- })
- check(
- 'G3-locate-drives-camera-and-highlight',
- ok.result === LOCATE_RESULT.OK &&
- calls.focus.length === 1 &&
- calls.focus[0].lon === objects[0].longitude &&
- calls.highlight.length === 1 &&
- calls.highlight[0].includes(selected.deviceCode) &&
- store.sections.currentEvents === sectionBefore,
- `已下发相机目标 + 高亮编码 ${JSON.stringify(calls.highlight[0])};列表分区仍是同一个(定位不重取)`,
- )
- const notOnMap = await store.locateEvent(store.selectedEvent, {
- objects,
- mapReady: true,
- focus: () => true,
- highlight: () => 0,
- })
- check(
- 'G4-highlight-zero-is-reported-honestly',
- notOnMap.result === LOCATE_RESULT.UNAVAILABLE && /尚未出现在地图上/.test(notOnMap.reasonText),
- `高亮命中 0 个 → ${notOnMap.result}(「${notOnMap.reasonText}」,不假装定位成功)`,
- )
- store.closeEvent()
- check(
- 'G5-close-returns-to-list-untouched',
- store.selectedEvent === null &&
- store.locate.result === '' &&
- store.mode === 'WARNING' &&
- store.sections.currentEvents === sectionBefore &&
- JSON.stringify(store.view.rows.map((row) => row.code)) === JSON.stringify(rowsBefore),
- '返回事件列表:详情收起、定位结果清空,族/筛选/分区/行序全部原样(验收第 4 条)',
- )
- // 事件列表行本身就是详情的数据源(详情不需要第二个端点)。
- const sharedSeat = latestEventRows(domainBefore.sections.events.data).map(eventRowView)
- check(
- 'G6-detail-reads-the-list-row',
- sharedSeat.length >= 12 && sharedSeat[0].code === store.view.rows[0].code && !!eventDetailView(sharedSeat[0]).threshold,
- '详情直接消费列表行(票 13 的 14 条样例里阈值/量值都在),没有为详情新开接口',
- )
- }
- // ================================================================ H 静态守卫
- {
- const pageAndStoreFiles = [
- 'components/EventDetailCard.vue',
- 'components/EventListPane.vue',
- 'stores/eventStore.js',
- ]
- const BACKEND_FIELD_PATTERN = /\b(eventNo|eventName|eventType|levelColor|monitoringPointCode|coordinateValidation|thresholdMinValue|thresholdMaxValue|workOrderSummary|mapFilter|workOrders|displayName)\b/
- for (const file of pageAndStoreFiles) {
- const text = read(file)
- const found = BACKEND_FIELD_PATTERN.exec(text)
- check(`H1-${file}-no-raw-fields`, !found, found ? `出现后端裸字段 ${found[0]}` : '零后端裸字段')
- }
- const ROUTE_PATTERN = /['"`]\/[a-z-]+\//
- for (const file of pageAndStoreFiles) {
- check(`H2-${file}-no-route-string`, !ROUTE_PATTERN.test(readCode(file)) && !ROUTE_PATTERN.test(readVueCode(file)), '不自己拼接口路径(路径归 services)')
- }
- const CREDENTIAL_PATTERN = /Authorization|Bearer|api\.token|ws\.token/
- for (const file of [...pageAndStoreFiles, 'domain/eventDetail.js']) {
- check(`H3-${file}-no-credential`, !CREDENTIAL_PATTERN.test(read(file)), '不碰凭据')
- }
- check('H4-domain-pure', !/\bfetch\s*\(|XMLHttpRequest/.test(read('domain/eventDetail.js')), 'domain/eventDetail.js 不自己发请求')
- check('H5-domain-no-service-import', !/from '\.\.\/services\//.test(read('domain/eventDetail.js')), 'domain/eventDetail.js 不 import service')
- // 详情必须能收起(返回列表),且返回不重取:收起只清选择。
- const storeCode = readCode('stores/eventStore.js')
- const closeBody = functionBody(storeCode, 'closeEvent')
- check(
- 'H6-close-does-not-refetch',
- !!closeBody && !/loadCurrentEvents|fetch/.test(closeBody) && /selectedEvent\.value = null/.test(closeBody),
- 'closeEvent 只清选择与高亮,零取数(否则「返回」会把列表滚回第 1 页)',
- )
- // 事件详情不新增标记:domain 不 import 引擎,也不产生 marker 计划。
- check(
- 'H7-detail-plans-no-marker',
- !/planPointMarkers|syncPointMarkers|Cesium/.test(read('domain/eventDetail.js')),
- '详情/定位层不建标记计划、不认识 Cesium',
- )
- }
- const failed = checks.filter((item) => !item.pass)
- console.log('')
- console.log(`T14 事件详情与设备定位检查:${checks.length - failed.length}/${checks.length} 项通过。`)
- if (failed.length) {
- console.log('未通过:')
- for (const item of failed) console.log(` · ${item.name}`)
- process.exit(1)
- }
- console.log('T14 事件详情与设备定位全部通过。')
|