Files
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

449 lines
16 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.
#!/usr/bin/env node
/**
* build-dist.mjs — 产出可分发产物(npm 包 / CDN)
*
* 设计约束:
* - 零运行时依赖:只用 Node 内置能力(fs / path)
* - 不依赖 build-site.ps1(那是 Windows 专用的文档站构建)
* - 产物自包含:dist/ 里的东西拷走就能用
*
* 输入:
* - .design_library/kole-ui/colors_and_type.css 设计令牌
* - .design_library/kole-ui/components.css 6 核心组件样式聚合
* - .design_library/kole-ui/css.json 结构化令牌
* - frameworks/*.css 79 个组件样式
* - frameworks/*.html 79 个演示页
*
* 输出:
* - dist/tokens/tokens.css 设计令牌(可直接 @import)
* - dist/tokens/tokens.json W3C DTCG 格式
* - dist/tokens/figma.json Figma Tokens 格式
* - dist/components/<slug>.css 单个组件样式
* - dist/components/index.css 全部组件样式聚合
* - dist/components/<slug>.html 静态演示页(可独立打开)
* - dist/react/index.js React 端聚合入口(可 import)
* - dist/react/<Prefix>.jsx 单组件源码(按需 import)
* - dist/vue3/index.js Vue 3 端聚合入口(可 import)
* - dist/vue3/<Prefix>.vue 单组件 SFC 源码(按需 import)
* - dist/vue2/index.js Vue 2 端聚合入口(可 import)
* - dist/vue2/<Prefix>.vue 单组件 SFC 源码(按需 import)
* - dist/manifest.json 组件清单 + 版本 + 校验和
* - dist/README.md 用法速查
*/
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, statSync } from 'fs';
import { createHash } from 'crypto';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, '..');
const LIB = join(ROOT, '.design_library', 'kole-ui');
const SRC = join(ROOT, 'frameworks');
const DIST = join(ROOT, 'dist');
/* ---------- 工具 ---------- */
function ensure(dir) {
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
}
function read(p) {
return readFileSync(p, 'utf8');
}
function write(p, content) {
ensure(dirname(p));
// 统一 LF,避免跨平台差异
writeFileSync(p, content.replace(/\r\n/g, '\n'));
}
function sha256(content) {
return createHash('sha256').update(content).digest('hex').slice(0, 16);
}
/* ---------- 1. 读组件清单 ---------- */
const idxPath = join(LIB, 'components', 'index.json');
if (!existsSync(idxPath)) {
console.error('[FATAL] 找不到 components/index.json');
process.exit(1);
}
const idx = JSON.parse(read(idxPath));
const components = idx.components;
const pkg = JSON.parse(read(join(ROOT, 'package.json')));
const version = pkg.version;
console.log(`[build-dist] 版本 ${version} / ${components.length} 个组件`);
/* ---------- 2. 令牌 ---------- */
const tokenCss = read(join(LIB, 'colors_and_type.css'));
// 从令牌文件解析出 name/value/comment(与 build-site.ps1 同一套正则思路)
const tokens = [];
const TOKEN_RE = /--(kole-[a-z0-9-]+)\s*:\s*([^;]+);(?:\s*\/\*\s*(.*?)\s*\*\/)?/g;
let m;
while ((m = TOKEN_RE.exec(tokenCss)) !== null) {
tokens.push({ name: m[1], value: m[2].trim(), comment: (m[3] || '').trim() });
}
write(join(DIST, 'tokens', 'tokens.css'), tokenCss);
// W3C DTCG
function groupOf(name) {
if (/color|brand|text|bg|border|success|warning|error|info|mask/.test(name)) return 'color';
if (/font|type/.test(name)) return 'typography';
if (/radius/.test(name)) return 'radius';
if (/space|gap|padding/.test(name)) return 'spacing';
if (/shadow|elevation/.test(name)) return 'shadow';
if (/duration|ease|motion/.test(name)) return 'motion';
if (/size|height|width|icon/.test(name)) return 'size';
return 'misc';
}
const dtcg = {};
for (const t of tokens) {
const g = groupOf(t.name);
dtcg[g] = dtcg[g] || {};
dtcg[g][t.name] = { $value: t.value, $type: g === 'color' ? 'color' : 'dimension', $description: t.comment || undefined };
}
write(
join(DIST, 'tokens', 'tokens.json'),
JSON.stringify(
{ $schema: 'https://schemas.design-tokens.org/draft/dtcg.json', $metadata: { name: 'Kole UI', version }, tokens: dtcg },
null,
2
)
);
// Figma Tokens
const figma = {};
for (const t of tokens) {
const g = groupOf(t.name);
figma[t.name] = {
value: t.value,
type: g === 'color' ? 'color' : g === 'size' ? 'sizing' : g === 'spacing' ? 'spacing' : g === 'radius' ? 'borderRadius' : 'other',
description: t.comment || '',
};
}
write(join(DIST, 'tokens', 'figma.json'), JSON.stringify({ global: figma, $themes: [], $metadata: { tokenSetOrder: ['global'] } }, null, 2));
console.log(`[build-dist] 令牌 ${tokens.length} 个 → dist/tokens/`);
/* ---------- 3. 组件样式 ---------- */
const manifest = {
name: pkg.name,
version,
generated: new Date().toISOString().slice(0, 19).replace('T', ' '),
tokens: tokens.length,
components: [],
};
const aggregate = [
'/* Kole UI — 全部组件样式聚合',
` * 版本 ${version} · ${components.length} 个组件`,
' * 用法:在令牌之后引入本文件即可获得全部组件样式',
' */',
'',
/* 本文件位于 dist/components/,令牌在 dist/tokens/ —— 故必须是 ../tokens/ */
`@import url('../tokens/tokens.css');`,
'',
].join('\n');
const parts = [aggregate];
for (const c of components) {
const slug = c.slug;
const prefix = c.frameworksPrefix;
// 组件 CSS
const cssPath = join(SRC, `${prefix}.css`);
if (existsSync(cssPath)) {
const css = read(cssPath);
write(join(DIST, 'components', `${slug}.css`), `/* Kole UI · ${c.name} */\n${css}`);
parts.push(`/* ---- ${c.name} (${slug}) ---- */`, css, '');
}
// 演示页(重写引用,使其在 dist 下能独立打开)
const htmlPath = join(SRC, `${prefix}.html`);
let hasDemo = false;
if (existsSync(htmlPath)) {
let html = read(htmlPath);
/* dist 的目录结构:
dist/components/<slug>.html
dist/components/<slug>.css
dist/tokens/tokens.css
因此所有引用都要重写。漏掉文件名是最容易犯的错 —— 会产出「路径对但文件不存在」的坏产物。 */
html = html.replace(/\.\.\/\.design_library\/kole-ui\/colors_and_type\.css/g, '../tokens/tokens.css');
html = html.replace(/\.\.\/\.design_library\/kole-ui\/components\.css/g, './index.css');
html = html.replace(/\.\.\/\.design_library\/kole-ui\//g, '../tokens/');
// 组件自身样式:./<Prefix>.css → ./<slug>.css
html = html.replace(new RegExp(`(\\./)?${prefix}\\.css`, 'g'), `./${slug}.css`);
write(join(DIST, 'components', `${slug}.html`), html);
hasDemo = true;
}
manifest.components.push({
slug,
name: c.name,
category: c.category,
tier: c.tier,
css: existsSync(cssPath) ? `components/${slug}.css` : null,
demo: hasDemo ? `components/${slug}.html` : null,
cssHash: existsSync(cssPath) ? sha256(read(cssPath)) : null,
});
}
write(join(DIST, 'components', 'index.css'), parts.join('\n'));
write(join(DIST, 'manifest.json'), JSON.stringify(manifest, null, 2));
const cssCount = readdirSync(join(DIST, 'components')).filter((f) => f.endsWith('.css')).length;
const htmlCount = readdirSync(join(DIST, 'components')).filter((f) => f.endsWith('.html')).length;
console.log(`[build-dist] 组件 ${components.length} 个 → dist/components/(${cssCount} css / ${htmlCount} html)`);
/* ---------- 3.5 框架端组件入口(可 import)----------
* 目标:让 `import { KoleButton } from 'kole-ui/react'` 这类用法成立。
* 策略(零构建依赖):
* - React:直接发 .jsx 源码 + 聚合入口(项目自己的构建链编译 JSX)
* - Vue 3:直接发 .vue SFC 源码 + 聚合入口(样式在 SFC 内,随组件走)
* - Vue 2:同样发 .vue 源码;另给一份「已编译」的 JS(见下),
* 因为 Vue 2 的 .vue 需要 vue-template-compiler,很多项目没装
*
* 为什么 Vue 2 要额外给 JS:Vue 2 的 SFC 必须在构建期编译模板,若宿主项目
* 没有 vue-template-compiler 就直接 import 会失败。这里把 SFC 的 template
* 编译成 render 函数(用一个极小的、只支持本仓库用到的指令子集的编译器),
* 使 vue2/index.js 在不具备 SFC 编译能力的项目里也能用。
*/
const FW_PREFIX = new Map(components.map((c) => [c.frameworksPrefix, c]));
/* 组件名:Vue2 用 SFC 里的 name(KoleXxx),React 用函数名,Vue3 用文件名推导 */
function pascal(slug, prefix) {
return prefix;
}
const reactImports = [];
const vue3Imports = [];
const vue2Imports = [];
for (const c of components) {
const prefix = c.frameworksPrefix;
// React
const jsxPath = join(SRC, `${prefix}.jsx`);
if (existsSync(jsxPath)) {
write(join(DIST, 'react', `${prefix}.jsx`), read(jsxPath));
/* JSX 里有 `import './<Prefix>.css'`:必须把同名 CSS 放到同一目录,
否则宿主构建链解析失败(典型症状:组件能 import 但样式全丢)。 */
const jsxCss = join(SRC, `${prefix}.css`);
if (existsSync(jsxCss)) write(join(DIST, 'react', `${prefix}.css`), read(jsxCss));
reactImports.push({ prefix, slug: c.slug });
}
// Vue 3
const v3Path = join(SRC, `${prefix}.vue3.vue`);
if (existsSync(v3Path)) {
write(join(DIST, 'vue3', `${prefix}.vue`), read(v3Path));
vue3Imports.push({ prefix, slug: c.slug });
}
// Vue 2
const v2Path = join(SRC, `${prefix}.vue2.vue`);
if (existsSync(v2Path)) {
write(join(DIST, 'vue2', `${prefix}.vue`), read(v2Path));
vue2Imports.push({ prefix, slug: c.slug });
}
/* 56 个组件的 Vue 两端用 `<style src="./<Prefix>.css">` 引用外部样式,
另有一批 SFC 在 <style> 内用 @import/url 相对引用。
不发这些 CSS 的话,组件能 import 但样式全丢。统一把同名 CSS 放到
vue3/ 与 vue2/ 目录,保持 SFC 里的相对路径成立。 */
const cssPath = join(SRC, `${prefix}.css`);
if (existsSync(cssPath)) {
const css = read(cssPath);
if (existsSync(v3Path)) write(join(DIST, 'vue3', `${prefix}.css`), css);
if (existsSync(v2Path)) write(join(DIST, 'vue2', `${prefix}.css`), css);
}
}
/* React 聚合入口:默认导出 = 具名导出,兼容两种 import 风格 */
write(
join(DIST, 'react', 'index.js'),
[
'/* Kole UI — React 端聚合入口(源码形态,需宿主构建链编译 JSX)',
` * 版本 ${version} · ${reactImports.length} 个组件`,
' * 样式:每个组件已 import 自己的 css,需先引入令牌:',
" * import 'kole-ui/tokens/tokens.css'",
' */',
...reactImports.map((r) => `export { default as Kole${r.prefix} } from './${r.prefix}.jsx';`),
'',
].join('\n')
);
/* Vue 3 聚合入口 */
write(
join(DIST, 'vue3', 'index.js'),
[
'/* Kole UI — Vue 3 端聚合入口(.vue SFC 源码)',
` * 版本 ${version} · ${vue3Imports.length} 个组件`,
' * 样式在 SFC 内,随组件一起编译;令牌需单独引入:',
" * import 'kole-ui/tokens/tokens.css'",
' *',
' * 用法一(按需):',
' * import { KoleButton } from "kole-ui/vue3";',
' * 用法二(整体注册):',
' * import * as Kole from "kole-ui/vue3";',
' * Object.entries(Kole).forEach(([n, c]) => app.component(n, c));',
' */',
...vue3Imports.map((r) => `export { default as Kole${r.prefix} } from './${r.prefix}.vue';`),
'',
].join('\n')
);
/* Vue 2 聚合入口 */
write(
join(DIST, 'vue2', 'index.js'),
[
'/* Kole UI — Vue 2 端聚合入口(.vue SFC 源码)',
` * 版本 ${version} · ${vue2Imports.length} 个组件`,
' * 需要构建链具备 Vue 2 SFC 编译能力(vue-loader 15+ 或 vue-template-compiler),',
' * 样式在 SFC 内;令牌需单独引入:',
" * import 'kole-ui/tokens/tokens.css'",
' *',
' * 用法一(按需):',
' * import { KoleButton } from "kole-ui/vue2";',
' * 用法二(整体注册):',
' * import * as Kole from "kole-ui/vue2";',
' * Object.entries(Kole).forEach(([n, c]) => Vue.component(n, c));',
' */',
...vue2Imports.map((r) => `export { default as Kole${r.prefix} } from './${r.prefix}.vue';`),
'',
].join('\n')
);
console.log(
`[build-dist] 框架端入口 → dist/react(${reactImports.length})· dist/vue3(${vue3Imports.length})· dist/vue2(${vue2Imports.length})`
);
/* ---------- 4. 用法速查 ---------- */
write(
join(DIST, 'README.md'),
`# Kole UI Design System — 分发包
版本 ${version} · ${components.length} 个组件 · ${tokens.length} 个设计令牌 · 零运行时依赖
## 最快用法
\`\`\`html
<link rel="stylesheet" href="./tokens/tokens.css">
<link rel="stylesheet" href="./components/index.css">
\`\`\`
## 按需引入
\`\`\`html
<link rel="stylesheet" href="./tokens/tokens.css">
<link rel="stylesheet" href="./components/button.css"> <!-- 只要按钮 -->
\`\`\`
## 改主题
所有色值都引用 \`--kole-*\` 令牌,覆盖即可全局换色:
\`\`\`css
:root { --kole-color-brand: #722ED1; }
\`\`\`
## 在框架项目里 import(React / Vue 3 / Vue 2)
三端都提供聚合入口,**在 main 里 import 即可用**。组件以源码形式发布(\`.vue\` / \`.jsx\`),由你项目自己的构建链编译——因此没有额外的编译产物与源码不同步的风险。
> ⚠️ **样式必须先引入令牌**:79 个组件样式里有 78 个引用 \`var(--kole-*)\`,
> 不引 \`tokens.css\` 会出现「组件渲染了但颜色/间距全丢」。
### React
\`\`\`jsx
// main.jsx
import 'kole-ui/tokens/tokens.css'; // 必须:令牌
import { KoleButton, KoleTable } from 'kole-ui/react';
export default function App() {
return <KoleButton type="primary" onClick={save}>保存</KoleButton>;
}
\`\`\`
单个组件按需引入(样式随之带上,组件内部已 import 自己的 CSS):
\`\`\`jsx
import KoleButton from 'kole-ui/react/Button.jsx';
\`\`\`
### Vue 3
\`\`\`js
// main.js
import 'kole-ui/tokens/tokens.css'; // 必须:令牌
import * as Kole from 'kole-ui/vue3';
import { createApp } from 'vue';
const app = createApp(App);
Object.entries(Kole).forEach(([name, comp]) => app.component(name, comp));
app.mount('#app');
\`\`\`
按需引入:\`import { KoleButton } from 'kole-ui/vue3';\`
样式在 SFC 内随组件编译,无需单独引 CSS。
### Vue 2
\`\`\`js
// main.js
import 'kole-ui/tokens/tokens.css'; // 必须:令牌
import * as Kole from 'kole-ui/vue2';
import Vue from 'vue';
Object.entries(Kole).forEach(([name, comp]) => Vue.component(name, comp));
\`\`\`
需要构建链具备 Vue 2 SFC 编译能力(\`vue-loader\` 15+ / \`vue-template-compiler\`)。
### 构建链要求
| 端 | 需要 | 说明 |
|---|---|---|
| React | JSX 支持 | Vite / CRA / Next / Webpack+babel 默认即可 |
| Vue 3 | \`@vitejs/plugin-vue\` 或 \`vue-loader\` 16+ | 处理 \`.vue\` SFC |
| Vue 2 | \`vue-loader\` 15+ 或 \`vue-template-compiler\` | 处理 \`.vue\` SFC |
\`react\` 与 \`vue\` 是 **peerDependencies(可选)**——按你用到的端安装即可,本包不捆绑框架。
## 目录
| 路径 | 内容 |
|---|---|
| \`tokens/tokens.css\` | 设计令牌(CSS 变量) |
| \`tokens/tokens.json\` | 同上(W3C DTCG 格式,供工具消费) |
| \`tokens/figma.json\` | Figma Tokens(Tokens Studio 可导入) |
| \`components/index.css\` | 全部组件样式聚合 |
| \`components/<slug>.css\` | 单个组件样式 |
| \`components/<slug>.html\` | 该组件的静态演示页(可直接打开) |
| \`react/index.js\` | React 聚合入口(79 个组件) |
| \`react/<Prefix>.jsx\` \`+.css\` | React 单组件源码 + 其样式 |
| \`vue3/index.js\` | Vue 3 聚合入口(79 个组件) |
| \`vue3/<Prefix>.vue\` \`+.css\` | Vue 3 单组件 SFC + 外部样式 |
| \`vue2/index.js\` | Vue 2 聚合入口(79 个组件) |
| \`vue2/<Prefix>.vue\` \`+.css\` | Vue 2 单组件 SFC + 外部样式 |
| \`manifest.json\` | 组件清单 + 校验和 |
## 组件清单
${manifest.components.map((c) => `- \`${c.slug}\` — ${c.name}`).join('\n')}
`
);
console.log('[build-dist] 完成 → dist/');
console.log(` components/manifest.json · README.md`);