diff --git a/src-next/grid-item/index.js b/src-next/grid-item/index.js
new file mode 100644
index 000000000..c003d9dbb
--- /dev/null
+++ b/src-next/grid-item/index.js
@@ -0,0 +1,151 @@
+// Utils
+import { createNamespace, addUnit } from '../utils';
+import { BORDER } from '../utils/constant';
+import { route, routeProps } from '../utils/router';
+
+// Mixins
+import { ChildrenMixin } from '../mixins/relation';
+
+// Components
+import Info from '../info';
+import Icon from '../icon';
+
+const [createComponent, bem] = createNamespace('grid-item');
+
+export default createComponent({
+ mixins: [ChildrenMixin('vanGrid')],
+
+ props: {
+ ...routeProps,
+ dot: Boolean,
+ text: String,
+ icon: String,
+ iconPrefix: String,
+ badge: [Number, String],
+ },
+
+ emits: ['click'],
+
+ computed: {
+ style() {
+ const { square, gutter, columnNum } = this.parent;
+ const percent = `${100 / columnNum}%`;
+
+ const style = {
+ flexBasis: percent,
+ };
+
+ if (square) {
+ style.paddingTop = percent;
+ } else if (gutter) {
+ const gutterValue = addUnit(gutter);
+ style.paddingRight = gutterValue;
+
+ if (this.index >= columnNum) {
+ style.marginTop = gutterValue;
+ }
+ }
+
+ return style;
+ },
+
+ contentStyle() {
+ const { square, gutter } = this.parent;
+
+ if (square && gutter) {
+ const gutterValue = addUnit(gutter);
+
+ return {
+ right: gutterValue,
+ bottom: gutterValue,
+ height: 'auto',
+ };
+ }
+ },
+ },
+
+ methods: {
+ onClick(event) {
+ this.$emit('click', event);
+ route(this.$router, this);
+ },
+
+ genIcon() {
+ if (this.$slots.icon) {
+ return (
+
+ {this.$slots.icon()}
+
+
+ );
+ }
+
+ if (this.icon) {
+ return (
+
+ );
+ }
+ },
+
+ getText() {
+ if (this.$slots.text) {
+ return this.$slots.text();
+ }
+
+ if (this.text) {
+ return {this.text};
+ }
+ },
+
+ genContent() {
+ if (this.$slots.default) {
+ return this.$slots.default();
+ }
+
+ return [this.genIcon(), this.getText()];
+ },
+ },
+
+ render() {
+ const {
+ center,
+ border,
+ square,
+ gutter,
+ direction,
+ clickable,
+ } = this.parent;
+
+ return (
+
+
+ {this.genContent()}
+
+
+ );
+ },
+});
diff --git a/src-next/grid-item/index.less b/src-next/grid-item/index.less
new file mode 100644
index 000000000..322982c53
--- /dev/null
+++ b/src-next/grid-item/index.less
@@ -0,0 +1,78 @@
+@import '../style/var';
+
+.van-grid-item {
+ position: relative;
+ box-sizing: border-box;
+
+ &--square {
+ height: 0;
+ }
+
+ &__icon {
+ font-size: @grid-item-icon-size;
+ }
+
+ &__icon-wrapper {
+ position: relative;
+ }
+
+ &__text {
+ color: @grid-item-text-color;
+ font-size: @grid-item-text-font-size;
+ line-height: 1.5;
+ word-wrap: break-word;
+ }
+
+ &__icon + &__text {
+ margin-top: @padding-xs;
+ }
+
+ &__content {
+ display: flex;
+ flex-direction: column;
+ box-sizing: border-box;
+ height: 100%;
+ padding: @grid-item-content-padding;
+ background-color: @grid-item-content-background-color;
+
+ &::after {
+ z-index: 1;
+ border-width: 0 @border-width-base @border-width-base 0;
+ }
+
+ &--square {
+ position: absolute;
+ top: 0;
+ right: 0;
+ left: 0;
+ }
+
+ &--center {
+ align-items: center;
+ justify-content: center;
+ }
+
+ &--horizontal {
+ flex-direction: row;
+
+ .van-grid-item__icon + .van-grid-item__text {
+ margin-top: 0;
+ margin-left: @padding-xs;
+ }
+ }
+
+ &--surround {
+ &::after {
+ border-width: @border-width-base;
+ }
+ }
+
+ &--clickable {
+ cursor: pointer;
+
+ &:active {
+ background-color: @grid-item-content-active-color;
+ }
+ }
+ }
+}
diff --git a/src-next/grid/README.md b/src-next/grid/README.md
new file mode 100644
index 000000000..0ab19d44b
--- /dev/null
+++ b/src-next/grid/README.md
@@ -0,0 +1,134 @@
+# Grid
+
+### Install
+
+```js
+import Vue from 'vue';
+import { Grid, GridItem } from 'vant';
+
+Vue.use(Grid);
+Vue.use(GridItem);
+```
+
+## Usage
+
+### Basic Usage
+
+```html
+
+
+
+
+
+
+```
+
+### Column Num
+
+```html
+
+
+
+```
+
+### Custom Content
+
+```html
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### Square
+
+```html
+
+
+
+```
+
+### Gutter
+
+```html
+
+
+
+```
+
+### Horizontal
+
+```html
+
+
+
+
+
+```
+
+### Route
+
+```html
+
+
+
+
+```
+
+### Show Badge
+
+```html
+
+
+
+
+```
+
+## API
+
+### Grid Props
+
+| Attribute | Description | Type | Default |
+| --- | --- | --- | --- |
+| column-num `v2.0.4` | Column Num | _number \| string_ | `4` |
+| icon-size `v2.2.6` | Icon size | _number \| string_ | `28px` |
+| gutter | Gutter | _number \| string_ | `0` |
+| border | Whether to show border | _boolean_ | `true` |
+| center | Whether to center content | _boolean_ | `true` |
+| square | Whether to be square shape | _boolean_ | `false` |
+| clickable | Whether to show click feedback when clicked | _boolean_ | `false` |
+| direction `v2.8.2` | Content arrangement direction, can be set to `horizontal` | _string_ | `vertical` |
+
+### GridItem Props
+
+| Attribute | Description | Type | Default |
+| --- | --- | --- | --- |
+| text | Text | _string_ | - |
+| icon | Icon name or URL | _string_ | - |
+| icon-prefix `v2.5.3` | Icon className prefix | _string_ | `van-icon` |
+| dot `v2.2.1` | Whether to show red dot | _boolean_ | `false` |
+| badge `v2.5.6` | Content of the badge | _number \| string_ | - |
+| url | Link URL | _string_ | - |
+| to | Target route of the link, same as to of vue-router | _string \| object_ | - |
+| replace | If true, the navigation will not leave a history record | _boolean_ | `false` |
+
+### GridItem Events
+
+| Event | Description | Arguments |
+| ----- | ---------------------- | -------------- |
+| click | Triggered when clicked | _event: Event_ |
+
+### GridItem Slots
+
+| Name | Description |
+| ------- | -------------- |
+| default | Custom content |
+| icon | Custom icon |
+| text | Custom text |
diff --git a/src-next/grid/README.zh-CN.md b/src-next/grid/README.zh-CN.md
new file mode 100644
index 000000000..1dd484706
--- /dev/null
+++ b/src-next/grid/README.zh-CN.md
@@ -0,0 +1,155 @@
+# Grid 宫格
+
+### 介绍
+
+宫格可以在水平方向上把页面分隔成等宽度的区块,用于展示内容或进行页面导航
+
+### 引入
+
+```js
+import Vue from 'vue';
+import { Grid, GridItem } from 'vant';
+
+Vue.use(Grid);
+Vue.use(GridItem);
+```
+
+## 代码演示
+
+### 基础用法
+
+通过`icon`属性设置格子内的图标,`text`属性设置文字内容
+
+```html
+
+
+
+
+
+
+```
+
+### 自定义列数
+
+默认一行展示四个格子,可以通过`column-num`自定义列数
+
+```html
+
+
+
+```
+
+### 自定义内容
+
+通过插槽可以自定义格子展示的内容
+
+```html
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### 正方形格子
+
+设置`square`属性后,格子的高度会和宽度保持一致
+
+```html
+
+
+
+```
+
+### 格子间距
+
+通过`gutter`属性设置格子之间的距离
+
+```html
+
+
+
+```
+
+### 内容横排
+
+将`direction`属性设置为`horizontal`,可以让宫格的内容呈横向排列
+
+```html
+
+
+
+
+
+```
+
+### 页面导航
+
+通过`to`属性设置`vue-router`跳转链接,通过`url`属性设置 URL 跳转链接
+
+```html
+
+
+
+
+```
+
+### 徽标提示
+
+设置`dot`属性后,会在图标右上角展示一个小红点。设置`badge`属性后,会在图标右上角展示相应的徽标
+
+```html
+
+
+
+
+```
+
+## API
+
+### Grid Props
+
+| 参数 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| column-num `v2.0.4` | 列数 | _number \| string_ | `4` |
+| icon-size `v2.2.6` | 图标大小,默认单位为`px` | _number \| string_ | `28px` |
+| gutter | 格子之间的间距,默认单位为`px` | _number \| string_ | `0` |
+| border | 是否显示边框 | _boolean_ | `true` |
+| center | 是否将格子内容居中显示 | _boolean_ | `true` |
+| square | 是否将格子固定为正方形 | _boolean_ | `false` |
+| clickable | 是否开启格子点击反馈 | _boolean_ | `false` |
+| direction `v2.8.2` | 格子内容排列的方向,可选值为 `horizontal` | _string_ | `vertical` |
+
+### GridItem Props
+
+| 参数 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| text | 文字 | _string_ | - |
+| icon | [图标名称](#/zh-CN/icon)或图片链接 | _string_ | - |
+| icon-prefix `v2.5.3` | 图标类名前缀,同 Icon 组件的 [class-prefix 属性](#/zh-CN/icon#props) | _string_ | `van-icon` |
+| dot `v2.2.1` | 是否显示图标右上角小红点 | _boolean_ | `false` |
+| badge `v2.5.6` | 图标右上角徽标的内容 | _number \| string_ | - |
+| info `2.2.1` | 图标右上角徽标的内容(已废弃,请使用 badge 属性) | _number \| string_ | - |
+| url | 点击后跳转的链接地址 | _string_ | - |
+| to | 点击后跳转的目标路由对象,同 vue-router 的 [to 属性](https://router.vuejs.org/zh/api/#to) | _string \| object_ | - |
+| replace | 是否在跳转时替换当前页面历史 | _boolean_ | `false` |
+
+### GridItem Events
+
+| 事件名 | 说明 | 回调参数 |
+| ------ | -------------- | -------------- |
+| click | 点击格子时触发 | _event: Event_ |
+
+### GridItem Slots
+
+| 名称 | 说明 |
+| ------- | -------------------- |
+| default | 自定义宫格的所有内容 |
+| icon | 自定义图标 |
+| text | 自定义文字 |
diff --git a/src-next/grid/demo/index.vue b/src-next/grid/demo/index.vue
new file mode 100644
index 000000000..544f524f1
--- /dev/null
+++ b/src-next/grid/demo/index.vue
@@ -0,0 +1,131 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src-next/grid/index.js b/src-next/grid/index.js
new file mode 100644
index 000000000..5c0d7f62c
--- /dev/null
+++ b/src-next/grid/index.js
@@ -0,0 +1,52 @@
+import { createNamespace, addUnit } from '../utils';
+import { BORDER_TOP } from '../utils/constant';
+import { ParentMixin } from '../mixins/relation';
+
+const [createComponent, bem] = createNamespace('grid');
+
+export default createComponent({
+ mixins: [ParentMixin('vanGrid')],
+
+ props: {
+ square: Boolean,
+ gutter: [Number, String],
+ iconSize: [Number, String],
+ direction: String,
+ clickable: Boolean,
+ columnNum: {
+ type: [Number, String],
+ default: 4,
+ },
+ center: {
+ type: Boolean,
+ default: true,
+ },
+ border: {
+ type: Boolean,
+ default: true,
+ },
+ },
+
+ computed: {
+ style() {
+ const { gutter } = this;
+
+ if (gutter) {
+ return {
+ paddingLeft: addUnit(gutter),
+ };
+ }
+ },
+ },
+
+ render() {
+ return (
+
+ {this.$slots.default?.()}
+
+ );
+ },
+});
diff --git a/src-next/grid/index.less b/src-next/grid/index.less
new file mode 100644
index 000000000..47a890003
--- /dev/null
+++ b/src-next/grid/index.less
@@ -0,0 +1,6 @@
+@import '../style/var';
+
+.van-grid {
+ display: flex;
+ flex-wrap: wrap;
+}
diff --git a/src-next/grid/test/__snapshots__/demo.spec.js.snap b/src-next/grid/test/__snapshots__/demo.spec.js.snap
new file mode 100644
index 000000000..4b9a61ac3
--- /dev/null
+++ b/src-next/grid/test/__snapshots__/demo.spec.js.snap
@@ -0,0 +1,196 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders demo correctly 1`] = `
+
+
+
+
+
+
+
+

+
+
+
+
+
+
+
+

+
+
+
+
+
+
+
+

+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/src-next/grid/test/__snapshots__/index.spec.js.snap b/src-next/grid/test/__snapshots__/index.spec.js.snap
new file mode 100644
index 000000000..e7d763ddc
--- /dev/null
+++ b/src-next/grid/test/__snapshots__/index.spec.js.snap
@@ -0,0 +1,37 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`icon-size prop 1`] = `
+
+`;
+
+exports[`render icon-slot 1`] = `
+
+`;
+
+exports[`sqaure and set gutter 1`] = `
+
+`;
diff --git a/src-next/grid/test/demo.spec.js b/src-next/grid/test/demo.spec.js
new file mode 100644
index 000000000..5c70922b5
--- /dev/null
+++ b/src-next/grid/test/demo.spec.js
@@ -0,0 +1,4 @@
+import Demo from '../demo';
+import { snapshotDemo } from '../../../test/demo';
+
+snapshotDemo(Demo);
diff --git a/src-next/grid/test/index.spec.js b/src-next/grid/test/index.spec.js
new file mode 100644
index 000000000..77377b69f
--- /dev/null
+++ b/src-next/grid/test/index.spec.js
@@ -0,0 +1,60 @@
+import { mount } from '../../../test';
+
+test('click grid item', () => {
+ const onClick = jest.fn();
+ const wrapper = mount({
+ template: `
+
+
+
+ `,
+ methods: {
+ onClick,
+ },
+ });
+
+ const Item = wrapper.find('.van-grid-item__content');
+ Item.trigger('click');
+
+ expect(onClick).toHaveBeenCalledTimes(1);
+});
+
+test('sqaure and set gutter', () => {
+ const wrapper = mount({
+ template: `
+
+
+
+
+
+ `,
+ });
+
+ expect(wrapper).toMatchSnapshot();
+});
+
+test('icon-size prop', () => {
+ const wrapper = mount({
+ template: `
+
+
+
+ `,
+ });
+
+ expect(wrapper).toMatchSnapshot();
+});
+
+test('render icon-slot', () => {
+ const wrapper = mount({
+ template: `
+
+
+
+
+
+ `,
+ });
+
+ expect(wrapper).toMatchSnapshot();
+});
diff --git a/vant.config.js b/vant.config.js
index 44c0a1233..d5eaad36d 100644
--- a/vant.config.js
+++ b/vant.config.js
@@ -286,10 +286,10 @@ module.exports = {
{
title: '导航组件',
items: [
- // {
- // path: 'grid',
- // title: 'Grid 宫格',
- // },
+ {
+ path: 'grid',
+ title: 'Grid 宫格',
+ },
// {
// path: 'index-bar',
// title: 'IndexBar 索引栏',
@@ -620,10 +620,10 @@ module.exports = {
{
title: 'Navigation Components',
items: [
- // {
- // path: 'grid',
- // title: 'Grid',
- // },
+ {
+ path: 'grid',
+ title: 'Grid',
+ },
// {
// path: 'index-bar',
// title: 'IndexBar',