| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155 |
- // 依据维护者最终命名切图生成运行时 manifest(批次 1,第三轮)
- //
- // 素材全部由维护者从蓝湖导出并以中文语义命名,直接放在
- // public/cockpit/assets/icons/ 下。映射原则:**一个文件一个 key**(按文件语义),
- // 多个消费方(面板/行业/chip)经 domain/icons.js 的注册表共享同一 key。
- // 路径保留原中文(浏览器会自动编码请求 URL;manifest 内不做 encode,磁盘校验才对得上)。
- import { readFileSync, writeFileSync, existsSync } from 'node:fs'
- import { createHash } from 'node:crypto'
- import { join } from 'node:path'
- const publicDir = 'public/cockpit/assets'
- const ICON_MAP = {
- // ---- 顶栏品牌位 ----
- 'icon.brand': { file: 'brand-title.png', frame: { width: 470, height: 114 }, meaning: '品牌主标题(城市主生命线驾驶舱系统 + 光带)', core: true },
- // 票 17 第六轮:品牌区装饰素材(蓝湖 D2 Group 5061 的「光」层与 header 底边光路)。
- // 标题改 HTML 大字后,光带/光路单独消费,避免整块切图缩放导致文字过小。
- 'icon.brand.glow': { file: 'brand-glow.png', frame: { width: 442, height: 70 }, meaning: '品牌标题下方光带(蓝湖「光」层 221×35@2x,实测亮线在高度 44%)' },
- 'icon.brand.glow.path': { file: 'brand-glow-path.png', frame: { width: 2302, height: 44 }, meaning: '顶栏底部分隔光路(左段高位、25%–59% 处下沉的折线光线)' },
- // 票 17 第七轮:一级模式按钮选中态底板(蓝湖 D2 `nav/title-bg` 90×34@2x:顶部青色亮线 + 浅蓝渐变板)。
- 'icon.nav.active-bg': { file: 'title-select-bg.png', frame: { width: 180, height: 68 }, meaning: '模式按钮选中态底板(nav/title-bg,青色亮线 + 浅蓝渐变板)' },
- // 票 18(批次 5):主画板品牌标题后方「光池」——椭圆多层渐晕 + 中心亮斑的复杂位图光效,
- // 按规格 03「不可 CSS/SVG 无损复现才资源化」判据入库。设计稿 D6 realFrame 489×128,
- // 相对品牌标题左上 (-19,-1)。同源小档(brand-glow-1/3 = 300×80 / 442×116)无独立消费点,
- // 不入运行时(无死资源纪律),原文件移入设计归档区 unconsumed-decor/。
- 'icon.brand.pool': { file: 'brand-pool.png', frame: { width: 489, height: 128 }, meaning: '品牌标题后方光池(蓝湖「光」层 489×128@2x 位图,复杂光效不可 CSS 无损复现)' },
- // ---- 地图摘要四个 chip(维护者指派的语义对应)----
- 'icon.layers': { file: 'pipeline.png', frame: { width: 256, height: 256 }, meaning: '管线图层(管网设施)' },
- 'icon.points': { file: 'monitoring.png', frame: { width: 256, height: 256 }, meaning: '监测点(预警处置)' },
- 'icon.event': { file: 'emergency-event.png', frame: { width: 256, height: 256 }, meaning: '事件定位(应急指挥)' },
- 'icon.resource': { file: 'resource-asset.png', frame: { width: 256, height: 256 }, meaning: '应急资源(规模种类)' },
- // ---- 行业筛选 ----
- 'icon.industry.gas': { file: 'industry-gas.png', frame: { width: 15, height: 15 }, meaning: '燃气(火焰)' },
- 'icon.industry.water': { file: 'industry-water.png', frame: { width: 16, height: 15 }, meaning: '供水(水滴)' },
- 'icon.industry.drainage': { file: 'industry-drainage.png', frame: { width: 32, height: 32 }, meaning: '排水(水波)' },
- 'icon.manhole': { file: 'manhole.png', frame: { width: 15, height: 15 }, meaning: '窨井/井盖(紫色网格球)' },
- // ---- 面板小标题(第三轮:Texture/Icon 的 I/Pipe/Q 系列切图,去色 + 主蓝 #01abea 染色统一风格)----
- // 旧按文件语义的 8 个 key(equipment/heartbeat/alert/device/danger/history/check/list)
- // 已被 panel 系列取代并移除。同一源图在不同面板复用同一 key(PANEL_ICONS 里指向它),
- // 不为「同图不同面板」重复建 key——panel-infrastructure 等镜像文件已删,只留 11 个唯一图。
- 'icon.panel.situation': { file: 'panel/panel-situation.png', frame: { width: 256, height: 256 }, meaning: '态势总览/基础设施(设备机柜)' },
- 'icon.panel.monitoring': { file: 'panel/panel-monitoring.png', frame: { width: 256, height: 256 }, meaning: '运行监测/运行信息/监测概览(在线)' },
- 'icon.panel.alert': { file: 'panel/panel-alert.png', frame: { width: 256, height: 256 }, meaning: '预警设置(报警等级)' },
- 'icon.panel.archive': { file: 'panel/panel-archive.png', frame: { width: 256, height: 256 }, meaning: '基础档案/管理信息(设备开发)' },
- 'icon.panel.emergency': { file: 'panel/panel-emergency.png', frame: { width: 256, height: 256 }, meaning: '应急保障/当前预警(报警产生)' },
- 'icon.panel.distribution': { file: 'panel/panel-distribution.png', frame: { width: 256, height: 256 }, meaning: '监测分布(资源整合)' },
- 'icon.panel.ledger': { file: 'panel/panel-ledger.png', frame: { width: 256, height: 256 }, meaning: '监测点台账(设备)' },
- 'icon.panel.history': { file: 'panel/panel-history.png', frame: { width: 256, height: 256 }, meaning: '历史分析(时间)' },
- 'icon.panel.efficiency': { file: 'panel/panel-efficiency.png', frame: { width: 256, height: 256 }, meaning: '预警效率(协议实施)' },
- 'icon.panel.analysis': { file: 'panel/panel-analysis.png', frame: { width: 256, height: 256 }, meaning: '预警分析(规划设计)' },
- // ---- 状态标识(地图图例行,各状态用语义色染色:--lv1/lv2/lv3/lv4/neutral)----
- 'icon.status.alarm': { file: 'status/status-alarm.png', frame: { width: 256, height: 256 }, meaning: '报警(铃铛警示 @#ff3b52)' },
- 'icon.status.warning': { file: 'status/status-warning.png', frame: { width: 256, height: 256 }, meaning: '预警(警灯等级 @#ffb020)' },
- 'icon.status.workOrder': { file: 'status/status-workOrder.png', frame: { width: 256, height: 256 }, meaning: '工单处置(解除对勾 @#01abea)' },
- 'icon.status.normal': { file: 'status/status-normal.png', frame: { width: 256, height: 256 }, meaning: '正常(在线信号 @#27d17a)' },
- 'icon.status.offline': { file: 'status/status-offline.png', frame: { width: 256, height: 256 }, meaning: '离线(断线信号 @#5680b0)' },
- 'icon.status.empty': { file: 'status/status-empty.png', frame: { width: 256, height: 256 }, meaning: '无设备(设备机柜 @中性色)' },
- 'icon.status.unknown': { file: 'status/status-unknown.png', frame: { width: 256, height: 256 }, meaning: '状态未发布(问号铃 @中性色)' },
- 'icon.status.unassociated': { file: 'status/status-unassociated.png', frame: { width: 256, height: 256 }, meaning: '无关联设备(中性色)' },
- // ---- 位置符号(批次 3:双层水滴徽标,规格 10)----
- 'marker.master.solid': { file: 'T_MarkPoint_WhiteFull.png', frame: { width: 512, height: 512 }, meaning: '位置符号母版·实心(纯白剪影,监测点与关联设备)', core: true },
- 'marker.master.hollow': { file: 'T_MarkPoint_White.png', frame: { width: 512, height: 512 }, meaning: '位置符号母版·镂空(纯白剪影,EMPTY 点位与未关联设备)', core: true },
- 'marker.inner.gas': { file: 'markers/marker-gas.png', frame: { width: 256, height: 256 }, meaning: '内层行业图标·燃气' },
- 'marker.inner.water': { file: 'markers/marker-water.png', frame: { width: 256, height: 256 }, meaning: '内层行业图标·供水' },
- 'marker.inner.drainage': { file: 'markers/marker-drainage.png', frame: { width: 256, height: 256 }, meaning: '内层行业图标·排水' },
- 'marker.inner.manhole': { file: 'markers/marker-manhole.png', frame: { width: 256, height: 256 }, meaning: '内层行业图标·窨井' },
- }
- const RELEASE = '2026.10-lanhu-final-icons'
- const entries = []
- for (const [key, cfg] of Object.entries(ICON_MAP)) {
- const abs = join(publicDir, 'icons', cfg.file)
- if (!existsSync(abs)) throw new Error(`切图缺失: ${abs}`)
- const buf = readFileSync(abs)
- entries.push({
- key,
- // path 相对 public/cockpit(解析层拼 `${BASE_URL}cockpit/${path}`),保留中文原名。
- path: `assets/icons/${cfg.file}`,
- source: { board: null, delivery: 'user-imported-lanhu-slices', layer: cfg.file, meaning: cfg.meaning },
- format: 'png',
- dimensions: cfg.frame,
- pixelRatio: 1,
- transparent: true,
- renderMode: 'image',
- version: RELEASE,
- hash: `sha256:${createHash('sha256').update(buf).digest('hex')}`,
- status: 'formal',
- appliesTo: { areas: [key], states: ['default'], meaning: cfg.meaning },
- fallbackKey: null,
- core: key === 'icon.brand',
- })
- console.log(` ${key} | ${cfg.file}`)
- }
- // blank:设计稿未覆盖,保留原占位几何(temporary)
- const blankBuf = readFileSync(join(publicDir, 'icons/blank.svg'))
- entries.push({
- key: 'icon.blank',
- path: 'assets/icons/blank.svg',
- source: { board: null, delivery: 'placeholder-migrated-from-inline', layer: 'blank.svg', meaning: '空面板占位图示(设计稿未覆盖)' },
- format: 'svg',
- dimensions: { width: 48, height: 32 },
- pixelRatio: 1,
- transparent: true,
- renderMode: 'mask',
- version: RELEASE,
- hash: `sha256:${createHash('sha256').update(blankBuf).digest('hex')}`,
- status: 'temporary',
- appliesTo: { areas: ['panel-placeholder'], states: ['default'], meaning: '空面板占位图示(设计稿未覆盖)' },
- fallbackKey: null,
- core: false,
- })
- // 保留已存在的 fonts 块(批次 4 / 规格 11):本脚本只负责图标 entries,但整文件重写会把
- // 其他批次登记的顶层块一起抹掉。2026-10-13 实测踩过一次——重跑本脚本后字体授权门禁
- // 拿不到 fonts 块,浏览器静默回退系统字体(页面"看起来没异常"但字体已失效)。
- // 因此重写前先读回旧 manifest,把未知顶层键原样保留,只替换 entries/release/source。
- // 票 18(批次 5)加固:**entries 内未知 key 同样保留**——其他批次/会话(如票 37 的
- // icon.kpi.*)可能已把条目写进 manifest 而 ICON_MAP 未同步;直接重建会抹掉它们,
- // 与 fonts 块被抹是同一类事故。闸门:遇未知条目即中止,先同步 ICON_MAP 再重跑。
- const manifestPath = join(publicDir, 'manifest.json')
- let previous = {}
- try {
- previous = JSON.parse(readFileSync(manifestPath, 'utf8'))
- } catch {
- previous = {}
- }
- const PRESERVED_TOP_LEVEL = Object.fromEntries(
- Object.entries(previous).filter(([key]) => !['schemaVersion', 'release', 'source', 'entries'].includes(key))
- )
- const knownKeys = new Set(Object.keys(ICON_MAP).concat(['icon.blank']))
- const foreignEntries = (previous.entries || []).filter((e) => e && e.key && !knownKeys.has(e.key))
- if (foreignEntries.length) {
- console.error(
- `中止:manifest 中存在本脚本未登记的条目(${foreignEntries.map((e) => e.key).join(', ')})。` +
- '整文件重建会抹掉它们(fonts 块同类事故)。请先把对应映射补进 ICON_MAP(含 frame/meaning),再重跑本脚本。'
- )
- process.exit(1)
- }
- writeFileSync(manifestPath, JSON.stringify({
- schemaVersion: 1,
- release: RELEASE,
- source: { project: '城市生命线驾驶舱', importedBy: '维护者(蓝湖切图全量导入并按语义命名)' },
- ...PRESERVED_TOP_LEVEL,
- entries,
- }, null, 2) + '\n')
- const preservedKeys = Object.keys(PRESERVED_TOP_LEVEL)
- console.log(`\nmanifest 写入完成:${entries.length} 条,release ${RELEASE}` + (preservedKeys.length ? `;保留顶层块:${preservedKeys.join(', ')}` : ''))
|