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

245 lines
9.4 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
/**
* verify-uniapp.mjs — uni-app 端的静态门禁(移动端 × uni-app + PC 端 × uni-app)
*
* 为什么是静态门禁而不是编译验证:
* uni-app 的真实编译需要 @dcloudio/vite-plugin-uni(npm 依赖),本仓库零运行时依赖,
* 且 CI 不装 uni-app 工具链。因此本脚本做**能在零依赖下做到的最强静态检查**:
* 1. SFC 三段结构完整(template / script / style)
* 2. <script> 段用 `node --check` 做真实语法校验(ESM 口径)
* 3. <template> 段做标签配平检查(识别自闭合)
* 4. 禁 DOM / 浏览器 API(document / window / localStorage / querySelector / PointerEvent)
* 5. 手势必须用 touch 事件(小程序与 App 端无 PointerEvent)
* 6. 只用 uni 基础组件(view / text / input / textarea / scroll-view / image …)
* 7. 类名与令牌前缀按平台隔离(移动端 kole-m- + --kole-m-;PC 端 kole- + --kole-)
* 8. 移动端用 rpx、PC 端不得用 rpx
*
* **未执行**:uni-app 真实编译(H5/小程序/App 三目标构建)。
* 复现命令(需联网装依赖,写在 ROADMAP S7-P25 的验收步骤里):
* npx degit dcloudio/uni-preset-vue#vite my-app && cd my-app && npm i \
* && cp -r <repo>/dist/mobile/uniapp node_modules/kole-ui-mobile/uniapp \
* && npm run build:h5
*
* 零依赖;退出码 0 = 全部通过。
*/
import { readFileSync, writeFileSync, mkdtempSync, existsSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { tmpdir } from 'node:os';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, '..');
const read = (p) => readFileSync(p, 'utf8').replace(/^\uFEFF/, '');
let pass = 0;
const failures = [];
function check(ok, id, detail) {
if (ok) {
pass++;
console.log(` PASS ${id}${detail ? ' — ' + detail : ''}`);
} else {
failures.push(id + (detail ? ' — ' + detail : ''));
console.log(` FAIL ${id}${detail ? ' — ' + detail : ''}`);
}
}
/* ---------- 待检清单 ---------- */
const targets = [];
const mbIndex = JSON.parse(read(join(ROOT, '.design_library', 'kole-ui-mobile', 'components', 'index.json')));
for (const c of mbIndex.components) {
const f = c.files && c.files.uniapp;
if (f) {
targets.push({
platform: 'mobile',
slug: c.slug,
path: join(ROOT, 'frameworks-mobile', f),
rel: `frameworks-mobile/${f}`,
targets: mbIndex.uniappTargets || [],
});
}
}
const pcIndexPath = join(ROOT, '.design_library', 'kole-ui-uniapp', 'index.json');
if (existsSync(pcIndexPath)) {
const ui = JSON.parse(read(pcIndexPath));
for (const c of ui.components) {
targets.push({
platform: 'pc',
slug: c.slug,
path: join(ROOT, 'frameworks-uniapp-pc', c.file),
rel: `frameworks-uniapp-pc/${c.file}`,
targets: ['h5'],
});
}
}
console.log('uni-app 端静态门禁');
console.log(`待检 SFC ${targets.length} 个(移动端 ${targets.filter((t) => t.platform === 'mobile').length} · PC ${targets.filter((t) => t.platform === 'pc').length})`);
const TMP = mkdtempSync(join(tmpdir(), 'kole-uniapp-'));
const UNI_PRIMITIVES = new Set([
'template', 'view', 'text', 'image', 'input', 'textarea', 'button', 'scroll-view', 'swiper', 'swiper-item',
'navigator', 'slot', 'checkbox', 'radio', 'switch', 'picker', 'progress', 'rich-text', 'cover-view',
]);
/* 注释剥离:DOM/单位这类检查必须只看**代码**,否则「注释里解释为什么不用 PointerEvent」
会被判成违规。行注释只剥离「行首(可含缩进)的 //」,避免误伤 https:// 这类值。 */
function stripComments(src) {
return src
.replace(/<!--[\s\S]*?-->/g, ' ')
.replace(/\/\*[\s\S]*?\*\//g, ' ')
.replace(/^[ \t]*\/\/.*$/gm, ' ');
}
/* ---------- 逐文件检查 ---------- */
const problems = {
exist: [],
sections: [],
syntax: [],
tags: [],
dom: [],
gesture: [],
primitives: [],
prefix: [],
units: [],
};
for (const t of targets) {
if (!existsSync(t.path)) {
problems.exist.push(t.rel);
continue;
}
const src = read(t.path);
/* 1. SFC 三段 */
const hasTpl = /<template>/.test(src);
const hasScript = /<script(\s|>)/.test(src);
const hasStyle = /<style(\s|>)/.test(src);
if (!hasTpl || !hasScript || !hasStyle) {
problems.sections.push(`${t.rel}(template=${hasTpl} script=${hasScript} style=${hasStyle})`);
}
/* 2. script 语法(真实 node --check) */
const scriptMatch = src.match(/<script[^>]*>([\s\S]*?)<\/script>/);
if (scriptMatch) {
const tmpFile = join(TMP, `${t.platform}-${t.slug}.mjs`);
writeFileSync(tmpFile, scriptMatch[1], 'utf8');
try {
execFileSync(process.execPath, ['--check', tmpFile], { stdio: 'pipe' });
} catch (e) {
problems.syntax.push(`${t.rel} → ${String(e.stderr || e.message).split('\n').slice(0, 3).join(' / ')}`);
}
}
/* 3. template 标签配平 */
const tplMatch = src.match(/<template>([\s\S]*)<\/template>/);
if (tplMatch) {
const stack = [];
const tagRe = /<(\/?)([a-zA-Z][a-zA-Z0-9-]*)((?:"[^"]*"|'[^']*'|[^>"'])*?)(\/?)>/g;
let m;
let bad = null;
while ((m = tagRe.exec(tplMatch[1]))) {
const closing = m[1] === '/';
const name = m[2];
const selfClose = m[4] === '/';
if (closing) {
const top = stack.pop();
if (top !== name) {
bad = `</${name}> 与 <${top || '空'}> 不匹配`;
break;
}
} else if (!selfClose) {
stack.push(name);
}
}
if (!bad && stack.length) bad = `未闭合:${stack.join(', ')}`;
if (bad) problems.tags.push(`${t.rel} → ${bad}`);
}
/* 4. DOM / 浏览器 API(只看代码,注释不计) */
const code = stripComments(src);
const DOM_PATTERNS = [
/\bdocument\./,
/\bwindow\./,
/\blocalStorage\b/,
/\bquerySelector(All)?\b/,
/\bnew PointerEvent\b/,
/\baddEventListener\(\s*['"]pointer/,
/\bgetComputedStyle\b/,
/\bgetBoundingClientRect\b/,
];
DOM_PATTERNS.forEach((re) => {
if (re.test(code)) problems.dom.push(`${t.rel} → ${re}`);
});
/* 5. 手势必须 touch 事件 */
const usesPointer = /@pointer(down|move|up|cancel)/.test(code);
if (usesPointer) problems.gesture.push(`${t.rel} → 使用了 @pointer* 事件(小程序/App 端无 PointerEvent)`);
const isGesture = /@touch(move|start|end)/.test(code);
if (isGesture && !/@touch(start|move|end)/.test(code)) {
problems.gesture.push(`${t.rel} → 手势事件不完整`);
}
/* 6. 只用 uni 基础组件 */
if (tplMatch) {
const tags = new Set();
const re = /<([a-zA-Z][a-zA-Z0-9-]*)/g;
let m2;
while ((m2 = re.exec(tplMatch[1]))) tags.add(m2[1]);
tags.forEach((tag) => {
if (!UNI_PRIMITIVES.has(tag) && !/^[A-Z]/.test(tag) && !tag.includes('-')) {
problems.primitives.push(`${t.rel} → <${tag}>`);
}
});
}
/* 7. 类名 / 令牌前缀按平台 */
const cls = src.match(/\.kole-[a-zA-Z0-9_-]+/g) || [];
if (t.platform === 'mobile') {
cls.forEach((c) => {
if (!c.slice(1).startsWith('kole-m-')) problems.prefix.push(`${t.rel} → ${c}`);
});
if (!src.includes('--kole-m-')) problems.prefix.push(`${t.rel} → 未使用 --kole-m- 令牌`);
} else {
cls.forEach((c) => {
if (c.slice(1).startsWith('kole-m-')) problems.prefix.push(`${t.rel} → PC 端不得用移动端类 ${c}`);
});
}
/* 8. 单位策略(rpx 检查只看代码:注释里提到 rpx 不算数) */
if (t.platform === 'mobile' && !/[0-9]rpx\b/.test(code)) {
problems.units.push(`${t.rel} → 移动端 uni-app 未使用 rpx`);
}
if (t.platform === 'pc' && /[0-9]rpx\b/.test(code)) {
problems.units.push(`${t.rel} → PC 端不得使用 rpx`);
}
}
/* ---------- 汇总断言 ---------- */
console.log('');
check(problems.exist.length === 0, `U1 ${targets.length} 个 SFC 文件齐全`, problems.exist.join(', ') || null);
check(problems.sections.length === 0, 'U2 SFC 三段结构完整', problems.sections.join('; ') || null);
check(problems.syntax.length === 0, 'U3 <script> 段语法通过 node --check', problems.syntax.join('; ') || null);
check(problems.tags.length === 0, 'U4 <template> 标签配平', problems.tags.join('; ') || null);
check(problems.dom.length === 0, 'U5 无 DOM / 浏览器 API 依赖', problems.dom.slice(0, 5).join('; ') || null);
check(problems.gesture.length === 0, 'U6 手势只用 touch 事件', problems.gesture.join('; ') || null);
check(problems.primitives.length === 0, 'U7 只用 uni 基础组件', problems.primitives.slice(0, 5).join('; ') || null);
check(problems.prefix.length === 0, 'U8 类名与令牌前缀按平台隔离', problems.prefix.slice(0, 5).join('; ') || null);
check(problems.units.length === 0, 'U9 单位策略(移动 rpx / PC px)', problems.units.join('; ') || null);
console.log('');
console.log('未执行:uni-app 真实编译(H5 / 小程序 / App 三目标)—— 需 npm 依赖,见本文件头部复现命令');
console.log('\n────────────────────────────');
if (failures.length) {
console.error(`[FAIL] ${failures.length} 条 uni-app 断言失败(通过 ${pass} 条):`);
failures.forEach((f) => console.error(' - ' + f));
process.exit(1);
}
console.log(`[OK] uni-app 门禁全部通过(${pass} 条断言 · ${targets.length} 个 SFC)`);