移动端导航列表

列表List

把一组同类信息按行排列,用于「设置项 / 订单 / 成员」这类需要扫读的场合

数据展示 规格 24 · 列表 List 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-list.css">

<!-- ③ 结构照抄下方任一演示块(类名与 6 端实现一致) -->

演示

每个演示都是真实渲染:预览帧加载 frameworks-mobile/List.html?demo=<id>(只显示该演示块),代码是该演示块在演示页里的原文,可复制。全部演示同屏可看 演示页 ↗。

01 组件类型

基础列表border=true / divider=inset

标题 + 副标题 + 后缀位(值 + 箭头),行间 1px 分隔线且最后一条不画。

查看代码(演示页原文 · 20 行)
frameworks-mobile/List.html · basic
<section class="demo-block" data-demo="basic">
  <p class="demo-label">基础列表(可点行整行热区 ≥56px,后缀位放值与箭头)</p>
  <ul class="kole-m-list kole-m-list--border" data-assert="list-basic">
    <li class="kole-m-list__item" data-assert="list-basic-row-1">
      <span class="kole-m-list__body">
        <span class="kole-m-list__title">待付款</span>
        <span class="kole-m-list__desc">共 2 笔,合计 ¥ 328.00</span>
      </span>
      <span class="kole-m-list__suffix"><span class="kole-m-list__value">去支付</span><span class="kole-m-list__arrow" aria-hidden="true">›</span></span>
    </li>
    <li class="kole-m-list__item">
      <span class="kole-m-list__body"><span class="kole-m-list__title">待收货</span></span>
      <span class="kole-m-list__suffix"><span class="kole-m-list__value">1 件</span><span class="kole-m-list__arrow" aria-hidden="true">›</span></span>
    </li>
    <li class="kole-m-list__item">
      <span class="kole-m-list__body"><span class="kole-m-list__title">退款/售后</span></span>
      <span class="kole-m-list__suffix"><span class="kole-m-list__value">无</span><span class="kole-m-list__arrow" aria-hidden="true">›</span></span>
    </li>
  </ul>
</section>
带前缀位anatomy.prefix

前缀放头像、图标或序号;它是装饰,点击等于点整行。

查看代码(演示页原文 · 21 行)
frameworks-mobile/List.html · prefix
<section class="demo-block" data-demo="prefix">
  <p class="demo-label">带前缀位(前缀是装饰,点击等于点整行)</p>
  <ul class="kole-m-list kole-m-list--border" data-assert="list-prefix">
    <li class="kole-m-list__item">
      <span class="kole-m-list__prefix"><span class="demo-dot">王</span></span>
      <span class="kole-m-list__body">
        <span class="kole-m-list__title">王小明</span>
        <span class="kole-m-list__desc">产品设计部 · 交互设计师</span>
      </span>
      <span class="kole-m-list__suffix"><span class="kole-m-list__value">在职</span></span>
    </li>
    <li class="kole-m-list__item">
      <span class="kole-m-list__prefix"><span class="demo-dot">李</span></span>
      <span class="kole-m-list__body">
        <span class="kole-m-list__title">李小红</span>
        <span class="kole-m-list__desc">前端研发部 · 工程师</span>
      </span>
      <span class="kole-m-list__suffix"><span class="kole-m-list__value">休假</span></span>
    </li>
  </ul>
</section>
自定义后缀anatomy.suffix

后缀位放标签这类非文字内容,与值文字共用一行。

查看代码(演示页原文 · 16 行)
frameworks-mobile/List.html · custom
<section class="demo-block" data-demo="custom">
  <p class="demo-label">自定义后缀(后缀位放标签等非文字内容,与值文字共用一行)</p>
  <ul class="kole-m-list kole-m-list--border" data-assert="list-custom">
    <li class="kole-m-list__item">
      <span class="kole-m-list__body">
        <span class="kole-m-list__title">订单 20260920-001</span>
        <span class="kole-m-list__desc">顺丰标快 · 预计 09-22 送达</span>
      </span>
      <span class="kole-m-list__suffix"><span class="demo-tag">已发货</span><span class="kole-m-list__arrow" aria-hidden="true">›</span></span>
    </li>
    <li class="kole-m-list__item">
      <span class="kole-m-list__body"><span class="kole-m-list__title">订单 20260919-114</span></span>
      <span class="kole-m-list__suffix"><span class="demo-tag">待付款</span></span>
    </li>
  </ul>
</section>
分隔线两态divider=inset|full

divider=inset 时线从行内容起点开始;divider=full 时通栏不缩进。

查看代码(演示页原文 · 21 行)
frameworks-mobile/List.html · divider
<section class="demo-block" data-demo="divider">
  <p class="demo-label">分隔线两态(divider=inset 缩进到内容起点 / full 通栏)</p>
  <ul class="kole-m-list kole-m-list--border kole-m-list--inset" data-assert="list-divider-inset">
    <li class="kole-m-list__item">
      <span class="kole-m-list__prefix"><span class="demo-dot">A</span></span>
      <span class="kole-m-list__body"><span class="kole-m-list__title">inset:线从内容起点开始</span></span>
    </li>
    <li class="kole-m-list__item">
      <span class="kole-m-list__prefix"><span class="demo-dot">B</span></span>
      <span class="kole-m-list__body"><span class="kole-m-list__title">缩进量 = 页面留白</span></span>
    </li>
  </ul>
  <ul class="kole-m-list kole-m-list--border kole-m-list--full" data-assert="list-divider-full" style="margin-top: 12px;">
    <li class="kole-m-list__item">
      <span class="kole-m-list__body"><span class="kole-m-list__title">full:线通栏,不缩进</span></span>
    </li>
    <li class="kole-m-list__item">
      <span class="kole-m-list__body"><span class="kole-m-list__title">用于无明显前缀位的列表</span></span>
    </li>
  </ul>
</section>

02 组件状态

紧凑尺寸size=compact

size=compact 行高 44px,仍是触控最小边长,用于高密度筛选类列表。

查看代码(演示页原文 · 17 行)
frameworks-mobile/List.html · compact
<section class="demo-block" data-demo="compact">
  <p class="demo-label">紧凑尺寸(size=compact:行高 44px,仍是触控最小边长)</p>
  <ul class="kole-m-list kole-m-list--border kole-m-list--compact" data-assert="list-compact">
    <li class="kole-m-list__item">
      <span class="kole-m-list__body"><span class="kole-m-list__title">全部订单</span></span>
      <span class="kole-m-list__suffix"><span class="kole-m-list__value">128</span></span>
    </li>
    <li class="kole-m-list__item">
      <span class="kole-m-list__body"><span class="kole-m-list__title">待评价</span></span>
      <span class="kole-m-list__suffix"><span class="kole-m-list__value">6</span></span>
    </li>
    <li class="kole-m-list__item">
      <span class="kole-m-list__body"><span class="kole-m-list__title">已取消</span></span>
      <span class="kole-m-list__suffix"><span class="kole-m-list__value">2</span></span>
    </li>
  </ul>
