Browse Source

feat: popover 组件增加新功能 (#1932)

* 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: 文件回撤

* feat: 单元测试修改

* fix: 组件popover样式问题修改

* feat: 新增 clamp 函数

* fix: 组件Infiniteloading Review 内容修改

* fix: 组件Infiniteloading问题更新

* feat: getScrollTopRoot函数修改

* feat: 组件popover新增功能
yangxiaolu1993 3 years ago
parent
commit
eaf379bedd

+ 55 - 15
src/packages/__VUE/popover/demo.vue

@@ -11,7 +11,7 @@
         </nut-popover>
       </nut-col>
       <nut-col :span="8">
-        <nut-popover v-model:visible="darkTheme" theme="dark" :list="iconItemList">
+        <nut-popover v-model:visible="darkTheme" theme="dark" location="bottom-start" :list="iconItemList">
           <template #reference>
             <nut-button type="primary" shape="square">{{ translate('dark') }}</nut-button>
           </template>
@@ -30,7 +30,7 @@
         </nut-popover>
       </nut-col>
       <nut-col :span="8">
-        <nut-popover v-model:visible="disableAction" :list="itemListDisabled" location="bottom-end">
+        <nut-popover v-model:visible="disableAction" :list="itemListDisabled" location="right">
           <template #reference>
             <nut-button type="primary" shape="square">{{ translate('disableAction') }}</nut-button>
           </template>
@@ -60,14 +60,38 @@
     <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 class="brick" id="pickerTarget"></div>
         </div>
       </template>
     </nut-picker>
+
+    <nut-popover
+      v-model:visible="customPositon"
+      targetId="pickerTarget"
+      :location="curPostion"
+      theme="dark"
+      :list="positionList"
+    >
+    </nut-popover>
+
+    <h2>{{ translate('contentTarget') }}</h2>
+    <nut-button type="primary" shape="square" id="popid" @click="clickCustomHandle">
+      {{ translate('contentTarget') }}
+    </nut-button>
+    <nut-popover
+      v-model:visible="customTarget"
+      targetId="popid"
+      :list="iconItemList"
+      location="top-start"
+    ></nut-popover>
+
+    <h2>{{ translate('contentColor') }}</h2>
+
+    <nut-popover v-model:visible="customColor" :list="iconItemList" location="right-start" bgColor="#f00" theme="dark">
+      <template #reference>
+        <nut-button type="primary" shape="square">{{ translate('contentColor') }}</nut-button>
+      </template>
+    </nut-popover>
   </div>
 </template>
 <script lang="ts">
@@ -87,7 +111,9 @@ const initTranslate = () =>
       dark: '暗黑风格',
       showIcon: '展示图标',
       disableAction: '禁用选项',
-      content: '自定义内容'
+      content: '自定义内容',
+      contentColor: '自定义颜色',
+      contentTarget: '自定义对象'
     },
     'en-US': {
       title: 'Basic Usage',
@@ -98,7 +124,9 @@ const initTranslate = () =>
       dark: 'dark',
       showIcon: 'show icon',
       disableAction: 'disabled',
-      content: 'custom content'
+      content: 'custom content',
+      contentColor: 'custom color',
+      contentTarget: 'custom target'
     }
   });
 export default createDemo({
@@ -116,7 +144,10 @@ export default createDemo({
       leftLocation: false, //向左弹出
       customPositon: false,
 
-      showPicker: false
+      showPicker: false,
+
+      customTarget: false,
+      customColor: false
     });
     const curPostion = ref('top');
 
@@ -220,13 +251,19 @@ export default createDemo({
       state.showPicker = true;
       setTimeout(() => {
         state.customPositon = true;
-      });
+      }, 0);
     };
 
     const change = ({ selectedValue }) => {
+      console.log('change');
       curPostion.value = selectedValue[0];
-      state.customPositon = true;
+      if (state.showPicker) state.customPositon = true;
+    };
+
+    const clickCustomHandle = () => {
+      state.customTarget = !state.customTarget;
     };
+
     return {
       iconItemList,
       itemList,
@@ -239,7 +276,8 @@ export default createDemo({
       translate,
       columns,
       change,
-      handlePicker
+      handlePicker,
+      clickCustomHandle
     };
   }
 });
