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

241 lines
15 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
/* 三态主题专项验证:日间 / 夜间 / 自动 + iframe 同步。 */
import { readFileSync } from 'fs';
import { join } from 'path';
const ROOT = process.cwd();
const BASE = process.env.REG_BASE || 'http://127.0.0.1:3311';
let fail = 0;
const ok = (condition, message) => {
console.log((condition ? 'PASS' : 'FAIL') + ' ' + message);
if (!condition) fail++;
};
const app = readFileSync(join(ROOT, 'site/app.js'), 'utf8');
const index = readFileSync(join(ROOT, 'site/index.html'), 'utf8');
const css = readFileSync(join(ROOT, '.design_library/kole-ui/colors_and_type.css'), 'utf8');
ok(/prefers-color-scheme/.test(app), '静态:支持 prefers-color-scheme');
ok(/localStorage\.setItem\(['"]kole-mode/.test(app), '静态:持久化 kole-mode');
ok(/data-mode="auto"/.test(index), '静态:站点包含自动模式选项');
ok(/id="topbar-mode"/.test(index), '静态:顶栏含主题模式入口');
/* 顶栏菜单块内三态齐全:块内容取到悬浮菜单 id 之前(两个菜单之间只隔 fab-theme / fab-top 两个按钮),
不设长度窗口 —— 选项之间隔着内联 SVG,窗口大小会随图标改动失真 */
const topbarMenuBlock = index.slice(index.indexOf('id="topbar-mode-menu"'), index.indexOf('id="mode-menu"'));
ok(['light', 'dark', 'auto'].every((m) => topbarMenuBlock.includes(`data-mode="${m}"`)), '静态:顶栏菜单三态齐全');
ok(/html\.kole-dark/.test(css), '静态:暗色令牌入口存在');
/* 默认自动:kole-mode 缺省值必须是 auto —— 静态卡住回退成 light 的回归 */
ok(/var MODE_DEFAULT = 'auto'/.test(app), '静态:模式默认值是 auto');
ok(/'kole-mode'\) \|\| 'auto'/.test(index), '静态:解析期 bootstrap 默认 auto');
ok(index.indexOf("|| 'auto'") < index.indexOf("data.js"), '静态:bootstrap 在 data.js 之前执行(无主题闪烁)');
ok(/<meta name="color-scheme" content="light dark">/.test(index), '静态:声明 color-scheme(原生控件跟随)');
import { probeContrast } from './lib/contrast-probe.mjs';
let chromium;
try {
({ chromium } = await import('playwright'));
} catch {
console.error('[FATAL] 未安装 playwright');
process.exit(2);
}
const browser = await chromium.launch();
try {
async function siteCase(colorScheme, storedMode, expectedDark, label) {
const page = await browser.newPage({ colorScheme });
await page.addInitScript((mode) => localStorage.setItem('kole-mode', mode), storedMode);
await page.goto(BASE + '/site/index.html', { waitUntil: 'load', timeout: 15000 });
await page.waitForTimeout(150);
const result = await page.evaluate(() => ({
dark: document.documentElement.classList.contains('kole-dark'),
stored: localStorage.getItem('kole-mode'),
menu: document.querySelectorAll('#mode-menu [role="radio"]').length,
auto: document.querySelector('#mode-menu [data-mode="auto"]')?.getAttribute('aria-checked'),
iframe: Array.from(document.querySelectorAll('iframe')).every((f) => {
try { return f.contentDocument?.documentElement.classList.contains('kole-dark') === document.documentElement.classList.contains('kole-dark'); } catch { return true; }
})
}));
ok(result.dark === expectedDark, `${label}:kole-dark=${result.dark}`);
ok(result.stored === storedMode, `${label}:kole-mode=${result.stored}`);
ok(result.menu === 3 && result.auto === (storedMode === 'auto' ? 'true' : 'false'), `${label}:模式菜单 ARIA`);
ok(result.iframe, `${label}:iframe 主题同步`);
await page.close();
}
await siteCase('light', 'auto', false, '自动 + 系统日间');
await siteCase('dark', 'auto', true, '自动 + 系统夜间');
await siteCase('dark', 'light', false, '手动日间覆盖系统夜间');
await siteCase('light', 'dark', true, '手动夜间覆盖系统日间');
for (const mode of ['light', 'dark']) {
await chromeContrastCase(mode, '/site/guide', mode + ' · /site/guide');
await chromeContrastCase(mode, '/site/component/table', mode + ' · /site/component/table');
await chromeContrastCase(mode, '/site/component/button', mode + ' · /site/component/button');
}
/* 站点 chrome 的对比度(亮/暗双模式):徽章、警告 callout、代码块、侧栏选中态等
在两种模式下都要过 AA。判据与暗色那份共用 tools/lib/contrast-probe.mjs。 */
async function chromeContrastCase(mode, route, label) {
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
await page.addInitScript((m) => localStorage.setItem('kole-mode', m), mode);
const resp = await page.goto(BASE + route, { waitUntil: 'networkidle', timeout: 20000 }).catch(() => null);
const status = resp ? resp.status() : 0;
ok(status === 200, `${label}:可访问(HTTP ${status})`);
if (status !== 200) { await page.close(); return; }
const isDark = await page.evaluate(() => document.documentElement.classList.contains('kole-dark'));
ok(isDark === (mode === 'dark'), `${label}:kole-dark=${isDark}`);
const res = await probeContrast(page);
ok(res.bad.length === 0, `${label}:chrome 对比度抽查 ${res.checked} 节点` + (res.bad.length ? ' 不足:' + res.bad.join(';') : ''));
await page.close();
}
/* 首次访问(无 kole-mode):默认 auto,且跟随系统实时生效 */
const firstRun = await browser.newPage({ colorScheme: 'dark' });
await firstRun.goto(BASE + '/site/index.html', { waitUntil: 'load', timeout: 15000 });
await firstRun.waitForTimeout(150);
const first = await firstRun.evaluate(() => {
const cur = document.getElementById('mode-current');
return {
stored: localStorage.getItem('kole-mode'),
dark: document.documentElement.classList.contains('kole-dark'),
auto: document.querySelector('#mode-menu [data-mode="auto"]')?.getAttribute('aria-checked'),
current: (cur || {}).textContent || '',
currentLines: cur ? cur.children.length : 0,
currentEm: cur && cur.querySelector('em') ? cur.querySelector('em').textContent : ''
};
});
ok(first.stored === 'auto', `首访默认:kole-mode=${first.stored}(落盘默认 auto)`);
ok(first.dark === true, '首访默认:系统夜间时生效夜间');
ok(first.auto === 'true', '首访默认:菜单选中自动');
ok(first.currentLines === 2 && first.current.indexOf('自动模式') > -1, `首访默认:菜单头两行("${first.current.trim().replace(/\s+/g, ' ')}")`);
ok(first.currentEm.indexOf('夜间') > -1, `首访默认:第二行标出生效主题("${first.currentEm}")`);
await firstRun.emulateMedia({ colorScheme: 'light' });
await firstRun.waitForTimeout(120);
ok(!(await firstRun.locator('html').evaluate((el) => el.classList.contains('kole-dark'))), '首访默认:系统切日间后即时反色');
await firstRun.close();
const changing = await browser.newPage({ colorScheme: 'light' });
await changing.addInitScript(() => localStorage.setItem('kole-mode', 'auto'));
await changing.goto(BASE + '/site/index.html', { waitUntil: 'load', timeout: 15000 });
await changing.waitForTimeout(100);
await changing.emulateMedia({ colorScheme: 'dark' });
await changing.waitForTimeout(100);
ok(await changing.locator('html').evaluate((el) => el.classList.contains('kole-dark')), '自动模式:系统切换到夜间即时生效');
await changing.emulateMedia({ colorScheme: 'light' });
await changing.waitForTimeout(100);
ok(!(await changing.locator('html').evaluate((el) => el.classList.contains('kole-dark'))), '自动模式:系统切换到日间即时生效');
await changing.close();
const menuPage = await browser.newPage({ colorScheme: 'light' });
await menuPage.addInitScript(() => localStorage.removeItem('kole-mode'));
await menuPage.goto(BASE + '/site/index.html', { waitUntil: 'load', timeout: 15000 });
await menuPage.locator('#fab-mode').click();
await menuPage.locator('#mode-menu [data-mode="dark"]').click();
ok(await menuPage.evaluate(() => localStorage.getItem('kole-mode') === 'dark' && document.documentElement.classList.contains('kole-dark')), '菜单选择夜间模式');
await menuPage.close();
/* 顶栏选择器:可见、可选、与悬浮菜单互斥、窄屏收成图标 */
const topbar = await browser.newPage({ colorScheme: 'light', viewport: { width: 1280, height: 800 } });
await topbar.addInitScript(() => localStorage.removeItem('kole-mode'));
await topbar.goto(BASE + '/site/index.html', { waitUntil: 'load', timeout: 15000 });
await topbar.waitForTimeout(150);
ok(await topbar.locator('#topbar-mode').isVisible(), '顶栏入口:默认可⻅');
const topbarLabel = (await topbar.locator('#topbar-mode-label').textContent()).trim();
ok(topbarLabel === '自动模式', `顶栏入口:显示当前模式("${topbarLabel}")`);
await topbar.locator('#topbar-mode').click();
ok(await topbar.locator('#topbar-mode-menu').isVisible(), '顶栏入口:点开三态菜单');
ok(await topbar.locator('#topbar-mode-menu [data-mode="auto"]').getAttribute('aria-checked') === 'true', '顶栏菜单:默认选中自动');
await topbar.locator('#topbar-mode-menu [data-mode="dark"]').click();
ok(await topbar.evaluate(() => document.documentElement.classList.contains('kole-dark') && localStorage.getItem('kole-mode') === 'dark'), '顶栏菜单:选夜间即生效并落盘');
ok((await topbar.locator('#topbar-mode-label').textContent()).trim() === '夜间模式', '顶栏入口:模式名同步更新');
/* 两个入口互斥:顶栏开着时点悬浮按钮,顶栏那份必须收起 */
await topbar.locator('#topbar-mode').click();
await topbar.locator('#fab-mode').click();
ok(await topbar.evaluate(() => document.getElementById('topbar-mode-menu').classList.contains('hidden')
&& !document.getElementById('mode-menu').classList.contains('hidden')), '两个入口互斥:悬浮菜单打开时顶栏菜单收起');
await topbar.keyboard.press('Escape');
ok(await topbar.evaluate(() => document.getElementById('mode-menu').classList.contains('hidden')), 'Esc 关闭菜单');
await topbar.setViewportSize({ width: 900, height: 800 });
await topbar.waitForTimeout(120);
const narrow = await topbar.evaluate(() => {
const label = document.getElementById('topbar-mode-label');
const trigger = document.getElementById('topbar-mode');
const navToggle = document.getElementById('nav-toggle');
return {
labelHidden: getComputedStyle(label).display === 'none',
triggerVisible: trigger.getBoundingClientRect().width > 0 && trigger.getBoundingClientRect().right <= window.innerWidth,
navToggleVisible: getComputedStyle(navToggle).display !== 'none'
};
});
ok(narrow.labelHidden, '窄屏:顶栏模式名收起(仅留图标 + 箭头)');
ok(narrow.triggerVisible, '窄屏:入口仍在顶栏内可见(未被挤出屏幕)');
ok(narrow.navToggleVisible, '窄屏:汉堡按钮未被挤掉');
/* 超窄屏(320px):顶栏只留品牌标记 / 搜索 / 主题 / 汉堡,语言与框架选择器让位。
只量顶栏自身:320px 下首页内容(.stat 统计卡)另有 15px 溢出,与本改动无关,
量整页会把既有问题算到顶栏头上(既有问题已另行记录,不在此处放宽或掩盖)。 */
await topbar.setViewportSize({ width: 375, height: 700 });
await topbar.waitForTimeout(120);
const phone = await topbar.evaluate(() => {
const shown = (sel) => getComputedStyle(document.querySelector(sel)).display !== 'none';
const r = document.getElementById('topbar-mode').getBoundingClientRect();
return {
themeVisible: shown('#theme-select') && r.right <= window.innerWidth,
langVisible: shown('#lang-select')
};
});
ok(phone.themeVisible, '375px:主题入口在顶栏可见');
ok(phone.langVisible, '375px:语言选择器未被牵连隐藏(常见手机宽度仍可切语言)');
await topbar.setViewportSize({ width: 320, height: 700 });
await topbar.waitForTimeout(120);
const tiny = await topbar.evaluate(() => {
const inner = document.querySelector('.topbar-inner');
const cs = getComputedStyle(inner);
const visible = [...inner.children].filter((el) => {
const s = getComputedStyle(el);
if (s.display === 'none' || s.position === 'fixed' || s.visibility === 'hidden') return false;
return el.getBoundingClientRect().width > 0;
});
const need = visible.reduce((a, el) => a + el.getBoundingClientRect().width, 0)
+ Math.max(0, visible.length - 1) * parseFloat(cs.gap)
+ parseFloat(cs.paddingLeft) + parseFloat(cs.paddingRight);
const box = (sel) => document.querySelector(sel).getBoundingClientRect();
const shown = (sel) => getComputedStyle(document.querySelector(sel)).display !== 'none';
return {
topbarOverflow: Math.max(0, Math.ceil(need - document.documentElement.clientWidth)),
themeVisible: shown('#theme-select') && box('#topbar-mode').right <= window.innerWidth,
langHidden: !shown('#lang-select'),
fwHidden: !shown('.fw-select-wrap'),
navToggleVisible: shown('#nav-toggle')
};
});
ok(tiny.topbarOverflow <= 0, `超窄屏 320px:顶栏自身无横向溢出(溢出 ${tiny.topbarOverflow}px)`);
ok(tiny.themeVisible, '超窄屏 320px:主题入口仍在顶栏可见');
ok(tiny.langHidden && tiny.fwHidden, '超窄屏 320px:语言与框架选择器让位(保主题入口)');
ok(tiny.navToggleVisible, '超窄屏 320px:汉堡按钮保留');
await topbar.close();
const switcher = await browser.newPage({ colorScheme: 'light' });
await switcher.addInitScript(() => localStorage.removeItem('kole-mode'));
await switcher.goto(BASE + '/frameworks/ThemeSwitcher.html', { waitUntil: 'load', timeout: 15000 });
const modes = await switcher.locator('.kole-themesw-opt').count();
ok(modes === 3, 'ThemeSwitcher H5:三种模式');
ok(await switcher.locator('[data-mode="auto"]').getAttribute('aria-checked') === 'true', 'ThemeSwitcher H5:默认自动');
await switcher.locator('[data-mode="dark"]').click();
ok(await switcher.locator('html').evaluate((el) => el.classList.contains('kole-dark')), 'ThemeSwitcher H5:夜间模式生效');
await switcher.locator('[data-mode="auto"]').click();
ok(await switcher.locator('[data-mode="auto"]').getAttribute('aria-checked') === 'true', 'ThemeSwitcher H5:自动模式 ARIA');
await switcher.close();
const pg = await browser.newPage({ colorScheme: 'dark' });
await pg.addInitScript(() => localStorage.removeItem('kole-mode'));
await pg.goto(BASE + '/site/playground.html', { waitUntil: 'load', timeout: 15000 });
await pg.waitForTimeout(150);
ok(await pg.locator('html').evaluate((el) => el.classList.contains('kole-dark')), 'playground:默认自动 + 系统夜间生效');
await pg.close();
} finally {
await browser.close();
}
console.log(fail ? `THEME VERIFY FAIL (${fail})` : 'THEME VERIFY OK');
process.exit(fail ? 1 : 0);