Browse Source

Merge branch 'next' of https://github.com/jdf2e/nutui into next

Drjnigfubo 4 years ago
parent
commit
42a5c1372c

+ 5 - 3
README.md

@@ -1,7 +1,7 @@
 # NutUI 3
 ![npm version](https://img.shields.io/npm/v/@nutui/nutui.svg)  [![Build Status](https://api.travis-ci.org/jdf2e/nutui.svg?branch=master)](https://github.com/jdf2e/nutui/) [![Coverage Status](https://coveralls.io/repos/github/jdf2e/nutui/badge.svg?branch=master)](https://coveralls.io/github/jdf2e/nutui?branch=master) ![license](https://img.shields.io/npm/l/@nutui/nutui.svg)
  
-京东风格的轻量级移动端 Vue3 组件库
+京东风格的轻量级移动端 Vue2、Vue3 组件库(支持小程序开发)
     
 ![NutUI](https://img14.360buyimg.com/imagetools/s200x200_jfs/t1/167902/2/8762/791358/603742d7E9b4275e3/e09d8f9a8bf4c0ef.png)
 
@@ -9,6 +9,7 @@
 ## 特性
 
 * 70+ 高质量组件(3.0 持续开发中)
+* 支持小程序开发
 * 基于京东APP 10.0 视觉规范
 * 支持按需引用
 * 详尽的文档和示例
@@ -17,9 +18,10 @@
 * 支持定制主题
 * 单元测试覆盖(3.0 开发中)
 
-## 示例
+## 示例 H5 & 小程序
 
-<img src="https://img12.360buyimg.com/imagetools/jfs/t1/162421/39/13392/9425/6052ea60E592310a9/264bdff23ef5fe95.png" width="200" alt="NutUI">
+<img src="https://img12.360buyimg.com/imagetools/jfs/t1/162421/39/13392/9425/6052ea60E592310a9/264bdff23ef5fe95.png" width="200" alt="NutUI" />
+<img src="https://storage.360buyimg.com/jdc-article/gh_f2231eb941be_258.jpg" width="200" alt="NutUI" />
 
 ## 支持环境
 

+ 2 - 1
package.json

@@ -1,7 +1,7 @@
 {
   "name": "@nutui/nutui",
   "version": "3.1.1",
-  "description": "京东风格的轻量级移动端 Vue 组件库",
+  "description": "京东风格的轻量级移动端 Vue2、Vue3 组件库(支持小程序开发)",
   "main": "dist/nutui.umd.js",
   "module": "dist/nutui.es.js",
   "style": "dist/style.css",
@@ -94,6 +94,7 @@
     "pinyin": "^2.10.2",
     "prettier": "^2.0.0",
     "standard-version": "^9.3.0",
+    "swiper": "5.3.6",
     "transliteration": "^2.2.0",
     "ts-jest": "^26.5.5",
     "typescript": "^4.1.5",

+ 12 - 6
src/config.json

@@ -115,7 +115,6 @@
           "version": "3.0.0",
           "name": "Cell",
           "taro": true,
-          "tarodoc": true,
           "sort": 1,
           "cName": "单元格",
           "type": "component",
@@ -133,7 +132,6 @@
           "desc": "展示分组列表",
           "author": "richard1015"
         },
-        
         {
           "version": "3.0.0",
           "name": "Icon",
@@ -156,7 +154,6 @@
           "desc": "价格组件",
           "author": "ailululu"
         },
-       
         {
           "version": "3.0.0",
           "name": "OverLay",
@@ -213,6 +210,17 @@
       "packages": [
         {
           "version": "3.0.0",
+          "name": "Swipe",
+          "taro": true,
+          "type": "component",
+          "cName": "滑动手势操作",
+          "desc": "列表项左滑删除场景使用",
+          "sort": 4,
+          "show": true,
+          "author": "richard1015"
+        },
+        {
+          "version": "3.0.0",
           "name": "ActionSheet",
           "taro": true,
           "sort": "1",
@@ -325,7 +333,6 @@
           "show": false,
           "author": "yangxiaolu3"
         },
-    
         {
           "version": "3.0.0",
           "name": "Video",
@@ -413,7 +420,6 @@
           "desc": "轻提示",
           "author": "undo"
         }
-       
       ]
     },
     {
@@ -682,4 +688,4 @@
       ]
     }
   ]
-}
+}

+ 1 - 0
src/packages/__VUE/cell/demo.vue

@@ -8,6 +8,7 @@
       desc="描述文字"
     ></nut-cell>
     <nut-cell title="点击测试" @click="testClick"></nut-cell>
+    <nut-cell title="圆角设置 0" round-radius="0"></nut-cell>
 
     <h2>直接使用插槽(slot)</h2>
 

+ 15 - 13
src/packages/__VUE/cell/doc.md

@@ -8,10 +8,10 @@
 
 ``` javascript
 import { createApp } from 'vue';
-import { Cell } from '@nutui/nutui';
+import { Cell,Icon } from '@nutui/nutui';
 
 const app = createApp();
-app.use(Cell);
+app.use(Cell).use(Icon);
 
 ```
 
@@ -23,6 +23,7 @@ app.use(Cell);
 <nut-cell title="我是标题" desc="描述文字"></nut-cell>
 <nut-cell title="我是标题" sub-title="副标题描述" desc="描述文字"></nut-cell>
 <nut-cell title="点击测试" @click="testClick"></nut-cell>
