表格Table
把多条同构记录按列并排,用于订单列表 / 库存明细 / 对账单这类需要逐行横向对比的场合
数据展示 规格 47 · 表格 Table 6 端实现 触摸优先
<!-- ① 令牌:PC 令牌 + 移动端 --kole-m-* 合成单文件,引一次 -->
<link rel="stylesheet" href="kole-ui/mobile/tokens.css">
<!-- ② 本组件样式(全量则用 kole-ui/mobile/components/index.css) -->
<link rel="stylesheet" href="kole-ui/mobile/components/mobile-table.css">
<!-- ③ 结构照抄下方任一演示块(类名与 6 端实现一致) -->
演示
每个演示都是真实渲染:预览帧加载 frameworks-mobile/Table.html?demo=<id>(只显示该演示块),代码是该演示块在演示页里的原文,可复制。全部演示同屏可看 演示页 ↗。
01 组件类型
列多时在表格自己的容器里左右滑,页面整体不横向溢出。
查看代码(演示页原文 · 50 行)
<section class="demo-block" data-demo="scroll">
<p class="demo-label">横向滚动(列多时在表格自己的容器里左右滑;页面整体不横向溢出)</p>
<div class="demo-pad" data-assert="table-scroll">
<p class="demo-hint">← 左右滑动查看全部 6 列 →</p>
<div class="kole-m-table kole-m-table--bordered" id="tb-scroll"
style="--kole-m-table-min-width: 560px" data-assert="table-scroll-wide">
<div class="kole-m-table__viewport" id="tb-scroll-viewport">
<table class="kole-m-table__inner">
<caption class="kole-m-table__caption">近 3 日订单</caption>
<thead class="kole-m-table__head">
<tr class="kole-m-table__row">
<th class="kole-m-table__head-cell kole-m-table__head-cell--start" scope="col">订单号</th>
<th class="kole-m-table__head-cell kole-m-table__head-cell--start" scope="col">客户</th>
<th class="kole-m-table__head-cell" scope="col">状态</th>
<th class="kole-m-table__head-cell kole-m-table__head-cell--number" scope="col">金额</th>
<th class="kole-m-table__head-cell" scope="col">下单时间</th>
<th class="kole-m-table__head-cell kole-m-table__head-cell--end" scope="col">操作</th>
</tr>
</thead>
<tbody>
<tr class="kole-m-table__row">
<td class="kole-m-table__cell kole-m-table__cell--start">SO-2026-0920-001</td>
<td class="kole-m-table__cell kole-m-table__cell--start">王小明</td>
<td class="kole-m-table__cell">待发货</td>
<td class="kole-m-table__cell kole-m-table__cell--number">¥ 1,280.00</td>
<td class="kole-m-table__cell">09-20 09:12</td>
<td class="kole-m-table__cell kole-m-table__cell--end">查看</td>
</tr>
<tr class="kole-m-table__row">
<td class="kole-m-table__cell kole-m-table__cell--start">SO-2026-0920-002</td>
<td class="kole-m-table__cell kole-m-table__cell--start">李小红</td>
<td class="kole-m-table__cell">已发货</td>
<td class="kole-m-table__cell kole-m-table__cell--number">¥ 368.50</td>
<td class="kole-m-table__cell">09-20 10:35</td>
<td class="kole-m-table__cell kole-m-table__cell--end">查看</td>
</tr>
<tr class="kole-m-table__row">
<td class="kole-m-table__cell kole-m-table__cell--start">SO-2026-0920-003</td>
<td class="kole-m-table__cell kole-m-table__cell--start">赵大力</td>
<td class="kole-m-table__cell">已完成</td>
<td class="kole-m-table__cell kole-m-table__cell--number">¥ 5,600.00</td>
<td class="kole-m-table__cell">09-20 14:08</td>
<td class="kole-m-table__cell kole-m-table__cell--end">查看</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</section>mode=card:小屏上把每行摊成键值对,彻底不需要横向滚动。
查看代码(演示页原文 · 30 行)
<section class="demo-block" data-demo="card">
<p class="demo-label">卡片模式(mode=card:小屏上把每行摊成键值对,彻底不需要横向滚动)</p>
<div class="demo-pad" data-assert="table-card">
<div class="kole-m-table kole-m-table--card" id="tb-card">
<div class="kole-m-table__viewport">
<table class="kole-m-table__inner">
<thead class="kole-m-table__head">
<tr class="kole-m-table__row">
<th class="kole-m-table__head-cell" scope="col">客户</th>
<th class="kole-m-table__head-cell" scope="col">等级</th>
<th class="kole-m-table__head-cell" scope="col">本月消费</th>
</tr>
</thead>
<tbody>
<tr class="kole-m-table__row">
<td class="kole-m-table__cell" data-label="客户">王小明</td>
<td class="kole-m-table__cell" data-label="等级">黄金</td>
<td class="kole-m-table__cell" data-label="本月消费">¥ 1,280.00</td>
</tr>
<tr class="kole-m-table__row">
<td class="kole-m-table__cell" data-label="客户">李小红</td>
<td class="kole-m-table__cell" data-label="等级">白银</td>
<td class="kole-m-table__cell" data-label="本月消费">¥ 368.50</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</section>02 组件状态
点整行选中该行;数字列等宽右对齐便于按位对比,斑马纹便于横向对齐行。
查看代码(演示页原文 · 41 行)
<section class="demo-block" data-demo="clickable">
<p class="demo-label">表头固定 + 列对齐 + 行点击(点整行选中该行;数字列等宽右对齐,便于按位对比)</p>
<div class="demo-pad" data-assert="table-clickable">
<div class="kole-m-table kole-m-table--clickable kole-m-table--stripe kole-m-table--bordered" id="tb-clickable"
data-selected="" data-assert="table-clickable-grid">
<div class="kole-m-table__viewport">
<table class="kole-m-table__inner">
<thead class="kole-m-table__head">
<tr class="kole-m-table__row">
<th class="kole-m-table__head-cell kole-m-table__head-cell--start" scope="col">商品</th>
<th class="kole-m-table__head-cell" scope="col">仓库</th>
<th class="kole-m-table__head-cell kole-m-table__head-cell--number" scope="col">库存</th>
<th class="kole-m-table__head-cell kole-m-table__head-cell--number" scope="col">单价</th>
</tr>
</thead>
<tbody>
<tr class="kole-m-table__row" data-row="sku-1" tabindex="0" role="row" aria-selected="false"
data-behavior="click-sets-attr:#tb-clickable|data-selected|sku-1">
<td class="kole-m-table__cell kole-m-table__cell--start">有机棉 T 恤</td>
<td class="kole-m-table__cell">杭州仓</td>
<td class="kole-m-table__cell kole-m-table__cell--number">1,240</td>
<td class="kole-m-table__cell kole-m-table__cell--number">¥ 89.00</td>
</tr>
<tr class="kole-m-table__row" data-row="sku-2" tabindex="0" role="row" aria-selected="false">
<td class="kole-m-table__cell kole-m-table__cell--start">轻薄羽绒服</td>
<td class="kole-m-table__cell">广州仓</td>
<td class="kole-m-table__cell kole-m-table__cell--number">86</td>
<td class="kole-m-table__cell kole-m-table__cell--number">¥ 459.00</td>
</tr>
<tr class="kole-m-table__row is-selected" data-row="sku-3" tabindex="0" role="row" aria-selected="true">
<td class="kole-m-table__cell kole-m-table__cell--start">帆布托特包</td>
<td class="kole-m-table__cell">杭州仓</td>
<td class="kole-m-table__cell kole-m-table__cell--number">512</td>
<td class="kole-m-table__cell kole-m-table__cell--number">¥ 129.00</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</section>没有数据时显示占位文案,列宽语义不变,不会把容器撑宽。
查看代码(演示页原文 · 20 行)
<section class="demo-block" data-demo="empty">
<p class="demo-label">空态(没有数据时显示占位文案;列宽语义不变,不会把容器撑宽)</p>
<div class="demo-pad" data-assert="table-empty">
<div class="kole-m-table" id="tb-empty" data-empty="true">
<div class="kole-m-table__viewport">
<table class="kole-m-table__inner">
<thead class="kole-m-table__head">
<tr class="kole-m-table__row">
<th class="kole-m-table__head-cell kole-m-table__head-cell--start" scope="col">订单号</th>
<th class="kole-m-table__head-cell" scope="col">状态</th>
<th class="kole-m-table__head-cell kole-m-table__head-cell--number" scope="col">金额</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<p class="kole-m-table__empty" role="status" aria-live="polite">当前筛选条件下没有订单</p>
</div>
</div>
</section>禁用行置灰不响应;长文本列单行省略,完整值在 title 里。
查看代码(演示页原文 · 28 行)
<section class="demo-block" data-demo="states">
<p class="demo-label">禁用行与省略列(禁用行置灰不响应;长文本列单行省略,完整值在 title 里)</p>
<div class="demo-pad" data-assert="table-states">
<div class="kole-m-table kole-m-table--bordered" id="tb-states">
<div class="kole-m-table__viewport">
<table class="kole-m-table__inner">
<thead class="kole-m-table__head">
<tr class="kole-m-table__row">
<th class="kole-m-table__head-cell kole-m-table__head-cell--start" scope="col">收货地址</th>
<th class="kole-m-table__head-cell" scope="col">配送</th>
</tr>
</thead>
<tbody>
<tr class="kole-m-table__row">
<td class="kole-m-table__cell kole-m-table__cell--start kole-m-table__cell--ellipsis"
title="浙江省杭州市西湖区文三路 100 号 1 幢 1801 室">浙江省杭州市西湖区文三路 100 号 1 幢 1801 室</td>
<td class="kole-m-table__cell">顺丰</td>
</tr>
<tr class="kole-m-table__row is-disabled" aria-disabled="true">
<td class="kole-m-table__cell kole-m-table__cell--start">广东省深圳市南山区科技园南区</td>
<td class="kole-m-table__cell">暂不可达</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</section>API
props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs
Props
| 名称 | 类型 | 默认值 | 说明 | 必传 |
|---|---|---|---|---|
columns | Array<{ title: string, key?: string, align?: 'start'|'center'|'end', number?: boolean }> | [] | 列定义(宿主提供;number 是数字列的便捷写法,等价于 align=end + 等宽字)(规格 §47.3) | N |
rows | Array<Array | { key?: string, selected?: boolean, disabled?: boolean }> | [] | 行数据,二维数组或对象数组;单元格支持 { text, align, ellipsis, number, title }(规格 §47.2) | N |
mode | 'scroll' | 'card' | 'scroll' | 变体 mode:scroll 横向滚动,card 每行摊成键值对(规格 §47.3) | N |
size | 'default' | 'compact' | 'default' | 变体 size:compact 收紧内边距与字号,行高仍不小于 44px(规格 §47.3) | N |
stripe | boolean | false | 变体 stripe:偶数行浅底,便于横向扫读时对齐行(规格 §47.3) | N |
bordered | boolean | false | 变体 bordered:单元格之间也画竖线(规格 §47.3) | N |
clickable | boolean | false | 状态 clickable:整行可点,按下反馈是整行背景变化(规格 §47.4) | N |
caption | string | '' | 可选表格标题(说明这张表在讲什么)(规格 §47.2) | N |
emptyText | string | '暂无数据' | 状态 empty 的占位文案(规格 §47.4) | N |
「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。
事件
| 名称 | 参数 | 说明 |
|---|---|---|
select | index: number, row: any | 点整行时触发,回传行下标与该行数据(clickable=true 时才有意义)(规格 §47.5) |
插槽
| 名称 | 说明 |
|---|---|
default | 自定义内容(追加在空态之后) |
CSS 变量
组件级变量(在组件样式表里定义)。业务侧可在自己的作用域内覆盖,不必改组件源码。
| 名称 | 默认值 | 说明 |
|---|---|---|
--kole-m-table-row-height | var(--kole-m-touch-target) | 行最小高度(≥44px 触控最小边长) |
--kole-m-table-cell-padding | var(--kole-space-12) | 单元格左右内边距 |
--kole-m-table-min-width | 0 | 内容最小宽度;超过容器即横向滚动 |
何时使用
- 把多条同构记录按列并排,用于订单列表 / 库存明细 / 对账单这类需要逐行横向对比的场合
- 移动端表格必须在组件自己的容器里横向滚动,绝不能让页面整体横向溢出,否则整页会跟着左右晃
- 横向滚动发生在 table__viewport 上:根与视口都写 min-width: 0,否则内部 min-width 会把父级顶宽
- 表头用 position: sticky 而不是 fixed —— fixed 会脱出滚动容器,横向滚动时表头不跟随
- 长文本列默认单行省略,完整值放 title;数字列用等宽字右对齐,便于按位对比
- 点整行即选中(同表互斥);行内的独立控件要阻止冒泡,避免一次点击触发两个动作
交互与触控
- 横向滚动发生在 table__viewport 上:根与视口都写 min-width: 0,否则内部 min-width 会把父级顶宽、页面整体横向溢出
- 选项行高不小于 44px;触屏上横向滑动不被父级纵向滚动吞掉(touch-action: pan-x pan-y)
- 表头用 position: sticky 而不是 fixed——fixed 会脱出滚动容器,横向滚动时表头不跟随
- 长文本列默认单行省略,完整值放 title;数字列用等宽字右对齐,便于按位对比
- 点整行即选中(同表互斥);行内的独立控件(按钮 / 链接)要阻止冒泡,避免一次点击触发两个动作
- 行可点时同时提供键盘路径(聚焦后回车 / 空格),不把鼠标点击当成唯一入口
无障碍
- 用原生 table / thead / tbody / tr / th / td,表头写 scope="col"(读屏能报出列名)
- 空态用 role="status" + aria-live="polite" 播报占位文案
- 禁用行写 aria-disabled="true",读屏播报不可用
- 可点行必须同时可聚焦:clickable=true 时行写 tabindex="0" + role="row" + aria-selected,聚焦后回车 / 空格即选中 —— 只有鼠标点击而没有键盘路径是缺陷,不是简化
- 焦点环画在行内第一个单元格上(tr 上的 outline 在部分浏览器不渲染)
- mode=card 时字段名由 data-label 生成并参与读屏(键值对在视觉与语义上都成立)
相似组件
从「该用哪一个」的角度区分;PC 端的对应实现见 PC 文档站。
| 组件 | 何时用它而不是本组件 |
|---|---|
| 列表List | 一行只需「标题 + 值 + 箭头」时用列表,不需要列对齐;列对比才是表格的强项 |
| 单元格Cell | 键值对数量很少(三四项)时用单元格纵向排列,比建一张两列表格更轻 |
| 折叠面板Collapse | 需要展开某一行看明细时用折叠面板承载详情,表格本身不做行内展开 |
| 加载Loading | 表格数据加载中用它占位,不要在表体里自己画骨架屏 |
| 标签Tag | 状态列用小标签呈现(已发货 / 待付款)比纯文字更好扫读 |
规格未定 / 禁止发明
| 类别 | 条目 |
|---|---|
| 禁止发明 | 虚拟滚动与无限加载的触发规则(由宿主容器负责;本组件不接管纵向滚动) |
| 禁止发明 | 服务端排序 / 筛选 / 分页(排序图标与请求都由宿主发起) |
| 禁止发明 | 列宽拖拽调整与列显隐设置(移动端没有这个操作面) |
| 禁止发明 | 冻结列(position: sticky 的横向版本;当前只固定表头的纵向位置) |
| 规格未定 | 表头是否需要在纵向滚动时也吸顶(当前只在宿主给固定高度时生效) |
| 规格未定 | 行选中是单选还是多选(当前单选;多选应交给 Checkbox 列) |
| 规格未定 | 卡片模式下字段名的排列是「左标签右值」还是「上标签下值」 |
结构(anatomy)
| 字段 | 说明 |
|---|---|
table | 根元素,纵向排列「视口 + 空态」 |
table__viewport | 横向滚动的唯一发生点(overflow-x: auto),页面整体不横向溢出 |
table__inner | 原生 table,min-width 由 --kole-m-table-min-width 给出;超过容器即滚 |
table__caption | 可选表格标题(说明这张表在讲什么) |
table__head | 表头,position: sticky 固定在滚动视口顶部 |
table__head-cell | 表头单元格,写 scope=col 让读屏报出列名 |
table__row | 数据行,行高不小于 44px |
table__cell | 单元格,可带 data-label(卡片模式的字段名来源) |
table__empty | 空态占位,role=status + aria-live=polite |
变体维度与类名映射
类名映射由构建脚本从契约 variantClasses 生成,并被 verify:mobile-docs 逐条对照组件 CSS 校验(类/变量必须真实存在)。
| 维度 | 取值 | 对应类名 / 变量 |
|---|---|---|
mode | scroll / card | scroll .kole-m-table--scroll card .kole-m-table--card |
size | default / compact | default (由数据驱动,无专属类) compact .kole-m-table--compact |
stripe | false / true | false (由数据驱动,无专属类) true .kole-m-table--stripe |
bordered | false / true | false (由数据驱动,无专属类) true .kole-m-table--bordered |
代表变体
| 变体 | 标签 |
|---|---|
mode=scroll · size=default · stripe=false · bordered=true | 横向滚动(列多时在组件容器里左右滑) |
mode=scroll · size=default · stripe=true · bordered=false | 斑马纹(偶数行浅底,便于对齐行) |
mode=card · size=default · stripe=false · bordered=false | 卡片模式(每行摊成键值对,小屏不横滚) |
mode=scroll · size=compact · stripe=false · bordered=true | 紧凑(收紧内边距与字号,行高仍 ≥44px) |
用到的令牌
构建时从本组件样式表扫描得出。蓝色为移动端自有令牌,绿色为继承的 PC 令牌(改一处两端生效)。
6 端源码
同一组件的六份实现(生产环境的类名与结构一致,差异只在技术栈写法与单位)。点开查看,右侧可复制。
frameworks-mobile/Table.css · 纯样式(CSS) · 229 行
/* Kole UI Mobile · Table 样式 — 对齐移动端规格 §47
表格:列多时必须能横向滚动,但**不得让页面整体横向溢出** ——
滚动发生在组件自己的容器上(overflow-x: auto),而不是文档上。
表头固定用 position: sticky(不用 fixed:fixed 会脱出滚动容器,横向滚动时表头不跟随)。
行高不小于 44px;空态与加载态都在同一个容器内,不改变表格宽度。 */
.kole-m-table {
/* 组件级变量:业务侧可在容器上覆盖 */
--kole-m-table-row-height: var(--kole-m-touch-target); /* 行最小高度(≥44px 触控最小边长) */
--kole-m-table-cell-padding: var(--kole-space-12); /* 单元格左右内边距 */
--kole-m-table-min-width: 0; /* 内容最小宽度;超过容器即横向滚动 */
box-sizing: border-box;
display: flex;
flex-direction: column;
width: 100%;
/* 横向滚动容器:min-width: 0 让它能在 flex 父级里收缩,不把父级顶宽 */
min-width: 0;
background: var(--kole-color-card-bg);
color: var(--kole-color-text-body);
font-family: var(--kole-font-family);
font-size: var(--kole-m-font-size-body);
line-height: 1.4;
}
/* 滚动视口:横向滚动的唯一发生点。页面级横向溢出由它兜住。 */
.kole-m-table__viewport {
box-sizing: border-box;
width: 100%;
min-width: 0;
max-width: 100%;
overflow-x: auto;
overflow-y: visible;
-webkit-overflow-scrolling: touch;
/* 触屏上横向滑动不被父级纵向滚动吞掉 */
touch-action: pan-x pan-y;
}
.kole-m-table__caption {
margin: 0;
padding: var(--kole-m-table-cell-padding) var(--kole-m-gutter);
color: var(--kole-color-text-secondary);
font-size: var(--kole-m-font-size-label);
}
.kole-m-table__inner {
min-width: var(--kole-m-table-min-width);
margin: 0;
border-collapse: separate;
border-spacing: 0;
width: 100%;
}
/* 变体 mode=scroll:默认形态 —— 列多时在组件自己的视口里横向滚动 */
.kole-m-table--scroll .kole-m-table__viewport { overflow-x: auto; }
/* 变体 size=compact:行高 44px 不变(触控下限),只收紧上下内边距与字号 */
.kole-m-table--compact .kole-m-table__inner { font-size: var(--kole-m-font-size-label); }
/* 表头:sticky 在滚动视口顶部。由于视口只横向滚动、不设最大高度,
sticky 主要保证纵向滚动由页面承担时表头仍跟随(宿主给固定高度时也生效)。 */
.kole-m-table__head-cell {
box-sizing: border-box;
position: sticky;
top: 0;
z-index: 2;
padding: var(--kole-m-table-cell-padding) var(--kole-m-table-cell-padding);
margin: 0;
background: var(--kole-color-table-header-bg);
color: var(--kole-color-text-secondary);
font-weight: 500;
font-size: var(--kole-m-font-size-label);
line-height: 1.3;
white-space: nowrap;
text-align: start;
border-bottom: 1px solid var(--kole-color-border);
}
/* 变体 bordered=true:单元格之间也画竖线 */
.kole-m-table--bordered .kole-m-table__head-cell + .kole-m-table__head-cell,
.kole-m-table--bordered .kole-m-table__cell + .kole-m-table__cell {
border-inline-start: 1px solid var(--kole-color-border);
}
/* 变体 stripe=true:偶数行浅底,便于横向扫读时对齐行 */
.kole-m-table--stripe .kole-m-table__row:nth-child(even) .kole-m-table__cell {
background: var(--kole-color-table-header-bg);
}
.kole-m-table__cell {
box-sizing: border-box;
padding: var(--kole-m-table-cell-padding);
margin: 0;
background: var(--kole-color-card-bg);
color: var(--kole-color-text-body);
font-size: inherit;
line-height: 1.4;
vertical-align: middle;
text-align: start;
}
.kole-m-table__row { min-height: var(--kole-m-table-row-height); }
.kole-m-table__row .kole-m-table__cell { border-bottom: 1px solid var(--kole-color-border); }
.kole-m-table__row:last-child .kole-m-table__cell { border-bottom: 0; }
/* 列对齐(变体 align 由列定义给出;这里是三档工具类) */
.kole-m-table__cell--start,
.kole-m-table__head-cell--start { text-align: start; }
.kole-m-table__cell--center,
.kole-m-table__head-cell--center { text-align: center; }
.kole-m-table__cell--end,
.kole-m-table__head-cell--end { text-align: end; }
/* 数字列:等宽字 + 右对齐,方便按位对比 */
.kole-m-table__cell--number,
.kole-m-table__head-cell--number {
font-family: var(--kole-font-family-num);
text-align: end;
font-variant-numeric: tabular-nums;
}
/* 省略:长文本列默认单行省略,完整值由 title 承载 */
.kole-m-table__cell--ellipsis {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
max-width: 160px;
}
/* 状态 clickable:整行是热区(clickable=true)。
可点行必须**同时可聚焦**(tabindex="0" + role="row")—— 否则点击成了唯一的交互路径,
键盘与读屏用户点不到(规格 §47.6 明说「整行点击不能是唯一的键盘路径」)。
cursor 只给表体行:表头行不承载点击,跟着变手型会把「这里能点」这个信号弄错。 */
.kole-m-table__body .kole-m-table__row { cursor: pointer; }
.kole-m-table__body .kole-m-table__row:active .kole-m-table__cell {
background: var(--kole-color-brand-bg);
}
/* 可聚焦行的焦点环:画在行内第一个单元格上(tr 上画 outline 在部分浏览器不渲染) */
.kole-m-table__row:focus-visible .kole-m-table__cell:first-child {
outline: 2px solid var(--kole-color-focus-ring);
outline-offset: -2px;
}
/* 状态 selected:当前选中行 */
.kole-m-table__row.is-selected .kole-m-table__cell {
background: var(--kole-color-brand-bg);
color: var(--kole-color-brand);
font-weight: 500;
}
/* 状态 disabled:置灰且不响应 */
.kole-m-table__row.is-disabled .kole-m-table__cell {
color: var(--kole-color-text-disabled);
cursor: not-allowed;
}
/* 行内可聚焦元素(链接 / 按钮)的焦点环 */
.kole-m-table__cell a:focus-visible,
.kole-m-table__cell button:focus-visible {
outline: 2px solid var(--kole-color-focus-ring);
outline-offset: 2px;
}
/* 状态 empty:空态占位(不改变表格列宽语义,直接替换表格区) */
.kole-m-table__empty {
margin: 0;
padding: var(--kole-space-40) var(--kole-m-gutter);
background: var(--kole-color-card-bg);
color: var(--kole-color-text-secondary);
font-size: var(--kole-m-font-size-label);
text-align: center;
border-bottom: 1px solid var(--kole-color-border);
}
/* 变体 mode=card:小屏上把每行渲染成一张卡(键值对纵排),彻底不横向滚动 */
.kole-m-table--card .kole-m-table__viewport { overflow-x: visible; }
.kole-m-table--card .kole-m-table__inner { display: block; }
.kole-m-table--card .kole-m-table__head { display: none; }
.kole-m-table--card .kole-m-table__row {
display: block;
padding: var(--kole-m-table-cell-padding) var(--kole-m-gutter);
border-bottom: 1px solid var(--kole-color-border);
}
.kole-m-table--card .kole-m-table__row .kole-m-table__cell {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: var(--kole-space-12);
padding: var(--kole-space-4) 0;
border-bottom: 0;
}
/* 卡片模式下的字段名:由列的 label 生成(data-label 由实现写出) */
.kole-m-table--card .kole-m-table__cell::before {
content: attr(data-label);
flex: 0 0 auto;
color: var(--kole-color-text-secondary);
font-size: var(--kole-m-font-size-label);
}
/* 演示页触发器(非组件交付样式) */
.kole-m-table__demo-btn {
box-sizing: border-box;
min-height: var(--kole-m-touch-target);
padding: 0 var(--kole-m-gutter);
border: 1px solid var(--kole-color-border);
border-radius: var(--kole-radius-base);
background: var(--kole-color-card-bg);
color: var(--kole-color-text-body);
font-family: inherit;
font-size: var(--kole-m-font-size-label);
cursor: pointer;
touch-action: manipulation;
}
.kole-m-table__demo-btn:focus-visible {
outline: 2px solid var(--kole-color-focus-ring);
outline-offset: 2px;
}
frameworks-mobile/Table.html · H5 原生(无框架) · 256 行
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>Kole UI Mobile · Table(H5)</title>
<link rel="stylesheet" href="../.design_library/kole-ui-mobile/colors_and_type.css">
<link rel="stylesheet" href="Table.css">
<style>
body { margin: 0; background: var(--kole-color-page-bg); font-family: var(--kole-font-family); color: var(--kole-color-text-body); }
/* 关键:.demo 与各演示块都写 min-width: 0,否则内部表格的 min-width 会把页面顶宽 */
.demo { max-width: 375px; margin: 0 auto; padding: var(--kole-m-gutter) 0; }
.demo-block { min-width: 0; }
.demo-label { margin: 0; padding: var(--kole-space-12) var(--kole-m-gutter) var(--kole-space-8);
font-size: var(--kole-m-font-size-label); color: var(--kole-color-text-secondary); }
.demo-pad { min-width: 0; padding: 0 0 var(--kole-space-8); background: var(--kole-color-card-bg);
border-block: 1px solid var(--kole-color-border); }
/* 横向滚动的提示条:说明「这里可以左右滑」,不是组件的一部分 */
.demo-hint { margin: 0; padding: var(--kole-space-8) var(--kole-m-gutter) 0;
font-size: var(--kole-m-font-size-caption); color: var(--kole-color-text-secondary); }
.demo-block[hidden] { display: none; }
</style>
</head>
<body>
<div class="demo">
<section class="demo-block" data-demo="scroll">
<p class="demo-label">横向滚动(列多时在表格自己的容器里左右滑;页面整体不横向溢出)</p>
<div class="demo-pad" data-assert="table-scroll">
<p class="demo-hint">← 左右滑动查看全部 6 列 →</p>
<div class="kole-m-table kole-m-table--bordered" id="tb-scroll"
style="--kole-m-table-min-width: 560px" data-assert="table-scroll-wide">
<div class="kole-m-table__viewport" id="tb-scroll-viewport">
<table class="kole-m-table__inner">
<caption class="kole-m-table__caption">近 3 日订单</caption>
<thead class="kole-m-table__head">
<tr class="kole-m-table__row">
<th class="kole-m-table__head-cell kole-m-table__head-cell--start" scope="col">订单号</th>
<th class="kole-m-table__head-cell kole-m-table__head-cell--start" scope="col">客户</th>
<th class="kole-m-table__head-cell" scope="col">状态</th>
<th class="kole-m-table__head-cell kole-m-table__head-cell--number" scope="col">金额</th>
<th class="kole-m-table__head-cell" scope="col">下单时间</th>
<th class="kole-m-table__head-cell kole-m-table__head-cell--end" scope="col">操作</th>
</tr>
</thead>
<tbody>
<tr class="kole-m-table__row">
<td class="kole-m-table__cell kole-m-table__cell--start">SO-2026-0920-001</td>
<td class="kole-m-table__cell kole-m-table__cell--start">王小明</td>
<td class="kole-m-table__cell">待发货</td>
<td class="kole-m-table__cell kole-m-table__cell--number">¥ 1,280.00</td>
<td class="kole-m-table__cell">09-20 09:12</td>
<td class="kole-m-table__cell kole-m-table__cell--end">查看</td>
</tr>
<tr class="kole-m-table__row">
<td class="kole-m-table__cell kole-m-table__cell--start">SO-2026-0920-002</td>
<td class="kole-m-table__cell kole-m-table__cell--start">李小红</td>
<td class="kole-m-table__cell">已发货</td>
<td class="kole-m-table__cell kole-m-table__cell--number">¥ 368.50</td>
<td class="kole-m-table__cell">09-20 10:35</td>
<td class="kole-m-table__cell kole-m-table__cell--end">查看</td>
</tr>
<tr class="kole-m-table__row">
<td class="kole-m-table__cell kole-m-table__cell--start">SO-2026-0920-003</td>
<td class="kole-m-table__cell kole-m-table__cell--start">赵大力</td>
<td class="kole-m-table__cell">已完成</td>
<td class="kole-m-table__cell kole-m-table__cell--number">¥ 5,600.00</td>
<td class="kole-m-table__cell">09-20 14:08</td>
<td class="kole-m-table__cell kole-m-table__cell--end">查看</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</section>
<section class="demo-block" data-demo="clickable">
<p class="demo-label">表头固定 + 列对齐 + 行点击(点整行选中该行;数字列等宽右对齐,便于按位对比)</p>
<div class="demo-pad" data-assert="table-clickable">
<div class="kole-m-table kole-m-table--clickable kole-m-table--stripe kole-m-table--bordered" id="tb-clickable"
data-selected="" data-assert="table-clickable-grid">
<div class="kole-m-table__viewport">
<table class="kole-m-table__inner">
<thead class="kole-m-table__head">
<tr class="kole-m-table__row">
<th class="kole-m-table__head-cell kole-m-table__head-cell--start" scope="col">商品</th>
<th class="kole-m-table__head-cell" scope="col">仓库</th>
<th class="kole-m-table__head-cell kole-m-table__head-cell--number" scope="col">库存</th>
<th class="kole-m-table__head-cell kole-m-table__head-cell--number" scope="col">单价</th>
</tr>
</thead>
<tbody>
<tr class="kole-m-table__row" data-row="sku-1" tabindex="0" role="row" aria-selected="false"
data-behavior="click-sets-attr:#tb-clickable|data-selected|sku-1">
<td class="kole-m-table__cell kole-m-table__cell--start">有机棉 T 恤</td>
<td class="kole-m-table__cell">杭州仓</td>
<td class="kole-m-table__cell kole-m-table__cell--number">1,240</td>
<td class="kole-m-table__cell kole-m-table__cell--number">¥ 89.00</td>
</tr>
<tr class="kole-m-table__row" data-row="sku-2" tabindex="0" role="row" aria-selected="false">
<td class="kole-m-table__cell kole-m-table__cell--start">轻薄羽绒服</td>
<td class="kole-m-table__cell">广州仓</td>
<td class="kole-m-table__cell kole-m-table__cell--number">86</td>
<td class="kole-m-table__cell kole-m-table__cell--number">¥ 459.00</td>
</tr>
<tr class="kole-m-table__row is-selected" data-row="sku-3" tabindex="0" role="row" aria-selected="true">
<td class="kole-m-table__cell kole-m-table__cell--start">帆布托特包</td>
<td class="kole-m-table__cell">杭州仓</td>
<td class="kole-m-table__cell kole-m-table__cell--number">512</td>
<td class="kole-m-table__cell kole-m-table__cell--number">¥ 129.00</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</section>
<section class="demo-block" data-demo="card">
<p class="demo-label">卡片模式(mode=card:小屏上把每行摊成键值对,彻底不需要横向滚动)</p>
<div class="demo-pad" data-assert="table-card">
<div class="kole-m-table kole-m-table--card" id="tb-card">
<div class="kole-m-table__viewport">
<table class="kole-m-table__inner">
<thead class="kole-m-table__head">
<tr class="kole-m-table__row">
<th class="kole-m-table__head-cell" scope="col">客户</th>
<th class="kole-m-table__head-cell" scope="col">等级</th>
<th class="kole-m-table__head-cell" scope="col">本月消费</th>
</tr>
</thead>
<tbody>
<tr class="kole-m-table__row">
<td class="kole-m-table__cell" data-label="客户">王小明</td>
<td class="kole-m-table__cell" data-label="等级">黄金</td>
<td class="kole-m-table__cell" data-label="本月消费">¥ 1,280.00</td>
</tr>
<tr class="kole-m-table__row">
<td class="kole-m-table__cell" data-label="客户">李小红</td>
<td class="kole-m-table__cell" data-label="等级">白银</td>
<td class="kole-m-table__cell" data-label="本月消费">¥ 368.50</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</section>
<section class="demo-block" data-demo="empty">
<p class="demo-label">空态(没有数据时显示占位文案;列宽语义不变,不会把容器撑宽)</p>
<div class="demo-pad" data-assert="table-empty">
<div class="kole-m-table" id="tb-empty" data-empty="true">
<div class="kole-m-table__viewport">
<table class="kole-m-table__inner">
<thead class="kole-m-table__head">
<tr class="kole-m-table__row">
<th class="kole-m-table__head-cell kole-m-table__head-cell--start" scope="col">订单号</th>
<th class="kole-m-table__head-cell" scope="col">状态</th>
<th class="kole-m-table__head-cell kole-m-table__head-cell--number" scope="col">金额</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<p class="kole-m-table__empty" role="status" aria-live="polite">当前筛选条件下没有订单</p>
</div>
</div>
</section>
<section class="demo-block" data-demo="states">
<p class="demo-label">禁用行与省略列(禁用行置灰不响应;长文本列单行省略,完整值在 title 里)</p>
<div class="demo-pad" data-assert="table-states">
<div class="kole-m-table kole-m-table--bordered" id="tb-states">
<div class="kole-m-table__viewport">
<table class="kole-m-table__inner">
<thead class="kole-m-table__head">
<tr class="kole-m-table__row">
<th class="kole-m-table__head-cell kole-m-table__head-cell--start" scope="col">收货地址</th>
<th class="kole-m-table__head-cell" scope="col">配送</th>
</tr>
</thead>
<tbody>
<tr class="kole-m-table__row">
<td class="kole-m-table__cell kole-m-table__cell--start kole-m-table__cell--ellipsis"
title="浙江省杭州市西湖区文三路 100 号 1 幢 1801 室">浙江省杭州市西湖区文三路 100 号 1 幢 1801 室</td>
<td class="kole-m-table__cell">顺丰</td>
</tr>
<tr class="kole-m-table__row is-disabled" aria-disabled="true">
<td class="kole-m-table__cell kole-m-table__cell--start">广东省深圳市南山区科技园南区</td>
<td class="kole-m-table__cell">暂不可达</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</section>
</div>
<script>
/* 演示页交互:点整行即选中(同表互斥),选中态写 is-selected 与 data-selected。
横向滚动由 .kole-m-table__viewport 自己承担 —— 页面整体不横向溢出。
每块各自独立:状态只落在本块 DOM 上。 */
(function () {
Array.prototype.forEach.call(document.querySelectorAll('.kole-m-table--clickable'), function (root) {
root.addEventListener('click', function (e) {
var row = e.target.closest ? e.target.closest('.kole-m-table__row') : null;
if (!row || !root.contains(row) || row.classList.contains('is-disabled')) return;
if (row.getAttribute('aria-disabled') === 'true') return;
Array.prototype.forEach.call(root.querySelectorAll('.kole-m-table__row'), function (r) {
var on = r === row;
r.classList.toggle('is-selected', on);
r.setAttribute('aria-selected', on ? 'true' : 'false');
});
root.setAttribute('data-selected', row.getAttribute('data-row') || '');
});
/* 键盘路径:聚焦后按回车 / 空格选中该行(整行点击不能是唯一的交互路径) */
root.addEventListener('keydown', function (e) {
if (e.key !== 'Enter' && e.key !== ' ' && e.key !== 'Spacebar') return;
var row = e.target.closest ? e.target.closest('.kole-m-table__row') : null;
if (!row || !root.contains(row) || row.classList.contains('is-disabled')) return;
e.preventDefault();
Array.prototype.forEach.call(root.querySelectorAll('.kole-m-table__row'), function (r) {
var on = r === row;
r.classList.toggle('is-selected', on);
r.setAttribute('aria-selected', on ? 'true' : 'false');
});
root.setAttribute('data-selected', row.getAttribute('data-row') || '');
});
});
})();
</script>
<script>
/* ?demo=<id> → 只显示该演示块(文档站按块预览用;无参数时全部显示,测试与回归走无参数路径) */
(function () {
var id = new URLSearchParams(location.search).get('demo');
if (!id) return;
var blocks = Array.prototype.slice.call(document.querySelectorAll('.demo-block'));
var hit = false;
blocks.forEach(function (b) {
var on = b.getAttribute('data-demo') === id;
if (on) hit = true;
b.hidden = !on;
});
if (!hit) { blocks.forEach(function (b) { b.hidden = false; }); return; }
document.body.classList.add('demo-single');
blocks.forEach(function (b) {
var label = b.querySelector('.demo-label');
if (label && !b.hidden) label.hidden = true;
});
})();
</script>
</body>
</html>
frameworks-mobile/Table.jsx · React · 152 行
import React from 'react';
import './Table.css';
/* 表格(移动端)— 规格 §47。
结构:table(根)> viewport(横向滚动视口)> inner(原生 table)+ empty(空态)。
**横向滚动只发生在 viewport 上**(overflow-x: auto),页面整体不横向溢出(规格 §47.1);
根与视口都写 min-width: 0,否则内部 min-width 会把父级顶宽。
表头 sticky(不是 fixed:fixed 会脱出滚动容器,横向滚动时表头不跟随)。
mode=card 把小屏行摊成键值对,彻底不需要横向滚动。
rows 为二维数组或对象数组;cells 支持 { text, align, ellipsis, number, title }。 */
const ALIGN = { start: 'start', center: 'center', end: 'end', number: 'number' };
function cellOf(row, col, ci) {
if (Array.isArray(row)) return { text: row[ci] };
if (row && typeof row === 'object') {
const raw = row[col.key === undefined ? ci : col.key];
if (raw && typeof raw === 'object') return raw;
return { text: raw };
}
return { text: '' };
}
function textOf(cell) {
if (cell === null || cell === undefined) return '';
return String(cell.text === undefined ? cell : cell.text);
}
function alignOf(col, cell) {
if (cell && cell.number) return 'number';
if (cell && cell.align && ALIGN[cell.align]) return cell.align;
if (col && col.number) return 'number';
if (col && col.align && ALIGN[col.align]) return col.align;
return 'start';
}
export default function Table({
columns = [],
rows = [],
mode = 'scroll',
size = 'default',
stripe = false,
bordered = false,
clickable = false,
caption = '',
emptyText = '暂无数据',
onSelect,
children = null,
}) {
const card = mode === 'card';
const cls =
'kole-m-table' +
` kole-m-table--${card ? 'card' : 'scroll'}` +
(size === 'compact' ? ' kole-m-table--compact' : '') +
(stripe ? ' kole-m-table--stripe' : '') +
(bordered ? ' kole-m-table--bordered' : '') +
(clickable ? ' kole-m-table--clickable' : '');
const cols = (Array.isArray(columns) ? columns : []).map((c, i) =>
c && typeof c === 'object' ? c : { title: String(c === undefined ? '' : c), key: i }
);
return (
<div className={cls} data-mode={card ? 'card' : 'scroll'}>
<div className="kole-m-table__viewport">
<table className="kole-m-table__inner">
{caption ? <caption className="kole-m-table__caption">{caption}</caption> : null}
<thead className="kole-m-table__head">
<tr className="kole-m-table__row">
{cols.map((col, ci) => (
<th
className={
'kole-m-table__head-cell' +
(col && col.number ? ' kole-m-table__head-cell--number' : '') +
(col && col.align ? ' kole-m-table__head-cell--' + col.align : '')
}
scope="col"
key={col.key === undefined ? ci : String(col.key)}
>
{col && col.title !== undefined ? col.title : ''}
</th>
))}
</tr>
</thead>
{rows.length ? (
<tbody className="kole-m-table__body">
{rows.map((row, ri) => {
const rowDisabled = !!(row && row.disabled);
const rowSelected = !!(row && row.selected);
/* 可点行必须同时可聚焦:整行点击不能是唯一的键盘路径(规格 §47.6) */
const rowProps = clickable && !rowDisabled
? {
tabIndex: 0,
role: 'row',
'aria-selected': rowSelected ? 'true' : 'false',
onKeyDown: (e) => {
if (e.key !== 'Enter' && e.key !== ' ' && e.key !== 'Spacebar') return;
e.preventDefault();
if (onSelect) onSelect(ri, row);
},
}
: { role: 'row' };
return (
<tr
className={
'kole-m-table__row' +
(rowSelected ? ' is-selected' : '') +
(rowDisabled ? ' is-disabled' : '')
}
key={row && row.key !== undefined ? String(row.key) : ri}
aria-disabled={rowDisabled ? 'true' : undefined}
{...rowProps}
onClick={() => {
if (rowDisabled) return;
if (onSelect) onSelect(ri, row);
}}
>
{cols.map((col, ci) => {
const cell = cellOf(row, col, ci);
const align = alignOf(col, cell);
return (
<td
className={
'kole-m-table__cell' +
` kole-m-table__cell--${align}` +
(cell && cell.ellipsis ? ' kole-m-table__cell--ellipsis' : '')
}
key={(col.key === undefined ? ci : String(col.key)) + '-' + ri}
data-label={col && col.title !== undefined ? String(col.title) : ''}
title={cell && cell.title ? String(cell.title) : undefined}
>
{textOf(cell)}
</td>
);
})}
</tr>
);
})}
</tbody>
) : null}
</table>
</div>
{rows.length ? null : (
<p className="kole-m-table__empty" role="status" aria-live="polite">
{emptyText}
</p>
)}
{children}
</div>
);
}
frameworks-mobile/Table.vue2.vue · Vue 2 · 144 行
<template>
<div class="kole-m-table" :class="rootClass" :data-mode="isCard ? 'card' : 'scroll'">
<div class="kole-m-table__viewport">
<table class="kole-m-table__inner">
<caption v-if="caption" class="kole-m-table__caption">{{ caption }}</caption>
<thead class="kole-m-table__head">
<tr class="kole-m-table__row">
<th
v-for="(col, ci) in cols"
:key="keyOf(col, ci)"
class="kole-m-table__head-cell"
:class="headClass(col)"
scope="col"
>{{ col && col.title !== undefined ? col.title : '' }}</th>
</tr>
</thead>
<tbody v-if="rows.length" class="kole-m-table__body">
<tr
v-for="(row, ri) in rows"
:key="row && row.key !== undefined ? String(row.key) : ri"
class="kole-m-table__row"
:class="{ 'is-selected': !!(row && row.selected), 'is-disabled': !!(row && row.disabled) }"
role="row"
:aria-selected="clickable && !(row && row.disabled) ? (row && row.selected ? 'true' : 'false') : null"
:aria-disabled="row && row.disabled ? 'true' : null"
:tabindex="clickable && !(row && row.disabled) ? 0 : null"
@click="onRowClick(ri, row)"
@keydown="onRowKey(ri, row, $event)"
>
<td
v-for="(col, ci) in cols"
:key="keyOf(col, ci) + '-' + ri"
class="kole-m-table__cell"
:class="cellClass(col, row, ci)"
:data-label="col && col.title !== undefined ? String(col.title) : ''"
:title="titleOf(row, col, ci)"
>{{ textOf(cellOf(row, col, ci)) }}</td>
</tr>
</tbody>
</table>
</div>
<p v-if="!rows.length" class="kole-m-table__empty" role="status" aria-live="polite">{{ emptyText }}</p>
<slot></slot>
</div>
</template>
<script>
/* 表格(移动端)— 规格 §47。
结构:table(根)> viewport(横向滚动视口)> inner(原生 table)+ empty(空态)。
**横向滚动只发生在 viewport 上**(overflow-x: auto),页面整体不横向溢出(规格 §47.1);
根与视口都写 min-width: 0,否则内部 min-width 会把父级顶宽。
表头 sticky(不是 fixed:fixed 会脱出滚动容器,横向滚动时表头不跟随)。
mode=card 把小屏行摊成键值对,彻底不需要横向滚动。
rows 为二维数组或对象数组;单元格支持 { text, align, ellipsis, number, title }。 */
export default {
name: 'KoleMTable',
props: {
columns: { type: Array, default: function () { return []; } },
rows: { type: Array, default: function () { return []; } },
mode: { type: String, default: 'scroll' },
size: { type: String, default: 'default' },
stripe: { type: Boolean, default: false },
bordered: { type: Boolean, default: false },
clickable: { type: Boolean, default: false },
caption: { type: String, default: '' },
emptyText: { type: String, default: '暂无数据' }
},
computed: {
isCard: function () {
return this.mode === 'card';
},
cols: function () {
return (Array.isArray(this.columns) ? this.columns : []).map(function (c, i) {
return c && typeof c === 'object' ? c : { title: String(c === undefined ? '' : c), key: i };
});
},
rootClass: function () {
return [
'kole-m-table--' + (this.isCard ? 'card' : 'scroll'),
this.size === 'compact' ? 'kole-m-table--compact' : '',
this.stripe ? 'kole-m-table--stripe' : '',
this.bordered ? 'kole-m-table--bordered' : '',
this.clickable ? 'kole-m-table--clickable' : ''
].filter(Boolean);
}
},
methods: {
keyOf: function (col, ci) {
return col && col.key !== undefined ? String(col.key) : String(ci);
},
cellOf: function (row, col, ci) {
if (Array.isArray(row)) return { text: row[ci] };
if (row && typeof row === 'object') {
var raw = row[col && col.key !== undefined ? col.key : ci];
if (raw && typeof raw === 'object') return raw;
return { text: raw };
}
return { text: '' };
},
textOf: function (cell) {
if (cell === null || cell === undefined) return '';
return String(cell.text === undefined ? cell : cell.text);
},
titleOf: function (row, col, ci) {
var cell = this.cellOf(row, col, ci);
return cell && cell.title ? String(cell.title) : null;
},
alignOf: function (col, cell) {
if (cell && cell.number) return 'number';
if (cell && cell.align) return cell.align;
if (col && col.number) return 'number';
if (col && col.align) return col.align;
return 'start';
},
headClass: function (col) {
if (col && col.number) return 'kole-m-table__head-cell--number';
if (col && col.align) return 'kole-m-table__head-cell--' + col.align;
return '';
},
cellClass: function (col, row, ci) {
var cell = this.cellOf(row, col, ci);
return [
'kole-m-table__cell--' + this.alignOf(col, cell),
cell && cell.ellipsis ? 'kole-m-table__cell--ellipsis' : ''
].filter(Boolean);
},
onRowClick: function (ri, row) {
if (row && row.disabled) return;
this.$emit('select', ri, row);
},
/* 键盘路径:聚焦后按回车 / 空格选中该行(整行点击不能是唯一的交互路径,规格 §47.6) */
onRowKey: function (ri, row, e) {
if (!this.clickable || (row && row.disabled)) return;
if (e.key !== 'Enter' && e.key !== ' ' && e.key !== 'Spacebar') return;
e.preventDefault();
this.$emit('select', ri, row);
}
}
};
</script>
<style src="./Table.css"></style>
frameworks-mobile/Table.vue3.vue · Vue 3 · 142 行
<template>
<div class="kole-m-table" :class="rootClass" :data-mode="isCard ? 'card' : 'scroll'">
<div class="kole-m-table__viewport">
<table class="kole-m-table__inner">
<caption v-if="caption" class="kole-m-table__caption">{{ caption }}</caption>
<thead class="kole-m-table__head">
<tr class="kole-m-table__row">
<th
v-for="(col, ci) in cols"
:key="keyOf(col, ci)"
class="kole-m-table__head-cell"
:class="headClass(col)"
scope="col"
>{{ col && col.title !== undefined ? col.title : '' }}</th>
</tr>
</thead>
<tbody v-if="rows.length" class="kole-m-table__body">
<tr
v-for="(row, ri) in rows"
:key="row && row.key !== undefined ? String(row.key) : ri"
class="kole-m-table__row"
:class="{ 'is-selected': !!(row && row.selected), 'is-disabled': !!(row && row.disabled) }"
role="row"
:aria-selected="clickable && !(row && row.disabled) ? (row && row.selected ? 'true' : 'false') : null"
:aria-disabled="row && row.disabled ? 'true' : null"
:tabindex="clickable && !(row && row.disabled) ? 0 : null"
@click="onRowClick(ri, row)"
@keydown="onRowKey(ri, row, $event)"
>
<td
v-for="(col, ci) in cols"
:key="keyOf(col, ci) + '-' + ri"
class="kole-m-table__cell"
:class="cellClass(col, row, ci)"
:data-label="col && col.title !== undefined ? String(col.title) : ''"
:title="cellOf(row, col, ci).title ? String(cellOf(row, col, ci).title) : null"
>{{ textOf(cellOf(row, col, ci)) }}</td>
</tr>
</tbody>
</table>
</div>
<p v-if="!rows.length" class="kole-m-table__empty" role="status" aria-live="polite">{{ emptyText }}</p>
<slot></slot>
</div>
</template>
<script setup>
/* 表格(移动端)— 规格 §47。
结构:table(根)> viewport(横向滚动视口)> inner(原生 table)+ empty(空态)。
**横向滚动只发生在 viewport 上**(overflow-x: auto),页面整体不横向溢出(规格 §47.1);
根与视口都写 min-width: 0,否则内部 min-width 会把父级顶宽。
表头 sticky(不是 fixed:fixed 会脱出滚动容器,横向滚动时表头不跟随)。
mode=card 把小屏行摊成键值对,彻底不需要横向滚动。
rows 为二维数组或对象数组;单元格支持 { text, align, ellipsis, number, title }。 */
import { computed } from 'vue';
const props = defineProps({
columns: { type: Array, default: () => [] },
rows: { type: Array, default: () => [] },
mode: { type: String, default: 'scroll' },
size: { type: String, default: 'default' },
stripe: { type: Boolean, default: false },
bordered: { type: Boolean, default: false },
clickable: { type: Boolean, default: false },
caption: { type: String, default: '' },
emptyText: { type: String, default: '暂无数据' }
});
const emit = defineEmits(['select']);
const isCard = computed(() => props.mode === 'card');
const cols = computed(() =>
(Array.isArray(props.columns) ? props.columns : []).map((c, i) =>
c && typeof c === 'object' ? c : { title: String(c === undefined ? '' : c), key: i }
)
);
const rootClass = computed(() => [
'kole-m-table--' + (isCard.value ? 'card' : 'scroll'),
props.size === 'compact' ? 'kole-m-table--compact' : '',
props.stripe ? 'kole-m-table--stripe' : '',
props.bordered ? 'kole-m-table--bordered' : '',
props.clickable ? 'kole-m-table--clickable' : ''
].filter(Boolean));
function keyOf(col, ci) {
return col && col.key !== undefined ? String(col.key) : String(ci);
}
function cellOf(row, col, ci) {
if (Array.isArray(row)) return { text: row[ci] };
if (row && typeof row === 'object') {
const raw = row[col && col.key !== undefined ? col.key : ci];
if (raw && typeof raw === 'object') return raw;
return { text: raw };
}
return { text: '' };
}
function textOf(cell) {
if (cell === null || cell === undefined) return '';
return String(cell.text === undefined ? cell : cell.text);
}
function alignOf(col, cell) {
if (cell && cell.number) return 'number';
if (cell && cell.align) return cell.align;
if (col && col.number) return 'number';
if (col && col.align) return col.align;
return 'start';
}
function headClass(col) {
if (col && col.number) return 'kole-m-table__head-cell--number';
if (col && col.align) return 'kole-m-table__head-cell--' + col.align;
return '';
}
function cellClass(col, row, ci) {
const cell = cellOf(row, col, ci);
return [
'kole-m-table__cell--' + alignOf(col, cell),
cell && cell.ellipsis ? 'kole-m-table__cell--ellipsis' : ''
].filter(Boolean);
}
function onRowClick(ri, row) {
if (row && row.disabled) return;
emit('select', ri, row);
}
/* 键盘路径:聚焦后按回车 / 空格选中该行(整行点击不能是唯一的交互路径,规格 §47.6) */
function onRowKey(ri, row, e) {
if (!props.clickable || (row && row.disabled)) return;
if (e.key !== 'Enter' && e.key !== ' ' && e.key !== 'Spacebar') return;
e.preventDefault();
emit('select', ri, row);
}
</script>
<style src="./Table.css"></style>
frameworks-mobile/Table.uniapp.vue · uni-app(跨端:小程序 / App / H5) · 278 行
<template>
<view class="kole-m-table" :class="rootClass" :data-mode="isCard ? 'card' : 'scroll'">
<!-- 横向滚动视口:滚动只发生在这里,页面整体不横向溢出(规格 §47.1) -->
<scroll-view class="kole-m-table__viewport" :scroll-x="!isCard" scroll-y="false">
<view class="kole-m-table__inner">
<view class="kole-m-table__head">
<view class="kole-m-table__row kole-m-table__row--head">
<view
v-for="(col, ci) in cols"
:key="keyOf(col, ci)"
class="kole-m-table__head-cell"
:class="headClass(col)"
:role="'columnheader'"
>
<text>{{ col && col.title !== undefined ? col.title : '' }}</text>
</view>
</view>
</view>
<view v-if="rows.length" class="kole-m-table__body">
<view
v-for="(row, ri) in rows"
:key="row && row.key !== undefined ? String(row.key) : ri"
class="kole-m-table__row"
:class="{ 'is-selected': !!(row && row.selected), 'is-disabled': !!(row && row.disabled) }"
:role="'row'"
:aria-selected="clickable && !(row && row.disabled) ? (row && row.selected ? 'true' : 'false') : 'false'"
:aria-disabled="row && row.disabled ? 'true' : 'false'"
@tap="onRowTap(ri, row)"
>
<view
v-for="(col, ci) in cols"
:key="keyOf(col, ci) + '-' + ri"
class="kole-m-table__cell"
:class="cellClass(col, row, ci)"
:role="'cell'"
:data-label="col && col.title !== undefined ? String(col.title) : ''"
>
<text>{{ textOf(cellOf(row, col, ci)) }}</text>
</view>
</view>
</view>
</view>
</scroll-view>
<text v-if="!rows.length" class="kole-m-table__empty">{{ emptyText }}</text>
<slot></slot>
</view>
</template>
<script setup>
/* uni-app 端 · 表格(移动端)— 规格 §47
跨端差异:小程序没有原生 table,用 view 网格 + role="row" / "cell" / "columnheader" 表达语义;
横向滚动用 scroll-view(scroll-x)承担 —— 滚动只发生在视口内,页面整体不横向溢出(规格 §47.1)。
mode=card 时关掉 scroll-x,把每行摊成键值对(字段名由 data-label 生成)。
表头在卡片模式外始终渲染在内容之前(scroll-view 内不依赖 sticky:小程序 sticky 支持不一)。
点击一律 @tap;尺寸用 rpx(88rpx = 375pt 下的 44px 触控最小边长)。 */
import { computed } from 'vue';
const props = defineProps({
columns: { type: Array, default: () => [] },
rows: { type: Array, default: () => [] },
mode: { type: String, default: 'scroll' },
size: { type: String, default: 'default' },
stripe: { type: Boolean, default: false },
bordered: { type: Boolean, default: false },
clickable: { type: Boolean, default: false },
caption: { type: String, default: '' },
emptyText: { type: String, default: '暂无数据' }
});
const emit = defineEmits(['select']);
const isCard = computed(() => props.mode === 'card');
const cols = computed(() =>
(Array.isArray(props.columns) ? props.columns : []).map((c, i) =>
c && typeof c === 'object' ? c : { title: String(c === undefined ? '' : c), key: i }
)
);
const rootClass = computed(() => [
'kole-m-table--' + (isCard.value ? 'card' : 'scroll'),
props.size === 'compact' ? 'kole-m-table--compact' : '',
props.stripe ? 'kole-m-table--stripe' : '',
props.bordered ? 'kole-m-table--bordered' : '',
props.clickable ? 'kole-m-table--clickable' : ''
].filter(Boolean));
function keyOf(col, ci) {
return col && col.key !== undefined ? String(col.key) : String(ci);
}
function cellOf(row, col, ci) {
if (Array.isArray(row)) return { text: row[ci] };
if (row && typeof row === 'object') {
const raw = row[col && col.key !== undefined ? col.key : ci];
if (raw && typeof raw === 'object') return raw;
return { text: raw };
}
return { text: '' };
}
function textOf(cell) {
if (cell === null || cell === undefined) return '';
return String(cell.text === undefined ? cell : cell.text);
}
function alignOf(col, cell) {
if (cell && cell.number) return 'number';
if (cell && cell.align) return cell.align;
if (col && col.number) return 'number';
if (col && col.align) return col.align;
return 'start';
}
function headClass(col) {
if (col && col.number) return 'kole-m-table__head-cell--number';
if (col && col.align) return 'kole-m-table__head-cell--' + col.align;
return '';
}
function cellClass(col, row, ci) {
const cell = cellOf(row, col, ci);
return [
'kole-m-table__cell--' + alignOf(col, cell),
cell && cell.ellipsis ? 'kole-m-table__cell--ellipsis' : ''
].filter(Boolean);
}
function onRowTap(ri, row) {
if (row && row.disabled) return;
emit('select', ri, row);
}
</script>
<style>
.kole-m-table {
--kole-m-table-row-height: 88rpx;
--kole-m-table-cell-padding: 24rpx;
--kole-m-touch-target: 88rpx;
--kole-m-font-size-body: 32rpx;
--kole-m-font-size-label: 28rpx;
--kole-m-gutter: 32rpx;
box-sizing: border-box;
display: flex;
flex-direction: column;
width: 100%;
min-width: 0;
background-color: var(--kole-color-card-bg);
color: var(--kole-color-text-body);
font-size: var(--kole-m-font-size-body);
}
/* 横向滚动视口:滚动只发生在这里 */
.kole-m-table__viewport {
width: 100%;
min-width: 0;
max-width: 100%;
white-space: nowrap;
}
.kole-m-table__inner {
display: inline-block;
min-width: 100%;
}
.kole-m-table__row {
display: flex;
align-items: stretch;
min-height: var(--kole-m-table-row-height);
}
.kole-m-table__head-cell {
display: flex;
align-items: center;
box-sizing: border-box;
flex-shrink: 0;
padding: 24rpx var(--kole-m-table-cell-padding);
background-color: var(--kole-color-table-header-bg);
color: var(--kole-color-text-secondary);
font-size: var(--kole-m-font-size-label);
border-bottom: 1rpx solid var(--kole-color-border);
}
.kole-m-table__cell {
display: flex;
align-items: center;
box-sizing: border-box;
flex-shrink: 0;
padding: 24rpx var(--kole-m-table-cell-padding);
background-color: var(--kole-color-card-bg);
color: var(--kole-color-text-body);
font-size: var(--kole-m-font-size-body);
border-bottom: 1rpx solid var(--kole-color-border);
}
.kole-m-table__row:last-child .kole-m-table__cell { border-bottom: 0; }
/* 变体 size=compact:行高不变(触控下限),只收紧字号 */
.kole-m-table--compact .kole-m-table__inner { font-size: var(--kole-m-font-size-label); }
/* 列对齐 */
.kole-m-table__cell--center,
.kole-m-table__head-cell--center { justify-content: center; }
.kole-m-table__cell--end,
.kole-m-table__head-cell--end { justify-content: flex-end; }
/* 数字列:等宽字 + 右对齐,方便按位对比 */
.kole-m-table__cell--number,
.kole-m-table__head-cell--number { justify-content: flex-end; }
/* 变体 bordered=true:单元格之间也画竖线 */
.kole-m-table--bordered .kole-m-table__head-cell + .kole-m-table__head-cell,
.kole-m-table--bordered .kole-m-table__cell + .kole-m-table__cell {
border-left: 1rpx solid var(--kole-color-border);
}
/* 变体 stripe=true:偶数行浅底 */
.kole-m-table--stripe .kole-m-table__body .kole-m-table__row:nth-child(even) .kole-m-table__cell {
background-color: var(--kole-color-table-header-bg);
}
/* 状态 selected:当前选中行 */
.kole-m-table__row.is-selected .kole-m-table__cell {
background-color: var(--kole-color-brand-bg);
color: var(--kole-color-brand);
}
/* 状态 disabled:置灰且不响应 */
.kole-m-table__row.is-disabled .kole-m-table__cell { color: var(--kole-color-text-disabled); }
/* 状态 empty:空态占位 */
.kole-m-table__empty {
display: block;
padding: 80rpx var(--kole-m-gutter);
color: var(--kole-color-text-secondary);
font-size: var(--kole-m-font-size-label);
text-align: center;
border-bottom: 1rpx solid var(--kole-color-border);
}
/* 变体 mode=card:每行摊成键值对,彻底不横向滚动 */
.kole-m-table--card .kole-m-table__viewport { white-space: normal; }
.kole-m-table--card .kole-m-table__inner {
display: block;
min-width: 0;
}
.kole-m-table--card .kole-m-table__head { display: none; }
.kole-m-table--card .kole-m-table__row {
display: block;
padding: 24rpx var(--kole-m-gutter);
border-bottom: 1rpx solid var(--kole-color-border);
}
.kole-m-table--card .kole-m-table__cell {
display: flex;
justify-content: space-between;
padding: 8rpx 0;
border-bottom: 0;
}
.kole-m-table--card .kole-m-table__cell::before {
content: attr(data-label);
color: var(--kole-color-text-secondary);
font-size: var(--kole-m-font-size-label);
}
/* 状态 clickable:整行是热区;行必须同时可聚焦(role="row" + tabindex 由宿主传入),
否则点击成了唯一的交互路径(规格 §47.6)。cursor 只给表体行,表头不变手型。 */
.kole-m-table__body .kole-m-table__row:active .kole-m-table__cell {
background-color: var(--kole-color-brand-bg);
}
</style>
测试与回归
断言在真实的 375×640 设备帧里跑(引擎与 PC 侧共用 tests/_runtime.js,触控行为动词来自移动端 tests/mobile/_behaviors.js)。
断言 18 条 · 全部通过 报告 2026-09-20 16:50:33
node site/dev-server.js &
REG_BASE=http://127.0.0.1:3311 node tools/run-mobile-regression.mjs # 全量 5 个组件
npm run verify:mobile-docs # 本页内容完整性 + API 与源码一致性
设计契约
components/mobile-table.json(点击展开原始 JSON)
{
"schemaVersion": 1,
"sourceKind": "authored-spec",
"provenance": "authored-in-repo",
"specFile": "spec/移动端规格.md",
"specSection": "47 · 表格 Table",
"confidence": "high",
"slug": "mobile-table",
"name": "表格 Table",
"semanticTypeCandidates": [
"table",
"datagrid",
"list-grid"
],
"variantDimensions": [
{
"name": "mode",
"values": [
"scroll",
"card"
]
},
{
"name": "size",
"values": [
"default",
"compact"
]
},
{
"name": "stripe",
"values": [
"false",
"true"
]
},
{
"name": "bordered",
"values": [
"false",
"true"
]
}
],
"representativeVariants": [
{
"mode": "scroll",
"size": "default",
"stripe": "false",
"bordered": "true",
"label": "横向滚动(列多时在组件容器里左右滑)"
},
{
"mode": "scroll",
"size": "default",
"stripe": "true",
"bordered": "false",
"label": "斑马纹(偶数行浅底,便于对齐行)"
},
{
"mode": "card",
"size": "default",
"stripe": "false",
"bordered": "false",
"label": "卡片模式(每行摊成键值对,小屏不横滚)"
},
{
"mode": "scroll",
"size": "compact",
"stripe": "false",
"bordered": "true",
"label": "紧凑(收紧内边距与字号,行高仍 ≥44px)"
}
],
"anatomy": {
"table": "根元素,纵向排列「视口 + 空态」",
"table__viewport": "横向滚动的唯一发生点(overflow-x: auto),页面整体不横向溢出",
"table__inner": "原生 table,min-width 由 --kole-m-table-min-width 给出;超过容器即滚",
"table__caption": "可选表格标题(说明这张表在讲什么)",
"table__head": "表头,position: sticky 固定在滚动视口顶部",
"table__head-cell": "表头单元格,写 scope=col 让读屏报出列名",
"table__row": "数据行,行高不小于 44px",
"table__cell": "单元格,可带 data-label(卡片模式的字段名来源)",
"table__empty": "空态占位,role=status + aria-live=polite"
},
"structurePatterns": {
"mode": "scroll(横向滚动,列多时用)/ card(每行摊成键值对,小屏彻底不横滚)",
"size": "default(常规行高)/ compact(收紧内边距与字号;行高仍不小于 44px)",
"stripe": "false(纯白底)/ true(偶数行浅底,便于横向扫读时对齐行)",
"bordered": "false(只有横线)/ true(单元格之间也有竖线)"
},
"usageHints": [
"把多条同构记录按列并排,用于订单列表 / 库存明细 / 对账单这类需要逐行横向对比的场合",
"移动端表格必须在组件自己的容器里横向滚动,绝不能让页面整体横向溢出,否则整页会跟着左右晃",
"横向滚动发生在 table__viewport 上:根与视口都写 min-width: 0,否则内部 min-width 会把父级顶宽",
"表头用 position: sticky 而不是 fixed —— fixed 会脱出滚动容器,横向滚动时表头不跟随",
"长文本列默认单行省略,完整值放 title;数字列用等宽字右对齐,便于按位对比",
"点整行即选中(同表互斥);行内的独立控件要阻止冒泡,避免一次点击触发两个动作"
],
"doNotInvent": [
"虚拟滚动与无限加载的触发规则(由宿主容器负责;本组件不接管纵向滚动)",
"服务端排序 / 筛选 / 分页(排序图标与请求都由宿主发起)",
"列宽拖拽调整与列显隐设置(移动端没有这个操作面)",
"冻结列(position: sticky 的横向版本;当前只固定表头的纵向位置)"
],
"unknowns": [
"表头是否需要在纵向滚动时也吸顶(当前只在宿主给固定高度时生效)",
"行选中是单选还是多选(当前单选;多选应交给 Checkbox 列)",
"卡片模式下字段名的排列是「左标签右值」还是「上标签下值」"
],
"interaction": [
"横向滚动发生在 table__viewport 上:根与视口都写 min-width: 0,否则内部 min-width 会把父级顶宽、页面整体横向溢出",
"选项行高不小于 44px;触屏上横向滑动不被父级纵向滚动吞掉(touch-action: pan-x pan-y)",
"表头用 position: sticky 而不是 fixed——fixed 会脱出滚动容器,横向滚动时表头不跟随",
"长文本列默认单行省略,完整值放 title;数字列用等宽字右对齐,便于按位对比",
"点整行即选中(同表互斥);行内的独立控件(按钮 / 链接)要阻止冒泡,避免一次点击触发两个动作",
"行可点时同时提供键盘路径(聚焦后回车 / 空格),不把鼠标点击当成唯一入口"
],
"accessibility": [
"用原生 table / thead / tbody / tr / th / td,表头写 scope=\"col\"(读屏能报出列名)",
"空态用 role=\"status\" + aria-live=\"polite\" 播报占位文案",
"禁用行写 aria-disabled=\"true\",读屏播报不可用",
"可点行必须同时可聚焦:clickable=true 时行写 tabindex=\"0\" + role=\"row\" + aria-selected,聚焦后回车 / 空格即选中 —— 只有鼠标点击而没有键盘路径是缺陷,不是简化",
"焦点环画在行内第一个单元格上(tr 上的 outline 在部分浏览器不渲染)",
"mode=card 时字段名由 data-label 生成并参与读屏(键值对在视觉与语义上都成立)"
],
"api": {
"source": "implementation",
"note": "props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs",
"requiredNote": "「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。",
"props": [
{
"name": "columns",
"type": "Array<{ title: string, key?: string, align?: 'start'|'center'|'end', number?: boolean }>",
"default": "[]",
"desc": "列定义(宿主提供;number 是数字列的便捷写法,等价于 align=end + 等宽字)(规格 §47.3)",
"required": false
},
{
"name": "rows",
"type": "Array",
"default": "[]",
"desc": "行数据,二维数组或对象数组;单元格支持 { text, align, ellipsis, number, title }(规格 §47.2)",
"required": false
},
{
"name": "mode",
"type": "'scroll' | 'card'",
"default": "'scroll'",
"desc": "变体 mode:scroll 横向滚动,card 每行摊成键值对(规格 §47.3)",
"required": false
},
{
"name": "size",
"type": "'default' | 'compact'",
"default": "'default'",
"desc": "变体 size:compact 收紧内边距与字号,行高仍不小于 44px(规格 §47.3)",
"required": false
},
{
"name": "stripe",
"type": "boolean",
"default": "false",
"desc": "变体 stripe:偶数行浅底,便于横向扫读时对齐行(规格 §47.3)",
"required": false
},
{
"name": "bordered",
"type": "boolean",
"default": "false",
"desc": "变体 bordered:单元格之间也画竖线(规格 §47.3)",
"required": false
},
{
"name": "clickable",
"type": "boolean",
"default": "false",
"desc": "状态 clickable:整行可点,按下反馈是整行背景变化(规格 §47.4)",
"required": false
},
{
"name": "caption",
"type": "string",
"default": "''",
"desc": "可选表格标题(说明这张表在讲什么)(规格 §47.2)",
"required": false
},
{
"name": "emptyText",
"type": "string",
"default": "'暂无数据'",
"desc": "状态 empty 的占位文案(规格 §47.4)",
"required": false
}
],
"events": [
{
"name": "select",
"params": "index: number, row: any",
"desc": "点整行时触发,回传行下标与该行数据(clickable=true 时才有意义)(规格 §47.5)"
}
],
"slots": [
{
"name": "default",
"desc": "自定义内容(追加在空态之后)"
}
]
},
"variantClasses": {
"mode": {
"scroll": [
".kole-m-table--scroll"
],
"card": [
".kole-m-table--card"
]
},
"size": {
"default": [],
"compact": [
".kole-m-table--compact"
]
},
"stripe": {
"false": [],
"true": [
".kole-m-table--stripe"
]
},
"bordered": {
"false": [],
"true": [
".kole-m-table--bordered"
]
}
},
"demos": [
{
"id": "scroll",
"group": "01 组件类型",
"title": "横向滚动",
"desc": "列多时在表格自己的容器里左右滑,页面整体不横向溢出。",
"variant": "mode=scroll"
},
{
"id": "clickable",
"group": "02 组件状态",
"title": "表头固定与行点击",
"desc": "点整行选中该行;数字列等宽右对齐便于按位对比,斑马纹便于横向对齐行。",
"variant": "状态 selected | clickable"
},
{
"id": "card",
"group": "01 组件类型",
"title": "卡片模式",
"desc": "mode=card:小屏上把每行摊成键值对,彻底不需要横向滚动。",
"variant": "mode=card"
},
{
"id": "empty",
"group": "02 组件状态",
"title": "空态",
"desc": "没有数据时显示占位文案,列宽语义不变,不会把容器撑宽。",
"variant": "状态 empty"
},
{
"id": "states",
"group": "02 组件状态",
"title": "禁用行与省略列",
"desc": "禁用行置灰不响应;长文本列单行省略,完整值在 title 里。",
"variant": "状态 disabled"
}
],
"related": [
{
"slug": "mobile-list",
"why": "一行只需「标题 + 值 + 箭头」时用列表,不需要列对齐;列对比才是表格的强项"
},
{
"slug": "cell",
"why": "键值对数量很少(三四项)时用单元格纵向排列,比建一张两列表格更轻"
},
{
"slug": "mobile-collapse",
"why": "需要展开某一行看明细时用折叠面板承载详情,表格本身不做行内展开"
},
{
"slug": "mobile-loading",
"why": "表格数据加载中用它占位,不要在表体里自己画骨架屏"
},
{
"slug": "mobile-tag",
"why": "状态列用小标签呈现(已发货 / 待付款)比纯文字更好扫读"
}
]
}