cl-sticky.uvue 2.9 KB

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