</section>
可点行events.select

可点行是原生 button:键盘可达、按下整行背景变化。

查看代码(演示页原文 · 21 行)
frameworks-mobile/List.html · clickable
<section class="demo-block" data-demo="clickable">
  <p class="demo-label">可点行(整行是原生 button:键盘可达、按下整行背景变化)</p>
  <ul class="kole-m-list kole-m-list--border" data-assert="list-clickable">
    <li>
      <button class="kole-m-list__item" type="button" id="list-row-link"
              data-behavior="click-sets-attr:#list-row-link|data-tapped|true">
        <span class="kole-m-list__body">
          <span class="kole-m-list__title">收货地址</span>
          <span class="kole-m-list__desc">杭州市余杭区 · 王小明</span>
        </span>
        <span class="kole-m-list__suffix"><span class="kole-m-list__value">修改</span><span class="kole-m-list__arrow" aria-hidden="true">›</span></span>
      </button>
    </li>
    <li>
      <button class="kole-m-list__item" type="button">
        <span class="kole-m-list__body"><span class="kole-m-list__title">发票信息</span></span>
        <span class="kole-m-list__suffix"><span class="kole-m-list__arrow" aria-hidden="true">›</span></span>
      </button>
    </li>
  </ul>
</section>
禁用行状态 disabled

状态 disabled:置灰且不响应点击,用于无权限项。

查看代码(演示页原文 · 20 行)
frameworks-mobile/List.html · disabled
<section class="demo-block" data-demo="disabled">
  <p class="demo-label">禁用行(置灰且不响应点击,用于无权限项)</p>
  <ul class="kole-m-list kole-m-list--border" data-assert="list-disabled">
    <li>
      <button class="kole-m-list__item is-disabled" type="button" disabled aria-disabled="true">
        <span class="kole-m-list__body">
          <span class="kole-m-list__title">企业认证</span>
          <span class="kole-m-list__desc">需要管理员权限</span>
        </span>
        <span class="kole-m-list__suffix"><span class="kole-m-list__value">暂无权限</span></span>
      </button>
    </li>
    <li>
      <button class="kole-m-list__item" type="button">
        <span class="kole-m-list__body"><span class="kole-m-list__title">实名认证</span></span>
        <span class="kole-m-list__suffix"><span class="kole-m-list__value">已完成</span></span>
      </button>
    </li>
  </ul>
</section>
空列表状态 empty

状态 empty:占位文案用 aria-live 播报,读屏能听到「暂无订单」。

查看代码(演示页原文 · 6 行)
frameworks-mobile/List.html · empty
<section class="demo-block" data-demo="empty">
  <p class="demo-label">空列表(状态 empty:占位文案用 aria-live 播报)</p>
  <ul class="kole-m-list kole-m-list--border" data-assert="list-empty">
    <li><p class="kole-m-list__empty" aria-live="polite">暂无订单,去逛逛吧</p></li>
  </ul>
</section>

API

props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs

Props

名称类型默认值说明必传
itemsArray[]数据驱动的行数组,每项含 title / desc / value / prefix / arrow / static / disabled(规格 §24.2)N
size'default' | 'compact''default'变体 size:default 行高 56px / compact 44px(规格 §24.3)N
borderbooleantrue变体 border:是否画行间 1px 分隔线(规格 §24.3)N
divider'inset' | 'full''inset'变体 divider:分隔线缩进到内容起点 / 通栏(规格 §24.3)N
headerstring''分组标题,位于列表之上(规格 §24.2 header)N
footerstring''分组脚注,用于补充说明(规格 §24.2 footer)N
emptyTextstring''状态 empty:列表为空时的占位文案(规格 §24.4)N

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

事件

名称参数说明
select(index)可点行被点击时触发,回传该行在 items 里的下标

插槽

名称说明
default追加在数据驱动行之后的自定义行(规格 §24.2 item)
empty状态 empty 的自定义占位内容,替代 emptyText(规格 §24.4)

CSS 变量

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

名称默认值说明
--kole-m-list-row-height56pxdefault 行高(compact 44px)
--kole-m-list-divider-insetvar(--kole-m-gutter)分隔线左端缩进(= 页面留白)
--kole-m-list-row-height44px组件内部默认值,可在业务侧覆盖

何时使用

  • 把一组同类信息按行排列,用于「设置项 / 订单 / 成员」这类需要扫读的场合
  • 移动端一屏只有 6~8 行,每行信息层级必须收敛到「标题 + 可选的说明或值」
  • 分组之间用标题与留白分隔,而不是靠边框
  • 可点行整行都是热区,default 行高不小于 56px、compact 不小于 44px
  • 列表滚动由宿主容器负责;本组件不接管滚动、不做虚拟列表

交互与触控

  • 可点行整行都是热区,default 行高不小于 56px、compact 不小于 44px
  • 按下反馈是整行背景变化,不是只有文字变色
  • 前缀位不参与点击判定(点图标等于点整行),后缀位里的独立控件(开关、按钮)要阻止事件冒泡,避免一次点击触发两个动作
  • 列表滚动由宿主容器负责;本组件不接管滚动、不做虚拟列表

无障碍

  • 容器 role="list",纯展示行 role="listitem";可点行用原生 button(原生语义优先于 listitem)
  • 分组标题用 aria-label 或可见文本,读屏在进入分组时能读到
  • 空列表用 aria-live="polite" 播报占位文案

从「该用哪一个」的角度区分;PC 端的对应实现见 PC 文档站。

组件何时用它而不是本组件
单元格Cell单行信息用单元格(自带 arrow / value 语义);成组出现且需要分组标题与脚注时用列表
滑动单元格SwipeCell行内还需要左滑操作时用滑动单元格;只做扫读与点击用列表
头像Avatar行的前缀位放头像时用头像组件;列表只负责行的骨架
折叠面板Collapse行点击后要就地展开内容用折叠面板;跳转到新页面用列表的可点行

规格未定 / 禁止发明

类别条目
禁止发明虚拟滚动与无限加载的触发规则
禁止发明行的拖拽排序与左滑操作(那是 SwipeCell 的职责)
规格未定单行最多几列(前缀 + 主区 + 后缀之外的排布)
规格未定分组标题是否吸顶

结构(anatomy)

字段说明
list根元素,一个列表区块
header可选分组标题,位于列表之上
item列表项,一行承载「前缀 + 主区 + 后缀」
prefix可选前缀位(头像、图标、序号)
body主区,标题 + 可选副标题,两行都单行省略
suffix可选后缀位(值文字、标签、箭头、开关)
footer可选分组脚注,用于补充说明

