Regression / regression (push) Canceled after 0s
## 品牌标识(本次会话) 起因:品牌此前没有任何图形标识 —— 唯一 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 未做部分)
159 lines
6.7 KiB
JavaScript
159 lines
6.7 KiB
JavaScript
/* 通用合并脚本(保留在 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} 节`);
|