Browse Source

Feat empty 空状态组件 (#899)

* feat(empty): add empty component
Jess 4 years ago
parent
commit
c834450eae

+ 12 - 0
src/config.json

@@ -468,6 +468,18 @@
           "show": true,
           "taro": true,
           "author": "zongyue3"
+        },
+        {
+          "version": "3.0.0",
+          "name": "Empty",
+          "tarodoc": true,
+          "taro": true,
+          "type": "component",
+          "cName": "空状态",
+          "desc": "空状态时占位提示",
+          "sort": 24,
+          "show": true,
+          "author": "wujia8"
         }
       ]
     },

+ 43 - 0
src/packages/__VUE/empty/__tests__/empty.spec.ts

@@ -0,0 +1,43 @@
+import { mount } from '@vue/test-utils';
+import Empty from '../../empty/index.vue';
+
+test('prop image description', () => {
+  const wrapper = mount(Empty, {
+    props: {
+      image: 'empty',
+      description: '文字描述'
+    }
+  });
+
+  expect(wrapper.html()).toContain('img');
+  expect(wrapper.html()).toContain('文字描述');
+});
+
+test('slot image', () => {
+  const wrapper = mount(Empty, {
+    slots: {
+      image: '<img src="https://xxx.png"/>'
+    }
+  });
+
+  expect(wrapper.html()).toContain('xxx.png');
+});
+
+test('slot description', () => {
+  const wrapper = mount(Empty, {
+    slots: {
+      description: 'jest'
+    }
+  });
+
+  expect(wrapper.html()).toContain('jest');
+});
+
+test('slot defalut', () => {
+  const wrapper = mount(Empty, {
+    slots: {
+      default: '加载失败,请刷新页面'
+    }
+  });
+  expect(wrapper.html()).toContain('请刷新页面');
+});

+ 64 - 0
src/packages/__VUE/empty/demo.vue

@@ -0,0 +1,64 @@
+<template>
+  <div class="demo">
+    <h2>基础用法</h2>
+    <div class="show">
+      <nut-empty description="无数据"></nut-empty>
+    </div>
+
+    <h2>图片类型,内置3个</h2>
+    <div class="show">
+      <nut-tabs v-model="tabValue">
+        <nut-tabpane title="无内容">
+          <nut-empty image="empty" description="无内容"></nut-empty>
+        </nut-tabpane>
+        <nut-tabpane title="加载失败/错误">
+          <nut-empty image="error" description="加载失败/错误"></nut-empty>
+        </nut-tabpane>
+        <nut-tabpane title="无网络">
+          <nut-empty image="network" description="无网络"></nut-empty>
+        </nut-tabpane>
+      </nut-tabs>
+    </div>
+
+    <h2>自定义图片</h2>
+    <div class="show">
+      <nut-empty description="无优惠券">
+        <template #image>
+          <img src="https://static-ftcms.jd.com/p/files/61a9e3313985005b3958672e.png" />
+        </template>
+      </nut-empty>
+    </div>
+
+    <h2>底部内容</h2>
+    <div class="show">
+      <nut-empty image="error" description="加载失败">
+        <div style="margin-top: 10px">
+          <nut-button icon="refresh" type="primary">重试</nut-button>
+        </div>
+      </nut-empty>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { ref } from 'vue';
+import { createComponent } from '../../utils/create';
+const { createDemo } = createComponent('empty');
+export default createDemo({
+  props: {},
+  setup() {
+    const tabValue = ref(0);
+    return {
+      tabValue
+    };
+  }
+});
+</script>
+
+<style lang="scss" scoped>
+.demo {
+  .show {
+    background: #ffffff;
+  }
+}
+</style>

+ 68 - 0
src/packages/__VUE/empty/doc.md

@@ -0,0 +1,68 @@
+# empty组件
+
+### 介绍
+
+空状态时的占位提示
+
+### 安装
+```javascript
+import { createApp } from 'vue';
+// vue
+import { Empty } from '@nutui/nutui';
+
+// taro
+import { Empty } from '@nutui/nutui-taro';
+
+const app = createApp();
+app.use(Empty);
+```
+
+### 基础用法
+```html
+<nut-empty description="无数据"></nut-empty>
+```
+
+### 图片类型,内置 3 个
+```html
+<nut-empty image="empty" description="无内容"></nut-empty>
+    
+<nut-empty image="error" description="加载失败/错误"></nut-empty>
+
+<nut-empty image="network" description="无网络"></nut-empty>
+```
+### 自定义图片
+```html
+<nut-empty description="无优惠券">
+    <template #image>
+        <img src="https://static-ftcms.jd.com/p/files/61a9e3313985005b3958672e.png" />
+    </template>
+</nut-empty>
+```
+
+### 底部内容
+```html
+<nut-empty image="error" description="加载失败">
+    <div style="margin-top: 10px">
+        <nut-button icon="refresh" type="primary">重试</nut-button>
+    </div>
+</nut-empty>
+```
+
+## API
+
+### Props
+
+| 参数         | 说明                             | 类型   | 默认值           |
+|--------------|----------------------------------|--------|------------------|
+| image         | 图片类型,可选值为 error network search,支持传入图片 URL               | String | empty        |
+| image-size        | 图片大小,Number 类型单位为 px                         | Number \| String | -       |
+| description         | 图片下方的描述文字 | String | 无内容                |
+
+### Slots
+
+| 事件名 | 说明           | 
+|--------|----------------|
+| default  | 	自定义底部内容 | 
+| image  | 自定义图片 | 
+| description  | 自定义描述文字 | 
+    

