/* 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' }; /* 注意:这些标签必须在「渲染时」求值,不能在这里预先 T() —— 模块级常量只算一次,语言切换后不会更新。故改为函数。 */ var CAT_KEYS = ['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 = CAT_KEYS; /* KIND_KEYS 存中文源串,翻译在 kindLabel() 里运行时求值(语言可切换) */ var KIND_KEYS = { html: 'H5 静态页', css: '样式表', jsx: 'React 组件', vue2: 'Vue 2 组件', vue3: 'Vue 3 组件' }; /* 框架感知:每种技术栈需要的文件形态 */ var FRAMEWORKS = { h5: 'H5', react: 'React', vue2: 'Vue 2', vue3: 'Vue 3' }; /* ---------- 国际化 ---------- 以中文源串为键(见 i18n.js):T() 在 en 模式下查表,未收录则回退原文, 因此新增文案即使漏翻也不会出现空白。 注意:T() 必须在任何使用点之前定义(模块级常量若预先求值会在切换语言后失效)。 */ var I18N = window.AA_I18N || { T: function (s) { return s; }, getLang: function () { return 'zh'; }, setLang: function () {} }; function T(s) { return I18N.T(s); } function isEn() { return I18N.getLang() === 'en'; } function kindLabel(k) { return T(KIND_KEYS[k] || k); } /* 分类名:中文取字典、英文取 CAT_EN(分类是固定六项,不必进 i18n 字典) */ function catLabel(key) { if (!key) return T('未分类'); if (isEn()) return CAT_EN[key] || key; var zh = { general: T('通用'), navigation: T('导航'), input: T('数据录入'), display: T('数据展示'), feedback: T('反馈'), system: T('工具与系统') }; return zh[key] || key; } function catDisplay(key) { return isEn() ? catEn(key) : catLabel(key); } /* 组件名:中英并存,按语言决定主次(splitName 拆出的 zh/en) */ function namePrimary(n) { return isEn() ? (n.en || n.zh) : n.zh; } 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(T('数据加载失败: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; /* ---------- 源码取值(P4:部分源码已移出 data.js,改为按需加载)---------- data.js 里 components[].sources 可能只有部分端,其余在 sourcesRef 里给路径。 srcOf 同步返回「已有的」源码;缺失时返回空串并触发后台加载,加载完回调刷新。 缓存避免重复请求。 */ var srcCache = {}; var srcPending = {}; function srcOf(c, kind) { if (c.sources && c.sources[kind] != null) return c.sources[kind]; var key = c.slug + '/' + kind; if (srcCache[key] != null) { /* 回填到 c.sources:Playground 轮询与 extractComponentAPI 回退读 c.sources, 若只留 srcCache 它们会永远等到超时后落空(空 html 进试玩)。 */ if (srcCache[key] && c.sources && c.sources[kind] == null) { try { c.sources[kind] = srcCache[key]; } catch (e) {} } return srcCache[key]; } var ref = (c.sourcesRef || {})[kind]; if (ref && !srcPending[key]) { srcPending[key] = true; fetch(ref, { cache: 'force-cache' }) .then(function (r) { return r.ok ? r.text() : ''; }) .then(function (t) { srcCache[key] = t; srcPending[key] = false; try { if (t && c.sources) c.sources[kind] = t; } catch (e) {} if (window.__aaSrcReady) window.__aaSrcReady(c.slug, kind, t); }) .catch(function () { srcCache[key] = ''; srcPending[key] = false; }); } return ''; } /* 该端源码是否「存在」(本地或引用)—— 决定 tab 是否显示 */ function hasSrc(c, kind) { return (c.sources && c.sources[kind] != null) || !!((c.sourcesRef || {})[kind]); } /* ---------- 详情取值(P4 阶段D:contract/specLines 已移出 data.js,按需加载)---------- getDetail 同 srcOf 模式:同步返回缓存(或内嵌旧数据),缺失时后台 fetch, 加载完回调 window.__aaDetailReady(slug) 触发详情区回填。 data.json 保持全量,data.js 外壳只留 hasContract。 */ var detailCache = {}; var detailPending = {}; function getDetail(c) { if (c.contract !== undefined || c.specLines !== undefined) { return { contract: c.contract || null, specLines: c.specLines || [] }; } if (detailCache[c.slug]) return detailCache[c.slug]; if (!detailPending[c.slug]) { detailPending[c.slug] = true; fetch('details/' + c.slug + '.json', { cache: 'force-cache' }) .then(function (r) { return r.ok ? r.json() : { contract: null, specLines: [] }; }) .then(function (d) { detailCache[c.slug] = d; detailPending[c.slug] = false; if (window.__aaDetailReady) window.__aaDetailReady(c.slug, d); }) .catch(function () { detailCache[c.slug] = { contract: null, specLines: [] }; detailPending[c.slug] = false; if (window.__aaDetailReady) window.__aaDetailReady(c.slug, detailCache[c.slug]); }); } return null; } /* ---------- changelog 懒加载(P4 阶段D:已移出 data.js)---------- 同步返回缓存数组;未加载返回 null 并触发后台 fetch,完成后回调重渲染。 */ var changelogCache = null; var changelogPending = false; function getChangelog() { if (data.changelog) return data.changelog; if (changelogCache) return changelogCache; if (!changelogPending) { changelogPending = true; fetch('changelog.json', { cache: 'force-cache' }) .then(function (r) { return r.ok ? r.json() : { changelog: [] }; }) .then(function (d) { changelogCache = d.changelog || []; changelogPending = false; if (window.__aaChangelogReady) window.__aaChangelogReady(); }) .catch(function () { changelogCache = []; changelogPending = false; if (window.__aaChangelogReady) window.__aaChangelogReady(); }); } return 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, '&').replace(//g, '>').replace(/"/g, '"'); } 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) { if (b === 1) return T('核心规范'); return isEn() ? ('Batch ' + b) : (T('第') + b + T('批')); } 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: T('组件总览'), hash: '#/overview', active: r === 'overview' || r === 'component' }, { label: T('快速开始'), hash: '#/guide', active: r === 'guide' }, { label: T('设计规范'), 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 = '' + items.length + ''; 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 = ''; /* 主次随语言调换:中文模式主显中文名,英文模式主显英文名 */ b.querySelector('.zh').textContent = namePrimary(n); b.querySelector('.en').textContent = isEn() ? n.zh : 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', T('本页目录'))); 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 = '
AURORA ADMIN DESIGN SYSTEM
' + '

' + T('Aurora Admin 组件库') + '

' + '

' + T('理性、高效、克制的 B 端中后台设计系统——信息密度适中,层级清晰,交互反馈即时。') + '

' + '
' + ' ' + T('浏览组件 →') + '' + ' ' + T('场景示范') + '' + ' ' + T('快速开始') + '' + ' ' + T('测试总览') + '' + ' ' + T('设计规范') + '' + '
' + '
' + '
' + data.meta.total + '' + T('组件') + '
' + '
' + CAT_ORDER.length + '' + T('大类') + '
' + '
' + data.tokens.length + '' + T('设计令牌') + '
' + '
5' + T('端实现') + '
' + '
10' + T('份规范') + '
' + '
79' + T('测试页') + '
' + '
'; 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 = T('通过率 ') + rep.passRate + '%'; node.title = rep.generated ? T('headless 回归 ') + rep.generated + ':' + rep.assertions.pass + '/' + rep.assertions.total + ' 断言通过' : ''; } }).catch(function () { /* 报告不存在时不显示 */ }); /* 设计原则四卡 */ inner.appendChild(el('h2', 'home-h2', T('设计原则'))); var feats = [ [T('一致优先'), T('同类组件使用相同的尺寸、颜色与间距,跨页面保持一致体验。'), 'M4 12l5 5L20 7'], [T('清晰高效'), T('信息层级明确,操作路径最短,避免过度装饰。'), 'M13 2L3 14h9l-1 8 10-12h-9l1-8z'], [T('反馈及时'), T('所有操作都有加载、成功、失败三类状态反馈。'), 'M22 11.08V12a10 10 0 1 1-5.93-9.14'], [T('无障碍'), T('满足键盘操作与焦点管理,正文对比度 ≥ 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 = '
' + '

'; card.querySelector('h3').textContent = f[0]; card.querySelector('p').textContent = f[1]; fgrid.appendChild(card); }); inner.appendChild(fgrid); /* 核心组件快捷入口 */ inner.appendChild(el('h2', 'home-h2', T('核心组件'))); var core = el('div', 'core-grid'); comps.filter(function (c) { return c.hasContract; }).forEach(function (c) { var n = splitName(c.name); var a = el('a', 'core-card'); a.href = '#/component/' + c.slug; a.innerHTML = '
'; a.querySelector('.core-zh').textContent = namePrimary(n); a.querySelector('.core-en').textContent = isEn() ? n.zh : n.en; core.appendChild(a); }); inner.appendChild(core); /* 批次入口 */ inner.appendChild(el('h2', 'home-h2', T('规格批次'))); 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', T('快速开始'))); inner.appendChild(el('p', 'page-desc', T('了解如何在本项目中使用 Aurora Admin 组件:引入设计令牌、按端取用组件源码、以及目录与命名约定。'))); var tocItems = []; /* 1. 引入设计令牌 */ var s1 = el('div', 'sec'); s1.id = 'install'; s1.appendChild(el('h2', null, T('引入设计令牌'))); s1.appendChild(el('p', 'guide-p', T('所有组件都基于同一份 CSS 变量令牌。在使用任何组件前,先在页面中引入令牌文件:'))); var code1 = el('pre', 'code-inline'); code1.appendChild(el('code', null, esc(''))); s1.appendChild(code1); s1.appendChild(el('p', 'guide-p', T('如需结构化的令牌数据(供工具消费),可读取同目录的 css.json;聚合组件样式见 components.css。'))); inner.appendChild(s1); tocItems.push({ id: 'install', label: '引入设计令牌' }); /* 2. 按端使用组件 */ var s2 = el('div', 'sec'); s2.id = 'usage'; s2.appendChild(el('h2', null, T('按端使用组件'))); s2.appendChild(el('p', 'guide-p', T('每个组件在 frameworks/ 下有五种形态(H5 / CSS / React / Vue 2 / Vue 3),按你的技术栈取用:'))); var tbl = el('table', 'res-table'); var thead = el('thead'); var trh = el('tr'); [T('端'), T('文件'), T('使用方式')].forEach(function (t) { trh.appendChild(el('th', null, t)); }); thead.appendChild(trh); tbl.appendChild(thead); var tbody = el('tbody'); [ [kindLabel('html'), 'Button.html', T('直接在浏览器打开即为完整演示页;将结构与内联样式拷贝进你的页面')], ['React', 'Button.jsx + Button.css', T('将 JSX 与 CSS 拷入工程,按需改造数据绑定')], ['Vue 2', 'Button.vue2.vue', T('Options API 单文件组件,直接注册使用')], ['Vue 3', 'Button.vue3.vue', T('