宫格Grid
把图标 / 文字入口按等分列排成网格,用于首页功能入口区
导航 规格 14 · 宫格 Grid 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-grid.css">
<!-- ③ 结构照抄下方任一演示块(类名与 6 端实现一致) -->
演示
每个演示都是真实渲染:预览帧加载 frameworks-mobile/Grid.html?demo=<id>(只显示该演示块),代码是该演示块在演示页里的原文,可复制。全部演示同屏可看 演示页 ↗。
01 组件类型
最常用的首页入口排布;每格是原生按钮,整块可点、键盘可达。
查看代码(演示页原文 · 21 行)
<section class="demo-block" data-demo="columns-4">
<p class="demo-label">四列宫格(columns=4,最常用的首页入口排布;整块可点)</p>
<nav class="kole-m-grid kole-m-grid--4" data-assert="grid-columns-4" aria-label="常用功能">
<button class="kole-m-grid__item" type="button" data-behavior="click-sets-attr:#grid-entry-1|data-clicked|true" id="grid-entry-1">
<span class="kole-m-grid__icon" aria-hidden="true">▤</span>
<span class="kole-m-grid__text">订单</span>
</button>
<button class="kole-m-grid__item" type="button" data-function="wallet">
<span class="kole-m-grid__icon" aria-hidden="true">◈</span>
<span class="kole-m-grid__text">钱包</span>
</button>
<button class="kole-m-grid__item" type="button">
<span class="kole-m-grid__icon" aria-hidden="true">◉</span>
<span class="kole-m-grid__text">卡券</span>
</button>
<button class="kole-m-grid__item" type="button">
<span class="kole-m-grid__icon" aria-hidden="true">⚙</span>
<span class="kole-m-grid__text">设置</span>
</button>
</nav>
</section>每格更宽,适合带较长标题的入口。
查看代码(演示页原文 · 17 行)
<section class="demo-block" data-demo="columns-3">
<p class="demo-label">三列宫格(columns=3:每格更宽,适合带副标题的入口)</p>
<nav class="kole-m-grid kole-m-grid--3" data-assert="grid-columns-3" aria-label="数据看板">
<button class="kole-m-grid__item" type="button">
<span class="kole-m-grid__icon" aria-hidden="true">▦</span>
<span class="kole-m-grid__text">销售看板</span>
</button>
<button class="kole-m-grid__item" type="button">
<span class="kole-m-grid__icon" aria-hidden="true">◫</span>
<span class="kole-m-grid__text">库存预警</span>
</button>
<button class="kole-m-grid__item" type="button">
<span class="kole-m-grid__icon" aria-hidden="true">◐</span>
<span class="kole-m-grid__text">客户分析</span>
</button>
</nav>
</section>入口更少、信息更重时用;square=true 把格子撑成正方形,视觉更稳。
查看代码(演示页原文 · 13 行)
<section class="demo-block" data-demo="columns-2">
<p class="demo-label">两列宫格(columns=2:入口更少、信息更重时用)</p>
<nav class="kole-m-grid kole-m-grid--2 kole-m-grid--square" data-assert="grid-columns-2" aria-label="快捷操作">
<button class="kole-m-grid__item" type="button">
<span class="kole-m-grid__icon" aria-hidden="true">↑</span>
<span class="kole-m-grid__text">扫码入库</span>
</button>
<button class="kole-m-grid__item" type="button">
<span class="kole-m-grid__icon" aria-hidden="true">↓</span>
<span class="kole-m-grid__text">扫码出库</span>
</button>
</nav>
</section>02 组件状态
格线用 outline 绘制,相邻边不会叠成 2px;适合需要明确分区边界的场景。
查看代码(演示页原文 · 21 行)
<section class="demo-block" data-demo="border">
<p class="demo-label">带格线(border=true:格线用 outline 画,相邻边不会叠粗)</p>
<nav class="kole-m-grid kole-m-grid--4 kole-m-grid--border" data-assert="grid-border" aria-label="带格线示例">
<button class="kole-m-grid__item" type="button">
<span class="kole-m-grid__icon" aria-hidden="true">◧</span>
<span class="kole-m-grid__text">待发货</span>
</button>
<button class="kole-m-grid__item" type="button">
<span class="kole-m-grid__icon" aria-hidden="true">◨</span>
<span class="kole-m-grid__text">运输中</span>
</button>
<button class="kole-m-grid__item" type="button">
<span class="kole-m-grid__icon" aria-hidden="true">◩</span>
<span class="kole-m-grid__text">已完成</span>
</button>
<button class="kole-m-grid__item" type="button">
<span class="kole-m-grid__icon" aria-hidden="true">◪</span>
<span class="kole-m-grid__text">已关闭</span>
</button>
</nav>
</section>禁用项置灰且不响应点击,多用于无权限入口;纯展示格子不绑任何交互。
查看代码(演示页原文 · 21 行)
<section class="demo-block" data-demo="disabled">
<p class="demo-label">含禁用项(disabled:置灰且不响应,多用于无权限入口)</p>
<nav class="kole-m-grid kole-m-grid--4" data-assert="grid-disabled" aria-label="含禁用入口">
<button class="kole-m-grid__item" type="button">
<span class="kole-m-grid__icon" aria-hidden="true">▤</span>
<span class="kole-m-grid__text">订单</span>
</button>
<button class="kole-m-grid__item" type="button" disabled aria-label="财务报表(无权限)">
<span class="kole-m-grid__icon" aria-hidden="true">▥</span>
<span class="kole-m-grid__text">财务报表</span>
</button>
<span class="kole-m-grid__item kole-m-grid__item--static" aria-hidden="true">
<span class="kole-m-grid__icon">▧</span>
<span class="kole-m-grid__text">占位</span>
</span>
<button class="kole-m-grid__item" type="button">
<span class="kole-m-grid__icon" aria-hidden="true">⚙</span>
<span class="kole-m-grid__text">设置</span>
</button>
</nav>
</section>API
props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs
Props
| 名称 | 类型 | 默认值 | 说明 | 必传 |
|---|---|---|---|---|
columns | number | 4 | 变体 columns:每行格数(规格 §14.3) | N |
border | boolean | false | 变体 border:是否画格线(规格 §14.3) | N |
square | boolean | false | 变体 square:格子是否撑成正方形(规格 §14.3) | N |
items | Array<{ text, icon, disabled, static }> | [] | 格子数据;static=true 的格子是纯展示(不绑点击)(规格 §14.2) | N |
「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。
事件
| 名称 | 参数 | 说明 |
|---|---|---|
select | index: number | 点击可点格子时触发,回传格子下标;跳转由宿主决定(规格 §14.5) |
插槽
| 名称 | 说明 |
|---|---|
default | 额外的自定义格子(追加在 items 之后) |
CSS 变量
组件级变量(在组件样式表里定义)。业务侧可在自己的作用域内覆盖,不必改组件源码。
| 名称 | 默认值 | 说明 |
|---|---|---|
--kole-m-grid-columns | 4 | 组件内部默认值,可在业务侧覆盖 |
--kole-m-grid-icon-size | 32px | 组件内部默认值,可在业务侧覆盖 |
何时使用
- 把图标 / 文字入口按等分列排成网格,用于首页功能入口区
- 每个格子整块可点,热区不小于 44×44
- 按下反馈用 :active(不用 :hover —— 触屏没有悬停)
- 可点格子用原生 button(整块热区 + 键盘可达)
- 纯展示格子用 div 且不加交互角色
- 图标装饰(aria-hidden="true"),文字即语义
交互与触控
- 每个格子整块可点,热区不小于 44×44
- 按下反馈用 :active(不用 :hover —— 触屏没有悬停)
无障碍
- 可点格子用原生 button(整块热区 + 键盘可达)
- 纯展示格子用 div 且不加交互角色
- 图标装饰(aria-hidden="true"),文字即语义
相似组件
从「该用哪一个」的角度区分;PC 端的对应实现见 PC 文档站。
| 组件 | 何时用它而不是本组件 |
|---|---|
| 单元格Cell | 一列一条的入口用单元格,2 列以上的入口才用宫格 |
| 标签Tag | 格子上的状态标记用标签,不要把状态写进格子的标题文字里 |
| 通知栏NoticeBar | 宫格上方需要一条公告时用通知栏,不要在宫格里插说明文字 |
规格未定 / 禁止发明
| 类别 | 条目 |
|---|---|
| 禁止发明 | 格子的拖拽排序 |
| 禁止发明 | 超出 4 列的响应式折行列数 |
| 规格未定 | 图标区的推荐尺寸 |
| 规格未定 | 一格最多几个字 |
结构(anatomy)
| 字段 | 说明 |
|---|---|
grid | 根容器 |
grid__item | 单个格子(图标 + 文字) |
grid__icon | 图标区 |
grid__text | 文字标签 |
变体维度与类名映射
类名映射由构建脚本从契约 variantClasses 生成,并被 verify:mobile-docs 逐条对照组件 CSS 校验(类/变量必须真实存在)。
| 维度 | 取值 | 对应类名 / 变量 |
|---|---|---|
columns | 2 / 3 / 4 | 2 .kole-m-grid--2 3 .kole-m-grid--3 4 .kole-m-grid--4 |
border | false / true | false (由数据驱动,无专属类) true .kole-m-grid--border |
square | false / true | false (由数据驱动,无专属类) true .kole-m-grid--square |
代表变体
| 变体 | 标签 |
|---|---|
columns=4 · border=false · square=false | 四列宫格(最常用) |
columns=3 · border=false · square=false | 三列宫格 |
columns=2 · border=false · square=true | 两列正方形 |
columns=4 · border=true · square=false | 带格线 |
用到的令牌
构建时从本组件样式表扫描得出。蓝色为移动端自有令牌,绿色为继承的 PC 令牌(改一处两端生效)。
6 端源码
同一组件的六份实现(生产环境的类名与结构一致,差异只在技术栈写法与单位)。点开查看,右侧可复制。
frameworks-mobile/Grid.css · 纯样式(CSS) · 84 行
/* Kole UI Mobile · Grid 样式 — 对齐移动端规格 §14
宫格:等分列排布;可点格子用原生 button(整块热区 + 键盘可达);
按下反馈用 :active(触屏无悬停);格线用 border + 负外边距抵消。 */
.kole-m-grid {
--kole-m-grid-columns: 4;
--kole-m-grid-icon-size: 32px;
display: grid;
grid-template-columns: repeat(var(--kole-m-grid-columns), 1fr);
background: var(--kole-color-card-bg);
}
/* 变体 columns:每行格数 */
.kole-m-grid--2 { --kole-m-grid-columns: 2; }
.kole-m-grid--3 { --kole-m-grid-columns: 3; }
.kole-m-grid--4 { --kole-m-grid-columns: 4; }
.kole-m-grid__item {
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--kole-space-8);
min-height: var(--kole-m-touch-target);
padding: var(--kole-space-16) var(--kole-space-8);
border: 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-label);
line-height: 1.3;
text-align: center;
cursor: pointer;
touch-action: manipulation;
}
.kole-m-grid__item:focus-visible {
outline: 2px solid var(--kole-color-focus-ring);
outline-offset: -2px;
}
/* 状态 active:按下反馈(触屏不用 :hover) */
.kole-m-grid__item:active { background: var(--kole-color-table-header-bg); }
.kole-m-grid__icon {
font-size: var(--kole-m-grid-icon-size);
line-height: 1;
color: var(--kole-color-brand);
}
.kole-m-grid__text {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
max-width: 100%;
}
/* 纯展示格子(不可点) */
.kole-m-grid__item--static {
cursor: default;
}
.kole-m-grid__item--static:active { background: var(--kole-color-card-bg); }
/* 变体 square=true:格子撑成正方形 */
.kole-m-grid--square .kole-m-grid__item { aspect-ratio: 1 / 1; }
/* 状态 disabled:置灰且不可点 */
.kole-m-grid__item:disabled,
.kole-m-grid__item.is-disabled {
color: var(--kole-color-text-disabled);
cursor: not-allowed;
}
.kole-m-grid__item:disabled .kole-m-grid__icon { color: var(--kole-color-text-disabled); }
.kole-m-grid__item:disabled:active { background: var(--kole-color-card-bg); }
/* 变体 border=true:格线(用 outline 避免相邻边框叠加变粗) */
.kole-m-grid--border .kole-m-grid__item {
outline: 1px solid var(--kole-color-border);
outline-offset: -1px;
}
frameworks-mobile/Grid.html · H5 原生(无框架) · 149 行
<!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 · Grid(H5)</title>
<link rel="stylesheet" href="../.design_library/kole-ui-mobile/colors_and_type.css">
<link rel="stylesheet" href="Grid.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-block[hidden] { display: none; }
</style>
</head>
<body>
<div class="demo">
<section class="demo-block" data-demo="columns-4">
<p class="demo-label">四列宫格(columns=4,最常用的首页入口排布;整块可点)</p>
<nav class="kole-m-grid kole-m-grid--4" data-assert="grid-columns-4" aria-label="常用功能">
<button class="kole-m-grid__item" type="button" data-behavior="click-sets-attr:#grid-entry-1|data-clicked|true" id="grid-entry-1">
<span class="kole-m-grid__icon" aria-hidden="true">▤</span>
<span class="kole-m-grid__text">订单</span>
</button>
<button class="kole-m-grid__item" type="button" data-function="wallet">
<span class="kole-m-grid__icon" aria-hidden="true">◈</span>
<span class="kole-m-grid__text">钱包</span>
</button>
<button class="kole-m-grid__item" type="button">
<span class="kole-m-grid__icon" aria-hidden="true">◉</span>
<span class="kole-m-grid__text">卡券</span>
</button>
<button class="kole-m-grid__item" type="button">
<span class="kole-m-grid__icon" aria-hidden="true">⚙</span>
<span class="kole-m-grid__text">设置</span>
</button>
</nav>
</section>
<section class="demo-block" data-demo="columns-3">
<p class="demo-label">三列宫格(columns=3:每格更宽,适合带副标题的入口)</p>
<nav class="kole-m-grid kole-m-grid--3" data-assert="grid-columns-3" aria-label="数据看板">
<button class="kole-m-grid__item" type="button">
<span class="kole-m-grid__icon" aria-hidden="true">▦</span>
<span class="kole-m-grid__text">销售看板</span>
</button>
<button class="kole-m-grid__item" type="button">
<span class="kole-m-grid__icon" aria-hidden="true">◫</span>
<span class="kole-m-grid__text">库存预警</span>
</button>
<button class="kole-m-grid__item" type="button">
<span class="kole-m-grid__icon" aria-hidden="true">◐</span>
<span class="kole-m-grid__text">客户分析</span>
</button>
</nav>
</section>
<section class="demo-block" data-demo="columns-2">
<p class="demo-label">两列宫格(columns=2:入口更少、信息更重时用)</p>
<nav class="kole-m-grid kole-m-grid--2 kole-m-grid--square" data-assert="grid-columns-2" aria-label="快捷操作">
<button class="kole-m-grid__item" type="button">
<span class="kole-m-grid__icon" aria-hidden="true">↑</span>
<span class="kole-m-grid__text">扫码入库</span>
</button>
<button class="kole-m-grid__item" type="button">
<span class="kole-m-grid__icon" aria-hidden="true">↓</span>
<span class="kole-m-grid__text">扫码出库</span>
</button>
</nav>
</section>
<section class="demo-block" data-demo="border">
<p class="demo-label">带格线(border=true:格线用 outline 画,相邻边不会叠粗)</p>
<nav class="kole-m-grid kole-m-grid--4 kole-m-grid--border" data-assert="grid-border" aria-label="带格线示例">
<button class="kole-m-grid__item" type="button">
<span class="kole-m-grid__icon" aria-hidden="true">◧</span>
<span class="kole-m-grid__text">待发货</span>
</button>
<button class="kole-m-grid__item" type="button">
<span class="kole-m-grid__icon" aria-hidden="true">◨</span>
<span class="kole-m-grid__text">运输中</span>
</button>
<button class="kole-m-grid__item" type="button">
<span class="kole-m-grid__icon" aria-hidden="true">◩</span>
<span class="kole-m-grid__text">已完成</span>
</button>
<button class="kole-m-grid__item" type="button">
<span class="kole-m-grid__icon" aria-hidden="true">◪</span>
<span class="kole-m-grid__text">已关闭</span>
</button>
</nav>
</section>
<section class="demo-block" data-demo="disabled">
<p class="demo-label">含禁用项(disabled:置灰且不响应,多用于无权限入口)</p>
<nav class="kole-m-grid kole-m-grid--4" data-assert="grid-disabled" aria-label="含禁用入口">
<button class="kole-m-grid__item" type="button">
<span class="kole-m-grid__icon" aria-hidden="true">▤</span>
<span class="kole-m-grid__text">订单</span>
</button>
<button class="kole-m-grid__item" type="button" disabled aria-label="财务报表(无权限)">
<span class="kole-m-grid__icon" aria-hidden="true">▥</span>
<span class="kole-m-grid__text">财务报表</span>
</button>
<span class="kole-m-grid__item kole-m-grid__item--static" aria-hidden="true">
<span class="kole-m-grid__icon">▧</span>
<span class="kole-m-grid__text">占位</span>
</span>
<button class="kole-m-grid__item" type="button">
<span class="kole-m-grid__icon" aria-hidden="true">⚙</span>
<span class="kole-m-grid__text">设置</span>
</button>
</nav>
</section>
</div>
<script>
/* 演示页交互:可点宫格被点击后打个标记(真实业务里是路由跳转)。 */
(function () {
var entry = document.getElementById('grid-entry-1');
if (!entry) return;
entry.addEventListener('click', function () {
entry.setAttribute('data-clicked', '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/Grid.jsx · React · 60 行
import React from 'react';
import './Grid.css';
/* 宫格(移动端)— 规格 §14
可点格子是原生 button(整块热区 ≥ 44×44、键盘可达),static 格子用 div 且不绑点击;
select 只回传被点格子的下标,跳转由宿主决定。 */
export default function Grid({
columns = 4,
border = false,
square = false,
items = [],
onSelect,
children = null,
}) {
const cls =
'kole-m-grid' +
` kole-m-grid--${columns}` +
(border ? ' kole-m-grid--border' : '') +
(square ? ' kole-m-grid--square' : '');
/* 图标是装饰(规格 §14.6),文字即语义 */
const iconOf = (it) =>
it && it.icon ? (
<span className="kole-m-grid__icon" aria-hidden="true">
{it.icon}
</span>
) : null;
const textOf = (it) => (it && it.text ? <span className="kole-m-grid__text">{it.text}</span> : null);
const itemCls = (it) => 'kole-m-grid__item' + (it && it.disabled ? ' is-disabled' : '');
return (
<div className={cls}>
{items.map((it, i) =>
it && it.static ? (
/* 纯展示格子:div 且不加交互角色(规格 §14.6) */
<div key={i} className={`${itemCls(it)} kole-m-grid__item--static`}>
{iconOf(it)}
{textOf(it)}
</div>
) : (
<button
key={i}
className={itemCls(it)}
type="button"
disabled={!!(it && it.disabled)}
onClick={() => {
if (!it || it.disabled) return;
if (onSelect) onSelect(i);
}}
>
{iconOf(it)}
{textOf(it)}
</button>
)
)}
{children}
</div>
);
}
frameworks-mobile/Grid.vue2.vue · Vue 2 · 59 行
<template>
<div class="kole-m-grid" :class="gridClass">
<template v-for="(it, i) in items">
<!-- 纯展示格子:div 且不加交互角色(规格 §14.6) -->
<div
v-if="it && it.static"
:key="'static-' + i"
class="kole-m-grid__item kole-m-grid__item--static"
:class="{ 'is-disabled': it.disabled }"
>
<span v-if="it.icon" class="kole-m-grid__icon" aria-hidden="true">{{ it.icon }}</span>
<span v-if="it.text" class="kole-m-grid__text">{{ it.text }}</span>
</div>
<button
v-else
:key="'item-' + i"
class="kole-m-grid__item"
:class="{ 'is-disabled': it && it.disabled }"
type="button"
:disabled="!!(it && it.disabled)"
@click="onItemClick(it, i)"
>
<span v-if="it && it.icon" class="kole-m-grid__icon" aria-hidden="true">{{ it.icon }}</span>
<span v-if="it && it.text" class="kole-m-grid__text">{{ it.text }}</span>
</button>
</template>
<slot></slot>
</div>
</template>
<script>
export default {
name: 'KoleMGrid',
props: {
columns: { type: Number, default: 4 },
border: { type: Boolean, default: false },
square: { type: Boolean, default: false },
items: { type: Array, default: function () { return []; } }
},
computed: {
gridClass: function () {
return [
'kole-m-grid--' + this.columns,
this.border ? 'kole-m-grid--border' : '',
this.square ? 'kole-m-grid--square' : ''
].filter(Boolean);
}
},
methods: {
onItemClick: function (it, i) {
if (!it || it.disabled) return;
this.$emit('select', i);
}
}
};
</script>
<style src="./Grid.css"></style>
frameworks-mobile/Grid.vue3.vue · Vue 3 · 49 行
<template>
<div class="kole-m-grid" :class="gridClass">
<template v-for="(it, i) in items" :key="i">
<!-- 纯展示格子:div 且不加交互角色(规格 §14.6) -->
<div v-if="it && it.static" class="kole-m-grid__item kole-m-grid__item--static" :class="{ 'is-disabled': it.disabled }">
<span v-if="it.icon" class="kole-m-grid__icon" aria-hidden="true">{{ it.icon }}</span>
<span v-if="it.text" class="kole-m-grid__text">{{ it.text }}</span>
</div>
<button
v-else
class="kole-m-grid__item"
:class="{ 'is-disabled': it && it.disabled }"
type="button"
:disabled="!!(it && it.disabled)"
@click="onItemClick(it, i)"
>
<span v-if="it && it.icon" class="kole-m-grid__icon" aria-hidden="true">{{ it.icon }}</span>
<span v-if="it && it.text" class="kole-m-grid__text">{{ it.text }}</span>
</button>
</template>
<slot></slot>
</div>
</template>
<script setup>
import { computed } from 'vue';
const props = defineProps({
columns: { type: Number, default: 4 },
border: { type: Boolean, default: false },
square: { type: Boolean, default: false },
items: { type: Array, default: () => [] }
});
const emit = defineEmits(['select']);
const gridClass = computed(() => [
`kole-m-grid--${props.columns}`,
props.border ? 'kole-m-grid--border' : '',
props.square ? 'kole-m-grid--square' : ''
].filter(Boolean));
function onItemClick(it, i) {
if (!it || it.disabled) return;
emit('select', i);
}
</script>
<style src="./Grid.css"></style>
frameworks-mobile/Grid.uniapp.vue · uni-app(跨端:小程序 / App / H5) · 139 行
<template>
<view class="kole-m-grid" :class="gridClass">
<!-- 纯展示格子:不绑点击、无交互角色(规格 §14.6) -->
<view
v-for="row in staticRows"
:key="'static-' + row.index"
class="kole-m-grid__item kole-m-grid__item--static"
:class="{ 'is-disabled': row.item.disabled }"
>
<text v-if="row.item.icon" class="kole-m-grid__icon" aria-hidden="true">{{ row.item.icon }}</text>
<text v-if="row.item.text" class="kole-m-grid__text">{{ row.item.text }}</text>
</view>
<view
v-for="row in actionRows"
:key="'action-' + row.index"
class="kole-m-grid__item"
:class="{ 'is-disabled': row.item.disabled }"
:role="row.item.disabled ? '' : 'button'"
:aria-disabled="row.item.disabled ? 'true' : 'false'"
@tap="onItemTap(row)"
>
<text v-if="row.item.icon" class="kole-m-grid__icon" aria-hidden="true">{{ row.item.icon }}</text>
<text v-if="row.item.text" class="kole-m-grid__text">{{ row.item.text }}</text>
</view>
<slot></slot>
</view>
</template>
<script setup>
/* uni-app 端 · 宫格(移动端)— 规格 §14
跨端差异:小程序与 App 端没有 <button> 的布局语义,可点格子用 view + role="button"
(role 在 H5 产物里生效,与 Tag / Cell 的写法一致),点击一律 @tap;
尺寸用 rpx(88rpx = 375pt 下的 44px 触控最小边长)。
两段列表(可点 / 纯展示)是刻意的:小程序模板不支持在同一元素上同时用 v-for 与 v-if,
分开渲染才能让「可点格子有交互角色、纯展示格子没有」在编译后依然成立。 */
import { computed } from 'vue';
const props = defineProps({
columns: { type: Number, default: 4 },
border: { type: Boolean, default: false },
square: { type: Boolean, default: false },
items: { type: Array, default: () => [] }
});
const emit = defineEmits(['select']);
const gridClass = computed(() => [
`kole-m-grid--${props.columns}`,
props.border ? 'kole-m-grid--border' : '',
props.square ? 'kole-m-grid--square' : ''
].filter(Boolean));
/* 行里带上原始下标:select 回传的是被点格子在 items 里的下标 */
const rows = computed(() => (props.items || []).map((item, index) => ({ item: item || {}, index: index })));
const staticRows = computed(() => rows.value.filter((row) => row.item.static));
const actionRows = computed(() => rows.value.filter((row) => !row.item.static));
function onItemTap(row) {
if (!row || !row.item || row.item.disabled) return;
emit('select', row.index);
}
</script>
<style>
/* rpx 化的移动端令牌(与其它 uni-app 端组件同一写法) */
.kole-m-grid {
--kole-m-grid-columns: 4;
--kole-m-grid-icon-size: 64rpx;
--kole-m-touch-target: 88rpx;
--kole-m-font-size-label: 28rpx;
display: flex;
flex-wrap: wrap;
background-color: var(--kole-color-card-bg);
}
/* 变体 columns:每行格数(小程序端不用 grid 布局,用「宽度 = 100% / 列数」的 flex 折行) */
.kole-m-grid--2 { --kole-m-grid-columns: 2; }
.kole-m-grid--3 { --kole-m-grid-columns: 3; }
.kole-m-grid--4 { --kole-m-grid-columns: 4; }
.kole-m-grid__item {
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: calc(100% / var(--kole-m-grid-columns));
min-height: var(--kole-m-touch-target);
padding: 32rpx 16rpx;
background-color: var(--kole-color-card-bg);
color: var(--kole-color-text-body);
font-size: var(--kole-m-font-size-label);
line-height: 1.3;
text-align: center;
}
/* 状态 active:按下反馈(触屏不用 :hover) */
.kole-m-grid__item:active { background-color: var(--kole-color-table-header-bg); }
.kole-m-grid__item--static:active { background-color: var(--kole-color-card-bg); }
/* 状态 disabled:置灰且不可点 */
.kole-m-grid__item.is-disabled {
color: var(--kole-color-text-disabled);
}
.kole-m-grid__item.is-disabled .kole-m-grid__icon { color: var(--kole-color-text-disabled); }
.kole-m-grid__icon {
font-size: var(--kole-m-grid-icon-size);
line-height: 1;
color: var(--kole-color-brand);
}
.kole-m-grid__text {
max-width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
/* 变体 square=true:格子撑成正方形(750rpx = 视口宽度) */
.kole-m-grid--square .kole-m-grid__item {
height: calc(750rpx / var(--kole-m-grid-columns));
}
/* 变体 border=true:格线画在容器上沿/左沿 + 每格右沿/下沿,相邻边不叠粗 */
.kole-m-grid--border {
border-top: 1rpx solid var(--kole-color-border);
border-left: 1rpx solid var(--kole-color-border);
}
.kole-m-grid--border .kole-m-grid__item {
border-right: 1rpx solid var(--kole-color-border);
border-bottom: 1rpx solid var(--kole-color-border);
}
</style>
测试与回归
断言在真实的 375×640 设备帧里跑(引擎与 PC 侧共用 tests/_runtime.js,触控行为动词来自移动端 tests/mobile/_behaviors.js)。
断言 16 条 · 全部通过 报告 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-grid.json(点击展开原始 JSON)
{
"schemaVersion": 1,
"sourceKind": "authored-spec",
"provenance": "authored-in-repo",
"specFile": "spec/移动端规格.md",
"confidence": "high",
"specSection": "14 · 宫格 Grid",
"slug": "mobile-grid",
"name": "宫格 Grid",
"semanticTypeCandidates": [
"grid",
"menu",
"shortcut"
],
"variantDimensions": [
{
"name": "columns",
"values": [
"2",
"3",
"4"
]
},
{
"name": "border",
"values": [
"false",
"true"
]
},
{
"name": "square",
"values": [
"false",
"true"
]
}
],
"representativeVariants": [
{
"columns": "4",
"border": "false",
"square": "false",
"label": "四列宫格(最常用)"
},
{
"columns": "3",
"border": "false",
"square": "false",
"label": "三列宫格"
},
{
"columns": "2",
"border": "false",
"square": "true",
"label": "两列正方形"
},
{
"columns": "4",
"border": "true",
"square": "false",
"label": "带格线"
}
],
"anatomy": {
"grid": "根容器",
"grid__item": "单个格子(图标 + 文字)",
"grid__icon": "图标区",
"grid__text": "文字标签"
},
"structurePatterns": {
"columns": "2 / 3 / 4(每行格数)",
"border": "false / true(是否画格线)",
"square": "false / true(格子是否为正方形)"
},
"usageHints": [
"把图标 / 文字入口按等分列排成网格,用于首页功能入口区",
"每个格子整块可点,热区不小于 44×44",
"按下反馈用 :active(不用 :hover —— 触屏没有悬停)",
"可点格子用原生 button(整块热区 + 键盘可达)",
"纯展示格子用 div 且不加交互角色",
"图标装饰(aria-hidden=\"true\"),文字即语义"
],
"doNotInvent": [
"格子的拖拽排序",
"超出 4 列的响应式折行列数"
],
"unknowns": [
"图标区的推荐尺寸",
"一格最多几个字"
],
"interaction": [
"每个格子整块可点,热区不小于 44×44",
"按下反馈用 :active(不用 :hover —— 触屏没有悬停)"
],
"accessibility": [
"可点格子用原生 button(整块热区 + 键盘可达)",
"纯展示格子用 div 且不加交互角色",
"图标装饰(aria-hidden=\"true\"),文字即语义"
],
"api": {
"source": "implementation",
"note": "props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs",
"requiredNote": "「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。",
"props": [
{
"name": "columns",
"type": "number",
"default": "4",
"desc": "变体 columns:每行格数(规格 §14.3)",
"required": false
},
{
"name": "border",
"type": "boolean",
"default": "false",
"desc": "变体 border:是否画格线(规格 §14.3)",
"required": false
},
{
"name": "square",
"type": "boolean",
"default": "false",
"desc": "变体 square:格子是否撑成正方形(规格 §14.3)",
"required": false
},
{
"name": "items",
"type": "Array<{ text, icon, disabled, static }>",
"default": "[]",
"desc": "格子数据;static=true 的格子是纯展示(不绑点击)(规格 §14.2)",
"required": false
}
],
"events": [
{
"name": "select",
"params": "index: number",
"desc": "点击可点格子时触发,回传格子下标;跳转由宿主决定(规格 §14.5)"
}
],
"slots": [
{
"name": "default",
"desc": "额外的自定义格子(追加在 items 之后)"
}
]
},
"variantClasses": {
"columns": {
"2": [
".kole-m-grid--2"
],
"3": [
".kole-m-grid--3"
],
"4": [
".kole-m-grid--4"
]
},
"border": {
"false": [],
"true": [
".kole-m-grid--border"
]
},
"square": {
"false": [],
"true": [
".kole-m-grid--square"
]
}
},
"demos": [
{
"id": "columns-4",
"group": "01 组件类型",
"title": "四列宫格",
"desc": "最常用的首页入口排布;每格是原生按钮,整块可点、键盘可达。",
"variant": "columns=4"
},
{
"id": "columns-3",
"group": "01 组件类型",
"title": "三列宫格",
"desc": "每格更宽,适合带较长标题的入口。",
"variant": "columns=3"
},
{
"id": "columns-2",
"group": "01 组件类型",
"title": "两列正方形",
"desc": "入口更少、信息更重时用;square=true 把格子撑成正方形,视觉更稳。",
"variant": "columns=2 · square=true"
},
{
"id": "border",
"group": "02 组件状态",
"title": "带格线",
"desc": "格线用 outline 绘制,相邻边不会叠成 2px;适合需要明确分区边界的场景。",
"variant": "border=true"
},
{
"id": "disabled",
"group": "02 组件状态",
"title": "含禁用项",
"desc": "禁用项置灰且不响应点击,多用于无权限入口;纯展示格子不绑任何交互。",
"variant": "状态 disabled"
}
],
"related": [
{
"slug": "cell",
"why": "一列一条的入口用单元格,2 列以上的入口才用宫格"
},
{
"slug": "mobile-tag",
"why": "格子上的状态标记用标签,不要把状态写进格子的标题文字里"
},
{
"slug": "mobile-noticebar",
"why": "宫格上方需要一条公告时用通知栏,不要在宫格里插说明文字"
}
]
}