+ 33 - 0
src/packages/__VUE/empty/index.scss

@@ -0,0 +1,33 @@
+.nut-empty {
+  box-sizing: border-box;
+  display: flex;
+  align-items: center;
+  flex-direction: column;
+  justify-content: center;
+  padding: $empty-padding;
+
+  &-image {
+    width: $empty-image-size;
+    height: $empty-image-size;
+
+    .img {
+      width: 100%;
+      height: 100%;
+    }
+
+    // 兼容小程序标签和img-slot
+    img,
+    image {
+      width: 100%;
+      height: 100%;
+    }
+  }
+
+  &-description {
+    margin-top: $empty-description-margin-top;
+    padding: $empty-description-padding;
+    color: $empty-description-color;
+    font-size: $empty-description-font-size;
+    line-height: $empty-description-line-height;
+  }
+}

+ 88 - 0
src/packages/__VUE/empty/index.taro.vue

@@ -0,0 +1,88 @@
+<template>
+  <view class="nut-empty">
+    <!-- 占位图 -->
+    <view class="nut-empty-image" :style="imgStyle">
+      <template v-if="$slots.image">
+        <slot name="image"></slot>
+      </template>
+      <template v-else>
+        <img v-if="imageUrl" class="img" :src="imageUrl" />
+      </template>
+    </view>
+
+    <!-- 文本区 -->
+    <template v-if="$slots.description">
+      <slot name="description"></slot>
+    </template>
+    <template v-else>
+      <view class="nut-empty-description">{{ description }}</view>
+    </template>
+
+    <!-- 自定义slot -->
+    <template v-if="$slots.default">
+      <slot></slot>
+    </template>
+  </view>
+</template>
+<script lang="ts">
+import { toRefs, computed } from 'vue';
+import { createComponent } from '../../utils/create';
+const { componentName, create } = createComponent('empty');
+
+type statusOptions = {
+  [key: string]: string;
+};
+
+/**
+ * 内置图片地址
+ */
+const defaultStatus: statusOptions = {
+  empty: 'https://static-ftcms.jd.com/p/files/61a9e3183985005b3958672b.png',
+  error: 'https://ftcms.jd.com/p/files/61a9e33ee7dcdbcc0ce62736.png',
+  network: 'https://static-ftcms.jd.com/p/files/61a9e31de7dcdbcc0ce62734.png'
+};
+
+export default create({
+  props: {
+    image: {
+      type: String,
+      default: 'empty' //默认empty
+    },
+    imageSize: {
+      type: [Number, String], // 图片大小,正方形
+      default: ''
+    },
+    description: {
+      type: String, // 文字区
+      default: '无内容'
+    }
+  },
+
+  setup(props) {
+    const { image, imageSize } = toRefs(props);
+
+    /**
+     * 根据imgSize计算行内样式
+     */
+    const imgStyle = computed(() => {
+      if (!imageSize.value) {
+        return '';
+      }
+      if (typeof imageSize.value === 'number') {
+        return `width:${imageSize.value}px;height:${imageSize.value}px`;
+      }
+      return `width:${imageSize.value};height:${imageSize.value}`;
+    });
+
+    // 是否 URL
+    const isHttpUrl =
+      image.value.startsWith('https://') || image.value.startsWith('http://') || image.value.startsWith('//');
+    const imageUrl = isHttpUrl ? image.value : defaultStatus[image.value];
+
+    return {
+      imageUrl,
+      imgStyle
+    };
+  }
+});
+</script>

+ 87 - 0
src/packages/__VUE/empty/index.vue

