cl-sticky.uvue 3.1 KB

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