Browse Source

feat: popover功能补全 (#1306)

* feat: datepicker 国际化

* feat: popover 功能补全
yangxiaolu1993 3 years ago
parent
commit
c36d087b39

+ 1 - 0
package.json

@@ -76,6 +76,7 @@
   "devDependencies": {
     "@commitlint/cli": "^10.0.0",
     "@commitlint/config-conventional": "^10.0.0",
+    "@popperjs/core": "^2.11.5",
     "@tarojs/taro": "^3.4.0",
     "@types/jest": "^26.0.22",
     "@types/node": "^17.0.16",

+ 0 - 302
src/packages/__VUE/audio/index.taro.vue

@@ -1,302 +0,0 @@
-<template>
-  <!-- 显示进度条 、 播放时长、 兼容是否支持 、暂停、 开启-->
-
-  <view class="nut-audio">
-    <!-- 进度条 -->
-    <view class="progress-wrapper" v-if="type == 'progress'">
-      <!-- 时间显示 -->
-      <view class="time">{{ currentDuration }}</view>
-      <view class="progress-bar-wrapper">
-        <nut-range
-          v-model="percent"
-          hidden-range
-          @change="progressChange"
-          inactive-color="#cccccc"
-          active-color="#fa2c19"
-        >
-          <template #button>
-            <view class="custom-button"></view>
-          </template>
-        </nut-range>
-      </view>
-
-      <view class="time">{{ duration }}</view>
-    </view>
-
-    <!-- 自定义 -->
-    <view class="nut-audio-icon" v-if="type == 'icon'">
-      <view
-        :class="['nut-audio-icon-box', playing ? 'nut-audio-icon-play' : 'nut-audio-icon-stop']"
-        @click="changeStatus"
-      >
-        <nut-icon v-if="playing" name="service" class="nut-icon-am-rotate nut-icon-am-infinite"></nut-icon>
-        <nut-icon v-if="!playing" name="service"></nut-icon>
-      </view>
-    </view>
-
-    <view v-if="type == 'none'" @click="changeStatus">
-      <slot></slot>
-    </view>
-
-    <!-- 操作按钮 -->
-    <template v-if="type != 'none'">
-      <slot></slot>
-    </template>
-
-    <audio
-      class="audioMain"
-      :controls="type == 'controls'"
-      ref="audioRef"
-      :src="url"
-      :preload="preload"
-      :autoplay="autoplay"
-      :loop="loop"
-      @timeupdate="onTimeupdate"
-      @ended="audioEnd"
-      :muted="hanMuted"
-    >
-    </audio>
-  </view>
-</template>
-<script lang="ts">
-import { toRefs, ref, onMounted, reactive, watch, provide } from 'vue';
-import { createComponent } from '@/packages/utils/create';
-const { componentName, create } = createComponent('audio');
-
-export default create({
-  props: {
-    url: {
-      type: String,
-      default() {
-        return '';
-      }
-    },
-    // 静音
-    muted: {
-      type: Boolean,
-      default() {
-        return false;
-      }
-    },
-    // 自动播放
-    autoplay: {
-      type: Boolean,
-      default() {
-        return false;
-      }
-    },
-
-    // 循环播放
-    loop: {
-      type: Boolean,
-      default() {
-        return false;
-      }
-    },
-
-    // 是否预加载音频
-    preload: {
-      type: String,
-      default() {
-        return 'auto';
-      }
-    },
-    /* 总时长秒数 */
-    second: {
-      type: Number,
-      default() {
-        return 0;
-      }
-    },
-
-    // 展示的形式   controls 控制面板   progress 进度条  icon 图标 none 自定义
-    type: {
-      type: String,
-      default() {
-        return 'progress';
-      }
-    }
-  },
-  components: {},
-  emits: ['fastBack', 'play', 'forward', 'ended', 'changeProgress', 'mute'],
-
-  setup(props, { emit }) {
-    const audioRef = ref(null);
-
-    const audioData = reactive({
-      currentTime: 0,
-      currentDuration: '00:00:00',
-      percent: 0,
-      duration: '00:00:00',
-      second: 0,
-      hanMuted: props.muted,
-      playing: props.autoplay
-    });
-
-    onMounted(() => {
-      // 播放的兼容性
-      var arr = ['webkitVisibilityState', 'visibilitychange'];
-      try {
-        for (let i = 0; i < arr.length; i++) {
-          document.addEventListener(arr[i], () => {
-            if (document.hidden) {
-              // 页面被挂起
-              // 这里要根据用户当前播放状态,做音频暂停操作
-              (audioRef.value as any).pause();
-            } else {
-              // 页面呼出
-              if (audioData.playing) {
-                setTimeout(() => {
-                  // 这里要 根据页面挂起前音频的播放状态,做音频播放操作
-                  (audioRef.value as any).play();
-                }, 200);
-              }
-            }
-          });
-        }
-      } catch (e) {
-        console.log((e as any).message);
-      }
-
-      // 获取当前音频播放时长
-      setTimeout(() => {
-        // 自动播放
-        if (props.autoplay) {
-          if (audioRef.value && audioRef.value.paused) {
-            audioRef.value.play();
-          }
-        }
-        audioData.second = audioRef.value.duration;
-        audioData.duration = formatSeconds(audioRef.value.duration);
-      }, 500);
-    });
-
-    //播放时间
-    const onTimeupdate = (e) => {
-      audioData.currentTime = parseInt(e.target.currentTime);
-    };
-
-    //后退
-    const fastBack = () => {
-      audioData.currentTime--;
-      audioRef.value.currentTime = audioData.currentTime;
-
-      emit('fastBack', audioData.currentTime);
-    };
-
-    //改变播放状态
-    const changeStatus = () => {
-      if (audioData.playing) {
-        audioRef.value.pause();
-
-        audioData.handPlaying = false;
-      } else {
-        audioRef.value.play();
-        audioData.handPlaying = true;
-      }
-      audioData.playing = !audioData.playing;
-
-      emit('play', audioData.playing);
-    };
-
-    //快进
-    const forward = () => {
-      audioData.currentTime++;
-      audioRef.value.currentTime = audioData.currentTime;
-
-      emit('forward', audioData.currentTime);
-    };
-
-    //处理
-    const handle = (val) => {
-      //毫秒数转为时分秒
-      audioData.currentDuration = formatSeconds(val);
-      audioData.percent = (val / audioData.second) * 100;
-    };
-    //播放结束 修改播放状态
-    const audioEnd = () => {
-      audioData.playing = false;
-      emit('ended');
-    };
-
-    //点击进度条
-    const progressChange = (val) => {
-      audioRef.value.currentTime = (audioData.second * val) / 100;
-
-      emit('changeProgress', audioRef.value.currentTime);
-    };
-
-    // 静音
-    const handleMute = () => {
-      audioData.hanMuted = !audioData.hanMuted;
-
-      emit('mute', audioData.hanMuted);
-    };
-
-    const formatSeconds = (value) => {
-      let theTime = parseInt(value); // 秒
-      let theTime1 = 0; // 分
-      let theTime2 = 0; // 小时
-      if (theTime > 60) {
-        theTime1 = parseInt(theTime / 60);
-        theTime = parseInt(theTime % 60);
-        if (theTime1 > 60) {
-          theTime2 = parseInt(theTime1 / 60);
-          theTime1 = parseInt(theTime1 % 60);
-        }
-      }
-      let result = '' + parseInt(theTime);
-      if (result < 10) {
-        result = '0' + result;
-      }
-      if (theTime1 > 0) {
-        result = '' + parseInt(theTime1) + ':' + result;
-        if (theTime1 < 10) {
-          result = '0' + result;
-        }
-      } else {
-        result = '00:' + result;
-      }
-      if (theTime2 > 0) {
-        result = '' + parseInt(theTime2) + ':' + result;
-        if (theTime2 < 10) {
-          result = '0' + result;
-        }
-      } else {
-        result = '00:' + result;
-      }
-      return result;
-    };
-
-    watch(
-      () => audioData.currentTime,
-      (value) => {
-        handle(value);
-      }
-    );
-
-    provide('audioParent', {
-      children: [],
-      props,
-      audioData,
-      handleMute,
-      forward,
-      fastBack,
-      changeStatus
-    });
-
-    return {
-      ...toRefs(props),
-      ...toRefs(audioData),
-      audioRef,
-      fastBack,
-      forward,
-      changeStatus,
-      progressChange,
-      audioEnd,
-      onTimeupdate,
-      handleMute
-    };
-  }
-});
-</script>

+ 116 - 39
src/packages/__VUE/popover/demo.vue

@@ -1,33 +1,50 @@
 <template>
   <div class="demo">
     <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="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="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>
-    <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="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="visible.disableAction" :list="itemListDisabled">
+          <template #reference>
+            <nut-button type="primary" shape="square">禁用选项</nut-button>
+          </template>
+        </nut-popover>
+      </nut-col>
+    </nut-row>
 
     <h2>自定义内容</h2>
-    <nut-popover v-model:visible="visible.Customized">
+    <nut-popover v-model:visible="visible.Customized" location="bottom-start">
       <template #reference>
         <nut-button type="primary" shape="square">自定义内容</nut-button>
       </template>
@@ -43,23 +60,26 @@
     </nut-popover>
 
     <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>
-      </template>
-    </nut-popover>
+    <nut-row type="flex" justify="center">
+      <nut-col :span="24" style="text-align: center">
+        <nut-popover
+          v-model:visible="visible.customPositon"
+          :location="curPostion"
+          theme="dark"
+          :list="positionList"
+          customClass="brickBox"
+        >
+          <template #reference>
+            <div class="brick"></div>
+          </template>
+        </nut-popover>
+      </nut-col>
+    </nut-row>
+
+    <nut-radiogroup v-model="curPostion" direction="horizontal" class="radiogroup">
+      <nut-radio shape="button" :label="pos" v-for="(pos, i) in position" :key="i">{{ pos }}</nut-radio>
+    </nut-radiogroup>
   </div>
 </template>
 <script lang="ts">
@@ -78,8 +98,24 @@ export default createDemo({
       disableAction: false,
       topLocation: false, //向上弹出
       rightLocation: false, //向右弹出
-      leftLocation: false //向左弹出
+      leftLocation: false, //向左弹出
+      customPositon: false
     });
+    const curPostion = ref('top');
+    const position = ref([
+      'top',
+      'top-start',
+      'top-end',
+      'right',
+      'right-start',
+      'right-end',
+      'bottom',
+      'bottom-start',
+      'bottom-end',
+      'left',
+      'left-start',
+      'left-end'
+    ]);
 
     const iconItemList = reactive([
       {
@@ -93,6 +129,15 @@ export default createDemo({
       }
     ]);
 
+    const positionList = reactive([
+      {
+        name: '选项一'
+      },
+      {
+        name: '选项二'
+      }
+    ]);
+
     const itemList = reactive([
       {
         name: '选项一',
@@ -160,12 +205,44 @@ export default createDemo({
       visible,
       itemListDisabled,
       selfContent,
-      chooseItem
+      chooseItem,
+      position,
+      curPostion,
+      positionList
     };
   }
 });
 </script>
 <style lang="scss">
+.demo > h2 {
+  padding: 0;
+}
+.brickBox {
+  margin: 80px 0;
+  .brick {
+    width: 60px;
+    height: 60px;
+    background: #1989fa;
+    border-radius: 10px;
+  }
+}
+
+.radiogroup {
+  display: flex;
+  flex-wrap: wrap;
+  background: #fff;
+  padding: 10px 6px;
+
+  > .nut-radio {
+    width: 110px;
+
+    > .nut-radio__button {
+      padding: 5px 12px;
+      border: 1px solid #f6f7f9;
+    }
+  }
+}
+
 .self-content {
   width: 195px;
   display: flex;

+ 40 - 49
src/packages/__VUE/popover/doc.md

@@ -10,9 +10,9 @@
 
 import { createApp } from 'vue';
 // vue
-import { Popover } from '@nutui/nutui';
+import { Popover, Popup } from '@nutui/nutui';
 // taro
-import { Popover } from '@nutui/nutui-taro';
+import { Popover, Popup } from '@nutui/nutui-taro';
 
 const app = createApp();
 app.use(Popover);
@@ -33,27 +33,26 @@ Popover 支持明朗和暗黑两种风格,默认为明朗风格,将 theme 
       <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>
 </template>
 <script>
 import { reactive, ref } from 'vue';
 export default {
   setup() {
     const visible = ref({
-     lightTheme: false,
+      darkTheme: false,
+      lightTheme: false,
     });
-
     const iconItemList = reactive([
-      {
-        name: '选项一'
-      },
-      {
-        name: '选项二'
-      },
-      {
-        name: '选项三'
-      }
+      { name: '选项一' },
+      { name: '选项二' },
+      { name: '选项三' }
     ]);
-
     return {
         visible,
         iconItemList,
@@ -62,8 +61,6 @@ export default {
 }
 </script>
 
-
-
 ```
 :::
 
@@ -100,12 +97,10 @@ export default {
       {
         name: '选项一',
         icon: 'my2'
-      },
-      {
+      },{
         name: '选项二',
         icon: 'cart2'
-      },
-      {
+      },{
         name: '选项三',
         icon: 'location2'
       }
@@ -115,12 +110,10 @@ export default {
       {
         name: '选项一',
         disabled: true
-      },
-      {
+      },{
         name: '选项二',
         disabled: true
-      },
-      {
+      },{
         name: '选项三'
       }
     ]);
@@ -233,26 +226,28 @@ export default {
 
 ### 位置自定义
 
-支持 bottom, top, left, right 四种弹出位置,默认值为 bottom。
+通过 location 属性来控制气泡的弹出位置。可选值
+```
+top           # 顶部中间位置
+top-start     # 顶部左侧位置
+top-end       # 顶部右侧位置
+left          # 左侧中间位置
+left-start    # 左侧上方位置
+left-end      # 左侧下方位置
+right         # 右侧中间位置
+right-start   # 右侧上方位置
+right-end     # 右侧下方位置
+bottom        # 底部中间位置
+bottom-start  # 底部左侧位置
+bottom-end    # 底部右侧位置
+```
 
 :::demo
 ```html
 <template>
-  <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">
+  <nut-popover v-model:visible="visible" location="top" theme="dark" :list="iconItemList">
     <template #reference>
-    <nut-button type="primary" shape="square">向左弹出</nut-button>
+      <div class="brick"></div>
     </template>
   </nut-popover>
 </template>
@@ -261,11 +256,7 @@ export default {
 import { reactive, ref } from 'vue';
 export default {
   setup() {
-    const visible = ref({
-      topLocation: false, 
-      rightLocation: false, 
-      leftLocation: false 
-    });
+    const visible = ref(false);
 
     const iconItemList = reactive([
         {
@@ -273,11 +264,7 @@ export default {
         },
         {
           name: '选项二'
-        },
-        {
-          name: '选项三'
-        }
-      ]);
+        }]);
 
       return {
         iconItemList,
@@ -300,6 +287,9 @@ export default {
 | visible      | 是否展示气泡弹出层                 | boolean  | false     |
 | theme          | 主题风格,可选值为 dark            | string   | `light`   |
 | location       | 弹出位置,可选值为 top,left,right  | string   | `bottom`  |
+| offset       | 出现位置的偏移量  | [number, number]   | [0, 12]  |
+| show-arrow       | 是否显示小箭头  | boolean  | true  |
+| custom-class       | 自定义 class 值  | string  | ''  |
 
 ### List 数据结构  
 
@@ -310,6 +300,7 @@ List 属性是一个由对象构成的数组,数组中的每个对象配置一
 | name           | 选项文字               | string   | -      |
 | icon           | nut-icon 图标名称      | string   | -      |
 | disabled       | 是否为禁用状态          | boolean  | false  | 
+| className       | 为对应选项添加额外的类名          | string/Array/object  | -  | 
 
 
 ### Slots

+ 402 - 81
src/packages/__VUE/popover/index.scss

@@ -1,8 +1,8 @@
-.nut-popover--dark,
 .nut-popover {
-  position: relative;
-  display: inline-block;
-  margin-right: 20px;
+  overflow-y: inherit;
+  transform: inherit;
+
+  // 遮罩
   .more-background {
     opacity: 0;
     position: fixed;
@@ -12,74 +12,402 @@
     left: 0;
     top: 0;
   }
-  .popoverContent--left,
-  .popoverContent--right,
-  .popoverContent--top,
-  .popoverContent {
-    z-index: 12;
+
+  .popover-arrow {
+    position: absolute;
+    width: 0;
+    height: 0;
+    border: 8px solid transparent;
+  }
+  // top
+  &[data-popper-placement^='top'] {
+    .popover-arrow {
+      bottom: 0;
+      border-top-color: $popover-white-background-color;
+      border-bottom-width: 0;
+      margin-bottom: -8px;
+    }
+  }
+
+  &[data-popper-placement='top'] {
+    .popover-arrow {
+      left: 50%;
+      transform: translateX(-50%);
+    }
+  }
+
+  &[data-popper-placement='top-start'] {
+    .popover-arrow {
+      left: 16px;
+      transform: translateX(0%);
+    }
+  }
+
+  &[data-popper-placement='top-end'] {
+    .popover-arrow {
+      right: 16px;
+      transform: translateX(0%);
+    }
+  }
+
+  // left
+  &[data-popper-placement^='left'] {
+    .popover-arrow {
+      right: 0px;
+      border-left-color: $popover-white-background-color;
+      border-right-width: 0;
+      margin-right: -8px;
+    }
+  }
+
+  &[data-popper-placement='left'] {
+    .popover-arrow {
+      top: 50%;
+      transform: translateY(-50%);
+    }
+  }
+
+  &[data-popper-placement='left-start'] {
+    .popover-arrow {
+      top: 16px;
+      transform: translateY(0%);
+    }
+  }
+
+  &[data-popper-placement='left-end'] {
+    .popover-arrow {
+      bottom: 16px;
+      transform: translateY(0%);
+    }
+  }
+
+  // right
+
+  &[data-popper-placement^='right'] {
+    .popover-arrow {
+      left: 0px;
+      border-right-color: $popover-white-background-color;
+      border-left-width: 0;
+      margin-left: -8px;
+    }
+  }
+
+  &[data-popper-placement='right'] {
+    .popover-arrow {
+      top: 50%;
+      transform: translateY(-50%);
+    }
+  }
+
+  &[data-popper-placement='right-start'] {
+    .popover-arrow {
+      top: 16px;
+      transform: translateY(0%);
+    }
+  }
+
+  &[data-popper-placement='right-end'] {
+    .popover-arrow {
+      bottom: 16px;
+      transform: translateY(0%);
+    }
+  }
+  // bottom
+  &[data-popper-placement^='bottom'] {
+    .popover-arrow {
+      top: 0px;
+      border-bottom-color: $popover-white-background-color;
+      border-top-width: 0;
+      margin-top: -8px;
+    }
+  }
+
+  &[data-popper-placement='bottom'] {
+    .popover-arrow {
+      left: 50%;
+      transform: translateX(-50%);
+    }
+  }
+
+  &[data-popper-placement='bottom-start'] {
+    .popover-arrow {
+      left: 16px;
+      transform: translateX(0%);
+    }
+  }
+
+  &[data-popper-placement='bottom-end'] {
+    .popover-arrow {
+      right: 16px;
+      transform: translateX(0%);
+    }
+  }
+
+  .popover-menu {
+    display: block;
     background: $popover-white-background-color;
     border-radius: 5px;
-    opacity: 1;
     font-size: 14px;
     font-family: PingFangSC;
     font-weight: normal;
     color: $popover-primary-text-color;
-    position: absolute;
-    .popoverArrow {
-      position: absolute;
-      width: 0;
-      height: 0;
-      border-left: 8px solid transparent;
-      border-right: 8px solid transparent;
-      border-top: 10px solid transparent;
-      border-bottom: 10px solid $popover-white-background-color;
-    }
-    .title-item {
+    box-shadow: 0 2px 12px rgb(50 50 51 / 12%);
+    .popover-menu-item {
       display: flex;
       align-items: center;
       padding-bottom: 8px;
-      margin: 8px;
+      padding: 8px 0;
+      margin: 0 8px;
       border-bottom: 1px solid $popover-border-bottom-color;
-      &:first-child {
-        margin-top: 15px;
-      }
+
       &:last-child {
-        margin-bottom: 2px;
         border-bottom: none;
       }
-      .title-name {
+      .popover-menu-name {
+        display: block;
         margin-right: 12px;
         margin-left: 8px;
-        width: 100%;
       }
     }
   }
-  .popoverContent--top {
-    .popoverArrow--top {
-      position: absolute;
-      top: auto;
-      border-left: 8px solid transparent;
-      border-right: 8px solid transparent;
-      border-top: 10px solid $popover-white-background-color;
-      border-bottom: 10px solid transparent;
+
+  .disabled {
+    color: $popover-disable-color;
+    cursor: not-allowed;
+  }
+}
+
+.nut-popover--dark {
+  color: $popover-white-background-color;
+
+  &[data-popper-placement^='top'] {
+    .popover-arrow {
+      border-top-color: $popover-dark-background-color;
     }
   }
-  .popoverContent--left {
-    .popoverArrow--left {
-      position: absolute;
-      border-left: 10px solid $popover-white-background-color;
-      border-right: 10px solid transparent;
-      border-top: 8px solid transparent;
-      border-bottom: 8px solid transparent;
+
+  &[data-popper-placement^='left'] {
+    .popover-arrow {
+      border-left-color: $popover-dark-background-color;
     }
   }
-  .popoverContent--right {
-    .popoverArrow--right {
+
+  &[data-popper-placement^='right'] {
+    .popover-arrow {
+      border-right-color: $popover-dark-background-color;
+    }
+  }
+
+  &[data-popper-placement^='bottom'] {
+    .popover-arrow {
+      border-bottom-color: $popover-dark-background-color;
+    }
+  }
+
+  .popover-menu {
+    background: $popover-dark-background-color;
+    color: $popover-white-background-color;
+  }
+}
+
+// Taro
+.nut-popover-taro {
+  position: relative;
+  display: inline-block;
+  .more-background {
+    opacity: 0;
+    position: fixed;
+    width: 100%;
+    height: 100%;
+    z-index: 10;
+    left: 0;
+    top: 0;
+  }
+
+  .popover-content {
+    position: absolute;
+    z-index: 12;
+    min-width: 80px;
+
+    .popover-arrow {
       position: absolute;
-      border-left: 10px solid transparent;
-      border-right: 10px solid $popover-white-background-color;
-      border-top: 8px solid transparent;
-      border-bottom: 8px solid transparent;
+      width: 0;
+      height: 0;
+      border: 8px solid transparent;
+    }
+
+    .popover-menu {
+      box-shadow: 0 2px 12px rgb(50 50 51 / 12%);
+      border-radius: 5px;
+      font-size: 14px;
+      font-family: PingFangSC;
+      font-weight: normal;
+      color: $popover-primary-text-color;
+      background: $popover-white-background-color;
+      .popover-menu-item {
+        display: flex;
+        align-items: center;
+        padding: 8px 0;
+        margin: 0 8px;
+        border-bottom: 1px solid $popover-border-bottom-color;
+        &:last-child {
+          border-bottom: none;
+        }
+        .popover-menu-name {
+          margin-right: 12px;
+          margin-left: 8px;
+          width: 100%;
+        }
+      }
+    }
+  }
+  // bottom
+  .popover-content--bottom {
+    left: 50%;
+    transform: translateX(-50%);
+    .popover-arrow {
+      top: 0px;
+      border-bottom-color: $popover-white-background-color;
+      border-top-width: 0;
+      margin-top: -8px;
+      left: 50%;
+      transform: translateX(-50%);
+    }
+  }
+
+  .popover-content--bottom-start {
+    .popover-arrow {
+      top: 0px;
+      border-bottom-color: $popover-white-background-color;
+      border-top-width: 0;
+      margin-top: -8px;
+      left: 16px;
+      transform: translateX(0%);
+    }
+  }
+
+  .popover-content--bottom-end {
+    right: 0;
+    .popover-arrow {
+      top: 0px;
+      border-bottom-color: $popover-white-background-color;
+      border-top-width: 0;
+      margin-top: -8px;
+      right: 16px;
+      transform: translateX(0%);
+    }
+  }
+
+  // top
+  .popover-content--top {
+    left: 50%;
+    transform: translateX(-50%);
+    .popover-arrow {
+      bottom: 0;
+      border-top-color: $popover-white-background-color;
+      border-bottom-width: 0;
+      margin-bottom: -8px;
+
+      left: 50%;
+      transform: translateX(-50%);
+    }
+  }
+
+  .popover-content--top-start {
+    .popover-arrow {
+      bottom: 0;
+      border-top-color: $popover-white-background-color;
+      border-bottom-width: 0;
+      margin-bottom: -8px;
+
+      left: 16px;
+      transform: translateX(0%);
+    }
+  }
+
+  .popover-content--top-end {
+    right: 0;
+    .popover-arrow {
+      bottom: 0;
+      border-top-color: $popover-white-background-color;
+      border-bottom-width: 0;
+      margin-bottom: -8px;
+
+      right: 16px;
+      transform: translateX(0%);
+    }
+  }
+  // left
+  .popover-content--left {
+    top: 50%;
+    transform: translateY(-50%);
+    .popover-arrow {
+      right: 0px;
+      border-left-color: $popover-white-background-color;
+      border-right-width: 0;
+      margin-right: -8px;
+      top: 50%;
+      transform: translateY(-50%);
+    }
+  }
+  .popover-content--left-start {
+    top: 0%;
+    .popover-arrow {
+      right: 0px;
+      border-left-color: $popover-white-background-color;
+      border-right-width: 0;
+      margin-right: -8px;
+      top: 16px;
+      transform: translateX(0%);
+    }
+  }
+
+  .popover-content--left-end {
+    bottom: 0%;
+    .popover-arrow {
+      right: 0px;
+      border-left-color: $popover-white-background-color;
+      border-right-width: 0;
+      margin-right: -8px;
+      bottom: 16px;
+      transform: translateX(0%);
+    }
+  }
+  // right
+  .popover-content--right {
+    top: 50%;
+    transform: translateY(-50%);
+    .popover-arrow {
+      left: 0px;
+      border-right-color: $popover-white-background-color;
+      border-left-width: 0;
+      margin-left: -8px;
+      top: 50%;
+      transform: translateY(-50%);
+    }
+  }
+
+  .popover-content--right-start {
+    top: 0%;
+    .popover-arrow {
+      left: 0px;
+      border-right-color: $popover-white-background-color;
+      border-left-width: 0;
+      margin-left: -8px;
+      top: 16px;
+      transform: translateY(0%);
+    }
+  }
+
+  .popover-content--right-end {
+    bottom: 0%;
+    .popover-arrow {
+      left: 0px;
+      border-right-color: $popover-white-background-color;
+      border-left-width: 0;
+      margin-left: -8px;
+      bottom: 16px;
+      transform: translateY(0%);
     }
   }
 
@@ -89,45 +417,38 @@
   }
 }
 
-.nut-popover--dark {
-  background: $popover-dark-background-color;
+.nut-popover-taro--dark {
   color: $popover-white-background-color;
-  .popoverContent--left,
-  .popoverContent--right,
-  .popoverContent--top,
-  .popoverContent {
-    background: $popover-dark-background-color;
-    color: $popover-white-background-color;
-    .popoverArrow {
-      border-bottom: 10px solid $popover-dark-background-color;
+  .popover-content--bottom,
+  .popover-content--bottom-start,
+  .popover-content--bottom-end {
+    .popover-arrow {
+      border-bottom-color: $popover-dark-background-color;
     }
   }
-  .popoverContent--top {
-    .popoverArrow--top {
-      position: absolute;
-      top: auto;
-      border-left: 8px solid transparent;
-      border-right: 8px solid transparent;
-      border-top: 10px solid $popover-dark-background-color;
-      border-bottom: 10px solid transparent;
+  .popover-content--top,
+  .popover-content--top-start,
+  .popover-content--top-end {
+    .popover-arrow {
+      border-top-color: $popover-dark-background-color;
     }
   }
-  .popoverContent--left {
-    .popoverArrow--left {
-      position: absolute;
-      border-left: 10px solid $popover-dark-background-color;
-      border-right: 10px solid transparent;
-      border-top: 8px solid transparent;
-      border-bottom: 8px solid transparent;
+  .popover-content--left,
+  .popover-content--left-start,
+  .popover-content--left-end {
+    .popover-arrow {
+      border-left-color: $popover-dark-background-color;
     }
   }
-  .popoverContent--right {
-    .popoverArrow--right {
-      position: absolute;
-      border-left: 10px solid transparent;
-      border-right: 10px solid $popover-dark-background-color;
-      border-top: 8px solid transparent;
-      border-bottom: 8px solid transparent;
+  .popover-content--right,
+  .popover-content--right-start,
+  .popover-content--right-end {
+    .popover-arrow {
+      border-right-color: $popover-dark-background-color;
     }
   }
+  .popover-menu {
+    background: $popover-dark-background-color !important;
+    color: $popover-white-background-color !important;
+  }
 }

+ 64 - 49
src/packages/__VUE/popover/index.taro.vue

@@ -1,21 +1,22 @@
 <template>
   <view @click.stop="openPopover" :class="classes">
-    <div ref="reference" :id="'reference-' + refRandomId"> <slot name="reference"></slot></div>
+    <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="popoverArrow" :style="getArrowStyle"> </view>
-
-        <slot name="content"></slot>
-
-        <view
-          v-for="(item, index) in list"
-          :key="index"
-          :class="{ 'title-item': true, disabled: item.disabled }"
-          @click.stop="chooseItem(item, index)"
-        >
-          <slot v-if="item.icon"> <nut-icon class="item-img" :name="item.icon"></nut-icon></slot>
-          <view class="title-name">{{ item.name }}</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 class="item-img" :name="item.icon"></nut-icon></slot>
+            <view class="popover-menu-name">{{ item.name }}</view>
+          </view>
         </view>
       </view>
     </template>
@@ -51,9 +52,22 @@ export default create({
     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
     }
   },
-  emits: ['update', 'update:visible', 'close', 'choose', 'openPopover', 'open'],
+  emits: ['update', 'update:visible', 'close', 'choose', 'open'],
   setup(props, { emit }) {
     const reference = ref<HTMLElement>();
     const state = reactive({
@@ -62,26 +76,26 @@ export default create({
     });
     const showPopup = ref(props.visible);
 
-    const { theme, location } = toRefs(props);
+    const { theme, location, offset } = toRefs(props);
 
     const classes = computed(() => {
       const prefixCls = componentName;
       return {
-        [prefixCls]: true,
-        [`${prefixCls}--${theme.value}`]: theme.value
+        [`${prefixCls}-taro`]: true,
+        [`${prefixCls}-taro--${theme.value}`]: theme.value
       };
     });
 
     const popoverContent = computed(() => {
-      const prefixCls = 'popoverContent';
+      const prefixCls = 'popover-content';
       return {
-        [prefixCls]: true,
+        [`${prefixCls}`]: true,
         [`${prefixCls}--${location.value}`]: location.value
       };
     });
 
     const popoverArrow = computed(() => {
-      const prefixCls = 'popoverArrow';
+      const prefixCls = 'popover-arrow';
       return {
         [prefixCls]: true,
         [`${prefixCls}--${location.value}`]: location.value
@@ -90,41 +104,22 @@ export default create({
 
     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 style: CSSProperties = {};
-      if (location.value == 'top') {
-        style.bottom = state.elHeight + 10 + 'px';
-      } else if (location.value == 'right') {
-        style.top = 0 + 'px';
-        style.right = -state.elWidth + 'px';
-      } else if (location.value == 'left') {
-        style.top = 0 + 'px';
-        style.left = -state.elWidth + 'px';
+      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';
       } else {
-        style.top = state.elHeight + 10 + 'px';
-      }
-
-      return style;
-    });
-
-    const getArrowStyle = computed(() => {
-      const style: CSSProperties = {};
-      if (location.value == 'top') {
-        style.bottom = -20 + 'px';
-        style.left = state.elWidth / 2 + 'px';
-      } else if (location.value == 'right') {
-        style.top = 20 + 'px';
-        style.left = -20 + 'px';
-      } else if (location.value == 'left') {
-        style.top = 20 + 'px';
-        style.right = -20 + 'px';
-      } else {
-        style.left = state.elWidth / 2 + 'px';
-        style.top = -20 + 'px';
+        style.top = state.elHeight + (offset.value as any)[1] + 'px';
       }
 
       return style;
@@ -175,9 +170,29 @@ export default create({
       getReference,
       reference,
       getStyle,
-      getArrowStyle,
       refRandomId
     };
   }
 });
 </script>
+<style lang="scss">
+.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>

+ 111 - 75
src/packages/__VUE/popover/index.vue

@@ -1,32 +1,43 @@
 <template>
-  <view @click.stop="openPopover" :class="classes">
-    <div ref="reference"> <slot name="reference"></slot></div>
-    <template v-if="showPopup">
-      <view class="more-background" @click.stop="closePopover"> </view>
-      <view :class="popoverContent" :style="getStyle">
-        <view :class="popoverArrow" :style="getArrowStyle"> </view>
-
-        <slot name="content"></slot>
-
-        <view
-          v-for="(item, index) in list"
-          :key="index"
-          :class="{ 'title-item': true, disabled: item.disabled }"
-          @click.stop="chooseItem(item, index)"
-        >
-          <slot v-if="item.icon"> <nut-icon class="item-img" :name="item.icon"></nut-icon></slot>
-          <view class="title-name">{{ item.name }}</view>
-        </view>
+  <!-- 气泡弹出层  按钮 -->
+  <view style="display: inline-block" :class="customClass" @click.stop="openPopover" ref="reference">
+    <slot name="reference"></slot
+  ></view>
+
+  <nut-popup
+    ref="popoverRef"
+    :pop-class="classes"
+    v-model:visible="showPopup"
+    :overlay="false"
+    @clickOverlay="clickOverlay"
+  >
+    <!-- 气泡弹出层  箭头 -->
+    <view :class="popoverArrow" v-if="showArrow"> </view>
+    <!-- 气泡弹出层  内容 -->
+    <slot name="content"></slot>
+    <view class="popover-menu" :class="popoverContent" ref="popoverMenu">
+      <view
+        v-for="(item, index) in list"
+        :key="index"
+        :class="[item.className, { 'popover-menu-item': true, disabled: item.disabled }]"
+        @click.stop="chooseItem(item, index)"
+      >
+        <slot v-if="item.icon"> <nut-icon class="item-img" :name="item.icon"></nut-icon></slot>
+        <view class="popover-menu-name">{{ item.name }}</view>
       </view>
-    </template>
-  </view>
+    </view>
+  </nut-popup>
 </template>
 <script lang="ts">
-import { onMounted, computed, watch, ref, PropType, toRefs, reactive, CSSProperties } from 'vue';
+import { onMounted, computed, watch, ref, PropType, toRefs, nextTick, onUnmounted } from 'vue';
 import { createComponent } from '@/packages/utils/create';
 const { componentName, create } = createComponent('popover');
 import Popup, { popupProps } from '../popup/index.vue';
 import Button from '../button/index.vue';
+import { createPopper } from '@popperjs/core/lib/popper-lite';
+import offsetModifier from '@popperjs/core/lib/modifiers/offset';
+import type { Instance, Placement } from '@popperjs/core';
+
 export default create({
   inheritAttrs: false,
   components: {
@@ -48,29 +59,39 @@ export default create({
     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
     }
   },
-  emits: ['update', 'update:visible', 'close', 'choose', 'openPopover', 'open'],
+  emits: ['update', 'update:visible', 'close', 'choose', 'open'],
   setup(props, { emit }) {
+    let popper: Instance | null;
     const reference = ref();
-    const state = reactive({
-      elWidth: 0,
-      elHeight: 0
-    });
+    const popoverRef = ref();
+
     const showPopup = ref(props.visible);
 
     const { theme, location } = toRefs(props);
 
     const classes = computed(() => {
       const prefixCls = componentName;
-      return {
-        [prefixCls]: true,
-        [`${prefixCls}--${theme.value}`]: theme.value
-      };
+
+      return `${prefixCls} ${prefixCls}--${theme.value}`;
     });
 
     const popoverContent = computed(() => {
-      const prefixCls = 'popoverContent';
+      const prefixCls = 'popover-content';
       return {
         [prefixCls]: true,
         [`${prefixCls}--${location.value}`]: location.value
@@ -78,65 +99,81 @@ export default create({
     });
 
     const popoverArrow = computed(() => {
-      const prefixCls = 'popoverArrow';
+      const prefixCls = 'popover-arrow';
       return {
         [prefixCls]: true,
         [`${prefixCls}--${location.value}`]: location.value
       };
     });
 
-    function getReference() {
-      const domElem = document.documentElement;
-      state.elWidth = reference.value.offsetWidth;
-      state.elHeight = reference.value.offsetHeight;
-    }
-
-    const getStyle = computed(() => {
-      const style: CSSProperties = {};
-      if (location.value == 'top') {
-        style.bottom = state.elHeight + 20 + 'px';
-        style.left = 0 + 'px';
-      } else if (location.value == 'right') {
-        style.top = 0 + 'px';
-        style.right = -state.elWidth + 'px';
-      } else if (location.value == 'left') {
-        style.top = 0 + 'px';
-        style.left = -state.elWidth + 'px';
-      } else {
-        style.top = state.elHeight + 20 + 'px';
-        style.left = 0 + 'px';
+    const createPopperInstance = () => {
+      console.log(reference.value, popoverRef.value);
+      if (reference.value && popoverRef.value) {
+        return createPopper(reference.value, popoverRef.value.popupRef, {
+          placement: props.location,
+          modifiers: [
+            {
+              name: 'computeStyles',
+              options: {
+                adaptive: false,
+                gpuAcceleration: false
+              }
+            },
+            Object.assign({}, offsetModifier, {
+              options: {
+                offset: props.offset
+              }
+            })
+          ]
+        });
       }
+      return null;
+    };
 
-      return style;
-    });
+    const clickOverlay = () => {
+      console.log('关闭');
+    };
 
-    const getArrowStyle = computed(() => {
-      const style: CSSProperties = {};
-      if (location.value == 'top') {
-        style.bottom = -20 + 'px';
-        style.left = state.elWidth / 2 + 'px';
-      } else if (location.value == 'right') {
-        style.top = 20 + 'px';
-        style.left = -20 + 'px';
-      } else if (location.value == 'left') {
-        style.top = 20 + 'px';
-        style.right = -20 + 'px';
-      } else {
-        style.left = state.elWidth / 2 + 'px';
-        style.top = -20 + 'px';
-      }
+    const uploadLocation = () => {
+      nextTick(() => {
+        if (!showPopup.value) return;
+        if (!popper) {
+          popper = createPopperInstance();
+          console.log(popper);
+        } else {
+          popper.setOptions({
+            placement: props.location
+          });
+        }
+      });
+    };
 
-      return style;
+    const clickAway = (event: any) => {
+      const element = reference.value;
+      if (element && !element.contains(event.target as Node)) {
+        closePopover();
+      }
+    };
+    onMounted(() => {
+      window.addEventListener('click', clickAway, true);
     });
 
-    onMounted(() => {
-      getReference();
+    onUnmounted(() => {
+      window.removeEventListener('click', clickAway, true);
     });
 
     watch(
       () => props.visible,
       (value) => {
         showPopup.value = value;
+        uploadLocation();
+      }
+    );
+
+    watch(
+      () => props.location,
+      (value) => {
+        uploadLocation();
       }
     );
 
@@ -170,10 +207,9 @@ export default create({
       popoverArrow,
       closePopover,
       chooseItem,
-      getReference,
       reference,
-      getStyle,
-      getArrowStyle
+      popoverRef,
+      clickOverlay
     };
   }
 });

+ 13 - 1
src/packages/__VUE/popover/type.ts

@@ -1,3 +1,15 @@
 export type PopoverTheme = 'light' | 'dark';
 
-export type PopoverLocation = 'bottom' | 'top' | 'left' | 'right';
+export type PopoverLocation =
+  | 'bottom'
+  | 'top'
+  | 'left'
+  | 'right'
+  | 'top-start'
+  | 'top-end'
+  | 'bottom-start'
+  | 'bottom-end'
+  | 'left-start'
+  | 'left-end'
+  | 'right-start'
+  | 'right-end';

+ 6 - 3
src/packages/__VUE/popup/index.vue

@@ -12,7 +12,7 @@
       @click="onClickOverlay"
     />
     <Transition :name="transitionName" @after-enter="onOpened" @after-leave="onClosed">
-      <view v-show="visible" :class="classes" :style="popStyle" @click="onClick">
+      <view v-show="visible" :class="classes" :style="popStyle" @click="onClick" ref="popupRef">
         <slot v-if="showSlot"></slot>
         <view
           v-if="closed"
@@ -64,7 +64,8 @@ import {
   reactive,
   PropType,
   CSSProperties,
-  toRefs
+  toRefs,
+  ref
 } from 'vue';
 import { useLockScroll } from './use-lock-scroll';
 import { overlayProps } from './../overlay/index.vue';
@@ -148,6 +149,7 @@ export default create({
   emits: ['click', 'click-close-icon', 'open', 'close', 'opend', 'closed', 'update:visible', 'click-overlay'],
 
   setup(props, { emit }) {
+    const popupRef = ref();
     const state = reactive({
       zIndex: props.zIndex ? (props.zIndex as number) : _zIndex,
       showSlot: true,
@@ -295,7 +297,8 @@ export default create({
       onClickCloseIcon,
       onClickOverlay,
       onOpened,
-      onClosed
+      onClosed,
+      popupRef
     };
   }
 });