移动端导航表格

表格Table

把多条同构记录按列并排,用于订单列表 / 库存明细 / 对账单这类需要逐行横向对比的场合

数据展示 规格 47 · 表格 Table 6 端实现 触摸优先

引入(H5 原生;其余 5 端见「快速开始」)
<!-- ① 令牌: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 组件类型

横向滚动mode=scroll

列多时在表格自己的容器里左右滑,页面整体不横向溢出。

查看代码(演示页原文 · 50 行)
frameworks-mobile/Table.html · scroll
<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

mode=card:小屏上把每行摊成键值对,彻底不需要横向滚动。

查看代码(演示页原文 · 30 行)
frameworks-mobile/Table.html · card
<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 组件状态

表头固定与行点击状态 selected | clickable

点整行选中该行;数字列等宽右对齐便于按位对比,斑马纹便于横向对齐行。

查看代码(演示页原文 · 41 行)
frameworks-mobile/Table.html · clickable
<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>
空态状态 empty

没有数据时显示占位文案,列宽语义不变,不会把容器撑宽。

查看代码(演示页原文 · 20 行)
frameworks-mobile/Table.html · empty
<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>
禁用行与省略列状态 disabled

禁用行置灰不响应;长文本列单行省略,完整值在 title 里。

查看代码(演示页原文 · 28 行)
frameworks-mobile/Table.html · states
<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

名称类型默认值说明必传
columnsArray<{ title: string, key?: string, align?: 'start'|'center'|'end', number?: boolean }>[]列定义(宿主提供;number 是数字列的便捷写法,等价于 align=end + 等宽字)(规格 §47.3)N
rowsArray<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
stripebooleanfalse变体 stripe:偶数行浅底,便于横向扫读时对齐行(规格 §47.3)N
borderedbooleanfalse变体 bordered:单元格之间也画竖线(规格 §47.3)N
clickablebooleanfalse状态 clickable:整行可点,按下反馈是整行背景变化(规格 §47.4)N
captionstring''可选表格标题(说明这张表在讲什么)(规格 §47.2)N
emptyTextstring'暂无数据'状态 empty 的占位文案(规格 §47.4)N

「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。

事件

名称参数说明
selectindex: number, row: any点整行时触发,回传行下标与该行数据(clickable=true 时才有意义)(规格 §47.5)

插槽

名称说明
default自定义内容(追加在空态之后)

CSS 变量

组件级变量(在组件样式表里定义)。业务侧可在自己的作用域内覆盖,不必改组件源码。

名称默认值说明
--kole-m-table-row-heightvar(--kole-m-touch-target)行最小高度(≥44px 触控最小边长)
--kole-m-table-cell-paddingvar(--kole-space-12)单元格左右内边距
--kole-m-table-min-width0内容最小宽度;超过容器即横向滚动

何时使用

  • 把多条同构记录按列并排,用于订单列表 / 库存明细 / 对账单这类需要逐行横向对比的场合
  • 移动端表格必须在组件自己的容器里横向滚动,绝不能让页面整体横向溢出,否则整页会跟着左右晃
  • 横向滚动发生在 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 校验(类/变量必须真实存在)。

维度取值对应类名 / 变量
modescroll / card
scroll .kole-m-table--scroll
card .kole-m-table--card
sizedefault / compact
default (由数据驱动,无专属类)
compact .kole-m-table--compact
stripefalse / true
false (由数据驱动,无专属类)
true .kole-m-table--stripe
borderedfalse / 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 令牌(改一处两端生效)。

--kole-m-font-size-body --kole-m-font-size-label --kole-m-gutter --kole-m-touch-target --kole-color-border --kole-color-brand --kole-color-brand-bg --kole-color-card-bg --kole-color-focus-ring --kole-color-table-header-bg --kole-color-text-body --kole-color-text-disabled --kole-color-text-secondary --kole-font-family --kole-font-family-num --kole-radius-base --kole-space-12 --kole-space-4 --kole-space-40 --kole-m-table-cell-padding --kole-m-table-min-width --kole-m-table-row-height

6 端源码

同一组件的六份实现(生产环境的类名与结构一致,差异只在技术栈写法与单位)。点开查看,右侧可复制。

frameworks-mobile/Table.css · 纯样式(CSS) · 229 行
frameworks-mobile/Table.css
/* 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 行
frameworks-mobile/Table.html
<!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 行
frameworks-mobile/Table.jsx
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 行
frameworks-mobile/Table.vue2.vue
<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 行
frameworks-mobile/Table.vue3.vue
<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 行
frameworks-mobile/Table.uniapp.vue
<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-22 23:01:05

复现命令
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": "状态列用小标签呈现(已发货 / 待付款)比纯文字更好扫读"
    }
  ]
}