ソースを参照

feat(grid): add grid component (#895)

HaiWei Lian 4 年 前
コミット
b81c9572a8

+ 24 - 0
src/config.json

@@ -587,6 +587,30 @@
           "taro": true,
           "show": false,
           "author": "richard1015"
+        },
+        {
+          "version": "3.1.13",
+          "name": "Grid",
+          "type": "component",
+          "cName": "宫格",
+          "desc": "用于分隔成等宽区块进行页面导航",
+          "sort": 13,
+          "taro": true,
+          "show": true,
+          "author": "haiweilian"
+        },
+        {
+          "version": "3.1.13",
+          "name": "GridItem",
+          "type": "component",
+          "cName": "宫格子组件",
+          "desc": "",
+          "sort": 13,
+          "taro": true,
+          "show": false,
+          "exportEmpty": true,
+          "exportEmptyTaro": true,
+          "author": "haiweilian"
         }
       ]
     },

+ 59 - 0
src/packages/__VUE/grid/__test__/__snapshots__/grid.spec.ts.snap

@@ -0,0 +1,59 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render default slot correctly 1`] = `
+"<view class=\\"nut-grid nut-grid--border\\">
+  <view class=\\"nut-grid-item\\" style=\\"flex-basis: 25%;\\">
+    <view class=\\"nut-grid-item__content nut-grid-item__content--border nut-grid-item__content--center\\">Default Slot</view>
+  </view>
+</view>"
+`;
+
+exports[`should render gutter correctly 1`] = `
+"<view class=\\"nut-grid\\" style=\\"padding-left: 20px;\\">
+  <view class=\\"nut-grid-item\\" style=\\"flex-basis: 25%; padding-right: 20px; margin-bottom: 20px;\\">
+    <view class=\\"nut-grid-item__content nut-grid-item__content--border nut-grid-item__content--surround nut-grid-item__content--center\\"><i class=\\"nutui-iconfont nut-icon nut-icon-\\" style=\\"font-size: 28px; width: 28px; height: 28px;\\" src=\\"\\"></i>
+      <view class=\\"nut-grid-item__text\\"></view>
+    </view>
+  </view>
+  <view class=\\"nut-grid-item\\" style=\\"flex-basis: 25%; padding-right: 20px; margin-bottom: 20px;\\">
+    <view class=\\"nut-grid-item__content nut-grid-item__content--border nut-grid-item__content--surround nut-grid-item__content--center\\"><i class=\\"nutui-iconfont nut-icon nut-icon-\\" style=\\"font-size: 28px; width: 28px; height: 28px;\\" src=\\"\\"></i>
+      <view class=\\"nut-grid-item__text\\"></view>
+    </view>
+  </view>
+  <view class=\\"nut-grid-item\\" style=\\"flex-basis: 25%; padding-right: 20px; margin-bottom: 20px;\\">
+    <view class=\\"nut-grid-item__content nut-grid-item__content--border nut-grid-item__content--surround nut-grid-item__content--center\\"><i class=\\"nutui-iconfont nut-icon nut-icon-\\" style=\\"font-size: 28px; width: 28px; height: 28px;\\" src=\\"\\"></i>
+      <view class=\\"nut-grid-item__text\\"></view>
+    </view>
+  </view>
+  <view class=\\"nut-grid-item\\" style=\\"flex-basis: 25%; padding-right: 20px; margin-bottom: 20px;\\">
+    <view class=\\"nut-grid-item__content nut-grid-item__content--border nut-grid-item__content--surround nut-grid-item__content--center\\"><i class=\\"nutui-iconfont nut-icon nut-icon-\\" style=\\"font-size: 28px; width: 28px; height: 28px;\\" src=\\"\\"></i>
+      <view class=\\"nut-grid-item__text\\"></view>
+    </view>
+  </view>
+</view>"
+`;
+
+exports[`should render square correctly 1`] = `
+"<view class=\\"nut-grid nut-grid--border\\">
+  <view class=\\"nut-grid-item\\" style=\\"flex-basis: 50%; padding-top: 50%;\\">
+    <view class=\\"nut-grid-item__content nut-grid-item__content--border nut-grid-item__content--center nut-grid-item__content--square\\"><i class=\\"nutui-iconfont nut-icon nut-icon-\\" style=\\"font-size: 28px; width: 28px; height: 28px;\\" src=\\"\\"></i>
+      <view class=\\"nut-grid-item__text\\"></view>
+    </view>
+  </view>
+  <view class=\\"nut-grid-item\\" style=\\"flex-basis: 50%; padding-top: 50%;\\">
+    <view class=\\"nut-grid-item__content nut-grid-item__content--border nut-grid-item__content--center nut-grid-item__content--square\\"><i class=\\"nutui-iconfont nut-icon nut-icon-\\" style=\\"font-size: 28px; width: 28px; height: 28px;\\" src=\\"\\"></i>
+      <view class=\\"nut-grid-item__text\\"></view>
+    </view>
+  </view>
+  <view class=\\"nut-grid-item\\" style=\\"flex-basis: 50%; padding-top: 50%;\\">
+    <view class=\\"nut-grid-item__content nut-grid-item__content--border nut-grid-item__content--center nut-grid-item__content--square\\"><i class=\\"nutui-iconfont nut-icon nut-icon-\\" style=\\"font-size: 28px; width: 28px; height: 28px;\\" src=\\"\\"></i>
+      <view class=\\"nut-grid-item__text\\"></view>
+    </view>
+  </view>
+  <view class=\\"nut-grid-item\\" style=\\"flex-basis: 50%; padding-top: 50%;\\">
+    <view class=\\"nut-grid-item__content nut-grid-item__content--border nut-grid-item__content--center nut-grid-item__content--square\\"><i class=\\"nutui-iconfont nut-icon nut-icon-\\" style=\\"font-size: 28px; width: 28px; height: 28px;\\" src=\\"\\"></i>
+      <view class=\\"nut-grid-item__text\\"></view>
+    </view>
+  </view>
+</view>"
+`;

+ 111 - 0
src/packages/__VUE/grid/__test__/grid.spec.ts

@@ -0,0 +1,111 @@
+import { h, nextTick } from 'vue';
+import { config, mount } from '@vue/test-utils';
+import { useRouter } from 'vue-router';
+import Grid from '../index.vue';
+import GridItem from '../../griditem/index.vue';
+import NutIcon from '../../icon/index.vue';
+
+beforeAll(() => {
+  config.global.components = {
+    NutIcon
+  };
+});
+
+afterAll(() => {
+  config.global.components = {};
+});
+
+// mock module
+jest.mock('vue-router', () => ({
+  useRouter: jest.fn()
+}));
+
+test('should render square correctly', () => {
+  const wrapper = mount(Grid, {
+    props: {
+      square: true,
+      columnNum: 2
+    },
+    slots: {
+      default: [GridItem, GridItem, GridItem, GridItem]
+    }
+  });
+
+  expect(wrapper.html()).toMatchSnapshot();
+});
+
+test('should render gutter correctly', () => {
+  const wrapper = mount(Grid, {
+    props: {
+      gutter: 20
+    },
+    slots: {
+      default: [GridItem, GridItem, GridItem, GridItem]
+    }
+  });
+
+  expect(wrapper.html()).toMatchSnapshot();
+});
+
+test('should change icon and color when using icon-size and icon-color prop', () => {
+  const wrapper = mount(Grid, {
+    props: {
+      iconSize: 30
+    },
+    slots: {
+      default: h(GridItem, {
+        iconColor: 'red'
+      })
+    }
+  });
+
+  expect(wrapper.find<HTMLElement>('.nut-icon').element.style.fontSize).toEqual('30px');
+  expect(wrapper.find<HTMLElement>('.nut-icon').element.style.color).toEqual('red');
+});
+
+test('should render default slot correctly', () => {
+  const wrapper = mount(Grid, {
+    slots: {
+      default: h(GridItem, null, {
+        default: () => 'Default Slot'
+      })
+    }
+  });
+
+  expect(wrapper.find('.nut-grid-item__content').html()).toContain('Default Slot');
+  expect(wrapper.html()).toMatchSnapshot();
+});
+
+test('should emit click correctly', async () => {
+  const wrapper = mount(Grid, {
+    slots: {
+      default: [GridItem]
+    }
+  });
+
+  wrapper.find('.nut-grid-item').trigger('click');
+  await nextTick();
+
+  expect(wrapper.emitted('click')).toHaveLength(1);
+});
+
+test('should navifation correctly', async () => {
+  // 当 `useRouter()` 时返回 `push` 方法
+  const push = jest.fn((url: string) => url);
+  (useRouter as jest.Mock).mockImplementationOnce(() => ({
+    push
+  }));
+
+  const wrapper = mount(Grid, {
+    slots: {
+      default: h(GridItem, {
+        to: '/home'
+      })
+    }
+  });
+
+  wrapper.find('.nut-grid-item').trigger('click');
+  await nextTick();
+
+  expect(push.mock.calls[0][0]).toEqual('/home');
+});

+ 99 - 0
src/packages/__VUE/grid/common.ts

@@ -0,0 +1,99 @@
+import { h, provide, computed } from 'vue';
+import type { PropType, CSSProperties, ExtractPropTypes, SetupContext, RenderFunction } from 'vue';
+import { createComponent } from '../../utils/create';
+import { pxCheck } from '../../utils/pxCheck';
+
+const { componentName } = createComponent('grid');
+
+export const GRID_KEY = Symbol('grid');
+
+export type GridDirection = 'horizontal' | 'vertical';
+
+export const gridProps = {
+  // 列数
+  columnNum: {
+    type: [Number, String],
+    default: 4
+  },
+  // 图标大小
+  iconSize: {
+    type: [Number, String],
+    default: 28
+  },
+  // 图标颜色
+  iconColor: {
+    type: String
+  },
+  // 是否显示边框
+  border: {
+    type: Boolean,
+    default: true
+  },
+  // 格子之间间隔距离
+  gutter: {
+    type: [Number, String],
+    default: 0
+  },
+  // 是否内容居中
+  center: {
+    type: Boolean,
+    default: true
+  },
+  // 是否固定正方形
+  square: {
+    type: Boolean,
+    default: false
+  },
+  // 内容与文字翻转
+  reverse: {
+    type: Boolean,
+    default: false
+  },
+  // 内容排列方向
+  direction: {
+    type: String as PropType<GridDirection>
+  },
+  // 是否开启点击反馈
+  clickable: {
+    type: Boolean,
+    default: false
+  }
+};
+
+export type GridProps = ExtractPropTypes<typeof gridProps>;
+
+export const component = {
+  props: gridProps,
+  setup(props: GridProps, { slots }: SetupContext): RenderFunction {
+    provide(GRID_KEY, props);
+
+    const rootClass = computed(() => {
+      const prefixCls = componentName;
+      return {
+        [prefixCls]: true,
+        [`${prefixCls}--border`]: props.border && !props.gutter
+      };
+    });
+
+    const rootStyle = computed(() => {
+      const style: CSSProperties = {};
+
+      if (props.gutter) {
+        style.paddingLeft = pxCheck(props.gutter);
+      }
+
+      return style;
+    });
+
+    return () => {
+      return h(
+        'view',
+        {
+          class: rootClass.value,
+          style: rootStyle.value
+        },
+        slots.default?.()
+      );
+    };
+  }
+};

+ 91 - 0
src/packages/__VUE/grid/demo.vue

@@ -0,0 +1,91 @@
+<template>
+  <div class="demo full">
+    <h2>基础用法</h2>
+    <nut-grid>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+    </nut-grid>
+
+    <h2>自定义列数</h2>
+    <nut-grid :column-num="3">
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+    </nut-grid>
+
+    <h2>正方形格子</h2>
+    <nut-grid :column-num="3" square>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+    </nut-grid>
+
+    <h2>格子间距</h2>
+    <nut-grid :gutter="10">
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+    </nut-grid>
+
+    <h2>内容翻转</h2>
+    <nut-grid reverse>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+    </nut-grid>
+
+    <h2>内容横向</h2>
+    <nut-grid direction="horizontal">
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+    </nut-grid>
+
+    <h2>图标颜色/大小</h2>
+    <nut-grid :column-num="3" icon-color="#fa2c19">
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" icon-color="#478EF2" icon-size="40" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+    </nut-grid>
+
+    <h2>页面导航</h2>
+    <nut-grid :column-num="2">
+      <nut-grid-item icon="home" text="路由跳转 ’/‘ " to="/"></nut-grid-item>
+      <nut-grid-item icon="search" text="URL 跳转" url="https://jd.com"></nut-grid-item>
+    </nut-grid>
+
+    <h2>自定义内容</h2>
+    <nut-grid :border="false">
+      <nut-grid-item v-for="i in 4" :key="i">
+        <nut-avatar
+          size="large"
+          icon="https://img12.360buyimg.com/imagetools/jfs/t1/143702/31/16654/116794/5fc6f541Edebf8a57/4138097748889987.png"
+        />
+      </nut-grid-item>
+    </nut-grid>
+  </div>
+</template>
+
+<script lang="ts">
+import { createComponent } from '../../utils/create';
+const { createDemo } = createComponent('grid');
+export default createDemo({
+  props: {},
+  setup() {
+    return {};
+  }
+});
+</script>

+ 164 - 0
src/packages/__VUE/grid/doc.md

@@ -0,0 +1,164 @@
+# Grid 宫格
+
+### 介绍
+
+用于分隔成等宽区块进行页面导航。
+
+### 安装
+
+``` javascript
+import { createApp } from 'vue';
+// vue
+import { Grid, GridItem } from '@nutui/nutui';
+// taro
+import { Grid, GridItem } from '@nutui/nutui-taro';
+
+const app = createApp();
+app.use(Grid);
+app.use(GridItem);
+```
+
+### 基础用法
+
+``` html
+<nut-grid>
+  <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+  <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+  <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+  <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+  <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+  <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+  <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+  <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+</nut-grid>
+```
+
+### 自定义列数
+
+``` html
+<nut-grid :column-num="3">
+  <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+  <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+  <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+</nut-grid>
+```
+
+### 正方形格子
+
+``` html
+<nut-grid :column-num="3" square>
+  <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+  <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+  <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+</nut-grid>
+```
+
+### 格子间距
+
+``` html
+<nut-grid :gutter="10">
+  <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+  <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+  <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+  <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+  <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+  <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+  <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+  <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+</nut-grid>
+```
+
+### 内容翻转
+
+``` html
+<nut-grid reverse>
+  <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+  <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+  <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+  <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+</nut-grid>
+```
+
+### 内容横向
+
+``` html
+<nut-grid direction="horizontal">
+  <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+  <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+  <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+  <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+</nut-grid>
+```
+
+### 图标颜色/大小
+
+``` html
+<nut-grid :column-num="3" icon-color="#fa2c19">
+  <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+  <nut-grid-item icon="dongdong" icon-color="#478EF2" icon-size="40" text="文字"></nut-grid-item>
+  <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+</nut-grid>
+```
+
+### 页面导航
+
+``` html
+<nut-grid :column-num="2">
+  <nut-grid-item icon="home" text="路由跳转 ’/‘ " to="/"></nut-grid-item>
+  <nut-grid-item icon="search" text="URL 跳转" url="https://jd.com"></nut-grid-item>
+</nut-grid>
+```
+
+### 自定义内容
+
+``` html
+<nut-grid :border="false">
+  <nut-grid-item v-for="i in 4" :key="i">
+    <nut-avatar
+      size="large"
+      icon="https://img12.360buyimg.com/imagetools/jfs/t1/143702/31/16654/116794/5fc6f541Edebf8a57/4138097748889987.png"
+    />
+  </nut-grid-item>
+</nut-grid>
+```
+
+### Grid Props
+
+| 参数          | 说明                                      | 类型                    | 默认值      |
+|---------------|------------------------------------------|------------------------|------------|
+| column-num    | 列数                                     | number \| string         | `4`        |
+| icon-size     | 图标大小,如 `20px` `2em` `2rem`          | number \| string        | `28px`     |
+| icon-color    | 图标颜色                                  | string                 | -          |
+| border        | 是否显示边框                               | boolean                | `true`     |
+| gutter        | 格子之间的间距,默认单位为`px`               | number \| string        | `0`        |
+| center        | 是否将格子内容居中显示                      | boolean                | `true`      |
+| square        | 是否将格子固定为正方形                      | boolean                | `false`     |
+| reverse       | 内容翻转                                  | boolean                | `false`     |
+| direction     | 格子内容排列的方向,可选值为 `horizontal`    | string                 | `vertical`  |
+| clickable     | 是否开启格子点击反馈                        | boolean                | `false`     |
+
+### GridItem Props
+
+| 参数                  | 说明                                                                                     | 类型               | 默认值      |
+|----------------------|-----------------------------------------------------------------------------------------|--------------------|------------|
+| text                 | 文字                                                                                     | string             | -          |
+| icon                 | [图标名称](#/icon) 或图片链接                                                              | string             | -          |
+| icon-size            | 图标大小,如 `20px` `2em` `2rem`                                                          | number \| string   | `28px`     |
+| icon-color           | 图标颜色                                                                                  | string            | -           |
+| url `小程序不支持`     | 点击后跳转的链接地址                                                                        | string            | -           |
+| to `小程序不支持`      | 点击后跳转的目标路由对象,同 vue-router 的 [to 属性](https://router.vuejs.org/zh/api/#to) 属性 | string \| object  | -           |
+| replace `小程序不支持` | 是否在跳转时替换当前页面历史                                                                 | boolean           | `false`     |
+
+### GridItem Slots
+
+| 名称                   | 说明                 |
+|-----------------------|----------------------|
+| default               | 自定义所有内容         |
+| icon                  | 自定义图标            |
+| text                  | 自定义文字            |
+
+### GridItem Event
+
+| 事件名                 | 说明                   | 回调参数               |
+|-----------------------|-----------------------|-----------------------|
+| click                 | 点击格子时触发          | event: Event          |

+ 10 - 0
src/packages/__VUE/grid/index.scss

@@ -0,0 +1,10 @@
+.nut-grid {
+  display: flex;
+  flex-wrap: wrap;
+  border: 0 solid $grid-border-color;
+
+  &--border {
+    border-top-width: 1px;
+    border-left-width: 1px;
+  }
+}

+ 6 - 0
src/packages/__VUE/grid/index.taro.vue

@@ -0,0 +1,6 @@
+<script lang="ts">
+import { createComponent } from '../../utils/create';
+import { component } from './common';
+const { create } = createComponent('grid');
+export default create(component);
+</script>

+ 6 - 0
src/packages/__VUE/grid/index.vue

@@ -0,0 +1,6 @@
+<script lang="ts">
+import { createComponent } from '../../utils/create';
+import { component } from './common';
+const { create } = createComponent('grid');
+export default create(component);
+</script>

+ 92 - 0
src/packages/__VUE/griditem/index.scss

@@ -0,0 +1,92 @@
+.nut-grid-item {
+  position: relative;
+  box-sizing: border-box;
+
+  $block: &;
+
+  &__text {
+    color: $grid-item-text-color;
+    font-size: $grid-item-text-font-size;
+    line-height: 1.5;
+    word-break: break-all;
+    margin: $grid-item-text-margin 0 0 0;
+  }
+
+  &__content {
+    display: flex;
+    flex-direction: column;
+    box-sizing: border-box;
+    height: 100%;
+    padding: $grid-item-content-padding;
+    background: $grid-item-content-bg-color;
+    border: 0 solid $grid-border-color;
+
+    &--border {
+      border-right-width: 1px;
+      border-bottom-width: 1px;
+    }
+
+    &--surround {
+      border-top-width: 1px;
+      border-left-width: 1px;
+    }
+
+    &--center {
+      align-items: center;
+      justify-content: center;
+    }
+
+    &--square {
+      position: absolute;
+      top: 0;
+      right: 0;
+      left: 0;
+    }
+
+    &--reverse {
+      flex-direction: column-reverse;
+
+      #{$block}__text {
+        margin: 0 0 $grid-item-text-margin;
+      }
+    }
+
+    &--horizontal {
+      flex-direction: row;
+
+      #{$block}__text {
+        margin: 0 0 0 $grid-item-text-margin;
+      }
+    }
+
+    &--horizontal#{&}--reverse {
+      flex-direction: row-reverse;
+      #{$block}__text {
+        margin: 0 $grid-item-text-margin 0 0;
+      }
+    }
+
+    &--clickable {
+      cursor: pointer;
+
+      &::before {
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        width: 100%;
+        height: 100%;
+        background-color: $black;
+        border: inherit;
+        border-color: $black;
+        border-radius: inherit;
+        transform: translate(-50%, -50%);
+        opacity: 0;
+        content: ' ';
+      }
+
+      &:active::before {
+        opacity: 0.1;
+      }
+    }
+  }
+}

+ 120 - 0
src/packages/__VUE/griditem/index.taro.vue

@@ -0,0 +1,120 @@
+<template>
+  <view :class="rootClass" :style="rootStyle" @click="handleClick">
+    <view :class="contentClass">
+      <template v-if="$slots.default">
+        <slot></slot>
+      </template>
+      <template v-else>
+        <slot v-if="$slots.icon" name="icon"></slot>
+        <nut-icon v-else :name="iconProps.name" :size="iconProps.size" :color="iconProps.color"></nut-icon>
+
+        <slot v-if="$slots.text" name="text"></slot>
+        <view v-else class="nut-grid-item__text">{{ text }}</view>
+      </template>
+    </view>
+  </view>
+</template>
+
+<script lang="ts">
+import { inject, computed, CSSProperties } from 'vue';
+import { createComponent } from '../../utils/create';
+import { pxCheck } from '../../utils/pxCheck';
+import { GRID_KEY, GridProps } from '../grid/common';
+const { create, componentName } = createComponent('grid-item');
+
+export default create({
+  props: {
+    text: {
+      type: String
+    },
+    // icon
+    icon: {
+      type: String
+    },
+    iconSize: {
+      type: [Number, String]
+    },
+    iconColor: {
+      type: String
+    }
+    // router
+    // to: {
+    //   type: [String, Object]
+    // },
+    // url: {
+    //   type: String,
+    //   default: ''
+    // },
+    // replace: {
+    //   type: Boolean,
+    //   default: false
+    // }
+  },
+  emits: ['click'],
+  setup(props, { emit }) {
+    const parent = inject(GRID_KEY) as Required<GridProps>;
+
+    // root
+    const rootClass = computed(() => {
+      const prefixCls = componentName;
+      return {
+        [prefixCls]: true
+      };
+    });
+
+    const rootStyle = computed(() => {
+      const style: CSSProperties = {
+        flexBasis: `${100 / +parent.columnNum}%`
+      };
+
+      if (parent.square) {
+        style.paddingTop = `${100 / +parent.columnNum}%`;
+      } else if (parent.gutter) {
+        style.paddingRight = pxCheck(parent.gutter);
+        // TODO: 上边缘间隔处理 index >= columnNum
+        // style.marginTop = pxCheck(parent.gutter);
+        style.marginBottom = pxCheck(parent.gutter);
+      }
+
+      return style;
+    });
+
+    // content
+    const contentClass = computed(() => {
+      const prefixCls = `${componentName}__content`;
+      return {
+        [`${prefixCls}`]: true,
+        [`${prefixCls}--border`]: parent.border,
+        [`${prefixCls}--surround`]: parent.border && parent.gutter,
+        [`${prefixCls}--center`]: parent.center,
+        [`${prefixCls}--square`]: parent.square,
+        [`${prefixCls}--reverse`]: parent.reverse,
+        [`${prefixCls}--${parent.direction}`]: !!parent.direction,
+        [`${prefixCls}--clickable`]: parent.clickable
+      };
+    });
+
+    // icon
+    const iconProps = computed(() => {
+      return {
+        name: props.icon,
+        size: props.iconSize || parent.iconSize,
+        color: props.iconColor || parent.iconColor
+      };
+    });
+
+    // click
+    const handleClick = (event: Event) => {
+      emit('click', event);
+    };
+
+    return {
+      rootClass,
+      rootStyle,
+      contentClass,
+      iconProps,
+      handleClick
+    };
+  }
+});
+</script>

+ 128 - 0
src/packages/__VUE/griditem/index.vue

@@ -0,0 +1,128 @@
+<template>
+  <view :class="rootClass" :style="rootStyle" @click="handleClick">
+    <view :class="contentClass">
+      <template v-if="$slots.default">
+        <slot></slot>
+      </template>
+      <template v-else>
+        <slot v-if="$slots.icon" name="icon"></slot>
+        <nut-icon v-else :name="iconProps.name" :size="iconProps.size" :color="iconProps.color"></nut-icon>
+
+        <slot v-if="$slots.text" name="text"></slot>
+        <view v-else class="nut-grid-item__text">{{ text }}</view>
+      </template>
+    </view>
+  </view>
+</template>
+
+<script lang="ts">
+import { inject, computed, CSSProperties } from 'vue';
+import { useRouter } from 'vue-router';
+import { createComponent } from '../../utils/create';
+import { pxCheck } from '../../utils/pxCheck';
+import { GRID_KEY, GridProps } from '../grid/common';
+const { create, componentName } = createComponent('grid-item');
+
+export default create({
+  props: {
+    text: {
+      type: String
+    },
+    // icon
+    icon: {
+      type: String
+    },
+    iconSize: {
+      type: [Number, String]
+    },
+    iconColor: {
+      type: String
+    },
+    // router
+    to: {
+      type: [String, Object]
+    },
+    url: {
+      type: String,
+      default: ''
+    },
+    replace: {
+      type: Boolean,
+      default: false
+    }
+  },
+  emits: ['click'],
+  setup(props, { emit }) {
+    const parent = inject(GRID_KEY) as Required<GridProps>;
+
+    // root
+    const rootClass = computed(() => {
+      const prefixCls = componentName;
+      return {
+        [prefixCls]: true
+      };
+    });
+
+    const rootStyle = computed(() => {
+      const style: CSSProperties = {
+        flexBasis: `${100 / +parent.columnNum}%`
+      };
+
+      if (parent.square) {
+        style.paddingTop = `${100 / +parent.columnNum}%`;
+      } else if (parent.gutter) {
+        style.paddingRight = pxCheck(parent.gutter);
+        // TODO: 上边缘间隔处理 index >= columnNum
+        // style.marginTop = pxCheck(parent.gutter);
+        style.marginBottom = pxCheck(parent.gutter);
+      }
+
+      return style;
+    });
+
+    // content
+    const contentClass = computed(() => {
+      const prefixCls = `${componentName}__content`;
+      return {
+        [`${prefixCls}`]: true,
+        [`${prefixCls}--border`]: parent.border,
+        [`${prefixCls}--surround`]: parent.border && parent.gutter,
+        [`${prefixCls}--center`]: parent.center,
+        [`${prefixCls}--square`]: parent.square,
+        [`${prefixCls}--reverse`]: parent.reverse,
+        [`${prefixCls}--${parent.direction}`]: !!parent.direction,
+        [`${prefixCls}--clickable`]: parent.clickable || props.to || props.url
+      };
+    });
+
+    // icon
+    const iconProps = computed(() => {
+      return {
+        name: props.icon,
+        size: props.iconSize || parent.iconSize,
+        color: props.iconColor || parent.iconColor
+      };
+    });
+
+    // click
+    const router = useRouter();
+    const handleClick = (event: Event) => {
+      emit('click', event);
+
+      if (props.to && router) {
+        router[props.replace ? 'replace' : 'push'](props.to);
+      } else if (props.url) {
+        props.replace ? location.replace(props.url) : (location.href = props.url);
+      }
+    };
+
+    return {
+      rootClass,
+      rootStyle,
+      contentClass,
+      iconProps,
+      handleClick
+    };
+  }
+});
+</script>

+ 10 - 0
src/packages/styles/variables.scss

@@ -406,10 +406,20 @@ $sku-opetate-bg-buy: linear-gradient(
   rgba(255, 195, 13, 1) 69%,
   rgba(255, 207, 13, 1) 100%
 );
+
 // card
 $card-font-size-0: $font-size-0;
 $card-font-size-1: $font-size-1;
 $card-font-size-2: $font-size-2;
 $card-font-size-3: $font-size-3;
+
+// grid
+$grid-border-color: #f5f6f7 !default;
+$grid-item-content-padding: 16px 8px !default;
+$grid-item-content-bg-color: $white !default;
+$grid-item-text-margin: 8px !default;
+$grid-item-text-color: $title-color2 !default;
+$grid-item-text-font-size: $font-size-1 !default;
+
 @import './mixins/index';
 @import './animation/index';

+ 2 - 1
src/sites/mobile-taro/vue/src/app.config.ts

@@ -49,7 +49,8 @@ export default {
         'pages/fixednav/index',
         'pages/elevator/index',
         'pages/menu/index',
-        'pages/pagination/index'
+        'pages/pagination/index',
+        'pages/grid/index'
       ]
     },
     {

+ 3 - 0
src/sites/mobile-taro/vue/src/nav/pages/grid/index.config.ts

@@ -0,0 +1,3 @@
+export default {
+  navigationBarTitleText: 'Grid'
+};

+ 80 - 0
src/sites/mobile-taro/vue/src/nav/pages/grid/index.vue

@@ -0,0 +1,80 @@
+<template>
+  <div class="demo full">
+    <h2>基础用法</h2>
+    <nut-grid>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+    </nut-grid>
+
+    <h2>自定义列数</h2>
+    <nut-grid :column-num="3">
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+    </nut-grid>
+
+    <h2>正方形格子</h2>
+    <nut-grid :column-num="3" square>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+    </nut-grid>
+
+    <h2>格子间距</h2>
+    <nut-grid :gutter="10">
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+    </nut-grid>
+
+    <h2>内容翻转</h2>
+    <nut-grid reverse>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+    </nut-grid>
+
+    <h2>内容横向</h2>
+    <nut-grid direction="horizontal">
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+    </nut-grid>
+
+    <h2>图标颜色/大小</h2>
+    <nut-grid :column-num="3" icon-color="#fa2c19">
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" icon-color="#478EF2" icon-size="40" text="文字"></nut-grid-item>
+      <nut-grid-item icon="dongdong" text="文字"></nut-grid-item>
+    </nut-grid>
+
+    <!-- <h2>页面导航</h2>
+    <nut-grid :column-num="2">
+      <nut-grid-item icon="home" text="路由跳转 ’/‘ " to="/"></nut-grid-item>
+      <nut-grid-item icon="search" text="URL 跳转" url="https://jd.com"></nut-grid-item>
+    </nut-grid> -->
+
+    <h2>自定义内容</h2>
+    <nut-grid :border="false">
+      <nut-grid-item v-for="i in 4" :key="i">
+        <nut-avatar
+          size="large"
+          icon="https://img12.360buyimg.com/imagetools/jfs/t1/143702/31/16654/116794/5fc6f541Edebf8a57/4138097748889987.png"
+        />
+      </nut-grid-item>
+    </nut-grid>
+  </div>
+</template>