变体维度与类名映射

类名映射由构建脚本从契约 variantClasses 生成,并被 verify:mobile-docs 逐条对照组件 CSS 校验(类/变量必须真实存在)。

维度取值对应类名 / 变量
bordertrue / false
true .kole-m-list--border
false (由数据驱动,无专属类)
sizedefault / compact
default (由数据驱动,无专属类)
compact .kole-m-list--compact
dividerinset / full
inset .kole-m-list--inset
full .kole-m-list--full

代表变体

变体标签
border=true · size=default · divider=inset标准分组列表(默认:行高 56px、分隔线缩进)
border=true · size=compact · divider=full紧凑高密度列表(行高 44px、通栏分隔线)
border=false · size=default · divider=inset无分隔线列表(靠留白分组)

用到的令牌

构建时从本组件样式表扫描得出。蓝色为移动端自有令牌,绿色为继承的 PC 令牌(改一处两端生效)。

--kole-m-font-size-body --kole-m-font-size-label --kole-m-gutter --kole-color-border --kole-color-card-bg --kole-color-focus-ring --kole-color-page-bg --kole-color-table-header-bg --kole-color-text-body --kole-color-text-disabled --kole-color-text-placeholder --kole-color-text-secondary --kole-color-text-title --kole-font-family --kole-space-12 --kole-space-40 --kole-space-8 --kole-m-list-divider-inset --kole-m-list-row-height

6 端源码

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

frameworks-mobile/List.css · 纯样式(CSS) · 167 行
frameworks-mobile/List.css
/* Kole UI Mobile · List 样式 — 对齐移动端规格 §24
   列表:一组同类信息按行排列。default 行高 56px、compact 44px;
   行间 1px 分隔线用 ::after 画(inset 缩进到行内容起点,full 通栏),可点行整行热区。 */

.kole-m-list {
  --kole-m-list-row-height: 56px;                  /* default 行高(compact 44px) */
  --kole-m-list-divider-inset: var(--kole-m-gutter); /* 分隔线左端缩进(= 页面留白) */
  box-sizing: border-box;
  width: 100%;
  margin: 0;
  padding: 0;
  list-style: none;
  background: var(--kole-color-card-bg);
  font-family: var(--kole-font-family);
  color: var(--kole-color-text-body);
  font-size: var(--kole-m-font-size-body);
  line-height: 1.4;
}

/* 变体 size=compact:行高 44px(仍不小于触控最小边长),用于高密度列表 */
.kole-m-list--compact {
  --kole-m-list-row-height: 44px;
  font-size: var(--kole-m-font-size-label);
}

/* 变体 divider:inset 缩进到行内容起点 / full 通栏 —— 只在 border=true 时可见 */
.kole-m-list--inset { --kole-m-list-divider-inset: var(--kole-m-gutter); }
.kole-m-list--full { --kole-m-list-divider-inset: 0px; }

/* 分组标题与脚注:在卡片之外,靠留白区分分组 */
.kole-m-list__header,
.kole-m-list__footer {
  margin: 0;
  padding: var(--kole-space-12) var(--kole-m-gutter) var(--kole-space-8);
  background: var(--kole-color-page-bg);
  color: var(--kole-color-text-secondary);
  font-size: var(--kole-m-font-size-label);
}

.kole-m-list__footer {
  padding-top: var(--kole-space-8);
  padding-bottom: var(--kole-space-12);
}

/* 行:前缀位 + 主区 + 后缀位 */
.kole-m-list__item {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  gap: var(--kole-space-12);
  width: 100%;
  min-height: var(--kole-m-list-row-height);
  padding: var(--kole-space-12) var(--kole-m-gutter);
  margin: 0;
  border: 0;
  background: var(--kole-color-card-bg);
  color: var(--kole-color-text-body);
  font-family: inherit;
  font-size: inherit;
  line-height: 1.4;
  text-align: start;
}

/* 变体 border=true:行间 1px 分隔线。
   线画在**列表子项**(li)的上沿而不是行的下沿 —— 行既可能是 li 本身,
   也可能是 li 里的原生 button(可点行),挂在下沿会让两层 :last-child 都对不上。
   绝对定位保证线画在行背景之上。 */
.kole-m-list--border > * { position: relative; }

.kole-m-list--border > * + *::after {
  content: '';
  position: absolute;
  top: 0;
  left: var(--kole-m-list-divider-inset);
  right: 0;
  height: 1px;
  background: var(--kole-color-border);
}

/* 可点行:整行热区,按下反馈是整行背景变化(不是只有文字变色) */
button.kole-m-list__item {
  cursor: pointer;
  touch-action: manipulation;
}

button.kole-m-list__item:active { background: var(--kole-color-table-header-bg); }

/* 状态 disabled:置灰且不响应点击 */
.kole-m-list__item.is-disabled,
.kole-m-list__item:disabled {
  color: var(--kole-color-text-disabled);
  cursor: not-allowed;
}

.kole-m-list__item.is-disabled:active,
.kole-m-list__item:disabled:active { background: var(--kole-color-card-bg); }

/* 前缀位(头像 / 图标 / 序号):装饰性,点击等于点整行 */
.kole-m-list__prefix {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  color: var(--kole-color-text-secondary);
}

