浏览代码

feat: Popover 组件 Taro 小程序适配 (#1882)

* fix: 修复 ImagePreview 在Taro编译成H5后报错的问题

* fix: 地址关闭时, Close 事件触发两次问题解决

* feat: 组件DatePicker 添加双向绑定

* docs: 组件Picker文档修改

* feat: 组件Picker与DatePicker新增属性safe-area-inset-bottom

* feat: imagepreview

* fix: 组件imagepreview点击视频遮罩关闭(#1729)

* fix: 解决 Picker 在微信小程序中无法使用问题 (#1774)

* fix: 修改 Picker 组件 v-model 失效问题

* fix: 组件NoticeBar修改height之后,垂直轮播会卡顿

* fix: 删除Datepicker Demo演示多余内容

* fix: 组件Picker在JD小程序上适配

* fix: 组件Address京东小程序适配

* feat: 京东小程序适配

* fix: 删除空格

* feat: 删除console

* fix: 京东小程序imagepreview适配

* fix: 修复 imagepreview 动态设置 initNo 显示不正确问题

* fix: 组件 InfiniteLoading 某些情况下会错误触发下拉刷新#1819

* fix: 删除pullrefresh

* feat: 组件 imagepreview瘦身

* feat: 组件Picker 瘦身

* fix: address线上问题修改

* fix: 完善imagepreview

* feat: 公共函数提取

* feat: 函数式改用 createComponent

* fix: 组件 Imagepreview与Address兼容性修改

* feat: 组件 popover 小程序适配

* feat: 单元测试

* fix: 文件回撤

* feat: 组件popover添加单元测试

* feat: 组件 popup 修改
yangxiaolu1993 3 年之前
父节点
当前提交
8f5447cb6b

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

@@ -33,7 +33,7 @@
 
 
       <!-- 请选择 -->
       <!-- 请选择 -->
       <view class="custom-address" v-if="privateType == 'custom'">
       <view class="custom-address" v-if="privateType == 'custom'">
-        <view class="region-tab" ref="tabRegion">
+        <view class="nut-address-region-tab" ref="tabRegion">
           <view
           <view
             :class="{ 'tab-item': true, active: index == tabIndex, [tabName[index]]: true }"
             :class="{ 'tab-item': true, active: index == tabIndex, [tabName[index]]: true }"
             v-for="(item, key, index) in selectedRegion"
             v-for="(item, key, index) in selectedRegion"
@@ -72,7 +72,7 @@
 
 
       <!-- 请选择 -->
       <!-- 请选择 -->
       <view class="custom-address" v-else-if="privateType == 'custom2'">
       <view class="custom-address" v-else-if="privateType == 'custom2'">
-        <view class="region-tab" ref="tabRegion">
+        <view class="nut-address-region-tab" ref="tabRegion">
           <view
           <view
             :class="{ 'tab-item': true, active: index == tabIndex, [tabName[index]]: true }"
             :class="{ 'tab-item': true, active: index == tabIndex, [tabName[index]]: true }"
             v-for="(item, key, index) in selectedRegion"
             v-for="(item, key, index) in selectedRegion"

+ 26 - 75
src/packages/__VUE/imagepreview/index.taro.vue

@@ -1,6 +1,5 @@
 <template>
 <template>
-  <nut-popup pop-class="nut-imagepreview-custom-pop" v-model:visible="showPop" @click="onClose" style="width: 100%">
-    <!-- @click.stop="closeOnImg" -->
+  <nut-popup pop-class="nut-imagepreview-custom-pop" v-model:visible="showPop" @closed="onClose">
     <view class="nut-imagepreview" @touchstart.capture="onTouchStart">
     <view class="nut-imagepreview" @touchstart.capture="onTouchStart">
       <nut-swiper
       <nut-swiper
         v-if="showPop"
         v-if="showPop"
@@ -9,20 +8,20 @@
         :loop="isLoop"
         :loop="isLoop"
         :is-preventDefault="false"
         :is-preventDefault="false"
         direction="horizontal"
         direction="horizontal"
-        @change="slideChangeEnd"
-        :init-page="initPage"
+        @change="setActive"
+        :init-page="initNo"
         :pagination-visible="paginationVisible"
         :pagination-visible="paginationVisible"
         :pagination-color="paginationColor"
         :pagination-color="paginationColor"
       >
       >
-        <nut-swiper-item v-for="(item, index) in images" :key="index">
+        <nut-swiper-item v-for="(item, index) in images" :key="index" @click="onClose">
           <image mode="aspectFit" :src="item.src" class="nut-imagepreview-taro-img" v-if="ENV != ENV_TYPE.WEB" />
           <image mode="aspectFit" :src="item.src" class="nut-imagepreview-taro-img" v-if="ENV != ENV_TYPE.WEB" />
           <img :src="item.src" mode="aspectFit" class="nut-imagepreview-img" v-else />
           <img :src="item.src" mode="aspectFit" class="nut-imagepreview-img" v-else />
         </nut-swiper-item>
         </nut-swiper-item>
       </nut-swiper>
       </nut-swiper>
     </view>
     </view>
 
 
-    <view class="nut-imagepreview-index" v-if="showIndex"> {{ active }} / {{ images.length }} </view>
-    <view class="nut-imagepreview-close-icon" @click="handleCloseIcon" :style="styles" v-if="closeable"
+    <view class="nut-imagepreview-index" v-if="showIndex"> {{ active + 1 }} / {{ images.length }} </view>
+    <view class="nut-imagepreview-close-icon" @click="onClose" :style="styles" v-if="closeable"
       ><nut-icon :name="closeIcon" v-bind="$attrs" color="#ffffff"></nut-icon
       ><nut-icon :name="closeIcon" v-bind="$attrs" color="#ffffff"></nut-icon
     ></view>
     ></view>
   </nut-popup>
   </nut-popup>
@@ -30,11 +29,7 @@
 <script lang="ts">
 <script lang="ts">
 import { toRefs, reactive, watch, onMounted, computed, CSSProperties, PropType } from 'vue';
 import { toRefs, reactive, watch, onMounted, computed, CSSProperties, PropType } from 'vue';
 import { createComponent } from '@/packages/utils/create';
 import { createComponent } from '@/packages/utils/create';
-import Popup from '../popup/index.taro.vue';
-import Swiper from '../swiper/index.taro.vue';
-import SwiperItem from '../swiperitem/index.taro.vue';
-import Icon from '../icon/index.taro.vue';
-import { isPromise } from '@/packages/utils/util';
+import { funInterceptor, Interceptor } from '@/packages/utils/util';
 import { ImageInterface } from './types';
 import { ImageInterface } from './types';
 import Taro from '@tarojs/taro';
 import Taro from '@tarojs/taro';
 const { create } = createComponent('imagepreview');
 const { create } = createComponent('imagepreview');
@@ -55,7 +50,7 @@ export default create({
     },
     },
     initNo: {
     initNo: {
       type: Number,
       type: Number,
-      default: 1
+      default: 0
     },
     },
     paginationVisible: {
     paginationVisible: {
       type: Boolean,
       type: Boolean,
@@ -85,29 +80,19 @@ export default create({
       type: String,
       type: String,
       default: 'top-right' // top-right  top-left
       default: 'top-right' // top-right  top-left
     },
     },
-    beforeClose: Function,
+    beforeClose: Function as PropType<Interceptor>,
     isLoop: {
     isLoop: {
       type: Boolean,
       type: Boolean,
       default: true
       default: true
     }
     }
   },
   },
   emits: ['close', 'change'],
   emits: ['close', 'change'],
-  components: {
-    [Popup.name]: Popup,
-    // [Video.name]: Video,
-    [Swiper.name]: Swiper,
-    [SwiperItem.name]: SwiperItem,
-    [Icon.name]: Icon
-  },
+  components: {},
 
 
   setup(props, { emit }) {
   setup(props, { emit }) {
     const state = reactive({
     const state = reactive({
       showPop: false,
       showPop: false,
-      active: 1,
-      source: {
-        src: 'https://storage.jd.com/about/big-final.mp4?Expires=3730193075&AccessKey=3LoYX1dQWa6ZXzQl&Signature=ViMFjz%2BOkBxS%2FY1rjtUVqbopbJI%3D',
-        type: 'video/mp4'
-      },
+      active: 0,
       options: {
       options: {
         muted: true,
         muted: true,
         controls: true
         controls: true
@@ -135,9 +120,12 @@ export default create({
       return style;
       return style;
     });
     });
 
 
-    const slideChangeEnd = function (page: number) {
-      state.active = page + 1;
-      emit('change', state.active);
+    // 设置当前选中第几个
+    const setActive = (active: number) => {
+      if (active !== state.active) {
+        state.active = active;
+        emit('change', state.active);
+      }
     };
     };
 
 
     const closeOnImg = () => {
     const closeOnImg = () => {
@@ -148,35 +136,21 @@ export default create({
     };
     };
 
 
     const onClose = () => {
     const onClose = () => {
-      if (props.beforeClose) {
-        const returnVal = props.beforeClose.apply(null, state.active);
-
-        if (isPromise(returnVal)) {
-          returnVal.then((value) => {
-            if (value) {
-              closeDone();
-            }
-          });
-        } else if (returnVal) {
-          closeDone();
-        }
-      } else {
-        closeDone();
-      }
+      funInterceptor(props.beforeClose, {
+        args: [state.active],
+        done: () => closeDone()
+      });
     };
     };
     // 执行关闭
     // 执行关闭
     const closeDone = () => {
     const closeDone = () => {
       state.showPop = false;
       state.showPop = false;
       state.store.scale = 1;
       state.store.scale = 1;
       scaleNow();
       scaleNow();
-      // state.active = 1;
       emit('close');
       emit('close');
     };
     };
 
 
     // 计算两个点的距离
     // 计算两个点的距离
     const getDistance = (first: { x: number; y: number }, second: { x: number; y: number }) => {
     const getDistance = (first: { x: number; y: number }, second: { x: number; y: number }) => {
-      // 计算两个点起始时刻的距离和终止时刻的距离,终止时刻距离变大了则放大,变小了则缩小
-      // 放大 k 倍则 scale 也 扩大 k 倍
       return Math.hypot(Math.abs(second.x - first.x), Math.abs(second.y - first.y));
       return Math.hypot(Math.abs(second.x - first.x), Math.abs(second.y - first.y));
     };
     };
 
 
@@ -187,8 +161,6 @@ export default create({
     };
     };
 
 
     const onTouchStart = (event: TouchEvent) => {
     const onTouchStart = (event: TouchEvent) => {
-      // console.log('start');
-      // 如果已经放大,双击应变回原尺寸;如果是原尺寸,双击应放大
       const curTouchTime = new Date().getTime();
       const curTouchTime = new Date().getTime();
       if (curTouchTime - state.lastTouchEndTime < 300) {
       if (curTouchTime - state.lastTouchEndTime < 300) {
         const store = state.store;
         const store = state.store;
@@ -204,13 +176,10 @@ export default create({
       var events = touches[0];
       var events = touches[0];
       var events2 = touches[1];
       var events2 = touches[1];
 
 
-      // event.preventDefault();
-
       const store = state.store;
       const store = state.store;
       store.moveable = true;
       store.moveable = true;
 
 
       if (events2) {
       if (events2) {
-        // 如果开始两指操作,记录初始时刻两指间的距离
         store.oriDistance = getDistance(
         store.oriDistance = getDistance(
           {
           {
             x: events.pageX,
             x: events.pageX,
@@ -222,7 +191,7 @@ export default create({
           }
           }
         );
         );
       }
       }
-      // 取到开始两指操作时的放大(缩小比例),store.scale 存储的是当前的放缩比(相对于标准大小 scale 为 1 的情况的放大缩小比)
+
       store.originScale = store.scale || 1;
       store.originScale = store.scale || 1;
     };
     };
 
 
@@ -265,7 +234,6 @@ export default create({
     };
     };
 
 
     const onTouchEnd = () => {
     const onTouchEnd = () => {
-      // console.log('end');
       state.lastTouchEndTime = new Date().getTime();
       state.lastTouchEndTime = new Date().getTime();
       const store = state.store;
       const store = state.store;
       store.moveable = false;
       store.moveable = false;
@@ -293,33 +261,17 @@ export default create({
     watch(
     watch(
       () => props.initNo,
       () => props.initNo,
       (val) => {
       (val) => {
-        if (val != state.active) {
-          state.active = val;
-        }
+        if (val != state.active) setActive(val);
       }
       }
     );
     );
 
 
-    const initPage = computed(() => {
-      const maxNo = props.images.length;
-      const _initPage = props.initNo > maxNo ? maxNo - 1 : props.initNo - 1;
-      return _initPage >= 0 ? _initPage : 0;
-    });
-
-    // 点击关闭按钮
-    const handleCloseIcon = () => {
-      onClose();
-    };
-
     onMounted(() => {
     onMounted(() => {
-      // 初始化页码
-      state.active = props.initNo;
-      state.showPop = props.show;
+      setActive(props.initNo);
     });
     });
 
 
     return {
     return {
       ...toRefs(state),
       ...toRefs(state),
-      initPage,
-      slideChangeEnd,
+      setActive,
       onClose,
       onClose,
       closeOnImg,
       closeOnImg,
       onTouchStart,
       onTouchStart,
@@ -327,8 +279,7 @@ export default create({
       onTouchEnd,
       onTouchEnd,
       getDistance,
       getDistance,
       scaleNow,
       scaleNow,
-      styles,
-      handleCloseIcon
+      styles
     };
     };
   }
   }
 });
 });

+ 97 - 0
src/packages/__VUE/popover/__test__/popover.spec.ts

@@ -0,0 +1,97 @@
+import { config, mount } from '@vue/test-utils';
+import Popover from '../index.vue';
+import NutPupup from '../../popup/index.vue';
+import NutOverlay from '../../overlay/index.vue';
+import NutIcon from '../../icon/index.vue';
+import { nextTick, reactive } from 'vue';
+
+beforeAll(() => {
+  config.global.components = {
+    NutIcon,
+    NutPupup,
+    NutOverlay
+  };
+});
+
+afterAll(() => {
+  config.global.components = {};
+});
+
+const iconItemList = [{ name: 'option1' }, { name: 'option2' }, { name: 'option3' }];
+
+const listDisabled = reactive([
+  { name: 'option1', disabled: true },
+  { name: 'option2', disabled: true },
+  { name: 'option3' }
+]);
+
+test('first render', async () => {
+  const wrapper = mount(Popover, {
+    props: {
+      visible: true,
+      list: iconItemList,
+      teleportDisable: false
+    }
+  });
+  await nextTick();
+  expect(wrapper.find('.nut-popover-menu-item').exists()).toBeTruthy();
+});
+
+test('Props theme dark', async () => {
+  const wrapper = mount(Popover, {
+    props: {
+      visible: true,
+      list: iconItemList,
+      teleportDisable: false,
+      theme: 'dark'
+    }
+  });
+  await nextTick();
+  expect(wrapper.find('.nut-popover--dark').exists()).toBeTruthy();
+});
+
+test('should not emit select event when the action is disabled', async () => {
+  const wrapper = mount(Popover, {
+    props: {
+      visible: true,
+      list: listDisabled,
+      teleportDisable: false
+    }
+  });
+  await nextTick();
+  expect(wrapper.findAll('.nut-popover-menu-disabled').length).toEqual(2);
+
+  wrapper.find('.nut-popover-menu-item').trigger('click');
+  expect(wrapper.emitted('choose')).toBeFalsy();
+});
+
+test('should close popover when clicking the action', async () => {
+  const wrapper = mount(Popover, {
+    props: {
+      visible: true,
+      list: iconItemList,
+      teleportDisable: false
+    }
+  });
+  await nextTick();
+
+  await wrapper.find('.nut-popover-menu-item').trigger('click');
+  expect(wrapper.emitted('update:visible')![0]).toEqual([false]);
+
+  await wrapper.setProps({ closeOnClickAction: false });
+  await wrapper.find('.nut-popover-menu-item').trigger('click');
+  expect(wrapper.emitted('update:visible')).toHaveLength(1);
+});
+
+test('Set Props Position', async () => {
+  const wrapper = mount(Popover, {
+    props: {
+      visible: true,
+      list: iconItemList,
+      teleportDisable: false,
+      location: 'top-start'
+    }
+  });
+  await nextTick();
+  expect(wrapper.find('.nut-popover-arrow--top-start').exists()).toBeTruthy();
+});

+ 18 - 0
src/packages/__VUE/popover/index.scss

@@ -125,6 +125,10 @@
         border-bottom: none;
         border-bottom: none;
       }
       }
 
 
+      .nut-popover-item-img {
+        vertical-align: top;
+      }
+
       .nut-popover-menu-item-name {
       .nut-popover-menu-item-name {
         margin-right: 12px;
         margin-right: 12px;
         margin-left: 8px;
         margin-left: 8px;
@@ -135,6 +139,10 @@
         color: $popover-disable-color;
         color: $popover-disable-color;
         cursor: not-allowed;
         cursor: not-allowed;
       }
       }
+
+      &.nut-popover-menu-taroitem {
+        display: flex;
+      }
     }
     }
 
 
     &--top {
     &--top {
@@ -263,3 +271,13 @@
 .nut-popover-leave-active {
 .nut-popover-leave-active {
   transition-timing-function: ease-in;
   transition-timing-function: ease-in;
 }
 }
+
+.nut-popover-content-bg {
+  position: fixed;
+  height: 100%;
+  width: 100%;
+  top: 0;
+  left: 0;
+  background: transparent;
+  z-index: 1999;
+}

+ 119 - 117
src/packages/__VUE/popover/index.taro.vue

@@ -1,177 +1,179 @@
 <template>
 <template>
-  <view @click.stop="openPopover" :class="classes">
-    <div ref="reference" :id="'reference-' + refRandomId" :class="customClass"> <slot name="reference"></slot></div>
-    <template v-if="showPopup">
-      <view class="more-background" @click.stop="closePopover"> </view>
-      <view :class="popoverContent" :style="getStyle">
+  <view :class="['nut-popover', `nut-popover--${theme}`, `${customClass}`]">
+    <view class="nut-popover-wrapper" @click="openPopover" ref="popoverRef" :id="'popoverRef' + refRandomId"
+      ><slot name="reference"></slot
+    ></view>
+
+    <nut-popup
+      :popClass="`nut-popover-content nut-popover-content--${location}`"
+      :style="getStyles"
+      v-model:visible="showPopup"
+      position=""
+      transition="nut-popover"
+      :overlay="overlay"
+      :duration="duration"
+      :overlayStyle="overlayStyle"
+      :overlayClass="overlayClass"
+      :closeOnClickOverlay="closeOnClickOverlay"
+    >
+      <view ref="popoverContentRef" :id="'popoverContentRef' + refRandomId" class="nut-popover-content-group">
         <view :class="popoverArrow" v-if="showArrow"> </view>
         <view :class="popoverArrow" v-if="showArrow"> </view>
-
-        <view class="popover-menu">
-          <slot name="content"></slot>
-          <view
-            v-for="(item, index) in list"
-            :key="index"
-            :class="{ 'popover-menu-item': true, disabled: item.disabled }"
-            @click.stop="chooseItem(item, index)"
-          >
-            <slot v-if="item.icon"> <nut-icon v-bind="$attrs" class="item-img" :name="item.icon"></nut-icon></slot>
-            <view class="popover-menu-name">{{ item.name }}</view>
-          </view>
+        <slot name="content"></slot>
+        <view
+          v-for="(item, index) in list"
+          :key="index"
+          :class="[
+            item.className,
+            item.disabled && 'nut-popover-menu-disabled',
+            'nut-popover-menu-item',
+            'nut-popover-menu-taroitem'
+          ]"
+          @click.stop="chooseItem(item, index)"
+        >
+          <slot v-if="item.icon">
+            <nut-icon
+              v-bind="$attrs"
+              class="nut-popover-item-img"
+              :classPrefix="iconPrefix"
+              :name="item.icon"
+            ></nut-icon
+          ></slot>
+          <view class="nut-popover-menu-name">{{ item.name }}</view>
         </view>
         </view>
       </view>
       </view>
-    </template>
+    </nut-popup>
+
+    <view class="nut-popover-content-bg" v-if="showPopup" @touchmove="clickAway" @click="clickAway"></view>
   </view>
   </view>
 </template>
 </template>
 <script lang="ts">
 <script lang="ts">
 import { onMounted, computed, watch, ref, PropType, toRefs, reactive, CSSProperties } from 'vue';
 import { onMounted, computed, watch, ref, PropType, toRefs, reactive, CSSProperties } from 'vue';
 import { createComponent } from '@/packages/utils/create';
 import { createComponent } from '@/packages/utils/create';
 const { componentName, create } = createComponent('popover');
 const { componentName, create } = createComponent('popover');
-import Popup from '../popup/index.taro.vue';
-import { popupProps } from '../popup/props';
-import Button from '../button/index.vue';
 import { useTaroRect } from '@/packages/utils/useTaroRect';
 import { useTaroRect } from '@/packages/utils/useTaroRect';
+import { isArray } from '@/packages/utils/util';
 import Taro from '@tarojs/taro';
 import Taro from '@tarojs/taro';
 
 
 export default create({
 export default create({
   inheritAttrs: false,
   inheritAttrs: false,
-  components: {
-    [Popup.name]: Popup,
-    [Button.name]: Button
-  },
+  components: {},
   props: {
   props: {
-    ...popupProps,
-    list: {
-      type: Array,
-      default: []
-    },
-
-    theme: {
-      type: String as PropType<import('./type').PopoverTheme>,
-      default: 'light'
-    },
-
-    location: {
-      type: String as PropType<import('./type').PopoverLocation>,
-      default: 'bottom'
-    },
-    // 位置出现的偏移量
-    offset: {
-      type: Array,
-      default: [0, 12]
-    },
-    customClass: {
-      type: String,
-      default: ''
-    },
-    showArrow: {
-      type: Boolean,
-      default: true
-    }
+    visible: { type: Boolean, default: false },
+    list: { type: Array, default: [] },
+    theme: { type: String as PropType<import('./type').PopoverTheme>, default: 'light' },
+    location: { type: String as PropType<import('./type').PopoverLocation>, default: 'bottom' },
+    offset: { type: Array, default: [0, 12] },
+    customClass: { type: String, default: '' },
+    showArrow: { type: Boolean, default: true },
+    iconPrefix: { type: String, default: 'nut-icon' },
+    duration: { type: [Number, String], default: 0.3 },
+    overlay: { type: Boolean, default: false },
+    overlayClass: { type: String, default: '' },
+    overlayStyle: { type: Object as PropType<CSSProperties> },
+    closeOnClickOverlay: { type: Boolean, default: true },
+    closeOnClickAction: { type: Boolean, default: true },
+    closeOnClickOutside: { type: Boolean, default: true }
   },
   },
   emits: ['update', 'update:visible', 'close', 'choose', 'open'],
   emits: ['update', 'update:visible', 'close', 'choose', 'open'],
   setup(props, { emit }) {
   setup(props, { emit }) {
-    const reference = ref<HTMLElement>();
-    const state = reactive({
-      elWidth: 0,
-      elHeight: 0
-    });
+    const popoverRef = ref();
+    const popoverContentRef = ref();
     const showPopup = ref(props.visible);
     const showPopup = ref(props.visible);
-
-    const { theme, location, offset } = toRefs(props);
-
-    const classes = computed(() => {
-      const prefixCls = componentName;
-      return {
-        [`${prefixCls}-taro`]: true,
-        [`${prefixCls}-taro--${theme.value}`]: theme.value
-      };
-    });
-
-    const popoverContent = computed(() => {
-      const prefixCls = 'popover-content';
-      return {
-        [`${prefixCls}`]: true,
-        [`${prefixCls}--${location.value}`]: location.value
-      };
+    const state = reactive({
+      rootWidth: 0,
+      rootHeight: 0
     });
     });
 
 
     const popoverArrow = computed(() => {
     const popoverArrow = computed(() => {
-      const prefixCls = 'popover-arrow';
-      return {
-        [prefixCls]: true,
-        [`${prefixCls}--${location.value}`]: location.value
-      };
+      const prefixCls = 'nut-popover-arrow';
+      const loca = props.location;
+      const direction = loca.split('-')[0];
+      return `${prefixCls} ${prefixCls}-${direction} ${prefixCls}--${loca}`;
     });
     });
-
-    const getReference = async () => {
-      const refe = await useTaroRect(reference, Taro);
-      console.log(refe);
-      state.elWidth = refe.width;
-      state.elHeight = refe.height;
-    };
-
-    const getStyle = computed(() => {
-      console.log(offset);
+    const getStyles = computed(() => {
+      let cross = +state.rootHeight;
+      let lengthways = +state.rootWidth;
+      let { offset, location } = props;
+      if (isArray(offset) && offset.length == 2) {
+        cross += +offset[1];
+        lengthways += +offset[1];
+      }
+      const direction = location.split('-')[0];
       const style: CSSProperties = {};
       const style: CSSProperties = {};
-      if (location.value.indexOf('top') !== -1) {
-        style.bottom = state.elHeight + (offset.value as any)[1] + 'px';
-      } else if (location.value.indexOf('right') !== -1) {
-        style.left = state.elWidth + (offset.value as any)[1] + 'px';
-      } else if (location.value.indexOf('left') !== -1) {
-        style.right = state.elWidth + (offset.value as any)[1] + 'px';
+      const mapd: any = {
+        top: 'bottom',
+        bottom: 'top',
+        left: 'right',
+        right: 'left'
+      };
+      if (['top', 'bottom'].includes(direction)) {
+        style[mapd[direction]] = `${cross}px`;
+        style.marginLeft = `${offset[0]}px`;
       } else {
       } else {
-        style.top = state.elHeight + (offset.value as any)[1] + 'px';
+        style[mapd[direction]] = `${lengthways}px`;
+        style.marginTop = `${offset[0]}px`;
       }
       }
-
       return style;
       return style;
     });
     });
-
-    onMounted(() => {
-      setTimeout(() => {
-        getReference();
-      }, 200);
-    });
-
+    // 获取宽度
+    const getContentWidth = async () => {
+      const refe = await useTaroRect(popoverRef, Taro);
+      const { height, width } = refe;
+      state.rootHeight = height;
+      state.rootWidth = width;
+    };
     watch(
     watch(
       () => props.visible,
       () => props.visible,
       (value) => {
       (value) => {
         showPopup.value = value;
         showPopup.value = value;
+        if (value) {
+          setTimeout(() => {
+            getContentWidth();
+          }, 200);
+        }
       }
       }
     );
     );
-
     const update = (val: boolean) => {
     const update = (val: boolean) => {
       emit('update', val);
       emit('update', val);
       emit('update:visible', val);
       emit('update:visible', val);
     };
     };
-
     const openPopover = () => {
     const openPopover = () => {
       update(!props.visible);
       update(!props.visible);
       emit('open');
       emit('open');
     };
     };
-
     const closePopover = () => {
     const closePopover = () => {
-      emit('close');
       emit('update:visible', false);
       emit('update:visible', false);
+      emit('close');
     };
     };
-
-    const chooseItem = (item: unknown, index: number) => {
+    const chooseItem = (item: any, index: number) => {
       emit('choose', item, index);
       emit('choose', item, index);
+      if (props.closeOnClickAction) {
+        closePopover();
+      }
+    };
+    const clickAway = (event: Event) => {
+      closePopover();
     };
     };
 
 
+    onMounted(() => {
+      setTimeout(() => {
+        getContentWidth();
+      }, 200);
+    });
+
     const refRandomId = Math.random().toString(36).slice(-8);
     const refRandomId = Math.random().toString(36).slice(-8);
 
 
     return {
     return {
-      classes,
       showPopup,
       showPopup,
       openPopover,
       openPopover,
-      popoverContent,
       popoverArrow,
       popoverArrow,
       closePopover,
       closePopover,
       chooseItem,
       chooseItem,
-      getReference,
-      reference,
-      getStyle,
-      refRandomId
+      popoverRef,
+      getStyles,
+      popoverContentRef,
+      refRandomId,
+      clickAway
     };
     };
   }
   }
 });
 });

+ 9 - 3
src/packages/__VUE/popover/index.vue

@@ -24,7 +24,12 @@
           @click.stop="chooseItem(item, index)"
           @click.stop="chooseItem(item, index)"
         >
         >
           <slot v-if="item.icon">
           <slot v-if="item.icon">
-            <nut-icon v-bind="$attrs" class="item-img" :classPrefix="iconPrefix" :name="item.icon"></nut-icon
+            <nut-icon
+              v-bind="$attrs"
+              class="nut-popover-item-img"
+              :classPrefix="iconPrefix"
+              :name="item.icon"
+            ></nut-icon
           ></slot>
           ></slot>
           <view class="nut-popover-menu-name">{{ item.name }}</view>
           <view class="nut-popover-menu-name">{{ item.name }}</view>
         </view>
         </view>
@@ -35,6 +40,7 @@
 <script lang="ts">
 <script lang="ts">
 import { computed, watch, ref, PropType, CSSProperties, reactive } from 'vue';
 import { computed, watch, ref, PropType, CSSProperties, reactive } from 'vue';
 import { createComponent } from '@/packages/utils/create';
 import { createComponent } from '@/packages/utils/create';
+import { isArray } from '@/packages/utils/util';
 const { create } = createComponent('popover');
 const { create } = createComponent('popover');
 export default create({
 export default create({
   components: {},
   components: {},
@@ -75,7 +81,7 @@ export default create({
       let cross = +state.rootHeight;
       let cross = +state.rootHeight;
       let lengthways = +state.rootWidth;
       let lengthways = +state.rootWidth;
       let { offset, location } = props;
       let { offset, location } = props;
-      if (Array.isArray(offset) && offset.length == 2) {
+      if (isArray(offset) && offset.length == 2) {
         cross += +offset[1];
         cross += +offset[1];
         lengthways += +offset[1];
         lengthways += +offset[1];
       }
       }
@@ -127,7 +133,7 @@ export default create({
       emit('close');
       emit('close');
     };
     };
     const chooseItem = (item: any, index: number) => {
     const chooseItem = (item: any, index: number) => {
-      emit('choose', item, index);
+      !item.disabled && emit('choose', item, index);
       if (props.closeOnClickAction) {
       if (props.closeOnClickAction) {
         closePopover();
         closePopover();
       }
       }

+ 2 - 0
src/packages/__VUE/popup/common.ts

@@ -77,6 +77,7 @@ export const component = (componentName: string) => {
       };
       };
 
 
       const onClickOverlay = (e: Event) => {
       const onClickOverlay = (e: Event) => {
+        console.log('关闭遮罩');
         if (props.closeOnClickOverlay) {
         if (props.closeOnClickOverlay) {
           emit('click-overlay', e);
           emit('click-overlay', e);
           emit('update:visible', false);
           emit('update:visible', false);
@@ -93,6 +94,7 @@ export const component = (componentName: string) => {
       };
       };
 
 
       watchEffect(() => {
       watchEffect(() => {
+        console.log('展示popup', props.visible);
         props.visible ? open() : close();
         props.visible ? open() : close();
         state.closed = props.closeable;
         state.closed = props.closeable;
       });
       });

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

@@ -7,15 +7,15 @@
     "miniprogram": {
     "miniprogram": {
       "list": [
       "list": [
         {
         {
-          "name": "business/pages/signature/index",
-          "pathName": "business/pages/signature/index",
+          "name": "exhibition/pages/imagepreview/index",
+          "pathName": "exhibition/pages/imagepreview/index",
           "query": "",
           "query": "",
           "launchMode": "default",
           "launchMode": "default",
           "scene": null
           "scene": null
         },
         },
         {
         {
-          "name": "business/pages/barrage/index",
-          "pathName": "business/pages/barrage/index",
+          "name": "exhibition/pages/popover/index",
+          "pathName": "exhibition/pages/popover/index",
           "query": "",
           "query": "",
           "launchMode": "default",
           "launchMode": "default",
           "scene": null
           "scene": null

+ 1 - 1
src/sites/mobile-taro/vue/src/basic/pages/popup/index.vue

@@ -7,7 +7,7 @@
     >
     >
     <h2>弹出位置</h2>
     <h2>弹出位置</h2>
     <nut-cell title="顶部弹出" is-link @click="showTop = true"></nut-cell>
     <nut-cell title="顶部弹出" is-link @click="showTop = true"></nut-cell>
-    <nut-popup position="top" :style="{ height: '20%' }" v-model:visible="showTop"></nut-popup>
+    <nut-popup position="top" :style="{ height: '10%' }" v-model:visible="showTop"></nut-popup>
     <nut-cell title="底部弹出" is-link @click="showBottom = true"></nut-cell>
     <nut-cell title="底部弹出" is-link @click="showBottom = true"></nut-cell>
     <nut-popup position="bottom" :style="{ height: '20%' }" v-model:visible="showBottom"></nut-popup>
     <nut-popup position="bottom" :style="{ height: '20%' }" v-model:visible="showBottom"></nut-popup>
     <nut-cell title="左侧弹出" is-link @click="showLeft = true"></nut-cell>
     <nut-cell title="左侧弹出" is-link @click="showLeft = true"></nut-cell>

+ 165 - 75
src/sites/mobile-taro/vue/src/exhibition/pages/popover/index.vue

@@ -1,33 +1,43 @@
 <template>
 <template>
   <div class="demo">
   <div class="demo">
     <h2>基础用法</h2>
     <h2>基础用法</h2>
-    <nut-popover v-model:visible="visible.lightTheme" :list="iconItemList" @choose="chooseItem">
-      <template #reference>
-        <nut-button type="primary" shape="square">明朗风格</nut-button>
-      </template>
-    </nut-popover>
-
-    <nut-popover v-model:visible="visible.darkTheme" theme="dark" :list="iconItemList">
-      <template #reference>
-        <nut-button type="primary" shape="square">暗黑风格</nut-button>
-      </template>
-    </nut-popover>
+    <nut-row type="flex">
+      <nut-col :span="8">
+        <nut-popover v-model:visible="lightTheme" :list="iconItemList" location="bottom-start" @choose="chooseItem">
+          <template #reference>
+            <nut-button type="primary" shape="square">明朗风格</nut-button>
+          </template>
+        </nut-popover>
+      </nut-col>
+      <nut-col :span="8">
+        <nut-popover v-model:visible="darkTheme" theme="dark" :list="iconItemList">
+          <template #reference>
+            <nut-button type="primary" shape="square">暗黑风格</nut-button>
+          </template>
+        </nut-popover>
+      </nut-col>
+    </nut-row>
 
 
     <h2>选项配置</h2>
     <h2>选项配置</h2>
-    <nut-popover v-model:visible="visible.showIcon" theme="dark" :list="itemList">
-      <template #reference>
-        <nut-button type="primary" shape="square">展示图标</nut-button>
-      </template>
-    </nut-popover>
-
-    <nut-popover v-model:visible="visible.disableAction" :list="itemListDisabled">
-      <template #reference>
-        <nut-button type="primary" shape="square">禁用选项</nut-button>
-      </template>
-    </nut-popover>
+    <nut-row type="flex">
+      <nut-col :span="8">
+        <nut-popover v-model:visible="showIcon" theme="dark" :list="itemList">
+          <template #reference>
+            <nut-button type="primary" shape="square">展示图标</nut-button>
+          </template>
+        </nut-popover>
+      </nut-col>
+      <nut-col :span="8">
+        <nut-popover v-model:visible="disableAction" :list="itemListDisabled" location="bottom-end">
+          <template #reference>
+            <nut-button type="primary" shape="square">禁用选项</nut-button>
+          </template>
+        </nut-popover>
+      </nut-col>
+    </nut-row>
 
 
     <h2>自定义内容</h2>
     <h2>自定义内容</h2>
-    <nut-popover v-model:visible="visible.Customized">
+    <nut-popover v-model:visible="Customized" location="top-start" custom-class="customClass">
       <template #reference>
       <template #reference>
         <nut-button type="primary" shape="square">自定义内容</nut-button>
         <nut-button type="primary" shape="square">自定义内容</nut-button>
       </template>
       </template>
@@ -43,31 +53,26 @@
     </nut-popover>
     </nut-popover>
 
 
     <h2>位置自定义</h2>
     <h2>位置自定义</h2>
-    <nut-popover v-model:visible="visible.topLocation" location="top" theme="dark" :list="iconItemList">
-      <template #reference>
-        <nut-button type="primary" shape="square">向上弹出</nut-button>
-      </template>
-    </nut-popover>
-
-    <h2></h2>
-    <nut-popover v-model:visible="visible.rightLocation" location="right" theme="dark" :list="iconItemList">
-      <template #reference>
-        <nut-button type="primary" shape="square">向右弹出</nut-button>
-      </template>
-    </nut-popover>
-    <nut-popover v-model:visible="visible.leftLocation" location="left" theme="dark" :list="iconItemList">
-      <template #reference>
-        <nut-button type="primary" shape="square">向左弹出</nut-button>
+    <nut-cell title="点击查看更多方向" @click="handlePicker"></nut-cell>
+    <nut-picker v-model:visible="showPicker" :columns="columns" title="" @change="change" :swipe-duration="500">
+      <template #top>
+        <div class="brickBox">
+          <nut-popover v-model:visible="customPositon" :location="curPostion" theme="dark" :list="positionList">
+            <template #reference>
+              <div class="brick"></div>
+            </template>
+          </nut-popover>
+        </div>
       </template>
       </template>
-    </nut-popover>
+    </nut-picker>
   </div>
   </div>
 </template>
 </template>
 <script lang="ts">
 <script lang="ts">
-import { reactive, ref } from 'vue';
+import { reactive, ref, toRefs } from 'vue';
 
 
 export default {
 export default {
   setup() {
   setup() {
-    const visible = ref({
+    const state = reactive({
       showIcon: false,
       showIcon: false,
       placement: false,
       placement: false,
       darkTheme: false,
       darkTheme: false,
@@ -76,110 +81,195 @@ export default {
       disableAction: false,
       disableAction: false,
       topLocation: false, //向上弹出
       topLocation: false, //向上弹出
       rightLocation: false, //向右弹出
       rightLocation: false, //向右弹出
-      leftLocation: false //向左弹出
+      leftLocation: false, //向左弹出
+      customPositon: false,
+
+      showPicker: false
     });
     });
+    const curPostion = ref('top');
+
+    const columns = ref([
+      { text: 'top', value: 'top' },
+      { text: 'top-start', value: 'top-start' },
+      { text: 'top-end', value: 'top-end' },
+      { text: 'right', value: 'right' },
+      { text: 'right-start', value: 'right-start' },
+      { text: 'right-end', value: 'right-end' },
+      { text: 'bottom', value: 'bottom' },
+      { text: 'bottom-start', value: 'bottom-start' },
+      { text: 'bottom-end', value: 'bottom-end' },
+      { text: 'left', value: 'left' },
+      { text: 'left-start', value: 'left-start' },
+      { text: 'left-end', value: 'left-end' }
+    ]);
 
 
     const iconItemList = reactive([
     const iconItemList = reactive([
       {
       {
-        name: '选项一'
+        name: 'option1'
       },
       },
       {
       {
-        name: '选项二'
+        name: 'option2'
       },
       },
       {
       {
-        name: '选项三'
+        name: 'option3'
+      }
+    ]);
+
+    const positionList = reactive([
+      {
+        name: 'option1'
+      },
+      {
+        name: 'option2'
       }
       }
     ]);
     ]);
 
 
     const itemList = reactive([
     const itemList = reactive([
       {
       {
-        name: '选项一',
+        name: 'option1',
         icon: 'my2'
         icon: 'my2'
       },
       },
       {
       {
-        name: '选项二',
+        name: 'option2',
         icon: 'cart2'
         icon: 'cart2'
       },
       },
       {
       {
-        name: '选项三',
+        name: 'option3',
         icon: 'location2'
         icon: 'location2'
       }
       }
     ]);
     ]);
 
 
     const itemListDisabled = reactive([
     const itemListDisabled = reactive([
       {
       {
-        name: '选项一',
+        name: 'option1',
         disabled: true
         disabled: true
       },
       },
       {
       {
-        name: '选项二',
+        name: 'option2',
         disabled: true
         disabled: true
       },
       },
       {
       {
-        name: '选项三'
+        name: 'option3'
       }
       }
     ]);
     ]);
 
 
     const selfContent = reactive([
     const selfContent = reactive([
       {
       {
         name: 'service',
         name: 'service',
-        desc: '选项一'
+        desc: 'option1'
       },
       },
       {
       {
         name: 'notice',
         name: 'notice',
-        desc: '选项二'
+        desc: 'option2'
       },
       },
       {
       {
         name: 'location',
         name: 'location',
-        desc: '选项三'
+        desc: 'option3'
       },
       },
       {
       {
         name: 'category',
         name: 'category',
-        desc: '选项四'
+        desc: 'option4'
       },
       },
       {
       {
         name: 'scan2',
         name: 'scan2',
-        desc: '选项五'
+        desc: 'option5'
       },
       },
       {
       {
         name: 'message',
         name: 'message',
-        desc: '选项六'
+        desc: 'option6'
       }
       }
     ]);
     ]);
 
 
-    const chooseItem = () => {
-      console.log('选择项');
+    const chooseItem = (item: unknown, index: number) => {
+      console.log(item, index);
+    };
+
+    const handlePicker = () => {
+      state.showPicker = true;
+      setTimeout(() => {
+        state.customPositon = true;
+      });
+    };
+
+    const change = ({ selectedValue }) => {
+      curPostion.value = selectedValue[0];
+      state.customPositon = true;
     };
     };
 
 
     return {
     return {
       iconItemList,
       iconItemList,
       itemList,
       itemList,
-      visible,
+      ...toRefs(state),
       itemListDisabled,
       itemListDisabled,
       selfContent,
       selfContent,
-      chooseItem
+      chooseItem,
+      curPostion,
+      positionList,
+      columns,
+      change,
+      handlePicker
     };
     };
   }
   }
 };
 };
 </script>
 </script>
 <style lang="scss">
 <style lang="scss">
-.self-content {
-  width: 195px;
+.demo > h2 {
+  padding: 0;
+}
+.brickBox {
+  margin: 80px 0;
+  display: flex;
+  justify-content: center;
+  .brick {
+    width: 60px;
+    height: 60px;
+    background: linear-gradient(135deg, #fa2c19 0%, #fa6419 100%);
+    border-radius: 10px;
+  }
+}
+
+.radiogroup {
   display: flex;
   display: flex;
   flex-wrap: wrap;
   flex-wrap: wrap;
-  &-item {
-    margin-top: 10px;
-    margin-bottom: 10px;
-    display: flex;
-    justify-content: center;
-    align-items: center;
-    flex-direction: column;
+  background: #fff;
+  padding: 10px 6px;
+
+  > .nut-radio {
+    width: 110px;
+
+    > .nut-radio__button {
+      padding: 5px 12px;
+      border: 1px solid #f6f7f9;
+    }
   }
   }
-  &-desc {
-    margin-top: 5px;
-    width: 60px;
-    font-size: 10px;
-    text-align: center;
+}
+
+.nut-popover-content {
+  width: 120px;
+}
+
+.customClass {
+  .nut-popover-content {
+    width: auto;
+  }
+  .self-content {
+    width: 195px;
+    display: flex;
+    flex-wrap: wrap;
+    &-item {
+      margin-top: 10px;
+      margin-bottom: 10px;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      flex-direction: column;
+    }
+    &-desc {
+      margin-top: 5px;
+      width: 60px;
+      font-size: 10px;
+      text-align: center;
+    }
   }
   }
 }
 }
 </style>
 </style>