+<nut-cell title="圆角设置 0" round-radius="0"></nut-cell>
 ```
 
 ``` javascript
@@ -87,17 +88,18 @@ export default {
 
 ### Prop
 
-| 字段            | 说明                                                                                           | 类型    | 默认值 |
-|-----------------|------------------------------------------------------------------------------------------------|---------|--------|
-| title           | 标题名称                                                                                       | String  | -      |
-| sub-title       | 左侧副标题                                                                                     | String  | -      |
-| desc            | 右侧描述                                                                                       | String  | -      |
-| desc-text-align | 右侧描述文本对齐方式 [text-align](https://www.w3school.com.cn/cssref/pr_text_text-align.asp)   | String  | right  |
-| is-link         | 是否展示右侧箭头并开启点击反馈                                                                 | Boolean | false  |
-| icon            | 左侧 [图标名称](#/icon) 或图片链接                                                             | String  | -      |
-| url             | 点击后跳转的链接地址                                                                           | String  | -      |
-| to              | 点击后跳转的目标路由对象,同 vue-router 的 [to 属性](https://router.vuejs.org/zh/api/#to) 属性 | String  | -      |
-| replace         | 是否在跳转时替换当前页面历史                                                                   | Boolean | false  |
+| 字段                   | 说明                                                                                           | 类型    | 默认值 |
+|------------------------|------------------------------------------------------------------------------------------------|---------|--------|
+| title                  | 标题名称                                                                                       | String  | -      |
+| sub-title              | 左侧副标题                                                                                     | String  | -      |
+| desc                   | 右侧描述                                                                                       | String  | -      |
+| desc-text-align        | 右侧描述文本对齐方式 [text-align](https://www.w3school.com.cn/cssref/pr_text_text-align.asp)   | String  | right  |
+| is-link                | 是否展示右侧箭头并开启点击反馈                                                                 | Boolean | false  |
+| icon                   | 左侧 [图标名称](#/icon) 或图片链接                                                             | String  | -      |
+| round-radius           | 圆角半径                                                                                       | Number  | 6px    |
+| url `小程序不支持`     | 点击后跳转的链接地址                                                                           | String  | -      |
+| to   `小程序不支持`    | 点击后跳转的目标路由对象,同 vue-router 的 [to 属性](https://router.vuejs.org/zh/api/#to) 属性 | String  | -      |
+| replace `小程序不支持` | 是否在跳转时替换当前页面历史                                                                   | Boolean | false  |
 
 ### Event
 

+ 0 - 104
src/packages/__VUE/cell/doc.taro.md

@@ -1,104 +0,0 @@
-# Cell 单元格
-
-### 介绍
-
-列表项,可组成列表。
-
-### 安装
-
-``` javascript
-import { createApp } from 'vue';
-import { Cell } from '@nutui/nutui-taro';
-
-const app = createApp();
-app.use(Cell);
-
-```
-
-## 代码示例
-
-### 基本用法
-
-``` html
-<nut-cell title="我是标题" desc="描述文字"></nut-cell>
-<nut-cell title="我是标题" sub-title="副标题描述" desc="描述文字"></nut-cell>
-<nut-cell title="点击测试" @click="testClick"></nut-cell>
-```
-
-``` javascript
-// ...
-import { ref } from 'vue';
-export default {
-  setup() {
-      const switchChecked = ref(true);
-      const testClick = (event) => {
-        console.log('点击事件')
-      };
-      return { testClick,switchChecked };
-  }
-}
-// ...
-```
-
-### 直接使用插槽
-
-``` html
- <nut-cell title="我是标题" desc="描述文字">
-  <div>自定义内容</div>
- </nut-cell>  
-```
-
-### 链接 | 分组用法
-
-``` html
-<nut-cell-group title="链接 | 分组用法">
-  <nut-cell title="链接" is-link></nut-cell>
-  <nut-cell title="URL 跳转" desc="https://jd.com" is-link url="https://jd.com"></nut-cell>
-  <nut-cell title="路由跳转 ’/‘ " to="/"></nut-cell>
-</nut-cell-group>
-```
-
-### 自定义右侧箭头区域
-
-``` html
-<nut-cell-group title="自定义右侧箭头区域">
-  <nut-cell title="Switch">
-    <template v-slot:link>
-      <nut-switch v-model="switchChecked" />
-    </template>
-  </nut-cell>
-</nut-cell-group>
-```
-
-
-### 单元格展示图标
-
-``` html
-<nut-cell title="姓名" icon="my" desc="张三"></nut-cell>
-```
-### 只展示 desc ,可通过 desc-text-align 调整内容位置
-
-``` html
-<nut-cell desc-text-align="left" desc="张三"></nut-cell>
-```
-
-## API
-
-### Prop
-
-| 字段            | 说明                                                                                           | 类型    | 默认值 |
-|-----------------|------------------------------------------------------------------------------------------------|---------|--------|
-| title           | 标题名称                                                                                       | String  | -      |
-| sub-title       | 左侧副标题                                                                                     | String  | -      |
-| desc            | 右侧描述                                                                                       | String  | -      |
-| desc-text-align | 右侧描述文本对齐方式 [text-align](https://www.w3school.com.cn/cssref/pr_text_text-align.asp)   | String  | right  |
-| is-link         | 是否展示右侧箭头并开启点击反馈                                                                 | Boolean | false  |
-| icon            | 左侧 [图标名称](#/icon) 或图片链接                                                             | String  | -      |
-
-### Event
-
-| 名称  | 说明     | 回调参数    |
-|-------|----------|-------------|
-| click | 点击事件 | event:Event |
-
-

+ 12 - 2
src/packages/__VUE/cell/index.taro.vue

@@ -1,5 +1,5 @@
 <template>
-  <view :class="classes" @click="handleClick">
+  <view :class="classes" :style="baseStyle" @click="handleClick">
     <slot>
       <view
         class="nut-cell__title"
@@ -37,6 +37,7 @@ import { computed } from 'vue';
 import { createComponent } from '../../utils/create';
 import { useRouter } from 'vue-router';
 import CellGroup from '../cellgroup/index.vue';
+import { pxCheck } from '../../utils/pxCheck';
 const { componentName, create } = createComponent('cell');
 export default create({
   children: [CellGroup],
@@ -48,6 +49,7 @@ export default create({
     isLink: { type: Boolean, default: false },
     to: { type: String, default: '' },
     replace: { type: Boolean, default: false },
+    roundRadius: { type: [String, Number], default: '' },
     url: { type: String, default: '' },
     icon: { type: String, default: '' }
   },
@@ -64,6 +66,13 @@ export default create({
       };
     });
     const router = useRouter();
+
+    const baseStyle = computed(() => {
+      return {
+        borderRadius: pxCheck(props.roundRadius)
+      };
+    });
+
     const handleClick = (event: Event) => {
       emit('click', event);
 
@@ -83,7 +92,8 @@ export default create({
 
     return {
       handleClick,
-      classes
+      classes,
+      baseStyle
     };
   }
 });

+ 13 - 3
src/packages/__VUE/cell/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <view :class="classes" @click="handleClick">
+  <view :class="classes" :style="baseStyle" @click="handleClick">
     <slot>
       <view
         class="nut-cell__title"
@@ -35,8 +35,9 @@
 <script lang="ts">
 import { computed } from 'vue';
 import { createComponent } from '../../utils/create';
-import { useRoute, useRouter } from 'vue-router';
+import { useRouter } from 'vue-router';
 import CellGroup from '../cellgroup/index.vue';
+import { pxCheck } from '@/packages/utils/pxCheck';
 const { componentName, create } = createComponent('cell');
 export default create({
   props: {
@@ -47,6 +48,7 @@ export default create({
     isLink: { type: Boolean, default: false },
     to: { type: String, default: '' },
     replace: { type: Boolean, default: false },
+    roundRadius: { type: [String, Number], default: '' },
     url: { type: String, default: '' },
     icon: { type: String, default: '' }
   },
@@ -64,6 +66,13 @@ export default create({
       };
     });
     const router = useRouter();
+
+    const baseStyle = computed(() => {
+      return {
+        borderRadius: pxCheck(props.roundRadius)
+      };
+    });
+
     const handleClick = (event: Event) => {
       emit('click', event);
 
@@ -83,7 +92,8 @@ export default create({
 
     return {
       handleClick,
-      classes
+      classes,
+      baseStyle
     };
   }
 });

+ 109 - 0
src/packages/__VUE/swipe/demo.vue

@@ -0,0 +1,109 @@
+<template>
+  <div class="demo full">
+    <h2>基础用法</h2>
+    <nut-swipe>
+      <nut-cell round-radius="0" desc="左滑删除" />
+      <template #right>
+        <nut-button shape="square" style="height: 100%" type="danger"
+          >删除</nut-button
+        >
+      </template>
+    </nut-swipe>
+    <h2>禁止滑动</h2>
+    <nut-swipe disabled>
+      <nut-cell round-radius="0" desc="禁止滑动" />
+      <template #right>
+        <nut-button shape="square" style="height: 100%" type="danger"
+          >删除</nut-button
+        >
+      </template>
+    </nut-swipe>
+    <h2>左右滑动</h2>
+    <nut-swipe>
+      <template #left>
+        <nut-button shape="square" style="height: 100%" type="success"
+          >选择</nut-button
+        >
+      </template>
+      <nut-cell round-radius="0" desc="左滑右滑都可以哦" />
+      <template #right>
+        <nut-button shape="square" style="height: 100%" type="danger"
+          >删除</nut-button
+        >
+        <nut-button shape="square" style="height: 100%" type="info"
+          >收藏</nut-button
+        >
+      </template>
+    </nut-swipe>
+    <h2>异步控制</h2>
+    <nut-swipe ref="refSwipe" @open="open" @close="close">
+      <nut-cell title="异步打开关闭">
+        <template v-slot:link>
+          <nut-switch
+            v-model="checked"
+            @change="changSwitch"
+            active-text="开"
+            inactive-text="关"
+          />
+        </template>
+      </nut-cell>
+      <template #right>
+        <nut-button shape="square" style="height: 100%" type="danger"
+          >删除</nut-button
+        >
+      </template>
+    </nut-swipe>
+    <h2>自定义</h2>
+    <nut-swipe>
+      <template #left>
+        <nut-button shape="square" style="height: 100%" type="success"
+          >选择</nut-button
+        >
+      </template>
+      <nut-cell title="商品描述">
+        <template v-slot:link>
+          <nut-inputnumber v-model="number" />
+        </template>
+      </nut-cell>
+      <template #right>
+        <nut-button shape="square" style="height: 100%" type="danger"
+          >删除</nut-button
+        >
+        <nut-button shape="square" style="height: 100%" type="info"
+          >收藏</nut-button
+        >
+      </template>
+    </nut-swipe>
+  </div>
+</template>
+
+<script lang="ts">
+import { ref } from 'vue';
+import { createComponent } from '../../utils/create';
+const { createDemo } = createComponent('swipe');
+export default createDemo({
+  props: {},
+  setup() {
+    const refSwipe = ref<HTMLElement>();
+    const checked = ref(false);
+    const number = ref(0);
+    const changSwitch = (value: boolean) => {
+      if (value) {
+        refSwipe.value?.open('left');
+      } else {
+        refSwipe.value?.close();
+      }
+    };
+    const open = (obj: any) => {
+      console.log(obj);
+      checked.value = true;
+    };
+    const close = () => {
+      checked.value = false;
+    };
+    return { checked, number, changSwitch, refSwipe, open, close };
+  }
+});
+</script>
+
+<style lang="scss" scoped></style>

+ 154 - 0
src/packages/__VUE/swipe/doc.md

@@ -0,0 +1,154 @@
+#  swipe组件
+
+### 介绍
+
+常用于单元格左滑删除等手势操作
+
+### 安装
+
+``` javascript
+import { createApp } from 'vue';
+//vue
+import { Swipe } from '@nutui/nutui';
+//taro
+import { Swipe } from '@nutui/nutui-taro';
+
+const app = createApp();
+app.use(Swipe);
+```
+
+## 代码演示
+
+### 基础用法
+
+``` html
+<nut-swipe>
+    <nut-cell round-radius="0" desc="左滑删除" />
+    <template #right>
+        <nut-button shape="square" style="height:100%" type="danger">删除</nut-button>
+    </template>
+</nut-swipe>
+```
+
+
+### 禁止滑动
+
+``` html
+<nut-swipe disabled>
+    <nut-cell round-radius="0" desc="禁止滑动" />
+    <template #right>
+        <nut-button shape="square" style="height:100%" type="danger">删除</nut-button>
+    </template>
+</nut-swipe>
+```
+
+
+### 左右滑动
+
+``` html
+<nut-swipe>
+    <template #left>
+        <nut-button shape="square" style="height:100%" type="success">选择</nut-button>
+    </template>
+    <nut-cell round-radius="0" desc="左滑右滑都可以哦" />
+    <template #right>
+        <nut-button shape="square" style="height:100%" type="danger">删除</nut-button>
+        <nut-button shape="square" style="height:100%" type="info">收藏</nut-button>
+    </template>
+</nut-swipe>
+```
+
+
+### 异步控制
+
+``` html
+<nut-swipe ref="refSwipe" @open="open" @close="close">
+    <nut-cell title="异步打开关闭">
+    <template v-slot:link>
+        <nut-switch v-model="checked" @change="changSwitch" active-text="开" inactive-text="关" />
+    </template>
+    </nut-cell>
+    <template #right>
+        <nut-button shape="square" style="height:100%" type="danger">删除</nut-button>
+    </template>
+</nut-swipe>
+```
+``` typescript
+    setup() {
+        const refSwipe = ref<HTMLElement>();
+        const checked = ref(false);
+        const changSwitch = (value: boolean) => {
+            if (value) {
+                refSwipe.value?.open('left');
+            } else {
+                refSwipe.value?.close();
+            }
+        };
+         const open = (obj: any) => {
+            console.log(obj);
+            checked.value = true;
+        };
+        const close = () => {
+            checked.value = false;
+        };
+        return { checked, changSwitch, refSwipe, open, close };
+    }
+```
+
+### 自定义
+
+``` html
+<nut-swipe>
+    <template #left>
+        <nut-button shape="square" style="height:100%" type="success">选择</nut-button>
+    </template>
+    <nut-cell title="商品描述">
+    <template v-slot:link>
+        <nut-inputnumber v-model="number" />
+    </template>
+    </nut-cell>
+    <template #right>
+        <nut-button shape="square" style="height:100%" type="danger">删除</nut-button>
+        <nut-button shape="square" style="height:100%" type="info">收藏</nut-button>
+    </template>
+</nut-swipe>
+```
+
+``` typescript
+    setup() {
+        const number = ref(0);
+        return { number };
+    }
+```
+
+
+
+### Props
+
+| 参数     | 说明         | 类型   | 默认值 |
+|----------|--------------|--------|--------|
+| name     | 唯一标识     | String | -      |
+| disabled | 是否禁用滑动 | String | false  |
+
+### Events
+
+| 事件名 | 说明       | 回调参数               |
+|--------|------------|------------------------|
+| open   | 打开时触发 | {type:'left or right'} |
+| close  | 关闭时触发 | {type:'left or right'} |
+    
+
+### Slots
+| 名称    | 说明         |
+|---------|--------------|
+| left    | 左侧滑动内容 |
+| default | 自定义内容   |
+| right   | 右侧滑动内容 |
+
+### 方法
+通过 ref 可以获取到 Swipe 实例并调用实例方法。
+
+| 方法名 | 说明             | 参数          |
+|--------|------------------|---------------|
+| open   | 打开单元格侧边栏 | left or right |
+| close  | 收起单元格侧边栏 |               |

+ 22 - 0
src/packages/__VUE/swipe/index.scss

@@ -0,0 +1,22 @@
+.nut-swipe {
+  position: relative;
+  display: block;
+  transition: all 0.3s linear;
+  &__left,
+  &__right {
+    position: absolute;
+    top: 0;
+    height: 100%;
+  }
+  &__left {
+    left: 0;
+    transform: translate3d(-100%, 0, 0);
+  }
+  &__right {
+    right: 0;
+    transform: translate3d(100%, 0, 0);
+  }
+  &__content {
+    display: inherit;
+  }
+}

+ 200 - 0
src/packages/__VUE/swipe/index.taro.vue

@@ -0,0 +1,200 @@
+<template>
+  <view
+    :class="classes"
+    :style="touchStyle"
+    @touchstart="onTouchStart"
+    @touchmove="onTouchMove"
+    @touchend="onTouchEnd"
+    @touchcancel="onTouchEnd"
+  >
+    <view class="nut-swipe__left" ref="leftRef" :id="'leftRef-' + refRandomId">
+      <slot name="left"></slot>
+    </view>
+
+    <view class="nut-swipe__content">
+      <slot name="default"></slot>
+    </view>
+
+    <view
+      class="nut-swipe__right"
+      ref="rightRef"
+      :id="'rightRef-' + refRandomId"
+    >
+      <slot name="right"></slot>
+    </view>
+  </view>
+</template>
+<script lang="ts">
+import Taro from '@tarojs/taro';
+import { useTouch } from '../../utils/useTouch';
+import { computed, onMounted, reactive, Ref, ref } from 'vue';
+import { createComponent } from '../../utils/create';
+import { useTaroRect } from '../../utils/useTaroRect';
+const { componentName, create } = createComponent('swipe');
+export type SwipePosition = 'left' | 'right' | '';
+export default create({
+  props: {
+    name: {
+      type: String,
+      default: ''
+    },
+    disabled: {
+      type: Boolean,
+      default: false
+    }
+  },
+  emits: ['open', 'close'],
+
+  setup(props, { emit }) {
+    const refRandomId = Math.random().toString(36).slice(-8);
+    const classes = computed(() => {
+      const prefixCls = componentName;
+      return {
+        [prefixCls]: true
+      };
+    });
+
+    const getRefWidth = async (ref: Ref<HTMLElement | undefined>) => {
+      if (Taro.getEnv() === 'WEB') {
+        return ref.value?.clientWidth || ref.value?.$el?.clientWidth || 0;
+      } else {
+        let rect = await useTaroRect(ref, Taro);
+        return rect.width || 0;
+      }
+    };
+
+    const leftRef = ref<HTMLElement>();
+    const leftRefWidth = ref(0);
+    const rightRef = ref<HTMLElement>();
+    const rightRefWidth = ref(0);
+
+    const initWidth = async () => {
+      leftRefWidth.value = await getRefWidth(leftRef);
+      rightRefWidth.value = await getRefWidth(rightRef);
+    };
+
+    onMounted(() => {
+      Taro.nextTick(initWidth);
+    });
+
+    let opened: boolean = false;
+    let position: SwipePosition = '';
+    let oldPosition: SwipePosition = '';
+
+    const state = reactive({
+      offset: 0,
+      moving: false
+    });
+
+    const open = (p: SwipePosition = '') => {
+      opened = true;
+      if (p) {
+        state.offset = p === 'left' ? -rightRefWidth.value : leftRefWidth.value;
+      }
+      emit('open', {
+        name: props.name,
+        position: position || p
+      });
+    };
+
+    const close = () => {
+      state.offset = 0;
+      opened = false;
+      emit('close', {
+        name: props.name,
+        position
+      });
+    };
+
+    const touchStyle = computed(() => {
+      return {
+        transform: `translate3d(${state.offset}px, 0, 0)`
+      };
+    });
+
+    const setoffset = (deltaX: number) => {
+      position = deltaX > 0 ? 'right' : 'left';
+      let offset = deltaX;
+      switch (position) {
+        case 'left':
+          if (opened && oldPosition === position) {
+            offset = -rightRefWidth.value;
+          } else {
+            offset =
+              Math.abs(deltaX) > rightRefWidth.value
+                ? -rightRefWidth.value
+                : deltaX;
+          }
+          break;
+        case 'right':
+          if (opened && oldPosition === position) {
+            offset = leftRefWidth.value;
+          } else {
+            offset =
+              Math.abs(deltaX) > leftRefWidth.value
+                ? leftRefWidth.value
+                : deltaX;
+          }
+          break;
+      }
+      state.offset = offset;
+    };
+
+    const touch = useTouch();
+    const touchMethods = {
+      onTouchStart(event: Event) {
+        if (props.disabled) return;
+        touch.start(event);
+      },
+      async onTouchMove(event: Event) {
+        if (props.disabled) return;
+        if (touch.isVertical()) return;
+        state.moving = true;
+        touch.move(event);
+        setoffset(touch.deltaX.value);
+
+        event.preventDefault();
+      },
+      onTouchEnd() {
+        if (state.moving) {
+          state.moving = false;
+          oldPosition = position;
+          switch (position) {
+            case 'left':
+              if (Math.abs(state.offset) <= rightRefWidth.value / 2) {
+                close();
+              } else {
+                state.offset = -rightRefWidth.value;
+                open();
+              }
+              break;
+            case 'right':
+              if (Math.abs(state.offset) <= leftRefWidth.value / 2) {
+                close();
+              } else {
+                state.offset = leftRefWidth.value;
+                open();
+              }
+              break;
+          }
+        }
+      }
+    };
+
+    return {
+      classes,
+      touchStyle,
+      ...touchMethods,
+      leftRef,
+      rightRef,
+      refRandomId,
+      open,
+      close
+    };
+  }
+});
+</script>
+
+<style lang="scss">
+@import 'index.scss';
+</style>

+ 183 - 0
src/packages/__VUE/swipe/index.vue

@@ -0,0 +1,183 @@
+<template>
+  <view
+    :class="classes"
+    :style="touchStyle"
+    @touchstart="onTouchStart"
+    @touchmove="onTouchMove"
+    @touchend="onTouchEnd"
+    @touchcancel="onTouchEnd"
+  >
+    <view class="nut-swipe__left" ref="leftRef">
+      <slot name="left"></slot>
+    </view>
+
+    <view class="nut-swipe__content">
+      <slot name="default"></slot>
+    </view>
+
+    <view class="nut-swipe__right" ref="rightRef">
+      <slot name="right"></slot>
+    </view>
+  </view>
+</template>
+<script lang="ts">
+import { useTouch } from '@/packages/utils/useTouch';
+import { computed, reactive, Ref, ref } from 'vue';
+import { createComponent } from '../../utils/create';
+const { componentName, create } = createComponent('swipe');
+
+export type SwipePosition = 'left' | 'right' | '';
+export default create({
+  props: {
+    name: {
+      type: String,
+      default: ''
+    },
+    disabled: {
+      type: Boolean,
+      default: false
+    }
+  },
+  emits: ['open', 'close'],
+
+  setup(props, { emit }) {
+    const classes = computed(() => {
+      const prefixCls = componentName;
+      return {
+        [prefixCls]: true
+      };
+    });
+
+    const getRefWidth = (ref: Ref<HTMLElement | undefined>): number => {
+      return ref.value?.clientWidth || 0;
+    };
+
+    const leftRef = ref<HTMLElement>(),
+      leftRefWidth = computed(() => {
+        return getRefWidth(leftRef);
+      });
+    const rightRef = ref<HTMLElement>(),
+      rightRefWidth = computed(() => {
+        return getRefWidth(rightRef);
+      });
+
+    let opened: boolean = false;
+    let position: SwipePosition = '';
+    let oldPosition: SwipePosition = '';
+
+    const state = reactive({
+      offset: 0,
+      moving: false
+    });
+
+    const open = (p: SwipePosition = '') => {
+      opened = true;
+      if (p) {
+        state.offset = p === 'left' ? -rightRefWidth.value : leftRefWidth.value;
+      }
+      emit('open', {
+        name: props.name,
+        position: position || p
+      });
+    };
+
+    const close = () => {
+      state.offset = 0;
+      opened = false;
+      emit('close', {
+        name: props.name,
+        position
+      });
+    };
+
+    const touchStyle = computed(() => {
+      return {
+        transform: `translate3d(${state.offset}px, 0, 0)`
+      };
+    });
+
+    const setoffset = (deltaX: number) => {
+      position = deltaX > 0 ? 'right' : 'left';
+      let offset = deltaX;
+      switch (position) {
+        case 'left':
+          if (opened && oldPosition === position) {
+            offset = -rightRefWidth.value;
+          } else {
+            offset =
+              Math.abs(deltaX) > rightRefWidth.value
+                ? -rightRefWidth.value
+                : deltaX;
+          }
+          break;
+        case 'right':
+          if (opened && oldPosition === position) {
+            offset = leftRefWidth.value;
+          } else {
+            offset =
+              Math.abs(deltaX) > leftRefWidth.value
+                ? leftRefWidth.value
+                : deltaX;
+          }
+          break;
+      }
+      state.offset = offset;
+    };
+
+    const touch = useTouch();
+    const touchMethods = {
+      onTouchStart(event: Event) {
+        if (props.disabled) return;
+        touch.start(event);
+      },
+      onTouchMove(event: Event) {
+        if (props.disabled) return;
+        if (touch.isVertical()) return;
+        state.moving = true;
+        touch.move(event);
+        setoffset(touch.deltaX.value);
+
+        event.preventDefault();
+      },
+      onTouchEnd() {
+        if (state.moving) {
+          state.moving = false;
+          oldPosition = position;
+          switch (position) {
+            case 'left':
+              if (Math.abs(state.offset) <= rightRefWidth.value / 2) {
+                close();
+              } else {
+                state.offset = -rightRefWidth.value;
+                open();
+              }
+              break;
+            case 'right':
+              if (Math.abs(state.offset) <= leftRefWidth.value / 2) {
+                close();
+              } else {
+                state.offset = leftRefWidth.value;
+                open();
+              }
+              break;
+          }
+        }
+      }
+    };
+
+    return {
+      classes,
+      touchStyle,
+      ...touchMethods,
+      leftRef,
+      rightRef,
+      open,
+      close
+    };
+  }
+});
+</script>
+
+<style lang="scss">
+@import 'index.scss';
+</style>

+ 1 - 1
src/packages/styles/variables.scss

@@ -100,7 +100,7 @@ $cell-title-font: $font-size-2;
 $cell-title-desc-font: $font-size-1;
 $cell-desc-font: $font-size-2;
 $cell-desc-color: $disable-color;
-$cell-border-radius: 7px;
+$cell-border-radius: 6px;
 
 // cell-group
 

+ 102 - 64
src/sites/doc/views/Main.vue

@@ -94,22 +94,33 @@
             <p>{{ currentCaseItem.product_info }}</p>
             <img :src="currentCaseItem.logo" />
           </div>
-          <div class="doc-content-cases-content__main-iphone"></div>
+          <div
+            class="doc-content-cases-content__main-iphone"
+            :style="{
+              backgroundImage: 'url(' + currentCaseItem.cover_image + ')'
+            }"
+          >
+            <img src="../../assets/images/iphone-cases.png" alt="" srcset="" />
+          </div>
           <div
             class="doc-content-cases-content__main-righticon"
             @click="onRight"
           ></div>
         </div>
-        <ul
-          :class="[
-            'doc-content-cases-content__list',
-            themeNameValue() == 'black' ? 'noShadow' : ''
-          ]"
-        >
-          <li v-for="(img, index) in casesImages" :key="index">
-            <img :src="img" />
-          </li>
-        </ul>
+        <div class="doc-content-cases-content__list">
+          <div
+            class="swiper-wrapper"
+            :class="[themeNameValue() == 'black' ? 'noShadow' : '']"
+          >
+            <div
+              class="swiper-slide"
+              v-for="(item, index) in casesImages"
+              :key="index"
+            >
+              <img :src="item.cover_image" />
+            </div>
+          </div>
+        </div>
       </div>
     </div>
     <div class="doc-content-more" v-if="articleList.length">
@@ -133,12 +144,21 @@
   <doc-footer></doc-footer>
 </template>
 <script lang="ts">
-import { defineComponent, onMounted, reactive, toRefs, computed } from 'vue';
+import {
+  defineComponent,
+  onMounted,
+  reactive,
+  toRefs,
+  computed,
+  ref
+} from 'vue';
 import Header from '@/sites/doc/components/Header.vue';
 import Footer from '@/sites/doc/components/Footer.vue';
 import router from '../router';
 import { RefData } from '@/sites/assets/util/ref';
 import { ApiService } from '@/sites/service/ApiService';
+import 'swiper/css/swiper.min.css';
+import Swiper from 'swiper/js/swiper.min.js';
 export default defineComponent({
   name: 'main',
   components: {
@@ -146,17 +166,15 @@ export default defineComponent({
     [Footer.name]: Footer
   },
   setup() {
-    const articleList: any[] = [];
-    let casesList: any[] = [];
-    const casesImages: string[] = [];
-    const currentCaseItem: any = {};
     const data = reactive({
       // theme: 'white',
-      articleList,
-      casesImages,
-      currentCaseItem,
+      articleList: new Array(),
+      casesImages: new Array(),
+      currentCaseItem: {},
+      currentCaseIndex: 0,
       localTheme: localStorage.getItem('nutui-theme-color')
     });
+    let caseSwiper: any = null;
     onMounted(() => {
       // 文章列表接口
       const apiService = new ApiService();
@@ -173,29 +191,47 @@ export default defineComponent({
       });
       apiService.getCases().then((res) => {
         if (res?.state == 0) {
-          data.casesImages = (res.value.data.arrays as any[])
-            .map((item) => {
-              return item.cover_image.split(',');
-            })
-            .toString()
-            .split(',');
-          casesList = res.value.data.arrays as any[];
-          data.currentCaseItem = casesList[0];
+          data.casesImages = (res.value.data.arrays as any[]).map((item) => {
+            if (item.cover_image?.length) {
+              item.cover_image = item.cover_image.split(',')[0];
+            }
+            return item;
+          });
+          if (data.casesImages?.length) {
+            data.currentCaseItem = data.casesImages[data.currentCaseIndex];
+          }
+
+          setTimeout(() => {
+            caseSwiper = new Swiper('.doc-content-cases-content__list', {
+              direction: 'horizontal',
+              slidesPerView: 'auto',
+              initialSlide: 1,
+              loop: true,
+              on: {
+                slideChange: function () {
+                  let realIndex = (this as any).realIndex;
+                  data.currentCaseIndex =
+                    realIndex === 0
+                      ? data.casesImages.length - 1
+                      : realIndex - 1;
+                  setTimeout(() => {
+                    data.currentCaseItem =
+                      data.casesImages[data.currentCaseIndex];
+                  }, 230);
+                }
+              }
+            });
+          }, 500);
         }
       });
     });
-    const findCasesItem = (url: string) => {
-      data.currentCaseItem = casesList.find((i) => i.cover_image.includes(url));
-    };
+
     const onLeft = () => {
-      let url = data.casesImages.shift() as string;
-      findCasesItem(url);
-      data.casesImages.push(url);
+      caseSwiper.slidePrev();
     };
+
     const onRight = () => {
-      let url = data.casesImages.pop() as string;
-      findCasesItem(url);
-      data.casesImages.unshift(url);
+      caseSwiper.slideNext();
     };
 
     const themeName = computed(() => {
@@ -363,10 +399,17 @@ export default defineComponent({
         &-iphone {
           width: 210px;
           height: 420px;
-          background-image: url('../../assets/images/iphone-cases.png');
           background-repeat: no-repeat;
-          background-size: 100% 100%;
-          z-index: 1;
+          background-position: center center;
+          background-size: 188px 397px;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          overflow: hidden;
+          > img {
+            width: 100%;
+            height: 100%;
+          }
         }
         &-lefticon {
           margin-right: 20px;
@@ -421,33 +464,28 @@ export default defineComponent({
       }
       &__list {
         flex: 1;
-        display: flex;
-        margin-left: -275px;
-        > li {
-          width: 180px;
-          height: 390px;
-          flex-shrink: 0;
-          margin-right: 20px;
-          transition: all 0.5s;
-          &:first-child {
-            margin-right: 139px;
-            transform: scale(1.04);
-            border-radius: 10px;
-            overflow: hidden;
-          }
-          &:nth-child(-n + 4) {
-            box-shadow: 0 4px 7px 0 rgb(144 156 164 / 80%);
-            border-radius: 3px;
+        overflow: hidden;
+        margin-left: 30px;
+        .swiper-wrapper {
+          display: flex;
+          transform: translate3d(0, 0, 0);
+          transition: all 0.6s ease;
+          .swiper-slide {
+            width: 180px;
+            height: 390px;
+            flex-shrink: 0;
+            margin-right: 20px;
+            border-radius: 4px;
             overflow: hidden;
+            > img {
+              width: 100%;
+              height: 100%;
+            }
           }
-          > img {
-            width: 100%;
-            height: 100%;
-          }
-        }
-        &.noShadow {
-          > li {
-            box-shadow: none !important;
+          &.noShadow {
+            .swiper-slide {
+              box-shadow: none !important;
+            }
           }
         }
       }

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

@@ -8,6 +8,7 @@ export default {
     {
       root: 'feedback',
       pages: [
+        'pages/swipe/index',
         'pages/collapse/index',
         'pages/backtop/index',
         'pages/actionsheet/index',

+ 1 - 0
src/sites/mobile-taro/vue/src/base/pages/cell/index.vue

@@ -8,6 +8,7 @@
       desc="描述文字"
     ></nut-cell>
     <nut-cell title="点击测试" @click="testClick"></nut-cell>
+    <nut-cell title="圆角设置 0" round-radius="0"></nut-cell>
 
     <h2>直接使用插槽(slot)</h2>
 

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

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

+ 107 - 0
src/sites/mobile-taro/vue/src/feedback/pages/swipe/index.vue

@@ -0,0 +1,107 @@
+<template>
+  <div class="demo full">
+    <h2>基础用法</h2>
+    <nut-swipe>
+      <nut-cell round-radius="0" desc="左滑删除" />
+      <template #right>
+        <nut-button shape="square" style="height: 100%" type="danger"
+          >删除</nut-button
+        >
+      </template>
+    </nut-swipe>
+    <h2>禁止滑动</h2>
+    <nut-swipe disabled>
+      <nut-cell round-radius="0" desc="禁止滑动" />
+      <template #right>
+        <nut-button shape="square" style="height: 100%" type="danger"
+          >删除</nut-button
+        >
+      </template>
+    </nut-swipe>
+    <h2>左右滑动</h2>
+    <nut-swipe>
+      <template #left>
+        <nut-button shape="square" style="height: 100%" type="success"
+          >选择</nut-button
+        >
+      </template>
+      <nut-cell round-radius="0" desc="左滑右滑都可以哦" />
+      <template #right>
+        <nut-button shape="square" style="height: 100%" type="danger"
+          >删除</nut-button
+        >
+        <nut-button shape="square" style="height: 100%" type="info"
+          >收藏</nut-button
+        >
+      </template>
+    </nut-swipe>
+    <h2>异步控制</h2>
+    <nut-swipe ref="refSwipe" @open="open" @close="close">
+      <nut-cell title="异步打开关闭">
+        <template v-slot:link>
+          <nut-switch
+            v-model="checked"
+            @change="changSwitch"
+            active-text="开"
+            inactive-text="关"
+          />
+        </template>
+      </nut-cell>
+      <template #right>
+        <nut-button shape="square" style="height: 100%" type="danger"
+          >删除</nut-button
+        >
+      </template>
+    </nut-swipe>
+    <h2>自定义</h2>
+    <nut-swipe>
+      <template #left>
+        <nut-button shape="square" style="height: 100%" type="success"
+          >选择</nut-button
+        >
+      </template>
+      <nut-cell title="商品描述">
+        <template v-slot:link>
+          <nut-inputnumber v-model="number" />
+        </template>
+      </nut-cell>
+      <template #right>
+        <nut-button shape="square" style="height: 100%" type="danger"
+          >删除</nut-button
+        >
+        <nut-button shape="square" style="height: 100%" type="info"
+          >收藏</nut-button
+        >
+      </template>
+    </nut-swipe>
+  </div>
+</template>
+
+<script lang="ts">
+import { ref } from 'vue';
+export default {
+  props: {},
+  setup() {
+    const refSwipe = ref<HTMLElement>();
+    const checked = ref(false);
+    const number = ref(0);
+    const changSwitch = (value: boolean) => {
+      if (value) {
+        refSwipe.value?.open('left');
+      } else {
+        refSwipe.value?.close();
+      }
+    };
+    const open = (obj: any) => {
+      console.log(obj);
+      checked.value = true;
+    };
+    const close = () => {
+      checked.value = false;
+    };
+    return { checked, number, changSwitch, refSwipe, open, close };
+  }
+};
+</script>
+
+<style lang="scss" scoped></style>