@@ -0,0 +1,87 @@
+<template>
+  <view class="nut-empty">
+    <!-- 占位图 -->
+    <view class="nut-empty-image" :style="imgStyle">
+      <template v-if="$slots.image">
+        <slot name="image"></slot>
+      </template>
+      <template v-else>
+        <img v-if="imageUrl" class="img" :src="imageUrl" />
+      </template>
+    </view>
+
+    <!-- 文本区 -->
+    <template v-if="$slots.description">
+      <slot name="description"></slot>
+    </template>
+    <template v-else>
+      <view class="nut-empty-description">{{ description }}</view>
+    </template>
+
+    <!-- 自定义slot -->
+    <template v-if="$slots.default">
+      <slot></slot>
+    </template>
+  </view>
+</template>
+<script lang="ts">
+import { toRefs, computed } from 'vue';
+import { createComponent } from '../../utils/create';
+const { componentName, create } = createComponent('empty');
+
+type statusOptions = {
+  [key: string]: string;
+};
+
+/**
+ * 内置图片地址
+ */
+const defaultStatus: statusOptions = {
+  empty: 'https://static-ftcms.jd.com/p/files/61a9e3183985005b3958672b.png',
+  error: 'https://ftcms.jd.com/p/files/61a9e33ee7dcdbcc0ce62736.png',
+  network: 'https://static-ftcms.jd.com/p/files/61a9e31de7dcdbcc0ce62734.png'
+};
+
+export default create({
+  props: {
+    image: {
+      type: String,
+      default: 'empty' //默认empty
+    },
+    imageSize: {
+      type: [Number, String], // 图片大小,正方形
+      default: ''
+    },
+    description: {
+      type: String, // 文字区
+      default: '无内容'
+    }
+  },
+
+  setup(props) {
+    const { image, imageSize } = toRefs(props);
+
+    /**
+     * 根据imgSize计算行内样式
+     */
+    const imgStyle = computed(() => {
+      if (!imageSize.value) {
+        return '';
+      }
+      if (typeof imageSize.value === 'number') {
+        return `width:${imageSize.value}px;height:${imageSize.value}px`;
+      }
+      return `width:${imageSize.value};height:${imageSize.value}`;
+    });
+
+    const isHttpUrl =
+      image.value.startsWith('https://') || image.value.startsWith('http://') || image.value.startsWith('//');
+    const imageUrl = isHttpUrl ? image.value : defaultStatus[image.value];
+
+    return {
+      imageUrl,
+      imgStyle
+    };
+  }
+});
+</script>

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

@@ -394,6 +394,15 @@ $searchbar-background: $white !default;
 $searchbar-input-background: #f7f7f7 !default;
 $searchbar-right-out-color: $black !default;
 
+// empty
+$empty-padding: 32px 0;
+$empty-image-size: 170px;
+$empty-description-margin-top: 4px;
+$empty-description-color: #666666;
+$empty-description-font-size: 14px;
+$empty-description-line-height: 20px;
+$empty-description-padding: 0 40px;
+
 // cascader
 $nut-cascader-font-size: $font-size-2;
 $nut-cascader-line-height: 22px;

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

@@ -15,6 +15,7 @@ export default {
         'pages/dialog/index',
         'pages/toast/index',
         'pages/notify/index',
+        'pages/empty/index',
         'pages/noticebar/index',
         'pages/range/index',
         'pages/popup/index',

+ 3 - 0
src/sites/mobile-taro/vue/src/feedback/pages/empty/index.config.ts

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

+ 62 - 0
src/sites/mobile-taro/vue/src/feedback/pages/empty/index.vue

@@ -0,0 +1,62 @@
+<template>
+  <div class="demo">
+    <h2>基础用法</h2>
+    <div class="show">
+      <nut-empty description="无数据"></nut-empty>
+    </div>
+
+    <h2>图片类型,内置3个</h2>
+    <div class="show">
+      <nut-tabs v-model="tabValue">
+        <nut-tabpane title="无内容">
+          <nut-empty image="empty" description="无内容"></nut-empty>
+        </nut-tabpane>
+        <nut-tabpane title="加载失败/错误">
+          <nut-empty image="error" description="加载失败/错误"></nut-empty>
+        </nut-tabpane>
+        <nut-tabpane title="无网络">
+          <nut-empty image="network" description="无网络"></nut-empty>
+        </nut-tabpane>
+      </nut-tabs>
+    </div>
+
+    <h2>自定义图片</h2>
+    <div class="show">
+      <nut-empty description="无优惠券">
+        <template #image>
+          <img src="https://static-ftcms.jd.com/p/files/61a9e3313985005b3958672e.png" />
+        </template>
+      </nut-empty>
+    </div>
+
+    <h2>底部内容</h2>
+    <div class="show">
+      <nut-empty image="error" description="加载失败">
+        <div style="margin-top: 10px">
+          <nut-button icon="refresh" type="primary">重试</nut-button>
+        </div>
+      </nut-empty>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { ref } from 'vue';
+export default {
+  props: {},
+  setup() {
+    const tabValue = ref(0);
+    return {
+      tabValue
+    };
+  }
+};
+</script>
+
+<style lang="scss">
+.demo {
+  .show {
+    background: #ffffff;
+  }
+}
+</style>