Browse Source

feat: picker&range (#503)

* feat: range

* feat: 增加useTaroRect

* feat: range完成

* fix: rect-ts
Jerry 4 years ago
parent
commit
c2336cc489

+ 3 - 1
src/config.json

@@ -187,6 +187,7 @@
         {
         {
           "version": "3.0.0",
           "version": "3.0.0",
           "name": "Picker",
           "name": "Picker",
+          "taro": true,
           "type": "component",
           "type": "component",
           "cName": "选择器",
           "cName": "选择器",
           "desc": "提供多个选型集合供用户选择,支持单列选择和多列级联,通常与弹出层配合使用",
           "desc": "提供多个选型集合供用户选择,支持单列选择和多列级联,通常与弹出层配合使用",
@@ -382,12 +383,13 @@
         {
         {
           "version": "3.0.0",
           "version": "3.0.0",
           "name": "Range",
           "name": "Range",
+          "taro": true,
           "type": "component",
           "type": "component",
           "cName": "区间选择器",
           "cName": "区间选择器",
           "desc": "滑动输入条,用于在给定的范围内选择一个值。",
           "desc": "滑动输入条,用于在给定的范围内选择一个值。",
           "sort": 16,
           "sort": 16,
           "show": true,
           "show": true,
-          "author": "Jerry"
+          "author": "zy19940510"
         },
         },
         {
         {
           "name": "PullRefresh",
           "name": "PullRefresh",

+ 1 - 1
src/packages/__VUE/actionsheet/index.taro.vue

@@ -86,7 +86,7 @@ export default create({
       };
       };
     });
     });
 
 
-    const isHighlight = item => {
+    const isHighlight = (item) => {
       return props.chooseTagValue &&
       return props.chooseTagValue &&
         props.chooseTagValue === item[props.optionTag]
         props.chooseTagValue === item[props.optionTag]
         ? props.color
         ? props.color

+ 3 - 4
src/packages/__VUE/collapseitem/index.taro.vue

@@ -136,9 +136,8 @@ export default create({
 
 
     // 清除 willChange 减少性能浪费
     // 清除 willChange 减少性能浪费
     const onTransitionEnd = () => {
     const onTransitionEnd = () => {
-      const wrapperRefEle: any = document.getElementsByClassName(
-        'collapse-wrapper'
-      )[0];
+      const wrapperRefEle: any =
+        document.getElementsByClassName('collapse-wrapper')[0];
       wrapperRefEle.style.willChange = 'auto';
       wrapperRefEle.style.willChange = 'auto';
     };
     };
 
 
@@ -231,7 +230,7 @@ export default create({
           defaultOpen();
           defaultOpen();
         }
         }
       } else if (Object.values(active) instanceof Array) {
       } else if (Object.values(active) instanceof Array) {
-        const f = Object.values(active).filter(item => item == name);
+        const f = Object.values(active).filter((item) => item == name);
         if (f.length > 0) {
         if (f.length > 0) {
           defaultOpen();
           defaultOpen();
         }
         }

+ 3 - 4
src/packages/__VUE/collapseitem/index.vue

@@ -132,9 +132,8 @@ export default create({
 
 
     // 清除 willChange 减少性能浪费
     // 清除 willChange 减少性能浪费
     const onTransitionEnd = () => {
     const onTransitionEnd = () => {
-      const wrapperRefEle: any = document.getElementsByClassName(
-        'collapse-wrapper'
-      )[0];
+      const wrapperRefEle: any =
+        document.getElementsByClassName('collapse-wrapper')[0];
       wrapperRefEle.style.willChange = 'auto';
       wrapperRefEle.style.willChange = 'auto';
 
 
       // const query = wx.createSelectorQuery();
       // const query = wx.createSelectorQuery();
@@ -227,7 +226,7 @@ export default create({
           defaultOpen();
           defaultOpen();
         }
         }
       } else if (Object.values(active) instanceof Array) {
       } else if (Object.values(active) instanceof Array) {
-        const f = Object.values(active).filter(item => item == name);
+        const f = Object.values(active).filter((item) => item == name);
         if (f.length > 0) {
         if (f.length > 0) {
           defaultOpen();
           defaultOpen();
         }
         }

+ 1 - 1
src/packages/__VUE/picker/index.taro.vue

@@ -14,7 +14,7 @@
 import { onUpdated, ref, watch } from 'vue';
 import { onUpdated, ref, watch } from 'vue';
 const { create } = createComponent('picker');
 const { create } = createComponent('picker');
 import { commonProps } from './commonProps';
 import { commonProps } from './commonProps';
-import { createComponent } from './../../utils/create';
+import { createComponent } from '@/packages/utils/create';
 export default create({
 export default create({
   props: {
   props: {
     mode: {
     mode: {

+ 2 - 2
src/packages/__VUE/popup/index.taro.vue

@@ -217,7 +217,7 @@ export default create({
     });
     });
     watch(
     watch(
       () => props.visible,
       () => props.visible,
-      value => {
+      (value) => {
         if (value) {
         if (value) {
           open();
           open();
         } else {
         } else {
@@ -227,7 +227,7 @@ export default create({
     );
     );
     watch(
     watch(
       () => props.position,
       () => props.position,
-      value => {
+      (value) => {
         value === 'center'
         value === 'center'
           ? (state.transitionName = 'popup-fade')
           ? (state.transitionName = 'popup-fade')
           : (state.transitionName = `popup-slide-${value}`);
           : (state.transitionName = `popup-slide-${value}`);

+ 325 - 0
src/packages/__VUE/range/index.taro.vue

@@ -0,0 +1,325 @@
+<template>
+  <view class="nut-range-container">
+    <view class="min" v-if="!hiddenRange">{{ +min }}</view>
+    <view
+      ref="root"
+      id="root"
+      :style="wrapperStyle"
+      :class="classes"
+      @click.stop="onClick"
+    >
+      <view class="nut-range-bar" :style="barStyle">
+        <template v-if="range">
+          <view
+            v-for="index of [0, 1]"
+            :key="index"
+            role="slider"
+            :class="{
+              'nut-range-button-wrapper-left': index == 0,
+              'nut-range-button-wrapper-right': index == 1
+            }"
+            :tabindex="disabled ? -1 : 0"
+            :aria-valuemin="+min"
+            :aria-valuenow="curValue(index)"
+            :aria-valuemax="+max"
+            aria-orientation="horizontal"
+            @touchstart.stop.prevent="
+              (e) => {
+                if (typeof index === 'number') {
+                  // 实时更新当前拖动的按钮索引
+                  buttonIndex = index;
+                }
+                onTouchStart(e);
+              }
+            "
+            @touchmove.stop.prevent="onTouchMove"
+            @touchend.stop.prevent="onTouchEnd"
+            @touchcancel.stop.prevent="onTouchEnd"
+            @click="(e) => e.stopPropagation()"
+          >
+            <slot v-if="$slots.button" name="button"></slot>
+            <view class="nut-range-button" v-else :style="buttonStyle">
+              <view class="number" v-if="!hiddenTag">{{
+                curValue(index)
+              }}</view>
+            </view>
+          </view>
+        </template>
+        <template v-else>
+          <view
+            role="slider"
+            class="nut-range-button-wrapper"
+            :tabindex="disabled ? -1 : 0"
+            :aria-valuemin="+min"
+            :aria-valuenow="curValue()"
+            :aria-valuemax="+max"
+            aria-orientation="horizontal"
+            @touchstart.stop.prevent="
+              (e) => {
+                onTouchStart(e);
+              }
+            "
+            @touchmove.stop.prevent="onTouchMove"
+            @touchend.stop.prevent="onTouchEnd"
+            @touchcancel.stop.prevent="onTouchEnd"
+            @click="(e) => e.stopPropagation()"
+          >
+            <slot v-if="$slots.button" name="button"></slot>
+            <view class="nut-range-button" v-else :style="buttonStyle">
+              <view class="number" v-if="!hiddenTag">{{
+                curValue(index)
+              }}</view>
+            </view>
+          </view>
+        </template>
+      </view>
+    </view>
+    <view class="max" v-if="!hiddenRange">{{ +max }}</view>
+  </view>
+</template>
+<script lang="ts">
+import Taro from '@tarojs/taro';
+import { ref, toRefs, computed, PropType, CSSProperties, onUpdated } from 'vue';
+import { createComponent } from '@/packages/utils/create';
+import { useTouch } from '@/packages/utils/useTouch';
+import { useTaroRect } from '@/packages/utils/useTaroRect';
+const { componentName, create } = createComponent('range');
+
+type SliderValue = number | number[];
+
+export default create({
+  props: {
+    range: {
+      type: Boolean,
+      default: false
+    },
+    disabled: Boolean,
+    activeColor: String,
+    inactiveColor: String,
+    buttonColor: String,
+    hiddenRange: {
+      type: Boolean,
+      default: false
+    },
+    hiddenTag: {
+      type: Boolean,
+      default: false
+    },
+    min: {
+      type: [Number, String],
+      default: 0
+    },
+    max: {
+      type: [Number, String],
+      default: 100
+    },
+    step: {
+      type: [Number, String],
+      default: 1
+    },
+    modelValue: {
+      type: [Number, Array] as PropType<SliderValue>,
+      default: 0
+    }
+  },
+
+  emits: ['change', 'drag-end', 'drag-start', 'update:modelValue'],
+
+  setup(props, { emit, slots }) {
+    const buttonIndex = ref(0);
+    let startValue: SliderValue;
+    let currentValue: SliderValue;
+
+    const root = ref<HTMLElement>();
+    const dragStatus = ref<'start' | 'draging' | ''>();
+    const touch = useTouch();
+
+    const scope = computed(() => Number(props.max) - Number(props.min));
+
+    const classes = computed(() => {
+      const prefixCls = componentName;
+      return {
+        [prefixCls]: true,
+        [`${prefixCls}-disabled`]: props.disabled,
+        [`${prefixCls}-show-number`]: !props.hiddenRange
+      };
+    });
+
+    const wrapperStyle = computed(() => {
+      return {
+        background: props.inactiveColor
+      };
+    });
+
+    const buttonStyle = computed(() => {
+      return {
+        borderColor: props.buttonColor
+      };
+    });
+
+    const isRange = (val: unknown): val is number[] =>
+      !!props.range && Array.isArray(val);
+
+    const calcMainAxis = () => {
+      const { modelValue, min } = props;
+      if (isRange(modelValue)) {
+        return `${((modelValue[1] - modelValue[0]) * 100) / scope.value}%`;
+      }
+      return `${((modelValue - Number(min)) * 100) / scope.value}%`;
+    };
+
+    const calcOffset = () => {
+      const { modelValue, min } = props;
+      if (isRange(modelValue)) {
+        return `${((modelValue[0] - Number(min)) * 100) / scope.value}%`;
+      }
+      return `0%`;
+    };
+
+    const barStyle = computed<CSSProperties>(() => {
+      return {
+        width: calcMainAxis(),
+        left: calcOffset(),
+        background: props.activeColor,
+        transition: dragStatus.value ? 'none' : undefined
+      };
+    });
+
+    const format = (value: number) => {
+      const { min, max, step } = props;
+      value = Math.max(+min, Math.min(value, +max));
+      return Math.round(value / +step) * +step;
+    };
+
+    const isSameValue = (newValue: SliderValue, oldValue: SliderValue) =>
+      JSON.stringify(newValue) === JSON.stringify(oldValue);
+
+    const handleOverlap = (value: number[]) => {
+      if (value[0] > value[1]) {
+        return value.slice(0).reverse();
+      }
+      return value;
+    };
+
+    const updateValue = (value: SliderValue, end?: boolean) => {
+      if (isRange(value)) {
+        value = handleOverlap(value).map(format);
+      } else {
+        value = format(value);
+      }
+
+      if (!isSameValue(value, props.modelValue)) {
+        emit('update:modelValue', value);
+      }
+
+      if (end && !isSameValue(value, startValue)) {
+        emit('change', value);
+      }
+    };
+
+    const onClick = async (event: MouseEvent) => {
+      if (props.disabled) {
+        return;
+      }
+
+      const { min, modelValue } = props;
+      let rect = await useTaroRect(root, Taro);
+      const delta = (event as any).touches[0].clientX - rect.left;
+      const total = rect.width;
+      const value = Number(min) + (delta / total) * scope.value;
+      if (isRange(modelValue)) {
+        const [left, right] = modelValue;
+        const middle = (left + right) / 2;
+        if (value <= middle) {
+          updateValue([value, right], true);
+        } else {
+          updateValue([left, value], true);
+        }
+      } else {
+        updateValue(value, true);
+      }
+    };
+
+    const onTouchStart = (event: TouchEvent) => {
+      if (props.disabled) {
+        return;
+      }
+
+      touch.start(event);
+      currentValue = props.modelValue;
+
+      if (isRange(currentValue)) {
+        startValue = currentValue.map(format);
+      } else {
+        startValue = format(currentValue);
+      }
+
+      dragStatus.value = 'start';
+    };
+
+    const onTouchMove = async (event: TouchEvent) => {
+      if (props.disabled) {
+        return;
+      }
+
+      if (dragStatus.value === 'start') {
+        emit('drag-start');
+      }
+
+      touch.move(event);
+      dragStatus.value = 'draging';
+
+      const rect = await useTaroRect(root, Taro);
+      const delta = touch.deltaX.value;
+      const total = rect.width;
+      const diff = (delta / total) * scope.value;
+
+      if (isRange(startValue)) {
+        (currentValue as number[])[buttonIndex.value] =
+          startValue[buttonIndex.value] + diff;
+      } else {
+        currentValue = startValue + diff;
+      }
+      updateValue(currentValue);
+    };
+
+    const onTouchEnd = () => {
+      if (props.disabled) {
+        return;
+      }
+      if (dragStatus.value === 'draging') {
+        updateValue(currentValue, true);
+        emit('drag-end');
+      }
+      dragStatus.value = '';
+    };
+
+    const curValue = (idx?: number) => {
+      const value =
+        typeof idx === 'number'
+          ? (props.modelValue as number[])[idx]
+          : props.modelValue;
+      return value;
+    };
+
+    return {
+      root,
+      classes,
+      wrapperStyle,
+      buttonStyle,
+      onClick,
+      onTouchStart,
+      onTouchMove,
+      onTouchEnd,
+      ...toRefs(props),
+      barStyle,
+      curValue,
+      buttonIndex
+    };
+  }
+});
+</script>
+
+<style lang="scss">
+@import 'index.scss';
+</style>

+ 26 - 0
src/packages/utils/useTaroRect/index.ts

@@ -0,0 +1,26 @@
+/**
+  获取元素的大小及其相对于视口的位置,等价于 Element.getBoundingClientRect。
+  width 宽度	number
+  height 高度	number
+  top	顶部与视图窗口左上角的距离	number
+  left	左侧与视图窗口左上角的距离	number
+  right	右侧与视图窗口左上角的距离	number
+  bottom	底部与视图窗口左上角的距离	number
+ */
+
+import { Ref, unref } from 'vue';
+
+export const useTaroRect = (
+  elementRef: (Element | Window) | Ref<Element | Window | undefined>,
+  Taro: any
+): any => {
+  const element = unref(elementRef);
+
+  return new Promise((resolve) => {
+    const query = Taro.createSelectorQuery();
+    query.select(`#${(element as any).id}`).boundingClientRect();
+    query.exec(function (res: any) {
+      resolve(res[0]);
+    });
+  });
+};

+ 1 - 1
src/sites/mobile-taro/vue/project.config.json

@@ -2,7 +2,7 @@
   "miniprogramRoot": "dist/",
   "miniprogramRoot": "dist/",
   "projectname": "%40nutui%2Fnutui-taro-mobile",
   "projectname": "%40nutui%2Fnutui-taro-mobile",
   "description": "nutui-taro-vue",
   "description": "nutui-taro-vue",
-  "appid": "wxca272ba6bdf568d8",
+  "appid": "wxee296c9bffc451d9",
   "setting": {
   "setting": {
     "urlCheck": true,
     "urlCheck": true,
     "es6": false,
     "es6": false,

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

@@ -1,5 +1,7 @@
 export default {
 export default {
   pages: [
   pages: [
+    'pages/range/index',
+    'pages/picker/index',
     'pages/uploader/index',
     'pages/uploader/index',
     'pages/infiniteloading/index',
     'pages/infiniteloading/index',
     'pages/address/index',
     'pages/address/index',

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

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

+ 65 - 0
src/sites/mobile-taro/vue/src/pages/picker/index.vue

@@ -0,0 +1,65 @@
+<template>
+  <div class="demo">
+    <h2>基础用法</h2>
+    <nut-picker mode="selector" :list-data="listData1" @confirm="confirm">
+      <nut-cell title="请选择城市" :desc="desc"></nut-cell>
+    </nut-picker>
+    <h2>多列样式</h2>
+    <nut-picker mode="multiSelector" :list-data="listData2" @confirm="confirm2">
+      <nut-cell title="请选择时间" :desc="desc2"></nut-cell>
+    </nut-picker>
+  </div>
+</template>
+<script lang="ts">
+import { ref } from 'vue';
+export default {
+  props: {},
+  setup() {
+    const listData1 = [
+      '南京市',
+      '无锡市',
+      '海北藏族自治区',
+      '北京市',
+      '连云港市',
+      '浙江市',
+      '江苏市'
+    ];
+    const listData2 = ref([
+      {
+        values: ['周一', '周二', '周三', '周四', '周五'],
+        defaultIndex: 2
+      },
+      {
+        values: ['上午', '下午', '晚上'],
+        defaultIndex: 1
+      }
+    ]);
+
+    const desc = ref(listData1[0]);
+    const desc2 = ref(
+      `${listData2.value[0].values[listData2.value[0].defaultIndex]} ${
+        listData2.value[1].values[listData2.value[1].defaultIndex]
+      }`
+    );
+    const confirm = (value: any, res: any) => {
+      desc.value = res;
+    };
+
+    const confirm2 = (value: any, res: any) => {
+      desc2.value = res.join(' ');
+      listData2.value.forEach((item, idx) => {
+        item.defaultIndex = value[idx];
+      });
+    };
+
+    return {
+      listData1,
+      listData2,
+      desc,
+      desc2,
+      confirm,
+      confirm2
+    };
+  }
+};
+</script>

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

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

+ 96 - 0
src/sites/mobile-taro/vue/src/pages/range/index.vue

@@ -0,0 +1,96 @@
+<template>
+  <div class="demo">
+    <h2>基础用法</h2>
+    <nut-cell class="cell">
+      <nut-range v-model="value1" @change="onChange"></nut-range>
+    </nut-cell>
+    <h2>双滑块</h2>
+    <nut-cell class="cell">
+      <nut-range range v-model="value2" @change="onChange"></nut-range>
+    </nut-cell>
+    <h2>指定范围</h2>
+    <nut-cell class="cell">
+      <nut-range
+        v-model="value3"
+        :max="10"
+        :min="-10"
+        @change="onChange"
+      ></nut-range>
+    </nut-cell>
+    <h2>设置步长</h2>
+    <nut-cell class="cell">
+      <nut-range v-model="value4" :step="5" @change="onChange"></nut-range>
+    </nut-cell>
+    <h2>隐藏范围</h2>
+    <nut-cell class="cell">
+      <nut-range hidden-range v-model="value5" @change="onChange"></nut-range>
+    </nut-cell>
+    <h2>隐藏标签</h2>
+    <nut-cell class="cell">
+      <nut-range hidden-tag v-model="value6" @change="onChange"></nut-range>
+    </nut-cell>
+    <h2>禁用</h2>
+    <nut-cell class="cell">
+      <nut-range disabled v-model="value7"></nut-range>
+    </nut-cell>
+    <h2>自定义样式</h2>
+    <nut-cell class="cell">
+      <nut-range
+        v-model="value8"
+        @change="onChange"
+        inactive-color="rgba(163,184,255,1)"
+        button-color="rgba(52,96,250,1)"
+        active-color="linear-gradient(315deg, rgba(73,143,242,1) 0%,rgba(73,101,242,1) 100%)"
+      ></nut-range>
+    </nut-cell>
+    <h2>自定义按钮</h2>
+    <nut-cell class="cell">
+      <nut-range v-model="value9" @change="onChange">
+        <template #button>
+          <div class="custom-button">{{ value10 }}</div>
+        </template>
+      </nut-range>
+    </nut-cell>
+  </div>
+</template>
+
+<script lang="ts">
+import { toRefs, reactive } from 'vue';
+export default {
+  props: {},
+  setup() {
+    const state = reactive({
+      value1: 40,
+      value2: [20, 80],
+      value3: 0,
+      value4: 20,
+      value5: 30,
+      value6: 40,
+      value7: 50,
+      value8: 40,
+      value9: 60,
+      value10: 50
+    });
+    const onChange = (value: number) => console.log('当前值:' + value);
+    return {
+      ...toRefs(state),
+      onChange
+    };
+  }
+};
+</script>
+
+<style lang="scss">
+.cell {
+  padding: 40px 18px;
+}
+.custom-button {
+  width: 26px;
+  color: #fff;
+  font-size: 10px;
+  line-height: 18px;
+  text-align: center;
+  background-color: #ee0a24;
+  border-radius: 100px;
+}
+</style>