Files
aurora-admin/tools/merge-mobile-batch.mjs
aurora-admin f1fbfc2ddb
Regression / regression (push) Canceled after 0s
feat(品牌标识): 几何 K 图标(favicon/顶栏标记/theme-color) + 并行会话成果入库
## 品牌标识(本次会话)

起因:品牌此前没有任何图形标识 —— 唯一 favicon 是内联 data-URI 里的字母「A」,
那是 v2.0.0「Aurora Admin → Kole UI」改名漏掉的一处(PC 顶栏也是「A」,
移动端站已是「K」;移动端文档站则完全没有 favicon)。

- 几何:24 网格三个互不接触的笔画(竖 + 两斜),圆头描边;
  描边 2.25 → 16px 标签页尺寸下正好 1.5px = 规范原文「描边1.5px」
- 取色分两套(刻意):favicon 硬编码品牌蓝/白(渲染在浏览器标签栏,不继承 kole-dark);
  顶栏标记走 currentColor(实测暗色下自动转 rgb(20,22,28))
- 新增 theme-color 双条(light #FFFFFF / dark #1C1F26,取 --kole-color-card-bg)
- 修 site/app.js hero 标语 KOLE ADMIN → KOLE UI(改名变形残留)
- 移动端 7 个模板补 favicon(此前计数 0)

验收:门禁 9 条全 OK(site-routing/site-routes/mobile-docs/mobile-site/isolation/
theme/nav/i18n/icons);PC 回归 1464/1464 · 移动端 807/807,各连跑 8 次一致;
两端 favicon 405 字节逐字节一致;PC 站控制台错误 1→0。

## 并行会话成果(本次一并入库)

- 图标系统:2576 图标(TDesign/Element Plus,MIT)+ 11 端注入 + 5 个构建门禁工具
  + IconPreview 预览页 + ICON-SPEC.md 冻结规格
- 移动端平台:47 组件 × 6 端 + 文档站 53 页 + 隔离门禁
- PC 组件:103 个大后台组件 / 组件11 批次
- uni-app:PC 端试点 + 移动端端实现 + 真实编译验证

## 工程

- .gitignore 补 .scratch/ 与 .zcode-preexisting-*.txt(会话中间产物,实测 9.1MB,不入库)
- CHANGELOG 补品牌标识条目
- ROADMAP 登 S8-P4(品牌标识任务包 + og:image/apple-touch-icon 未做部分)
2026-09-21 10:05:48 +08:00

159 lines
6.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 通用合并脚本(保留在 tools/ 供后续批次复用):
* node tools/merge-mobile-batch.mjs # 合并 spec/parts/*.md + 未登记的 frameworks-mobile 组件进索引
*
* 做三件事,全部幂等:
* 1) 把 spec/parts/*.md 的规格片段按编号拼进 spec/移动端规格.md(已存在同编号则跳过)
* 2) 把 frameworks-mobile 里**未登记**的组件写进 components/index.json
* (slug 缺省 = kebab-case(prefix),可用 --slug-prefix 调整;分类由 --category 指定或按启发式)
* 3) 已拼接的片段移入 spec/parts/_merged/,避免下次重复拼接
*
* 为什么需要它:子 agent 只写自己的文件(不改 index.json,避免并发写同一文件),
* 合并由主 agent 统一做;但每个批次都手写一遍合并脚本容易漏步骤(上一轮就漏了「已存在同编号」的幂等判断)。
* 用法:批次完成后 `node tools/merge-mobile-batch.mjs`,再跑 build + 门禁 + 回归。
*/
import { readFileSync, writeFileSync, readdirSync, existsSync, mkdirSync, renameSync, statSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const LIB = join(ROOT, '.design_library', 'kole-ui-mobile');
const PARTS = join(LIB, 'spec', 'parts');
const SPEC = join(LIB, 'spec', '移动端规格.md');
const INDEX = join(LIB, 'components', 'index.json');
const SRC = join(ROOT, 'frameworks-mobile');
const arg = (k, d) => {
const i = process.argv.indexOf(k);
return i > -1 ? process.argv[i + 1] : d;
};
const CATEGORY_HINT = {
input: ['input', 'search', 'textarea', 'stepper', 'switch', 'numberkeyboard', 'datepicker', 'picker', 'radio', 'checkbox', 'slider', 'rate', 'upload', 'form'],
navigation: ['navbar', 'tabbar', 'grid', 'steps', 'tabs', 'drawer', 'indexes', 'sidebar', 'menu', 'fab', 'backtop', 'sticky', 'guide'],
display: ['cell', 'list', 'avatar', 'badge', 'tag', 'collapse', 'table', 'image', 'swiper', 'progress', 'countdown', 'qrcode', 'watermark', 'skeleton', 'typography', 'icon', 'link', 'layout'],
feedback: ['toast', 'dialog', 'popup', 'loading', 'noticebar', 'message', 'actionsheet', 'pullrefresh', 'swipecell', 'drawer', 'result', 'empty', 'overlay', 'progress'],
general: ['button', 'divider', 'config'],
};
function guessCategory(slug) {
const bare = slug.replace(/^mobile-/, '');
for (const [cat, keys] of Object.entries(CATEGORY_HINT)) {
if (keys.some((k) => bare === k)) return cat;
}
for (const [cat, keys] of Object.entries(CATEGORY_HINT)) {
if (keys.some((k) => bare.includes(k))) return cat;
}
return 'general';
}
/* ---------- 0. 磁盘扫描:哪些组件还没登记 ---------- */
const index = JSON.parse(readFileSync(INDEX, 'utf8'));
const haveSlug = new Set(index.components.map((c) => c.slug));
const havePrefix = new Set(index.components.map((c) => c.frameworksPrefix));
const ENDS = index.ends;
const SUFFIX = {
css: '.css',
html: '.html',
jsx: '.jsx',
vue2: '.vue2.vue',
vue3: '.vue3.vue',
uniapp: '.uniapp.vue',
};
const prefixesOnDisk = [...new Set(readdirSync(SRC).map((f) => f.split('.')[0]))];
const unregistered = prefixesOnDisk.filter((p) => !havePrefix.has(p));
/* ---------- 1. 规格片段拼接 ---------- */
const partFiles = existsSync(PARTS)
? readdirSync(PARTS).filter((f) => /\.md$/.test(f) && !f.startsWith('_'))
: [];
const parts = partFiles
.map((f) => {
const text = readFileSync(join(PARTS, f), 'utf8').trim();
const m = text.match(/^## (\d+) · /);
return { file: f, text, num: m ? Number(m[1]) : 999 };
})
.sort((a, b) => a.num - b.num);
let spec = readFileSync(SPEC, 'utf8').replace(/\s+$/, '');
let specAdded = 0;
for (const p of parts) {
if (new RegExp(`^## ${p.num} · `, 'm').test(spec)) {
console.log(`规格 §${p.num} 已存在,归档而不拼接(${p.file})`);
} else {
spec += `\n\n---\n\n${p.text}\n`;
specAdded++;
console.log(`规格 ← §${p.num}(${p.file})`);
}
}
if (specAdded) writeFileSync(SPEC, spec + '\n', 'utf8');
if (parts.length) {
const merged = join(PARTS, '_merged');
if (!existsSync(merged)) mkdirSync(merged, { recursive: true });
for (const p of parts) {
const from = join(PARTS, p.file);
if (existsSync(from)) renameSync(from, join(merged, p.file));
}
console.log(`parts 片段已归档 ${parts.length} 个 → spec/parts/_merged/`);
}
/* ---------- 2. 未登记组件写入索引 ---------- */
let added = 0;
const skipped = [];
for (const prefix of unregistered.sort()) {
/* slug:优先用契约里登记的(按 specSection 反查),否则 kebab-case(prefix) */
let slug = 'mobile-' + prefix.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
/* 若已有同名契约(例如 Cell.txt 场景),直接用契约文件名 */
const candidates = readdirSync(join(LIB, 'components'))
.filter((f) => f.endsWith('.json') && f !== 'index.json')
.map((f) => f.replace(/\.json$/, ''));
const bySection = candidates.find((s) => {
const ct = JSON.parse(readFileSync(join(LIB, 'components', s + '.json'), 'utf8'));
return new RegExp(`\\b${prefix}\\b`).test(String(ct.specSection || '')) && !haveSlug.has(s);
});
if (bySection) slug = bySection;
if (haveSlug.has(slug)) {
skipped.push(`${slug}(索引已存在)`);
continue;
}
const contractPath = join(LIB, 'components', slug + '.json');
if (!existsSync(contractPath)) {
skipped.push(`${prefix}(缺契约 ${slug}.json,跳过)`);
continue;
}
const files = {};
const missing = [];
for (const end of ENDS) {
const f = prefix + SUFFIX[end];
if (existsSync(join(SRC, f))) files[end] = f;
else missing.push(end);
}
if (missing.length) {
skipped.push(`${prefix}(缺 ${missing.join('/')} 端文件,跳过)`);
continue;
}
const ct = JSON.parse(readFileSync(contractPath, 'utf8'));
const category = arg('--category') || guessCategory(slug);
index.components.push({
slug,
name: ct.name,
tier: 'mobile',
category,
confidence: ct.confidence || 'high',
specSection: ct.specSection,
contract: 'components/' + slug + '.json',
frameworksPrefix: prefix,
files,
variantDimensions: (ct.variantDimensions || []).map((d) => d.name),
behaviors: [],
});
haveSlug.add(slug);
havePrefix.add(prefix);
added++;
console.log(`索引 ← ${slug}(${ct.name} · ${category} · prefix=${prefix} · ${Object.keys(files).length} 端)`);
}
if (added) {
writeFileSync(INDEX, JSON.stringify(index, null, 2) + '\n', 'utf8');
}
if (skipped.length) console.log('跳过:' + skipped.join('; '));
console.log(`索引组件数: ${index.components.length}(新增 ${added})· 规格新增 ${specAdded} 节`);