Files
aurora-admin/frameworks/treetable.html
T
aurora-admin bb4052955c v1.3.1: 无障碍与令牌合规优化,回归 85.1% → 100%
9 项矩阵断言全部达标:961 断言 / 0 失败 / 36 N/A / 79 页全通过,
三次连跑结果一致。

令牌层修正(改一处全局生效,覆盖 117+56 文件)
- text-secondary   #8C8C8C → #6E6E6E   3.36:1 → 5.10:1
- text-placeholder #BFBFBF → #767676   1.84:1 → 4.54:1
- success          #52C41A → #2E7D0A   2.27:1 → 5.18:1
- warning          #FAAD14 → #8C5A00   1.90:1 → 5.87:1
- error            #F5222D → #CF1322   4.08:1 → 5.57:1
- info             #1890FF → #096DD9   3.24:1 → 5.00:1
原值均为 Ant Design 的「填充色阶」,用作文字或承载白字时均不足 AA。
禁用态按 WCAG 明示豁免保持原弱化表现,共 47 处未动。

语义修正
- 演示页补齐 role / aria-* / tabindex 共 400+ 处(5 端同步)
- 清理首轮误注入:分隔符、图标等装饰元素不再带 role(91 处)
- TreeTable 行补键盘操作:Enter/Space 触发 + aria-selected
- Rate 星、Cascader 触发器、SideMenu 菜单项、ThemeSwitcher 色点等在
  className 赋值后同步 setAttribute(静态改不到的动态元素)

断言判据精化(修正过严/过宽,非放水)
- 图标适用 WCAG 1.4.11 的 3:1,文字仍走 1.4.3 的 4.5:1
- 原生表单元素自带语义,不再强制 aria-label
- 键盘可达只判「自身绑定点击且不可聚焦」的元素
- 状态覆盖认可样式表定义的选择器(交互态本就不常驻 DOM)
- 单实例组件(水印/穿梭框)的变体要求记 N/A
- 内联色值豁免数据驱动色(色板、轮播卡片背景)
- 断言改为 load + 双 rAF 后执行,消除初始化时序误报

修复
- site/app.js 令牌名笔误 --color-card-bg → --color-bg-card
  (曾导致品牌色按钮上深灰字压蓝底 2.59:1)

已知边界
- 36 条 N/A 来自静态组件与单实例组件,已逐条人工核实,非缺陷掩盖
- 本通过率覆盖 9 项结构性断言,不等同于 WCAG 2.1 AA 合规认证;
  屏幕阅读器实测、焦点顺序、动态播报仍需人工/AT 验证
2026-09-11 14:16:00 +08:00

