From f73ed394f41740aa47fee70fb9c57614c4c47807 Mon Sep 17 00:00:00 2001 From: aurora-admin Date: Mon, 14 Sep 2026 03:28:46 +0800 Subject: [PATCH] =?UTF-8?q?feat(S2-P9+S4-P12):=20FAQ=E9=A1=B5240=E6=9D=A1?= =?UTF-8?q?=E8=81=9A=E5=90=88+=E5=8F=91=E5=B8=83=E6=B5=81=E7=A8=8Brelease.?= =?UTF-8?q?mjs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P9: #/faq 路由+renderFaq(details批量懒加载每批10个)+顶栏/侧边栏双入口 +guide-h3样式+9条i18n英文;浏览器实测240条/搜索宽度11条/0 JS错误。 规划偏差:契约已在P4-D移出data.js,FAQ走details懒加载。 P12: tools/release.mjs(检查+bump双模式,只做本地准备不自动push/tag) +CONTRIBUTING发布5步+版本号规则;检查模式实测版本一致通过。 回归零影响:report.json 79/79、data.json未动。 --- CHANGELOG.md | 9 ++++ CONTRIBUTING.md | 8 +++ ROADMAP.md | 29 ++++++++++- site/app.js | 121 +++++++++++++++++++++++++++++++++++++++++++++- site/i18n.js | 16 ++++++ site/index.html | 1 + site/style.css | 2 + tools/release.mjs | 101 ++++++++++++++++++++++++++++++++++++++ 8 files changed, 284 insertions(+), 3 deletions(-) create mode 100644 tools/release.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index f5e2479..0828f5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ ## [Unreleased] +### S2-P9 · FAQ 页(details 懒加载版) +- 新增 `#/faq` 页:自动聚合 79 组件契约的 unknowns(139)+ doNotInvent(101),共 240 条;按组件分组 + 关键词搜索过滤 + 命中统计;顶栏/侧边栏双入口;9 条 i18n 英文。 +- 浏览器实测:groups 80/items 240,搜索“宽度”命中 11 条/11 组,清空恢复 240,0 JS 错误。 +- 规划偏差:契约已在 P4 阶段 D 移出 `data.js`,FAQ 改走 `details/*.json` 批量懒加载(每批 10 个)而非直接消费 `data.js`。 + +### S4-P12 · 版本发布流程 +- 新增 `tools/release.mjs`:检查模式(package.json vs CHANGELOG 版本一致性 + Unreleased 提醒)与 `--bump major|minor|patch --date` 提升模式;只做本地准备,tag/push 由人执行。 +- `CONTRIBUTING.md` 新增发布流程 5 步 + 版本号规则(major/minor/patch 判定)。 + ### S2-P6 · 跨端一致性自动验证(方案 B:静态结构比对) - 新增 `tools/verify-cross-platform.mjs`(零依赖零联网):79 组件 × 4 端(H5/React/Vue2/Vue3)class 集合 + 结构骨架双 diff,演示包装类/变量名/模板残留降噪,high/medium/low 分级;产出 `tests/cross-platform-report.json`(79 条全量 + samples 3)。 - 实测:完全一致 2,有差异 77(high 44/medium 22/low 11)。high 主因:React 端 `is-disabled/is-active` 缺失、H5 演示页缺框架端结构类、Tag/Select/Input 变体类缺失——已追加为 ROADMAP 的 S2-P6-F1~F4,不在本任务内修。 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1cd7f12..ae0fd17 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -98,6 +98,14 @@ - 不在框架实现里 hard-code API key - 不引入未审核的 npm 包(保持零依赖) +## 版本发布流程(S4-P12 · SemVer) + +1. 在 `CHANGELOG.md` 的 `[Unreleased]` 下写清本次变更条目(有内容才允许提升版本)。 +2. 检查是否可发布:`node tools/release.mjs`(校验 package.json 与 CHANGELOG 最新版本一致)。 +3. 提升版本:`node tools/release.mjs --bump --date YYYY-MM-DD`(自动改 CHANGELOG 与 package.json,复核后提交)。 +4. 人工执行:`git tag v` → `git push origin main v` → `npm run build && npm publish`(公开发布需先做 Q1 决策)。 +5. 版本号规则:破坏 `data.json` 全量承诺或删接口 → major;加组件/加页面 → minor;修 bug/改文案 → patch。 + ## 行为准则 - 中文优先;技术名词保留英文(如 avatar / tooltip) diff --git a/ROADMAP.md b/ROADMAP.md index 74c4e6b..d8c827b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -884,6 +884,19 @@ data OK **失败判据**:条目数少于 139/101,或页面空白。 +**✅ 完成(details 懒加载版)** — 验收输出: + +``` +title: 常见问题 / groups: 80 / items: 240 +stat: 共 240 条,79 个组件有条目 +search 宽度 -> items: 11 / groups: 11(stat: 命中 11 / 240 条,10 / 79 个组件) +restored: 240 / topnav faq: present / jserrors: none +FAQ BROWSER OK +``` + +改动文件:`site/app.js`(renderFaq + `#/faq` 路由 + 侧边栏入口)、`site/index.html`(顶栏入口)、`site/style.css`(guide-h3)、`site/i18n.js`(9 条英文)。 +规划偏差:契约已在 P4 阶段 D 移出 `data.js`(`site/data.js` 含 unknowns 0 条),故 FAQ 改走 `details/*.json` 批量懒加载(每批 10 个)而非规划写的“直接消费 data.js”。`data.json` 的 139/101 计数仍作为数据源完整性断言。 + --- ### S4-P10 · Figma 资源 | 预算 ~2 天 | 优先级 P2 @@ -965,6 +978,18 @@ figma export OK 1. `tools/release.mjs`:从 CHANGELOG 提取版本 → 更新 `package.json` → 打 tag → 提示推送。 2. 文档化到 `CONTRIBUTING.md`。 +**✅ 完成** — 验收输出: + +``` +package.json: 1.4.1 / CHANGELOG 最新: [1.4.1] 2026-09-11 +OK: 版本一致 +NOTE: [Unreleased] 段有 1251 字符未发布变更,发版前确认是否纳入本次 +OK: build-dist 可调用 +``` + +改动文件:`tools/release.mjs`(新建,检查模式 + `--bump major|minor|patch --date` 提升模式,只做本地准备不自动 push/tag)、`CONTRIBUTING.md`(发布流程 5 步 + 版本号规则)。 +规划偏差:原步骤写“打 tag → 提示推送”由脚本做,实际拆为脚本只做本地准备、tag/push 由人按输出清单执行(不可逆的远端动作不自动化)。 + --- ## 四 · 显式不做(及理由) @@ -1038,10 +1063,10 @@ figma export OK | P6 | 跨端一致性验证 | S2 | **P0** | 1 d | P3 | ✅ **已完成**(静态结构比对 79/79,high 44/medium 22/low 11,F1-F4 已追加) | | P7 | 行为断言 | S2 | P1 | 1.5 d | — | 🟢 可开工(优先级低于 P2-P4) | | P8 | RTL | S3 | P1 | 2 d | P5 | ⚪ 待 P5 | -| P9 | FAQ 页 | S3 | P2 | 1 d | — | 🟢 可开工(优先级低) | +| P9 | FAQ 页 | S3 | P2 | 1 d | — | ✅ **已完成**(240 条/79 组件/搜索过滤/浏览器实测,details 懒加载版) | | P10 | Figma 资源 | S4 | P2 | 2 d | P2 | ⚪ 待 P2 | | P11 | 模板页库 | S4 | P2 | 2 d | — | 🟢 可开工(优先级低) | -| P12 | 版本发布流程 | S4 | P3 | 1 d | P2 | ⚪ 待 P2 | +| P12 | 版本发布流程 | S4 | P3 | 1 d | P2 | ✅ **已完成**(release.mjs 检查/bump + CONTRIBUTING 文档化,只做本地准备不自动 push/tag) | ### 推荐执行顺序 diff --git a/site/app.js b/site/app.js index 6bb1fc2..f221de1 100644 --- a/site/app.js +++ b/site/app.js @@ -198,6 +198,7 @@ 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 === '/faq') return { page: 'faq', 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] }; @@ -217,6 +218,7 @@ 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 === 'faq') renderFaq(content); else if (state.route.page === 'changelog') renderChangelog(content); else renderComponent(content, state.route.slug); /* 仅切换框架时不滚到顶部,避免用户感觉"页面没切换" */ @@ -245,7 +247,8 @@ 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' } + { label: T('设计规范'), hash: '#/design', active: r === 'design' }, + { label: T('常见问题'), hash: '#/faq', active: r === 'faq' } ]; links.forEach(function (L) { var a = el('a', 'side-link' + (L.active ? ' active' : ''), esc(L.label)); @@ -584,6 +587,122 @@ renderToc(tocItems); } + /* ---------- 常见问题(S2-P9:契约 unknowns + doNotInvent 聚合) ---------- */ + function renderFaq(content) { + content.innerHTML = ''; + var inner = el('div', 'page-inner'); + inner.appendChild(el('h1', 'page-title', T('常见问题'))); + inner.appendChild(el('p', 'page-desc', T('自动聚合 79 个组件契约里的“规范未明示”(unknowns)与“不要自行发明”(doNotInvent),共 240 条。支持按组件浏览与关键词过滤。'))); + + var tocItems = []; + + /* 搜索框(纯前端过滤,零依赖) */ + var s0 = el('div', 'sec'); s0.id = 'faq-search'; + var input = el('input', 'search-input'); + input.placeholder = T('搜索问题,如:宽度、省略、对齐…'); + input.setAttribute('aria-label', T('搜索常见问题')); + s0.appendChild(input); + var stat = el('p', 'guide-p'); + s0.appendChild(stat); + inner.appendChild(s0); + + /* 按组件分组(P4 阶段D:contract 已移出 data.js,details/*.json 懒加载批量取) */ + var groups = []; + var listWrap = el('div', 'faq-list'); + inner.appendChild(listWrap); + + function buildGroup(c, contract) { + var un = (contract && contract.unknowns) || []; + var dn = (contract && (contract.doNot || contract.doNotInvent)) || []; + if (!un.length && !dn.length) return null; + var box = el('div', 'sec'); + box.id = 'faq-' + c.slug; + box.setAttribute('data-slug', c.slug); + var nm = splitName(c.name || c.slug); + box.appendChild(el('h2', null, esc(isEn() && nm.en ? nm.en : nm.zh) + ' · ' + esc(c.slug))); + if (un.length) { + box.appendChild(el('h3', 'guide-h3', T('规范未明示') + '(' + un.length + ')')); + var ul1 = el('ul', 'guide-ul'); + un.forEach(function (q) { + var li = el('li', null, esc(q)); + li.setAttribute('data-q', q); + ul1.appendChild(li); + }); + box.appendChild(ul1); + } + if (dn.length) { + box.appendChild(el('h3', 'guide-h3', T('不要自行发明') + '(' + dn.length + ')')); + var ul2 = el('ul', 'guide-ul'); + dn.forEach(function (q) { + var li = el('li', null, esc(q)); + li.setAttribute('data-q', q); + ul2.appendChild(li); + }); + box.appendChild(ul2); + } + inner.appendChild(box); + listWrap.appendChild(box); + groups.push({ id: 'faq-' + c.slug, label: c.slug, box: box }); + return box; + } + + function applyFilter() { + var q = (input.value || '').trim().toLowerCase(); + var shownGroups = 0, shownItems = 0, totalItems = 0; + groups.forEach(function (g) { + var items = g.box.querySelectorAll('li[data-q]'); + var vis = 0; + items.forEach(function (li) { + totalItems++; + var hit = !q || (li.getAttribute('data-q') || '').toLowerCase().indexOf(q) > -1; + li.style.display = hit ? '' : 'none'; + if (hit) vis++; + }); + g.box.style.display = vis ? '' : 'none'; + if (vis) { shownGroups++; shownItems += vis; } + }); + stat.textContent = loaded < comps.length + ? T('加载中…') + ' ' + loaded + ' / ' + comps.length + : (q + ? T('命中') + ' ' + shownItems + ' / ' + totalItems + T(' 条,') + shownGroups + ' / ' + groups.length + T(' 个组件') + : T('共') + ' ' + totalItems + ' ' + T('条,') + groups.length + ' ' + T('个组件有条目')); + } + input.addEventListener('input', applyFilter); + + /* 批量懒加载 details(每批 10 个,避免 79 并发打爆 dev-server) */ + var loaded = 0; + stat.textContent = T('加载中…') + ' 0 / ' + comps.length; + content.appendChild(inner); + renderToc([]); + + function loadOne(c) { + var got = getDetail(c); + if (got) { // data.js 内嵌旧数据或缓存命中:同步建组 + buildGroup(c, got.contract); + loaded++; + if (loaded === comps.length) { applyFilter(); renderToc(tocItems.map(function (t) { return { id: t.id, label: t.label }; })); } + return Promise.resolve(); + } + return fetch('details/' + c.slug + '.json', { cache: 'force-cache' }) + .then(function (r) { return r.ok ? r.json() : { contract: null }; }) + .catch(function () { return { contract: null }; }) + .then(function (d) { + detailCache[c.slug] = d; + buildGroup(c, d.contract); + loaded++; + stat.textContent = T('加载中…') + ' ' + loaded + ' / ' + comps.length; + if (loaded === comps.length) { applyFilter(); renderToc(tocItems.map(function (t) { return { id: t.id, label: t.label }; })); } + }); + } + (function loadBatch(i) { + if (!content.isConnected) return; + var batch = comps.slice(i, i + 10); + Promise.all(batch.map(loadOne)).then(function () { + if (i + 10 < comps.length) loadBatch(i + 10); + }); + })(0); + } + /* ---------- 更新日志 ---------- */ function renderChangelog(content) { content.innerHTML = ''; diff --git a/site/i18n.js b/site/i18n.js index 6b14732..907cbf1 100644 --- a/site/i18n.js +++ b/site/i18n.js @@ -199,6 +199,22 @@ 'No structured changelog data yet — run build-site.ps1 to regenerate; the build parses CHANGELOG.md into data.js.', '暂无数据': 'No data yet', + /* ---------- 常见问题(S2-P9) ---------- */ + '常见问题': 'FAQ', + '自动聚合 79 个组件契约里的“规范未明示”(unknowns)与“不要自行发明”(doNotInvent),共 240 条。支持按组件浏览与关键词过滤。': + 'Auto-aggregated from 79 component contracts: “unspecified by spec” (unknowns) and “do not invent” (doNotInvent), 240 entries. Browse by component or filter by keyword.', + '搜索问题,如:宽度、省略、对齐…': 'Search, e.g. width, ellipsis, alignment…', + '搜索常见问题': 'Search FAQ', + '规范未明示': 'Unspecified by spec', + '不要自行发明': 'Do not invent', + '命中': 'Matched', + '条,': ' entries, ', + '个组件': ' components', + '共': 'Total ', + '条,': ' entries, ', + '个组件有条目': ' components have entries', + '加载中…': 'Loading…', + /* ---------- 组件详情页 ---------- */ '何时使用': 'When to Use', '本组件暂无独立规格说明,参见 ': 'No standalone spec for this component; see ', diff --git a/site/index.html b/site/index.html index b468da7..9b2bf08 100644 --- a/site/index.html +++ b/site/index.html @@ -36,6 +36,7 @@ 组件 快速开始 设计规范 + 常见问题 更新日志 AI 消费 diff --git a/site/style.css b/site/style.css index 49e01e6..52bcb4a 100644 --- a/site/style.css +++ b/site/style.css @@ -860,6 +860,8 @@ a.chip:hover { color: var(--au-color-brand); } } .guide-ul { margin: 0; padding-left: 18px; } .guide-ul li { margin: 6px 0; } +/* S2-P9 FAQ 页:分组小标题(复用 contract-block h3 的视觉层级) */ +.guide-h3 { margin: 12px 0 4px; font-size: 14px; font-weight: 600; color: var(--au-color-text-title); } /* ---- 侧栏类目英文副标 ---- */ .side-group .cat-zh { font-style: normal; } diff --git a/tools/release.mjs b/tools/release.mjs new file mode 100644 index 0000000..ed37a9d --- /dev/null +++ b/tools/release.mjs @@ -0,0 +1,101 @@ +#!/usr/bin/env node +/** + * release.mjs — S4-P12 版本发布流程(SemVer + CHANGELOG 驱动) + * + * 只做可逆的本地准备,不自动 push/tag 到远端: + * 1) 从 CHANGELOG.md 解析最新 `## [x.y.z] - date` 段(Unreleased 不算版本) + * 2) 校验 package.json 的 version 与之*一致*(不一致则报错退出,不自动改) + * 3) 校验 CHANGELOG 该版本段非空;校验 dist 可构建(tools/build-dist.mjs --dry-run 若支持) + * 4) 输出发布检查清单 + 建议的 tag/push 命令(由人执行) + * + * 另支持 --bump --date YYYY-MM-DD:把 [Unreleased] 提升为新版本段 + *(改 CHANGELOG.md 与 package.json,需人复核后提交)。 + * + * 用法: + * node tools/release.mjs # 检查当前版本是否可发布 + * node tools/release.mjs --bump minor --date 2026-09-14 + */ +import { readFileSync, writeFileSync } from 'fs'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const ROOT = join(__dirname, '..'); +const arg = (k) => { + const i = process.argv.indexOf(k); + return i > -1 ? process.argv[i + 1] : null; +}; + +const fail = (msg) => { console.error('FAIL: ' + msg); process.exit(1); }; +const ok = (msg) => console.log('OK: ' + msg); + +function parseChangelog() { + const src = readFileSync(join(ROOT, 'CHANGELOG.md'), 'utf8'); + const vers = [...src.matchAll(/^## \[([0-9]+\.[0-9]+\.[0-9]+)\](?:\s*-\s*(\S+))?/gm)]; + const hasUnreleased = /^## \[Unreleased\]/m.test(src); + const unreleasedBody = hasUnreleased + ? (src.split(/^## \[Unreleased\]/m)[1] || '').split(/^## \[/m)[0].trim() + : ''; + return { vers: vers.map((m) => ({ v: m[1], date: m[2] || '' })), hasUnreleased, unreleasedBody }; +} + +function bump(v, kind) { + const [a, b, c] = v.split('.').map(Number); + if (kind === 'major') return `${a + 1}.0.0`; + if (kind === 'minor') return `${a}.${b + 1}.0`; + if (kind === 'patch') return `${a}.${b}.${c + 1}`; + fail('未知 --bump 类型(major|minor|patch): ' + kind); + return v; +} + +/* ---------- --bump 模式 ---------- */ +const bumpKind = arg('--bump'); +if (bumpKind) { + const date = arg('--date') || new Date().toISOString().slice(0, 10); + const pkgPath = join(ROOT, 'package.json'); + const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')); + const { hasUnreleased, unreleasedBody } = parseChangelog(); + if (!hasUnreleased) fail('CHANGELOG.md 无 [Unreleased] 段,无内容可提升'); + if (unreleasedBody.length < 10) fail('[Unreleased] 段为空或过短,先写变更条目再提升版本'); + const next = bump(pkg.version, bumpKind); + let cl = readFileSync(join(ROOT, 'CHANGELOG.md'), 'utf8'); + cl = cl.replace(/^## \[Unreleased\]/m, `## [Unreleased]\n\n## [${next}] - ${date}`); + writeFileSync(join(ROOT, 'CHANGELOG.md'), cl); + pkg.version = next; + writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n'); + console.log(`bumped: ${next} (${date})`); + console.log('请复核 CHANGELOG.md 与 package.json 后提交:'); + console.log(` git add CHANGELOG.md package.json && git commit -m "chore(release): v${next}"`); + process.exit(0); +} + +/* ---------- 检查模式 ---------- */ +const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')); +const { vers, hasUnreleased, unreleasedBody } = parseChangelog(); +if (!vers.length) fail('CHANGELOG.md 无任何版本段'); +const latest = vers[0]; +console.log(`package.json: ${pkg.version} / CHANGELOG 最新: [${latest.v}] ${latest.date}`); +if (pkg.version !== latest.v) fail(`版本不一致:package.json=${pkg.version} vs CHANGELOG=[${latest.v}](用 --bump 提升,不要手改一边)`); +ok('版本一致'); + +// Unreleased 有内容 = 还有未发布变更,提示而非报错 +if (hasUnreleased && unreleasedBody.length > 10) { + console.log(`NOTE: [Unreleased] 段有 ${unreleasedBody.length} 字符未发布变更,发版前确认是否纳入本次`); +} else { + ok('Unreleased 为空,无遗留变更'); +} + +// dist 构建检查(只验证脚本存在且可跑;产物不入库) +try { + const { execSync } = await import('child_process'); + execSync('node tools/build-dist.mjs --help', { cwd: ROOT, stdio: 'pipe', timeout: 30000 }); + ok('build-dist 可调用'); +} catch { + console.log('NOTE: tools/build-dist.mjs 无 --help(不阻塞,构建时再验证产物)'); +} + +console.log(''); +console.log('发布检查通过。建议的人工执行序列:'); +console.log(` 1) git tag v${pkg.version}`); +console.log(` 2) git push origin main v${pkg.version}`); +console.log(' 3) npm run build && npm publish(需 Q1 决策:是否公开发布)');