Files
aurora-admin/site/app.js
T

2043 lines
88 KiB
JavaScript
Raw 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.
/* Aurora Admin 组件库文档 — 交互逻辑(无依赖 vanilla JS)
布局与交互对齐 Element 文档:首页落地页 / 快速开始 / 分类侧栏 /
多演示块样式(居中箭头展开)/ 右侧锚点目录 / Ctrl+K 搜索 */
(function () {
'use strict';
var LIB_ROOT = '../.design_library/aurora-admin/';
var SPEC_ROOT = '../';
var TAB_ORDER = ['html', 'css', 'jsx', 'vue2', 'vue3'];
var TAB_LABELS = { html: 'H5 HTML', css: 'CSS', jsx: 'React JSX', vue2: 'Vue 2', vue3: 'Vue 3' };
var BATCH_LABELS = { 1: '核心规范', 2: '第二批', 3: '第三批', 4: '第四批', 5: '第五批', 6: '第六批', 7: '第七批', 8: '第八批', 9: '第九批', 10: '第十批' };
var CAT_LABELS = { general: '通用', navigation: '导航', input: '数据录入', display: '数据展示', feedback: '反馈', system: '工具与系统' };
var CAT_EN = { general: 'General', navigation: 'Navigation', input: 'Data Entry', display: 'Data Display', feedback: 'Feedback', system: 'System' };
var CAT_ORDER = ['general', 'navigation', 'input', 'display', 'feedback', 'system'];
var KIND_LABELS = { html: 'H5 静态页', css: '样式表', jsx: 'React 组件', vue2: 'Vue 2 组件', vue3: 'Vue 3 组件' };
/* 框架感知:每种技术栈需要的文件形态 */
var FRAMEWORKS = { h5: 'H5', react: 'React', vue2: 'Vue 2', vue3: 'Vue 3' };
var FW_FILES = {
h5: ['html', 'css'],
react: ['jsx', 'css'],
vue2: ['vue2'],
vue3: ['vue3']
};
var data = window.AA_DATA;
var $ = function (id) { return document.getElementById(id); };
function fatal(msg) {
var el = $('fatal');
el.textContent = msg;
el.classList.remove('hidden');
}
if (!data || !data.components || !data.components.length) {
fatal('数据加载失败:site/data.js 缺失或为空。请先在仓库根目录运行 build-site.ps1 重新生成。');
return;
}
var comps = data.components;
var bySlugMap = {};
comps.forEach(function (c) { bySlugMap[c.slug] = c; });
var state = { route: { page: 'home', slug: null }, query: '', ovCat: '', ovQ: '', framework: 'h5', searchCat: '' };
var thumbObserver = null;
function loadFramework() {
try {
var saved = localStorage.getItem('aa-framework');
if (saved && FRAMEWORKS[saved]) return saved;
} catch (e) { /* ignore */ }
return 'h5';
}
/* ---------- 工具 ---------- */
function el(tag, cls, html) {
var e = document.createElement(tag);
if (cls) e.className = cls;
if (html != null) e.innerHTML = html;
return e;
}
function esc(s) {
return String(s == null ? '' : s)
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
function splitName(name) {
var m = name.match(/^(.+?)\s+([A-Za-z0-9].*)$/);
return m ? { zh: m[1], en: m[2] } : { zh: name, en: '' };
}
function batchLabel(b) { return BATCH_LABELS[b] || ('第' + b + '批'); }
function catLabel(key) { return CAT_LABELS[key] || key || '未分类'; }
function catEn(key) { return CAT_EN[key] || ''; }
function fmtSize(bytes) {
if (bytes == null) return '—';
if (bytes < 1024) return bytes + ' B';
return (bytes / 1024).toFixed(1) + ' KB';
}
function specLink(file) { return SPEC_ROOT + file; }
function cleanSummary(s) { return (s || '').replace(/^[-•·]\s*/, '').trim(); }
/* ---------- 路由 ---------- */
function parseHash() {
var h = location.hash.replace(/^#/, '');
if (!h || h === '/') return { page: 'home', slug: null };
if (h === '/overview') return { page: 'overview', slug: null };
if (h === '/guide') return { page: 'guide', slug: null };
if (h === '/design') return { page: 'design', slug: null };
if (h === '/agents') return { page: 'agents', slug: null };
if (h === '/changelog') return { page: 'changelog', slug: null };
var m = h.match(/^\/component\/([a-z0-9-]+)/i);
if (m && bySlugMap[m[1]]) return { page: 'component', slug: m[1] };
return { page: 'home', slug: null };
}
function navigate(opts) {
var fwOnly = opts && opts.fwOnly;
state.route = parseHash();
closeSearch();
renderSidebar();
renderTopnav();
renderToc([]);
var content = $('content');
if (state.route.page === 'home') renderHome(content);
else if (state.route.page === 'overview') renderOverview(content);
else if (state.route.page === 'guide') renderGuide(content);
else if (state.route.page === 'design') renderDesign(content);
else if (state.route.page === 'agents') renderAgents(content);
else if (state.route.page === 'changelog') renderChangelog(content);
else renderComponent(content, state.route.slug);
/* 仅切换框架时不滚到顶部,避免用户感觉"页面没切换" */
if (!fwOnly) window.scrollTo(0, 0);
if (window.aaLogger) {
window.aaLogger.render(state.route.page || 'unknown', { slug: state.route.slug || null, framework: state.framework });
}
}
function renderTopnav() {
var r = state.route.page;
document.querySelectorAll('.topnav a').forEach(function (a) {
var nav = a.getAttribute('data-nav');
var active = (nav === r) ||
(nav === 'overview' && r === 'component');
a.classList.toggle('active', active);
});
}
/* ---------- 侧边栏 ---------- */
function renderSidebar() {
var sb = $('sidebar');
sb.innerHTML = '';
var r = state.route.page;
var links = [
{ label: '组件总览', hash: '#/overview', active: r === 'overview' || r === 'component' },
{ label: '快速开始', hash: '#/guide', active: r === 'guide' },
{ label: '设计规范', hash: '#/design', active: r === 'design' }
];
links.forEach(function (L) {
var a = el('a', 'side-link' + (L.active ? ' active' : ''), esc(L.label));
a.href = L.hash;
sb.appendChild(a);
});
CAT_ORDER.forEach(function (cat) {
var items = comps.filter(function (c) { return c.category === cat; });
if (!items.length) return;
var g = el('div', 'side-group');
g.innerHTML = '<span><i class="cat-zh"></i><i class="cat-en"></i></span><span class="cnt">' + items.length + '</span>';
g.querySelector('.cat-zh').textContent = catLabel(cat);
g.querySelector('.cat-en').textContent = catEn(cat);
sb.appendChild(g);
items.forEach(function (c) {
var n = splitName(c.name);
var b = el('button', 'side-item');
b.type = 'button';
b.setAttribute('data-slug', c.slug);
b.innerHTML = '<span class="zh"></span><span class="en"></span>';
b.querySelector('.zh').textContent = n.zh;
b.querySelector('.en').textContent = n.en;
if (r === 'component' && state.route.slug === c.slug) b.classList.add('active');
b.addEventListener('click', function () { location.hash = '#/component/' + c.slug; });
sb.appendChild(b);
});
});
}
/* ---------- 右侧目录 ---------- */
function renderToc(items) {
var toc = $('toc');
toc.innerHTML = '';
if (!items.length) { toc.style.display = 'none'; return; }
toc.style.display = '';
toc.appendChild(el('div', 'toc-title', '本页目录'));
items.forEach(function (it) {
var a = el('a', null, esc(it.label));
a.setAttribute('data-anchor', it.id);
a.addEventListener('click', function (e) {
e.preventDefault();
var target = document.getElementById(it.id);
if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' });
});
toc.appendChild(a);
});
setupScrollSpy(items);
}
var spyHandler = null;
function setupScrollSpy(items) {
if (spyHandler) window.removeEventListener('scroll', spyHandler);
spyHandler = function () {
var pos = window.scrollY + 120;
var current = items[0] && items[0].id;
items.forEach(function (it) {
var node = document.getElementById(it.id);
if (node && node.offsetTop <= pos) current = it.id;
});
document.querySelectorAll('.toc a').forEach(function (a) {
a.classList.toggle('active', a.getAttribute('data-anchor') === current);
});
};
window.addEventListener('scroll', spyHandler, { passive: true });
spyHandler();
}
/* ---------- 首页(落地页) ---------- */
function renderHome(content) {
content.innerHTML = '';
var inner = el('div', 'page-inner home');
/* Hero stats 第三项加入"测试" */
var hero = el('div', 'hero');
hero.innerHTML =
'<div class="hero-kicker">AURORA ADMIN DESIGN SYSTEM</div>' +
'<h1 class="hero-title">Aurora Admin 组件库</h1>' +
'<p class="hero-sub">理性、高效、克制的 B 端中后台设计系统——信息密度适中,层级清晰,交互反馈即时。</p>' +
'<div class="hero-actions">' +
' <a class="btn-primary-lg" href="#/overview">浏览组件 →</a>' +
' <a class="btn-ghost-lg" href="scenario/user-management.html" target="_blank">场景示范</a>' +
' <a class="btn-ghost-lg" href="#/guide">快速开始</a>' +
' <a class="btn-ghost-lg" href="../tests/index.html" target="_blank">测试总览</a>' +
' <a class="btn-ghost-lg" href="#/design">设计规范</a>' +
'</div>' +
'<div class="hero-stats">' +
' <div class="stat"><strong>' + data.meta.total + '</strong><span>组件</span></div>' +
' <div class="stat"><strong>' + CAT_ORDER.length + '</strong><span>大类</span></div>' +
' <div class="stat"><strong>' + data.tokens.length + '</strong><span>设计令牌</span></div>' +
' <div class="stat"><strong>5</strong><span>端实现</span></div>' +
' <div class="stat"><strong>10</strong><span>份规范</span></div>' +
' <div class="stat"><strong>79</strong><span>测试页</span><em class="stat-sub" id="stat-passrate"></em></div>' +
'</div>';
inner.appendChild(hero);
/* 测试通过率:来自 tests/report.json(headless 回归产出,缺失时静默不显示) */
fetch('../tests/report.json', { cache: 'no-cache' }).then(function (r) { return r.ok ? r.json() : null; }).then(function (rep) {
var node = document.getElementById('stat-passrate');
if (rep && rep.passRate != null && node) {
node.textContent = '通过率 ' + rep.passRate + '%';
node.title = rep.generated ? 'headless 回归 ' + rep.generated + ':' + rep.assertions.pass + '/' + rep.assertions.total + ' 断言通过' : '';
}
}).catch(function () { /* 报告不存在时不显示 */ });
/* 设计原则四卡 */
inner.appendChild(el('h2', 'home-h2', '设计原则'));
var feats = [
['一致优先', '同类组件使用相同的尺寸、颜色与间距,跨页面保持一致体验。', 'M4 12l5 5L20 7'],
['清晰高效', '信息层级明确,操作路径最短,避免过度装饰。', 'M13 2L3 14h9l-1 8 10-12h-9l1-8z'],
['反馈及时', '所有操作都有加载、成功、失败三类状态反馈。', 'M22 11.08V12a10 10 0 1 1-5.93-9.14'],
['无障碍', '满足键盘操作与焦点管理,正文对比度 ≥ 4.5:1。', 'M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z']
];
var fgrid = el('div', 'feat-grid');
feats.forEach(function (f) {
var card = el('div', 'feat-card');
card.innerHTML =
'<div class="feat-icon"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="' + f[2] + '"/></svg></div>' +
'<div class="feat-body"><h3></h3><p></p></div>';
card.querySelector('h3').textContent = f[0];
card.querySelector('p').textContent = f[1];
fgrid.appendChild(card);
});
inner.appendChild(fgrid);
/* 核心组件快捷入口 */
inner.appendChild(el('h2', 'home-h2', '核心组件'));
var core = el('div', 'core-grid');
comps.filter(function (c) { return c.contract; }).forEach(function (c) {
var n = splitName(c.name);
var a = el('a', 'core-card');
a.href = '#/component/' + c.slug;
a.innerHTML = '<div class="core-zh"></div><div class="core-en"></div>';
a.querySelector('.core-zh').textContent = n.zh;
a.querySelector('.core-en').textContent = n.en;
core.appendChild(a);
});
inner.appendChild(core);
/* 批次入口 */
inner.appendChild(el('h2', 'home-h2', '规格批次'));
var batchWrap = el('div', 'batch-wrap');
[1, 2, 4, 5, 6, 7, 9].forEach(function (b) {
var items = comps.filter(function (c) { return c.specBatch === b; });
if (!items.length) return;
var a = el('a', 'batch-chip');
a.href = specLink('组件' + b + '.txt');
a.target = '_blank';
a.textContent = batchLabel(b) + ' · ' + items.length + ' 组件';
batchWrap.appendChild(a);
});
inner.appendChild(batchWrap);
content.appendChild(inner);
}
/* ---------- 快速开始 ---------- */
function renderGuide(content) {
content.innerHTML = '';
var inner = el('div', 'page-inner');
inner.appendChild(el('h1', 'page-title', '快速开始'));
inner.appendChild(el('p', 'page-desc', '了解如何在本项目中使用 Aurora Admin 组件:引入设计令牌、按端取用组件源码、以及目录与命名约定。'));
var tocItems = [];
/* 1. 引入设计令牌 */
var s1 = el('div', 'sec'); s1.id = 'install';
s1.appendChild(el('h2', null, '引入设计令牌'));
s1.appendChild(el('p', 'guide-p', '所有组件都基于同一份 CSS 变量令牌。在使用任何组件前,先在页面中引入令牌文件:'));
var code1 = el('pre', 'code-inline');
code1.appendChild(el('code', null, esc('<link rel="stylesheet" href=".design_library/aurora-admin/colors_and_type.css">')));
s1.appendChild(code1);
s1.appendChild(el('p', 'guide-p', '如需结构化的令牌数据(供工具消费),可读取同目录的 <code>css.json</code>;聚合组件样式见 <code>components.css</code>。'));
inner.appendChild(s1);
tocItems.push({ id: 'install', label: '引入设计令牌' });
/* 2. 按端使用组件 */
var s2 = el('div', 'sec'); s2.id = 'usage';
s2.appendChild(el('h2', null, '按端使用组件'));
s2.appendChild(el('p', 'guide-p', '每个组件在 <code>frameworks/</code> 下有五种形态(H5 / CSS / React / Vue 2 / Vue 3),按你的技术栈取用:'));
var tbl = el('table', 'res-table');
var thead = el('thead');
var trh = el('tr');
['端', '文件', '使用方式'].forEach(function (t) { trh.appendChild(el('th', null, t)); });
thead.appendChild(trh); tbl.appendChild(thead);
var tbody = el('tbody');
[
['H5 静态页', 'Button.html', '直接在浏览器打开即为完整演示页;将结构与内联样式拷贝进你的页面'],
['React', 'Button.jsx + Button.css', '将 JSX 与 CSS 拷入工程,按需改造数据绑定'],
['Vue 2', 'Button.vue2.vue', 'Options API 单文件组件,直接注册使用'],
['Vue 3', 'Button.vue3.vue', '<script setup> 组合式 API,直接注册使用'],
['样式令牌', 'colors_and_type.css', '所有变量以 --au- 前缀暴露,可整体引入或按需拷贝']
].forEach(function (row) {
var tr = el('tr');
tr.appendChild(el('td', null, row[0]));
tr.appendChild(el('td', 'num', esc(row[1])));
tr.appendChild(el('td', null, esc(row[2])));
tbody.appendChild(tr);
});
tbl.appendChild(tbody);
s2.appendChild(tbl);
inner.appendChild(s2);
tocItems.push({ id: 'usage', label: '按端使用组件' });
/* 3. 目录结构 */
var s3 = el('div', 'sec'); s3.id = 'structure';
s3.appendChild(el('h2', null, '目录结构'));
var code3 = el('pre', 'code-inline');
code3.appendChild(el('code', null, esc(
'组件规范第一套/\n' +
'├─ 组件1~10.txt # 设计规范文档(核心 + 各批次组件规范)\n' +
'├─ frameworks/ # 79 个组件 × 5 端实现\n' +
'└─ .design_library/aurora-admin/\n' +
' ├─ colors_and_type.css # 设计令牌(--au-* CSS 变量)\n' +
' ├─ css.json # 结构化令牌数据\n' +
' ├─ components.css # 聚合组件样式\n' +
' ├─ components/ # 核心组件契约 JSON + 全量索引\n' +
' └─ preview/ # 核心组件预览页'
)));
s3.appendChild(code3);
inner.appendChild(s3);
tocItems.push({ id: 'structure', label: '目录结构' });
/* 4. 命名约定 */
var s4 = el('div', 'sec'); s4.id = 'naming';
s4.appendChild(el('h2', null, '命名约定'));
var ul = el('ul', 'guide-ul');
[
'组件 slug 为小写(如 button、sidemenu),文件名前缀为 PascalCase(如 SideMenu.vue3.vue)',
'H5 演示页自带演示数据与交互,可直接作为视觉验收基准',
'组件 ↔ 规格批次 ↔ 实现的权威映射见 .design_library/aurora-admin/components/index.json',
'全部组件默认桌面端优先(最小宽度 1280px),不推荐移动端场景'
].forEach(function (t) { ul.appendChild(el('li', null, esc(t))); });
s4.appendChild(ul);
inner.appendChild(s4);
tocItems.push({ id: 'naming', label: '命名约定' });
content.appendChild(inner);
renderToc(tocItems);
}
/* ---------- For Agents(AI 消费入口) ---------- */
function renderAgents(content) {
content.innerHTML = '';
var inner = el('div', 'page-inner');
inner.appendChild(el('h1', 'page-title', 'For Agents · AI 消费入口'));
inner.appendChild(el('p', 'page-desc', '本站全部数据机器可读:无需解析 HTML,一次抓取 data.json 即可获得 79 个组件、设计令牌与分类元数据。本页汇总 Agent 可直接消费的资源、数据形状与使用规则。'));
var tocItems = [];
/* 1. 机器可读资源 */
var s1 = el('div', 'sec'); s1.id = 'agents-resources';
s1.appendChild(el('h2', null, '机器可读资源'));
var t1 = el('table', 'res-table');
t1.innerHTML = '<thead><tr><th style="width:26%">资源</th><th style="width:38%">路径</th><th>说明</th></tr></thead><tbody></tbody>';
var tb1 = t1.querySelector('tbody');
[
['全量组件数据(首选)', 'site/data.json', 'meta + 分类 + 设计令牌 + 79 个组件(规格摘要、5 端文件路径、API 提炼),单次请求即可'],
['组件注册表', '.design_library/aurora-admin/components/index.json', '组件 ↔ 规格批次 ↔ 实现文件的权威映射(schemaVersion 3)'],
['核心组件契约 ×6', '.design_library/aurora-admin/components/{button,input,select,table,card,modal}.json', '变体维度、代表变体、使用要点、结构解剖、不发明与未明示清单'],
['设计令牌(结构化)', '.design_library/aurora-admin/css.json', '全部 CSS 变量的 JSON 形态:颜色 / 字体 / 间距 / 圆角 / 阴影 / 控件高度'],
['设计令牌(CSS)', '.design_library/aurora-admin/colors_and_type.css', '可直接 <link> 引入的令牌源文件,所有色值必须取自 au-* 变量'],
['组件样式聚合', '.design_library/aurora-admin/components.css', '全部组件样式的单文件聚合'],
['组件实现源码 ×5 端', 'frameworks/<Prefix>.{html,css,jsx,vue2.vue,vue3.vue}', 'H5 / React / Vue 2 / Vue 3 形态,文件名前缀为 PascalCase'],
['组件测试页 ×79', 'tests/<slug>.html(总览 tests/index.html)', '断言结果标注 data-assert / data-status="pass|fail",可机读回归'],
['LLM 入口清单', 'site/llms.txt', '本页内容的 llms.txt 形态,供爬虫与 Agent 定位']
].forEach(function (r) {
var tr = document.createElement('tr');
tr.innerHTML = '<td></td><td><code></code></td><td></td>';
tr.cells[0].textContent = r[0];
tr.cells[1].textContent = r[1];
tr.cells[2].textContent = r[2];
tb1.appendChild(tr);
});
s1.appendChild(t1);
inner.appendChild(s1);
tocItems.push({ id: 'agents-resources', label: '机器可读资源' });
/* 2. data.json 数据形状 */
var s2 = el('div', 'sec'); s2.id = 'agents-schema';
s2.appendChild(el('h2', null, 'data.json 数据形状'));
var t2 = el('table', 'res-table');
t2.innerHTML = '<thead><tr><th style="width:30%">字段</th><th>内容</th></tr></thead><tbody></tbody>';
var tb2 = t2.querySelector('tbody');
[
['meta', 'version(与 CHANGELOG 最新版本同步)、total=79、core/extension 数量、brand=#2F54EB、generated 构建时间'],
['categories', '六大分类及组件计数:general / navigation / input / display / feedback / system'],
['tokens', '设计令牌数组:name(au-*)、value、comment(中文用途说明)'],
['components[].slug / name / tier', '小写 slug、中文名 + 英文名、core 或 extension'],
['components[].specBatch / specFile / specLines', '规格批次 1-10、来源规格文件、规格章节原文(含"何时使用"等)'],
['components[].contract', '核心组件契约:dims(变体维度)、variants、usage(使用要点)、structure、anatomy、doNot(不发明)、unknowns(未明示)'],
['components[].files / sizes', '5 端实现文件相对路径与字节数:html / css / jsx / vue2 / vue3']
].forEach(function (r) {
var tr = document.createElement('tr');
tr.innerHTML = '<td><code></code></td><td></td>';
tr.cells[0].textContent = r[0];
tr.cells[1].textContent = r[1];
tb2.appendChild(tr);
});
s2.appendChild(t2);
inner.appendChild(s2);
tocItems.push({ id: 'agents-schema', label: 'data.json 数据形状' });
/* 3. 消费示例 */
var s3 = el('div', 'sec'); s3.id = 'agents-example';
s3.appendChild(el('h2', null, '消费示例'));
s3.appendChild(el('p', 'guide-p', '抓取 data.json 并按分类筛选组件、读取令牌与契约的最小示例:'));
var code3 = el('pre', 'code-inline');
code3.appendChild(el('code', null, esc(
"const data = await fetch('site/data.json').then(r => r.json());\n" +
"const inputs = data.components.filter(c => c.category === 'input');\n" +
"const brand = data.tokens.find(t => t.name === 'au-color-brand');\n" +
"const btnUsage = data.components.find(c => c.slug === 'button').contract.usage;\n" +
"// 生成 UI 时:色值只取 tokens 中的 au-* 值;变体不超出 contract.dims 声明的维度"
)));
s3.appendChild(code3);
inner.appendChild(s3);
tocItems.push({ id: 'agents-example', label: '消费示例' });
/* 4. 使用规则 */
var s4 = el('div', 'sec'); s4.id = 'agents-rules';
s4.appendChild(el('h2', null, '使用规则'));
var ul4 = el('ul', 'guide-ul');
[
'不发明:组件变体不得超出契约 dims 声明的维度与取值;扩展组件以规格原文 specLines 为准',
'不硬编码颜色:所有色值取自 tokens 中的 au-* 变量,品牌主色 #2F54EB,语义色独立于品牌色',
'尊重未明示:契约 unknowns 列出的项规格未定义,生成前应向用户澄清而非假设',
'文案遵循内容规范:按钮用动词短语(新建 / 编辑 / 删除),反馈文案明确可操作,不使用 emoji',
'桌面端优先:整套体系最小宽度 1280px,不适用于移动端场景'
].forEach(function (t) { ul4.appendChild(el('li', null, esc(t))); });
s4.appendChild(ul4);
inner.appendChild(s4);
tocItems.push({ id: 'agents-rules', label: '使用规则' });
content.appendChild(inner);
renderToc(tocItems);
}
/* ---------- 更新日志 ---------- */
function renderChangelog(content) {
content.innerHTML = '';
var inner = el('div', 'page-inner');
inner.appendChild(el('h1', 'page-title', '更新日志'));
inner.appendChild(el('p', 'page-desc', '按 SemVer 记录的重要变更,与仓库根目录 CHANGELOG.md 同源(构建时自动解析注入)。'));
var tocItems = [];
var entries = data.changelog || [];
if (!entries.length) {
var empty = el('div', 'sec'); empty.id = 'cl-empty';
empty.appendChild(el('p', 'guide-p', '暂无结构化日志数据:请运行 build-site.ps1 重新生成,构建脚本会解析 CHANGELOG.md 并注入 data.js。'));
inner.appendChild(empty);
tocItems.push({ id: 'cl-empty', label: '暂无数据' });
}
entries.forEach(function (e) {
var sec = el('div', 'sec'); sec.id = 'v-' + String(e.version).replace(/\./g, '-');
var head = el('div', 'cl-head');
head.appendChild(el('span', 'chip blue', 'v' + e.version));
head.appendChild(el('span', 'cl-date', e.date || ''));
sec.appendChild(head);
(e.sections || []).forEach(function (s) {
sec.appendChild(el('h3', 'cl-sec-title', esc(s.title)));
var ul = el('ul', 'guide-ul');
(s.items || []).forEach(function (it) { ul.appendChild(el('li', null, esc(it))); });
sec.appendChild(ul);
});
inner.appendChild(sec);
tocItems.push({ id: sec.id, label: 'v' + e.version });
});
content.appendChild(inner);
renderToc(tocItems);
}
/* ---------- 组件总览 ---------- */
function renderOverview(content) {
if (thumbObserver) { thumbObserver.disconnect(); thumbObserver = null; }
content.innerHTML = '';
var inner = el('div', 'page-inner');
inner.appendChild(el('h1', 'page-title', '组件总览'));
inner.appendChild(el('p', 'page-desc',
'共 <strong>' + data.meta.total + '</strong> 个组件(核心 <strong>' + data.meta.core + '</strong> · 扩展 <strong>' + data.meta.extension +
'</strong>),覆盖 ' + CAT_ORDER.length + ' 大类。基于 Aurora Admin 设计系统规范构建,提供 H5 / React / Vue 2 / Vue 3 四端实现。'));
var grid = el('div', 'ov-grid');
grid.id = 'ov-grid';
inner.appendChild(grid);
content.appendChild(inner);
/* 过滤工具条(分类 chips + 关键词) */
renderOverviewFilter(inner, grid);
rebuildOverviewGrid(grid);
thumbObserver = new IntersectionObserver(function (entries) {
entries.forEach(function (en) {
if (!en.isIntersecting) return;
var card = en.target;
thumbObserver.unobserve(card);
var slug = card.getAttribute('data-slug');
var c = bySlugMap[slug];
var thumb = card.querySelector('.ov-thumb');
thumb.innerHTML = '';
var ifr = document.createElement('iframe');
ifr.setAttribute('loading', 'lazy');
ifr.setAttribute('tabindex', '-1');
ifr.setAttribute('title', slug + ' 预览');
ifr.src = c.files.html;
hookIframeTheme(ifr);
thumb.appendChild(ifr);
});
}, { rootMargin: '400px' });
grid.querySelectorAll('.ov-card').forEach(function (card) { thumbObserver.observe(card); });
}
function rebuildOverviewGrid(grid) {
var q = state.ovQ.trim().toLowerCase();
grid.innerHTML = '';
comps.filter(function (c) {
if (state.ovCat && c.category !== state.ovCat) return false;
if (!q) return true;
var n = splitName(c.name);
return c.slug.toLowerCase().indexOf(q) >= 0 ||
n.zh.toLowerCase().indexOf(q) >= 0 ||
n.en.toLowerCase().indexOf(q) >= 0;
}).forEach(function (c) {
var n = splitName(c.name);
var card = el('div', 'ov-card');
card.setAttribute('data-slug', c.slug);
var thumb = el('div', 'ov-thumb');
thumb.appendChild(el('div', 'ph', '加载预览…'));
card.appendChild(thumb);
var info = el('div', 'ov-info');
info.innerHTML = '<span class="zh"></span><span class="en"></span>';
info.querySelector('.zh').textContent = n.zh;
info.querySelector('.en').textContent = n.en;
card.appendChild(info);
card.addEventListener('click', function () { location.hash = '#/component/' + c.slug; });
grid.appendChild(card);
if (thumbObserver) thumbObserver.observe(card);
});
if (!grid.children.length) grid.appendChild(el('div', 'sr-empty', '无匹配组件'));
}
function renderOverviewFilter(inner, grid) {
var bar = el('div', 'ov-filter');
var chips = el('div', 'filter-chips');
function chip(label, val, active) {
var b = el('button', 'filter-chip' + (active ? ' active' : ''), esc(label));
b.type = 'button';
b.addEventListener('click', function () {
state.ovCat = val;
chips.querySelectorAll('.filter-chip').forEach(function (x) { x.classList.remove('active'); });
b.classList.add('active');
rebuildOverviewGrid(grid);
});
return b;
}
chips.appendChild(chip('全部 (' + comps.length + ')', '', state.ovCat === ''));
CAT_ORDER.forEach(function (cat) {
var n = comps.filter(function (c) { return c.category === cat; }).length;
chips.appendChild(chip(catLabel(cat) + ' (' + n + ')', cat, state.ovCat === cat));
});
bar.appendChild(chips);
var inp = el('input', 'filter-input');
inp.type = 'search';
inp.placeholder = '过滤组件…';
inp.value = state.ovQ;
inp.addEventListener('input', function (e) {
state.ovQ = e.target.value;
rebuildOverviewGrid(grid);
});
bar.appendChild(inp);
/* 过滤条插在描述之后、网格之前 */
var desc = inner.querySelector('.page-desc');
if (desc && desc.nextSibling) inner.insertBefore(bar, desc.nextSibling);
else inner.appendChild(bar);
}
/* ---------- 设计规范页 ---------- */
function renderDesign(content) {
content.innerHTML = '';
var inner = el('div', 'page-inner');
inner.appendChild(el('h1', 'page-title', '设计规范'));
inner.appendChild(el('p', 'page-desc', 'Aurora Admin 设计系统的全部设计令牌,共 <strong>' + data.tokens.length + '</strong> 项。所有组件均基于这些令牌构建,来源 <code>colors_and_type.css</code>。'));
var T = data.tokens;
function tok(prefix) {
return T.filter(function (t) { return t.name.indexOf(prefix) === 0; });
}
function swatchGrid(list) {
var g = el('div', 'tok-grid');
list.forEach(function (t) {
var card = el('div', 'tok-card');
var sw = el('div', 'tok-swatch');
sw.style.background = t.value;
card.appendChild(sw);
var meta = el('div', 'tok-meta');
meta.innerHTML = '<div class="t-name">--' + esc(t.name) + '</div><div class="t-val">' + esc(t.value) + '</div>' +
(t.comment ? '<div class="t-cmt">' + esc(t.comment) + '</div>' : '');
card.appendChild(meta);
g.appendChild(card);
});
return g;
}
function group(title, node) {
var g = el('div', 'tok-group');
g.appendChild(el('h2', null, title));
g.appendChild(node);
inner.appendChild(g);
}
var colorDefs = [
['品牌色', function (t) { return /^au-color-(brand|secondary)/.test(t.name); }],
['背景与表面', function (t) { return /^au-color-(page-bg|card-bg|table-header-bg|disabled-bg|tag-green-bg)/.test(t.name); }],
['文字色', function (t) { return /^au-color-text/.test(t.name); }],
['边框', function (t) { return /^au-color-border/.test(t.name); }],
['语义色', function (t) { return /^au-color-(success|warning|error|info)/.test(t.name); }],
['遮罩与焦点', function (t) { return /^au-color-(mask|focus)/.test(t.name); }]
];
colorDefs.forEach(function (def) {
var list = T.filter(def[1]);
if (list.length) group('色彩 · ' + def[0], swatchGrid(list));
});
var typeTokens = T.filter(function (t) { return /^au-(type|font)-/.test(t.name); });
if (typeTokens.length) {
var wrap = el('div');
var families = typeTokens.filter(function (t) { return t.name.indexOf('au-font') === 0; });
var sizes = typeTokens.filter(function (t) { return t.name.indexOf('au-type') === 0; });
['display', 'h1', 'h2', 'body', 'caption'].forEach(function (k) {
var group2 = sizes.filter(function (t) { return t.name.indexOf('au-type-' + k) === 0; });
if (!group2.length) return;
var size = (group2.find(function (t) { return /-size/.test(t.name); }) || {}).value || '';
var height = (group2.find(function (t) { return /-height/.test(t.name); }) || {}).value || '';
var weight = (group2.find(function (t) { return /-weight/.test(t.name); }) || {}).value || '400';
var row = el('div', 'type-row');
var sample = el('div', 'type-sample', 'Aa 字体示例');
sample.style.fontSize = size;
sample.style.fontWeight = weight;
sample.style.lineHeight = height;
row.appendChild(sample);
row.appendChild(el('div', 'type-meta', k.toUpperCase() + ' · ' + size + ' / ' + height + ' · ' + weight));
wrap.appendChild(row);
});
families.forEach(function (t) {
var row = el('div', 'type-row');
row.appendChild(el('div', 'type-sample', '字体族 Aa 123'));
row.appendChild(el('div', 'type-meta', '--' + t.name + ' · ' + esc(t.value)));
wrap.appendChild(row);
});
group('字体', wrap);
}
var spaces = tok('au-space-');
if (spaces.length) {
var wrap2 = el('div');
spaces.forEach(function (t) {
var px = parseInt(t.value, 10) || 0;
var row = el('div', 'space-row');
var bar = el('div', 'space-bar');
bar.style.width = Math.max(px, 8) + 'px';
row.appendChild(bar);
row.appendChild(el('div', 'space-meta', '--' + t.name + ' = ' + t.value + (t.comment ? ' · ' + t.comment : '')));
wrap2.appendChild(row);
});
group('间距(4px 基准)', wrap2);
}
var radii = tok('au-radius-');
if (radii.length) {
var wrap3 = el('div', 'radius-row');
radii.forEach(function (t) {
var d = el('div', 'radius-demo', t.name.replace('au-radius-', '') + ' ' + t.value);
d.style.borderRadius = t.value;
wrap3.appendChild(d);
});
group('圆角', wrap3);
}
var shadows = tok('au-shadow-');
if (shadows.length) {
var wrap4 = el('div', 'shadow-row');
shadows.forEach(function (t) {
var d = el('div', 'shadow-demo', t.name.replace('au-shadow-', '') + ':' + (t.comment || ''));
d.style.boxShadow = t.value;
wrap4.appendChild(d);
});
group('阴影(3 层克制投影)', wrap4);
}
var ctrls = tok('au-control-height');
if (ctrls.length) {
var wrap5 = el('div');
ctrls.forEach(function (t) {
var row = el('div', 'ctrl-row');
var pill = el('div', 'ctrl-pill', t.value);
pill.style.height = t.value;
pill.style.padding = '0 16px';
row.appendChild(pill);
row.appendChild(el('div', 'space-meta', '--' + t.name + ' = ' + t.value));
wrap5.appendChild(row);
});
group('控件高度', wrap5);
}
content.appendChild(inner);
}
/* ---------- 轻量零依赖语法高亮 (Syntax Highlighting) ---------- */
function highlightCode(src) {
if (!src) return '';
var tokens = [];
/* 注意:本函数在步骤 3 之后文本已是转义态,saveToken 不再重复转义 */
function saveToken(content, cls) {
tokens.push('<span class="' + cls + '">' + content + '</span>');
return '___HL_' + (tokens.length - 1) + '___';
}
// 1. 注释(原始源码,需转义)
var s = src.replace(/<!--[\s\S]*?-->|\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, function (m) {
return saveToken(esc(m), 'hl-com');
});
// 2. 字符串(原始源码,需转义)
s = s.replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`[^`]*`/g, function (m) {
return saveToken(esc(m), 'hl-str');
});
// 3. 基础转义(此后文本已是转义态)
s = esc(s);
// 4. 属性(优先于标签匹配)
s = s.replace(/(\s+)([@:v-]?[a-zA-Z0-9_-]+)(=)/g, function (m, p1, p2, p3) {
return p1 + saveToken(p2, 'hl-attr') + p3;
});
// 5. 标签
s = s.replace(/(&lt;\/?[a-zA-Z0-9_-]+)/g, function (m) {
return saveToken(m, 'hl-tag');
});
// 6. 关键字
var kw = /\b(const|let|var|function|return|import|export|from|default|if|else|defineProps|defineEmits|ref|computed|reactive|watch)\b/g;
s = s.replace(kw, function (m) {
return saveToken(m, 'hl-kw');
});
// 7. 还原所有 token
s = s.replace(/___HL_(\d+)___/g, function (_, i) {
return tokens[Number(i)];
});
return s;
}
/* ---------- 组件 API 提炼 (Props / Events / Slots) ---------- */
function extractComponentAPI(c) {
var v3 = (c.sources && c.sources.vue3) || '';
var v2 = (c.sources && c.sources.vue2) || '';
var jsx = (c.sources && c.sources.jsx) || '';
var code = v3 || v2;
var props = [];
var emits = [];
var slots = [];
/* 1. 提炼 Props
策略:括号深度扫描定位对象体(兼容单行/多行/嵌套对象/箭头函数/字符串内花括号),
再按「顶层逗号」分段解析每个 prop。
覆盖写法:defineProps({...}) / props: {...} (Vue2) / 兜底 JSX 解构参数 */
function scanObjectAt(src, openIdx) {
var depth = 0, inStr = null, escCh = false, end = -1;
for (var i = openIdx; i < src.length; i++) {
var ch = src.charAt(i);
if (inStr) {
if (escCh) { escCh = false; continue; }
if (ch === '\\') { escCh = true; continue; }
if (ch === inStr) inStr = null;
continue;
}
if (ch === '"' || ch === "'" || ch === '`') { inStr = ch; continue; }
if (ch === '{') depth++;
else if (ch === '}') { depth--; if (depth === 0) { end = i; break; } }
}
return end < 0 ? null : src.slice(openIdx, end + 1);
}
function stripOuterBraces(s) { return s.replace(/^\s*\{/, '').replace(/\}\s*$/, ''); }
function splitTopLevel(body) {
var segs = [], cur = '', depth = 0, inStr = null, escCh = false;
for (var i = 0; i < body.length; i++) {
var ch = body.charAt(i);
if (inStr) {
cur += ch;
if (escCh) { escCh = false; continue; }
if (ch === '\\') { escCh = true; continue; }
if (ch === inStr) inStr = null;
continue;
}
if (ch === '"' || ch === "'" || ch === '`') { inStr = ch; cur += ch; continue; }
if (ch === '{' || ch === '[' || ch === '(') { depth++; cur += ch; continue; }
if (ch === '}' || ch === ']' || ch === ')') { depth--; cur += ch; continue; }
if (ch === ',' && depth === 0) { segs.push(cur); cur = ''; continue; }
cur += ch;
}
if (cur.trim()) segs.push(cur);
return segs;
}
var propsBody = null;
var dpM = /defineProps\s*\(/.exec(code);
if (dpM) {
var openIdx = code.indexOf('{', dpM.index);
if (openIdx >= 0) propsBody = stripOuterBraces(scanObjectAt(code, openIdx) || '');
}
if (!propsBody) {
var prM = /props\s*:\s*\{/.exec(code);
if (prM) {
var openIdx2 = prM.index + prM[0].length - 1;
propsBody = stripOuterBraces(scanObjectAt(code, openIdx2) || '');
}
}
if (propsBody && propsBody.trim()) {
splitTopLevel(propsBody).forEach(function (seg) {
var m = seg.match(/^\s*([a-zA-Z0-9_$]+)\s*:\s*([\s\S]*)$/);
if (!m) return;
var name = m[1], rest = m[2];
var cM = rest.match(/\/\/\s*([^\n]+)\s*$/);
props.push(finalizeProp({ name: name, comment: cM ? cM[1].trim() : '', raw: rest }));
});
} else if (jsx) {
var fnM = jsx.match(/export default function\s+[A-Za-z0-9_$]+\s*\(\s*\{([^}]+)\}/);
if (fnM) {
var params = fnM[1].split(',');
params.forEach(function (param) {
var p = param.trim();
if (!p || p.indexOf('...') === 0) return;
var parts = p.split('=');
var name = parts[0].trim();
var def = parts[1] ? parts[1].trim() : '—';
if (name.indexOf('on') === 0 && /[A-Z]/.test(name.charAt(2))) {
emits.push({ name: name, desc: '回调触发事件 (Event callback)', params: '(event: any) => void' });
} else if (name === 'children') {
slots.push({ name: 'default', desc: '子节点内容 (children)' });
} else {
props.push({ name: name, type: 'any', def: def, req: false, desc: '—' });
}
});
}
}
/* 2. 提炼 Emits */
var mEmits = code.match(/defineEmits\s*\(\s*(\[[^\]]+\])\s*\)/);
if (mEmits) {
try {
var emitArr = (new Function('return ' + mEmits[1]))();
emitArr.forEach(function (e) {
var desc = e.indexOf('update:') === 0 ? '双向绑定更新事件 (v-model)' : '组件交互触发事件';
if (!emits.some(function (x) { return x.name === e; })) {
emits.push({ name: e, desc: desc, params: '(val: any) => void' });
}
});
} catch (err) {}
}
var emitRegex = /\$emit\(\s*['"]([a-zA-Z0-9_:-]+)['"]/g;
var eMatch;
while ((eMatch = emitRegex.exec(code)) !== null) {
var eName = eMatch[1];
if (!emits.some(function (x) { return x.name === eName; })) {
emits.push({ name: eName, desc: '交互回调触发事件', params: '—' });
}
}
/* 3. 提炼 Slots */
var slotRegex = /<slot(?:\s+name=["']([^"']+)["'])?/g;
var sMatch;
while ((sMatch = slotRegex.exec(code)) !== null) {
var sName = sMatch[1] || 'default';
if (!slots.some(function (x) { return x.name === sName; })) {
slots.push({ name: sName, desc: sName === 'default' ? '默认插槽(组件文本或主体内容)' : sName + ' 自定义扩展插槽' });
}
}
return { props: props, emits: emits, slots: slots };
}
/* 从 str 起点扫描到「顶层逗号/字符串感知」为止,返回该段值文本 */
function scanTopLevelValue(str) {
var depth = 0, inStr = null, escCh = false;
for (var i = 0; i < str.length; i++) {
var ch = str.charAt(i);
if (inStr) {
if (escCh) { escCh = false; continue; }
if (ch === '\\') { escCh = true; continue; }
if (ch === inStr) inStr = null;
continue;
}
if (ch === '"' || ch === "'" || ch === '`') { inStr = ch; continue; }
if (ch === '{' || ch === '[' || ch === '(') depth++;
else if (ch === '}' || ch === ']' || ch === ')') { if (depth === 0) return str.slice(0, i); depth--; }
else if (ch === ',' && depth === 0) return str.slice(0, i);
}
return str;
}
function finalizeProp(p) {
var type = 'any';
var def = '—';
var req = false;
var typeM = p.raw.match(/type\s*:\s*(\[[^\]]+\]|[a-zA-Z0-9_$]+)/);
if (typeM) type = typeM[1].replace(/\[|\]/g, '').replace(/,\s*/g, ' | ');
var defM = /default\s*:/.exec(p.raw);
if (defM) {
var val = scanTopLevelValue(p.raw.slice(defM.index + defM[0].length)).trim();
def = val.replace(/\(\)\s*=>\s*/, '') || '—';
if (def === '()') def = '—';
/* 美化多行默认值(如数组工厂)为单行紧凑形式 */
def = def.replace(/\s*\n\s*/g, ' ').replace(/\s{2,}/g, ' ');
if (def.length > 48) def = def.slice(0, 45) + '…';
}
if (/required\s*:\s*true/.test(p.raw)) req = true;
return { name: p.name, type: type, def: def, req: req, desc: p.comment || '—' };
}
/* 渲染 API 规范表格 */
function buildApiSection(api) {
var wrap = el('div', 'api-section');
if (api.props && api.props.length) {
wrap.appendChild(el('h3', 'api-subhead', '属性 (Props)'));
var tblP = el('table', 'api-table');
tblP.innerHTML = '<thead><tr><th>参数名</th><th>说明</th><th>类型</th><th>默认值</th><th>必填</th></tr></thead>';
var tbP = el('tbody');
api.props.forEach(function (p) {
var tr = el('tr');
tr.innerHTML =
'<td class="prop-name"><code>' + esc(p.name) + '</code></td>' +
'<td class="prop-desc">' + esc(p.desc) + '</td>' +
'<td class="prop-type"><code>' + esc(p.type) + '</code></td>' +
'<td class="prop-def"><code>' + esc(p.def) + '</code></td>' +
'<td class="prop-req">' + (p.req ? '<span class="badge-req">必填</span>' : '<span class="badge-opt">可选</span>') + '</td>';
tbP.appendChild(tr);
});
tblP.appendChild(tbP);
wrap.appendChild(tblP);
}
if (api.emits && api.emits.length) {
wrap.appendChild(el('h3', 'api-subhead', '事件 (Events / Emits)'));
var tblE = el('table', 'api-table');
tblE.innerHTML = '<thead><tr><th>事件名</th><th>说明</th><th>回调参数</th></tr></thead>';
var tbE = el('tbody');
api.emits.forEach(function (e) {
var tr = el('tr');
tr.innerHTML =
'<td class="prop-name"><code>@' + esc(e.name) + '</code></td>' +
'<td class="prop-desc">' + esc(e.desc) + '</td>' +
'<td class="prop-type"><code>' + esc(e.params || '—') + '</code></td>';
tbE.appendChild(tr);
});
tblE.appendChild(tbE);
wrap.appendChild(tblE);
}
if (api.slots && api.slots.length) {
wrap.appendChild(el('h3', 'api-subhead', '插槽 (Slots)'));
var tblS = el('table', 'api-table');
tblS.innerHTML = '<thead><tr><th>插槽名</th><th>说明</th></tr></thead>';
var tbS = el('tbody');
api.slots.forEach(function (s) {
var tr = el('tr');
tr.innerHTML =
'<td class="prop-name"><code>#' + esc(s.name) + '</code></td>' +
'<td class="prop-desc">' + esc(s.desc) + '</td>';
tbS.appendChild(tr);
});
tblS.appendChild(tbS);
wrap.appendChild(tblS);
}
return wrap;
}
/* 渲染无障碍键盘指引 */
function buildA11ySection(c) {
var wrap = el('div', 'a11y-section');
var intro = el('p', 'a11y-intro', '遵循 W3C WAI-ARIA 规范,组件内置标准键盘交互与屏幕阅读器无障碍支持。');
wrap.appendChild(intro);
var tbl = el('table', 'api-table');
tbl.innerHTML = '<thead><tr><th style="width:160px">按键</th><th>交互说明</th></tr></thead>';
var tb = el('tbody');
var rules = [
{ key: 'Tab', desc: '将焦点移入当前组件或在可聚焦子项间依次流转' },
{ key: 'Shift + Tab', desc: '反向移动焦点到上一个交互元素' }
];
if (/button|link|action/.test(c.slug) || c.category === 'general') {
rules.push({ key: 'Enter / Space', desc: '触发按钮点击动作,执行绑定的业务逻辑' });
} else if (/input|mention|autocomplete/.test(c.slug)) {
rules.push({ key: 'Enter', desc: '确认输入内容或提交当前表单' });
rules.push({ key: 'Esc', desc: '清除当前临时输入或收起下拉联想菜单' });
} else if (/select|cascader|dropdown|picker/.test(c.slug)) {
rules.push({ key: 'Enter / Space', desc: '展开或收起下拉候选列表' });
rules.push({ key: '↑ / ↓', desc: '在下拉选项菜单中高亮切换候选项目' });
rules.push({ key: 'Esc', desc: '收起下拉菜单并保持输入框焦点' });
} else if (/modal|dialog|drawer|sheet/.test(c.slug) || c.category === 'feedback') {
rules.push({ key: 'Esc', desc: '快捷关闭当前弹窗并还原底层焦点' });
rules.push({ key: 'Tab (循环锁定)', desc: '弹窗激活时,焦点锁定在弹窗内部,不会跳出到蒙层外部背景' });
} else if (/tabs|steplist|steps|segmented/.test(c.slug) || c.category === 'navigation') {
rules.push({ key: '← / →', desc: '在导航标签项或步骤之间切换激活状态' });
rules.push({ key: 'Home / End', desc: '快速定位到首个或末尾标签项' });
} else if (/table|tree/.test(c.slug)) {
rules.push({ key: '↑ / ↓', desc: '在数据行或树形节点之间上下导航' });
rules.push({ key: 'Space', desc: '快速切换当前行或节点的勾选状态' });
}
rules.forEach(function (r) {
var tr = el('tr');
tr.innerHTML = '<td class="a11y-key"><kbd>' + esc(r.key) + '</kbd></td><td class="prop-desc">' + esc(r.desc) + '</td>';
tb.appendChild(tr);
});
tbl.appendChild(tb);
wrap.appendChild(tbl);
return wrap;
}
/* 渲染设计避坑原则 (Do\'s & Don\'ts) */
function buildDosAndDonts(c) {
var grid = el('div', 'dodont-grid');
var cardDo = el('div', 'dodont-card do');
var cardDont = el('div', 'dodont-card dont');
cardDo.appendChild(el('div', 'dodont-title', '✅ 推荐做法 (Do)'));
cardDont.appendChild(el('div', 'dodont-title', '❌ 避免反模式 (Don\'t)'));
var ulDo = el('ul');
var ulDont = el('ul');
var dos = [];
var donts = [];
if (c.contract && c.contract.usage && c.contract.usage.length) {
dos = c.contract.usage.slice(0, 3);
}
if (c.contract && c.contract.doNot && c.contract.doNot.length) {
donts = c.contract.doNot.slice(0, 3);
}
if (!dos.length) {
if (/button/.test(c.slug)) {
dos = ['主行动区原则上只保留一个 Primary 主按钮', '危险破坏性操作配合二次确认弹窗使用', '加载中状态下设置 disabled 避免二次误触'];
donts = ['避免在同一行并列 3 个以上相同颜色的主按钮', '避免在表单提交操作中使用弱化文字按钮', '不要在非页面跳转场景滥用链接按钮'];
} else if (/input/.test(c.slug)) {
dos = ['输入框建议配合明确的 Label 或 Placeholder', '密码及敏感凭据字段提供显隐切换图标', '必填项在标签旁显式标注红星 (*)'];
donts = ['避免仅依赖 Placeholder 传达关键业务提示', '避免移动端使用过长不可换行的输入区域', '不要在禁用态下保留光标闪烁效果'];
} else if (/table/.test(c.slug)) {
dos = ['列数较多(≥8列)时开启固定列与横向滚动', '长列表数据采用分页或虚拟滚动承载', '空数据状态提供具体的引导图文'];
donts = ['避免单元格无限制换行撑大表格高度', '避免全表无边框导致复杂数据对比混淆', '不要在无分页情况下一次性渲染数百行 DOM'];
} else if (/modal/.test(c.slug)) {
dos = ['弹窗宽度应适配内容复杂度,标准为 520px', '重要输入表单弹窗阻止点击蒙层空白处误关闭', '弹窗关闭后应将焦点自动交还触发按钮'];
donts = ['避免多层弹窗连续嵌套破坏视觉层级', '避免在弹窗内塞入两屏以上的长篇表单', '不要无确定按钮直接让用户点击右上角叉号关闭'];
} else {
dos = ['优先遵循 Aurora Admin 统一设计令牌体系', '保持组件语义化标签,便于辅助工具识别', '复杂业务合理组合基础原子组件'];
donts = ['避免自行通过 style 强行覆盖组件内边距', '避免自创非常规控件高度破坏栅格对齐', '不要移除原生的键盘焦点可视化轮廓'];
}
}
dos.forEach(function (d) { ulDo.appendChild(el('li', null, esc(d))); });
donts.forEach(function (d) { ulDont.appendChild(el('li', null, esc(d))); });
cardDo.appendChild(ulDo);
cardDont.appendChild(ulDont);
grid.appendChild(cardDo);
grid.appendChild(cardDont);
return grid;
}
/* ---------- 组件详情 ---------- */
function renderComponent(content, slug) {
var c = bySlugMap[slug];
content.innerHTML = '';
var inner = el('div', 'page-inner');
content.appendChild(inner);
/* 头部:Element 风格 —— 大标题 + 一句话描述 */
var head = el('div', 'detail-head');
var n = splitName(c.name);
var h1 = el('h1');
h1.appendChild(document.createTextNode(n.zh));
var enSpan = el('span', 'en', esc(n.en));
h1.appendChild(enSpan);
head.appendChild(h1);
var desc = cleanSummary(c.specSummary);
if (desc) head.appendChild(el('p', 'page-desc', esc(desc)));
var chips = el('div', 'chips');
chips.appendChild(el('span', 'chip blue', c.tier === 'core' ? '核心规范组件' : '扩展组件'));
chips.appendChild(el('span', 'chip', catLabel(c.category)));
var batchA = el('a', 'chip', batchLabel(c.specBatch) + ' · ' + c.specFile);
batchA.href = specLink(c.specFile);
batchA.target = '_blank';
chips.appendChild(batchA);
if (c.contractRef) {
var ca = el('a', 'chip blue', '契约 JSON ✓');
ca.href = contractLink(c);
ca.target = '_blank';
chips.appendChild(ca);
} else {
var srcChip = el('span', 'chip', 'API 源码提炼');
srcChip.title = '该组件暂无契约 JSON,API 表由 5 端源码自动提炼';
chips.appendChild(srcChip);
}
chips.appendChild(el('span', 'chip', 'slug: ' + c.slug));
head.appendChild(chips);
/* 测试页直达入口 */
var testLink = el('a', 'chip', '测试页 →');
testLink.href = '../tests/' + c.slug + '.html';
testLink.target = '_blank';
head.appendChild(testLink);
inner.appendChild(head);
var tocItems = [];
/* 1. 何时使用(规格说明) */
var secSpec = el('div', 'sec');
secSpec.id = 'spec';
secSpec.appendChild(el('h2', null, '何时使用'));
var box = el('div', 'spec-box');
if (c.specLines && c.specLines.length) {
/* specLines 兼容数组与单字符串两种形态 */
renderSpecLines(box, Array.isArray(c.specLines) ? c.specLines : String(c.specLines).split('\n'));
} else {
box.appendChild(el('p', null, '本组件暂无独立规格说明,参见 ' + esc(c.specFile) + '。'));
}
secSpec.appendChild(box);
inner.appendChild(secSpec);
tocItems.push({ id: 'spec', label: '何时使用' });
/* 2. 代码演示 */
var secDemo = el('div', 'sec');
secDemo.id = 'demo';
secDemo.appendChild(el('h2', null, '代码演示'));
var demoIntro = el('div', 'demo-intro');
demoIntro.innerHTML = '<h3>基础用法</h3><p></p><p class="demo-note"></p>';
demoIntro.querySelector('p').textContent = desc || '完整的组件演示,包含常用状态与场景。';
demoIntro.querySelector('.demo-note').textContent =
'视觉演示为 H5 形态(各端实现视觉一致);下方代码与资源已按「' + FRAMEWORKS[state.framework] + '」过滤。';
secDemo.appendChild(demoIntro);
/* 2.1 场景化微用例卡片:拆分演示页里的每个场景(h2 分组) */
var scenarioCard = buildScenarioCard(c);
if (scenarioCard) secDemo.appendChild(scenarioCard);
secDemo.appendChild(buildDemoCard(c));
inner.appendChild(secDemo);
tocItems.push({ id: 'demo', label: '代码演示' });
/* 2.5 组件 API 参数规范表(Props / Events / Slots) */
var apiData = extractComponentAPI(c);
if (apiData.props.length || apiData.emits.length || apiData.slots.length) {
var secApi = el('div', 'sec');
secApi.id = 'api';
secApi.appendChild(el('h2', null, '组件 API 规范'));
secApi.appendChild(buildApiSection(apiData));
inner.appendChild(secApi);
tocItems.push({ id: 'api', label: '组件 API' });
}
/* 2.6 无障碍指引(A11y & Keyboard) */
var secA11y = el('div', 'sec');
secA11y.id = 'a11y';
secA11y.appendChild(el('h2', null, '无障碍与键盘交互'));
secA11y.appendChild(buildA11ySection(c));
inner.appendChild(secA11y);
tocItems.push({ id: 'a11y', label: '无障碍指引' });
/* 2.7 设计避坑指南(Do\'s & Don\'ts) */
var secGuide = el('div', 'sec');
secGuide.id = 'guidelines';
secGuide.appendChild(el('h2', null, '设计避坑指南 (Do\'s & Don\'ts)'));
secGuide.appendChild(buildDosAndDonts(c));
inner.appendChild(secGuide);
tocItems.push({ id: 'guidelines', label: '设计避坑' });
/* 3. 设计契约(核心组件) */
if (c.contract) {
var secCon = el('div', 'sec');
secCon.id = 'contract';
secCon.appendChild(el('h2', null, '设计契约'));
secCon.appendChild(buildContract(c.contract));
inner.appendChild(secCon);
tocItems.push({ id: 'contract', label: '设计契约' });
}
/* 4. 实现资源 */
var secFiles = el('div', 'sec');
secFiles.id = 'files';
secFiles.appendChild(el('h2', null, '实现资源'));
secFiles.appendChild(buildFilesTable(c));
inner.appendChild(secFiles);
tocItems.push({ id: 'files', label: '实现资源' });
/* 5. 相关组件(交叉引用):同分类近邻 + 名称关键词映射(TDesign「相似组件」模式) */
var related = buildRelated(c);
if (related.length) {
var secRel = el('div', 'sec');
secRel.id = 'related';
secRel.appendChild(el('h2', null, '相关组件'));
secRel.appendChild(el('p', 'page-desc', '按「相似功能 / 常搭配使用」维度整理,点击跳转。'));
var relRow = el('div', 'rel-row');
related.forEach(function (r) {
var n = splitName(r.name);
var a = el('a', 'rel-card');
a.href = '#/component/' + r.slug;
a.innerHTML = '<strong></strong><span></span><i class="rel-why"></i>';
a.querySelector('strong').textContent = n.zh;
a.querySelector('span').textContent = n.en;
a.querySelector('.rel-why').textContent = r.why;
relRow.appendChild(a);
});
secRel.appendChild(relRow);
inner.appendChild(secRel);
tocItems.push({ id: 'related', label: '相关组件' });
}
/* 上一篇/下一篇 */
var idx2 = comps.indexOf(c);
var prev = comps[idx2 - 1], next = comps[idx2 + 1];
if (prev || next) {
var pager = el('div', 'pager');
if (prev) {
var a1 = el('a', null);
a1.href = '#/component/' + prev.slug;
a1.innerHTML = '<span class="dir">← 上一篇</span><span class="name"></span>';
a1.querySelector('.name').textContent = splitName(prev.name).zh;
pager.appendChild(a1);
} else {
pager.appendChild(el('div', null, ''));
}
if (next) {
var a2 = el('a', 'next');
a2.href = '#/component/' + next.slug;
a2.innerHTML = '<span class="dir">下一篇 →</span><span class="name"></span>';
a2.querySelector('.name').textContent = splitName(next.name).zh;
pager.appendChild(a2);
}
inner.appendChild(pager);
}
renderToc(tocItems);
}
/* 交叉引用:相似/搭配组件映射表(关键词 → 组件 slug 组),加同分类近邻兜底 */
var RELATED_MAP = [
{ match: /modal|confirmmodal|alertmodal|formmodal|fullscreenmodal|popconfirm|bottomsheet/, slugs: ['modal', 'confirmmodal', 'popconfirm', 'formmodal'], why: '同属弹层家族,按承载内容与打断强度选择' },
{ match: /table|treetable|expandabletable|mergedcelltable|summaryrowtable|fixedcolumntable|cardlist/, slugs: ['table', 'treetable', 'cardlist'], why: '数据列表的不同形态,按层级与信息量选择' },
{ match: /button|buttongroup|segmented/, slugs: ['buttongroup', 'segmented', 'radiocard'], why: '操作触发与选项切换的相近形态' },
{ match: /input|autocomplete|mention|codeinput|passwordinput|phoneinput|bankcardinput|idcardinput|plateinput/, slugs: ['autocomplete', 'input', 'mention'], why: '输入类组件,按提示方式与约束选择' },
{ match: /select|cascader|listpicker|transfer/, slugs: ['select', 'cascader', 'transfer'], why: '选择类组件,按层级深度与数量选择' },
{ match: /rate|slider|numberrangeinput/, slugs: ['slider', 'rate', 'numberrangeinput'], why: '数值输入的相近形态' },
{ match: /radiocard|checkboxcard|switchgroup/, slugs: ['radiocard', 'checkboxcard', 'switchgroup'], why: '卡片式选项与开关组合' },
{ match: /tabs|enhancedtabnav|pagetabs|steplist|steps/, slugs: ['tabs', 'steps', 'steplist'], why: '流程与视图组织的相近形态' }
];
function buildRelated(c) {
var seen = {};
var out = [];
RELATED_MAP.forEach(function (rule) {
var key = c.slug + '|' + c.name;
if (!rule.match.test(c.slug) && !rule.match.test(c.name)) return;
rule.slugs.forEach(function (s) {
if (s === c.slug || seen[s]) return;
var t = bySlugMap[s];
if (!t) return;
seen[s] = true;
out.push({ slug: s, name: t.name, why: rule.why });
});
});
/* 同分类近邻兜底,凑满 3 个 */
if (out.length < 3) {
comps.filter(function (x) { return x.category === c.category && x.slug !== c.slug && !seen[x.slug]; })
.slice(0, 3 - out.length)
.forEach(function (t) { seen[t.slug] = true; out.push({ slug: t.slug, name: t.name, why: '同属' + catLabel(c.category) + '分类' }); });
}
return out.slice(0, 4);
}
/* 规格说明渲染:- 列表 / 表格 / 普通段落 */
function renderSpecLines(box, lines) {
var list = null;
lines.forEach(function (raw) {
var line = raw.trim();
if (!line) { list = null; return; }
if (line.charAt(0) === '|' && line.charAt(line.length - 1) === '|') {
list = null;
var cells = line.split('|').slice(1, -1).map(function (s) { return s.trim(); });
if (cells.every(function (s) { return /^[-: ]*$/.test(s); })) return;
var tbl = box.querySelector('table:last-of-type');
if (!tbl || tbl.dataset.done) {
tbl = el('table');
tbl.dataset.done = '1';
var thead = el('thead');
var trh = el('tr');
cells.forEach(function (cell) { trh.appendChild(el('th', null, esc(cell))); });
thead.appendChild(trh);
tbl.appendChild(thead);
var tbody = el('tbody');
tbl.appendChild(tbody);
box.appendChild(tbl);
} else {
var tr = el('tr');
cells.forEach(function (cell) { tr.appendChild(el('td', null, esc(cell))); });
tbl.querySelector('tbody').appendChild(tr);
}
return;
}
if (/^[-•·]\s+/.test(line) || /^\d+[.、]\s*/.test(line)) {
if (!list) { list = el('ul'); box.appendChild(list); }
list.appendChild(el('li', null, esc(line.replace(/^[-•·]\s+/, ''))));
return;
}
list = null;
box.appendChild(el('p', null, esc(line)));
});
}
/* 代码演示卡片(Element 风格:底部居中箭头展开条) */
/* 场景化微用例卡片
从 H5 演示源码中提取 <h2> 场景分组标题,生成「场景速览」条。
点击场景 = 同源 iframe 内定位到对应分组(file:// 下自动降级隐藏跳转)。 */
function extractScenarios(c) {
var html = (c.sources && c.sources.html) || '';
var bodyM = html.match(/<body[^>]*>([\s\S]*)<\/body>/i);
var body = bodyM ? bodyM[1] : html;
var out = [];
var re = /<h2[^>]*>([\s\S]*?)<\/h2>/gi;
var m;
while ((m = re.exec(body)) !== null) {
var text = m[1].replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();
/* 去掉演示页里的「1. 」序号前缀,让场景名更干净 */
text = text.replace(/^\d+[.、]\s*/, '');
if (text) out.push(text);
}
return out;
}
function buildScenarioCard(c) {
var scenarios = extractScenarios(c);
if (scenarios.length < 2) return null;
var wrap = el('div', 'scenario-wrap');
wrap.appendChild(el('div', 'scenario-title', '场景速览(' + scenarios.length + ' 个典型用法)'));
var row = el('div', 'scenario-row');
scenarios.forEach(function (name, i) {
var chip = el('button', 'scenario-chip');
chip.type = 'button';
chip.textContent = (i + 1) + '. ' + name;
chip.addEventListener('click', function () {
/* 同源 iframe 内定位:给 H5 演示页 h2 动态打 id 后 scrollIntoView */
var iframe = wrap.parentElement ? wrap.parentElement.querySelector('.demo-stage iframe') : null;
if (!iframe) return;
try {
var doc = iframe.contentDocument;
if (!doc) throw new Error('cross-origin');
var hs = doc.querySelectorAll('h2');
for (var k = 0; k < hs.length; k++) {
var t = (hs[k].textContent || '').replace(/\s+/g, ' ').trim();
if (t === name) {
doc.documentElement.style.scrollBehavior = 'smooth';
hs[k].scrollIntoView({ block: 'start' });
hs[k].style.transition = 'color .2s';
hs[k].style.color = '#2F54EB';
(function (h) { setTimeout(function () { h.style.color = ''; }, 900); })(hs[k]);
return;
}
}
} catch (e) {
/* file:// 下 contentDocument 不可访问 → 退化为新窗口打开演示页 */
window.open(c.files.html, '_blank');
}
});
row.appendChild(chip);
});
wrap.appendChild(row);
return wrap;
}
function buildDemoCard(c) { var card = el('div', 'demo-card');
var stage = el('div', 'demo-stage');
var iframe = document.createElement('iframe');
iframe.src = c.files.html;
iframe.title = c.slug + ' 代码演示';
hookIframeTheme(iframe);
stage.appendChild(iframe);
card.appendChild(stage);
var open = false;
var codeWrap = null;
/* 在线试玩(Playground):编辑 H5 源码,srcdoc 实时重渲染,复位随时还原 */
var playbar = el('div', 'demo-playbar');
var playBtn = el('button', 'play-btn', '▶ 在线试玩');
var resetBtn = el('button', 'play-btn ghost hidden', '复位');
var exitBtn = el('button', 'play-btn ghost hidden', '退出试玩');
playBtn.type = resetBtn.type = exitBtn.type = 'button';
playBtn.title = '在线编辑 H5 演示源码,实时预览效果(不影响源文件)';
playbar.appendChild(playBtn);
playbar.appendChild(resetBtn);
playbar.appendChild(exitBtn);
card.appendChild(playbar);
var playing = false;
var ta = null;
var typeTimer = null;
function enterPlay() {
playing = true;
if (!open) strip.click(); /* 自动展开代码区 */
if (!codeWrap) { codeWrap = buildCodeArea(c); card.insertBefore(codeWrap, strip); }
if (!ta) {
ta = document.createElement('textarea');
ta.className = 'pg-textarea';
ta.spellcheck = false;
ta.value = c.sources.html || '';
ta.addEventListener('input', function () {
clearTimeout(typeTimer);
typeTimer = setTimeout(function () { iframe.srcdoc = ta.value; }, 300);
});
}
var pre = codeWrap.querySelector('pre');
if (pre) pre.classList.add('hidden');
codeWrap.appendChild(ta);
ta.classList.remove('hidden');
iframe.removeAttribute('src');
iframe.srcdoc = ta.value;
playBtn.textContent = '● 试玩中';
playBtn.classList.add('hidden');
resetBtn.classList.remove('hidden');
exitBtn.classList.remove('hidden');
}
function exitPlay() {
playing = false;
clearTimeout(typeTimer);
if (ta) ta.classList.add('hidden');
var pre = codeWrap && codeWrap.querySelector('pre');
if (pre) pre.classList.remove('hidden');
iframe.removeAttribute('srcdoc');
iframe.src = c.files.html; /* 重新加载原演示 */
playBtn.textContent = '▶ 在线试玩';
playBtn.classList.remove('hidden');
resetBtn.classList.add('hidden');
exitBtn.classList.add('hidden');
}
playBtn.addEventListener('click', enterPlay);
resetBtn.addEventListener('click', function () {
ta.value = c.sources.html || '';
iframe.srcdoc = ta.value;
});
exitBtn.addEventListener('click', exitPlay);
/* Element 风格展开条:整条居中 */
var strip = el('div', 'demo-strip');
strip.setAttribute('title', '展开代码');
strip.innerHTML = '<span class="arr">▼</span>';
strip.addEventListener('click', function () {
open = !open;
strip.classList.toggle('open', open);
strip.querySelector('.arr').textContent = open ? '▲' : '▼';
if (open && !codeWrap) { codeWrap = buildCodeArea(c); card.insertBefore(codeWrap, strip); }
if (codeWrap) codeWrap.classList.toggle('hidden', !open);
});
card.appendChild(strip);
return card;
}
function buildCodeArea(c) {
var area = el('div', 'demo-code');
var bar = el('div', 'code-bar');
var tabs = el('div', 'code-tabs');
var pre = el('pre');
var code = el('code');
pre.appendChild(code);
area.appendChild(bar);
area.appendChild(pre);
bar.appendChild(tabs);
var copyBtn = el('button', 'code-copy', '复制源码');
copyBtn.type = 'button';
bar.appendChild(copyBtn);
/* 只展示当前框架的源码形态 */
var kinds = FW_FILES[state.framework] || TAB_ORDER;
var available = kinds.filter(function (k) { return c.sources[k] != null; });
if (!available.length) available = TAB_ORDER.filter(function (k) { return c.sources[k] != null; });
var current = available[0];
available.forEach(function (k) {
var b = el('button', null, TAB_LABELS[k]);
b.type = 'button';
if (k === current) b.classList.add('active');
b.addEventListener('click', function () {
current = k;
tabs.querySelectorAll('button').forEach(function (x) { x.classList.remove('active'); });
b.classList.add('active');
code.innerHTML = highlightCode(c.sources[k] || '');
copyBtn.textContent = '复制' + (TAB_LABELS[k] || '');
});
tabs.appendChild(b);
});
copyBtn.addEventListener('click', function () {
copyText(c.sources[current] || '', copyBtn, '已复制 ✓');
});
code.innerHTML = highlightCode(c.sources[current] || '');
return area;
}
function copyText(text, btn, okMsg) {
function done() {
var old = btn.textContent;
btn.textContent = okMsg;
setTimeout(function () { btn.textContent = old; }, 1200);
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(done, function () { fallbackCopy(text); done(); });
} else {
fallbackCopy(text); done();
}
}
function fallbackCopy(text) {
var ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
try { document.execCommand('copy'); } catch (e) { /* ignore */ }
document.body.removeChild(ta);
}
/* 设计契约 */
function buildContract(ct) {
var wrap = el('div');
wrap.style.background = 'var(--au-color-card-bg)';
wrap.style.border = '1px solid var(--au-color-border)';
wrap.style.borderRadius = 'var(--au-radius-large)';
wrap.style.padding = 'var(--au-space-20)';
if (ct.dims && ct.dims.length) {
var b1 = el('div', 'contract-block');
b1.appendChild(el('h3', null, '变体维度'));
ct.dims.forEach(function (d) {
var row = el('div', 'dim-row');
row.appendChild(el('span', 'dim-name', d.name));
d.values.forEach(function (v) { row.appendChild(el('span', 'dim-chip', esc(v))); });
b1.appendChild(row);
});
wrap.appendChild(b1);
}
if (ct.variants && ct.variants.length) {
var b2 = el('div', 'contract-block');
b2.appendChild(el('h3', null, '代表变体'));
var row = el('div', 'dim-row');
ct.variants.forEach(function (v) { row.appendChild(el('span', 'dim-chip', esc(v))); });
b2.appendChild(row);
wrap.appendChild(b2);
}
if (ct.usage && ct.usage.length) {
var b3 = el('div', 'contract-block');
b3.appendChild(el('h3', null, '使用要点'));
var ul = el('ul');
ul.style.margin = '0';
ul.style.paddingLeft = '18px';
ct.usage.forEach(function (u) { ul.appendChild(el('li', null, esc(u))); });
b3.appendChild(ul);
wrap.appendChild(b3);
}
function kvBlock(title, obj) {
if (!obj) return;
var keys = Object.keys(obj);
if (!keys.length) return;
var b = el('div', 'contract-block');
b.appendChild(el('h3', null, title));
var tbl = el('table', 'kv-table');
keys.forEach(function (k) {
var tr = el('tr');
tr.appendChild(el('th', null, esc(k)));
tr.appendChild(el('td', null, esc(obj[k])));
tbl.appendChild(tr);
});
b.appendChild(tbl);
wrap.appendChild(b);
}
kvBlock('结构规格', ct.structure);
kvBlock('结构解剖', ct.anatomy);
if (ct.doNot && ct.doNot.length) {
var co1 = el('div', 'callout warn', '<strong>不要自行发明:</strong>');
var ul1 = el('ul');
ct.doNot.forEach(function (x) { ul1.appendChild(el('li', null, esc(x))); });
co1.appendChild(ul1);
wrap.appendChild(co1);
}
if (ct.unknowns && ct.unknowns.length) {
var co2 = el('div', 'callout note', '<strong>规范未明示(待补充):</strong>');
var ul2 = el('ul');
ct.unknowns.forEach(function (x) { ul2.appendChild(el('li', null, esc(x))); });
co2.appendChild(ul2);
wrap.appendChild(co2);
}
return wrap;
}
function contractLink(c) { return c.contractRef ? LIB_ROOT + c.contractRef : null; }
/* 实现资源(按当前框架过滤) */
function buildFilesTable(c) {
var fw = state.framework;
var wrap = el('div');
var note = el('p', 'guide-p');
note.innerHTML = '当前框架:<strong>' + esc(FRAMEWORKS[fw]) + '</strong> —— 仅展示该端需要的文件。切换框架请使用顶部选择器。';
wrap.appendChild(note);
var tbl = el('table', 'res-table');
var thead = el('thead');
var trh = el('tr');
['文件', '大小', '操作'].forEach(function (t) { trh.appendChild(el('th', null, t)); });
thead.appendChild(trh);
tbl.appendChild(thead);
var tbody = el('tbody');
(FW_FILES[fw] || []).forEach(function (k) {
if (!c.files[k]) return;
var tr = el('tr');
tr.appendChild(el('td', null, KIND_LABELS[k] || k));
var tdF = el('td');
tdF.appendChild(el('span', null, esc(c.files[k].replace('../frameworks/', ''))));
tr.appendChild(tdF);
var tdS = el('td', 'num', fmtSize(c.sizes ? c.sizes[k] : null));
tr.appendChild(tdS);
var tdA = el('td');
var view = el('a', null, '新窗口打开');
view.href = c.files[k];
view.target = '_blank';
tdA.appendChild(view);
tr.appendChild(tdA);
tbody.appendChild(tr);
});
tbl.appendChild(tbody);
wrap.appendChild(tbl);
/* 其余端入口(折叠提示) */
var others = TAB_ORDER.filter(function (k) { return (FW_FILES[fw] || []).indexOf(k) < 0 && c.files[k]; });
if (others.length) {
var det = el('details', 'other-files');
det.appendChild(el('summary', null, '查看其他 ' + others.length + ' 个端的文件'));
var ul = el('ul', 'guide-ul');
others.forEach(function (k) {
var li = el('li');
var a = el('a', null, esc(KIND_LABELS[k] + ' · ' + c.files[k].replace('../frameworks/', '')));
a.href = c.files[k];
a.target = '_blank';
li.appendChild(a);
ul.appendChild(li);
});
det.appendChild(ul);
wrap.appendChild(det);
}
return wrap;
}
/* ---------- 主题定制器 ---------- */
var THEME_DEFAULTS = {
'--au-color-brand': '#2F54EB',
'--au-color-brand-hover': '#1D39C4',
'--au-color-brand-active': '#10239E',
'--au-color-brand-bg': '#F0F5FF'
};
var THEME_META = [
['--au-color-brand', '品牌主色'],
['--au-color-brand-hover', '主色悬停'],
['--au-color-brand-active', '主色点击'],
['--au-color-brand-bg', '主色浅背景']
];
/* 主题预设:一键切换品牌色族(点击后仍可用色板微调) */
var THEME_PRESETS = [
{ key: 'aurora', label: '科技蓝', colors: ['#2F54EB', '#1D39C4', '#10239E', '#F0F5FF'] },
{ key: 'cyan', label: '青碧', colors: ['#13C2C2', '#0E8C8C', '#0A6E6E', '#E6FFFB'] },
{ key: 'violet', label: '极夜紫', colors: ['#722ED1', '#5B1FA8', '#45187F', '#F9F0FF'] },
{ key: 'sunset', label: '暖橙', colors: ['#FA8C16', '#D46B08', '#AD4E00', '#FFF7E6'] },
{ key: 'magenta', label: '绯红', colors: ['#EB2F96', '#C31D7C', '#941661', '#FFF0F6'] },
{ key: 'forest', label: '松石绿', colors: ['#52C41A', '#389E0D', '#237804', '#F6FFED'] }
];
function hexMix(hex, other, ratio) {
if (!/^#[0-9a-fA-F]{6}$/.test(hex) || !/^#[0-9a-fA-F]{6}$/.test(other)) return hex;
function ch(a, b) { return Math.round(parseInt(a, 16) * (1 - ratio) + parseInt(b, 16) * ratio); }
return '#' + [ch(hex.slice(1, 3), other.slice(1, 3)), ch(hex.slice(3, 5), other.slice(3, 5)), ch(hex.slice(5, 7), other.slice(5, 7))]
.map(function (v) { return ('0' + v.toString(16)).slice(-2); }).join('');
}
function hexToRgba(hex, alpha) {
if (!/^#[0-9a-fA-F]{6}$/.test(hex)) return hex;
return 'rgba(' + parseInt(hex.slice(1, 3), 16) + ',' + parseInt(hex.slice(3, 5), 16) + ',' + parseInt(hex.slice(5, 7), 16) + ',' + alpha + ')';
}
var themeState = (function () {
try {
var saved = localStorage.getItem('aa-theme');
if (saved) return Object.assign({}, THEME_DEFAULTS, JSON.parse(saved));
} catch (e) { /* ignore */ }
return Object.assign({}, THEME_DEFAULTS);
})();
function focusRingValue() {
/* 品牌色 20% 透明度作为聚焦光晕 */
var hex = themeState['--au-color-brand'] || '#2F54EB';
if (/^#[0-9a-fA-F]{6}$/.test(hex)) return hex + '33';
return 'rgba(47,84,235,0.2)';
}
function injectIframeTheme(ifr) {
try {
var d = ifr.contentDocument;
if (!d || !d.documentElement) return;
Object.keys(themeState).forEach(function (k) {
d.documentElement.style.setProperty(k, themeState[k]);
});
d.documentElement.style.setProperty('--au-color-focus-ring', focusRingValue());
} catch (e) { /* file:// 跨源时静默跳过 */ }
}
function hookIframeTheme(ifr) {
ifr.addEventListener('load', function () { injectIframeTheme(ifr); });
}
function applyTheme(persist) {
var dark = document.documentElement.classList.contains('aa-dark');
/* 暗色模式下品牌色族向白色混合一档,保证链接/选中态在深底上的对比度;
演示 iframe 仍注入原始值(保持浅色基调) */
var display = {
'--au-color-brand': dark ? hexMix(themeState['--au-color-brand'], '#FFFFFF', 0.25) : themeState['--au-color-brand'],
'--au-color-brand-hover': dark ? hexMix(themeState['--au-color-brand-hover'], '#FFFFFF', 0.25) : themeState['--au-color-brand-hover'],
'--au-color-brand-active': dark ? hexMix(themeState['--au-color-brand-active'], '#FFFFFF', 0.12) : themeState['--au-color-brand-active']
};
display['--au-color-brand-bg'] = dark ? hexToRgba(display['--au-color-brand'], 0.18) : themeState['--au-color-brand-bg'];
Object.keys(display).forEach(function (k) {
document.documentElement.style.setProperty(k, display[k]);
});
document.documentElement.style.setProperty('--au-color-focus-ring', focusRingValue());
document.querySelectorAll('iframe').forEach(injectIframeTheme);
if (persist) {
try { localStorage.setItem('aa-theme', JSON.stringify(themeState)); } catch (e) { /* ignore */ }
}
}
function bindThemeTools() {
var panel = $('theme-panel');
var wrap = $('theme-colors');
/* 预设行:一键切换品牌色族 */
var presetWrap = $('theme-presets');
if (presetWrap) {
THEME_PRESETS.forEach(function (p) {
var btn = el('button', 'tp-preset');
btn.type = 'button';
btn.title = p.label;
var sw = el('span', 'tp-swatch');
sw.style.background = 'linear-gradient(135deg, ' + p.colors[0] + ' 0%, ' + p.colors[2] + ' 100%)';
btn.appendChild(sw);
btn.appendChild(el('span', 'tp-preset-label', esc(p.label)));
btn.addEventListener('click', function () {
THEME_META.forEach(function (m, i) { themeState[m[0]] = p.colors[i]; });
try { localStorage.setItem('aa-theme', JSON.stringify(themeState)); } catch (e) { /* ignore */ }
wrap.querySelectorAll('input[type=color]').forEach(function (inp, i) {
inp.value = p.colors[i];
wrap.querySelectorAll('.tp-val')[i].textContent = p.colors[i].toUpperCase();
});
applyTheme(true);
});
presetWrap.appendChild(btn);
});
}
THEME_META.forEach(function (m) {
var row = el('div', 'tp-row');
var input = document.createElement('input');
input.type = 'color';
input.value = themeState[m[0]];
var label = el('span', 'tp-label', esc(m[1]));
var val = el('span', 'tp-val', esc(themeState[m[0]].toUpperCase()));
input.addEventListener('input', function () {
themeState[m[0]] = input.value;
val.textContent = input.value.toUpperCase();
applyTheme(true);
});
row.appendChild(input);
row.appendChild(label);
row.appendChild(val);
wrap.appendChild(row);
});
$('fab-theme').addEventListener('click', function () {
panel.classList.toggle('hidden');
});
$('theme-reset').addEventListener('click', function () {
themeState = Object.assign({}, THEME_DEFAULTS);
try { localStorage.removeItem('aa-theme'); } catch (e) { /* ignore */ }
wrap.querySelectorAll('input[type=color]').forEach(function (inp, i) {
inp.value = THEME_DEFAULTS[THEME_META[i][0]];
wrap.querySelectorAll('.tp-val')[i].textContent = THEME_DEFAULTS[THEME_META[i][0]].toUpperCase();
});
applyTheme(true);
});
$('theme-copy').addEventListener('click', function () {
var lines = [':root {'];
THEME_META.forEach(function (m) {
lines.push(' --' + m[0] + ': ' + themeState[m[0]] + ';');
});
lines.push(' --au-color-focus-ring: ' + focusRingValue() + ';');
lines.push('}');
copyText(lines.join('\n'), $('theme-copy'), '已复制 ✓');
});
/* 深色 / 浅色模式切换 */
var DARK_TOKENS = {
'--au-color-page-bg': '#14161C',
'--au-color-card-bg': '#1D2027',
'--au-color-table-header-bg': '#23262F',
'--au-color-disabled-bg': '#262A33',
'--au-color-tag-green-bg': '#16261A',
'--au-color-text-title': '#E8EAED',
'--au-color-text-body': '#C9CDD4',
'--au-color-text-secondary': '#8B95A7',
'--au-color-text-placeholder': '#5C6470',
'--au-color-border': '#2E323C'
};
function isDark() { return document.documentElement.classList.contains('aa-dark'); }
function applyMode() {
var dark = false;
try { dark = localStorage.getItem('aa-mode') === 'dark'; } catch (e) { /* ignore */ }
document.documentElement.classList.toggle('aa-dark', dark);
$('icon-moon').style.display = dark ? 'none' : '';
$('icon-sun').style.display = dark ? '' : 'none';
applyTheme(false); /* 模式切换后重算品牌色的暗色显示值 */
}
$('fab-mode').addEventListener('click', function () {
var dark = !isDark();
try { localStorage.setItem('aa-mode', dark ? 'dark' : 'light'); } catch (e) { /* ignore */ }
applyMode();
applyTheme(false); /* 重新应用品牌色 + 同步 iframe */
});
applyMode();
/* 悬浮按钮显隐 */
var topBtn = $('fab-top');
window.addEventListener('scroll', function () {
topBtn.style.display = window.scrollY > 400 ? 'flex' : 'none';
}, { passive: true });
topBtn.addEventListener('click', function () {
window.scrollTo({ top: 0, behavior: 'smooth' });
});
applyTheme(false);
}
/* ---------- 全局搜索 ---------- */
var searchIndex = comps.map(function (c) {
var n = splitName(c.name);
return { c: c, zh: n.zh.toLowerCase(), en: n.en.toLowerCase(), slug: c.slug.toLowerCase(), cat: catLabel(c.category) };
});
var srItems = [];
var srActive = -1;
function openSearch() {
$('search-modal').classList.remove('hidden');
var inp = $('search-input');
inp.value = '';
renderSearchCats();
doSearch('');
setTimeout(function () { inp.focus(); }, 0);
}
function closeSearch() {
$('search-modal').classList.add('hidden');
$('search-results').innerHTML = '';
srItems = []; srActive = -1;
}
/* 命中片段高亮:安全转义后用 <mark> 包裹首个匹配(大小写不敏感) */
function hiMark(text, q) {
var t = String(text == null ? '' : text);
if (!q) return esc(t);
var lower = t.toLowerCase();
var idx = lower.indexOf(q);
if (idx < 0) return esc(t);
return esc(t.slice(0, idx)) + '<mark>' + esc(t.slice(idx, idx + q.length)) + '</mark>' + esc(t.slice(idx + q.length));
}
/* 模糊容错:q 的每个字符按顺序出现在 s 中(子序列匹配),用于无精确命中时回退 */
function fuzzySubseq(q, s) {
var i = 0;
for (var j = 0; j < s.length && i < q.length; j++) {
if (s[j] === q[i]) i++;
}
return i === q.length;
}
/* 搜索弹窗顶部的分类过滤 chips(计数固定,初始化时渲染一次) */
function renderSearchCats() {
var wrap = $('search-cats');
if (!wrap) return;
var counts = {};
searchIndex.forEach(function (r) { counts[r.c.category] = (counts[r.c.category] || 0) + 1; });
var mk = function (key, label, cnt) {
var b = el('button', 'sc-chip' + (state.searchCat === key ? ' active' : ''));
b.type = 'button';
b.textContent = label + ' ' + cnt;
b.addEventListener('click', function () {
state.searchCat = key;
renderSearchCats();
doSearch($('search-input').value);
$('search-input').focus();
});
wrap.appendChild(b);
};
wrap.innerHTML = '';
mk('', '全部', searchIndex.length);
CAT_ORDER.forEach(function (cat) {
if (counts[cat]) mk(cat, catLabel(cat), counts[cat]);
});
}
function doSearch(q) {
q = (q || '').trim().toLowerCase();
var cat = state.searchCat || '';
var pool = searchIndex.filter(function (r) { return !cat || r.c.category === cat; });
var hits = pool.filter(function (r) {
return !q || r.zh.indexOf(q) >= 0 || r.en.indexOf(q) >= 0 || r.slug.indexOf(q) >= 0;
});
var fuzzy = false;
if (q && q.length >= 2 && !hits.length) {
fuzzy = true;
hits = pool.filter(function (r) {
return fuzzySubseq(q, r.zh) || fuzzySubseq(q, r.en) || fuzzySubseq(q, r.slug);
});
}
hits = hits.slice(0, 20);
var box = $('search-results');
box.innerHTML = '';
srItems = []; srActive = -1;
if (!hits.length) {
box.appendChild(el('div', 'sr-empty', fuzzy ? '无匹配组件(已尝试模糊匹配)' : '无匹配组件'));
return;
}
hits.forEach(function (r, i) {
var n = splitName(r.c.name);
var item = el('div', 'sr-item' + (i === 0 ? ' hover' : ''));
item.setAttribute('data-slug', r.c.slug);
item.innerHTML = '<span class="sr-name"><span class="zh"></span><span class="en"></span></span><span class="sr-cat"></span>';
item.querySelector('.zh').innerHTML = hiMark(n.zh, q);
item.querySelector('.en').innerHTML = hiMark(n.en, q);
item.querySelector('.sr-cat').textContent = r.cat;
item.addEventListener('click', function () { location.hash = '#/component/' + r.c.slug; navigate(); });
box.appendChild(item);
srItems.push(item);
});
srActive = 0;
}
function moveActive(delta) {
if (!srItems.length) return;
srItems[srActive] && srItems[srActive].classList.remove('hover');
srActive = (srActive + delta + srItems.length) % srItems.length;
srItems[srActive].classList.add('hover');
srItems[srActive].scrollIntoView({ block: 'nearest' });
}
function bindSearch() {
$('search-trigger').addEventListener('click', openSearch);
$('search-modal').addEventListener('click', function (e) {
if (e.target === this) closeSearch();
});
$('search-input').addEventListener('input', function (e) {
var q = e.target.value;
var t0 = performance.now();
doSearch(q);
var t1 = performance.now();
if (window.aaLogger) {
window.aaLogger.search(q, searchIndex.length, Math.round(t1 - t0));
}
});
document.addEventListener('keydown', function (e) {
var modalHidden = $('search-modal').classList.contains('hidden');
if ((e.ctrlKey || e.metaKey) && (e.key === 'k' || e.key === 'K')) {
e.preventDefault();
modalHidden ? openSearch() : closeSearch();
return;
}
if (modalHidden) return;
if (e.key === 'Escape') closeSearch();
else if (e.key === 'ArrowDown') { e.preventDefault(); moveActive(1); }
else if (e.key === 'ArrowUp') { e.preventDefault(); moveActive(-1); }
else if (e.key === 'Enter') {
var active = srItems[srActive];
if (active) {
if (window.aaLogger) {
var inp = $('search-input');
window.aaLogger.searchHit(inp ? inp.value : '', (bySlugMap[active.getAttribute('data-slug') || ''] || {}).slug);
}
active.click();
}
}
});
}
/* ---------- 启动 ---------- */
function init() {
$('ver').textContent = 'v' + (data.meta.version || '1.0.0');
state.framework = loadFramework();
$('fw-select').value = state.framework;
$('fw-select').addEventListener('change', function (e) {
state.framework = e.target.value;
try { localStorage.setItem('aa-framework', state.framework); } catch (err) { /* ignore */ }
navigate({ fwOnly: true }); /* 切换框架时不滚到顶部 */
});
bindSearch();
bindThemeTools();
window.addEventListener('hashchange', function () {
if (window.aaLogger) window.aaLogger.render(state.route.page || 'unknown', { slug: state.route.slug || null });
navigate();
});
if (window.aaLogger) window.aaLogger.render('init', null);
navigate();
}
init();
})();