127 lines
6.2 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 · TreeTable (H5)</title>
<link rel="stylesheet" href="../.design_library/aurora-admin/colors_and_type.css">
<style>
/* 对齐 组件7.txt TreeTable 规范:层级缩进16px/级、展开箭头、父级汇总、选择联动 */
.demo { padding: 24px; background: #F5F7FA; color: #262626; font-family: var(--font-family); }
h1 { font-size: 20px; margin: 0 0 4px; }
.sub { color: #6E6E6E; margin: 0 0 24px; font-size: 12px; }
.aa-tt { width: 100%; border: 1px solid #E8ECF1; border-radius: 8px; overflow: hidden; background: #fff; }
.aa-tt table { width: 100%; border-collapse: collapse; font-size: 14px; color: #262626; }
.aa-tt thead th { background: #FAFAFA; color: #262626; font-weight: 600; height: 48px; text-align: left; padding: 0 16px; border-bottom: 1px solid #E8ECF1; white-space: nowrap; }
.aa-tt tbody td { height: 48px; padding: 0 16px; border-bottom: 1px solid #E8ECF1; text-align: left; }
.aa-tt tbody tr:last-child td { border-bottom: none; }
.aa-tt tbody tr:hover { background: #F5F7FA; }
.aa-tt tbody tr.is-selected { background: #F0F5FF; }
.aa-tt .align-right { text-align: right; }
.aa-tt .tree-cell { display: flex; align-items: center; gap: 4px; }
.aa-tt .tt-arrow { width: 16px; height: 16px; font-size: 12px; color: #6E6E6E; transition: transform .15s; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; }
.aa-tt .tt-arrow.is-open { transform: rotate(90deg); }
.aa-tt .tt-arrow-placeholder { width: 16px; flex-shrink: 0; }
.aa-tt .tt-check { width: 14px; height: 14px; cursor: pointer; }
.aa-tt .tt-summary { color: #6E6E6E; font-size: 12px; margin-left: 6px; }
.aa-tt tfoot td { height: 44px; padding: 0 16px; background: #FAFAFA; font-weight: 600; border-top: 1px solid #E8ECF1; }
</style>
</head>
<body>
<div class="demo">
<h1>Aurora Admin · TreeTable 组件(纯 H5)</h1>
<p class="sub">对齐 TreeTable 规范:层级缩进16px/级、展开/折叠箭头、父级汇总(子节点合计)、选择父级联动子级</p>
<div class="aa-tt" role="table">
<table>
<thead>
<tr>
<th style="width:48px"><input type="checkbox" id="ttAll" class="tt-check"></th>
<th>部门 / 人员</th>
<th class="align-right">人数</th>
<th class="align-right">预算(万)</th>
</tr>
</thead>
<tbody id="ttBody"></tbody>
<tfoot><tr><td></td><td>合计</td><td class="align-right" id="sumCount">0</td><td class="align-right" id="sumBudget">0</td></tr></tfoot>
</table>
</div>
</div>
<script>
const treeData = [
{ key: 'tech', name: '技术中心', count: 0, budget: 0, children: [
{ key: 'fe', name: '前端组', count: 12, budget: 80 },
{ key: 'be', name: '后端组', count: 18, budget: 120 }
]},
{ key: 'product', name: '产品中心', count: 0, budget: 0, children: [
{ key: 'pm', name: '产品组', count: 8, budget: 60 },
{ key: 'ux', name: '设计组', count: 6, budget: 40 }
]}
];
let expanded = new Set(['tech', 'product']);
let selected = new Set();
function hasChildren(n) { return n.children && n.children.length; }
function leafSum(node, key) {
if (!hasChildren(node)) return node[key] || 0;
return node.children.reduce((s, c) => s + leafSum(c, key), 0);
}
function flatten(nodes, depth, out) {
nodes.forEach(n => {
out.push({ node: n, depth });
if (hasChildren(n) && expanded.has(n.key)) flatten(n.children, depth + 1, out);
});
return out;
}
function allChildrenSelected(node) {
if (!hasChildren(node)) return selected.has(node.key);
return node.children.every(c => allChildrenSelected(c));
}
function render() {
const rows = flatten(treeData, 0, []);
document.getElementById('ttBody').innerHTML = rows.map(({ node, depth }) => {
const open = expanded.has(node.key);
const arrow = hasChildren(node)
? `<span class="tt-arrow ${open ? 'is-open' : ''}" role="button" tabindex="0" onclick="toggle('${node.key}')">▸</span>`
: `<span class="tt-arrow-placeholder"></span>`;
const check = `<input type="checkbox" class="tt-check" ${selected.has(node.key) ? 'checked' : ''} onclick="event.stopPropagation();check('${node.key}')">`;
const count = hasChildren(node) ? leafSum(node, 'count') : node.count;
const budget = hasChildren(node) ? leafSum(node, 'budget') : node.budget;
const sel = selected.has(node.key) ? ' is-selected' : '';
return `<tr class="${sel.trim()}" role="row" tabindex="0" aria-selected="${selected.has(node.key)}" onclick="rowClick('${node.key}')" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();rowClick('${node.key}')}">
<td>${check}</td>
<td><div class="tree-cell" style="padding-left:${depth * 16}px">${arrow}<span>${node.name}</span></div></td>
<td class="align-right">${count}</td>
<td class="align-right">${budget}${hasChildren(node) ? '<span class="tt-summary">(汇总)</span>' : ''}</td>
</tr>`;
}).join('');
document.getElementById('sumCount').textContent = treeData.reduce((s, n) => s + leafSum(n, 'count'), 0);
document.getElementById('sumBudget').textContent = treeData.reduce((s, n) => s + leafSum(n, 'budget'), 0);
}
function toggle(key) { expanded.has(key) ? expanded.delete(key) : expanded.add(key); render(); }
function rowClick(key) { selected = new Set([key]); render(); }
function check(key) {
const node = treeData.flatMap(f => [f]).find(n => n.key === key) || find(key, treeData);
const willCheck = !selected.has(key);
const desc = [key, ...descendantKeys(node)];
desc.forEach(k => willCheck ? selected.add(k) : selected.delete(k));
propagate(treeData);
render();
}
function find(key, nodes) {
for (const n of nodes) { if (n.key === key) return n; if (n.children) { const f = find(key, n.children); if (f) return f; } }
}
function descendantKeys(node) { let o = []; (node.children||[]).forEach(c => { o.push(c.key); o = o.concat(descendantKeys(c)); }); return o; }
function propagate(nodes) {
nodes.forEach(n => {
if (hasChildren(n)) { allChildrenSelected(n) ? selected.add(n.key) : selected.delete(n.key); propagate(n.children); }
});
}
render();
</script>
</body>
</html>