Browse Source

chore(picker): 组件重构 (#981)

* fix: datepicker 组件最大值与最小值一样时出错

* feat: datePicker 增加在线调试

* docs: infiniteloading 补充文档

* docs: picker 组件添加在线调试

* feat: icon 新增动态icon

* feat: picker 组件更新

* feat: picker 完成

* feat: sku 组件添加在线调试

Co-authored-by: yangxiaolu3 <yangxiaolu1993@qq.com>
yangxiaolu1993 4 years ago
parent
commit
c8cd3a3d0d

+ 156 - 184
src/packages/__VUE/picker/Column.vue

@@ -1,259 +1,231 @@
 <template>
-  <view
-    class="nut-picker__content"
-    :style="{ height: height + 'px' }"
-    @touchstart="onTouchStart"
-    @touchmove="onTouchMove"
-    @touchend="onTouchEnd"
-    @touchcancel="onTouchEnd"
-    @transitionend="stopMomentum"
-  >
-    <view class="nut-picker__wrapper" ref="wrapper" :style="wrapperStyle">
+  <view class="nut-picker__list" @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd">
+    <view class="nut-picker-roller" ref="roller" :style="touchRollerStyle">
       <view
-        class="nut-picker__item"
-        :key="index"
-        v-for="(item, index) in options"
-        >{{ dataType === 'cascade' ? item.text : item }}</view
+        class="nut-picker-roller-item"
+        :class="{ 'nut-picker-roller-item-hidden': isHidden(index + 1) }"
+        v-for="(item, index) in listData.values"
+        :style="setRollerStyle(index + 1)"
+        :key="item.label ? item.label : index"
       >
+        {{ dataType === 'cascade' ? item.text : item }}
+      </view>
+    </view>
+
+    <view class="nut-picker-content">
+      <view class="nut-picker-list-panel" ref="list" :style="touchListStyle">
+        <view
+          class="nut-picker-item nut-picker-item-ref"
+          v-for="(item, index) in listData.values"
+          :key="item.label ? item.label : index"
+          >{{ dataType === 'cascade' ? item.text : item }}
+        </view>
+        <view class="nut-picker-placeholder" v-if="listData && listData.length === 1"></view>
+      </view>
     </view>
   </view>
 </template>
 <script lang="ts">
 import { reactive, ref, watch, computed, toRefs, onMounted } from 'vue';
 import { createComponent } from '../../utils/create';
-import { useTouch } from '../../utils/useTouch';
 import { commonProps } from './commonProps';
-import {
-  PickerObjOpt,
-  PickerOption,
-  PickerObjectColumn,
-  PickerObjectColumns
-} from './types';
-const MOMENTUM_LIMIT_DISTANCE = 15;
-const MOMENTUM_LIMIT_TIME = 300;
-const DEFAULT_DURATION = 200;
+import { TouchParams } from './types';
 const { create } = createComponent('picker-column');
-function range(num: number, min: number, max: number): number {
-  return Math.min(Math.max(num, min), max);
-}
-function stopPropagation(event: Event) {
-  event.stopPropagation();
-}
-function preventDefault(event: Event, isStopPropagation?: boolean) {
-  if (typeof event.cancelable !== 'boolean' || event.cancelable) {
-    event.preventDefault();
-  }
-
-  if (isStopPropagation) {
-    stopPropagation(event);
-  }
-}
-
-function getElementTranslateY(element: Element) {
-  const style = window.getComputedStyle(element);
-  const transform = style.transform || style.webkitTransform;
-  const translateY = transform.slice(7, transform.length - 1).split(', ')[5];
-  return Number(translateY);
-}
-export function isObject(val: unknown): val is Record<any, any> {
-  return val !== null && typeof val === 'object';
-}
-
-function isOptionDisabled(option: PickerObjectColumn) {
-  return isObject(option) && option.disabled;
-}
 
 export default create({
   props: {
     dataType: String,
+    itemShow: {
+      type: Boolean,
+      default: false
+    },
     ...commonProps
   },
 
   emits: ['click', 'change'],
   setup(props, { emit }) {
     const wrapper = ref();
-
     const state = reactive({
-      index: props.defaultIndex,
-      offset: 0,
-      duration: 0,
-      options: props.listData as PickerObjectColumn[],
-      moving: false,
-      startOffset: 0,
-      touchStartTime: 0,
-      momentumOffset: 0,
-      transitionEndTrigger: null as null | Function
+      touchParams: {
+        startY: 0,
+        endY: 0,
+        startTime: 0,
+        endTime: 0,
+        lastY: 0
+      },
+      currIndex: 1,
+      transformY: 0,
+      scrollDistance: 0,
+      lineSpacing: 36,
+      rotation: 20,
+      timer: null
     });
 
-    const touch = useTouch();
-
-    const wrapperStyle = computed(() => ({
-      transform: `translate3d(0, ${state.offset + baseOffset()}px, 0)`,
-      transitionDuration: `${state.duration}ms`,
-      transitionProperty: state.duration ? 'all' : 'none'
-    }));
-
-    const handleClick = (event: Event) => {
-      emit('click', event);
-    };
-
-    const getIndexByOffset = (offset: number) => {
-      return range(
-        Math.round(-offset / +props.itemHeight),
-        0,
-        state.options.length - 1
-      );
-    };
+    const roller = ref(null);
+    const list = ref(null);
+    const listItem = ref(null);
+
+    const touchDeg = ref(0);
+    const touchTime = ref(0);
+    const touchTranslateY = ref(0);
+    const touchListStyle = computed(() => {
+      return {
+        transition: `transform ${touchTime.value}ms cubic-bezier(0.19, 1, 0.22, 1)`,
+        transform: `translate3d(0, ${state.scrollDistance}px, 0)`
+      };
+    });
 
-    const baseOffset = () => {
-      return (+props.itemHeight * (+props.visibleItemCount - 1)) / 2;
-    };
+    const touchRollerStyle = computed(() => {
+      return {
+        transition: `transform ${touchTime.value}ms cubic-bezier(0.19, 1, 0.22, 1)`,
+        transform: `rotate3d(1, 0, 0, ${touchDeg.value})`
+      };
+    });
 
-    const stopMomentum = () => {
-      state.moving = false;
-      state.duration = 0;
-      if (state.transitionEndTrigger) {
-        state.transitionEndTrigger();
-        state.transitionEndTrigger = null;
-      }
+    const onTouchStart = (event: TouchEvent) => {
+      event.preventDefault();
+      let changedTouches = event.changedTouches[0];
+      state.touchParams.startY = changedTouches.pageY;
+      state.touchParams.startTime = event.timeStamp || Date.now();
+      state.transformY = state.scrollDistance;
     };
 
-    const adjustIndex = (index: number) => {
-      index = range(index, 0, state.options.length);
+    const onTouchMove = (event: TouchEvent) => {
+      event.preventDefault();
+      let changedTouches = event.changedTouches[0];
+      (state.touchParams as TouchParams).lastY = changedTouches.pageY;
+      (state.touchParams as TouchParams).lastTime = event.timeStamp || Date.now();
+      let move = state.touchParams.lastY - state.touchParams.startY;
 
-      for (let i = index; i < state.options.length; i++) {
-        if (!isOptionDisabled(state.options[i])) return i;
-      }
-      for (let i = index - 1; i >= 0; i--) {
-        if (!isOptionDisabled(state.options[i])) return i;
-      }
+      setMove(move);
     };
 
-    const setIndex = (index: number, emitChange = false) => {
-      index = adjustIndex(index) || 0;
-
-      const offset = -index * +props.itemHeight;
-      const trigger = () => {
-        if (index !== state.index) {
-          state.index = index;
+    const onTouchEnd = (event: TouchEvent) => {
+      event.preventDefault();
 
-          if (emitChange) {
-            emit('change', index);
-          }
-        }
-      };
+      let changedTouches = event.changedTouches[0];
+      state.touchParams.lastY = changedTouches.pageY;
+      state.touchParams.lastTime = event.timestamp || Date.now();
+      let move = state.touchParams.lastY - state.touchParams.startY;
 
-      if (state.moving && offset !== state.offset) {
-        state.transitionEndTrigger = trigger;
+      let moveTime = state.touchParams.lastTime - state.touchParams.startTime;
+      if (moveTime <= 300) {
+        move = move * 2;
+        moveTime = moveTime + 1000;
+        setMove(move, 'end', moveTime);
       } else {
-        trigger();
+        setMove(move, 'end');
       }
-
-      state.offset = offset;
     };
 
-    const momentum = (distance: number, duration: number) => {
-      const speed = Math.abs(distance / duration);
-
-      distance = state.offset + (speed / 0.03) * (distance < 0 ? -1 : 1);
-
-      const index = getIndexByOffset(distance);
-
-      setIndex(index, true);
+    const setRollerStyle = (index: number) => {
+      return `transform: rotate3d(1, 0, 0, ${-state.rotation * index}deg) translate3d(0px, 0px, 104px)`;
     };
 
-    const onTouchStart = (event: Event) => {
-      if (props.readonly) {
-        return;
+    const isHidden = (index: number) => {
+      if (index >= state.currIndex + 8 || index <= state.currIndex - 8) {
+        return true;
+      } else {
+        return false;
       }
-      touch.start(event);
+    };
 
-      if (state.moving) {
-        const translateY = getElementTranslateY(wrapper.value);
-        state.offset = Math.min(0, translateY - baseOffset());
-        state.startOffset = state.offset;
+    const setTransform = (translateY = 0, type: string | null, time = 1000, deg: string | number) => {
+      if (type === 'end') {
+        touchTime.value = time;
       } else {
-        state.startOffset = state.offset;
+        touchTime.value = 0;
       }
-
-      state.duration = 0;
-      state.touchStartTime = Date.now();
-      state.momentumOffset = state.startOffset;
-      state.transitionEndTrigger = null;
+      touchDeg.value = deg as number;
+      touchTranslateY.value = translateY;
+      state.scrollDistance = translateY;
     };
-    const onTouchMove = (event: Event) => {
-      if (props.readonly) {
-        return;
-      }
-      state.moving = true;
-      touch.move(event);
 
-      if (touch.isVertical()) {
-        state.moving = true;
-        preventDefault(event, true);
-      }
+    const setMove = (move: number, type?: string, time?: number) => {
+      let updateMove = move + state.transformY;
+      if (type === 'end') {
+        // 限定滚动距离
+        if (updateMove > 0) {
+          updateMove = 0;
+        }
+        if (updateMove < -(props.listData.values.length - 1) * state.lineSpacing) {
+          updateMove = -(props.listData.values.length - 1) * state.lineSpacing;
+        }
 
-      const moveOffset = state.startOffset + touch.deltaY.value;
-      if (moveOffset > props.itemHeight) {
-        state.offset = props.itemHeight as number;
-      } else {
-        state.offset = state.startOffset + touch.deltaY.value;
-      }
+        // 设置滚动距离为lineSpacing的倍数值
+        let endMove = Math.round(updateMove / state.lineSpacing) * state.lineSpacing;
+        let deg = `${(Math.abs(Math.round(endMove / state.lineSpacing)) + 1) * state.rotation}deg`;
+        setTransform(endMove, type, time, deg);
 
-      const now = Date.now();
+        let t = time ? time / 2 : 0;
+        (state.timer as any) = setTimeout(() => {
+          setChooseValue();
+        }, t);
 
-      if (now - state.touchStartTime > MOMENTUM_LIMIT_TIME) {
-        state.touchStartTime = now;
-        state.momentumOffset = state.offset;
+        state.currIndex = Math.abs(Math.round(endMove / state.lineSpacing)) + 1;
+      } else {
+        let deg = '0deg';
+        if (updateMove < 0) {
+          deg = `${(Math.abs(updateMove / state.lineSpacing) + 1) * state.rotation}deg`;
+        } else {
+          deg = `${(-updateMove / state.lineSpacing + 1) * state.rotation}deg`;
+        }
+
+        setTransform(updateMove, null, undefined, deg);
+        state.currIndex = Math.abs(Math.round(updateMove / state.lineSpacing)) + 1;
       }
     };
-    const onTouchEnd = () => {
-      const index = getIndexByOffset(state.offset);
-      state.duration = DEFAULT_DURATION;
-      setIndex(index, true);
-      const distance = state.offset - state.momentumOffset;
-      const duration = Date.now() - state.touchStartTime;
-
-      const allowMomentum =
-        duration < MOMENTUM_LIMIT_TIME &&
-        Math.abs(distance) > MOMENTUM_LIMIT_DISTANCE;
 
-      if (allowMomentum) {
-        momentum(distance, duration);
-        return;
-      }
+    const setChooseValue = () => {
+      emit('change', state.currIndex - 1);
     };
 
-    onMounted(() => {
-      setIndex(+props.defaultIndex);
-    });
+    const modifyStatus = (type: boolean) => {
+      let index = props.defaultIndex;
+
+      state.currIndex = index === -1 ? 1 : (index as number) + 1;
+      let move = index === -1 ? 0 : (index as number) * state.lineSpacing;
+      type && setChooseValue();
+      setMove(-move);
+    };
 
     watch(
       () => props.listData,
       (val) => {
-        if (val) {
-          state.options = val as PickerObjectColumn[];
-        }
+        state.transformY = 0;
+        modifyStatus(false);
+      },
+      {
+        deep: true
       }
     );
 
     watch(
       () => props.defaultIndex,
       (val) => {
-        setIndex(+val);
+        state.transformY = 0;
+        modifyStatus(false);
       }
     );
 
+    onMounted(() => {
+      modifyStatus(true);
+    });
+
     return {
       ...toRefs(state),
+      ...toRefs(props),
       wrapper,
+      setRollerStyle,
+      isHidden,
+      roller,
+      list,
+      listItem,
       onTouchStart,
       onTouchMove,
       onTouchEnd,
-      wrapperStyle,
-      stopMomentum,
-      columns: state.options,
-      height: Number(props.visibleItemCount) * +props.itemHeight
+      touchRollerStyle,
+      touchListStyle
     };
   }
 });

+ 258 - 0
src/packages/__VUE/picker/ColumnTaro.vue

@@ -0,0 +1,258 @@
+<template>
+  <view
+    class="nut-picker__list"
+    @touchstart="onTouchStart"
+    @touchmove="onTouchMove"
+    @touchend="onTouchEnd"
+    v-if="itemShow"
+  >
+    <view class="nut-picker-roller" ref="roller" :style="touchRollerStyle">
+      <view
+        class="nut-picker-roller-item"
+        :class="{ 'nut-picker-roller-item-hidden': isHidden(index + 1) }"
+        v-for="(item, index) in listData.values"
+        :style="setRollerStyle(index + 1)"
+        :key="item.label ? item.label : index"
+      >
+        {{ dataType === 'cascade' ? item.text : item }}
+      </view>
+    </view>
+
+    <view class="nut-picker-content">
+      <view class="nut-picker-list-panel" ref="list" :style="touchListStyle">
+        <view
+          class="nut-picker-item nut-picker-item-ref"
+          v-for="(item, index) in listData.values"
+          :key="item.label ? item.label : index"
+          >{{ dataType === 'cascade' ? item.text : item }}
+        </view>
+        <view class="nut-picker-placeholder" v-if="listData && listData.length === 1"></view>
+      </view>
+    </view>
+  </view>
+</template>
+<script lang="ts">
+import { reactive, ref, watch, computed, toRefs, onMounted } from 'vue';
+import { createComponent } from '../../utils/create';
+import { commonProps } from './commonProps';
+const { create } = createComponent('picker-column');
+import { TouchParams } from './types';
+import Taro from '@tarojs/taro';
+
+export default create({
+  props: {
+    dataType: String,
+    itemShow: {
+      type: Boolean,
+      default: false
+    },
+    ...commonProps
+  },
+
+  emits: ['click', 'change'],
+  setup(props, { emit }) {
+    const wrapper = ref();
+    const state = reactive({
+      touchParams: {
+        startY: 0,
+        endY: 0,
+        startTime: 0,
+        endTime: 0,
+        lastY: 0,
+        lastTime: 0
+      },
+      currIndex: 1,
+      transformY: 0,
+      scrollDistance: 0,
+      lineSpacing: 36,
+      rotation: 20,
+      timer: null
+    });
+
+    const roller = ref(null);
+    const list = ref(null);
+    const listItem = ref(null);
+
+    const touchDeg = ref(0);
+    const touchTime = ref(0);
+    const touchTranslateY = ref(0);
+    const touchListStyle = computed(() => {
+      return {
+        transition: `transform ${touchTime.value}ms cubic-bezier(0.19, 1, 0.22, 1)`,
+        transform: `translate3d(0, ${state.scrollDistance}px, 0)`
+      };
+    });
+
+    const touchRollerStyle = computed(() => {
+      return {
+        transition: `transform ${touchTime.value}ms cubic-bezier(0.19, 1, 0.22, 1)`,
+        transform: `rotate3d(1, 0, 0, ${touchDeg.value})`
+      };
+    });
+
+    const onTouchStart = (event: TouchEvent) => {
+      console.log(event);
+      event.preventDefault();
+      let changedTouches = event.changedTouches[0];
+      state.touchParams.startY = changedTouches.pageY;
+      state.touchParams.startTime = event.timeStamp || Date.now();
+      state.transformY = state.scrollDistance;
+    };
+
+    const onTouchMove = (event: TouchEvent) => {
+      event.preventDefault();
+      let changedTouches = event.changedTouches[0];
+      (state.touchParams as TouchParams).lastY = changedTouches.pageY;
+      (state.touchParams as TouchParams).lastTime = event.timeStamp || Date.now();
+      let move = state.touchParams.lastY - state.touchParams.startY;
+
+      setMove(move);
+    };
+
+    const onTouchEnd = (event: TouchEvent) => {
+      event.preventDefault();
+
+      let changedTouches = event.changedTouches[0];
+      state.touchParams.lastY = changedTouches.pageY;
+      state.touchParams.lastTime = event.timeStamp || Date.now();
+      let move = state.touchParams.lastY - state.touchParams.startY;
+
+      let moveTime = state.touchParams.lastTime - state.touchParams.startTime;
+      if (moveTime <= 300) {
+        move = move * 2;
+        moveTime = moveTime + 1000;
+        setMove(move, 'end', moveTime);
+      } else {
+        setMove(move, 'end');
+      }
+    };
+
+    const setRollerStyle = (index: number) => {
+      return `transform: rotate3d(1, 0, 0, ${-state.rotation * index}deg) translate3d(0px, 0px, 104px)`;
+    };
+
+    const isHidden = (index: number) => {
+      if (index >= state.currIndex + 8 || index <= state.currIndex - 8) {
+        return true;
+      } else {
+        return false;
+      }
+    };
+
+    const setTransform = (translateY = 0, type: string | null, time = 1000, deg: string | number) => {
+      if (type === 'end') {
+        touchTime.value = time;
+      } else {
+        touchTime.value = 0;
+      }
+      touchDeg.value = deg as number;
+      touchTranslateY.value = translateY;
+      state.scrollDistance = translateY;
+    };
+
+    const setMove = (move: number, type?: string, time?: number) => {
+      let updateMove = move + state.transformY;
+      if (type === 'end') {
+        // 限定滚动距离
+        if (updateMove > 0) {
+          updateMove = 0;
+        }
+        if (updateMove < -(props.listData.values.length - 1) * state.lineSpacing) {
+          updateMove = -(props.listData.values.length - 1) * state.lineSpacing;
+        }
+
+        // 设置滚动距离为lineSpacing的倍数值
+        let endMove = Math.round(updateMove / state.lineSpacing) * state.lineSpacing;
+        let deg = `${(Math.abs(Math.round(endMove / state.lineSpacing)) + 1) * state.rotation}deg`;
+        setTransform(endMove, type, time, deg);
+
+        let t = time ? time / 2 : 0;
+        (state.timer as any) = setTimeout(() => {
+          setChooseValue();
+        }, t);
+
+        state.currIndex = Math.abs(Math.round(endMove / state.lineSpacing)) + 1;
+      } else {
+        let deg = '0deg';
+        if (updateMove < 0) {
+          deg = `${(Math.abs(updateMove / state.lineSpacing) + 1) * state.rotation}deg`;
+        } else {
+          deg = `${(-updateMove / state.lineSpacing + 1) * state.rotation}deg`;
+        }
+
+        setTransform(updateMove, null, undefined, deg);
+        state.currIndex = Math.abs(Math.round(updateMove / state.lineSpacing)) + 1;
+      }
+    };
+
+    const setChooseValue = () => {
+      emit('change', state.currIndex - 1);
+    };
+
+    const modifyStatus = (type: boolean) => {
+      let index = props.defaultIndex;
+
+      state.currIndex = index === -1 ? 1 : (index as number) + 1;
+      let move = index === -1 ? 0 : (index as number) * state.lineSpacing;
+      type && setChooseValue();
+      setMove(-move);
+    };
+
+    watch(
+      () => props.listData,
+      (val) => {
+        state.transformY = 0;
+        modifyStatus(false);
+      },
+      {
+        deep: true
+      }
+    );
+
+    watch(
+      () => props.itemShow,
+      (val) => {
+        setTimeout(() => {
+          Taro.createSelectorQuery()
+            .selectAll('.nut-picker-item-ref')
+            .boundingClientRect((rects) => {
+              state.lineSpacing = (rects as any)[0].height;
+            })
+            .exec();
+        }, 500);
+      },
+      {
+        deep: true
+      }
+    );
+
+    watch(
+      () => props.defaultIndex,
+      (val) => {
+        state.transformY = 0;
+        modifyStatus(false);
+      }
+    );
+
+    onMounted(() => {
+      modifyStatus(true);
+    });
+
+    return {
+      ...toRefs(state),
+      ...toRefs(props),
+      wrapper,
+      setRollerStyle,
+      isHidden,
+      roller,
+      list,
+      listItem,
+      onTouchStart,
+      onTouchMove,
+      onTouchEnd,
+      touchRollerStyle,
+      touchListStyle
+    };
+  }
+});
+</script>

+ 0 - 4
src/packages/__VUE/picker/commonProps.ts

@@ -9,10 +9,6 @@ export const commonProps = {
     type: Boolean,
     default: false
   },
-  visibleItemCount: {
-    type: [Number, String],
-    default: 7
-  },
   defaultIndex: {
     type: [Number, String],
     default: 0

+ 58 - 26
src/packages/__VUE/picker/demo.vue

@@ -1,33 +1,43 @@
 <template>
   <div class="demo">
     <h2>基础用法</h2>
-    <nut-cell title="请选择城市" :desc="desc" @click="open(1)"></nut-cell>
+    <nut-cell title="请选择城市" :desc="desc" @click="open(0)"></nut-cell>
+    <h2>默认选中项</h2>
+    <nut-cell title="请选择城市" :desc="desc1" @click="open(1)"></nut-cell>
     <h2>多列样式</h2>
     <nut-cell title="请选择时间" :desc="desc2" @click="open(2)"></nut-cell>
     <h2>多级联动</h2>
     <nut-cell title="请选择地址" :desc="desc3" @click="open(3)"></nut-cell>
+    <h2>动态设置</h2>
+    <nut-cell title="请选择地址" :desc="desc4" @click="open(4)"></nut-cell>
 
     <nut-picker
       v-model:visible="show"
       :list-data="listData1"
       title="城市选择"
-      @confirm="confirm"
+      @confirm="(val) => confirm(0, val)"
       @close="close"
     >
     </nut-picker>
     <nut-picker
-      v-model:visible="show2"
-      :list-data="listData2"
-      title="多列选择"
-      @confirm="confirm2"
+      v-model:visible="show1"
+      :list-data="listData1"
+      title="城市选择"
+      :defaultIndex="2"
+      @confirm="(val) => confirm(1, val)"
       @close="close"
     >
     </nut-picker>
+    <nut-picker v-model:visible="show2" :list-data="listData2" title="多列选择" @confirm="confirm2" @close="close">
+    </nut-picker>
+    <nut-picker v-model:visible="show3" :list-data="listData3" title="地址选择" @confirm="confirm3"></nut-picker>
     <nut-picker
-      v-model:visible="show3"
-      :list-data="listData3"
+      v-model:visible="show4"
+      :list-data="listData4"
+      :demoIndex="demoIndex"
       title="地址选择"
-      @confirm="confirm3"
+      @change="onChange"
+      @confirm="(val) => confirm(4, val)"
     ></nut-picker>
   </div>
 </template>
@@ -38,15 +48,8 @@ const { createDemo } = createComponent('picker');
 export default createDemo({
   props: {},
   setup() {
-    const listData1 = [
-      '南京市',
-      '无锡市',
-      '海北藏族自治区',
-      '北京市',
-      '连云港市',
-      '浙江市',
-      '江苏市'
-    ];
+    const listData1 = ['南京市', '无锡市', '海北藏族自治区', '北京市', '连云港市', '浙江市', '江苏市'];
+
     const listData2 = [
       {
         values: ['周一', '周二', '周三', '周四', '周五'],
@@ -86,43 +89,72 @@ export default createDemo({
         ]
       }
     ];
+
+    const cities = {
+      浙江: ['杭州', '宁波', '温州', '嘉兴', '湖州'],
+      福建: ['福州', '厦门', '莆田', '三明']
+    };
+
+    const listData4 = ref([
+      {
+        values: Object.keys(cities)
+      },
+      {
+        values: cities['浙江']
+      }
+    ]);
+
     const show = ref(false);
+    const show1 = ref(false);
     const show2 = ref(false);
     const show3 = ref(false);
-    const showList = [show, show2, show3];
+    const show4 = ref(false);
+    const showList = [show, show1, show2, show3, show4];
     const desc = ref(listData1[0]);
+    const desc1 = ref(listData1[2]);
     const desc2 = ref(
-      `${listData2[0].values[listData2[0].defaultIndex]} ${
-        listData2[1].values[listData2[1].defaultIndex]
-      }`
+      `${listData2[0].values[listData2[0].defaultIndex]} ${listData2[1].values[listData2[1].defaultIndex]}`
     );
     const desc3 = ref(
       `${listData3[0].text}
       ${listData3[0].children[0].text}
       ${listData3[0].children[0].children[0].text}`
     );
-    const descList = [desc, desc2, desc3];
+    const desc4 = ref('浙江 杭州');
+    const descList = [desc, desc1, desc2, desc3, desc4];
     return {
       listData1,
       listData2,
       listData3,
+      listData4,
       show,
+      show1,
       show2,
       show3,
+      show4,
       desc,
+      desc1,
       desc2,
       desc3,
+      desc4,
       open: (index: number) => {
-        showList[index - 1].value = true;
+        showList[index].value = true;
       },
-      confirm: (res: any) => {
-        desc.value = res;
+      confirm: (type: number, res: any) => {
+        if (type == 4) {
+          descList[type].value = res[0] + ' ' + res[1];
+        } else {
+          descList[type].value = res;
+        }
       },
       confirm2: (res: any) => {
         desc2.value = res.join(' ');
       },
       confirm3: (res: any) => {
         desc3.value = res.join(' ');
+      },
+      onChange: (res: any, columnIndex, dataIndex) => {
+        listData4.value[1].values = cities[res[0]];
       }
     };
   }

+ 120 - 33
src/packages/__VUE/picker/doc.md

@@ -50,13 +50,48 @@ app.use(Popup);
       const confirm = (res)=>{
         desc.value = res;
       }
-      return {
-        show,
-        desc,
-        listData,
-        open,
-        confirm
-      };
+      return {show,desc,listData,open, confirm};
+    }
+  };
+</script>
+```
+:::
+
+### 默认选中项
+:::demo
+```html
+<template>
+  <nut-cell title="请选择城市" :desc="desc" @click="open"></nut-cell>
+  <nut-picker
+      v-model:visible="show"
+      :list-data="listData"
+      title="城市选择"
+      @confirm="confirm" 
+      :defaultIndex="2"
+  ></nut-picker>
+</template>
+<script>
+  import { ref } from 'vue';
+  export default {
+    setup(props) {
+      const show = ref(false);
+      const listData = [
+        '南京市',
+        '无锡市',
+        '海北藏族自治区',
+        '北京市',
+        '连云港市',
+        '浙江市',
+        '江苏市'
+      ];
+      const desc = ref(listData[2]);
+      const open = ()=>{
+        show.value = true;
+      }
+      const confirm = (res)=>{
+        desc.value = res;
+      }
+      return {show,desc,listData,open, confirm};
     }
   };
 </script>
@@ -65,17 +100,15 @@ app.use(Popup);
 
 ### 多列样式
 
-### 基础用法
 :::demo
 ```html
 <template>
-  <nut-cell title="请选择时间" :desc="desc" @click="open"></nut-cell>
+  <nut-cell title="请选择城市" :desc="desc" @click="open"></nut-cell>
   <nut-picker
       v-model:visible="show"
       :list-data="listData"
-      title="多列选择"
-      @confirm="confirm"
-      @close="close"
+      title="城市选择"
+      @confirm="confirm" 
   ></nut-picker>
 </template>
 <script>
@@ -94,30 +127,21 @@ app.use(Popup);
           defaultIndex: 1
         }
       ];
-      const desc = ref(
-      `${listData[0].values[listData[0].defaultIndex]} ${
-        listData[1].values[listData[1].defaultIndex]
-      }`
-    );
-
-  
-      return {
-        show,
-        desc,
-        listData,
-        open: (index) => {
-          show.value = true;
-        },
-        confirm: (res) => {
-          desc.value = res.join(' ');
-        }
-      };
+      const desc = ref(`${listData[0].values[listData[0].defaultIndex]} ${listData[1].values[listData[1].defaultIndex]}`);
+      const open = ()=>{
+        show.value = true;
+      }
+      const confirm = (res)=>{
+        desc.value = res.join(' ');
+      }
+      return {show,desc,listData,open, confirm};
     }
   };
 </script>
 ```
 :::
 
+
 ### 多级联动
 
 :::demo
@@ -188,7 +212,60 @@ app.use(Popup);
 ```
 :::
 
+### 动态设置
+:::demo
+```html
+<template>
+  <nut-cell title="请选择时间" :desc="desc" @click="open"></nut-cell>
+  <nut-picker
+      v-model:visible="show"
+      :list-data="listData"
+      :demoIndex="demoIndex"
+      title="地址选择"
+      @change="onChange"
+      @confirm="confirm"
+    ></nut-picker>
+</template>
+<script>
+  import { ref } from 'vue';
+  export default {
+    setup(props) {
+      const show = ref(false);
+      const cities = {
+        浙江: ['杭州', '宁波', '温州', '嘉兴', '湖州'],
+        福建: ['福州', '厦门', '莆田', '三明']
+      };
 
+      const listData = ref([
+        {
+          values: Object.keys(cities)
+        },
+        {
+          values: cities['浙江']
+        }
+      ]);
+      const desc = ref('浙江 杭州');
+
+  
+      return {
+        show,
+        desc,
+        listData,
+        open: (index) => {
+          show.value = true;
+        },
+        confirm: (res) => {
+          desc.value = res[0]+' '+res[1];
+        },
+        onChange: (res, columnIndex, dataIndex) => {
+          listData.value[1].values = cities[res[0]];
+        }
+      };
+    }
+  };
+</script>
+```
+:::
 
 ## API
     
@@ -200,17 +277,27 @@ app.use(Popup);
 | title                  | 设置标题                   | String  | -      |
 | cancel-text            | 取消按钮文案               | String  | 取消   |
 | ok-text                | 确定按钮文案               | String  | 确定   |
-| list-data              | 列表数据                   | Array   | -      |
-| default-value-index    | 初始选中项的索引,默认为 0 | number  | 0      |
+| list-data              | 列表数据                   | Column[]   | -      |
+| default-index    | 单列选择时,初始选中项的索引,默认为 0 | number  | 0      |
 | teleport               | 指定挂载节点               | String  | "body" |
 | close-on-click-overlay | 点击蒙层是否关闭对话框     | Boolean | false  |
 | lock-scroll            | 背景是否锁定               | Boolean | false  |
    
+### Column 数据结构
+
+当传入多列时,columns 为一个对象数组,数组中的每一个对象配置每一列
+    
+| 事件名  | 说明             | 回调参数     |
+|---------|------------------|--------------|
+| values   | 列中对应的值   | string |
+| default-index | 初始选中项的索引,默认值 0 | number |
+| children  | 级联选项       | Column |
+
 ### Events
     
 | 事件名  | 说明             | 回调参数     |
 |---------|------------------|--------------|
 | close   | 关闭弹窗时触发   | event: Event |
 | confirm | 点击确认时候触发 | event: Event |
-| change  | 改变时触发       | val          |
+| change  | 改变时触发       | 选中的值, 第几列, 第几个 |
     

+ 73 - 17
src/packages/__VUE/picker/index.scss

@@ -1,4 +1,9 @@
 .nut-picker {
+  color: #f00;
+
+  view {
+    display: block;
+  }
   &__content {
     display: block;
     position: relative;
@@ -8,31 +13,19 @@
       cursor: grab;
     }
   }
-  &__mask {
-    position: absolute;
-    top: 0;
-    left: 0;
-    z-index: 1;
-    width: 100%;
-    height: 100%;
-    background-image: linear-gradient(180deg, hsla(0, 0%, 100%, 0.9), hsla(0, 0%, 100%, 0.4)),
-      linear-gradient(0deg, hsla(0, 0%, 100%, 0.9), hsla(0, 0%, 100%, 0.4));
-    background-repeat: no-repeat;
-    background-position: top, bottom;
-    -webkit-transform: translateZ(0);
-    transform: translateZ(0);
-    pointer-events: none;
-  }
+
   &__bar {
     display: flex;
     height: 56px;
     align-items: center;
     justify-content: space-between;
-    padding: 15px;
+    padding: 0 15px;
+    font-size: 16px;
   }
   &__column {
     display: flex;
     position: relative;
+    height: 252px;
   }
   &__columnitem {
     width: 0;
@@ -57,10 +50,73 @@
   }
   &__hairline {
     position: absolute;
-    height: 35px;
+    top: 108px;
+    height: 34px;
     width: 100%;
     border: 1px solid #d8d8d8;
     border-left: 0;
     border-right: 0;
   }
+
+  &__list {
+    position: relative;
+    display: block;
+    width: 100%;
+    height: 252px;
+    overflow: hidden;
+    text-align: center;
+  }
+
+  &-roller {
+    display: block;
+    position: absolute;
+    top: 108px;
+    width: 100%;
+    height: 36px;
+    z-index: 1;
+    transform-style: preserve-3d;
+
+    &-item {
+      display: block;
+      backface-visibility: hidden;
+      position: absolute;
+      top: 0;
+      width: 100%;
+      height: 36px;
+      line-height: 36px;
+      color: #808080;
+      font-size: 16px;
+
+      &-hidden {
+        visibility: hidden;
+        opacity: 0;
+      }
+    }
+  }
+
+  &-content {
+    display: block;
+    position: absolute;
+    top: 108px;
+    width: 100%;
+    height: 36px;
+    z-index: 2;
+    overflow: hidden;
+    border-left: 0;
+    border-right: 0;
+  }
+  &-list-panel {
+    display: block;
+    transform-style: preserve-3d;
+  }
+  &-item {
+    display: block;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    height: 36px;
+    line-height: 36px;
+    text-align: center;
+    font-size: 16px;
+  }
 }

+ 193 - 54
src/packages/__VUE/picker/index.taro.vue

@@ -1,83 +1,222 @@
 <template>
-  <picker
-    :mode="mode"
-    :range="range"
-    @change="onChange"
-    @columnchange="onColumnChange"
-    :value="value"
+  <nut-popup
+    position="bottom"
+    v-model:visible="show"
+    :teleport="teleport"
+    :lock-scroll="lockScroll"
+    :close-on-click-overlay="closeOnClickOverlay"
+    @close="close"
+    :round="true"
   >
-    <slot></slot>
-  </picker>
-</template>
+    <view class="nut-picker__bar">
+      <view class="nut-picker__left nut-picker__button" @click="close">{{ cancelText }}</view>
+      <view> {{ title }}</view>
+      <view class="nut-picker__button" @click="confirm()">{{ okText }}</view>
+    </view>
 
+    <view class="nut-picker__column">
+      <view class="nut-picker__hairline"></view>
+      <view class="nut-picker__columnitem" v-for="(item, columnIndex) in columnList" :key="columnIndex">
+        <nut-picker-column
+          :itemShow="show"
+          :list-data="item"
+          :readonly="readonly"
+          :default-index="item.defaultIndex"
+          :visible-item-count="visibleItemCount"
+          :data-type="dataType"
+          @change="
+            (dataIndex) => {
+              changeHandler(columnIndex, dataIndex);
+            }
+          "
+        ></nut-picker-column>
+      </view>
+    </view>
+  </nut-popup>
+</template>
 <script lang="ts">
-import { onUpdated, ref, watch } from 'vue';
+import { reactive, watch, computed, toRaw, toRefs } from 'vue';
 import { createComponent } from '../../utils/create';
-const { create } = createComponent('picker');
+import column from './ColumnTaro.vue';
+import popup, { popupProps } from '../popup/index.vue';
 import { commonProps } from './commonProps';
+import { PickerObjectColumn, PickerObjectColumns } from './types';
+const { create, componentName } = createComponent('picker');
+
 export default create({
+  components: {
+    [column.name]: column
+  },
   props: {
-    mode: {
+    ...popupProps,
+    title: {
       type: String,
-      default: 'selector'
+      default: ''
+    },
+    cancelText: {
+      type: String,
+      default: '取消'
+    },
+    okText: {
+      type: String,
+      default: '确定'
     },
     ...commonProps
   },
-  emits: ['confirm'],
+  emits: ['close', 'change', 'confirm', 'update:visible'],
   setup(props, { emit }) {
-    let value = ref<any>([]);
-    let range = ref<any>([]);
+    const childrenKey = 'children';
+    const valuesKey = 'values';
+    const state = reactive({
+      show: false,
+      formattedColumns: props.listData as PickerObjectColumn[],
+      defaultIndex: props.defaultIndex as number
+    });
+    //临时变量,当点击确定时候赋值
+    let _defaultIndex = props.defaultIndex;
+    let defaultIndexList: number[] = [];
 
-    onUpdated(() => {
-      console.log('updated', props.listData);
+    const classes = computed(() => {
+      const prefixCls = componentName;
+      return {
+        [prefixCls]: true
+      };
     });
 
-    const onChange = (e: any) => {
-      let ret;
+    const dataType = computed(() => {
+      const firstColumn = state.formattedColumns[0] as PickerObjectColumn;
+      if (typeof firstColumn === 'object') {
+        if (firstColumn[childrenKey]) {
+          return 'cascade';
+        } else if (firstColumn?.[valuesKey]) {
+          addDefaultIndexList(props.listData as PickerObjectColumn[]);
+          return 'multipleColumns';
+        }
+      }
+      return 'text';
+    });
 
-      if (props.mode === 'selector') {
-        ret = props.listData[e.detail.value];
-      } else if (props.mode === 'multiSelector') {
-        ret = range.value
-          ?.map((item: any, idx: number) => item[e.detail.value[idx]])
-          .filter((res: any) => res);
+    const columnList = computed(() => {
+      if (dataType.value === 'text') {
+        return [{ values: state.formattedColumns, defaultIndex: state.defaultIndex }];
+      } else if (dataType.value === 'multipleColumns') {
+        return state.formattedColumns;
+      } else if (dataType.value === 'cascade') {
+        return formatCascade(state.formattedColumns as PickerObjectColumn[], state.defaultIndex);
       }
-      emit('confirm', e.detail.value, ret);
+      return state.formattedColumns;
+    });
+
+    const addDefaultIndexList = (listData: PickerObjectColumn[]) => {
+      defaultIndexList = [];
+      listData.forEach((res) => {
+        defaultIndexList.push((res.defaultIndex as number) || 0);
+      });
     };
 
-    watch(
-      props.listData,
-      (val: any) => {
-        try {
-          if (val.length) {
-            value.value = [];
-            range.value = [];
-            if (props.mode === 'selector') {
-              range.value = props.listData;
-            } else if (props.mode === 'multiSelector') {
-              val.forEach((item: any) => {
-                value.value.push(item.defaultIndex);
-                range.value.push(item.values);
-              });
-            }
+    const formatCascade = (listData: PickerObjectColumn[], defaultIndex: number) => {
+      const formatted: PickerObjectColumn[] = [];
+      let children = listData as PickerObjectColumns;
+      children.defaultIndex = defaultIndex;
+      while (children) {
+        formatted.push({
+          values: children,
+          defaultIndex: children.defaultIndex || 0
+        });
+        children = children?.[children.defaultIndex || 0].children;
+      }
+      addDefaultIndexList(formatted);
+      return formatted;
+    };
+
+    const getCascadeData = (listData: PickerObjectColumn[], defaultIndex: number) => {
+      let arr = listData as PickerObjectColumns;
+      arr.defaultIndex = defaultIndex;
+      const dataList: string[] = [];
+
+      while (arr) {
+        const item = arr[arr.defaultIndex ?? 0];
+        dataList.push(item.text as string);
+        arr = item.children;
+      }
+      return dataList;
+    };
+
+    const close = () => {
+      emit('close');
+      emit('update:visible', false);
+    };
+
+    const changeHandler = (columnIndex: number, dataIndex: number) => {
+      if (dataType.value === 'cascade') {
+        let cursor = state.formattedColumns as PickerObjectColumns;
+        if (columnIndex === 0) {
+          state.defaultIndex = dataIndex;
+        }
+        let i = 0;
+        while (cursor) {
+          if (i === columnIndex) {
+            cursor.defaultIndex = dataIndex;
+          } else if (i > columnIndex) {
+            cursor.defaultIndex = 0;
           }
-        } catch (error) {
-          console.log('listData参数格式错误', error);
+          cursor = cursor[cursor.defaultIndex || 0].children;
+          i++;
         }
-      },
-      { immediate: true, deep: true }
-    );
+      } else if (dataType.value === 'text') {
+        _defaultIndex = dataIndex;
+      } else if (dataType.value === 'multipleColumns') {
+        defaultIndexList[columnIndex] = dataIndex;
+        const val = defaultIndexList.map(
+          (res, i) => toRaw(state.formattedColumns as PickerObjectColumns)[i].values[res]
+        );
 
-    const onColumnChange = (e: any) => {
-      console.log('修改的列为', e.detail.column, ',值为', e.detail.value);
+        emit('change', val, columnIndex, dataIndex);
+      }
     };
 
+    const confirm = () => {
+      if (dataType.value === 'text') {
+        state.defaultIndex = _defaultIndex as number;
+        emit('confirm', state.formattedColumns[_defaultIndex as number]);
+      } else if (dataType.value === 'multipleColumns') {
+        for (let i = 0; i < defaultIndexList.length; i++) {
+          state.formattedColumns[i].defaultIndex = defaultIndexList[i];
+        }
+        const checkedArr = toRaw(state.formattedColumns).map(
+          (res: PickerObjectColumn) => res.values && res.values[res.defaultIndex as number]
+        );
+        emit('confirm', checkedArr);
+      } else if (dataType.value === 'cascade') {
+        emit('confirm', getCascadeData(toRaw(state.formattedColumns), state.defaultIndex));
+      }
+
+      emit('update:visible', false);
+    };
+
+    watch(
+      () => props.visible,
+      (val) => {
+        state.show = val;
+      }
+    );
+
+    watch(
+      () => props.listData,
+      (val) => {
+        state.formattedColumns = val as PickerObjectColumns;
+      }
+    );
+
     return {
-      confirm,
-      onChange,
-      value,
-      range,
-      onColumnChange
+      classes,
+      ...toRefs(state),
+      column,
+      dataType,
+      columnList,
+      close,
+      changeHandler,
+      confirm
     };
   }
 });

+ 17 - 56
src/packages/__VUE/picker/index.vue

@@ -2,38 +2,28 @@
   <view :class="classes">
     <nut-popup
       position="bottom"
-      :style="{ height: height + 56 + 'px' }"
       v-model:visible="show"
       :teleport="teleport"
       :lock-scroll="lockScroll"
       :close-on-click-overlay="closeOnClickOverlay"
       @close="close"
+      :round="true"
     >
       <view class="nut-picker__bar">
-        <view class="nut-picker__left nut-picker__button" @click="close">{{
-          cancelText
-        }}</view>
+        <view class="nut-picker__left nut-picker__button" @click="close">{{ cancelText }}</view>
         <view> {{ title }}</view>
         <view class="nut-picker__button" @click="confirm()">{{ okText }}</view>
       </view>
 
       <view class="nut-picker__column">
-        <view
-          class="nut-picker__mask"
-          :style="{ backgroundSize: `100% ${top}px` }"
-        ></view>
-        <view class="nut-picker__hairline" :style="{ top: ` ${top}px` }"></view>
-        <view
-          class="nut-picker__columnitem"
-          v-for="(item, columnIndex) in columnList"
-          :key="columnIndex"
-        >
+        <view class="nut-picker__hairline"></view>
+        <view class="nut-picker__columnitem" v-for="(item, columnIndex) in columnList" :key="columnIndex">
           <nut-picker-column
-            :list-data="item.values"
+            :itemShow="show"
+            :list-data="item"
             :readonly="readonly"
             :default-index="item.defaultIndex"
             :visible-item-count="visibleItemCount"
-            :item-height="itemHeight"
             :data-type="dataType"
             @change="
               (dataIndex) => {
@@ -52,12 +42,8 @@ import { createComponent } from '../../utils/create';
 import column from './Column.vue';
 import popup, { popupProps } from '../popup/index.vue';
 import { commonProps } from './commonProps';
-import {
-  PickerObjOpt,
-  PickerOption,
-  PickerObjectColumn,
-  PickerObjectColumns
-} from './types';
+import { PickerObjOpt, PickerOption, PickerObjectColumn, PickerObjectColumns } from './types';
+
 const { create, componentName } = createComponent('picker');
 
 export default create({
@@ -101,14 +87,6 @@ export default create({
       };
     });
 
-    const top = computed(() => {
-      return (Number(+props.visibleItemCount - 1) / 2) * +props.itemHeight;
-    });
-
-    const height = computed(() => {
-      return Number(props.visibleItemCount) * +props.itemHeight;
-    });
-
     const dataType = computed(() => {
       const firstColumn = state.formattedColumns[0] as PickerObjectColumn;
       if (typeof firstColumn === 'object') {
@@ -124,16 +102,11 @@ export default create({
 
     const columnList = computed(() => {
       if (dataType.value === 'text') {
-        return [
-          { values: state.formattedColumns, defaultIndex: state.defaultIndex }
-        ];
+        return [{ values: state.formattedColumns, defaultIndex: state.defaultIndex }];
       } else if (dataType.value === 'multipleColumns') {
         return state.formattedColumns;
       } else if (dataType.value === 'cascade') {
-        return formatCascade(
-          state.formattedColumns as PickerObjectColumn[],
-          state.defaultIndex
-        );
+        return formatCascade(state.formattedColumns as PickerObjectColumn[], state.defaultIndex);
       }
       return state.formattedColumns;
     });
@@ -145,10 +118,7 @@ export default create({
       });
     };
 
-    const formatCascade = (
-      listData: PickerObjectColumn[],
-      defaultIndex: number
-    ) => {
+    const formatCascade = (listData: PickerObjectColumn[], defaultIndex: number) => {
       const formatted: PickerObjectColumn[] = [];
       let children = listData as PickerObjectColumns;
       children.defaultIndex = defaultIndex;
@@ -163,10 +133,7 @@ export default create({
       return formatted;
     };
 
-    const getCascadeData = (
-      listData: PickerObjectColumn[],
-      defaultIndex: number
-    ) => {
+    const getCascadeData = (listData: PickerObjectColumn[], defaultIndex: number) => {
       let arr = listData as PickerObjectColumns;
       arr.defaultIndex = defaultIndex;
       const dataList: string[] = [];
@@ -205,10 +172,10 @@ export default create({
       } else if (dataType.value === 'multipleColumns') {
         defaultIndexList[columnIndex] = dataIndex;
         const val = defaultIndexList.map(
-          (res, i) =>
-            toRaw(state.formattedColumns as PickerObjectColumns)[i].values[res]
+          (res, i) => toRaw(state.formattedColumns as PickerObjectColumns)[i].values[res]
         );
-        emit('change', val);
+
+        emit('change', val, columnIndex, dataIndex);
       }
     };
 
@@ -221,15 +188,11 @@ export default create({
           state.formattedColumns[i].defaultIndex = defaultIndexList[i];
         }
         const checkedArr = toRaw(state.formattedColumns).map(
-          (res: PickerObjectColumn) =>
-            res.values && res.values[res.defaultIndex as number]
+          (res: PickerObjectColumn) => res.values && res.values[res.defaultIndex as number]
         );
         emit('confirm', checkedArr);
       } else if (dataType.value === 'cascade') {
-        emit(
-          'confirm',
-          getCascadeData(toRaw(state.formattedColumns), state.defaultIndex)
-        );
+        emit('confirm', getCascadeData(toRaw(state.formattedColumns), state.defaultIndex));
       }
 
       emit('update:visible', false);
@@ -255,8 +218,6 @@ export default create({
       column,
       dataType,
       columnList,
-      top,
-      height,
       close,
       changeHandler,
       confirm

+ 9 - 0
src/packages/__VUE/picker/types.ts

@@ -12,4 +12,13 @@ export type PickerObjectColumn = {
   [key: string]: any;
 };
 
+export type TouchParams = {
+  startY: number;
+  endY: number;
+  startTime: number;
+  endTime: number;
+  lastY: number;
+  lastTime: number;
+};
+
 export type PickerObjectColumns = PickerObjectColumn & PickerObjOpt[];

+ 14 - 9
src/packages/__VUE/sku/demo.vue

@@ -107,7 +107,6 @@
 
 <script lang="ts">
 import { reactive, ref, toRefs, onMounted } from 'vue';
-import { Sku, Goods, imagePathMap } from './data';
 
 import { createComponent } from '../../utils/create';
 import { Toast } from '@/packages/nutui.vue';
@@ -136,7 +135,8 @@ interface GoodsProps {
 
 interface Data {
   skuData: Skus[];
-  goodsInfo: GoodsProps;
+  goodsInfo: any;
+  imagePathMap: any;
 }
 
 export default createDemo({
@@ -153,7 +153,8 @@ export default createDemo({
 
     const data = reactive<Data>({
       skuData: [],
-      goodsInfo: {}
+      goodsInfo: {},
+      imagePathMap: {}
     });
 
     const stepperExtraText = () => {
@@ -206,10 +207,15 @@ export default createDemo({
     });
 
     const getData = () => {
-      setTimeout(() => {
-        data.skuData = Sku;
-        data.goodsInfo = Goods;
-      }, 500);
+      fetch('http://storage.360buyimg.com/nutui/3x/data.js')
+        .then((response) => response.json())
+        .then((res) => {
+          const { Sku, Goods, imagePathMap } = res;
+          data.skuData = Sku;
+          data.goodsInfo = Goods;
+          data.imagePathMap = imagePathMap;
+        }) //执行结果是 resolve就调用then方法
+        .catch((err) => console.log('Oh, error', err)); //执行结果是 reject就调用catch方法
     };
     const selectSku = (s: any) => {
       const { sku, parentIndex } = s;
@@ -227,7 +233,7 @@ export default createDemo({
 
       data.skuData[0].list.forEach((el) => {
         if (el.active && !el.disable) {
-          data.goodsInfo.imagePath = imagePathMap[el.id];
+          data.goodsInfo.imagePath = data.imagePathMap[el.id];
         }
       });
     };
@@ -262,7 +268,6 @@ export default createDemo({
     return {
       selectSku,
       changeStepper,
-      Goods,
       clickBtnOperate,
       close,
       existAddress,

+ 225 - 157
src/packages/__VUE/sku/doc.md

@@ -25,88 +25,111 @@ app.use(Price);
 
 ### 基础用法
 
+:::demo
 ```html
-<nut-sku
-  v-model:visible="base"
-  :sku="sku"
-  :goods="goods"
-  @selectSku="selectSku"
-  @clickBtnOperate="clickBtnOperate"
-  @close="close"
-></nut-sku>
-```
-
-```javascript
-setup() {
-    const base = ref(false);
-    const data = reactive({
-      sku: [
-          // 具体数据结构见下方文档
-        ],
-      goods: {
-          // 具体数据结构见下方文档
-        }
-    });
+<template>
+  <nut-cell :title="`基本用法`" desc="" @click="base = true"></nut-cell>
+  <nut-sku
+    v-model:visible="base"
+    :sku="sku"
+    :goods="goods"
+    @selectSku="selectSku"
+    @clickBtnOperate="clickBtnOperate"
+    @close="close"
+  ></nut-sku>
+</template>
+<script lang="ts">
+import { ref,reactive,onMounted,toRefs} from 'vue';
+export default {
+  setup() {
+      const base = ref(false);
+      const data = reactive({
+        sku: [],
+        goods: {}
+      });
 
-    onMounted(() => {});
-    // 切换规格类目
-    const selectSku = (ss: string) => {
-      const { sku, skuIndex, parentSku, parentIndex } = ss;
-      if (sku.disable) return false;
-      data.sku[parentIndex].list.forEach((s) => {
-        s.active = s.id == sku.id;
+      onMounted(() => {
+        fetch('https://storage.360buyimg.com/nutui/3x/data.js')
+          .then((response) => response.json())
+          .then((res) => {
+            const { Sku, Goods, imagePathMap } = res;
+            data.sku = Sku;
+            data.goods = Goods;
+          }) //执行结果是 resolve就调用then方法
+          .catch((err) => console.log('Oh, error', err)); //执行结果是 reject就调用catch方法
       });
-      data.goods = {
-        skuId: sku.id,
-        price: '4599.00',
-        imagePath:
-          '//img14.360buyimg.com/n4/jfs/t1/215845/12/3788/221990/618a5c4dEc71cb4c7/7bd6eb8d17830991.jpg' 
+      // 切换规格类目
+      const selectSku = (ss: string) => {
+        const { sku, skuIndex, parentSku, parentIndex } = ss;
+        if (sku.disable) return false;
+        data.sku[parentIndex].list.forEach((s) => {
+          s.active = s.id == sku.id;
+        });
+        data.goods = {
+          skuId: sku.id,
+          price: '4599.00',
+          imagePath:
+            '//img14.360buyimg.com/n4/jfs/t1/215845/12/3788/221990/618a5c4dEc71cb4c7/7bd6eb8d17830991.jpg' 
+        };
       };
-    };
-    // 底部操作按钮触发
-    const clickBtnOperate = (op:string)=>{
-      console.log('点击了操作按钮',op)
-    } 
-    // 关闭商品规格弹框
-    const close = ()=>{}
-    return { base, selectSku, clickBtnOperate,close, ...toRefs(data) };
+      // 底部操作按钮触发
+      const clickBtnOperate = (op:string)=>{
+        console.log('点击了操作按钮',op)
+      } 
+      // 关闭商品规格弹框
+      const close = ()=>{}
+      return { base, selectSku, clickBtnOperate,close, ...toRefs(data) };
+  }
 }
+</script>
 ```
+:::
 
 ### 不可售
 
+:::demo
 ```html
-<nut-sku
-  v-model:visible="notSell"
-  :sku="skuData"
-  :goods="goodsInfo"
-  :btnExtraText="btnExtraText"
-  @changeStepper="changeStepper"
-  @selectSku="selectSku"
-  @close="close"
->
-  <template #sku-operate>
-    <div class="sku-operate-box">
-      <nut-button class="sku-operate-box-dis" type="warning">查看相似商品</nut-button>
-      <nut-button class="sku-operate-box-dis" type="info">到货通知</nut-button>
-    </div>
-  </template>
-</nut-sku>
-```
-
-```javascript
+<template>
+  <nut-cell title="不可售" desc="" @click="notSell = true"></nut-cell>
+  <nut-sku
+    v-model:visible="notSell"
+    :sku="sku"
+    :goods="goods"
+    :btnExtraText="btnExtraText"
+    @changeStepper="changeStepper"
+    @selectSku="selectSku"
+  >
+    <template #sku-operate>
+      <div class="sku-operate-box">
+        <nut-button class="sku-operate-box-dis" type="warning">查看相似商品</nut-button>
+        <nut-button class="sku-operate-box-dis" type="info">到货通知</nut-button>
+      </div>
+    </template>
+  </nut-sku>
+</template>
+<script lang="ts">
+import { ref,reactive,onMounted,toRefs} from 'vue';
+export default {
 setup() {
     const notSell = ref(false);
     const data = reactive({
-      sku: [
-          // 数据结构见下方文档
-        ],
-      goods: {
-          // 数据结构见下方文档
-        }
+      sku: [],
+      goods: {}
     });
 
     const btnExtraText = ref('抱歉,此商品在所选区域暂无存货');
+
+    onMounted(() => {
+        fetch('https://storage.360buyimg.com/nutui/3x/data.js')
+          .then((response) => response.json())
+          .then((res) => {
+            const { Sku, Goods, imagePathMap } = res;
+            data.sku = Sku;
+            data.goods = Goods;
+          }) //执行结果是 resolve就调用then方法
+          .catch((err) => console.log('Oh, error', err)); //执行结果是 reject就调用catch方法
+    });
+
     // inputNumber 更改
     const changeStepper = (count: number) => {
       console.log('购买数量', count);
@@ -131,58 +154,68 @@ setup() {
       console.log('点击了操作按钮',op)
     } 
     return { notSell, changeStepper,selectSku,btnExtraText,...toRefs(data) };
+  }
 }
-```
-
-```css
+</script>
+<style>
 .sku-operate-box {
   width: 100%;
   display: flex;
   padding: 8px 10px;
   box-sizing: border-box;
-
-  .sku-operate-box-dis{
-    width: 100%;
-    flex-shrink: 1;
-    &:first-child{
-      margin-right: 18px;
-    }
-  }
 }
+.sku-operate-box-dis{
+    flex:1
+}
+.sku-operate-box-dis:first-child{
+  margin-right: 18px;
+}
+</style>
 ```
+:::
 
 ### 自定义步进器
 
 可以按照需求配置数字输入框的最大值、最小值、文案等
 
+:::demo
 ```html
-<nut-sku
-  v-model:visible="customStepper"
-  :sku="sku"
-  :goods="goods"
-  :showSaleLimit="true"
-  :stepperMax="7"
-  :stepperMin="2"
-  :stepperExtraText="stepperExtraText"
-  @changeStepper="changeStepper"
-  @overLimit="overLimit"
-  :btnOptions="['buy', 'cart']"
-  @selectSku="selectSku"
-  @clickBtnOperate="clickBtnOperate"
-  @close="close"
-></nut-sku>
-```
-
-```javascript
+<template>
+  <nut-cell title="自定义计步器" desc="" @click="customStepper = true"></nut-cell>
+  <nut-sku
+    v-model:visible="customStepper"
+    :sku="sku"
+    :goods="goods"
+    :showSaleLimit="true"
+    :stepperMax="7"
+    :stepperMin="2"
+    :stepperExtraText="stepperExtraText"
+    @changeStepper="changeStepper"
+    @overLimit="overLimit"
+    :btnOptions="['buy', 'cart']"
+    @selectSku="selectSku"
+    @clickBtnOperate="clickBtnOperate"
+  ></nut-sku>
+</template>
+<script lang="ts">
+import { ref,reactive,onMounted,toRefs} from 'vue';
+export default {
 setup() {
     const customStepper = ref(false);
     const data = reactive({
-      sku: [
-          // 数据结构见下方文档
-        ],
-      goods: {
-          // 数据结构见下方文档
-        }
+      sku: [],
+      goods: {}
+    });
+
+    onMounted(() => {
+        fetch('https://storage.360buyimg.com/nutui/3x/data.js')
+          .then((response) => response.json())
+          .then((res) => {
+            const { Sku, Goods, imagePathMap } = res;
+            data.sku = Sku;
+            data.goods = Goods;
+          }) //执行结果是 resolve就调用then方法
+          .catch((err) => console.log('Oh, error', err)); //执行结果是 reject就调用catch方法
     });
 
     const stepperExtraText = () => {
@@ -219,73 +252,75 @@ setup() {
     const clickBtnOperate = (op:string)=>{
       console.log('点击了操作按钮',op)
     } 
-    return { overLimit, changeStepper,selectSku, clickBtnOperate,stepperExtraText,...toRefs(data) };
+    return { customStepper, overLimit, changeStepper,selectSku, clickBtnOperate,stepperExtraText,...toRefs(data) };
 }
+}
+</script>
 ```
-
+::: 
 ### 自定义插槽
 
 Sku 组件默认划分为若干区域,这些区域都定义成了插槽,可以按照需求进行替换。
 
+:::demo
 ```html
-<nut-sku
-    v-model:visible="customBySlot"
-    :sku="sku"
-    :goods="goods"
-    :btnOptions="['buy', 'cart']"
-    @selectSku="selectSku"
-    @clickBtnOperate="clickBtnOperate"
-    @close="close()"
->
-    <!-- 商品展示区,价格区域 -->
-    <template #sku-header-price>
-        <div>
-            <nut-price :price="goodsInfo.price" :needSymbol="true" :thousands="false"> </nut-price>
-            <span class="tag"></span>
-        </div>
-    </template> 
-    <!-- 商品展示区,编号区域 -->
-    <template #sku-header-extra>
-        <span class="nut-sku-header-right-extra">重量:0.1kg  编号:{{skuId}}  </span>
-    </template> 
-    <!-- sku 展示区上方与商品信息展示区下方区域,无默认展示内容 -->
-    <template #sku-select-top>
-        <div class="address">
-            <nut-cell style="box-shadow:none;padding:13px 0" title="送至" :desc="addressDesc" @click="showAddressPopup=true"></nut-cell>
-        </div>
-    </template>
-    <!-- 底部按钮操作区 -->
-    <template #sku-operate>
-        <div class="sku-operate-box">
-        <nut-button class="sku-operate-item" shape="square" type="warning">加入购物车</nut-button>
-        <nut-button class="sku-operate-item" shape="square" type="primary">立即购买</nut-button>
-        </div>
-    </template>
-</nut-sku>
-
-<nut-address
-  v-model:visible="showAddressPopup"
-  type="exist"
-  :exist-address="existAddress"
-  @close="close"
-  :is-show-custom-address="false"
-  @selected="selectedAddress"
-  exist-address-title="配送至"
-></nut-address>
-```
-
-```javascript
+<template>
+  <nut-cell title="通过插槽自定义设置" desc="" @click="customBySlot = true"></nut-cell>
+  <nut-sku
+      v-model:visible="customBySlot"
+      :sku="sku"
+      :goods="goods"
+      :btnOptions="['buy', 'cart']"
+      @selectSku="selectSku"
+      @clickBtnOperate="clickBtnOperate"
+  >
+      <!-- 商品展示区,价格区域 -->
+      <template #sku-header-price>
+          <div>
+              <nut-price :price="goods.price" :needSymbol="true" :thousands="false"> </nut-price>
+              <span class="tag"></span>
+          </div>
+      </template> 
+      <!-- 商品展示区,编号区域 -->
+      <template #sku-header-extra>
+          <span class="nut-sku-header-right-extra">重量:0.1kg  编号:{{skuId}}  </span>
+      </template> 
+      <!-- sku 展示区上方与商品信息展示区下方区域,无默认展示内容 -->
+      <template #sku-select-top>
+          <div class="address">
+              <nut-cell style="box-shadow:none;padding:13px 0" title="送至" :desc="addressDesc" @click="showAddressPopup=true"></nut-cell>
+          </div>
+      </template>
+      <!-- 底部按钮操作区 -->
+      <template #sku-operate>
+          <div class="sku-operate-box">
+          <nut-button class="sku-operate-item" shape="square" type="warning">加入购物车</nut-button>
+          <nut-button class="sku-operate-item" shape="square" type="primary">立即购买</nut-button>
+          </div>
+      </template>
+  </nut-sku>
+
+  <nut-address
+    v-model:visible="showAddressPopup"
+    type="exist"
+    :exist-address="existAddress"
+    :is-show-custom-address="false"
+    @selected="selectedAddress"
+    exist-address-title="配送至"
+  ></nut-address>
+
+</template>
+<script lang="ts">
+import { ref,reactive,onMounted,toRefs} from 'vue';
+export default {
 setup() {
     const customBySlot = ref(false);
     const showAddressPopup = ref(false);
     const data = reactive({
-      sku: [
-          // 数据结构见下方文档
-        ],
-      goods: {
-          // 数据结构见下方文档
-        }
+      sku: [],
+      goods: {}
     });
+
     const addressDesc = ref('(配送地会影响库存,请先确认)');
     const existAddress = ref([
       {
@@ -326,6 +361,17 @@ setup() {
       }
     ]);
 
+    onMounted(() => {
+        fetch('https://storage.360buyimg.com/nutui/3x/data.js')
+          .then((response) => response.json())
+          .then((res) => {
+            const { Sku, Goods, imagePathMap } = res;
+            data.sku = Sku;
+            data.goods = Goods;
+          }) //执行结果是 resolve就调用then方法
+          .catch((err) => console.log('Oh, error', err)); //执行结果是 reject就调用catch方法
+    });
+
     // 切换规格类目
     const selectSku = (ss: string) => {
       const { sku, skuIndex, parentSku, parentIndex } = ss;
@@ -350,8 +396,30 @@ setup() {
     } 
     return { customBySlot, selectSku, clickBtnOperate,existAddress,addressDesc,selectedAddress,...toRefs(data) };
 }
-```
+}
+</script>
 
+<style>
+.sku-operate-box {
+  width: 100%;
+  display: flex;
+  padding: 8px 10px;
+  box-sizing: border-box;
+}
+.sku-operate-item {
+    flex:1
+}
+.sku-operate-item:first-child {
+      border-top-left-radius: 20px;
+      border-bottom-left-radius: 20px;
+    }
+.sku-operate-item:last-child {
+      border-top-right-radius: 20px;
+      border-bottom-right-radius: 20px;
+    }
+</style>
+```
+:::
 
 ## API
 

+ 1 - 1
src/packages/__VUE/sku/index.vue

@@ -181,7 +181,7 @@ export default create({
     );
 
     onMounted(() => {
-      console.log('更新参数');
+      // console.log('更新参数');
     });
 
     const getSlots = (name: string) => slots[name];

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

@@ -31,15 +31,15 @@
       "outputPath": ""
     },
     "enableEngineNative": false,
-    "useIsolateContext": true,
+    "useIsolateContext": false,
     "userConfirmedBundleSwitch": false,
     "packNpmManually": false,
     "packNpmRelationList": [],
     "minifyWXSS": true,
     "disableUseStrict": false,
+    "minifyWXML": true,
     "showES6CompileOption": false,
-    "useCompilerPlugins": false,
-    "minifyWXML": true
+    "useCompilerPlugins": false
   },
   "compileType": "miniprogram",
   "simulatorType": "wechat",

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

@@ -25,8 +25,8 @@
           "scene": null
         },
         {
-          "name": "dentry/pages/countup/index",
-          "pathName": "dentry/pages/countup/index",
+          "name": "dentry/pages/picker/index",
+          "pathName": "dentry/pages/picker/index",
           "query": "",
           "scene": null
         },

+ 128 - 32
src/sites/mobile-taro/vue/src/dentry/pages/picker/index.vue

@@ -1,64 +1,160 @@
 <template>
   <div class="demo">
     <h2>基础用法</h2>
-    <nut-picker mode="selector" :list-data="listData1" @confirm="confirm">
-      <nut-cell title="请选择城市" :desc="desc"></nut-cell>
-    </nut-picker>
+    <nut-cell title="请选择城市" :desc="desc" @click="open(0)"></nut-cell>
+    <h2>默认选中项</h2>
+    <nut-cell title="请选择城市" :desc="desc1" @click="open(1)"></nut-cell>
     <h2>多列样式</h2>
-    <nut-picker mode="multiSelector" :list-data="listData2" @confirm="confirm2">
-      <nut-cell title="请选择时间" :desc="desc2"></nut-cell>
+    <nut-cell title="请选择时间" :desc="desc2" @click="open(2)"></nut-cell>
+    <h2>多级联动</h2>
+    <nut-cell title="请选择地址" :desc="desc3" @click="open(3)"></nut-cell>
+    <h2>动态设置</h2>
+    <nut-cell title="请选择地址" :desc="desc4" @click="open(4)"></nut-cell>
+
+    <nut-picker
+      v-model:visible="show"
+      :list-data="listData1"
+      title="城市选择"
+      @confirm="(val) => confirm(0, val)"
+      @close="close"
+    >
+    </nut-picker>
+    <nut-picker
+      v-model:visible="show1"
+      :list-data="listData1"
+      title="城市选择"
+      :defaultIndex="2"
+      @confirm="(val) => confirm(1, val)"
+      @close="close"
+    >
+    </nut-picker>
+    <nut-picker v-model:visible="show2" :list-data="listData2" title="多列选择" @confirm="confirm2" @close="close">
     </nut-picker>
+    <nut-picker v-model:visible="show3" :list-data="listData3" title="地址选择" @confirm="confirm3"></nut-picker>
+    <nut-picker
+      v-model:visible="show4"
+      :list-data="listData4"
+      :demoIndex="demoIndex"
+      title="地址选择"
+      @change="onChange"
+      @confirm="(val) => confirm(4, val)"
+    ></nut-picker>
   </div>
 </template>
 <script lang="ts">
-import { ref } from 'vue';
+import { toRefs, ref } from 'vue';
+
 export default {
   props: {},
   setup() {
-    const listData1 = [
-      '南京市',
-      '无锡市',
-      '海北藏族自治区',
-      '北京市',
-      '连云港市',
-      '浙江市',
-      '江苏市'
-    ];
-    const listData2 = ref([
+    const listData1 = ['南京市', '无锡市', '海北藏族自治区', '北京市', '连云港市', '浙江市', '江苏市'];
+
+    const listData2 = [
       {
         values: ['周一', '周二', '周三', '周四', '周五'],
         defaultIndex: 2
       },
+      // 第二列
       {
         values: ['上午', '下午', '晚上'],
         defaultIndex: 1
       }
+    ];
+    const listData3 = [
+      {
+        text: '浙江',
+        children: [
+          {
+            text: '杭州',
+            children: [{ text: '西湖区' }, { text: '余杭区' }]
+          },
+          {
+            text: '温州',
+            children: [{ text: '鹿城区' }, { text: '瓯海区' }]
+          }
+        ]
+      },
+      {
+        text: '福建',
+        children: [
+          {
+            text: '福州',
+            children: [{ text: '鼓楼区' }, { text: '台江区' }]
+          },
+          {
+            text: '厦门',
+            children: [{ text: '思明区' }, { text: '海沧区' }]
+          }
+        ]
+      }
+    ];
+
+    const cities = {
+      浙江: ['杭州', '宁波', '温州', '嘉兴', '湖州'],
+      福建: ['福州', '厦门', '莆田', '三明']
+    };
+
+    const listData4 = ref([
+      {
+        values: Object.keys(cities)
+      },
+      {
+        values: cities['浙江']
+      }
     ]);
 
+    const show = ref(false);
+    const show1 = ref(false);
+    const show2 = ref(false);
+    const show3 = ref(false);
+    const show4 = ref(false);
+    const showList = [show, show1, show2, show3, show4];
     const desc = ref(listData1[0]);
+    const desc1 = ref(listData1[2]);
     const desc2 = ref(
-      `${listData2.value[0].values[listData2.value[0].defaultIndex]} ${
-        listData2.value[1].values[listData2.value[1].defaultIndex]
-      }`
+      `${listData2[0].values[listData2[0].defaultIndex]} ${listData2[1].values[listData2[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];
-      });
-    };
-
+    const desc3 = ref(
+      `${listData3[0].text}
+      ${listData3[0].children[0].text}
+      ${listData3[0].children[0].children[0].text}`
+    );
+    const desc4 = ref('浙江 杭州');
+    const descList = [desc, desc1, desc2, desc3, desc4];
     return {
       listData1,
       listData2,
+      listData3,
+      listData4,
+      show,
+      show1,
+      show2,
+      show3,
+      show4,
       desc,
+      desc1,
       desc2,
-      confirm,
-      confirm2
+      desc3,
+      desc4,
+      open: (index: number) => {
+        showList[index].value = true;
+      },
+      confirm: (type: number, res: any) => {
+        if (type == 4) {
+          descList[type].value = res[0] + ' ' + res[1];
+        } else {
+          descList[type].value = res;
+        }
+      },
+      confirm2: (res: any) => {
+        desc2.value = res.join(' ');
+      },
+      confirm3: (res: any) => {
+        desc3.value = res.join(' ');
+      },
+      onChange: (res: any, columnIndex, dataIndex) => {
+        listData4.value[1].values = cities[res[0]];
+      }
     };
   }
 };