Browse Source

refactor(swiper): 适配小程序(#944)

suzigang 3 years ago
parent
commit
327a979eed

+ 0 - 1
src/config.json

@@ -393,7 +393,6 @@
           "version": "3.0.0",
           "name": "Swiper",
           "taro": true,
-          "tarodoc": true,
           "sort": 6,
           "cName": "轮播",
           "type": "component",

+ 3 - 0
src/packages/__VUE/swiper/doc.md

@@ -8,7 +8,10 @@
 
 ```javascript
 import { createApp } from 'vue';
+//vue
 import { Swiper,SwiperItem } from '@nutui/nutui';
+//taro
+import { Swiper,SwiperItem } from '@nutui/nutui-taro';
 
 const app = createApp();
 app.use(Swiper).use(SwiperItem);

+ 0 - 89
src/packages/__VUE/swiper/doc.taro.md

@@ -1,89 +0,0 @@
-# Swiper 轮播
-
-### 介绍
-
-常用于一组图片或卡片轮播,当内容空间不足时,可以用走马灯的形式进行收纳,进行轮播展现。
-
-<!-- ### 安装
-
-```javascript
-import { createApp } from 'vue';
-import { Swiper } from '@nutui/nutui-taro';
-
-const app = createApp();
-app.use(Swiper);
-```
-
-## 代码演示
-
-### 基础用法
-
-`autoplay` 是否自动切换
-`interval` 自动切换时间间隔
-`current` 当前所在滑块的 index
-`indicator-dots` 是否显示面板指示点
-`indicator-color` 指示点颜色
-
-```html
-<nut-swiper current="1" indicator-dots="true" indicator-color="#426543" autoplay="true" interval="3000">
-   <nut-swiper-item>
-    <img src="https://storage.360buyimg.com/jdc-article/NutUItaro34.jpg" alt="" />
-  </nut-swiper-item>
-  <nut-swiper-item>
-    <img src="https://storage.360buyimg.com/jdc-article/NutUItaro2.jpg'" alt="" />
-  </nut-swiper-item>
-  <nut-swiper-item>
-    <img src="https://storage.360buyimg.com/jdc-article/welcomenutui.jpg" alt="" />
-  </nut-swiper-item>
-  <nut-swiper-item>
-    <img src="https://storage.360buyimg.com/jdc-article/fristfabu.jpg" alt="" />
-  </nut-swiper-item>
-</nut-swiper>
-```
-
-### 自定义大小
-
-`previous-margin` 前边距,可用于露出前一项的一小部分,接受 px 和 rpx 值
-`next-margin` 后边距,可用于露出后一项的一小部分,接受 px 和 rpx 值
-
-```html
-<nut-swiper previous-margin="10px" next-margin="10px">
-   <nut-swiper-item>
-    <img src="https://storage.360buyimg.com/jdc-article/NutUItaro34.jpg" alt="" />
-  </nut-swiper-item>
-  <nut-swiper-item>
-    <img src="https://storage.360buyimg.com/jdc-article/NutUItaro2.jpg'" alt="" />
-  </nut-swiper-item>
-  <nut-swiper-item>
-    <img src="https://storage.360buyimg.com/jdc-article/welcomenutui.jpg" alt="" />
-  </nut-swiper-item>
-  <nut-swiper-item>
-    <img src="https://storage.360buyimg.com/jdc-article/fristfabu.jpg" alt="" />
-  </nut-swiper-item>
-</nut-swiper>
-```
-
-### 垂直方向
-
-`vertical` 滑动方向是否为纵向
-
-
-```html
-<nut-swiper vertical="true">
-    <nut-swiper-item>
-    <img src="https://storage.360buyimg.com/jdc-article/NutUItaro34.jpg" alt="" />
-  </nut-swiper-item>
-  <nut-swiper-item>
-    <img src="https://storage.360buyimg.com/jdc-article/NutUItaro2.jpg'" alt="" />
-  </nut-swiper-item>
-  <nut-swiper-item>
-    <img src="https://storage.360buyimg.com/jdc-article/welcomenutui.jpg" alt="" />
-  </nut-swiper-item>
-  <nut-swiper-item>
-    <img src="https://storage.360buyimg.com/jdc-article/fristfabu.jpg" alt="" />
-  </nut-swiper-item>
-</nut-swiper>
-``` -->
-
-
-#### 直接使用 Taro 现有 Swiper 组件开发 [参考文档](https://docs.taro.zone/docs/components/viewContainer/swiper)

+ 448 - 9
src/packages/__VUE/swiper/index.taro.vue

@@ -1,20 +1,459 @@
 <template>
-  <swiper v-bind="attrs">
-    <slot></slot>
-  </swiper>
+  <view
+    ref="container"
+    :id="'container-' + refRandomId"
+    :class="classes"
+    @touchstart="onTouchStart"
+    @touchmove="onTouchMove"
+    @touchend="onTouchEnd"
+    @touchcancel="onTouchEnd"
+  >
+    <view
+      :class="{
+        [`${componentName}-inner`]: true,
+        [`${componentName}-vertical`]: isVertical
+      }"
+      :style="state.style"
+    >
+      <slot></slot>
+    </view>
+    <slot name="page"></slot>
+    <view
+      :class="{
+        [`${componentName}-pagination`]: true,
+        [`${componentName}-pagination-vertical`]: isVertical
+      }"
+      v-if="paginationVisible && !slots.page"
+    >
+      <i
+        :style="{
+          backgroundColor: activePagination === index ? paginationColor : '#ddd'
+        }"
+        v-for="(item, index) in state.children.length"
+        :key="index"
+      />
+    </view>
+  </view>
 </template>
 
 <script lang="ts">
+import {
+  onMounted,
+  onActivated,
+  onDeactivated,
+  onBeforeUnmount,
+  provide,
+  ComponentInternalInstance,
+  ComponentPublicInstance,
+  reactive,
+  computed,
+  nextTick,
+  ref,
+  watch
+} from 'vue';
 import { createComponent } from '../../utils/create';
+import { useTouch } from './use-touch';
+import { useTaroRect } from '../../utils/useTaroRect';
+import { useExpose } from '../../utils/useExpose/index';
+import Taro, { eventCenter, getCurrentInstance, useReady } from '@tarojs/taro';
 const { create, componentName } = createComponent('swiper');
 export default create({
-  inheritAttrs: false,
-  props: {},
-  emits: [],
+  props: {
+    width: {
+      type: [Number, String],
+      default: window.innerWidth
+    },
+    height: {
+      type: [Number, String],
+      default: 0
+    },
+    direction: {
+      type: [String],
+      default: 'horizontal' //horizontal and vertical
+    },
+    paginationVisible: {
+      type: Boolean,
+      default: false
+    },
+    paginationColor: {
+      type: String,
+      default: '#fff'
+    },
+    loop: {
+      type: Boolean,
+      default: true
+    },
+    duration: {
+      type: [Number, String],
+      default: 500
+    },
+    autoPlay: {
+      type: [Number, String],
+      default: 0
+    },
+    initPage: {
+      type: [Number, String],
+      default: 0
+    },
+    touchable: {
+      type: Boolean,
+      default: true
+    },
+    isPreventDefault: {
+      type: Boolean,
+      default: true
+    },
+    isStopPropagation: {
+      type: Boolean,
+      default: true
+    }
+  },
+  emits: ['change'],
 
-  setup(props, context) {
-    const attrs = context.attrs;
-    return { attrs };
+  setup(props, { emit, slots }) {
+    const container = ref<HTMLElement>();
+    const refRandomId = Math.random().toString(36).slice(-8);
+    const state = reactive({
+      active: 0,
+      num: 0,
+      rect: null as DOMRect | null,
+      width: 0,
+      height: 0,
+      moving: false,
+      offset: 0,
+      touchTime: 0,
+      autoplayTimer: 0 as number | undefined,
+      children: [] as ComponentPublicInstance[],
+      style: {}
+    });
+
+    const touch = useTouch();
+
+    const classes = computed(() => {
+      const prefixCls = componentName;
+      return {
+        [prefixCls]: true
+      };
+    });
+
+    const isVertical = computed(() => props.direction === 'vertical');
+
+    const delTa = computed(() => {
+      return isVertical.value ? touch.state.deltaY : touch.state.deltaX;
+    });
+
+    const isCorrectDirection = computed(() => {
+      return touch.state.direction === props.direction;
+    });
+
+    const childCount = computed(() => state.children.length);
+
+    const size = computed(() => state[isVertical.value ? 'height' : 'width']);
+
+    const trackSize = computed(() => childCount.value * size.value);
+
+    const minOffset = computed(() => {
+      if (state.rect) {
+        const base = isVertical.value ? state.rect.height : state.rect.width;
+        return base - size.value * childCount.value;
+      }
+      return 0;
+    });
+
+    const activePagination = computed(() => (state.active + childCount.value) % childCount.value);
+
+    const getStyle = () => {
+      state.style = {
+        transitionDuration: `${state.moving ? 0 : props.duration}ms`,
+        transform: `translate${isVertical.value ? 'Y' : 'X'}(${state.offset}px)`,
+        [isVertical.value ? 'height' : 'width']: `${size.value * childCount.value}px`,
+        [isVertical.value ? 'width' : 'height']: `${isVertical.value ? state.width : state.height}px`
+      };
+    };
+
+    const relation = (child: ComponentInternalInstance) => {
+      if (child.proxy) {
+        state.children.push(child.proxy);
+      }
+    };
+
+    const range = (num: number, min: number, max: number) => {
+      return Math.min(Math.max(num, min), max);
+    };
+
+    const requestFrame = (fn: FrameRequestCallback) => {
+      requestAnimationFrame.call(null, fn);
+    };
+
+    const getOffset = (active: number, offset = 0) => {
+      let currentPosition = active * size.value;
+      if (!props.loop) {
+        currentPosition = Math.min(currentPosition, -minOffset.value);
+      }
+
+      let targetOffset = offset - currentPosition;
+      if (!props.loop) {
+        targetOffset = range(targetOffset, minOffset.value, 0);
+      }
+
+      return targetOffset;
+    };
+
+    const getActive = (pace: number) => {
+      const { active } = state;
+      if (pace) {
+        if (props.loop) {
+          return range(active + pace, -1, childCount.value);
+        }
+        return range(active + pace, 0, childCount.value - 1);
+      }
+      return active;
+    };
+
+    const move = ({ pace = 0, offset = 0, isEmit = false }) => {
+      if (childCount.value <= 1) return;
+
+      const { active } = state;
+
+      const targetActive = getActive(pace);
+      const targetOffset = getOffset(targetActive, offset);
+
+      if (props.loop) {
+        if (state.children[0] && targetOffset !== minOffset.value) {
+          const rightBound = targetOffset < minOffset.value;
+          (state.children[0] as any).setOffset(rightBound ? trackSize.value : 0);
+        }
+        if (state.children[childCount.value - 1] && targetOffset !== 0) {
+          const leftBound = targetOffset > 0;
+          (state.children[childCount.value - 1] as any).setOffset(leftBound ? -trackSize.value : 0);
+        }
+      }
+
+      state.active = targetActive;
+      state.offset = targetOffset;
+
+      if (isEmit && active !== state.active) {
+        emit('change', activePagination.value);
+      }
+
+      getStyle();
+    };
+
+    const resettPosition = () => {
+      state.moving = true;
+
+      if (state.active <= -1) {
+        move({ pace: childCount.value });
+      }
+      if (state.active >= childCount.value) {
+        move({ pace: -childCount.value });
+      }
+    };
+
+    const stopAutoPlay = () => {
+      clearTimeout(state.autoplayTimer);
+    };
+
+    const prev = () => {
+      resettPosition();
+      touch.reset();
+
+      requestFrame(() => {
+        requestFrame(() => {
+          state.moving = false;
+          move({
+            pace: -1,
+            isEmit: true
+          });
+        });
+      });
+    };
+
+    const next = () => {
+      resettPosition();
+      touch.reset();
+
+      requestFrame(() => {
+        requestFrame(() => {
+          state.moving = false;
+          move({
+            pace: 1,
+            isEmit: true
+          });
+        });
+      });
+    };
+
+    const to = (index: number) => {
+      resettPosition();
+
+      touch.reset();
+
+      requestFrame(() => {
+        requestFrame(() => {
+          state.moving = false;
+          let targetIndex;
+          if (props.loop && childCount.value === index) {
+            targetIndex = state.active === 0 ? 0 : index;
+          } else {
+            targetIndex = index % childCount.value;
+          }
+          move({
+            pace: targetIndex - state.active,
+            isEmit: true
+          });
+        });
+      });
+    };
+
+    const autoplay = () => {
+      if (props.autoPlay <= 0 || childCount.value <= 1) return;
+      stopAutoPlay();
+
+      state.autoplayTimer = setTimeout(() => {
+        next();
+        autoplay();
+      }, Number(props.autoPlay));
+    };
+
+    const init = async (active: number = +props.initPage) => {
+      stopAutoPlay();
+      state.rect = await useTaroRect(container, Taro);
+      active = Math.min(childCount.value - 1, active);
+      state.width = props.width ? +props.width : (state.rect as DOMRect).width;
+      state.height = props.height ? +props.height : (state.rect as DOMRect).height;
+      state.active = active;
+      state.offset = getOffset(state.active);
+      state.moving = true;
+      getStyle();
+
+      autoplay();
+    };
+
+    const onTouchStart = (e: TouchEvent) => {
+      if (props.isPreventDefault) e.preventDefault();
+      if (props.isStopPropagation) e.stopPropagation();
+      if (!props.touchable) return;
+      touch.start(e);
+      state.touchTime = Date.now();
+      stopAutoPlay();
+      resettPosition();
+    };
+
+    const onTouchMove = (e: TouchEvent) => {
+      if (props.touchable && state.moving) {
+        touch.move(e);
+        if (isCorrectDirection.value) {
+          move({
+            offset: delTa.value
+          });
+        }
+      }
+    };
+
+    const onTouchEnd = (e: TouchEvent) => {
+      if (!props.touchable || !state.moving) return;
+      const speed = delTa.value / (Date.now() - state.touchTime);
+      const isShouldMove = Math.abs(speed) > 0.3 || Math.abs(delTa.value) > +(size.value / 2).toFixed(2);
+
+      if (isShouldMove && isCorrectDirection.value) {
+        let pace = 0;
+        const offset = isVertical.value ? touch.state.offsetY : touch.state.offsetX;
+        if (props.loop) {
+          pace = offset > 0 ? (delTa.value > 0 ? -1 : 1) : 0;
+        } else {
+          pace = -Math[delTa.value > 0 ? 'ceil' : 'floor'](delTa.value / size.value);
+        }
+        move({
+          pace,
+          isEmit: true
+        });
+      } else if (delTa.value) {
+        move({ pace: 0 });
+      }
+      state.moving = false;
+      getStyle();
+      autoplay();
+    };
+
+    provide('parent', {
+      props,
+      size,
+      relation
+    });
+
+    useExpose({
+      prev,
+      next,
+      to
+    });
+
+    onMounted(() => {
+      if (Taro.getEnv() === 'WEB') {
+        init();
+      } else {
+        eventCenter.once((getCurrentInstance() as any).router.onReady, () => {
+          init();
+        });
+      }
+    });
+
+    onActivated(() => {
+      if (Taro.getEnv() === 'WEB') {
+        init();
+      } else {
+        eventCenter.once((getCurrentInstance() as any).router.onReady, () => {
+          init();
+        });
+      }
+    });
+
+    onDeactivated(() => {
+      stopAutoPlay();
+    });
+
+    onBeforeUnmount(() => {
+      stopAutoPlay();
+    });
+
+    watch(
+      () => props.initPage,
+      (val) => {
+        eventCenter.once((getCurrentInstance() as any).router.onReady, () => {
+          init(+val);
+        });
+      }
+    );
+
+    watch(
+      () => state.children.length,
+      () => {
+        eventCenter.once((getCurrentInstance() as any).router.onReady, () => {
+          init(state.active);
+        });
+      }
+    );
+
+    watch(
+      () => props.autoPlay,
+      (val) => {
+        val > 0 ? autoplay() : stopAutoPlay();
+      }
+    );
+
+    return {
+      state,
+      refRandomId,
+      classes,
+      container,
+      componentName,
+      isVertical,
+      slots,
+      activePagination,
+      onTouchStart,
+      onTouchMove,
+      onTouchEnd
+    };
   }
 });
 </script>

+ 35 - 2
src/packages/__VUE/swiperitem/index.taro.vue

@@ -1,16 +1,28 @@
 <template>
-  <swiper-item :class="classes">
+  <view :class="classes" :style="style">
     <slot></slot>
-  </swiper-item>
+  </view>
 </template>
 
 <script lang="ts">
 import { computed, reactive, inject, getCurrentInstance, watch } from 'vue';
 import { createComponent } from '../../utils/create';
+import { useExpose } from '../swiper/use-expose';
 const { create, componentName } = createComponent('swiper-item');
+interface IStyle {
+  width?: string;
+  height?: string;
+  transform?: string;
+}
 export default create({
   props: {},
   setup(props, { slots }) {
+    const parent = inject('parent') as any;
+    parent['relation'](getCurrentInstance());
+    const state = reactive({
+      offset: 0
+    });
+
     const classes = computed(() => {
       const prefixCls = componentName;
       return {
@@ -18,7 +30,28 @@ export default create({
       };
     });
 
+    const style = computed(() => {
+      const style: IStyle = {};
+      const direction = parent?.props.direction;
+      if (parent?.size.value) {
+        style[direction === 'horizontal' ? 'width' : 'height'] = `${parent?.size.value}px`;
+      }
+
+      if (state.offset) {
+        style['transform'] = `translate${direction === 'horizontal' ? 'X' : 'Y'}(${state.offset}px)`;
+      }
+
+      return style;
+    });
+
+    const setOffset = (offset: number) => {
+      state.offset = offset;
+    };
+
+    useExpose({ setOffset });
+
     return {
+      style,
       classes
     };
   }

+ 22 - 20
src/sites/mobile-taro/vue/src/feedback/pages/swiper/index.vue

@@ -2,15 +2,15 @@
   <div class="demo">
     <h2>基本用法</h2>
     <view class="demo-box">
-      <nut-swiper autoplay="true" circular="true">
+      <nut-swiper :init-page="page" :pagination-visible="true" pagination-color="#426543" auto-play="2000">
         <nut-swiper-item v-for="item in list" :key="item">
           <img :src="item" alt="" />
         </nut-swiper-item>
       </nut-swiper>
     </view>
-    <h2>改变起始位置</h2>
+    <h2>自定义大小</h2>
     <view class="demo-box">
-      <nut-swiper current="1">
+      <nut-swiper :init-page="page2" :loop="false" width="300">
         <nut-swiper-item v-for="item in list" :key="item">
           <img :src="item" alt="" />
         </nut-swiper-item>
@@ -18,13 +18,7 @@
     </view>
     <h2>自定义指示器</h2>
     <view class="demo-box">
-      <nut-swiper
-        autoplay="true"
-        circular="true"
-        indicator-dots="true"
-        indicator-color="pink"
-        indicator-active-color="red"
-      >
+      <nut-swiper :init-page="page3" :loop="true" @change="change">
         <nut-swiper-item v-for="item in list" :key="item">
           <img :src="item" alt="" />
         </nut-swiper-item>
@@ -36,19 +30,17 @@
     <h2>垂直方向</h2>
     <view class="demo-box">
       <nut-swiper
-        vertical="true"
-        autoplay="true"
-        circular="true"
-        indicator-dots="true"
-        indicator-color="pink"
-        indicator-active-color="red"
+        :init-page="page4"
+        :loop="true"
+        auto-play="3000"
+        direction="vertical"
+        height="150"
+        :pagination-visible="true"
+        style="height: 150px"
       >
         <nut-swiper-item v-for="item in list" :key="item">
           <img :src="item" alt="" />
         </nut-swiper-item>
-        <template v-slot:page>
-          <div class="page"> {{ current }}/4 </div>
-        </template>
       </nut-swiper>
     </view>
   </div>
@@ -61,6 +53,11 @@ export default {
   props: {},
   setup() {
     const state = reactive({
+      page: 2,
+      page2: 0,
+      page3: 0,
+      page4: 0,
+      current: 1,
       list: [
         'https://storage.360buyimg.com/jdc-article/NutUItaro34.jpg',
         'https://storage.360buyimg.com/jdc-article/NutUItaro2.jpg',
@@ -68,8 +65,12 @@ export default {
         'https://storage.360buyimg.com/jdc-article/fristfabu.jpg'
       ]
     });
+    const change = (index: number) => {
+      state.current = index + 1;
+    };
     return {
-      ...toRefs(state)
+      ...toRefs(state),
+      change
     };
   }
 };
@@ -78,6 +79,7 @@ export default {
 <style lang="scss">
 .demo-box {
   .nut-swiper-item {
+    line-height: 150px;
     img {
       width: 100%;
       height: 100%;