u-popover.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. <template>
  2. <view class="u-popover" :style="[$u.addStyle(customStyle)]">
  3. <view class="u-popover__trigger" id="popover-trigger" ref="popoverTrigger" @click.stop="clickHandler">
  4. <slot/>
  5. </view>
  6. <u-overlay v-if="showOverlay" :show="showPopover" :z-index="zIndex - 1" @click="showPopover = false"></u-overlay>
  7. <u-transition
  8. mode="fade"
  9. :duration="duration"
  10. :show="showPopover"
  11. :custom-style="transitionStyle"
  12. >
  13. <view class="u-popover__popup" :style="{
  14. minWidth:$u.addUnit(minWidth),
  15. maxWidth:$u.addUnit(maxWidth),
  16. width:popoverWidth,
  17. ...popoverStyle
  18. }">
  19. <view v-if="showArrow" class="u-popover__popup__indicator"
  20. :class="[`u-popover__popup__indicator__${actualPosition}`]"
  21. :style="{
  22. backgroundColor: (arrowColor || bgColor),
  23. width: $u.addUnit(arrowSize),
  24. height: $u.addUnit(arrowSize),
  25. }"
  26. ></view>
  27. <view
  28. class="u-popover__popup__content"
  29. :class="[`u-popover__popup__content__${actualPosition}`]"
  30. :style="{
  31. backgroundColor: bgColor,
  32. padding: $u.addUnit(padding),
  33. borderRadius: $u.addUnit(round)
  34. }"
  35. >
  36. <slot name="content">
  37. <text :style="{
  38. color: color,
  39. fontSize: $u.addUnit(fontSize)
  40. }">{{ content }}</text>
  41. </slot>
  42. </view>
  43. </view>
  44. </u-transition>
  45. </view>
  46. </template>
  47. <script>
  48. import props from './props.js';
  49. import mixin from '../../libs/mixin/mixin'
  50. import mpMixin from '../../libs/mixin/mpMixin';
  51. /**
  52. * Popover
  53. * @description
  54. * @tutorial https://uview.d3u.cn/components/popover.html
  55. * @property {Boolean} show 是否显示(默认 false )
  56. * @property {String} position 弹出方向:top, bottom, left, right, auto
  57. * @property {Boolean} showArrow 是否显示箭头(默认 true )
  58. * @property {String | Number} arrowSize 箭头大小(默认 12px )
  59. * @property {String} arrowColor 箭头颜色(默认 '#000' )
  60. * @property {String} bgColor 弹出层背景色(默认 '#060607' )
  61. * @property {String} color 文字颜色(默认 '#fff' )
  62. * @property {String | Number} fontSize 字体大小(默认 14px )
  63. * @property {String | Number} padding 内边距(默认 8px 12px )
  64. * @property {String | Number} round 圆角(默认 4 )
  65. * @property {String | Number} width 弹出层宽度(默认 '' )
  66. * @property {String | Number} maxWidth 弹出层最大宽度(默认 200 )
  67. * @property {String | Number} minWidth 弹出层最小宽度(默认 50 )
  68. * @property {String | Number} zIndex 层级(默认 999 )
  69. * @property {Number} duration 动画时长(默认 300 )
  70. * @property {Boolean} disabled 是否禁用(默认 false )
  71. * @property {Object} popoverStyle 自定义弹出层样式
  72. * @property {Boolean} overlay 是否显示遮罩层(默认 false )
  73. * @example
  74. * <u-popover position="top" :content="content">
  75. * <u-button type="primary">点击触发</u-button>
  76. * </u-popover>
  77. */
  78. export default {
  79. name: 'u-popover',
  80. mixins: [mpMixin, mixin, props],
  81. data() {
  82. return {
  83. showPopover: false,
  84. popoverWidth: '',
  85. autoPosition: '', // 自动计算的位置
  86. }
  87. },
  88. computed: {
  89. // 获取实际使用的position值
  90. actualPosition() {
  91. return this.position === 'auto' ? this.autoPosition : this.position
  92. },
  93. // 计算气泡和指示器的位置信息
  94. transitionStyle() {
  95. let style = {
  96. position: 'absolute',
  97. zIndex: this.zIndex
  98. }
  99. // 根据actualPosition设置位置
  100. switch(this.actualPosition) {
  101. case 'top-left':
  102. style.left = '0'
  103. style.transform = 'translate(0, -100%)'
  104. break
  105. case 'top':
  106. case 'top-center':
  107. style.left = '50%'
  108. style.transform = 'translate(-50%, -100%)'
  109. break
  110. case 'top-right':
  111. style.right = '0'
  112. style.transform = 'translate(0, -100%)'
  113. break
  114. case 'bottom-left':
  115. style.bottom = '0'
  116. style.left = '0'
  117. style.transform = 'translate(0, 100%)'
  118. break
  119. case 'bottom':
  120. case 'bottom-center':
  121. style.bottom = '0'
  122. style.left = '50%'
  123. style.transform = 'translate(-50%, 100%)'
  124. break
  125. case 'bottom-right':
  126. style.bottom = '0'
  127. style.right = '0'
  128. style.transform = 'translate(0, 100%)'
  129. break
  130. case 'left-top':
  131. style.transform = 'translate(-100%, 0)'
  132. break
  133. case 'left':
  134. case 'left-center':
  135. style.top = '50%'
  136. style.transform = 'translate(-100%, -50%)'
  137. break
  138. case 'left-bottom':
  139. style.bottom = '0'
  140. style.transform = 'translate(-100%, 0)'
  141. break
  142. case 'right-top':
  143. style.right = '0'
  144. style.transform = 'translate(100%, 0)'
  145. break
  146. case 'right':
  147. case 'right-center':
  148. style.right = '0'
  149. style.top = '50%'
  150. style.transform = 'translate(100%, -50%)'
  151. break
  152. case 'right-bottom':
  153. style.right = '0'
  154. style.bottom = '0'
  155. style.transform = 'translate(100%, 0)'
  156. break
  157. default:
  158. // 默认为top
  159. style.left = '50%'
  160. style.transform = 'translate(-50%, -100%)'
  161. break
  162. }
  163. return style
  164. }
  165. },
  166. mounted() {
  167. this.init()
  168. },
  169. // #ifdef VUE3
  170. emits: ["click"],
  171. // #endif
  172. methods: {
  173. init() {
  174. this.addClickOutsideListener()
  175. },
  176. // 元素尺寸
  177. getElRect() {
  178. const windowInfo = uni.$u.window();
  179. if(this.width) {
  180. this.popoverWidth = uni.$u.addUnit(this.width)
  181. } else {
  182. this.$uGetRect('#popover-trigger').then(size => {
  183. // 确保popover宽度不超出屏幕范围
  184. let targetWidth = size.width
  185. if(this.actualPosition.startsWith('left')) {
  186. targetWidth = size.left
  187. } else if(this.actualPosition.startsWith('right')) {
  188. targetWidth = windowInfo.windowWidth - size.right
  189. }
  190. targetWidth -= 10;
  191. // 如果position为auto,自动计算最佳位置
  192. if(this.position == 'auto') {
  193. this.autoPosition = this.calculateBestPosition(targetWidth,size, windowInfo)
  194. }
  195. this.popoverWidth = uni.$u.addUnit(targetWidth)
  196. })
  197. }
  198. },
  199. // 计算最佳显示位置
  200. calculateBestPosition(popoverWidth, triggerRect, windowInfo) {
  201. const popoverHeight = 80 // 预估popover高度
  202. const margin = 10 // 安全边距
  203. // 计算各个方向的可用空间
  204. const spaceTop = triggerRect.top
  205. const spaceBottom = windowInfo.windowHeight - triggerRect.bottom
  206. const spaceLeft = triggerRect.left
  207. const spaceRight = windowInfo.windowWidth - triggerRect.right
  208. // 优先级:top > bottom > right > left
  209. if (spaceTop >= popoverHeight + margin) {
  210. // 上方有足够空间
  211. if (triggerRect.left + popoverWidth <= windowInfo.windowWidth - margin) {
  212. return 'top-left'
  213. } else if (triggerRect.right - popoverWidth >= margin) {
  214. return 'top-right'
  215. } else {
  216. return 'top'
  217. }
  218. } else if (spaceBottom >= popoverHeight + margin) {
  219. // 下方有足够空间
  220. if (triggerRect.left + popoverWidth <= windowInfo.windowWidth - margin) {
  221. return 'bottom-left'
  222. } else if (triggerRect.right - popoverWidth >= margin) {
  223. return 'bottom-right'
  224. } else {
  225. return 'bottom'
  226. }
  227. } else if (spaceRight >= popoverWidth + margin) {
  228. // 右侧有足够空间
  229. if (triggerRect.top + popoverHeight <= windowInfo.windowHeight - margin) {
  230. return 'right-top'
  231. } else if (triggerRect.bottom - popoverHeight >= margin) {
  232. return 'right-bottom'
  233. } else {
  234. return 'right'
  235. }
  236. } else if (spaceLeft >= popoverWidth + margin) {
  237. // 左侧有足够空间
  238. if (triggerRect.top + popoverHeight <= windowInfo.windowHeight - margin) {
  239. return 'left-top'
  240. } else if (triggerRect.bottom - popoverHeight >= margin) {
  241. return 'left-bottom'
  242. } else {
  243. return 'left'
  244. }
  245. } else {
  246. // 所有方向空间都不够,选择空间最大的方向
  247. const maxSpace = Math.max(spaceTop, spaceBottom, spaceLeft, spaceRight)
  248. if (maxSpace === spaceTop) return 'top'
  249. if (maxSpace === spaceBottom) return 'bottom'
  250. if (maxSpace === spaceRight) return 'right'
  251. return 'left'
  252. }
  253. },
  254. clickHandler(e) {
  255. if(this.disabled) return
  256. this.getElRect()
  257. // #ifndef H5
  258. uni.$emit('u-popover-close')
  259. // #endif
  260. // 然后切换当前组件状态
  261. this.showPopover = !this.showPopover
  262. },
  263. // 添加全局点击监听
  264. addClickOutsideListener() {
  265. // #ifdef H5
  266. document.addEventListener('click', this.handleClickOutside, true)
  267. // #endif
  268. // #ifndef H5
  269. uni.$on('u-popover-close', () => {
  270. this.handleClickOutside()
  271. })
  272. // #endif
  273. },
  274. // 移除全局点击监听
  275. removeClickOutsideListener() {
  276. // #ifdef H5
  277. document.removeEventListener('click', this.handleClickOutside, true)
  278. // #endif
  279. // #ifndef H5
  280. uni.$off('u-popover-close')
  281. // #endif
  282. },
  283. handleClickOutside(e) {
  284. if (!this.showPopover) return
  285. // #ifdef H5
  286. // 检查点击的目标是否在popover组件内部
  287. if (e && this.$el && this.$el.contains(e.target)) {
  288. return
  289. }
  290. // #endif
  291. this.showPopover = false
  292. }
  293. },
  294. // #ifdef VUE2
  295. beforeDestroy() {
  296. this.removeClickOutsideListener()
  297. },
  298. // #endif
  299. // #ifdef VUE3
  300. beforeUnmount() {
  301. this.removeClickOutsideListener()
  302. }
  303. // #endif
  304. }
  305. </script>
  306. <style lang="scss" scoped>
  307. @import "../../libs/css/components.scss";
  308. .u-popover {
  309. position: relative;
  310. @include flex;
  311. &__popup {
  312. @include flex;
  313. justify-content: center;
  314. align-items: center;
  315. &__indicator {
  316. position: absolute;
  317. z-index: -1;
  318. border-radius: 2px;
  319. transform: rotate(45deg);
  320. box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
  321. &__top,
  322. &__top-center {
  323. bottom: 5px;
  324. }
  325. &__top-left {
  326. bottom: 5px;
  327. left: 15px;
  328. }
  329. &__top-right {
  330. bottom: 5px;
  331. right: 15px;
  332. }
  333. &__bottom-left {
  334. top: 5px;
  335. left: 15px;
  336. }
  337. &__bottom,
  338. &__bottom-center {
  339. top: 5px;
  340. }
  341. &__bottom-right {
  342. top: 5px;
  343. right: 15px;
  344. }
  345. &__left-top {
  346. right: 5px;
  347. top: 15px;
  348. }
  349. &__left,
  350. &__left-center {
  351. right: 5px;
  352. }
  353. &__left-bottom {
  354. right: 5px;
  355. bottom: 15px;
  356. }
  357. &__right-top {
  358. left: 5px;
  359. top: 15px;
  360. }
  361. &__right,
  362. &__right-center {
  363. left: 5px;
  364. }
  365. &__right-bottom {
  366. left: 5px;
  367. bottom: 15px;
  368. }
  369. }
  370. &__content {
  371. position: relative;
  372. flex: 1;
  373. overflow: hidden;
  374. box-shadow: 0 6px 30px 5px rgba(0, 0, 0, .05), 0 10px 10px 2px rgba(0, 0, 0, .04), 0 10px 10px -5px rgba(0, 0, 0, .08);
  375. &__top,
  376. &__top-left,
  377. &__top-center,
  378. &__top-right {
  379. margin-bottom: 10px;
  380. }
  381. &__bottom,
  382. &__bottom-left,
  383. &__bottom-center,
  384. &__bottom-right {
  385. margin-top: 10px;
  386. }
  387. &__left,
  388. &__left-top,
  389. &__left-center,
  390. &__left-bottom {
  391. margin-right: 10px;
  392. }
  393. &__right,
  394. &__right-top,
  395. &__right-center,
  396. &__right-bottom {
  397. margin-left: 10px;
  398. }
  399. }
  400. }
  401. }
  402. </style>