cl-sticky.uvue 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. <template>
  2. <view
  3. class="cl-sticky-wrapper"
  4. :style="{
  5. height: rect.height == 0 ? 'auto' : rect.height + 'px',
  6. zIndex
  7. }"
  8. >
  9. <view
  10. class="cl-sticky"
  11. :class="[
  12. {
  13. 'is-active': isSticky
  14. }
  15. ]"
  16. :style="{
  17. width: isSticky ? rect.width + 'px' : '100%',
  18. left: isSticky ? rect.left + 'px' : 0,
  19. top: stickyTop + 'px'
  20. }"
  21. >
  22. <slot></slot>
  23. <slot name="content" :is-sticky="isSticky"></slot>
  24. </view>
  25. </view>
  26. </template>
  27. <script lang="ts" setup>
  28. import { isEmpty, router, usePage } from "@/cool";
  29. import { computed, getCurrentInstance, nextTick, onMounted, reactive, ref, watch } from "vue";
  30. defineOptions({
  31. name: "cl-sticky"
  32. });
  33. defineSlots<{
  34. default(): any;
  35. content(props: { isSticky: boolean }): any;
  36. }>();
  37. const props = defineProps({
  38. offsetTop: {
  39. type: Number,
  40. default: 0
  41. },
  42. zIndex: {
  43. type: Number,
  44. default: 100
  45. },
  46. scrollTop: {
  47. type: Number,
  48. default: 0
  49. }
  50. });
  51. const page = usePage();
  52. const { proxy } = getCurrentInstance()!;
  53. // 定义Rect类型,表示元素的位置信息
  54. type Rect = {
  55. height: number; // 高度
  56. width: number; // 宽度
  57. left: number; // 距离页面左侧的距离
  58. top: number; // 距离页面顶部的距离
  59. };
  60. // rect为响应式对象,存储当前sticky元素的位置信息
  61. const rect = reactive<Rect>({
  62. height: 0,
  63. width: 0,
  64. left: 0,
  65. top: 0
  66. });
  67. // scrollTop为当前页面滚动的距离
  68. const scrollTop = ref(0);
  69. // 计算属性,判断当前是否处于吸顶状态
  70. const isSticky = computed(() => {
  71. if (rect.height == 0) return false;
  72. return scrollTop.value >= rect.top;
  73. });
  74. // 计算属性,返回sticky元素的top值(吸顶时的偏移量)
  75. const stickyTop = computed(() => {
  76. if (isSticky.value) {
  77. let v = 0;
  78. // #ifdef H5
  79. // H5端默认导航栏高度为44
  80. if (!router.isCustomNavbarPage()) {
  81. v = 44;
  82. }
  83. // #endif
  84. return v + props.offsetTop;
  85. } else {
  86. return 0;
  87. }
  88. });
  89. // 获取sticky元素的位置信息,并更新rect
  90. function getRect() {
  91. nextTick(() => {
  92. uni.createSelectorQuery()
  93. .in(proxy)
  94. .select(".cl-sticky")
  95. .boundingClientRect()
  96. .exec((nodes) => {
  97. if (isEmpty(nodes)) {
  98. return;
  99. }
  100. const node = nodes[0] as NodeInfo;
  101. // 赋值时做空值处理,保证类型安全
  102. rect.height = node.height ?? 0;
  103. rect.width = node.width ?? 0;
  104. rect.left = node.left ?? 0;
  105. // top需要减去offsetTop并加上当前滚动距离,保证吸顶准确
  106. rect.top = (node.top ?? 0) - props.offsetTop + scrollTop.value;
  107. });
  108. });
  109. }
  110. // 监听页面滚动事件
  111. page.onPageScroll((top) => {
  112. scrollTop.value = top;
  113. });
  114. onMounted(() => {
  115. getRect();
  116. watch(
  117. computed(() => props.scrollTop),
  118. (top: number) => {
  119. scrollTop.value = top;
  120. },
  121. {
  122. immediate: true
  123. }
  124. );
  125. });
  126. defineExpose({
  127. getRect
  128. });
  129. </script>
  130. <style lang="scss" scoped>
  131. .cl-sticky {
  132. @apply relative;
  133. &.is-active {
  134. @apply fixed;
  135. }
  136. }
  137. </style>