布局Layout
把一行内容按比例切成若干列(等分或按 12 栅格取值)
通用 规格 20 · 布局 Layout 6 端实现 触摸优先
<!-- ① 令牌:PC 令牌 + 移动端 --kole-m-* 合成单文件,引一次 -->
<link rel="stylesheet" href="kole-ui/mobile/tokens.css">
<!-- ② 本组件样式(全量则用 kole-ui/mobile/components/index.css) -->
<link rel="stylesheet" href="kole-ui/mobile/components/mobile-layout.css">
<!-- ③ 结构照抄下方任一演示块(类名与 6 端实现一致) -->
演示
每个演示都是真实渲染:预览帧加载 frameworks-mobile/Layout.html?demo=<id>(只显示该演示块),代码是该演示块在演示页里的原文,可复制。全部演示同屏可看 演示页 ↗。
01 组件类型
左右对照型信息(待付款 / 待发货)用两等分,列宽自动均分不手算。
查看代码(演示页原文 · 9 行)
<section class="demo-block" data-demo="cols-2">
<p class="demo-label">两等分(columns=2:左右两栏的对照型信息)</p>
<div class="demo-box">
<div class="kole-m-layout kole-m-layout--cols-2" data-assert="layout-cols-2">
<div class="kole-m-layout__col"><div class="kole-m-layout__panel">待付款 3</div></div>
<div class="kole-m-layout__col"><div class="kole-m-layout__panel">待发货 1</div></div>
</div>
</div>
</section>金额、数量、优惠这类短字段用三等分;列内文字过长会换行而不是撑破列宽。
查看代码(演示页原文 · 10 行)
<section class="demo-block" data-demo="cols-3">
<p class="demo-label">三等分(columns=3:金额、数量、状态这类短字段)</p>
<div class="demo-box">
<div class="kole-m-layout kole-m-layout--cols-3" data-assert="layout-cols-3">
<div class="kole-m-layout__col"><div class="kole-m-layout__panel">合计 ¥128</div></div>
<div class="kole-m-layout__col"><div class="kole-m-layout__panel">共 2 件</div></div>
<div class="kole-m-layout__col"><div class="kole-m-layout__panel">已优惠 ¥20</div></div>
</div>
</div>
</section>指标卡与图标入口最常用的档位;窄屏下保持四列,不折行。
查看代码(演示页原文 · 11 行)
<section class="demo-block" data-demo="cols-4">
<p class="demo-label">四等分(columns=4:指标卡 / 图标入口;窄屏不折行)</p>
<div class="demo-box">
<div class="kole-m-layout kole-m-layout--cols-4 kole-m-layout--gutter-8" data-assert="layout-cols-4">
<div class="kole-m-layout__col"><div class="kole-m-layout__panel">访问</div></div>
<div class="kole-m-layout__col"><div class="kole-m-layout__panel">下单</div></div>
<div class="kole-m-layout__col"><div class="kole-m-layout__panel">支付</div></div>
<div class="kole-m-layout__col"><div class="kole-m-layout__panel">完成</div></div>
</div>
</div>
</section>02 组件状态
0 无缝只用于靠分割线或底色分栏的场景;常规内容用 8 或 16。
查看代码(演示页原文 · 15 行)
<section class="demo-block" data-demo="gutter">
<p class="demo-label">列间距(gutter=0 无缝 / 8 / 16 / 24;无缝只用于靠分割线分栏的场景)</p>
<div class="demo-box">
<div class="kole-m-layout kole-m-layout--cols-3 kole-m-layout--gutter-0" data-assert="layout-gutter" style="margin-bottom: var(--kole-space-12)">
<div class="kole-m-layout__col"><div class="kole-m-layout__panel">0</div></div>
<div class="kole-m-layout__col"><div class="kole-m-layout__panel">0</div></div>
<div class="kole-m-layout__col"><div class="kole-m-layout__panel">0</div></div>
</div>
<div class="kole-m-layout kole-m-layout--cols-3 kole-m-layout--gutter-24">
<div class="kole-m-layout__col"><div class="kole-m-layout__panel">24</div></div>
<div class="kole-m-layout__col"><div class="kole-m-layout__panel">24</div></div>
<div class="kole-m-layout__col"><div class="kole-m-layout__panel">24</div></div>
</div>
</div>
</section>主次分栏用列级 span 覆盖行级等分:主栏 8/12,次栏 4/12。
查看代码(演示页原文 · 9 行)
<section class="demo-block" data-demo="span">
<p class="demo-label">不等宽(列级 col--8 / col--4:主次分栏,主栏 2/3)</p>
<div class="demo-box">
<div class="kole-m-layout kole-m-layout--gutter-16" data-assert="layout-span">
<div class="kole-m-layout__col kole-m-layout__col--8"><div class="kole-m-layout__panel">订单详情(主栏)</div></div>
<div class="kole-m-layout__col kole-m-layout__col--4"><div class="kole-m-layout__panel">操作</div></div>
</div>
</div>
</section>列高不同时用 align=center 让内容垂直居中,默认 start 顶部对齐。
查看代码(演示页原文 · 9 行)
<section class="demo-block" data-demo="align">
<p class="demo-label">交叉轴对齐(align=center:列高不同时垂直居中)</p>
<div class="demo-box">
<div class="kole-m-layout kole-m-layout--cols-2 kole-m-layout--align-center" data-assert="layout-align">
<div class="kole-m-layout__col"><div class="kole-m-layout__panel">一行</div></div>
<div class="kole-m-layout__col"><div class="kole-m-layout__panel">两行<br>内容</div></div>
</div>
</div>
</section>wrap=false 时列横向滚动,纵向页面滚动不受影响(看板式横向列)。
查看代码(演示页原文 · 10 行)
<section class="demo-block" data-demo="nowrap">
<p class="demo-label">不换行(wrap=false:横向滚动,看板式横向列)</p>
<div class="demo-box">
<div class="kole-m-layout kole-m-layout--gutter-16 is-nowrap" data-assert="layout-nowrap">
<div class="kole-m-layout__col" style="flex-basis: 140px"><div class="kole-m-layout__panel">第 1 列</div></div>
<div class="kole-m-layout__col" style="flex-basis: 140px"><div class="kole-m-layout__panel">第 2 列</div></div>
<div class="kole-m-layout__col" style="flex-basis: 140px"><div class="kole-m-layout__panel">第 3 列</div></div>
</div>
</div>
</section>API
props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs
Props
| 名称 | 类型 | 默认值 | 说明 | 必传 |
|---|---|---|---|---|
gutter | 0 | 8 | 16 | 24 | 8 | 变体 gutter:列间距四档,由 CSS 变量承载(规格 §20.3) | N |
columns | 0 | 2 | 3 | 4 | 0 | 变体 columns:行级等分列数,0 表示不启用等分(规格 §20.3) | N |
align | 'start' | 'center' | 'end' | 'stretch' | 'start' | 变体 align:交叉轴对齐(规格 §20.3) | N |
wrap | boolean | true | 变体 wrap:false 时不换行、横向滚动(规格 §20.3) | N |
「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。
事件
| 名称 | 参数 | 说明 |
|---|
插槽
| 名称 | 说明 |
|---|---|
default | 列(.kole-m-layout__col)或任意业务内容(规格 §20.2) |
CSS 变量
组件级变量(在组件样式表里定义)。业务侧可在自己的作用域内覆盖,不必改组件源码。
| 名称 | 默认值 | 说明 |
|---|---|---|
--kole-m-layout-gutter | var(--kole-space-8) | 组件内部默认值,可在业务侧覆盖 |
何时使用
- 把一行内容按比例切成若干列(等分或按 12 栅格取值)
- 布局容器不绑点击,不设 cursor: pointer,热区始终由列内的业务元素提供
- 列内若是可点区域,其热区不小于 44×44(见 §19.5 与 §6.5)
- gutter 由 CSS 变量承载,同一行内所有列的宽度计算共用该变量,避免手工算宽
- 列宽使用 flex-basis: calc(...),窄屏下不产生横向溢出(列内文本默认 min-width: 0)
交互与触控
- 布局容器不绑点击,不设 cursor: pointer,热区始终由列内的业务元素提供
- 列内若是可点区域,其热区不小于 44×44(见 §19.5 与 §6.5)
- gutter 由 CSS 变量承载,同一行内所有列的宽度计算共用该变量,避免手工算宽
- wrap=false 时容器横向滚动,纵向页面滚动不受影响
无障碍
- 布局是纯视觉分组:不添加 role、不添加 aria-*
- 不要为了布局把语义节点(列表 / 按钮)拆到不相邻的列里,否则读屏顺序会错乱
- 视觉顺序必须与 DOM 顺序一致(不用 order 重排)
相似组件
从「该用哪一个」的角度区分;PC 端的对应实现见 PC 文档站。
| 组件 | 何时用它而不是本组件 |
|---|---|
| 宫格Grid | 等分的图标 / 文字入口用宫格(自带图标与文字样式);只是切分版面才用布局 |
| 分割线Divider | 列之间要靠线分栏时用分割线;布局只管间距与宽度 |
| 单元格Cell | 一行一个语义项(可点整行)用单元格;一行要放多个并列块才用布局 |
规格未定 / 禁止发明
| 类别 | 条目 |
|---|---|
| 禁止发明 | 响应式断点(移动端只有一档列数,不为大屏定义断点) |
| 禁止发明 | 列的排序 / 拖拽 |
| 禁止发明 | 栅格嵌套的层级规则 |
| 规格未定 | 列内推荐的最大列数(本实现提供 2 / 3 / 4 三档) |
| 规格未定 | 列高等分(stretch 之外的等高策略) |
结构(anatomy)
| 字段 | 说明 |
|---|---|
layout | 行容器,display: flex + 换行,承载列间距变量 |
col | 列,默认占满一行(flex-basis: 100%),由行级等分或列级 span 类决定实际宽度 |
panel | 列内容的可选包壳,提供内边距与最小高度,业务也可直接放自己的卡片 |
gutter | 列间距,四档(0 / 8 / 16 / 24),由行级类写到 CSS 变量上 |
span | 列级权重类(12 栅格取值),可覆盖行级等分 |
变体维度与类名映射
类名映射由构建脚本从契约 variantClasses 生成,并被 verify:mobile-docs 逐条对照组件 CSS 校验(类/变量必须真实存在)。
| 维度 | 取值 | 对应类名 / 变量 |
|---|---|---|
gutter | 0 / 8 / 16 / 24 | 0 .kole-m-layout--gutter-0 8 .kole-m-layout--gutter-8 --kole-m-layout-gutter16 .kole-m-layout--gutter-16 24 .kole-m-layout--gutter-24 |
columns | 0 / 2 / 3 / 4 | 0 (由数据驱动,无专属类) 2 .kole-m-layout--cols-2 3 .kole-m-layout--cols-3 4 .kole-m-layout--cols-4 |
align | start / center / end / stretch | start (由数据驱动,无专属类) center .kole-m-layout--align-center end .kole-m-layout--align-end stretch .kole-m-layout--align-stretch |
wrap | true / false | true (由数据驱动,无专属类) false .is-nowrap |
代表变体
| 变体 | 标签 |
|---|---|
columns=2 · gutter=8 | 两等分(左右对照) |
columns=3 · gutter=16 | 三等分(短字段) |
columns=4 · gutter=8 | 四等分(指标卡 / 入口) |
columns=0 · gutter=16 | 不等宽(列级 span 主次分栏) |
用到的令牌
构建时从本组件样式表扫描得出。蓝色为移动端自有令牌,绿色为继承的 PC 令牌(改一处两端生效)。
6 端源码
同一组件的六份实现(生产环境的类名与结构一致,差异只在技术栈写法与单位)。点开查看,右侧可复制。
frameworks-mobile/Layout.css · 纯样式(CSS) · 76 行
/* Kole UI Mobile · Layout 样式 — 对齐 .design_library/kole-ui-mobile/spec/移动端规格.md §20
栅格:一行(row)里放若干列(col)。移动端绝大多数场景是「等分」,所以 columns 是行级开关
(2 / 3 / 4 等分);需要不等宽时用列上的 span 类覆盖(span 规则写成两段选择器,
保证它能压过行级等分规则 —— 两者同源,顺序在后者胜出)。
--kole-m-layout-gutter 是组件级变量,业务侧可直接覆盖(不必传 gutter)。 */
.kole-m-layout {
--kole-m-layout-gutter: var(--kole-space-8);
display: flex;
flex-wrap: wrap;
align-items: flex-start;
gap: var(--kole-m-layout-gutter);
box-sizing: border-box;
width: 100%;
}
/* 变体 gutter:列间距四档(0 = 无缝,用分割线或底色分栏时才用) */
.kole-m-layout--gutter-0 { --kole-m-layout-gutter: 0px; }
.kole-m-layout--gutter-8 { --kole-m-layout-gutter: var(--kole-space-8); }
.kole-m-layout--gutter-16 { --kole-m-layout-gutter: var(--kole-space-16); }
.kole-m-layout--gutter-24 { --kole-m-layout-gutter: var(--kole-space-24); }
/* 变体 align:交叉轴对齐(列高不同时观察得到差别) */
.kole-m-layout--align-start { align-items: flex-start; }
.kole-m-layout--align-center { align-items: center; }
.kole-m-layout--align-end { align-items: flex-end; }
.kole-m-layout--align-stretch { align-items: stretch; }
/* 变体 columns:行内等分(0 = 不禁用等分,列宽由 span 或内容决定) */
.kole-m-layout--cols-2 > .kole-m-layout__col { flex-basis: calc((100% - var(--kole-m-layout-gutter)) / 2); }
.kole-m-layout--cols-3 > .kole-m-layout__col { flex-basis: calc((100% - 2 * var(--kole-m-layout-gutter)) / 3); }
.kole-m-layout--cols-4 > .kole-m-layout__col { flex-basis: calc((100% - 3 * var(--kole-m-layout-gutter)) / 4); }
/* 变体 wrap=false:不换行,横向滚动(看板式横向列) */
.kole-m-layout.is-nowrap { flex-wrap: nowrap; overflow-x: auto; }
.kole-m-layout__col {
box-sizing: border-box;
min-width: 0;
flex: 0 0 auto;
flex-basis: 100%;
}
/* 列宽(12 栅格):两段选择器,确保能覆盖行级等分 */
.kole-m-layout .kole-m-layout__col--12 { flex-basis: 100%; }
.kole-m-layout .kole-m-layout__col--8 { flex-basis: calc((100% - 2 * var(--kole-m-layout-gutter)) / 3); }
.kole-m-layout .kole-m-layout__col--6 { flex-basis: calc((100% - var(--kole-m-layout-gutter)) / 2); }
.kole-m-layout .kole-m-layout__col--4 { flex-basis: calc((100% - 2 * var(--kole-m-layout-gutter)) / 3); }
.kole-m-layout .kole-m-layout__col--3 { flex-basis: calc((100% - 3 * var(--kole-m-layout-gutter)) / 4); }
.kole-m-layout .kole-m-layout__col--2 { flex-basis: calc((100% - 5 * var(--kole-m-layout-gutter)) / 6); }
.kole-m-layout .kole-m-layout__col--auto { flex: 1 1 auto; flex-basis: 0; }
/* 列内容基座:业务把卡片 / 列表放进列里;本类只提供内边距与最小高度示例 */
.kole-m-layout__panel {
box-sizing: border-box;
min-height: 48px;
padding: var(--kole-space-12);
border-radius: var(--kole-radius-medium);
background: var(--kole-color-page-bg);
color: var(--kole-color-text-body);
font-size: var(--kole-m-font-size-label);
line-height: 1.4;
}
/* 整行占满的横条列(如「更多」入口),保持 44px 触控高度 */
.kole-m-layout__col--bar {
display: flex;
align-items: center;
min-height: var(--kole-m-touch-target);
padding: 0 var(--kole-space-12);
border-radius: var(--kole-radius-medium);
background: var(--kole-color-table-header-bg);
color: var(--kole-color-text-secondary);
font-size: var(--kole-m-font-size-label);
}
frameworks-mobile/Layout.html · H5 原生(无框架) · 122 行
<!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 · Layout(H5)</title>
<link rel="stylesheet" href="../.design_library/kole-ui-mobile/colors_and_type.css">
<link rel="stylesheet" href="Layout.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-box { padding: var(--kole-m-gutter); background: var(--kole-color-card-bg); border-block: 1px solid var(--kole-color-border); }
.demo-block[hidden] { display: none; }
</style>
</head>
<body>
<div class="demo">
<section class="demo-block" data-demo="cols-2">
<p class="demo-label">两等分(columns=2:左右两栏的对照型信息)</p>
<div class="demo-box">
<div class="kole-m-layout kole-m-layout--cols-2" data-assert="layout-cols-2">
<div class="kole-m-layout__col"><div class="kole-m-layout__panel">待付款 3</div></div>
<div class="kole-m-layout__col"><div class="kole-m-layout__panel">待发货 1</div></div>
</div>
</div>
</section>
<section class="demo-block" data-demo="cols-3">
<p class="demo-label">三等分(columns=3:金额、数量、状态这类短字段)</p>
<div class="demo-box">
<div class="kole-m-layout kole-m-layout--cols-3" data-assert="layout-cols-3">
<div class="kole-m-layout__col"><div class="kole-m-layout__panel">合计 ¥128</div></div>
<div class="kole-m-layout__col"><div class="kole-m-layout__panel">共 2 件</div></div>
<div class="kole-m-layout__col"><div class="kole-m-layout__panel">已优惠 ¥20</div></div>
</div>
</div>
</section>
<section class="demo-block" data-demo="cols-4">
<p class="demo-label">四等分(columns=4:指标卡 / 图标入口;窄屏不折行)</p>
<div class="demo-box">
<div class="kole-m-layout kole-m-layout--cols-4 kole-m-layout--gutter-8" data-assert="layout-cols-4">
<div class="kole-m-layout__col"><div class="kole-m-layout__panel">访问</div></div>
<div class="kole-m-layout__col"><div class="kole-m-layout__panel">下单</div></div>
<div class="kole-m-layout__col"><div class="kole-m-layout__panel">支付</div></div>
<div class="kole-m-layout__col"><div class="kole-m-layout__panel">完成</div></div>
</div>
</div>
</section>
<section class="demo-block" data-demo="gutter">
<p class="demo-label">列间距(gutter=0 无缝 / 8 / 16 / 24;无缝只用于靠分割线分栏的场景)</p>
<div class="demo-box">
<div class="kole-m-layout kole-m-layout--cols-3 kole-m-layout--gutter-0" data-assert="layout-gutter" style="margin-bottom: var(--kole-space-12)">
<div class="kole-m-layout__col"><div class="kole-m-layout__panel">0</div></div>
<div class="kole-m-layout__col"><div class="kole-m-layout__panel">0</div></div>
<div class="kole-m-layout__col"><div class="kole-m-layout__panel">0</div></div>
</div>
<div class="kole-m-layout kole-m-layout--cols-3 kole-m-layout--gutter-24">
<div class="kole-m-layout__col"><div class="kole-m-layout__panel">24</div></div>
<div class="kole-m-layout__col"><div class="kole-m-layout__panel">24</div></div>
<div class="kole-m-layout__col"><div class="kole-m-layout__panel">24</div></div>
</div>
</div>
</section>
<section class="demo-block" data-demo="span">
<p class="demo-label">不等宽(列级 col--8 / col--4:主次分栏,主栏 2/3)</p>
<div class="demo-box">
<div class="kole-m-layout kole-m-layout--gutter-16" data-assert="layout-span">
<div class="kole-m-layout__col kole-m-layout__col--8"><div class="kole-m-layout__panel">订单详情(主栏)</div></div>
<div class="kole-m-layout__col kole-m-layout__col--4"><div class="kole-m-layout__panel">操作</div></div>
</div>
</div>
</section>
<section class="demo-block" data-demo="align">
<p class="demo-label">交叉轴对齐(align=center:列高不同时垂直居中)</p>
<div class="demo-box">
<div class="kole-m-layout kole-m-layout--cols-2 kole-m-layout--align-center" data-assert="layout-align">
<div class="kole-m-layout__col"><div class="kole-m-layout__panel">一行</div></div>
<div class="kole-m-layout__col"><div class="kole-m-layout__panel">两行<br>内容</div></div>
</div>
</div>
</section>
<section class="demo-block" data-demo="nowrap">
<p class="demo-label">不换行(wrap=false:横向滚动,看板式横向列)</p>
<div class="demo-box">
<div class="kole-m-layout kole-m-layout--gutter-16 is-nowrap" data-assert="layout-nowrap">
<div class="kole-m-layout__col" style="flex-basis: 140px"><div class="kole-m-layout__panel">第 1 列</div></div>
<div class="kole-m-layout__col" style="flex-basis: 140px"><div class="kole-m-layout__panel">第 2 列</div></div>
<div class="kole-m-layout__col" style="flex-basis: 140px"><div class="kole-m-layout__panel">第 3 列</div></div>
</div>
</div>
</section>
</div>
<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/Layout.jsx · React · 24 行
import React from 'react';
import './Layout.css';
/* 布局(移动端)— 规格 §20;行级 columns 负责等分,列级 span 类负责不等宽覆盖。
纯排布组件:不绑点击、不带交互角色(规格 §20.6)。 */
export default function Layout({
gutter = 8,
columns = 0,
align = 'start',
wrap = true,
children = null,
}) {
const cls =
'kole-m-layout' +
` kole-m-layout--gutter-${gutter}` +
(columns ? ` kole-m-layout--cols-${columns}` : '') +
(align === 'center' ? ' kole-m-layout--align-center' : '') +
(align === 'end' ? ' kole-m-layout--align-end' : '') +
(align === 'stretch' ? ' kole-m-layout--align-stretch' : '') +
(wrap ? '' : ' is-nowrap');
return <div className={cls}>{children}</div>;
}
frameworks-mobile/Layout.vue2.vue · Vue 2 · 36 行
<template>
<div class="kole-m-layout" :class="layoutClass">
<slot></slot>
</div>
</template>
<script>
var ALIGN_CLASS = {
center: 'kole-m-layout--align-center',
end: 'kole-m-layout--align-end',
stretch: 'kole-m-layout--align-stretch'
};
export default {
name: 'KoleMLayout',
props: {
gutter: { type: Number, default: 8 },
columns: { type: Number, default: 0 },
align: { type: String, default: 'start' },
wrap: { type: Boolean, default: true }
},
computed: {
layoutClass: function () {
return [
'kole-m-layout--gutter-' + this.gutter,
this.columns ? 'kole-m-layout--cols-' + this.columns : '',
ALIGN_CLASS[this.align],
this.wrap ? '' : 'is-nowrap'
].filter(Boolean);
}
}
};
</script>
<style src="./Layout.css"></style>
frameworks-mobile/Layout.vue3.vue · Vue 3 · 35 行
<template>
<div class="kole-m-layout" :class="layoutClass">
<slot></slot>
</div>
</template>
<script setup>
import { computed } from 'vue';
const ALIGN_CLASS = {
center: 'kole-m-layout--align-center',
end: 'kole-m-layout--align-end',
stretch: 'kole-m-layout--align-stretch'
};
const props = defineProps({
gutter: { type: Number, default: 8 },
columns: { type: Number, default: 0 },
align: { type: String, default: 'start' },
wrap: { type: Boolean, default: true }
});
/* 与 React 端同一套类名组合规则 */
const layoutClass = computed(() =>
[
`kole-m-layout--gutter-${props.gutter}`,
props.columns ? `kole-m-layout--cols-${props.columns}` : '',
ALIGN_CLASS[props.align],
props.wrap ? '' : 'is-nowrap'
].filter(Boolean)
);
</script>
<style src="./Layout.css"></style>
frameworks-mobile/Layout.uniapp.vue · uni-app(跨端:小程序 / App / H5) · 92 行
<template>
<view class="kole-m-layout" :class="layoutClass">
<slot></slot>
</view>
</template>
<script setup>
/* uni-app 端 · 布局(移动端)— 规格 §20
跨端差异:用 view(无 DOM 布局差异,flex 行为一致);间距用 rpx
(750rpx = 视口宽度,16rpx = 8px @375pt);纯排布组件,不绑任何手势。 */
import { computed } from 'vue';
const ALIGN_CLASS = {
center: 'kole-m-layout--align-center',
end: 'kole-m-layout--align-end',
stretch: 'kole-m-layout--align-stretch'
};
const props = defineProps({
gutter: { type: Number, default: 8 },
columns: { type: Number, default: 0 },
align: { type: String, default: 'start' },
wrap: { type: Boolean, default: true }
});
const layoutClass = computed(() =>
[
`kole-m-layout--gutter-${props.gutter}`,
props.columns ? `kole-m-layout--cols-${props.columns}` : '',
ALIGN_CLASS[props.align],
props.wrap ? '' : 'is-nowrap'
].filter(Boolean)
);
</script>
<style>
.kole-m-layout {
--kole-m-layout-gutter: 16rpx;
display: flex;
flex-wrap: wrap;
align-items: flex-start;
box-sizing: border-box;
width: 100%;
}
.kole-m-layout--gutter-0 { --kole-m-layout-gutter: 0rpx; }
.kole-m-layout--gutter-16 { --kole-m-layout-gutter: 32rpx; }
.kole-m-layout--gutter-24 { --kole-m-layout-gutter: 48rpx; }
.kole-m-layout--align-center { align-items: center; }
.kole-m-layout--align-end { align-items: flex-end; }
.kole-m-layout--align-stretch { align-items: stretch; }
.kole-m-layout.is-nowrap { flex-wrap: nowrap; }
.kole-m-layout__col {
box-sizing: border-box;
min-width: 0;
flex: 0 0 auto;
flex-basis: 100%;
margin-bottom: var(--kole-m-layout-gutter);
}
.kole-m-layout--cols-2 > .kole-m-layout__col { flex-basis: calc((100% - var(--kole-m-layout-gutter)) / 2); }
.kole-m-layout--cols-3 > .kole-m-layout__col { flex-basis: calc((100% - 2 * var(--kole-m-layout-gutter)) / 3); }
.kole-m-layout--cols-4 > .kole-m-layout__col { flex-basis: calc((100% - 3 * var(--kole-m-layout-gutter)) / 4); }
.kole-m-layout .kole-m-layout__col--6 { flex-basis: calc((100% - var(--kole-m-layout-gutter)) / 2); }
.kole-m-layout .kole-m-layout__col--auto { flex: 1 1 auto; flex-basis: 0; }
.kole-m-layout__panel {
box-sizing: border-box;
min-height: 96rpx;
padding: 24rpx;
border-radius: var(--kole-radius-medium);
background-color: var(--kole-color-page-bg);
color: var(--kole-color-text-body);
font-size: var(--kole-m-font-size-label);
}
.kole-m-layout__col--bar {
display: flex;
align-items: center;
min-height: 88rpx;
padding: 0 24rpx;
border-radius: var(--kole-radius-medium);
background-color: var(--kole-color-table-header-bg);
color: var(--kole-color-text-secondary);
font-size: var(--kole-m-font-size-label);
}
</style>
测试与回归
断言在真实的 375×640 设备帧里跑(引擎与 PC 侧共用 tests/_runtime.js,触控行为动词来自移动端 tests/mobile/_behaviors.js)。
断言 17 条 · 全部通过 报告 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-layout.json(点击展开原始 JSON)
{
"schemaVersion": 1,
"sourceKind": "authored-spec",
"provenance": "authored-in-repo",
"specFile": "spec/移动端规格.md",
"specSection": "20 · 布局 Layout",
"confidence": "high",
"slug": "mobile-layout",
"name": "布局 Layout",
"semanticTypeCandidates": [
"layout",
"grid"
],
"variantDimensions": [
{
"name": "gutter",
"values": [
"0",
"8",
"16",
"24"
]
},
{
"name": "columns",
"values": [
"0",
"2",
"3",
"4"
]
},
{
"name": "align",
"values": [
"start",
"center",
"end",
"stretch"
]
},
{
"name": "wrap",
"values": [
"true",
"false"
]
}
],
"representativeVariants": [
{
"columns": "2",
"gutter": "8",
"label": "两等分(左右对照)"
},
{
"columns": "3",
"gutter": "16",
"label": "三等分(短字段)"
},
{
"columns": "4",
"gutter": "8",
"label": "四等分(指标卡 / 入口)"
},
{
"columns": "0",
"gutter": "16",
"label": "不等宽(列级 span 主次分栏)"
}
],
"anatomy": {
"layout": "行容器,display: flex + 换行,承载列间距变量",
"col": "列,默认占满一行(flex-basis: 100%),由行级等分或列级 span 类决定实际宽度",
"panel": "列内容的可选包壳,提供内边距与最小高度,业务也可直接放自己的卡片",
"gutter": "列间距,四档(0 / 8 / 16 / 24),由行级类写到 CSS 变量上",
"span": "列级权重类(12 栅格取值),可覆盖行级等分"
},
"structurePatterns": {
"gutter": "0 无缝 / 8 紧凑 / 16 常规 / 24 宽松,由 --kole-m-layout-gutter 承载",
"columns": "0 不启用等分 / 2 / 3 / 4 等分(行级)",
"align": "start / center / end / stretch(交叉轴)",
"wrap": "true 换行(默认)/ false 横向滚动"
},
"usageHints": [
"把一行内容按比例切成若干列(等分或按 12 栅格取值)",
"布局容器不绑点击,不设 cursor: pointer,热区始终由列内的业务元素提供",
"列内若是可点区域,其热区不小于 44×44(见 §19.5 与 §6.5)",
"gutter 由 CSS 变量承载,同一行内所有列的宽度计算共用该变量,避免手工算宽",
"列宽使用 flex-basis: calc(...),窄屏下不产生横向溢出(列内文本默认 min-width: 0)"
],
"doNotInvent": [
"响应式断点(移动端只有一档列数,不为大屏定义断点)",
"列的排序 / 拖拽",
"栅格嵌套的层级规则"
],
"unknowns": [
"列内推荐的最大列数(本实现提供 2 / 3 / 4 三档)",
"列高等分(stretch 之外的等高策略)"
],
"interaction": [
"布局容器不绑点击,不设 cursor: pointer,热区始终由列内的业务元素提供",
"列内若是可点区域,其热区不小于 44×44(见 §19.5 与 §6.5)",
"gutter 由 CSS 变量承载,同一行内所有列的宽度计算共用该变量,避免手工算宽",
"wrap=false 时容器横向滚动,纵向页面滚动不受影响"
],
"accessibility": [
"布局是纯视觉分组:不添加 role、不添加 aria-*",
"不要为了布局把语义节点(列表 / 按钮)拆到不相邻的列里,否则读屏顺序会错乱",
"视觉顺序必须与 DOM 顺序一致(不用 order 重排)"
],
"api": {
"source": "implementation",
"note": "props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs",
"requiredNote": "「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。",
"props": [
{
"name": "gutter",
"type": "0 | 8 | 16 | 24",
"default": "8",
"desc": "变体 gutter:列间距四档,由 CSS 变量承载(规格 §20.3)",
"required": false
},
{
"name": "columns",
"type": "0 | 2 | 3 | 4",
"default": "0",
"desc": "变体 columns:行级等分列数,0 表示不启用等分(规格 §20.3)",
"required": false
},
{
"name": "align",
"type": "'start' | 'center' | 'end' | 'stretch'",
"default": "'start'",
"desc": "变体 align:交叉轴对齐(规格 §20.3)",
"required": false
},
{
"name": "wrap",
"type": "boolean",
"default": "true",
"desc": "变体 wrap:false 时不换行、横向滚动(规格 §20.3)",
"required": false
}
],
"events": [],
"slots": [
{
"name": "default",
"desc": "列(.kole-m-layout__col)或任意业务内容(规格 §20.2)"
}
]
},
"variantClasses": {
"gutter": {
"0": [
".kole-m-layout--gutter-0"
],
"8": [
".kole-m-layout--gutter-8",
"--kole-m-layout-gutter"
],
"16": [
".kole-m-layout--gutter-16"
],
"24": [
".kole-m-layout--gutter-24"
]
},
"columns": {
"0": [],
"2": [
".kole-m-layout--cols-2"
],
"3": [
".kole-m-layout--cols-3"
],
"4": [
".kole-m-layout--cols-4"
]
},
"align": {
"start": [],
"center": [
".kole-m-layout--align-center"
],
"end": [
".kole-m-layout--align-end"
],
"stretch": [
".kole-m-layout--align-stretch"
]
},
"wrap": {
"true": [],
"false": [
".is-nowrap"
]
}
},
"demos": [
{
"id": "cols-2",
"group": "01 组件类型",
"title": "两等分",
"desc": "左右对照型信息(待付款 / 待发货)用两等分,列宽自动均分不手算。",
"variant": "columns=2 / gutter=8"
},
{
"id": "cols-3",
"group": "01 组件类型",
"title": "三等分",
"desc": "金额、数量、优惠这类短字段用三等分;列内文字过长会换行而不是撑破列宽。",
"variant": "columns=3 / gutter=16"
},
{
"id": "cols-4",
"group": "01 组件类型",
"title": "四等分",
"desc": "指标卡与图标入口最常用的档位;窄屏下保持四列,不折行。",
"variant": "columns=4 / gutter=8"
},
{
"id": "gutter",
"group": "02 组件状态",
"title": "列间距",
"desc": "0 无缝只用于靠分割线或底色分栏的场景;常规内容用 8 或 16。",
"variant": "gutter=0 | 24"
},
{
"id": "span",
"group": "02 组件状态",
"title": "不等宽",
"desc": "主次分栏用列级 span 覆盖行级等分:主栏 8/12,次栏 4/12。",
"variant": "col--8 + col--4"
},
{
"id": "align",
"group": "02 组件状态",
"title": "交叉轴对齐",
"desc": "列高不同时用 align=center 让内容垂直居中,默认 start 顶部对齐。",
"variant": "align=center"
},
{
"id": "nowrap",
"group": "02 组件状态",
"title": "不换行",
"desc": "wrap=false 时列横向滚动,纵向页面滚动不受影响(看板式横向列)。",
"variant": "wrap=false"
}
],
"related": [
{
"slug": "mobile-grid",
"why": "等分的图标 / 文字入口用宫格(自带图标与文字样式);只是切分版面才用布局"
},
{
"slug": "mobile-divider",
"why": "列之间要靠线分栏时用分割线;布局只管间距与宽度"
},
{
"slug": "cell",
"why": "一行一个语义项(可点整行)用单元格;一行要放多个并列块才用布局"
}
]
}