@@ -276,8 +314,10 @@ export default createDemo({
   }
 }
 
-.nut-popover-content {
-  width: 120px;
+.demo {
+  .nut-popover-content {
+    width: 120px;
+  }
 }
 
 .customClass {

+ 82 - 0
src/packages/__VUE/popover/doc.en-US.md

@@ -284,6 +284,85 @@ export default {
 :::
 
 
+### custom target 
+
+Popover 提供了 `targetId` 属性,用于匹配目标元素,在目标元素上添加对应的 id 值即可
+
+:::demo
+```html
+<template>
+  <nut-button type="primary" shape="square" id="popid" @click="clickCustomHandle">custom target</nut-button>
+    <nut-popover v-model:visible="customTarget" targetId="popid" :list="itemList" location="top-start"></nut-popover>
+</template>
+
+<script>
+import { reactive, ref } from 'vue';
+export default {
+  setup() {
+    const visible = ref({
+      customTarget:false
+    });
+
+    const itemList = reactive([
+      {name: 'option1'},
+      {name: 'option2'},
+      {name: 'option3'}
+    ]);
+
+    const clickCustomHandle = () => {
+      visible.customTarget = !visible.customTarget;
+    };
+
+    return {
+        itemList,
+        visible,
+        clickCustomHandle,
+      };
+    }
+}
+</script>
+
+
+```
+:::
+
+### Custom Color
+
+:::demo
+```html
+<template>
+  <nut-popover v-model:visible="customColor" :list="itemList" location="right-start" bgColor="#f00" theme="dark">
+      <template #reference>
+        <nut-button type="primary" shape="square" >Custom Color</nut-button>
+      </template>
+    </nut-popover>
+</template>
+
+<script>
+import { reactive, ref } from 'vue';
+export default {
+  setup() {
+    const visible = ref({
+      customColor:false
+    });
+
+    const itemList = reactive([
+      {name: 'option1'},
+      {name: 'option2'},
+      {name: 'option3'}
+    ]);
+
+    return {
+        itemList,
+        visible
+      };
+    }
+}
+</script>
+
+```
+:::
+
 ## API
 ### Props  
 
@@ -304,6 +383,9 @@ export default {
 | close-on-click-overlay `v3.2.8`       | Whether to close when clicking overlay  | boolean  | true  |
 | close-on-click-action `v3.2.8`       | Whether to close when clicking action  | boolean  | true |
 | close-on-click-outside `v3.2.8`       | Whether to close when clicking outside | boolean  | true  |
+| bg-color `v3.3.1`       | Custom color | String  | -  |
+| target-id `v3.3.1`       | Custom target id | String  | -  |
+| arrow-offset `v3.3.1`       | the offset of the arrow | Number  | 0  |
 
 ### List data structure  
 

+ 83 - 0
src/packages/__VUE/popover/doc.md

@@ -282,6 +282,86 @@ export default {
 ```
 :::
 
+### 自定义目标元素
+
+Popover 提供了 `targetId` 属性,用于匹配目标元素,在目标元素上添加对应的 id 值即可
+
+:::demo
+```html
+<template>
+  <nut-button type="primary" shape="square" id="popid" @click="clickCustomHandle">自定义目标元素</nut-button>
+    <nut-popover v-model:visible="customTarget" targetId="popid" :list="itemList" location="top-start"></nut-popover>
+</template>
+
+<script>
+import { reactive, ref } from 'vue';
+export default {
+  setup() {
+    const visible = ref({
+      customTarget:false
+    });
+
+    const itemList = reactive([
+      {name: 'option1'},
+      {name: 'option2'},
+      {name: 'option3'}
+    ]);
+
+    const clickCustomHandle = () => {
+      visible.customTarget = !visible.customTarget;
+    };
+
+    return {
+        itemList,
+        visible,
+        clickCustomHandle,
+      };
+    }
+}
+</script>
+
+
+```
+:::
+
+### 自定义颜色
+
+Popopver 提供了 2 种主题色,同样可以通过 `bgColor` 属性改变背景色
+
+:::demo
+```html
+<template>
+  <nut-popover v-model:visible="customColor" :list="itemList" location="right-start" bgColor="#f00" theme="dark">
+      <template #reference>
+        <nut-button type="primary" shape="square" >自定义颜色</nut-button>
+      </template>
+    </nut-popover>
+</template>
+
+<script>
+import { reactive, ref } from 'vue';
+export default {
+  setup() {
+    const visible = ref({
+      customColor:false
+    });
+
+    const itemList = reactive([
+      {name: 'option1'},
+      {name: 'option2'},
+      {name: 'option3'}
+    ]);
+
+    return {
+        itemList,
+        visible
+      };
+    }
+}
+</script>
+
+```
+:::
 
 ## API
 ### Props  
@@ -303,6 +383,9 @@ export default {
 | close-on-click-overlay `v3.2.8`       | 是否在点击遮罩层后关闭菜单  | boolean  | true  |
 | close-on-click-action `v3.2.8`       | 是否在点击选项后关闭  | boolean  | true |
 | close-on-click-outside `v3.2.8`       | 是否在点击外部元素后关闭菜单 | boolean  | true  |
+| bg-color `v3.3.1`       | 自定义背景色 | String  | -  |
+| target-id `v3.3.1`       | 自定义目标元素 id | String  | -  |
+| arrow-offset `v3.3.1`       | 小箭头的偏移量 | Number  | 0  |
 
 ### List 数据结构  
 

+ 10 - 38
src/packages/__VUE/popover/index.scss

@@ -1,13 +1,8 @@
 .nut-popover {
-  position: relative;
+  position: absolute;
   display: inline-block;
   word-break: normal;
 
-  > .nut-popover-wrapper {
-    display: inline-block;
-    vertical-align: top;
-  }
-
   &-arrow {
     position: absolute;
     width: 0;
@@ -112,8 +107,8 @@
 
     .nut-popover-menu-item {
       display: block;
-      padding-bottom: 8px;
-      margin: 8px;
+      // padding-bottom: 8px;
+      padding: 8px;
       border-bottom: 1px solid $popover-border-bottom-color;
 
       &:first-child {
@@ -145,9 +140,6 @@
     }
 
     &--top {
-      left: 50%;
-      transform: translateX(-50%);
-
       .nut-popover-arrow--top {
         left: 50%;
         transform: translateX(-50%);
@@ -172,28 +164,10 @@
       }
     }
 
-    // bottom
-    &--bottom {
-      left: 50%;
-      transform: translateX(-50%);
-      // transform-origin:50% 0 ;
-    }
-
     &--bottom-end {
       right: 0;
     }
 
-    &--bottom-start {
-      left: 0;
-      // transform-origin:0 0;
-    }
-
-    // left
-    &--left {
-      top: 50%;
-      transform: translateY(-50%);
-    }
-
     &--left-end {
       bottom: 0;
     }
@@ -202,12 +176,6 @@
       top: 0;
     }
 
-    // right
-    &--right {
-      top: 50%;
-      transform: translateY(-50%);
-    }
-
     &--right-end {
       bottom: 0;
     }
@@ -220,8 +188,8 @@
 
 .nut-popover--dark {
   .nut-popover-content {
-    background: $popover-dark-background-color !important;
-    color: $popover-white-background-color !important;
+    background: $popover-dark-background-color;
+    color: $popover-white-background-color;
 
     &--bottom,
     &--bottom-start,
@@ -259,7 +227,7 @@
 
 .nut-popover-enter-from,
 .nut-popover-leave-active {
-  // transform: scale(0.8);
+  transform: scale(0.8);
   opacity: 0;
 }
 
@@ -280,3 +248,7 @@
   background: transparent;
   z-index: 1999;
 }
+
+.nut-popover-wrapper {
+  display: inline-block;
+}

+ 148 - 39
src/packages/__VUE/popover/index.taro.vue

@@ -1,12 +1,16 @@
 <template>
-  <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>
-
+  <view
+    class="nut-popover-wrapper"
+    @click="openPopover"
+    v-if="!targetId"
+    ref="popoverRef"
+    :id="'popoverRef' + refRandomId"
+    ><slot name="reference"></slot
+  ></view>
+  <view :class="['nut-popover', `nut-popover--${theme}`, `${customClass}`]" :style="getRootPosition">
     <nut-popup
       :popClass="`nut-popover-content nut-popover-content--${location}`"
-      :style="getStyles"
+      :style="customStyle"
       v-model:visible="showPopup"
       position=""
       transition="nut-popover"
@@ -17,7 +21,7 @@
       :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" :style="popoverArrowStyle"> </view>
         <slot name="content"></slot>
         <view
           v-for="(item, index) in list"
@@ -51,6 +55,7 @@ import { onMounted, computed, watch, ref, PropType, toRefs, reactive, CSSPropert
 import { createComponent } from '@/packages/utils/create';
 const { componentName, create } = createComponent('popover');
 import { useTaroRect } from '@/packages/utils/useTaroRect';
+import { useRect, rect } from '@/packages/utils/useRect';
 import { isArray } from '@/packages/utils/util';
 import Taro from '@tarojs/taro';
 
@@ -63,6 +68,7 @@ export default create({
     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] },
+    arrowOffset: { type: Number, default: 0 },
     customClass: { type: String, default: '' },
     showArrow: { type: Boolean, default: true },
     iconPrefix: { type: String, default: 'nut-icon' },
@@ -72,17 +78,22 @@ export default create({
     overlayStyle: { type: Object as PropType<CSSProperties> },
     closeOnClickOverlay: { type: Boolean, default: true },
     closeOnClickAction: { type: Boolean, default: true },
-    closeOnClickOutside: { type: Boolean, default: true }
+    closeOnClickOutside: { type: Boolean, default: true },
+    targetId: { type: String, default: '' },
+    bgColor: { type: String, default: '' }
   },
   emits: ['update', 'update:visible', 'close', 'choose', 'open'],
   setup(props, { emit }) {
     const popoverRef = ref();
     const popoverContentRef = ref();
     const showPopup = ref(props.visible);
-    const state = reactive({
-      rootWidth: 0,
-      rootHeight: 0
-    });
+
+    let rootRect = ref<rect>();
+
+    let conentRootRect = ref<{
+      height: number;
+      width: number;
+    }>();
 
     const popoverArrow = computed(() => {
       const prefixCls = 'nut-popover-arrow';
@@ -90,37 +101,129 @@ export default create({
       const direction = loca.split('-')[0];
       return `${prefixCls} ${prefixCls}-${direction} ${prefixCls}--${loca}`;
     });
-    const getStyles = computed(() => {
-      let cross = +state.rootHeight;
-      let lengthways = +state.rootWidth;
-      let { offset, location } = props;
+    const popoverArrowStyle = computed(() => {
+      const styles: CSSProperties = {};
+      const { bgColor, arrowOffset, location } = props;
+      const direction = location.split('-')[0];
+      const skew = location.split('-')[1];
+      const base = 16;
+
+      if (bgColor) {
+        styles[`border${upperCaseFirst(direction)}Color`] = bgColor;
+      }
+
+      if (props.arrowOffset != 0) {
+        if (['bottom', 'top'].includes(direction)) {
+          if (!skew) {
+            styles.left = `calc(50% + ${arrowOffset}px)`;
+          }
+          if (skew == 'start') {
+            styles.left = `${base + arrowOffset}px`;
+          }
+          if (skew == 'end') {
+            styles.right = `${base - arrowOffset}px`;
+          }
+        }
+
+        if (['left', 'right'].includes(direction)) {
+          if (!skew) {
+            styles.top = `calc(50% - ${arrowOffset}px)`;
+          }
+          if (skew == 'start') {
+            styles.top = `${base - arrowOffset}px`;
+          }
+          if (skew == 'end') {
+            styles.bottom = `${base + arrowOffset}px`;
+          }
+        }
+      }
+      return styles;
+    });
+
+    const upperCaseFirst = (str: string) => {
+      var str = str.toLowerCase();
+      str = str.replace(/\b\w+\b/g, (word) => word.substring(0, 1).toUpperCase() + word.substring(1));
+      return str;
+    };
+
+    const getRootPosition = computed(() => {
+      let styles: CSSProperties = {};
+
+      if (!rootRect.value || !conentRootRect.value) return {};
+
+      const conentWidth = conentRootRect.value.width;
+      const conentHeight = conentRootRect.value.height;
+
+      const { width, height, left, top } = rootRect.value;
+
+      const { location, offset } = props;
+      const direction = location.split('-')[0];
+      const skew = location.split('-')[1];
+      let cross = 0;
+      let parallel = 0;
       if (isArray(offset) && offset.length == 2) {
         cross += +offset[1];
-        lengthways += +offset[1];
+        parallel += +offset[0];
       }
-      const direction = location.split('-')[0];
-      const style: CSSProperties = {};
-      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 {
-        style[mapd[direction]] = `${lengthways}px`;
-        style.marginTop = `${offset[0]}px`;
+      if (width) {
+        if (['bottom', 'top'].includes(direction)) {
+          const h = direction == 'bottom' ? height + cross : -(conentHeight + cross);
+
+          styles.top = `${top + h}px`;
+
+          if (!skew) {
+            styles.left = `${-(conentWidth - width) / 2 + left + parallel}px`;
+          }
+          if (skew == 'start') {
+            styles.left = `${left + parallel}px`;
+          }
+          if (skew == 'end') {
+            styles.left = `${rootRect.value.right + parallel}px`;
+          }
+        }
+        if (['left', 'right'].includes(direction)) {
+          const contentW = direction == 'left' ? -(conentWidth + cross) : width + cross;
+          styles.left = `${left + contentW}px`;
+          if (!skew) {
+            styles.top = `${top - conentHeight / 2 + height / 2 - 4 + parallel}px`;
+          }
+          if (skew == 'start') {
+            styles.top = `${top + parallel}px`;
+          }
+          if (skew == 'end') {
+            styles.top = `${top + height + parallel}px`;
+          }
+        }
+      }
+
+      return styles;
+    });
+
+    const customStyle = computed(() => {
+      const styles: CSSProperties = {};
+      if (props.bgColor) {
+        styles.background = props.bgColor;
       }
-      return style;
+
+      return styles;
     });
     // 获取宽度
     const getContentWidth = async () => {
-      const refe = await useTaroRect(popoverRef, Taro);
-      const { height, width } = refe;
-      state.rootHeight = height;
-      state.rootWidth = width;
+      let rect;
+      if (props.targetId) {
+        rect = await useTaroRect(props.targetId, Taro);
+      } else {
+        rect = await useTaroRect(popoverRef, Taro);
+      }
+      rootRect.value = rect;
+    };
+
+    const getPopoverContentW = async () => {
+      let rectContent = await useTaroRect(popoverContentRef, Taro);
+      conentRootRect.value = {
+        height: rectContent.height,
+        width: rectContent.width
+      };
     };
     watch(
       () => props.visible,
@@ -129,7 +232,11 @@ export default create({
         if (value) {
           setTimeout(() => {
             getContentWidth();
-          }, 200);
+          }, 100);
+
+          setTimeout(() => {
+            getPopoverContentW();
+          }, 300);
         }
       }
     );
@@ -170,10 +277,12 @@ export default create({
       closePopover,
       chooseItem,
       popoverRef,
-      getStyles,
       popoverContentRef,
       refRandomId,
-      clickAway
+      clickAway,
+      popoverArrowStyle,
+      customStyle,
+      getRootPosition
     };
   }
 });

+ 143 - 49
src/packages/__VUE/popover/index.vue

@@ -1,10 +1,11 @@
 <template>
-  <view :class="['nut-popover', `nut-popover--${theme}`, `${customClass}`]">
-    <view class="nut-popover-wrapper" @click="openPopover" ref="popoverRef"><slot name="reference"></slot></view>
-
+  <div class="nut-popover-wrapper" @click="openPopover" ref="popoverRef" v-if="!targetId"
+    ><slot name="reference"></slot
+  ></div>
+  <view :class="['nut-popover', `nut-popover--${theme}`, `${customClass}`]" :style="getRootPosition">
     <nut-popup
       :popClass="`nut-popover-content nut-popover-content--${location}`"
-      :style="getStyles"
+      :style="customStyle"
       v-model:visible="showPopup"
       position=""
       transition="nut-popover"
@@ -15,7 +16,7 @@
       :closeOnClickOverlay="closeOnClickOverlay"
     >
       <view ref="popoverContentRef" class="nut-popover-content-group">
-        <view :class="popoverArrow" v-if="showArrow"> </view>
+        <view :class="popoverArrow" v-if="showArrow" :style="popoverArrowStyle"> </view>
         <slot name="content"></slot>
         <view
           v-for="(item, index) in list"
@@ -24,12 +25,7 @@
           @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
+            <Icon v-bind="$attrs" class="nut-popover-item-img" :classPrefix="iconPrefix" :name="item.icon"></Icon
           ></slot>
           <view class="nut-popover-menu-item-name">{{ item.name }}</view>
         </view>
@@ -41,15 +37,17 @@
 import { computed, watch, ref, PropType, CSSProperties, reactive } from 'vue';
 import { createComponent } from '@/packages/utils/create';
 import { isArray } from '@/packages/utils/util';
+import { useRect, rect } from '@/packages/utils/useRect';
 const { create } = createComponent('popover');
 export default create({
   components: {},
   props: {
     visible: { type: Boolean, default: false },
-    list: { type: Array, default: [] },
+    list: { type: Array as PropType<import('./type').PopoverList[]>, 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] },
+    arrowOffset: { type: Number, default: 0 },
     customClass: { type: String, default: '' },
     showArrow: { type: Boolean, default: true },
     iconPrefix: { type: String, default: 'nut-icon' },
@@ -59,17 +57,22 @@ export default create({
     overlayStyle: { type: Object as PropType<CSSProperties> },
     closeOnClickOverlay: { type: Boolean, default: true },
     closeOnClickAction: { type: Boolean, default: true },
-    closeOnClickOutside: { type: Boolean, default: true }
+    closeOnClickOutside: { type: Boolean, default: true },
+    targetId: { type: String, default: '' },
+    bgColor: { type: String, default: '' }
   },
   emits: ['update', 'update:visible', 'close', 'choose', 'open'],
   setup(props, { emit }) {
     const popoverRef = ref();
     const popoverContentRef = ref();
     const showPopup = ref(props.visible);
-    const state = reactive({
-      rootWidth: 0,
-      rootHeight: 0
-    });
+
+    let rootRect = ref<rect>();
+
+    let conentRootRect = ref<{
+      height: number;
+      width: number;
+    }>();
 
     const popoverArrow = computed(() => {
       const prefixCls = 'nut-popover-arrow';
@@ -77,36 +80,124 @@ export default create({
       const direction = loca.split('-')[0];
       return `${prefixCls} ${prefixCls}-${direction} ${prefixCls}--${loca}`;
     });
-    const getStyles = computed(() => {
-      let cross = +state.rootHeight;
-      let lengthways = +state.rootWidth;
-      let { offset, location } = props;
+
+    const popoverArrowStyle = computed(() => {
+      const styles: CSSProperties = {};
+      const { bgColor, arrowOffset, location } = props;
+      const direction = location.split('-')[0];
+      const skew = location.split('-')[1];
+      const base = 16;
+
+      if (bgColor) {
+        styles[`border${upperCaseFirst(direction)}Color`] = bgColor;
+      }
+
+      if (props.arrowOffset != 0) {
+        if (['bottom', 'top'].includes(direction)) {
+          if (!skew) {
+            styles.left = `calc(50% + ${arrowOffset}px)`;
+          }
+          if (skew == 'start') {
+            styles.left = `${base + arrowOffset}px`;
+          }
+          if (skew == 'end') {
+            styles.right = `${base - arrowOffset}px`;
+          }
+        }
+
+        if (['left', 'right'].includes(direction)) {
+          if (!skew) {
+            styles.top = `calc(50% - ${arrowOffset}px)`;
+          }
+          if (skew == 'start') {
+            styles.top = `${base - arrowOffset}px`;
+          }
+          if (skew == 'end') {
+            styles.bottom = `${base + arrowOffset}px`;
+          }
+        }
+      }
+      return styles;
+    });
+
+    const upperCaseFirst = (str: string) => {
+      var str = str.toLowerCase();
+      str = str.replace(/\b\w+\b/g, (word) => word.substring(0, 1).toUpperCase() + word.substring(1));
+      return str;
+    };
+
+    const getRootPosition = computed(() => {
+      let styles: CSSProperties = {};
+
+      if (!rootRect.value || !conentRootRect.value) return {};
+
+      const conentWidth = conentRootRect.value.width;
+      const conentHeight = conentRootRect.value.height;
+      const { width, height, left, top } = rootRect.value;
+
+      const { location, offset } = props;
+      const direction = location.split('-')[0];
+      const skew = location.split('-')[1];
+      let cross = 0;
+      let parallel = 0;
       if (isArray(offset) && offset.length == 2) {
         cross += +offset[1];
-        lengthways += +offset[1];
+        parallel += +offset[0];
       }
-      const direction = location.split('-')[0];
-      const style: CSSProperties = {};
-      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 {
-        style[mapd[direction]] = `${lengthways}px`;
-        style.marginTop = `${offset[0]}px`;
+      if (width) {
+        if (['bottom', 'top'].includes(direction)) {
+          const h = direction == 'bottom' ? height + cross : -(conentHeight + cross);
+          styles.top = `${top + h}px`;
+
+          if (!skew) {
+            styles.left = `${-(conentWidth - width) / 2 + left + parallel}px`;
+          }
+          if (skew == 'start') {
+            styles.left = `${left + parallel}px`;
+          }
+          if (skew == 'end') {
+            styles.left = `${rootRect.value.right + parallel}px`;
+          }
+        }
+        if (['left', 'right'].includes(direction)) {
+          const contentW = direction == 'left' ? -(conentWidth + cross) : width + cross;
+          styles.left = `${left + contentW}px`;
+          if (!skew) {
+            styles.top = `${top - conentHeight / 2 + height / 2 - 4 + parallel}px`;
+          }
+          if (skew == 'start') {
+            styles.top = `${top + parallel}px`;
+          }
+          if (skew == 'end') {
+            styles.top = `${top + height + parallel}px`;
+          }
+        }
       }
-      return style;
+
+      return styles;
+    });
+
+    const customStyle = computed(() => {
+      const styles: CSSProperties = {};
+      if (props.bgColor) {
+        styles.background = props.bgColor;
+      }
+
+      return styles;
     });
     // 获取宽度
     const getContentWidth = () => {
-      const { offsetHeight, offsetWidth } = popoverRef.value;
-      state.rootHeight = offsetHeight;
-      state.rootWidth = offsetWidth;
+      let rect = useRect(popoverRef.value);
+      if (props.targetId) {
+        rect = useRect(document.querySelector(`#${props.targetId}`));
+      }
+      rootRect.value = rect;
+      setTimeout(() => {
+        conentRootRect.value = {
+          height: popoverContentRef.value.clientHeight,
+          width: popoverContentRef.value.clientWidth
+        };
+      }, 0);
     };
     watch(
       () => props.visible,
@@ -141,13 +232,14 @@ export default create({
     const clickAway = (event: Event) => {
       const element = popoverRef.value;
       const elContent = popoverContentRef.value;
-      if (
-        element &&
-        !element.contains(event.target) &&
-        elContent &&
-        !elContent.contains(event.target) &&
-        props.closeOnClickOutside
-      ) {
+
+      let el = element && !element.contains(event.target);
+
+      if (props.targetId) {
+        const dom = document.querySelector(`#${props.targetId}`);
+        el = dom && !dom.contains(event.target);
+      }
+      if (el && elContent && !elContent.contains(event.target) && props.closeOnClickOutside) {
         closePopover();
       }
     };
@@ -159,8 +251,10 @@ export default create({
       closePopover,
       chooseItem,
       popoverRef,
-      getStyles,
-      popoverContentRef
+      popoverContentRef,
+      getRootPosition,
+      customStyle,
+      popoverArrowStyle
     };
   }
 });

+ 8 - 0
src/packages/__VUE/popover/type.ts

@@ -13,3 +13,11 @@ export type PopoverLocation =
   | 'left-end'
   | 'right-start'
   | 'right-end';
+
+export type PopoverList = {
+  name: string;
+  icon?: string;
+  disabled?: boolean;
+  className?: any;
+  [key: PropertyKey]: any;
+};

+ 10 - 3
src/packages/utils/useRect/index.ts

@@ -14,9 +14,16 @@ function isWindow(val: unknown): val is Window {
   return val === window;
 }
 
-export const useRect = (
-  elementRef: (Element | Window) | Ref<Element | Window | undefined>
-) => {
+export interface rect {
+  top: number;
+  left: number;
+  right: number;
+  bottom: number;
+  width: number;
+  height: number;
+}
+
+export const useRect = (elementRef: (Element | Window) | Ref<Element | Window | undefined>) => {
   const element = unref(elementRef);
 
   if (isWindow(element)) {

+ 2 - 1
src/packages/utils/useTaroRect/index.ts

@@ -47,7 +47,8 @@ export const useTaroRect = (elementRef: (Element | Window | any) | Ref<Element |
       });
     } else {
       const query = Taro.createSelectorQuery();
-      query.select(`#${(element as any).id}`) && query.select(`#${(element as any).id}`).boundingClientRect();
+      let el = (element as any).id ? (element as any).id : (element as any);
+      query.select(`#${el}`) && query.select(`#${el}`).boundingClientRect();
       query.exec(function (res: any) {
         resolve(res[0]);
       });

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

@@ -40,8 +40,8 @@
           "scene": null
         },
         {
-          "name": "exhibition/pages/collapse/index",
-          "pathName": "exhibition/pages/collapse/index",
+          "name": "exhibition/pages/popover/index",
+          "pathName": "exhibition/pages/popover/index",
           "query": "",
           "launchMode": "default",
           "scene": null

+ 38 - 9
src/sites/mobile-taro/vue/src/exhibition/pages/popover/index.vue

@@ -57,14 +57,36 @@
     <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 class="brick" id="pickerTarget"></div>
         </div>
       </template>
     </nut-picker>
+
+    <nut-popover
+      v-model:visible="customPositon"
+      targetId="pickerTarget"
+      :location="curPostion"
+      theme="dark"
+      :list="positionList"
+    >
+    </nut-popover>
+
+    <h2>自定义对象</h2>
+    <nut-button type="primary" shape="square" id="popid" @click="clickCustomHandle"> 自定义对象 </nut-button>
+    <nut-popover
+      v-model:visible="customTarget"
+      targetId="popid"
+      :list="iconItemList"
+      location="top-start"
+    ></nut-popover>
+
+    <h2>自定义颜色</h2>
+
+    <nut-popover v-model:visible="customColor" :list="iconItemList" location="right-start" bgColor="#f00" theme="dark">
+      <template #reference>
+        <nut-button type="primary" shape="square">自定义颜色</nut-button>
+      </template>
+    </nut-popover>
   </div>
 </template>
 <script lang="ts">
@@ -84,7 +106,9 @@ export default {
       leftLocation: false, //向左弹出
       customPositon: false,
 
-      showPicker: false
+      showPicker: false,
+      customTarget: false,
+      customColor: false
     });
     const curPostion = ref('top');
 
@@ -188,12 +212,16 @@ export default {
       state.showPicker = true;
       setTimeout(() => {
         state.customPositon = true;
-      });
+      }, 500);
     };
 
     const change = ({ selectedValue }) => {
       curPostion.value = selectedValue[0];
-      state.customPositon = true;
+      if (state.showPicker) state.customPositon = true;
+    };
+
+    const clickCustomHandle = () => {
+      state.customTarget = !state.customTarget;
     };
 
     return {
@@ -207,7 +235,8 @@ export default {
       positionList,
       columns,
       change,
-      handlePicker
+      handlePicker,
+      clickCustomHandle
     };
   }
 };