Files
aurora-admin/frameworks/select.html
T
aurora-admin eb25feedaf feat(S6-P21): 组件族参数化(13族/44成员/导航族15文件同源/79→48概念组件)
【本次核心 · S6-P21】
- 族层数据:families.json + 44 份契约注入 family/familyRole/familyParams;
  data.json / data.js / site/details 同步。13 族 / 44 成员 / 35 独立 → 概念组件 79→48。
- 79 个 slug 全保留、集合逐一不变(铁律 5 对外承诺未破);frameworks 仍 395 文件、薄壳仍 79。
- 归族判据为契约中可核对字段(semanticTypeCandidates 重叠 / anatomy 为同一骨架子集 /
  变体维度同构 / doNotInvent 显式从属声明),每族 mergeBasis 写明依据,不按名字猜。
- 实现层合并(导航族端到端切片):tools/gen-family-impl.mjs 从 5 端模板生成
  TopMenu / SideMenu / MixedNavigation 共 15 文件,参数 direction=top|side|mixed;
  三份 CSS md5 完全相同 = 一份样式表服务三个组件。
- 新增 tools/gen-families.mjs、tools/gen-family-impl.mjs、tools/verify-families.mjs、
  tools/lib/family-model.mjs、tools/lib/family-impl/nav-menu/*.tpl。

【同时清掉此前已完成但未提交的批次】
生成物(data.json / data.js / site/sources / site/components 薄壳 / sitemap.xml / tests 报告)
跨阶段交织,无法拆成互相自洽的多个提交,故按既有批量风格合并提交:
- Package:三端可 import(S5-P18)+ 发布到私有 npm 源
- Docs site:导航语言改下拉(S5-P19)、详情页代码块默认展开、中英切换完整性
- Security:生产部署链审计修复(2026-09-19)+ 线上部署
- Theme modes 日间/夜间/自动;S1-P4 data.js 瘦身;S2-P5 暗色;S2-P6 跨端一致性;
  S2-P7 行为断言;S2-P9 FAQ;S3-P8 RTL;S3-P9 契约缺口解释层;S4-P12 发布流程
- 补入 tools/pack-deploy.mjs、run-site-smoke.mjs、verify-*.mjs,.dockerignore、
  安全审计修复与待决策项.md

【验收】
- node tools/verify-families.mjs → OK: 族层端到端一致(13 族 / 44 成员 / 79 组件不变 / 395 文件不变)
- node tools/verify-cross-platform.mjs → 79/79 identical(HEAD 基线 high 44)
- node tools/run-regression.mjs → 100%(79/79 页,1017/1017 断言,N/A 34),连跑 8 次一致,0 超时
- 逐页实测:topmenu / sidemenu / mixednavigation 各 13/13,帧内 direction 参数正确,0 JS 错误
- 零运行时依赖 OK;build-site.ps1 ASCII-only OK

【未纳入】site/components/<slug>/ 平台薄壳 316 个 —— 历史从未跟踪且属构建产物,保持现状。
2026-09-20 03:32:31 +08:00

220 lines
11 KiB
HTML
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.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Aurora Admin · Select (H5)</title>
<link rel="stylesheet" href="../.design_library/aurora-admin/colors_and_type.css">
<style>
/* 对齐 components.css 与 select.json 契约(触发器样式同 Input) */
.demo { padding: 24px; background: var(--au-color-page-bg); color: var(--au-color-text-body); font-family: var(--font-family); }
h1 { font-size: 20px; margin: 0 0 4px; }
.sub { color: var(--au-color-text-secondary); margin: 0 0 24px; font-size: 12px; }
.group { margin-bottom: 24px; }
.group h2 { font-size: 16px; margin: 0 0 12px; color: var(--au-color-text-title); }
.row { display: flex; flex-wrap: wrap; gap: 16px; align-items: flex-start; }
.aa-select { position: relative; display: inline-block; min-width: 220px; }
.aa-select-trigger {
display: flex; align-items: center; gap: 8px; min-height: 32px; height: auto;
padding: 2px 28px 2px 12px; background: var(--au-color-card-bg); border: 1px solid var(--au-color-border);
border-radius: 4px; box-sizing: border-box; cursor: pointer; transition: border-color .15s, box-shadow .15s;
font-size: 14px; color: var(--au-color-text-body); flex-wrap: wrap;
}
.aa-select-trigger:hover { border-color: var(--au-color-brand); }
.aa-select.is-open .aa-select-trigger { border-color: var(--au-color-brand); box-shadow: 0 0 0 2px rgba(47,84,235,0.2); }
.aa-select.is-disabled .aa-select-trigger { background: var(--au-color-disabled-bg); border-color: transparent; color: var(--au-color-text-disabled); cursor: not-allowed; }
.aa-select-trigger .placeholder { color: var(--au-color-text-placeholder); }
.aa-select-trigger .caret { position: absolute; right: 10px; top: 50%; transform: translateY(-50%); transition: transform .15s; color: var(--au-color-text-secondary); pointer-events: none; }
.aa-select.is-open .caret { transform: translateY(-50%) rotate(180deg); }
.aa-select-trigger .tag {
display: inline-flex; align-items: center; gap: 4px; background: var(--au-color-brand-bg); color: var(--au-color-brand);
border-radius: 2px; padding: 1px 6px; font-size: 12px; line-height: 20px;
}
.aa-select-trigger .tag .x { cursor: pointer; }
.aa-select-panel {
position: absolute; z-index: 10; top: calc(100% + 4px); left: 0; right: 0;
background: var(--au-color-card-bg); border-radius: 4px; box-shadow: 0 4px 16px rgba(0,0,0,0.12);
max-height: 256px; overflow-y: auto; padding: 4px 0;
}
.aa-select-search { padding: 6px 8px; border-bottom: 1px solid var(--au-color-border-strong); }
.aa-select-search input { width: 100%; box-sizing: border-box; border: 1px solid var(--au-color-border); border-radius: 4px; padding: 4px 8px; font-size: 13px; outline: none; }
.aa-select-search input:focus { border-color: var(--au-color-brand); }
.aa-select-group-label { padding: 6px 12px; font-size: 12px; color: var(--au-color-text-secondary); }
.aa-select-option {
display: flex; align-items: center; gap: 8px; padding: 0 12px; height: 32px; cursor: pointer;
font-size: 14px; color: var(--au-color-text-body); box-sizing: border-box;
}
.aa-select-option:hover { background: var(--au-color-page-bg); }
.aa-select-option.is-selected { background: var(--au-color-brand-bg); color: var(--au-color-brand); }
.aa-select-option .check { visibility: hidden; margin-left: auto; }
.aa-select-option.is-selected .check { visibility: visible; }
.aa-select-option.is-disabled { color: var(--au-color-text-disabled); cursor: not-allowed; }
.aa-select-empty { padding: 16px 12px; text-align: center; color: var(--au-color-text-placeholder); font-size: 13px; }
</style>
</head>
<body>
<div class="demo">
<h1>Aurora Admin · Select 组件(纯 H5)</h1>
<p class="sub">对齐 select.json 契约:触发器同 Input,面板圆角4px / 阴影中 / 最大高度256px,选中项品牌色 + 勾选图标</p>
<div class="group">
<h2>1. 单选</h2>
<div class="row">
<div class="aa-select" data-select data-mode="single" data-name="city">
<div class="aa-select-trigger" role="button" aria-haspopup="listbox" aria-expanded="false" tabindex="0" data-behavior="click-toggles-class:.aa-select|is-open"><span class="placeholder">请选择城市</span><span class="caret">▾</span></div>
<div class="aa-select-panel" hidden>
<div class="aa-select-option is-selected" role="option" aria-selected="true" data-value="bj">北京<span class="check">✓</span></div>
<div class="aa-select-option" role="option" data-value="sh">上海<span class="check">✓</span></div>
<div class="aa-select-option" role="option" data-value="gz">广州<span class="check">✓</span></div>
<div class="aa-select-option" role="option" data-value="sz">深圳<span class="check">✓</span></div>
</div>
</div>
</div>
</div>
<div class="group">
<h2>2. 多选</h2>
<div class="row">
<div class="aa-select" data-select data-mode="multiple" data-name="role">
<div class="aa-select-trigger" role="button" aria-haspopup="listbox" aria-expanded="false" tabindex="0"><span class="placeholder">请选择角色</span><span class="caret">▾</span></div>
<div class="aa-select-panel" hidden>
<div class="aa-select-option is-selected" role="option" aria-selected="true" data-value="admin">管理员<span class="check">✓</span></div>
<div class="aa-select-option" role="option" data-value="editor">编辑<span class="check">✓</span></div>
<div class="aa-select-option" role="option" data-value="viewer">访客<span class="check">✓</span></div>
<div class="aa-select-option is-disabled" role="option" aria-disabled="true" data-value="guest" data-disabled="1">受限访客<span class="check">✓</span></div>
</div>
</div>
</div>
</div>
<div class="group">
<h2>3. 可搜索</h2>
<div class="row">
<div class="aa-select" data-select data-mode="single" data-searchable="1" data-name="user">
<div class="aa-select-trigger" role="button" aria-haspopup="listbox" aria-expanded="false" tabindex="0"><span class="placeholder">搜索并选择用户</span><span class="caret">▾</span></div>
<div class="aa-select-panel" hidden>
<div class="aa-select-search"><input placeholder="输入关键字"></div>
<div class="aa-select-option is-selected" role="option" aria-selected="true" data-value="u1">张伟<span class="check">✓</span></div>
<div class="aa-select-option" role="option" data-value="u2">李娜<span class="check">✓</span></div>
<div class="aa-select-option" role="option" data-value="u3">王芳<span class="check">✓</span></div>
<div class="aa-select-option" role="option" data-value="u4">刘强<span class="check">✓</span></div>
</div>
</div>
</div>
</div>
<div class="group">
<h2>4. 分组</h2>
<div class="row">
<div class="aa-select" data-select data-mode="single" data-grouped="1" data-name="dept">
<div class="aa-select-trigger" role="button" aria-haspopup="listbox" aria-expanded="false" tabindex="0"><span class="placeholder">请选择部门</span><span class="caret">▾</span></div>
<div class="aa-select-panel" hidden>
<div class="aa-select-group-label">技术中心</div>
<div class="aa-select-option" role="option" data-value="fe">前端组<span class="check">✓</span></div>
<div class="aa-select-option" role="option" data-value="be">后端组<span class="check">✓</span></div>
<div class="aa-select-group-label">产品中心</div>
<div class="aa-select-option" role="option" data-value="pm">产品组<span class="check">✓</span></div>
<div class="aa-select-option" role="option" data-value="ux">设计组<span class="check">✓</span></div>
</div>
</div>
</div>
</div>
<div class="group">
<h2>5. 空态(无匹配选项)</h2>
<div class="row">
<div class="aa-select is-open" data-select data-mode="single" data-name="empty">
<div class="aa-select-trigger" role="button" aria-haspopup="listbox" aria-expanded="true" tabindex="0"><span class="placeholder">搜索无结果</span><span class="caret">▾</span></div>
<div class="aa-select-panel">
<div class="aa-select-empty">无匹配选项</div>
</div>
</div>
</div>
</div>
</div>
<script>
document.querySelectorAll('[data-select]').forEach(root => {
const trigger = root.querySelector('.aa-select-trigger');
const panel = root.querySelector('.aa-select-panel');
const mode = root.dataset.mode;
const multiple = mode === 'multiple';
const searchable = root.dataset.searchable === '1';
const grouped = root.dataset.grouped === '1';
let selected = multiple ? [] : null; // value | array of value
function optionLabel(opt) { return opt.textContent.trim(); }
function renderTrigger() {
if (multiple) {
if (!selected.length) { trigger.innerHTML = '<span class="placeholder">请选择角色</span><span class="caret">▾</span>'; return; }
const tags = selected.map(v => {
const txt = root.querySelector(`.aa-select-option[data-value="${v}"]`).textContent.trim();
return `<span class="tag" data-tag="${v}">${txt}<span class="x" data-x="${v}">✕</span></span>`;
}).join('');
trigger.innerHTML = tags + '<span class="caret">▾</span>';
} else {
if (selected == null) { trigger.innerHTML = '<span class="placeholder">请选择</span><span class="caret">▾</span>'; return; }
const txt = root.querySelector(`.aa-select-option[data-value="${selected}"]`).textContent.trim();
trigger.innerHTML = `<span>${txt}</span><span class="caret">▾</span>`;
}
}
function syncSelectedClasses() {
root.querySelectorAll('.aa-select-option').forEach(o => {
const v = o.dataset.value;
const isSel = multiple ? selected.includes(v) : selected === v;
o.classList.toggle('is-selected', isSel);
});
}
function open() {
root.classList.add('is-open');
panel.hidden = false;
if (searchable) panel.querySelector('.aa-select-search input').focus();
}
function close() { root.classList.remove('is-open'); panel.hidden = true; }
trigger.addEventListener('click', (e) => {
if (root.classList.contains('is-disabled')) return;
if (e.target.dataset.x) {
const v = e.target.dataset.x;
selected = selected.filter(x => x !== v);
syncSelectedClasses(); renderTrigger(); return;
}
root.classList.contains('is-open') ? close() : open();
});
panel.addEventListener('click', (e) => {
const opt = e.target.closest('.aa-select-option');
if (!opt || opt.classList.contains('is-disabled')) return;
const v = opt.dataset.value;
if (multiple) {
selected.includes(v) ? selected = selected.filter(x => x !== v) : selected.push(v);
syncSelectedClasses(); renderTrigger();
} else {
selected = v; syncSelectedClasses(); renderTrigger(); close();
}
});
if (searchable) {
panel.querySelector('.aa-select-search input').addEventListener('input', (e) => {
const q = e.target.value.trim().toLowerCase();
root.querySelectorAll('.aa-select-option').forEach(o => {
o.style.display = o.textContent.toLowerCase().includes(q) ? '' : 'none';
});
});
}
document.addEventListener('click', (e) => {
if (!root.contains(e.target)) close();
});
renderTrigger();
});
</script>
</body>
</html>