移动端导航宫格

宫格Grid

把图标 / 文字入口按等分列排成网格,用于首页功能入口区

导航 规格 14 · 宫格 Grid 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-grid.css">

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

演示

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

01 组件类型

四列宫格columns=4

最常用的首页入口排布;每格是原生按钮,整块可点、键盘可达。

查看代码(演示页原文 · 21 行)
frameworks-mobile/Grid.html · columns-4
<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>
三列宫格columns=3

每格更宽,适合带较长标题的入口。

查看代码(演示页原文 · 17 行)
frameworks-mobile/Grid.html · columns-3
<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>
两列正方形columns=2 · square=true

入口更少、信息更重时用;square=true 把格子撑成正方形,视觉更稳。

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

带格线border=true

格线用 outline 绘制,相邻边不会叠成 2px;适合需要明确分区边界的场景。

查看代码(演示页原文 · 21 行)
frameworks-mobile/Grid.html · border
<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>
含禁用项状态 disabled

禁用项置灰且不响应点击,多用于无权限入口;纯展示格子不绑任何交互。

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

名称类型默认值说明必传
columnsnumber4变体 columns:每行格数(规格 §14.3)N
borderbooleanfalse变体 border:是否画格线(规格 §14.3)N
squarebooleanfalse变体 square:格子是否撑成正方形(规格 §14.3)N
itemsArray<{ text, icon, disabled, static }>[]格子数据;static=true 的格子是纯展示(不绑点击)(规格 §14.2)N

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

事件

名称参数说明
selectindex: number点击可点格子时触发,回传格子下标;跳转由宿主决定(规格 §14.5)

插槽

名称说明
default额外的自定义格子(追加在 items 之后)

CSS 变量

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

名称默认值说明
--kole-m-grid-columns4组件内部默认值,可在业务侧覆盖
--kole-m-grid-icon-size32px组件内部默认值,可在业务侧覆盖

何时使用

  • 把图标 / 文字入口按等分列排成网格,用于首页功能入口区
  • 每个格子整块可点,热区不小于 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 校验(类/变量必须真实存在)。

维度取值对应类名 / 变量
columns2 / 3 / 4
2 .kole-m-grid--2
3 .kole-m-grid--3
4 .kole-m-grid--4
borderfalse / true
false (由数据驱动,无专属类)
true .kole-m-grid--border
squarefalse / 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 令牌(改一处两端生效)。

--kole-m-font-size-label --kole-m-touch-target --kole-color-border --kole-color-brand --kole-color-card-bg --kole-color-focus-ring --kole-color-table-header-bg --kole-color-text-body --kole-color-text-disabled --kole-font-family --kole-space-16 --kole-space-8 --kole-m-grid-columns --kole-m-grid-icon-size

6 端源码

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

frameworks-mobile/Grid.css · 纯样式(CSS) · 84 行
frameworks-mobile/Grid.css
/* 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 行
frameworks-mobile/Grid.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 · 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 行
frameworks-mobile/Grid.jsx
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 行
frameworks-mobile/Grid.vue2.vue
<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 行
frameworks-mobile/Grid.vue3.vue
<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 行
frameworks-mobile/Grid.uniapp.vue
<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": "宫格上方需要一条公告时用通知栏,不要在宫格里插说明文字"
    }
  ]
}