wd-sticky.vue 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. <template>
  2. <view :style="`${rootStyle};display: inline-block;`">
  3. <view :class="`wd-sticky ${customClass}`" :style="stickyStyle" :id="styckyId">
  4. <view class="wd-sticky__container" :style="containerStyle">
  5. <wd-resize @resize="handleResize" custom-style="display: inline-block;">
  6. <slot />
  7. </wd-resize>
  8. </view>
  9. </view>
  10. </view>
  11. </template>
  12. <script lang="ts">
  13. export default {
  14. name: 'wd-sticky',
  15. options: {
  16. addGlobalClass: true,
  17. virtualHost: true,
  18. styleIsolation: 'shared'
  19. }
  20. }
  21. </script>
  22. <script lang="ts" setup>
  23. import wdResize from '../wd-resize/wd-resize.vue'
  24. import { computed, getCurrentInstance, reactive, ref, type CSSProperties } from 'vue'
  25. import { addUnit, getRect, objToStyle, pause, uuid } from '../common/util'
  26. import { stickyProps } from './types'
  27. import { useParent } from '../composables/useParent'
  28. import { STICKY_BOX_KEY } from '../wd-sticky-box/types'
  29. const props = defineProps(stickyProps)
  30. const styckyId = ref<string>(`wd-sticky${uuid()}`)
  31. const observerList = ref<UniApp.IntersectionObserver[]>([])
  32. const stickyState = reactive({
  33. position: 'absolute',
  34. boxLeaved: false,
  35. top: 0,
  36. height: 0,
  37. width: 0,
  38. state: ''
  39. })
  40. const { parent: stickyBox } = useParent(STICKY_BOX_KEY)
  41. const { proxy } = getCurrentInstance() as any
  42. const rootStyle = computed(() => {
  43. const style: CSSProperties = {
  44. 'z-index': props.zIndex,
  45. height: addUnit(stickyState.height),
  46. width: addUnit(stickyState.width)
  47. }
  48. if (!stickyState.boxLeaved) {
  49. style['position'] = 'relative'
  50. }
  51. return `${objToStyle(style)}${props.customStyle}`
  52. })
  53. const stickyStyle = computed(() => {
  54. const style: CSSProperties = {
  55. 'z-index': props.zIndex,
  56. height: addUnit(stickyState.height),
  57. width: addUnit(stickyState.width)
  58. }
  59. if (!stickyState.boxLeaved) {
  60. style['position'] = 'relative'
  61. }
  62. return `${objToStyle(style)}`
  63. })
  64. const containerStyle = computed(() => {
  65. const style: CSSProperties = {
  66. position: stickyState.position as 'static' | 'relative' | 'absolute' | 'sticky' | 'fixed',
  67. top: addUnit(stickyState.top)
  68. }
  69. return objToStyle(style)
  70. })
  71. const innerOffsetTop = computed(() => {
  72. let top: number = 0
  73. // #ifdef H5
  74. // H5端,导航栏为普通元素,需要将组件移动到导航栏的下边沿
  75. // H5的导航栏高度为44px
  76. top = 44
  77. // #endif
  78. return top + props.offsetTop
  79. })
  80. /**
  81. * 清除对当前组件的监听
  82. */
  83. function clearObserver() {
  84. while (observerList.value.length !== 0) {
  85. observerList.value.pop()!.disconnect()
  86. }
  87. }
  88. /**
  89. * 添加对当前组件的监听
  90. */
  91. function createObserver() {
  92. const observer = uni.createIntersectionObserver(proxy, { thresholds: [0, 0.5] })
  93. observerList.value.push(observer)
  94. return observer
  95. }
  96. /**
  97. * 当前内容高度发生变化时重置监听
  98. */
  99. async function handleResize(detail: any) {
  100. stickyState.width = detail.width
  101. stickyState.height = detail.height
  102. await pause()
  103. observerContentScroll()
  104. if (!stickyBox || !stickyBox.observerForChild) return
  105. stickyBox.observerForChild(proxy)
  106. }
  107. /**
  108. * 监听吸顶元素滚动事件
  109. */
  110. function observerContentScroll() {
  111. if (stickyState.height === 0 && stickyState.width === 0) return
  112. const offset = innerOffsetTop.value + stickyState.height
  113. clearObserver()
  114. createObserver()
  115. .relativeToViewport({
  116. top: -offset
  117. })
  118. .observe(`#${styckyId.value}`, (result) => {
  119. handleRelativeTo(result)
  120. })
  121. getRect(`#${styckyId.value}`, false, proxy).then((res) => {
  122. // #ifdef H5
  123. // H5端,查询节点信息未计算导航栏高度
  124. res.bottom = Number(res.bottom) + 44
  125. // #endif
  126. if (Number(res.bottom) <= offset) handleRelativeTo({ boundingClientRect: res })
  127. })
  128. }
  129. /**
  130. * @description 根据位置进行吸顶
  131. */
  132. function handleRelativeTo({ boundingClientRect }: any) {
  133. // sticky 高度大于或等于 wd-sticky-box,使用 wd-sticky-box 无任何意义
  134. if (stickyBox && stickyBox.boxStyle && stickyState.height >= stickyBox.boxStyle.height) {
  135. stickyState.position = 'absolute'
  136. stickyState.top = 0
  137. return
  138. }
  139. let isStycky = boundingClientRect.top <= innerOffsetTop.value
  140. // #ifdef H5 || APP-PLUS
  141. isStycky = boundingClientRect.top < innerOffsetTop.value
  142. // #endif
  143. if (isStycky) {
  144. stickyState.state = 'sticky'
  145. stickyState.boxLeaved = false
  146. stickyState.position = 'fixed'
  147. stickyState.top = innerOffsetTop.value
  148. } else {
  149. stickyState.state = 'normal'
  150. stickyState.boxLeaved = false
  151. stickyState.position = 'absolute'
  152. stickyState.top = 0
  153. }
  154. }
  155. /**
  156. * 设置位置
  157. * @param setboxLeaved
  158. * @param setPosition
  159. * @param setTop
  160. */
  161. function setPosition(boxLeaved: boolean, position: string, top: number) {
  162. stickyState.boxLeaved = boxLeaved
  163. stickyState.position = position
  164. stickyState.top = top
  165. }
  166. defineExpose({
  167. setPosition,
  168. stickyState,
  169. offsetTop: props.offsetTop
  170. })
  171. </script>
  172. <style lang="scss" scoped>
  173. @import './index.scss';
  174. </style>