/* 主区:标题 + 可选副标题,两行都单行省略 */
.kole-m-list__body {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.kole-m-list__title {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  color: var(--kole-color-text-title);
}

.kole-m-list__desc {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: var(--kole-m-font-size-label);
  color: var(--kole-color-text-secondary);
}

/* 后缀位:值文字 / 标签 / 箭头 */
.kole-m-list__suffix {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: var(--kole-space-8);
  max-width: 45%;
  font-size: var(--kole-m-font-size-label);
  color: var(--kole-color-text-secondary);
}

.kole-m-list__value {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.kole-m-list__arrow {
  flex: 0 0 auto;
  display: inline-flex;
  color: var(--kole-color-text-placeholder);
}

/* 状态 empty:空列表占位(宿主用 aria-live 播报) */
.kole-m-list__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;
}

button.kole-m-list__item:focus-visible {
  outline: 2px solid var(--kole-color-focus-ring);
  outline-offset: -2px;
}
frameworks-mobile/List.html · H5 原生(无框架) · 208 行
frameworks-mobile/List.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 · List(H5)</title>
<link rel="stylesheet" href="../.design_library/kole-ui-mobile/colors_and_type.css">
<link rel="stylesheet" href="List.css">
<style>
  body { margin: 0; background: var(--kole-color-page-bg); font-family: var(--kole-font-family); color: var(--kole-color-text-body); }
  .demo { max-width: 375px; margin: 0 auto; padding: var(--kole-m-gutter) 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-tag { display: inline-flex; align-items: center; height: 22px; padding: 0 8px; border-radius: 11px;
    background: var(--kole-color-brand-bg); color: var(--kole-color-brand); font-size: var(--kole-m-font-size-caption); }
  /* 前缀位占位块:代表宿主放进来的头像 / 图标(纯展示,不承载交互) */
  .demo-dot { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px;
    border-radius: 50%; background: var(--kole-color-brand-bg); color: var(--kole-color-brand);
    font-size: var(--kole-m-font-size-label); }
  .demo-block[hidden] { display: none; }
</style>
</head>
<body>
<div class="demo">
  <section class="demo-block" data-demo="basic">
    <p class="demo-label">基础列表(可点行整行热区 ≥56px,后缀位放值与箭头)</p>
    <ul class="kole-m-list kole-m-list--border" data-assert="list-basic">
      <li class="kole-m-list__item" data-assert="list-basic-row-1">
        <span class="kole-m-list__body">
          <span class="kole-m-list__title">待付款</span>
          <span class="kole-m-list__desc">共 2 笔,合计 ¥ 328.00</span>
        </span>
        <span class="kole-m-list__suffix"><span class="kole-m-list__value">去支付</span><span class="kole-m-list__arrow" aria-hidden="true">›</span></span>
      </li>
      <li class="kole-m-list__item">
        <span class="kole-m-list__body"><span class="kole-m-list__title">待收货</span></span>
        <span class="kole-m-list__suffix"><span class="kole-m-list__value">1 件</span><span class="kole-m-list__arrow" aria-hidden="true">›</span></span>
      </li>
      <li class="kole-m-list__item">
        <span class="kole-m-list__body"><span class="kole-m-list__title">退款/售后</span></span>
        <span class="kole-m-list__suffix"><span class="kole-m-list__value">无</span><span class="kole-m-list__arrow" aria-hidden="true">›</span></span>
      </li>
    </ul>
  </section>

  <section class="demo-block" data-demo="prefix">
    <p class="demo-label">带前缀位(前缀是装饰,点击等于点整行)</p>
    <ul class="kole-m-list kole-m-list--border" data-assert="list-prefix">
      <li class="kole-m-list__item">
        <span class="kole-m-list__prefix"><span class="demo-dot">王</span></span>
        <span class="kole-m-list__body">
          <span class="kole-m-list__title">王小明</span>
          <span class="kole-m-list__desc">产品设计部 · 交互设计师</span>
        </span>
        <span class="kole-m-list__suffix"><span class="kole-m-list__value">在职</span></span>
      </li>
      <li class="kole-m-list__item">
        <span class="kole-m-list__prefix"><span class="demo-dot">李</span></span>
        <span class="kole-m-list__body">
          <span class="kole-m-list__title">李小红</span>
          <span class="kole-m-list__desc">前端研发部 · 工程师</span>
        </span>
        <span class="kole-m-list__suffix"><span class="kole-m-list__value">休假</span></span>
      </li>
    </ul>
  </section>

  <section class="demo-block" data-demo="custom">
    <p class="demo-label">自定义后缀(后缀位放标签等非文字内容,与值文字共用一行)</p>
    <ul class="kole-m-list kole-m-list--border" data-assert="list-custom">
      <li class="kole-m-list__item">
        <span class="kole-m-list__body">
          <span class="kole-m-list__title">订单 20260920-001</span>
          <span class="kole-m-list__desc">顺丰标快 · 预计 09-22 送达</span>
        </span>
        <span class="kole-m-list__suffix"><span class="demo-tag">已发货</span><span class="kole-m-list__arrow" aria-hidden="true">›</span></span>
      </li>
      <li class="kole-m-list__item">
        <span class="kole-m-list__body"><span class="kole-m-list__title">订单 20260919-114</span></span>
        <span class="kole-m-list__suffix"><span class="demo-tag">待付款</span></span>
      </li>
    </ul>
  </section>

  <section class="demo-block" data-demo="divider">
    <p class="demo-label">分隔线两态(divider=inset 缩进到内容起点 / full 通栏)</p>
    <ul class="kole-m-list kole-m-list--border kole-m-list--inset" data-assert="list-divider-inset">
      <li class="kole-m-list__item">
        <span class="kole-m-list__prefix"><span class="demo-dot">A</span></span>
        <span class="kole-m-list__body"><span class="kole-m-list__title">inset:线从内容起点开始</span></span>
      </li>
      <li class="kole-m-list__item">
        <span class="kole-m-list__prefix"><span class="demo-dot">B</span></span>
        <span class="kole-m-list__body"><span class="kole-m-list__title">缩进量 = 页面留白</span></span>
      </li>
    </ul>
    <ul class="kole-m-list kole-m-list--border kole-m-list--full" data-assert="list-divider-full" style="margin-top: 12px;">
      <li class="kole-m-list__item">
        <span class="kole-m-list__body"><span class="kole-m-list__title">full:线通栏,不缩进</span></span>
      </li>
      <li class="kole-m-list__item">
        <span class="kole-m-list__body"><span class="kole-m-list__title">用于无明显前缀位的列表</span></span>
      </li>
    </ul>
  </section>

  <section class="demo-block" data-demo="compact">
    <p class="demo-label">紧凑尺寸(size=compact:行高 44px,仍是触控最小边长)</p>
    <ul class="kole-m-list kole-m-list--border kole-m-list--compact" data-assert="list-compact">
      <li class="kole-m-list__item">
        <span class="kole-m-list__body"><span class="kole-m-list__title">全部订单</span></span>
        <span class="kole-m-list__suffix"><span class="kole-m-list__value">128</span></span>
      </li>
      <li class="kole-m-list__item">
        <span class="kole-m-list__body"><span class="kole-m-list__title">待评价</span></span>
        <span class="kole-m-list__suffix"><span class="kole-m-list__value">6</span></span>
      </li>
      <li class="kole-m-list__item">
        <span class="kole-m-list__body"><span class="kole-m-list__title">已取消</span></span>
        <span class="kole-m-list__suffix"><span class="kole-m-list__value">2</span></span>
      </li>
    </ul>
  </section>

  <section class="demo-block" data-demo="clickable">
    <p class="demo-label">可点行(整行是原生 button:键盘可达、按下整行背景变化)</p>
    <ul class="kole-m-list kole-m-list--border" data-assert="list-clickable">
      <li>
        <button class="kole-m-list__item" type="button" id="list-row-link"
                data-behavior="click-sets-attr:#list-row-link|data-tapped|true">
          <span class="kole-m-list__body">
            <span class="kole-m-list__title">收货地址</span>
            <span class="kole-m-list__desc">杭州市余杭区 · 王小明</span>
          </span>
          <span class="kole-m-list__suffix"><span class="kole-m-list__value">修改</span><span class="kole-m-list__arrow" aria-hidden="true">›</span></span>
        </button>
      </li>
      <li>
        <button class="kole-m-list__item" type="button">
          <span class="kole-m-list__body"><span class="kole-m-list__title">发票信息</span></span>
          <span class="kole-m-list__suffix"><span class="kole-m-list__arrow" aria-hidden="true">›</span></span>
        </button>
      </li>
    </ul>
  </section>

  <section class="demo-block" data-demo="disabled">
    <p class="demo-label">禁用行(置灰且不响应点击,用于无权限项)</p>
    <ul class="kole-m-list kole-m-list--border" data-assert="list-disabled">
      <li>
        <button class="kole-m-list__item is-disabled" type="button" disabled aria-disabled="true">
          <span class="kole-m-list__body">
            <span class="kole-m-list__title">企业认证</span>
            <span class="kole-m-list__desc">需要管理员权限</span>
          </span>
          <span class="kole-m-list__suffix"><span class="kole-m-list__value">暂无权限</span></span>
        </button>
      </li>
      <li>
        <button class="kole-m-list__item" type="button">
          <span class="kole-m-list__body"><span class="kole-m-list__title">实名认证</span></span>
          <span class="kole-m-list__suffix"><span class="kole-m-list__value">已完成</span></span>
        </button>
      </li>
    </ul>
  </section>

  <section class="demo-block" data-demo="empty">
    <p class="demo-label">空列表(状态 empty:占位文案用 aria-live 播报)</p>
    <ul class="kole-m-list kole-m-list--border" data-assert="list-empty">
      <li><p class="kole-m-list__empty" aria-live="polite">暂无订单,去逛逛吧</p></li>
    </ul>
  </section>
</div>
<script>
  /* 演示页脚本:可点行被点击后打个标记(真实业务里是路由跳转或拉取详情)。
     事件委托到列表容器,避免给每一行都绑监听。 */
  (function () {
    var row = document.getElementById('list-row-link');
    if (!row) return;
    row.addEventListener('click', function () {
      row.setAttribute('data-tapped', 'true');
    });
  })();
</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/List.jsx · React · 88 行
frameworks-mobile/List.jsx
import React from 'react';
import './List.css';

/* 列表(移动端)— 规格 §24
   一行 = 前缀位 + 主区 + 后缀位;可点行是原生 button(整行热区、键盘可达),
   纯展示行用 div + role="listitem"(原生语义优先于 listitem)。
   本端不接管滚动、不做虚拟列表(规格 §24.5);inline 控件要自己阻止冒泡,避免一次点击触发两个动作。 */
export default function List({
  items = [],
  size = 'default',
  border = true,
  divider = 'inset',
  header = '',
  footer = '',
  emptyText = '',
  onSelect,
  children = null,
  empty = null,
}) {
  const cls =
    'kole-m-list' +
    (size === 'compact' ? ' kole-m-list--compact' : '') +
    (border ? ' kole-m-list--border' : '') +
    ` kole-m-list--${divider}`;

  const suffixOf = (it) => (
    <>
      {it.value ? <span className="kole-m-list__value">{it.value}</span> : null}
      {it.arrow ? (
        <span className="kole-m-list__arrow" aria-hidden="true">
          ›
        </span>
      ) : null}
    </>
  );

  const bodyOf = (it) => (
    <>
      {it.prefix ? <span className="kole-m-list__prefix">{it.prefix}</span> : null}
      <span className="kole-m-list__body">
        <span className="kole-m-list__title">{it.title}</span>
        {it.desc ? <span className="kole-m-list__desc">{it.desc}</span> : null}
      </span>
    </>
  );

  if (!items.length) {
    return (
      <div className={cls} role="list" aria-live="polite">
        {header ? <p className="kole-m-list__header">{header}</p> : null}
        {empty || <p className="kole-m-list__empty">{emptyText || '暂无数据'}</p>}
        {footer ? <p className="kole-m-list__footer">{footer}</p> : null}
      </div>
    );
  }

  return (
    <div className={cls} role="list">
      {header ? <p className="kole-m-list__header">{header}</p> : null}
      {items.map((it, i) =>
        it && it.static ? (
          /* 纯展示行:div + role="listitem",不绑点击(规格 §24.6) */
          <div key={it.key || i} className={'kole-m-list__item' + (it.disabled ? ' is-disabled' : '')} role="listitem" aria-disabled={it.disabled ? 'true' : undefined}>
            {bodyOf(it)}
            <span className="kole-m-list__suffix">{suffixOf(it)}</span>
          </div>
        ) : (
          <button
            key={it.key || i}
            className={'kole-m-list__item' + (it.disabled ? ' is-disabled' : '')}
            type="button"
            disabled={!!it.disabled}
            onClick={(e) => {
              if (it.disabled) return;
              if (onSelect) onSelect(i, e);
            }}
          >
            {bodyOf(it)}
            <span className="kole-m-list__suffix">{suffixOf(it)}</span>
          </button>
        )
      )}
      {children}
      {footer ? <p className="kole-m-list__footer">{footer}</p> : null}
    </div>
  );
}
frameworks-mobile/List.vue2.vue · Vue 2 · 89 行
frameworks-mobile/List.vue2.vue
<template>
  <div :class="listClass" role="list" :aria-live="items.length ? null : 'polite'">
    <p v-if="header" class="kole-m-list__header">{{ header }}</p>

    <template v-if="items.length">
      <template v-for="(it, i) in items">
        <!-- 纯展示行:div + role="listitem",不绑点击(规格 §24.6) -->
        <div
          v-if="it && it.static"
          :key="'static-' + i"
          class="kole-m-list__item"
          :class="{ 'is-disabled': it.disabled }"
          role="listitem"
          :aria-disabled="it.disabled ? 'true' : null"
        >
          <span v-if="it.prefix" class="kole-m-list__prefix">{{ it.prefix }}</span>
          <span class="kole-m-list__body">
            <span class="kole-m-list__title">{{ it.title }}</span>
            <span v-if="it.desc" class="kole-m-list__desc">{{ it.desc }}</span>
          </span>
          <span class="kole-m-list__suffix">
            <span v-if="it.value" class="kole-m-list__value">{{ it.value }}</span>
            <span v-if="it.arrow" class="kole-m-list__arrow" aria-hidden="true">›</span>
          </span>
        </div>

        <button
          v-else
          :key="'item-' + i"
          class="kole-m-list__item"
          :class="{ 'is-disabled': it && it.disabled }"
          type="button"
          :disabled="!!(it && it.disabled)"
          @click="onSelect(i)"
        >
          <span v-if="it && it.prefix" class="kole-m-list__prefix">{{ it.prefix }}</span>
          <span class="kole-m-list__body">
            <span class="kole-m-list__title">{{ it && it.title }}</span>
            <span v-if="it && it.desc" class="kole-m-list__desc">{{ it.desc }}</span>
          </span>
          <span class="kole-m-list__suffix">
            <span v-if="it && it.value" class="kole-m-list__value">{{ it.value }}</span>
            <span v-if="it && it.arrow" class="kole-m-list__arrow" aria-hidden="true">›</span>
          </span>
        </button>
      </template>
      <slot></slot>
    </template>

    <!-- 状态 empty:空列表占位 -->
    <slot v-else name="empty">
      <p class="kole-m-list__empty">{{ emptyText || '暂无数据' }}</p>
    </slot>

    <p v-if="footer" class="kole-m-list__footer">{{ footer }}</p>
  </div>
</template>

<script>
export default {
  name: 'KoleMList',
  props: {
    items: { type: Array, default: function () { return []; } },
    size: { type: String, default: 'default' },
    border: { type: Boolean, default: true },
    divider: { type: String, default: 'inset' },
    header: { type: String, default: '' },
    footer: { type: String, default: '' },
    emptyText: { type: String, default: '' }
  },
  computed: {
    listClass: function () {
      return [
        this.size === 'compact' ? 'kole-m-list--compact' : '',
        this.border ? 'kole-m-list--border' : '',
        'kole-m-list--' + this.divider
      ].filter(Boolean);
    }
  },
  methods: {
    onSelect: function (i) {
      this.$emit('select', i);
    }
  }
};
</script>

<style src="./List.css"></style>
frameworks-mobile/List.vue3.vue · Vue 3 · 83 行
frameworks-mobile/List.vue3.vue
<template>
  <div :class="listClass" role="list" :aria-live="items.length ? null : 'polite'">
    <p v-if="header" class="kole-m-list__header">{{ header }}</p>

    <template v-if="items.length">
      <template v-for="(it, i) in items" :key="it.key || i">
        <!-- 纯展示行:div + role="listitem",不绑点击(规格 §24.6) -->
        <div
          v-if="it && it.static"
          class="kole-m-list__item"
          :class="{ 'is-disabled': it.disabled }"
          role="listitem"
          :aria-disabled="it.disabled ? 'true' : null"
        >
          <span v-if="it.prefix" class="kole-m-list__prefix">{{ it.prefix }}</span>
          <span class="kole-m-list__body">
            <span class="kole-m-list__title">{{ it.title }}</span>
            <span v-if="it.desc" class="kole-m-list__desc">{{ it.desc }}</span>
          </span>
          <span class="kole-m-list__suffix">
            <span v-if="it.value" class="kole-m-list__value">{{ it.value }}</span>
            <span v-if="it.arrow" class="kole-m-list__arrow" aria-hidden="true">›</span>
          </span>
        </div>

        <button
          v-else
          class="kole-m-list__item"
          :class="{ 'is-disabled': it && it.disabled }"
          type="button"
          :disabled="!!(it && it.disabled)"
          @click="onSelect(i)"
        >
          <span v-if="it && it.prefix" class="kole-m-list__prefix">{{ it.prefix }}</span>
          <span class="kole-m-list__body">
            <span class="kole-m-list__title">{{ it && it.title }}</span>
            <span v-if="it && it.desc" class="kole-m-list__desc">{{ it.desc }}</span>
          </span>
          <span class="kole-m-list__suffix">
            <span v-if="it && it.value" class="kole-m-list__value">{{ it.value }}</span>
            <span v-if="it && it.arrow" class="kole-m-list__arrow" aria-hidden="true">›</span>
          </span>
        </button>
      </template>
      <slot></slot>
    </template>

    <!-- 状态 empty:空列表占位 -->
    <slot v-else name="empty">
      <p class="kole-m-list__empty">{{ emptyText || '暂无数据' }}</p>
    </slot>

    <p v-if="footer" class="kole-m-list__footer">{{ footer }}</p>
  </div>
</template>

<script setup>
import { computed } from 'vue';

const props = defineProps({
  items: { type: Array, default: () => [] },
  size: { type: String, default: 'default' },
  border: { type: Boolean, default: true },
  divider: { type: String, default: 'inset' },
  header: { type: String, default: '' },
  footer: { type: String, default: '' },
  emptyText: { type: String, default: '' }
});
const emit = defineEmits(['select']);

const listClass = computed(() => [
  props.size === 'compact' ? 'kole-m-list--compact' : '',
  props.border ? 'kole-m-list--border' : '',
  `kole-m-list--${props.divider}`
].filter(Boolean));

function onSelect(i) {
  emit('select', i);
}
</script>

<style src="./List.css"></style>
frameworks-mobile/List.uniapp.vue · uni-app(跨端:小程序 / App / H5) · 206 行
frameworks-mobile/List.uniapp.vue
<template>
  <view class="kole-m-list" :class="listClass" role="list" :aria-live="items.length ? '' : 'polite'">
    <text v-if="header" class="kole-m-list__header">{{ header }}</text>

    <template v-if="items.length">
      <view v-for="row in rows" :key="row.index" class="kole-m-list__wrap">
        <view
          v-if="row.item.static"
          class="kole-m-list__item"
          :class="{ 'is-disabled': row.item.disabled }"
          role="listitem"
          :aria-disabled="row.item.disabled ? 'true' : 'false'"
        >
          <text v-if="row.item.prefix" class="kole-m-list__prefix">{{ row.item.prefix }}</text>
          <view class="kole-m-list__body">
            <text class="kole-m-list__title">{{ row.item.title }}</text>
            <text v-if="row.item.desc" class="kole-m-list__desc">{{ row.item.desc }}</text>
          </view>
          <view class="kole-m-list__suffix">
            <text v-if="row.item.value" class="kole-m-list__value">{{ row.item.value }}</text>
            <text v-if="row.item.arrow" class="kole-m-list__arrow">›</text>
          </view>
        </view>

        <view
          v-else
          class="kole-m-list__item"
          :class="{ 'is-disabled': row.item.disabled }"
          :role="row.item.disabled ? '' : 'button'"
          :aria-disabled="row.item.disabled ? 'true' : 'false'"
          @tap="onSelect(row)"
        >
          <text v-if="row.item.prefix" class="kole-m-list__prefix">{{ row.item.prefix }}</text>
          <view class="kole-m-list__body">
            <text class="kole-m-list__title">{{ row.item.title }}</text>
            <text v-if="row.item.desc" class="kole-m-list__desc">{{ row.item.desc }}</text>
          </view>
          <view class="kole-m-list__suffix">
            <text v-if="row.item.value" class="kole-m-list__value">{{ row.item.value }}</text>
            <text v-if="row.item.arrow" class="kole-m-list__arrow">›</text>
          </view>
        </view>
      </view>
      <slot></slot>
    </template>

    <!-- 状态 empty:空列表占位(宿主可换成自己的插槽内容) -->
    <slot v-else name="empty">
      <text class="kole-m-list__empty">{{ emptyText || '暂无数据' }}</text>
    </slot>

    <text v-if="footer" class="kole-m-list__footer">{{ footer }}</text>
  </view>
</template>

<script setup>
/* uni-app 端 · 列表(移动端)— 规格 §24
   跨端差异:小程序没有 ul/li/button 的列表语义,行一律用 view(标记 role="listitem" / "button"),
   可点行用 @tap;分隔线画在包裹层 .kole-m-list__wrap 的上沿(与 H5 端同一策略 —— 行本身
   既可能是纯展示层也可能是可点层,线挂在行下沿会在两种结构里对不上)。
   尺寸用 rpx(88rpx = 375pt 下的 44px,故 default 行高 112rpx / compact 88rpx)。 */
import { computed } from 'vue';

const props = defineProps({
  items: { type: Array, default: () => [] },
  size: { type: String, default: 'default' },
  border: { type: Boolean, default: true },
  divider: { type: String, default: 'inset' },
  header: { type: String, default: '' },
  footer: { type: String, default: '' },
  emptyText: { type: String, default: '' }
});
const emit = defineEmits(['select']);

const listClass = computed(() => [
  props.size === 'compact' ? 'kole-m-list--compact' : '',
  props.border ? 'kole-m-list--border' : '',
  `kole-m-list--${props.divider}`
].filter(Boolean));

/* 行里带上原始下标:select 回传的是被点行在 items 里的下标 */
const rows = computed(() => (props.items || []).map((item, index) => ({ item: item || {}, index: index })));

function onSelect(row) {
  if (!row || !row.item || row.item.disabled) return;
  emit('select', row.index);
}
</script>

<style>
.kole-m-list {
  --kole-m-list-row-height: 112rpx;                 /* default 行高(compact 88rpx) */
  --kole-m-list-divider-inset: 32rpx;               /* 分隔线左端缩进(= 页面留白) */
  --kole-m-font-size-body: 32rpx;
  --kole-m-font-size-label: 28rpx;
  --kole-m-gutter: 32rpx;
  box-sizing: border-box;
  width: 100%;
  background-color: var(--kole-color-card-bg);
  color: var(--kole-color-text-body);
  font-size: var(--kole-m-font-size-body);
}

.kole-m-list--compact {
  --kole-m-list-row-height: 88rpx;
  font-size: var(--kole-m-font-size-label);
}

.kole-m-list--inset { --kole-m-list-divider-inset: 32rpx; }
.kole-m-list--full { --kole-m-list-divider-inset: 0rpx; }

.kole-m-list__header,
.kole-m-list__footer {
  display: block;
  padding: 24rpx var(--kole-m-gutter) 16rpx;
  background-color: var(--kole-color-page-bg);
  color: var(--kole-color-text-secondary);
  font-size: var(--kole-m-font-size-label);
}

.kole-m-list__footer { padding-top: 16rpx; padding-bottom: 24rpx; }

.kole-m-list__wrap { position: relative; }

/* 变体 border=true:相邻行之间一条 1rpx 分隔线 */
.kole-m-list--border .kole-m-list__wrap + .kole-m-list__wrap::after {
  content: '';
  position: absolute;
  top: 0;
  left: var(--kole-m-list-divider-inset);
  right: 0;
  height: 1rpx;
  background-color: var(--kole-color-border);
}

.kole-m-list__item {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  width: 100%;
  min-height: var(--kole-m-list-row-height);
  padding: 24rpx var(--kole-m-gutter);
  background-color: var(--kole-color-card-bg);
  color: var(--kole-color-text-body);
  font-size: inherit;
}

/* 状态 active:按下反馈是整行背景变化(触屏不用 :hover) */
.kole-m-list__item:active { background-color: var(--kole-color-table-header-bg); }

.kole-m-list__item.is-disabled { color: var(--kole-color-text-disabled); }

.kole-m-list__prefix {
  flex-shrink: 0;
  margin-right: 24rpx;
  color: var(--kole-color-text-secondary);
}

.kole-m-list__body { flex: 1; display: flex; flex-direction: column; }

.kole-m-list__title {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  color: var(--kole-color-text-title);
}

.kole-m-list__desc {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: var(--kole-m-font-size-label);
  color: var(--kole-color-text-secondary);
}

.kole-m-list__suffix {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  max-width: 45%;
  font-size: var(--kole-m-font-size-label);
  color: var(--kole-color-text-secondary);
}

.kole-m-list__value {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.kole-m-list__arrow {
  flex-shrink: 0;
  padding-left: 12rpx;
  color: var(--kole-color-text-placeholder);
}

.kole-m-list__empty {
  display: block;
  padding: 80rpx var(--kole-m-gutter);
  background-color: var(--kole-color-card-bg);
  color: var(--kole-color-text-secondary);
  font-size: var(--kole-m-font-size-label);
  text-align: center;
}
</style>

测试与回归

断言在真实的 375×640 设备帧里跑(引擎与 PC 侧共用 tests/_runtime.js,触控行为动词来自移动端 tests/mobile/_behaviors.js)。

断言 21 条 · 全部通过 报告 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-list.json(点击展开原始 JSON)
{
  "schemaVersion": 1,
  "sourceKind": "authored-spec",
  "provenance": "authored-in-repo",
  "specFile": "spec/移动端规格.md",
  "specSection": "24 · 列表 List",
  "confidence": "high",
  "slug": "mobile-list",
  "name": "列表 List",
  "semanticTypeCandidates": [
    "list",
    "list-group",
    "settings-list"
  ],
  "variantDimensions": [
    {
      "name": "border",
      "values": [
        "true",
        "false"
      ]
    },
    {
      "name": "size",
      "values": [
        "default",
        "compact"
      ]
    },
    {
      "name": "divider",
      "values": [
        "inset",
        "full"
      ]
    }
  ],
  "representativeVariants": [
    {
      "border": "true",
      "size": "default",
      "divider": "inset",
      "label": "标准分组列表(默认:行高 56px、分隔线缩进)"
    },
    {
      "border": "true",
      "size": "compact",
      "divider": "full",
      "label": "紧凑高密度列表(行高 44px、通栏分隔线)"
    },
    {
      "border": "false",
      "size": "default",
      "divider": "inset",
      "label": "无分隔线列表(靠留白分组)"
    }
  ],
  "anatomy": {
    "list": "根元素,一个列表区块",
    "header": "可选分组标题,位于列表之上",
    "item": "列表项,一行承载「前缀 + 主区 + 后缀」",
    "prefix": "可选前缀位(头像、图标、序号)",
    "body": "主区,标题 + 可选副标题,两行都单行省略",
    "suffix": "可选后缀位(值文字、标签、箭头、开关)",
    "footer": "可选分组脚注,用于补充说明"
  },
  "structurePatterns": {
    "border": "true(行间 1px 分隔线)/ false(无分隔线,靠间距分组)",
    "size": "default(行高 56px)/ compact(行高 44px)",
    "divider": "inset(分隔线缩进到行内容起点)/ full(通栏分隔线)"
  },
  "usageHints": [
    "把一组同类信息按行排列,用于「设置项 / 订单 / 成员」这类需要扫读的场合",
    "移动端一屏只有 6~8 行,每行信息层级必须收敛到「标题 + 可选的说明或值」",
    "分组之间用标题与留白分隔,而不是靠边框",
    "可点行整行都是热区,default 行高不小于 56px、compact 不小于 44px",
    "列表滚动由宿主容器负责;本组件不接管滚动、不做虚拟列表"
  ],
  "doNotInvent": [
    "虚拟滚动与无限加载的触发规则",
    "行的拖拽排序与左滑操作(那是 SwipeCell 的职责)"
  ],
  "unknowns": [
    "单行最多几列(前缀 + 主区 + 后缀之外的排布)",
    "分组标题是否吸顶"
  ],
  "interaction": [
    "可点行整行都是热区,default 行高不小于 56px、compact 不小于 44px",
    "按下反馈是整行背景变化,不是只有文字变色",
    "前缀位不参与点击判定(点图标等于点整行),后缀位里的独立控件(开关、按钮)要阻止事件冒泡,避免一次点击触发两个动作",
    "列表滚动由宿主容器负责;本组件不接管滚动、不做虚拟列表"
  ],
  "accessibility": [
    "容器 role=\"list\",纯展示行 role=\"listitem\";可点行用原生 button(原生语义优先于 listitem)",
    "分组标题用 aria-label 或可见文本,读屏在进入分组时能读到",
    "空列表用 aria-live=\"polite\" 播报占位文案"
  ],
  "api": {
    "source": "implementation",
    "note": "props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs",
    "requiredNote": "「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。",
    "props": [
      {
        "name": "items",
        "type": "Array",
        "default": "[]",
        "desc": "数据驱动的行数组,每项含 title / desc / value / prefix / arrow / static / disabled(规格 §24.2)",
        "required": false
      },
      {
        "name": "size",
        "type": "'default' | 'compact'",
        "default": "'default'",
        "desc": "变体 size:default 行高 56px / compact 44px(规格 §24.3)",
        "required": false
      },
      {
        "name": "border",
        "type": "boolean",
        "default": "true",
        "desc": "变体 border:是否画行间 1px 分隔线(规格 §24.3)",
        "required": false
      },
      {
        "name": "divider",
        "type": "'inset' | 'full'",
        "default": "'inset'",
        "desc": "变体 divider:分隔线缩进到内容起点 / 通栏(规格 §24.3)",
        "required": false
      },
      {
        "name": "header",
        "type": "string",
        "default": "''",
        "desc": "分组标题,位于列表之上(规格 §24.2 header)",
        "required": false
      },
      {
        "name": "footer",
        "type": "string",
        "default": "''",
        "desc": "分组脚注,用于补充说明(规格 §24.2 footer)",
        "required": false
      },
      {
        "name": "emptyText",
        "type": "string",
        "default": "''",
        "desc": "状态 empty:列表为空时的占位文案(规格 §24.4)",
        "required": false
      }
    ],
    "events": [
      {
        "name": "select",
        "params": "(index)",
        "desc": "可点行被点击时触发,回传该行在 items 里的下标"
      }
    ],
    "slots": [
      {
        "name": "default",
        "desc": "追加在数据驱动行之后的自定义行(规格 §24.2 item)"
      },
      {
        "name": "empty",
        "desc": "状态 empty 的自定义占位内容,替代 emptyText(规格 §24.4)"
      }
    ]
  },
  "variantClasses": {
    "border": {
      "true": [
        ".kole-m-list--border"
      ],
      "false": []
    },
    "size": {
      "default": [],
      "compact": [
        ".kole-m-list--compact"
      ]
    },
    "divider": {
      "inset": [
        ".kole-m-list--inset"
      ],
      "full": [
        ".kole-m-list--full"
      ]
    }
  },
  "demos": [
    {
      "id": "basic",
      "group": "01 组件类型",
      "title": "基础列表",
      "desc": "标题 + 副标题 + 后缀位(值 + 箭头),行间 1px 分隔线且最后一条不画。",
      "variant": "border=true / divider=inset"
    },
    {
      "id": "prefix",
      "group": "01 组件类型",
      "title": "带前缀位",
      "desc": "前缀放头像、图标或序号;它是装饰,点击等于点整行。",
      "variant": "anatomy.prefix"
    },
    {
      "id": "custom",
      "group": "01 组件类型",
      "title": "自定义后缀",
      "desc": "后缀位放标签这类非文字内容,与值文字共用一行。",
      "variant": "anatomy.suffix"
    },
    {
      "id": "divider",
      "group": "01 组件类型",
      "title": "分隔线两态",
      "desc": "divider=inset 时线从行内容起点开始;divider=full 时通栏不缩进。",
      "variant": "divider=inset|full"
    },
    {
      "id": "compact",
      "group": "02 组件状态",
      "title": "紧凑尺寸",
      "desc": "size=compact 行高 44px,仍是触控最小边长,用于高密度筛选类列表。",
      "variant": "size=compact"
    },
    {
      "id": "clickable",
      "group": "02 组件状态",
      "title": "可点行",
      "desc": "可点行是原生 button:键盘可达、按下整行背景变化。",
      "variant": "events.select"
    },
    {
      "id": "disabled",
      "group": "02 组件状态",
      "title": "禁用行",
      "desc": "状态 disabled:置灰且不响应点击,用于无权限项。",
      "variant": "状态 disabled"
    },
    {
      "id": "empty",
      "group": "02 组件状态",
      "title": "空列表",
      "desc": "状态 empty:占位文案用 aria-live 播报,读屏能听到「暂无订单」。",
      "variant": "状态 empty"
    }
  ],
  "related": [
    {
      "slug": "cell",
      "why": "单行信息用单元格(自带 arrow / value 语义);成组出现且需要分组标题与脚注时用列表"
    },
    {
      "slug": "swipecell",
      "why": "行内还需要左滑操作时用滑动单元格;只做扫读与点击用列表"
    },
    {
      "slug": "mobile-avatar",
      "why": "行的前缀位放头像时用头像组件;列表只负责行的骨架"
    },
    {
      "slug": "mobile-collapse",
      "why": "行点击后要就地展开内容用折叠面板;跳转到新页面用列表的可点行"
    }
  ]
}