Browse Source

无限加载、地址组件迁移 (#500)

* fix: address组件中使用css3替换GASP动画函数

* feat: address 组件迁移

* feat: 无限加载

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

+ 1 - 0
package.json

@@ -66,6 +66,7 @@
   },
   "dependencies": {
     "sass": "^1.34.0",
+    "taro-ui": "^3.0.0-alpha.10",
     "vue": "^3.0.5",
     "vue-router": "^4.0.4"
   },

+ 2 - 0
src/config.json

@@ -368,6 +368,7 @@
         {
           "version": "3.0.0",
           "name": "InfiniteLoading",
+          "taro": true,
           "type": "component",
           "cName": "滚动加载",
           "desc": "列表滚动到底部自动加载更多数据",
@@ -600,6 +601,7 @@
       "packages": [
         {
           "version": "3.0.0",
+          "taro": true,
           "name": "Address",
           "type": "component",
           "cName": "地址组件",

+ 1 - 0
src/packages/__VUE/address/index.scss

@@ -30,6 +30,7 @@
       color: #1d1e1e;
 
       .tab-item {
+        min-width: 2px;
         margin-right: 30px;
         display: block;
         &.active {

+ 518 - 0
src/packages/__VUE/address/index.taro.vue

@@ -0,0 +1,518 @@
+<template>
+  <nut-popup
+    position="bottom"
+    @close="close"
+    @click-overlay="clickOverlay"
+    @open="closeWay = 'self'"
+    v-model:visible="showPopup"
+    :class="classes"
+  >
+    <view-block class="nut-address">
+      <view-block class="nut-address__header">
+        <view-block class="arrow-back" @click="switchModule">
+          <nut-icon
+            :name="backBtnIcon"
+            color="#cccccc"
+            v-if="privateType == 'custom' && type == 'exist' && backBtnIcon"
+          ></nut-icon>
+        </view-block>
+
+        <view-block class="nut-address__header__title">
+          {{ privateType == 'custom' ? customAddressTitle : existAddressTitle }}
+        </view-block>
+
+        <view-block class="arrow-close" @click="handClose('cross')">
+          <nut-icon
+            v-if="closeBtnIcon"
+            :name="closeBtnIcon"
+            color="#cccccc"
+            size="18px"
+          ></nut-icon>
+        </view-block>
+      </view-block>
+
+      <!-- 请选择 -->
+      <view-block class="custom-address" v-if="privateType == 'custom'">
+        <view-block class="region-tab">
+          <view-block
+            class="tab-item"
+            :class="[index == tabIndex ? 'active' : '', key]"
+            v-for="(item, key, index) in selectedRegion"
+            :key="index"
+            :ref="key"
+            @click="changeRegionTab(item, key, index)"
+          >
+            <view>{{ getTabName(item, index) }}</view>
+          </view-block>
+
+          <view-block
+            class="region-tab-line"
+            ref="regionLine"
+            :style="{ left: lineDistance + 'px' }"
+          ></view-block>
+        </view-block>
+
+        <view-block class="region-con">
+          <ul class="region-group">
+            <li
+              v-for="(item, index) in regionList[tabName[tabIndex]]"
+              :key="index"
+              class="region-item"
+              :class="[
+                selectedRegion[tabName[tabIndex]].id == item.id ? 'active' : ''
+              ]"
+              @click="nextAreaList(item)"
+            >
+              <nut-icon
+                class="region-item-icon"
+                type="self"
+                :name="selectedIcon"
+                color="#FA2C19"
+                size="13px"
+                v-if="selectedRegion[tabName[tabIndex]].id == item.id"
+              ></nut-icon
+              >{{ item.name }}
+            </li>
+          </ul>
+        </view-block>
+      </view-block>
+
+      <!-- 配送至 -->
+      <view-block class="exist-address" v-else-if="privateType == 'exist'">
+        <div class="exist-address-group">
+          <ul class="exist-ul">
+            <li
+              class="exist-item"
+              :class="[item.selectedAddress ? 'active' : '']"
+              v-for="(item, index) in existAddress"
+              :key="index"
+              @click="selectedExist(item)"
+            >
+              <nut-icon
+                class="exist-item-icon"
+                type="self"
+                :name="item.selectedAddress ? selectedIcon : defaultIcon"
+                :color="item.selectedAddress ? '#FA2C19' : ''"
+                size="13px"
+              ></nut-icon>
+
+              <view>{{
+                item.provinceName +
+                item.cityName +
+                item.countyName +
+                item.townName +
+                item.addressDetail
+              }}</view>
+            </li>
+          </ul>
+        </div>
+
+        <div
+          class="choose-other"
+          @click="switchModule"
+          v-if="isShowCustomAddress"
+        >
+          <div class="btn">{{ customAndExistTitle }}</div>
+        </div>
+      </view-block>
+    </view-block>
+  </nut-popup>
+</template>
+<script lang="ts">
+import { reactive, ref, toRefs, watch, nextTick, computed } from 'vue';
+import { createComponent } from '@/packages/utils/create';
+import Icon from '@/packages/__VUE/icon/index.taro.vue';
+import Popup from '@/packages/__VUE/popup/index.taro.vue';
+import Taro from '@tarojs/taro';
+
+const { create, componentName } = createComponent('address');
+
+interface RegionData {
+  name: string;
+  [key: string]: any;
+}
+interface Region {
+  province: RegionData[];
+  city: RegionData[];
+  country: RegionData[];
+  town: RegionData[];
+  [key: string]: any;
+}
+
+interface AddressList {
+  id?: string | number;
+  provinceName: string;
+  cityName: string;
+  countyName: string;
+  townName: string;
+  addressDetail: string;
+  selectedAddress: boolean;
+}
+export default create({
+  inheritAttrs: false,
+  props: {
+    visible: {
+      type: Boolean,
+      default: false
+    },
+    type: {
+      type: String,
+      default: 'custom'
+    },
+    customAddressTitle: {
+      type: String,
+      default: '请选择所在地区'
+    },
+    province: {
+      type: Array,
+      default: () => []
+    },
+    city: {
+      type: Array,
+      default: () => []
+    }, // 市
+    country: {
+      type: Array,
+      default: () => []
+    }, // 县
+    town: {
+      type: Array,
+      default: () => []
+    }, // 镇
+    isShowCustomAddress: {
+      type: Boolean,
+      default: true
+    }, // 是否显示‘选择其他地区’按钮 type=‘exist’ 生效
+    existAddress: {
+      type: Array,
+      default: () => []
+    }, // 现存地址列表
+    existAddressTitle: {
+      type: String,
+      default: '配送至'
+    },
+    customAndExistTitle: {
+      type: String,
+      default: '选择其他地址'
+    },
+    defaultIcon: {
+      // 地址选择列表前 - 默认的图标
+      type: String,
+      default: 'location2'
+    },
+    selectedIcon: {
+      // 地址选择列表前 - 选中的图标
+      type: String,
+      default: 'Check'
+    },
+    closeBtnIcon: {
+      // 关闭弹框按钮 icon
+      type: String,
+      default: 'circle-close'
+    },
+    backBtnIcon: {
+      // 选择其他地址左上角返回 icon
+      type: String,
+      default: 'left'
+    }
+  },
+  components: {
+    'nut-icon': Icon,
+    'nut-popup': Popup
+  },
+  emits: [
+    'update:visible',
+    'type',
+    'change',
+    'selected',
+    'close',
+    'close-mask',
+    'switch-module'
+  ],
+
+  setup(props, { emit }) {
+    const classes = computed(() => {
+      const prefixCls = componentName;
+      return {
+        [prefixCls]: true
+      };
+    });
+
+    const regionLine = ref<null | HTMLElement>(null);
+
+    const tabItemRef = reactive({
+      province: ref<null | HTMLElement>(null),
+      city: ref<null | HTMLElement>(null),
+      country: ref<null | HTMLElement>(null),
+      town: ref<null | HTMLElement>(null)
+    });
+    const showPopup = ref(props.visible);
+    const privateType = ref(props.type);
+    const tabIndex = ref(0);
+    const tabName = ref(['province', 'city', 'country', 'town']);
+
+    const regionList = reactive({
+      province: props.province,
+      city: props.city,
+      country: props.country,
+      town: props.town
+    });
+
+    const selectedRegion = reactive({
+      province: {} as RegionData,
+      city: {} as RegionData,
+      country: {} as RegionData,
+      town: {} as RegionData
+    }); //已选择的 省、市、县、镇
+
+    let selectedExistAddress = reactive({}); // 当前选择的地址
+
+    const closeWay = ref('self');
+
+    const lineDistance = ref(20);
+
+    //获取已选地区列表名称
+    const getTabName = (item: RegionData, index: number) => {
+      if (item.name) return item.name;
+
+      if (tabIndex.value < index) {
+        return item.name;
+      } else {
+        return '请选择';
+      }
+    };
+    // 手动关闭 点击叉号(cross),或者蒙层(mask)
+    const handClose = (type = 'self') => {
+      if (!props.closeBtnIcon) return;
+
+      closeWay.value = type == 'cross' ? 'cross' : 'self';
+
+      showPopup.value = false;
+    };
+    // 点击遮罩层关闭
+    const clickOverlay = () => {
+      closeWay.value = 'mask';
+    };
+    // 移动下面的红线
+    const lineAnimation = () => {
+      setTimeout(() => {
+        Taro.createSelectorQuery()
+          .selectAll(`.${tabName.value[tabIndex.value]}`)
+          .boundingClientRect((rects) => {
+            (rects as any).forEach((rect) => {
+              if (rect.width > 0) lineDistance.value = rect.left;
+            });
+          })
+          .exec();
+      }, 100);
+    };
+    // 切换下一级列表
+    const nextAreaList = (item: RegionData | string) => {
+      // onchange 接收的参数
+      const calBack = {
+        next: '',
+        value: '',
+        custom: tabName.value[tabIndex.value]
+      };
+
+      (selectedRegion as any)[tabName.value[tabIndex.value]] = item;
+
+      for (let i = tabIndex.value; i < tabIndex.value - 1; i++) {
+        (selectedRegion as any)[tabName.value[i + 1]] = {};
+      }
+
+      if (tabIndex.value < 3) {
+        tabIndex.value = tabIndex.value + 1;
+
+        lineAnimation();
+
+        // 切换下一个
+        calBack.next = tabName.value[tabIndex.value];
+        calBack.value = item as string;
+        emit('change', calBack);
+      } else {
+        handClose();
+      }
+    };
+    //切换地区Tab
+    const changeRegionTab = (item: RegionData, key: number, index: number) => {
+      tabIndex.value = index;
+      lineAnimation();
+    };
+
+    // 选择现有地址
+    const selectedExist = (item: RegionData) => {
+      const copyExistAdd = props.existAddress as AddressList[];
+      let prevExistAdd = {};
+
+      copyExistAdd.forEach((list, index) => {
+        if (list && (list as AddressList).selectedAddress) {
+          prevExistAdd = list;
+        }
+        (list as AddressList).selectedAddress = false;
+      });
+
+      item.selectedAddress = true;
+
+      selectedExistAddress = item;
+
+      emit('selected', prevExistAdd, item, copyExistAdd);
+
+      handClose();
+    };
+    // 初始化
+    const initAddress = () => {
+      for (let i = 0; i < tabName.value.length; i++) {
+        (selectedRegion as any)[tabName.value[i]] = {};
+      }
+      tabIndex.value = 0;
+      lineAnimation();
+    };
+
+    // 关闭
+    const close = () => {
+      const resCopy = Object.assign(
+        {
+          addressIdStr: '',
+          addressStr: ''
+        },
+        selectedRegion
+      );
+
+      const res = {
+        data: {},
+        type: privateType.value
+      };
+
+      if (privateType.value == 'custom') {
+        const { province, city, country, town } = resCopy;
+
+        resCopy.addressIdStr = [
+          (province as RegionData).id || 0,
+          (city as RegionData).id || 0,
+          (country as RegionData).id || 0,
+          (town as RegionData).id || 0
+        ].join('_');
+        resCopy.addressStr = [
+          (province as RegionData).name,
+          (city as RegionData).name,
+          (country as RegionData).name,
+          (town as RegionData).name
+        ].join('');
+        res.data = resCopy;
+      } else {
+        res.data = selectedExistAddress;
+      }
+
+      initAddress();
+
+      if (closeWay.value == 'self') {
+        emit('close', res);
+      } else {
+        emit('close-mask', { closeWay: closeWay });
+      }
+      emit('update:visible', false);
+    };
+
+    // 选择其他地址
+    const switchModule = () => {
+      if (privateType.value == 'exist') {
+        privateType.value = 'custom';
+      } else {
+        privateType.value = 'exist';
+      }
+
+      initAddress();
+
+      emit('switch-module', { type: privateType.value });
+    };
+
+    watch(
+      () => props.visible,
+      (value) => {
+        showPopup.value = value;
+      }
+    );
+    // watch(
+    //   () => props.type,
+    //   (value) => {
+    //     privateType.value = value;
+    //   }
+    // );
+
+    watch(
+      () => showPopup.value,
+      (value) => {
+        if (value == false) {
+          close();
+        }
+      }
+    );
+
+    watch(
+      () => props.province,
+      (value) => {
+        regionList.province = value;
+      }
+    );
+    watch(
+      () => props.city,
+      (value) => {
+        regionList.city = value;
+      }
+    );
+    watch(
+      () => props.country,
+      (value) => {
+        regionList.country = value;
+      }
+    );
+    watch(
+      () => props.town,
+      (value) => {
+        regionList.town = value;
+      }
+    );
+
+    watch(
+      () => props.existAddress,
+      (value) => {
+        //  existAddress.value = value;
+        value.forEach((item, index) => {
+          if ((item as AddressList).selectedAddress) {
+            selectedExistAddress = item as {};
+          }
+        });
+      }
+    );
+
+    return {
+      classes,
+      showPopup,
+      privateType,
+      tabIndex,
+      tabName,
+      regionList,
+      selectedRegion,
+      selectedExistAddress,
+      switchModule,
+      closeWay,
+      close,
+      getTabName,
+      nextAreaList,
+      regionLine,
+      lineDistance,
+      changeRegionTab,
+      selectedExist,
+      clickOverlay,
+      handClose,
+      ...toRefs(props),
+      ...toRefs(tabItemRef)
+    };
+  }
+});
+</script>
+
+<style lang="scss">
+@import './index.scss';
+</style>

+ 12 - 11
src/packages/__VUE/address/index.vue

@@ -97,10 +97,10 @@
 
               <view>{{
                 item.provinceName +
-                  item.cityName +
-                  item.countyName +
-                  item.townName +
-                  item.addressDetail
+                item.cityName +
+                item.countyName +
+                item.townName +
+                item.addressDetail
               }}</view>
             </li>
           </ul>
@@ -283,6 +283,7 @@ export default create({
         if (name) {
           const distance = name.offsetLeft;
           lineDistance.value = distance;
+          console.log(name);
         }
       });
     };
@@ -409,7 +410,7 @@ export default create({
 
     watch(
       () => props.visible,
-      value => {
+      (value) => {
         showPopup.value = value;
       }
     );
@@ -422,7 +423,7 @@ export default create({
 
     watch(
       () => showPopup.value,
-      value => {
+      (value) => {
         if (value == false) {
           close();
         }
@@ -431,32 +432,32 @@ export default create({
 
     watch(
       () => props.province,
-      value => {
+      (value) => {
         regionList.province = value;
       }
     );
     watch(
       () => props.city,
-      value => {
+      (value) => {
         regionList.city = value;
       }
     );
     watch(
       () => props.country,
-      value => {
+      (value) => {
         regionList.country = value;
       }
     );
     watch(
       () => props.town,
-      value => {
+      (value) => {
         regionList.town = value;
       }
     );
 
     watch(
       () => props.existAddress,
-      value => {
+      (value) => {
         //  existAddress.value = value;
         value.forEach((item, index) => {
           if ((item as AddressList).selectedAddress) {

+ 237 - 0
src/packages/__VUE/infiniteloading/index.taro.vue

@@ -0,0 +1,237 @@
+<template>
+  <scroll-view
+    :class="classes"
+    scrollY="true"
+    style="height: 100%"
+    id="scroller"
+    @scrolltolower="lower"
+    @scroll="scroll"
+    @touchstart="touchStart"
+    @touchmove="touchMove"
+    @touchend="touchEnd"
+  >
+    <view-block class="nut-infinite-top" :style="getStyle">
+      <view-block class="top-box" id="refreshTop">
+        <nut-icon class="top-img" :name="pullIcon"></nut-icon>
+        <view-block class="top-text">{{ pullTxt }}</view-block>
+      </view-block>
+    </view-block>
+
+    <view-block class="nut-infinite-container">
+      <slot></slot>
+    </view-block>
+
+    <view-block class="nut-infinite-bottom">
+      <template v-if="isInfiniting">
+        <view-block class="bottom-box">
+          <nut-icon class="bottom-img" :name="loadIcon"></nut-icon>
+          <view-block class="bottom-text">{{ loadTxt }}</view-block>
+        </view-block>
+      </template>
+      <template v-else-if="!hasMore">
+        <view-block class="tips">{{ loadMoreTxt }}</view-block>
+      </template>
+    </view-block>
+  </scroll-view>
+</template>
+<script lang="ts">
+import { toRefs, onMounted, reactive, computed, CSSProperties } from 'vue';
+import { createComponent } from '@/packages/utils/create';
+const { componentName, create } = createComponent('infiniteloading');
+import Icon from '@/packages/__VUE/icon/index.taro.vue';
+import Taro from '@tarojs/taro';
+export default create({
+  props: {
+    hasMore: {
+      type: Boolean,
+      default: true
+    },
+    threshold: {
+      type: Number,
+      default: 200
+    },
+    pullIcon: {
+      type: String,
+      default:
+        'https://img10.360buyimg.com/imagetools/jfs/t1/169863/6/4565/6306/60125948E7e92774e/40b3a0cf42852bcb.png'
+    },
+    pullTxt: {
+      type: String,
+      default: '松开刷新'
+    },
+    loadIcon: {
+      type: String,
+      default:
+        'https://img10.360buyimg.com/imagetools/jfs/t1/169863/6/4565/6306/60125948E7e92774e/40b3a0cf42852bcb.png'
+    },
+    loadTxt: {
+      type: String,
+      default: '加载中···'
+    },
+    loadMoreTxt: {
+      type: String,
+      default: '哎呀,这里是底部了啦'
+    },
+    useWindow: {
+      type: Boolean,
+      default: true
+    },
+    containerId: {
+      type: String,
+      default: ''
+    },
+    useCapture: {
+      type: Boolean,
+      default: false
+    },
+    isOpenRefresh: {
+      type: Boolean,
+      default: false
+    }
+  },
+  emits: ['scroll-change', 'load-more', 'refresh'],
+  components: {
+    'nut-icon': Icon
+  },
+  setup(props, { emit, slots }) {
+    const state = reactive({
+      scrollHeight: 0,
+      scrollTop: 0,
+      isInfiniting: false,
+      direction: 'down',
+      isTouching: false,
+      refreshMaxH: 0,
+      y: 0,
+      x: 0,
+      distance: 0
+    });
+
+    const classes = computed(() => {
+      const prefixCls = componentName;
+      return {
+        [prefixCls]: true
+      };
+    });
+
+    const getStyle = computed(() => {
+      const style: CSSProperties = {};
+      return {
+        height: state.distance < 0 ? `0px` : `${state.distance}px`,
+        transition: state.isTouching
+          ? `height 0s cubic-bezier(0.25,0.1,0.25,1)`
+          : `height 0.2s cubic-bezier(0.25,0.1,0.25,1)`
+      };
+    });
+    const getParentElement = (el) => {
+      return Taro.createSelectorQuery().select(
+        !!props.containerId ? `#${props.containerId} #${el}` : `#${el}`
+      );
+    };
+    /** 获取需要滚动的距离 */
+    const getScrollHeight = () => {
+      const parentElement = getParentElement('scroller');
+
+      parentElement
+        .boundingClientRect((rect) => {
+          state.scrollHeight = rect.height;
+        })
+        .exec();
+    };
+
+    /** 滚动到底部 */
+    const lower = () => {
+      if (state.direction == 'up' || !props.hasMore || state.isInfiniting) {
+        return false;
+      } else {
+        state.isInfiniting = true;
+        emit('load-more', infiniteDone);
+      }
+    };
+
+    const scroll = (e) => {
+      // 滚动方向
+      if (e.detail.scrollTop <= 0) {
+        // 滚动到最顶部
+        e.detail.scrollTop = 0;
+      } else if (e.detail.scrollTop >= state.scrollHeight) {
+        // 滚动到最底部
+        e.detail.scrollTop = state.scrollHeight;
+      }
+      if (
+        e.detail.scrollTop > state.scrollTop ||
+        e.detail.scrollTop >= state.scrollHeight
+      ) {
+        state.direction = 'down';
+      } else {
+        state.direction = 'up';
+      }
+      state.scrollTop = e.detail.scrollTop;
+
+      emit('scroll-change', e.detail.scrollTop);
+    };
+
+    const infiniteDone = () => {
+      state.isInfiniting = false;
+    };
+
+    const touchStart = (event: TouchEvent) => {
+      if (state.scrollTop == 0 && !state.isTouching && props.isOpenRefresh) {
+        state.y = event.touches[0].pageY;
+        state.isTouching = true;
+        getParentElement('refreshTop')
+          .boundingClientRect((rect) => {
+            state.refreshMaxH = Math.floor(rect.height * 1 + 10);
+          })
+          .exec();
+      }
+    };
+
+    const touchMove = (event: TouchEvent) => {
+      state.distance = event.touches[0].pageY - state.y;
+
+      if (state.distance > 0 && state.isTouching) {
+        event.preventDefault();
+        if (state.distance >= state.refreshMaxH)
+          state.distance = state.refreshMaxH;
+      } else {
+        state.distance = 0;
+        state.isTouching = false;
+      }
+    };
+
+    const touchEnd = () => {
+      if (state.distance < state.refreshMaxH) {
+        state.distance = 0;
+      } else {
+        emit('refresh', refreshDone);
+      }
+    };
+
+    const refreshDone = () => {
+      state.distance = 0;
+      state.isTouching = false;
+    };
+
+    onMounted(() => {
+      setTimeout(() => {
+        getScrollHeight();
+      }, 200);
+    });
+
+    return {
+      classes,
+      ...toRefs(state),
+      lower,
+      scroll,
+      touchStart,
+      touchMove,
+      touchEnd,
+      getStyle
+    };
+  }
+});
+</script>
+
+<style lang="scss">
+@import './index.scss';
+</style>

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

@@ -30,14 +30,13 @@
       "outputPath": ""
     },
     "enableEngineNative": false,
-    "bundle": false,
     "useIsolateContext": true,
     "useCompilerModule": true,
     "userConfirmedUseCompilerModuleSwitch": false,
     "userConfirmedBundleSwitch": false,
     "packNpmManually": false,
     "packNpmRelationList": [],
-    "minifyWXSS": true
+    "minifyWXSS": false
   },
   "compileType": "miniprogram",
   "simulatorType": "wechat",

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

@@ -1,5 +1,7 @@
 export default {
   pages: [
+    'pages/infiniteloading/index',
+    'pages/address/index',
     'pages/cell/index',
     'pages/rate/index',
     'pages/collapse/index',

+ 3 - 0
src/sites/mobile-taro/vue/src/pages/address/index.config.js

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

+ 301 - 0
src/sites/mobile-taro/vue/src/pages/address/index.vue

@@ -0,0 +1,301 @@
+<template>
+  <view class="demo">
+    <h2>选择自定义地址</h2>
+    <nut-cell
+      title="选择地址"
+      :desc="one"
+      is-link
+      @click="showAddress"
+    ></nut-cell>
+
+    <nut-address
+      v-model:visible="normal"
+      :province="province"
+      :city="city"
+      :country="country"
+      :town="town"
+      @change="(cal) => onChange(cal, 'normal')"
+      @close="close1"
+      custom-address-title="请选择所在地区"
+    ></nut-address>
+
+    <h2>选择已有地址</h2>
+    <nut-cell
+      title="选择地址"
+      :desc="two"
+      is-link
+      @click="showAddressExist"
+    ></nut-cell>
+
+    <nut-address
+      v-model:visible="exist"
+      type="exist"
+      :exist-address="existAddress"
+      @change="(cal) => onChange(cal, 'exist')"
+      @close="close2"
+      :is-show-custom-address="false"
+      @selected="selected"
+      exist-address-title="配送至"
+    ></nut-address>
+
+    <h2>自定义图标</h2>
+    <nut-cell
+      title="选择地址"
+      :desc="three"
+      is-link
+      @click="showCustomImg"
+    ></nut-cell>
+
+    <nut-address
+      v-model:visible="customImg"
+      type="exist"
+      :exist-address="existAddress"
+      @change="(cal) => onChange(cal, 'customImg')"
+      @close="close3"
+      :is-show-custom-address="false"
+      @selected="selected"
+      :default-icon="defaultIcon"
+      :selected-icon="selectedIcon"
+      :close-btn-icon="closeBtnIcon"
+    ></nut-address>
+
+    <h2>自定义地址与已有地址切换</h2>
+    <nut-cell
+      title="选择地址"
+      :desc="four"
+      is-link
+      @click="showAddressOther"
+    ></nut-cell>
+
+    <nut-address
+      v-model:visible="other"
+      type="exist"
+      :exist-address="existAddress"
+      :province="province"
+      :city="city"
+      :country="country"
+      :town="town"
+      :back-btn-icon="backBtnIcon"
+      @change="(cal) => onChange(cal, 'other')"
+      @close="close4"
+      @selected="selected"
+      custom-and-exist-title="选择其他地址"
+      @switch-module="switchModule"
+      @close-mask="closeMask"
+    ></nut-address>
+  </view>
+</template>
+
+<script lang="ts">
+import { reactive, ref, toRefs } from 'vue';
+
+interface CalBack {
+  next: string;
+  value: string;
+  custom: string;
+}
+interface RegionData {
+  name: string;
+  [key: string]: any;
+}
+interface CalResult {
+  type: string;
+  data: AddressResult;
+}
+interface AddressList {
+  id?: string | number;
+  provinceName: string;
+  cityName: string;
+  countyName: string;
+  townName: string;
+  addressDetail: string;
+  selectedAddress: boolean;
+}
+interface AddressResult extends AddressList {
+  addressIdStr: string;
+  addressStr: string;
+  province: RegionData[];
+  city: RegionData[];
+  country: RegionData[];
+  town: RegionData[];
+}
+export default {
+  props: {},
+  setup() {
+    const address = reactive({
+      province: [
+        { id: 1, name: '北京' },
+        { id: 2, name: '广西' },
+        { id: 3, name: '江西' },
+        { id: 4, name: '四川' }
+      ],
+      city: [
+        { id: 7, name: '朝阳区' },
+        { id: 8, name: '崇文区' },
+        { id: 9, name: '昌平区' },
+        { id: 6, name: '石景山区' }
+      ],
+      country: [
+        { id: 3, name: '八里庄街道' },
+        { id: 9, name: '北苑' },
+        { id: 4, name: '常营乡' }
+      ],
+      town: []
+    });
+
+    const showPopup = reactive({
+      normal: false,
+      exist: false,
+      customImg: false,
+      other: false
+    });
+
+    const icon = reactive({
+      selectedIcon: 'heart-fill',
+      defaultIcon: 'heart1',
+      closeBtnIcon: 'close',
+      backBtnIcon: 'left'
+    });
+
+    const existAddress = ref([
+      {
+        id: 1,
+        addressDetail: '',
+        cityName: '次渠镇',
+        countyName: '通州区',
+        provinceName: '北京市',
+        selectedAddress: true,
+        townName: ''
+      },
+      {
+        id: 2,
+        addressDetail: '',
+        cityName: '钓鱼岛全区',
+        countyName: '',
+        provinceName: '钓鱼岛',
+        selectedAddress: false,
+        townName: ''
+      },
+      {
+        id: 3,
+        addressDetail: '京东大厦',
+        cityName: '大兴区',
+        countyName: '科创十一街18号院',
+        provinceName: '北京市',
+        selectedAddress: false,
+        townName: ''
+      }
+    ]);
+
+    const text = reactive({
+      one: '请选择地址',
+      two: '请选择地址',
+      three: '请选择地址',
+      four: '请选择地址'
+    });
+
+    const showAddress = () => {
+      showPopup.normal = !showPopup.normal;
+    };
+
+    const onChange = (cal: CalBack, tag: string) => {
+      const name = (address as any)[cal.next];
+      if (name.length < 1) {
+        (showPopup as any)[tag] = false;
+      }
+    };
+    const close1 = (val: CalResult) => {
+      console.log(val);
+      text.one = val.data.addressStr;
+    };
+
+    const showAddressExist = () => {
+      showPopup.exist = true;
+    };
+
+    const close2 = (val: CalResult) => {
+      console.log(val);
+      if (val.type == 'exist') {
+        const { provinceName, cityName, countyName, townName, addressDetail } =
+          val.data;
+        text.two =
+          provinceName + cityName + countyName + townName + addressDetail;
+      } else {
+        text.two = val.data.addressStr;
+      }
+    };
+    const selected = (
+      prevExistAdd: AddressList,
+      nowExistAdd: RegionData,
+      arr: AddressList[]
+    ) => {
+      console.log(prevExistAdd);
+      console.log(nowExistAdd);
+    };
+
+    const showAddressOther = () => {
+      showPopup.other = true;
+    };
+    const showCustomImg = () => {
+      showPopup.customImg = true;
+    };
+
+    const close3 = (val: CalResult) => {
+      console.log(val);
+      if (val.type == 'exist') {
+        const { provinceName, cityName, countyName, townName, addressDetail } =
+          val.data;
+        text.three =
+          provinceName + cityName + countyName + townName + addressDetail;
+      } else {
+        text.three = val.data.addressStr;
+      }
+    };
+
+    const close4 = (val: CalResult) => {
+      console.log(val);
+      if (val.type == 'exist') {
+        const { provinceName, cityName, countyName, townName, addressDetail } =
+          val.data;
+        text.four =
+          provinceName + cityName + countyName + townName + addressDetail;
+      } else {
+        text.four = val.data.addressStr;
+      }
+    };
+
+    const switchModule = (val: CalResult) => {
+      if (val.type == 'custom') {
+        console.log('点击了“选择其他地址”按钮');
+      } else {
+        console.log('点击了自定义地址左上角的返回按钮');
+      }
+    };
+
+    const closeMask = (val: CalResult) => {
+      console.log('关闭弹层', val);
+    };
+
+    return {
+      showAddress,
+      showPopup,
+      onChange,
+      close1,
+      showAddressExist,
+      close2,
+      selected,
+      existAddress,
+      showAddressOther,
+      showCustomImg,
+      close3,
+      close4,
+      switchModule,
+      closeMask,
+      ...toRefs(icon),
+      ...toRefs(text),
+      ...toRefs(showPopup),
+      ...toRefs(address)
+    };
+  }
+};
+</script>

+ 3 - 0
src/sites/mobile-taro/vue/src/pages/infiniteloading/index.config.js

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

+ 99 - 0
src/sites/mobile-taro/vue/src/pages/infiniteloading/index.vue

@@ -0,0 +1,99 @@
+<template>
+  <view class="demo">
+    <h2>基础演示</h2>
+    <nut-cell>
+      <view-block class="infiniteUl" id="scrollDemo">
+        <nut-infiniteloading
+          pull-icon="JD"
+          load-txt="loading"
+          load-more-txt="没有啦~"
+          :is-open-refresh="true"
+          container-id="scrollDemo"
+          :has-more="hasMore"
+          @load-more="loadMore"
+          @refresh="refresh"
+        >
+          <view-block
+            class="infiniteLi"
+            v-for="(item, index) in defultList"
+            :key="index"
+            >{{ item }}</view-block
+          >
+        </nut-infiniteloading>
+      </view-block>
+    </nut-cell>
+  </view>
+</template>
+
+<script lang="ts">
+import { onMounted, ref, reactive, toRefs } from 'vue';
+
+export default {
+  props: {},
+  setup() {
+    const hasMore = ref(true);
+
+    const data = reactive({
+      defultList: ['']
+    });
+
+    const loadMore = (done) => {
+      setTimeout(() => {
+        const curLen = data.defultList.length;
+
+        for (let i = curLen; i < curLen + 10; i++) {
+          data.defultList.push(`${i}`);
+        }
+
+        if (data.defultList.length > 30) hasMore.value = false;
+
+        done();
+      }, 500);
+    };
+
+    const refresh = (done) => {
+      setTimeout(() => {
+        console.log('刷新成功');
+        done();
+      }, 1000);
+    };
+
+    const init = () => {
+      for (let i = 0; i < 20; i++) {
+        data.defultList.push(`${i}`);
+      }
+    };
+    onMounted(() => {
+      init();
+    });
+
+    return {
+      loadMore,
+      hasMore,
+      refresh,
+      ...toRefs(data)
+    };
+  }
+};
+</script>
+
+<style>
+.infiniteUl {
+  height: 500px;
+  width: 100%;
+  overflow-y: auto;
+  overflow-x: hidden;
+}
+.infiniteLi {
+  margin-top: 10px;
+  font-size: 14px;
+  color: rgba(100, 100, 100, 1);
+  text-align: center;
+}
+
+.loading {
+  display: block;
+  width: 100%;
+  text-align: center;
+}
+</style>