| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529 |
- /**
- * 票 44 离线闸门:类型/类别分布块降序的**完备性普查** + 统计条视觉密度守卫。
- *
- * 票 37 的 t50 只钉住了「监测概览 · 设备类型分布」这一块。本票范围第 1 条要的是
- * 「**所有**类型分布统计块真的降序」——只测一块等于没测完备性(票 20 的教训:
- * 测试与实现犯同一个错时闸门照样全绿)。做法:
- *
- * A. 普查(行为层,不是正则):把**故意打乱次序**的后端载荷灌进真实适配层
- * (`toXxx` → `xxxPanel`),对每一块 bars/donut/stacked 断言精确次序。两类期望:
- * · RANK —— 排名类别(类型/行业/片区/区域):计数从多到少、并列保序;
- * · FIXED —— 固定语义序(状态/等级/生命周期/建成年限/工单 0–7/异常桶):
- * 与输入逐位相同(非目标第 2 条:不许强行改降序)。
- * 期望表是**穷举**的:表外的分布块判红、表内没出现的期望(幽灵条目)也判红——
- * 新增块必须显式归类,这就是「补齐遗漏块」的机制,不靠人记得去查。
- * B. 视觉密度(静态守卫):行内收紧、轨道发丝边、填充端帽/高光、辉光删除,
- * 且 `EllipsisText` / `panel-action` / `data-bar-*` 三条接缝未被视觉改动带走。
- *
- * 变异(守卫有牙):
- * M1 `sortByCountDesc` 改恒等 → 所有 RANK 块判红;
- * M2 把 FIXED 块也套上降序(如异常桶/等级)→ 对应 FIXED 判红;
- * M3 `.panel-bar` 的 gap 改回 `var(--space-2)` → B1 红;
- * M4 删 `.panel-bar__fill::after`(端帽/分割层)→ B2b 红;
- * M5 轨道内描边删除 → B2 红;
- * M6 行标签退回裸 `<span>` → B4 红。
- *
- * 运行:node .scratch/cockpit-real-data-wayfinding/verify/t44-distribution-ranking.mjs
- */
- import assert from 'node:assert/strict'
- import { readFileSync } from 'node:fs'
- import { barsBlock, sortByCountDesc } from '../../../src/modules/cockpit/domain/panelBlocks.js'
- import {
- archivePanel,
- emergencyPanel,
- managementPanel,
- operationPanel,
- toBaseArchives,
- toEmergencySupport,
- toOperatingInformation,
- } from '../../../src/modules/cockpit/domain/infrastructurePanels.js'
- import {
- monitoringDistributionPanel,
- monitoringOverviewPanel,
- operationOverviewPanel,
- toMonitoringDistribution,
- toMonitoringOverview,
- toMonitoringSituation,
- } from '../../../src/modules/cockpit/domain/monitoringPanels.js'
- import {
- alertSettingsPanel,
- infrastructureSummaryPanel,
- monitoringSummaryPanel,
- situationSummaryPanel,
- toOverviewInfrastructure,
- toOverviewMonitoring,
- toOverviewRegions,
- toOverviewWarningSettings,
- } from '../../../src/modules/cockpit/domain/overviewPanels.js'
- import {
- eventAnalysisPanel,
- historyAnalysisPanel,
- responseEfficiencyPanel,
- toEventAnalysis,
- toEventHistory,
- toHandlingEfficiency,
- } from '../../../src/modules/cockpit/domain/warningResponsePanels.js'
- import { SECTION_STATE, section } from '../../../src/modules/cockpit/domain/sectionState.js'
- const read = (rel) => readFileSync(new URL(`../../../src/modules/cockpit/${rel}`, import.meta.url), 'utf8')
- const sv = (data) => section(SECTION_STATE.SUCCESS, { data })
- 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 isDesc = (counts) => counts.every((c, i) => i === 0 || counts[i - 1] >= c)
- /**
- * 普查一个面板视图:
- * · 每个分布块(bars/donut/stacked)的标题必须**恰好命中一条**期望(表外判红);
- * · 命中后比对精确次序(counts 与 keys 都断言);
- * · RANK 额外断言单调降序,FIXED 额外断言「不是碰巧也降序」(期望本身必须非单调,
- * 否则它证明不了没被重排——这是本组断言的牙)。
- * @param {string} name 面板名
- * @param {object} view 视图计划
- * @param {Array<{label: string, mode: 'RANK'|'FIXED', counts: number[], keys?: string[]}>} expect 期望表
- */
- function audit(name, view, expect) {
- const blocks = ((view && view.blocks) || []).filter((b) => ['bars', 'donut', 'stacked'].includes(b.kind))
- const seen = new Set()
- const orphans = []
- for (const block of blocks) {
- const hit = expect.filter((e) => e.label === block.label)
- if (hit.length !== 1) {
- orphans.push(`${block.label}(命中 ${hit.length} 条期望)`)
- continue
- }
- const e = hit[0]
- seen.add(e.label)
- const counts = block.rows.map((r) => r.count)
- const keys = block.rows.map((r) => r.key)
- let detail = `「${block.label}」次序 ${counts.join('、')}`
- if (e.keys) detail += `;键 ${keys.join('、')}`
- check(
- `A-${name}-${e.label}`,
- JSON.stringify(counts) === JSON.stringify(e.counts) && (!e.keys || JSON.stringify(keys) === JSON.stringify(e.keys)),
- `${detail}(期望 ${e.counts.join('、')}${e.keys ? `;键 ${e.keys.join('、')}` : ''})`,
- )
- if (e.mode === 'RANK') {
- check(`A-${name}-${e.label}-monotone`, isDesc(e.counts), `RANK 期望本身必须降序:${e.counts.join('、')}`)
- } else {
- check(`A-${name}-${e.label}-not-desc`, !isDesc(e.counts), `FIXED 期望必须**非单调**,否则证明不了没被强行降序:${e.counts.join('、')}`)
- }
- }
- check(`A-${name}-no-unclassified-block`, orphans.length === 0, `存在未归类的分布块:${orphans.join(';')} —— 新增块必须在期望表里显式声明 RANK/FIXED`)
- const ghosts = expect.filter((e) => !seen.has(e.label)).map((e) => e.label)
- check(`A-${name}-no-ghost-expectations`, ghosts.length === 0, `期望表里有面板不存在的块(块被改名/删除,表在骗人):${ghosts.join('、')}`)
- }
- // ================================================================ A. 真实适配层 + 打乱载荷
- // ---- A1 运行监测 · 监测概览(设备类型分布:并列 5、5 必须保持传入次序)
- {
- const data = toMonitoringOverview({
- counts: { monitoringPointCount: 6, equipmentCount: 11 },
- status: { onlineCount: 8, offlineCount: 1, staleCount: 1, unknownOrMissingCount: 1, denominator: 11 },
- onlineRate: { value: '0.7273', numerator: 8, denominator: 11, state: 'AVAILABLE' },
- typeDistribution: [
- { deviceType: { code: 'SMALL', name: '少' }, industry: 'GAS', deviceCount: 1 },
- { deviceType: { code: 'TIE_A', name: '并列甲' }, industry: 'GAS', deviceCount: 5 },
- { deviceType: { code: 'TIE_B', name: '并列乙' }, industry: 'WATER_SUPPLY', deviceCount: 5 },
- { deviceType: { code: 'MID', name: '中' }, industry: 'DRAINAGE', deviceCount: 3 },
- ],
- mapFilter: {},
- excluded: {},
- freshness: {},
- })
- const view = monitoringOverviewPanel(data, sv(data))
- audit('monitoring-overview', view, [
- { label: '设备类型分布', mode: 'RANK', counts: [5, 5, 3, 1], keys: ['TIE_A', 'TIE_B', 'MID', 'SMALL'] },
- ])
- }
- // ---- A2 运行监测 · 监测分布(片区/行业/设备类型降序;异常桶固定语义序)
- {
- const data = toMonitoringDistribution({
- districtDistribution: [
- { district: 0, pointCount: 1, deviceCount: 1 },
- { district: 1, pointCount: 4, deviceCount: 4 },
- { district: 2, pointCount: 2, deviceCount: 2 },
- ],
- industryDistribution: [
- { industry: 'GAS', pointCount: 3, deviceCount: 3 },
- { industry: 'WATER_SUPPLY', pointCount: 6, deviceCount: 6 },
- { industry: 'DRAINAGE', pointCount: 2, deviceCount: 2 },
- ],
- deviceTypeDistribution: [
- { deviceType: { code: 'A', name: '型A' }, industry: 'GAS', pointCount: 1, deviceCount: 1 },
- { deviceType: { code: 'B', name: '型B' }, industry: 'GAS', pointCount: 5, deviceCount: 5 },
- { deviceType: { code: 'C', name: '型C' }, industry: 'GAS', pointCount: 3, deviceCount: 3 },
- ],
- pointStatusDistribution: [],
- monitoringPoints: [],
- unassociatedEquipment: [],
- pagination: { page: 1, pageSize: 20, total: 0, totalPages: 0, hasMore: false },
- mapFilter: {},
- freshness: {},
- excluded: {},
- })
- const view = monitoringDistributionPanel(data, sv(data), {
- statusDistribution: { available: true, counts: { ALARM: 9, WARNING: 2, OFFLINE: 1, EMPTY: 4 } },
- selectedStatuses: [],
- })
- audit('monitoring-distribution', view, [
- { label: '覆盖情况 · 行业', mode: 'RANK', counts: [6, 3, 2] },
- { label: '覆盖情况 · 片区', mode: 'RANK', counts: [4, 2, 1] },
- { label: '覆盖情况 · 设备类型', mode: 'RANK', counts: [5, 3, 1] },
- // 异常桶=状态语义序(ALARM→WARNING→OFFLINE→EMPTY),不许被降序打乱。
- { label: '监测点异常统计(全局未筛)', mode: 'FIXED', counts: [9, 2, 1, 4] },
- ])
- }
- // ---- A3 运行监测 · 运行概况(指标五桶与状态分布都是字典固定序)
- {
- const data = toMonitoringSituation({
- // 故意乱序灌入:适配层按六态字典序重建,视图必须原样带出。
- metricStatusDistribution: [
- { status: 'OFFLINE', count: 8 },
- { status: 'WARNING', count: 7 },
- { status: 'NORMAL', count: 1 },
- { status: 'ALARM', count: 3 },
- { status: 'EXPIRED', count: 2 },
- { status: 'UNKNOWN', count: 0 },
- ],
- abnormalEquipment: [],
- warningTrend: [],
- alarmTrend: [],
- pagination: { total: 0 },
- freshness: {},
- excluded: {},
- })
- const view = operationOverviewPanel(data, sv(data))
- audit('monitoring-situation', view, [
- { label: '设备/指标异常统计', mode: 'FIXED', counts: [7, 3, 2, 8, 0] },
- { label: '异常态势 · 指标状态分布', mode: 'FIXED', counts: [1, 7, 3, 2, 8, 0] },
- ])
- }
- // ---- A4 总体概览(运行监测类型分布 + 基础设施行业构成/片区分布 + 态势总览无分布块)
- {
- const monitoring = toOverviewMonitoring({
- counts: { deviceCount: 11, monitoringPointCount: 6, monitoringFacilityCount: 3, infrastructureFacilityCount: 5 },
- status: { online: 8, offline: 1, stale: 1, unknownOrMissing: 1, denominator: 11 },
- onlineRate: { value: '0.7273', onlineCount: 8, denominator: 11, state: 'AVAILABLE' },
- formalScope: { industries: ['GAS'] },
- metricFreshness: {},
- typeDistribution: [
- { industry: { code: 'GAS', label: '燃气' }, deviceType: { code: 'A', name: '型A' }, deviceCount: 1 },
- { industry: { code: 'GAS', label: '燃气' }, deviceType: { code: 'B', name: '型B' }, deviceCount: 6 },
- { industry: { code: 'WATER_SUPPLY', label: '供水' }, deviceType: { code: 'C', name: '型C' }, deviceCount: 4 },
- ],
- excluded: {},
- })
- const infra = toOverviewInfrastructure({
- totals: { facilityCount: 9, smartDeviceCount: 11 },
- industries: [
- { code: 'DRAINAGE', label: '排水', key: 'drainage', facilityCount: 1, smartDeviceCount: 2 },
- { code: 'GAS', label: '燃气', key: 'gas', facilityCount: 5, smartDeviceCount: 4 },
- { code: 'WATER_SUPPLY', label: '供水', key: 'water', facilityCount: 3, smartDeviceCount: 9 },
- ],
- excluded: {},
- })
- const regions = toOverviewRegions({
- totals: { facilityCount: 9, lengthKm: 12.4, scale: 'medium' },
- districtDistribution: [
- { district: 'UNASSIGNED', count: 1 },
- { district: 2, count: 4 },
- { district: 0, count: 3 },
- ],
- ageReference: {},
- excluded: {},
- })
- audit('overview-monitoring', monitoringSummaryPanel(monitoring, sv(monitoring)), [
- { label: '类型分布 · 智能设备', mode: 'RANK', counts: [6, 4, 1] },
- ])
- audit('overview-infrastructure', infrastructureSummaryPanel({ infrastructure: infra, regions }, sv(infra)), [
- { label: '行业构成 · 设施', mode: 'RANK', counts: [5, 3, 1] },
- { label: '行业构成 · 智能设备', mode: 'RANK', counts: [9, 4, 2] },
- { label: '片区分布', mode: 'RANK', counts: [4, 3, 1] },
- ])
- // 态势总览没有分布块(KPI+趋势);空期望表 = 出现任何未声明的分布块即红。
- audit('overview-situation', situationSummaryPanel({ infrastructure: infra, monitoring, warningSettings: null }, sv(null)), [])
- }
- // ---- A5 总体概览 · 预警设置(票 42 生命周期四块:状态/等级固定序,行业/片区降序)
- {
- const data = toOverviewWarningSettings({
- warning: {
- eventFamily: 'WARNING',
- state: 'READY',
- total: 31,
- handled: null,
- pending: null,
- cancelledCount: null,
- unlinkedEventCount: null,
- workOrderStates: null,
- handlingEfficiency: { value: '0.6129', state: 'AVAILABLE', numerator: 19, denominator: 31 },
- statusDistribution: [
- { code: 'UNHANDLED', label: '待处置', count: 12 },
- { code: 'HANDLED', label: '已解除', count: 19 },
- ],
- levelDistribution: [
- { code: '1', label: '1 级', count: 2 },
- { code: '2', label: '2 级', count: 9 },
- ],
- industryDistribution: [
- { code: 'GAS', label: '燃气', count: 4 },
- { code: 'DRAINAGE', label: '排水', count: 17 },
- { code: 'WATER_SUPPLY', label: '供水', count: 10 },
- ],
- districtDistribution: [
- { code: '0', label: '城南', count: 1 },
- { code: '1', label: '城北', count: 20 },
- ],
- trend: [],
- },
- alarm: {
- eventFamily: 'ALARM',
- state: 'READY',
- total: 66,
- handled: 40,
- pending: 20,
- cancelledCount: 3,
- unlinkedEventCount: 3,
- workOrderStates: { 0: 5, 1: 4, 2: 6, 3: 7, 4: 8, 5: 40, 6: 2, 7: 3 },
- handlingEfficiency: { value: '0.6061', state: 'AVAILABLE', numerator: 40, denominator: 66 },
- statusDistribution: [
- { code: 'UNHANDLED', label: '未处理', count: 26 },
- { code: 'HANDLED', label: '已处理', count: 40 },
- ],
- levelDistribution: [
- { code: '1', label: '1 级', count: 26 },
- { code: '2', label: '2 级', count: 40 },
- ],
- industryDistribution: [
- { code: 'GAS', label: '燃气', count: 26 },
- { code: 'DRAINAGE', label: '排水', count: 40 },
- ],
- districtDistribution: [
- { code: '0', label: '城南', count: 6 },
- { code: '1', label: '城北', count: 60 },
- ],
- trend: [],
- },
- freshness: {},
- range: { type: 'LAST_7_DAYS' },
- })
- const view = alertSettingsPanel(data, sv(data))
- audit('alert-settings', view, [
- { label: '预警 · 生命周期状态', mode: 'FIXED', counts: [12, 19] },
- { label: '预警 · 等级分布', mode: 'FIXED', counts: [2, 9] },
- { label: '预警 · 行业分布', mode: 'RANK', counts: [17, 10, 4] },
- { label: '预警 · 片区分布', mode: 'RANK', counts: [20, 1] },
- { label: '报警 · 工单状态分布', mode: 'FIXED', counts: [5, 4, 6, 7, 8, 40, 2, 3] },
- { label: '报警 · 生命周期状态', mode: 'FIXED', counts: [26, 40] },
- { label: '报警 · 等级分布', mode: 'FIXED', counts: [26, 40] },
- { label: '报警 · 行业分布', mode: 'RANK', counts: [40, 26] },
- { label: '报警 · 片区分布', mode: 'RANK', counts: [60, 6] },
- ])
- }
- // ---- A6 预警处置 · 预警分析(类型/行业/片区降序;等级是语义阶梯固定序)
- {
- const data = toEventAnalysis({
- dimensions: [
- { dimensionCode: 'EVENT_TYPE', buckets: [
- { bucketCode: 'T1', bucketLabel: '类型甲', count: 2 },
- { bucketCode: 'T2', bucketLabel: '类型乙', count: 8 },
- { bucketCode: 'T3', bucketLabel: '类型丙', count: 5 },
- ] },
- { dimensionCode: 'LEVEL', buckets: [
- { bucketCode: '1', bucketLabel: '1 级', count: 1 },
- { bucketCode: '2', bucketLabel: '2 级', count: 9 },
- ] },
- { dimensionCode: 'INDUSTRY', buckets: [
- { bucketCode: 'GAS', bucketLabel: '燃气', count: 3 },
- { bucketCode: 'DRAINAGE', bucketLabel: '排水', count: 11 },
- ] },
- { dimensionCode: 'DISTRICT', buckets: [
- { bucketCode: '0', bucketLabel: '城南', count: 7 },
- { bucketCode: '1', bucketLabel: '城北', count: 4 },
- ] },
- ],
- highlights: [],
- })
- audit('event-analysis', eventAnalysisPanel(data, sv(data)), [
- { label: '按事件类型', mode: 'RANK', counts: [8, 5, 2] },
- { label: '按行业分布', mode: 'RANK', counts: [11, 3] },
- { label: '按事件等级', mode: 'FIXED', counts: [1, 9] },
- { label: '按片区分布', mode: 'RANK', counts: [7, 4] },
- ])
- }
- // ---- A7 基础设施 · 基础档案(行业/片区降序;建成年限是时间轴分桶固定序)
- {
- const data = toBaseArchives({
- totals: { facilityCount: 9, lengthKm: 12.4, scale: 'medium' },
- industryDistribution: [
- { code: 'MANHOLE', count: 1 },
- { code: 'GAS', count: 6 },
- { code: 'DRAINAGE', count: 3 },
- ],
- districtDistribution: [
- { district: 2, count: 2 },
- { district: 0, count: 5 },
- ],
- ageDistribution: [
- { code: '20_YEARS_OR_OVER', count: 9 },
- { code: 'UNDER_5_YEARS', count: 1 },
- { code: '10_TO_20_YEARS', count: 4 },
- { code: '5_TO_10_YEARS', count: 2 },
- ],
- ageReference: {},
- excluded: {},
- })
- audit('base-archives', archivePanel(data, sv(data)), [
- { label: '行业分类', mode: 'RANK', counts: [6, 3, 1] },
- { label: '片区分布', mode: 'RANK', counts: [5, 2] },
- { label: '档案摘要 · 建成年限', mode: 'FIXED', counts: [9, 1, 4, 2] },
- ])
- }
- // ---- A8 基础设施 · 应急保障(区域分布降序)
- {
- const data = toEmergencySupport({
- totals: { hazardSourceCount: 3, protectionTargetCount: 2, emergencyResourceCount: 1, totalObjectCount: 6 },
- sections: {
- hazardSources: { state: 'EMPTY', items: [] },
- protectionTargets: { state: 'EMPTY', items: [] },
- emergencyResources: { state: 'EMPTY', items: [] },
- },
- industryDistribution: [{ industry: 'GAS', count: 1 }],
- areaDistribution: [
- { area: '城东', count: 2 },
- { area: '城南', count: 9 },
- { area: '城北', count: 5 },
- ],
- eventSummary: {},
- })
- audit('emergency', emergencyPanel(data, sv(data)), [
- { label: '区域分布', mode: 'RANK', counts: [9, 5, 2] },
- ])
- }
- // ---- A9 预警处置 · 预警效率(工单 0–7 全铺字典序,计数最大不得置顶)
- {
- const data = toHandlingEfficiency({
- handlingEfficiency: { value: '0.5', state: 'AVAILABLE', numerator: 3, denominator: 6 },
- workOrderStateCounts: [
- { stateCode: '1', count: 9 },
- { stateCode: '0', count: 1 },
- { stateCode: '5', count: 3 },
- ],
- trend: [],
- })
- audit('response-efficiency', responseEfficiencyPanel(data, sv(data)), [
- { label: '工单状态分布', mode: 'FIXED', counts: [1, 9, 0, 0, 0, 3, 0, 0] },
- ])
- }
- // ---- A10 无分布块的面板也进普查:期望表为空,长出未声明的分布块即红(完备性反向锁定)
- {
- const history = historyAnalysisPanel(toEventHistory({ data: [], trend: [] }), sv(null))
- audit('history-analysis', history, [])
- const management = managementPanel(
- { facilityCount: 9, equipmentCount: 11, districtIndustry: [{ district: 0, industry: 'GAS', facilityCount: 5, equipmentCount: 2 }] },
- sv(null),
- )
- audit('management', management, [])
- // 运行信息:KPI + 指标清单(票 38 才把它图表化);现在长出分布块就是未声明的改动。
- const operation = operationPanel(
- toOperatingInformation({ industries: [{ code: 'GAS', label: '燃气', metrics: [] }] }),
- sv(null),
- )
- audit('operation', operation, [])
- }
- // ================================================================ B. 并列稳定性 + 视觉密度守卫
- {
- // B1 并列保序由代码结构保证(票 37 条文在本票复验):全并列原样带出、与码值无关。
- const rows = [{ code: 'A', count: 5 }, { code: 'B', count: 5 }, { code: 'C', count: 5 }]
- assert.deepEqual(sortByCountDesc(rows, (r) => r.count).map((r) => r.code), ['A', 'B', 'C'], '全并列必须原样带出')
- const rev = [{ code: 'C', count: 5 }, { code: 'B', count: 5 }, { code: 'A', count: 5 }]
- assert.deepEqual(sortByCountDesc(rev, (r) => r.count).map((r) => r.code), ['C', 'B', 'A'], '并列次序跟传入,不跟码值')
- check('B1-tie-stability-by-structure', true, '并列保序')
- // B2 并列行条长相同(视觉可比较);全 0 不产生 NaN。
- const block = barsBlock('x', [{ label: '甲', count: 5 }, { label: '乙', count: 5 }, { label: '丙', count: 1 }])
- check('B2-tie-ratio-equal', block.rows[0].ratio === block.rows[1].ratio && block.rows[0].ratio === 1, '并列计数得到相同条长,最大项为 1')
- const zero = barsBlock('x', [{ label: '甲', count: 0 }])
- check('B2b-all-zero-no-nan', Number.isFinite(zero.rows[0].ratio) && zero.rows[0].ratio === 0, '全 0 时条长 0,不得 NaN')
- }
- {
- const panelVue = read('components/PanelBlocks.vue')
- /** 取某选择器的声明块(简单括号配对;找不到回空串,断言会因此判红而不是漏判)。 */
- const ruleOf = (selector) => {
- const at = panelVue.indexOf(`\n${selector} {`)
- if (at < 0) return ''
- const open = panelVue.indexOf('{', at)
- let depth = 0
- for (let i = open; i < panelVue.length; i += 1) {
- if (panelVue[i] === '{') depth += 1
- else if (panelVue[i] === '}') {
- depth -= 1
- if (depth === 0) return panelVue.slice(open + 1, i)
- }
- }
- return ''
- }
- // B3 每行文字与统计条更紧密:列间距 8u(--space-2) → 4u,且不再引用宽间距 token。
- const barRule = ruleOf('.panel-bar')
- check('B3-bar-row-tightened', /gap:\s*calc\(4 \* var\(--u/.test(barRule) && !/gap:\s*var\(--space-2/.test(barRule),
- `条形行的列间距必须收到 4u(文字与条贴合),当前:${barRule.trim() || '(找不到 .panel-bar 规则)'}`)
- // B4 条段之间有明确分割:轨道内发丝描边(起止容器)+ 填充右端头帽与顶部高光(装饰层不挡悬停)。
- const trackRule = ruleOf('.panel-bar__track')
- const fillRule = ruleOf('.panel-bar__fill')
- const capRule = ruleOf('.panel-bar__fill::after')
- check('B4-track-hairline-boundary', /box-shadow:\s*inset 0 0 0 1px/.test(trackRule), '轨道必须有 1u 内描边,条段才看得出容器边界')
- check('B4b-fill-cap-and-layer', /inset -1px 0 0/.test(capRule) && /linear-gradient\(180deg/.test(capRule) && /pointer-events:\s*none/.test(capRule),
- '填充需要右端头帽 + 顶部高光装饰层(轻量层次),且不挡悬停')
- check('B4c-fill-is-positioning-parent', /position:\s*relative/.test(fillRule), '装饰层贴填充本体,填充需为定位父级')
- // B5 越界辉光删除:旧 `0 0 8px` 渗过 2u 行距糊进相邻条,正是「条段无分割」的直接成因。
- check('B5-no-bleeding-glow', !/box-shadow:\s*0 0 8px/.test(fillRule), '条形填充不得再带 8px 外辉光(跨行渗色 = 分割不清)')
- // B6 读数密度与可读性不牺牲:读数列仍右对齐等宽;行距维持 2u(收紧的是行内)。
- const countRule = ruleOf('.panel-bar__count')
- const barsRule = ruleOf('.panel-bars')
- check('B6-reading-density-kept', /text-align:\s*right/.test(countRule) && /font-family:\s*var\(--num/.test(countRule), '读数列保持右对齐等宽数字')
- check('B6b-row-pitch-unchanged', /gap:\s*calc\(2 \* var\(--u/.test(barsRule), '行间距维持 2u(不把行堆挤)')
- check('B6c-track-height-6u', /height:\s*calc\(6 \* var\(--u/.test(trackRule), '轨道 5u→6u 承载描边与端帽,但不越过读数行高')
- // B7 三条接缝原样(票 37 收口条件:EllipsisText / panel-action / data-bar-* 不变)。
- check('B7-ellipsis-seam-intact', /<EllipsisText class="panel-bar__label" :text="row\.label" \/>/.test(panelVue), '条形行标签必须仍走共用省略方法')
- check('B7b-panel-action-seam-intact', /emit\('panel-action', \{ kind: block\.action, key: row\.actionKey \}\)/.test(panelVue), 'panel-action 仍只转发 actionKey(视觉改动不碰语义接缝)')
- check('B7c-assertion-surface-intact', /data-bar="row\.key"/.test(panelVue) && /data-bar-count/.test(panelVue) && /data-bar-selected/.test(panelVue) && /data-bar-tone/.test(panelVue), '走查断言面 data-bar-* 原样保留')
- // B8 交互与既有横条图一致:悬停增亮保留;选中态不改条长口径。
- check('B8-hover-feedback-kept', /\.panel-bar:hover \.panel-bar__fill\s*\{[^}]*filter:\s*brightness/.test(panelVue), '条形悬停增亮必须保留')
- const activeRule = ruleOf('.panel-bar--button.is-active')
- check('B8b-selected-does-not-change-bar', activeRule.includes('background') && !/width/.test(activeRule), '选中态只给底色/边线,不得改条宽或计数呈现')
- // B9 省略声明仍只有一份(票 37 的 A4 不变量,视觉改动不许顺手加第二份)。
- check('B9-single-ellipsis-declaration', !/text-overflow/.test(panelVue), 'PanelBlocks.vue 不得重新长出第二份 text-overflow')
- }
- console.log('')
- console.log(`T44 distribution ranking & visual density: ${failed ? `${failed} FAILED` : `${passed} passed`}`)
- if (failed) process.exit(1)
|