IndicatorHome.vue 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. <template>
  2. <div class="indicator-home">
  3. <div class="notice" :style="noticeStyle"><span>下拉刷新内容</span></div>
  4. <div ref="toolbar" class="toolbar" :style="toolbarStyle">
  5. <Icon
  6. icon="tabler:menu-deep"
  7. class="search"
  8. style="transform: rotateY(180deg)"
  9. @click="$emit('showSlidebar')"
  10. />
  11. <div ref="tabCtn" class="tab-ctn">
  12. <div ref="tabs" class="tabs">
  13. <div
  14. v-for="(label, i) in tabLabels"
  15. :key="label"
  16. class="tab"
  17. :class="{ active: index === i }"
  18. @click.stop="change(i)"
  19. >
  20. <span>{{ label }}</span>
  21. </div>
  22. </div>
  23. <div ref="indicator" class="indicator" />
  24. </div>
  25. <Icon icon="ion:search" class="search" @click="_no" />
  26. </div>
  27. </div>
  28. </template>
  29. <script setup lang="ts">
  30. import { Icon } from '@iconify/vue'
  31. import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
  32. import bus from '@/utils/bus'
  33. import { _css } from '@/utils/dom'
  34. import { _no } from '@/utils/index'
  35. const props = defineProps({
  36. loading: { type: Boolean, default: false },
  37. name: { type: String, default: '' },
  38. index: { type: Number, default: 0 },
  39. })
  40. const emit = defineEmits<{ 'update:index': [number]; showSlidebar: [] }>()
  41. const tabLabels = ['我的', '推荐']
  42. const tabCtn = ref<HTMLElement | null>(null)
  43. const tabs = ref<HTMLElement | null>(null)
  44. const indicator = ref<HTMLElement | null>(null)
  45. const lefts = ref<number[]>([])
  46. const indicatorSpace = ref(0)
  47. const moveY = ref(0)
  48. const judgeValue = 20
  49. const homeRefresh = 60
  50. const transform = computed(
  51. () =>
  52. `translate3d(0, ${moveY.value - judgeValue > homeRefresh ? homeRefresh : moveY.value - judgeValue}px, 0)`,
  53. )
  54. const toolbarStyle = computed(() => {
  55. if (props.loading) {
  56. return { opacity: 1, transitionDuration: '300ms', transform: 'translate3d(0, 0, 0)' }
  57. }
  58. if (moveY.value) {
  59. return {
  60. opacity: 1 - (moveY.value - judgeValue) / (homeRefresh / 2),
  61. transform: transform.value,
  62. }
  63. }
  64. return { opacity: 1, transitionDuration: '300ms', transform: 'translate3d(0, 0, 0)' }
  65. })
  66. const noticeStyle = computed(() => {
  67. if (props.loading) return { opacity: 0 }
  68. if (moveY.value) {
  69. return {
  70. opacity: (moveY.value - judgeValue) / (homeRefresh / 2) - 0.5,
  71. transform: transform.value,
  72. }
  73. }
  74. return { opacity: 0 }
  75. })
  76. function updateIndicatorLeft(index: number, animate = true) {
  77. if (!indicator.value || !lefts.value.length) return
  78. const safeIndex = Math.max(0, Math.min(index, lefts.value.length - 1))
  79. _css(indicator.value, 'transition-duration', animate ? '300ms' : '0ms')
  80. _css(indicator.value, 'left', lefts.value[safeIndex] + 'px')
  81. }
  82. function change(index: number) {
  83. emit('update:index', index)
  84. updateIndicatorLeft(index)
  85. }
  86. function initTabs() {
  87. if (!tabs.value || !indicator.value || !tabCtn.value) return
  88. const indicatorWidth = _css(indicator.value, 'width') as number
  89. const tabCtnLeft = tabCtn.value.getBoundingClientRect().left
  90. const positions: number[] = []
  91. for (let i = 0; i < tabs.value.children.length; i++) {
  92. const item = tabs.value.children[i] as HTMLElement
  93. const rect = item.getBoundingClientRect()
  94. positions.push(rect.left - tabCtnLeft + (rect.width * 0.5 - indicatorWidth / 2))
  95. }
  96. lefts.value = positions
  97. indicatorSpace.value = positions.length > 1 ? positions[1] - positions[0] : 0
  98. updateIndicatorLeft(props.index, false)
  99. }
  100. function move(val?: unknown) {
  101. const e = val as number
  102. if (!indicator.value || !indicatorSpace.value) return
  103. const safeIndex = Math.max(0, Math.min(props.index, lefts.value.length - 1))
  104. _css(indicator.value, 'transition-duration', '0ms')
  105. _css(
  106. indicator.value,
  107. 'left',
  108. lefts.value[safeIndex] - e / (window.innerWidth / indicatorSpace.value) + 'px',
  109. )
  110. }
  111. function end(val?: unknown) {
  112. const index = val as number
  113. moveY.value = 0
  114. updateIndicatorLeft(index)
  115. setTimeout(() => {
  116. if (indicator.value) _css(indicator.value, 'transition-duration', '0ms')
  117. }, 300)
  118. }
  119. watch(
  120. () => props.index,
  121. (index) => {
  122. nextTick(() => updateIndicatorLeft(index))
  123. },
  124. )
  125. onMounted(() => {
  126. nextTick(() => {
  127. initTabs()
  128. requestAnimationFrame(initTabs)
  129. })
  130. bus.on(props.name + '-moveX', move)
  131. bus.on(props.name + '-moveY', (e) => {
  132. moveY.value = e as number
  133. })
  134. bus.on(props.name + '-end', end)
  135. })
  136. onUnmounted(() => {
  137. bus.off(props.name + '-moveX', move)
  138. bus.off(props.name + '-moveY')
  139. bus.off(props.name + '-end', end)
  140. })
  141. </script>
  142. <style scoped lang="less">
  143. .indicator-home {
  144. position: absolute;
  145. font-size: 16rem;
  146. top: 0;
  147. left: 0;
  148. z-index: 2;
  149. width: 100%;
  150. color: white;
  151. height: var(--home-header-height);
  152. font-weight: bold;
  153. .notice {
  154. opacity: 0;
  155. top: 0;
  156. position: absolute;
  157. width: 100vw;
  158. height: 100%;
  159. display: flex;
  160. justify-content: center;
  161. align-items: center;
  162. }
  163. .toolbar {
  164. z-index: 2;
  165. position: relative;
  166. width: 100%;
  167. height: 100%;
  168. box-sizing: border-box;
  169. padding: 0 15rem;
  170. display: flex;
  171. justify-content: center;
  172. align-items: center;
  173. .tab-ctn {
  174. position: relative;
  175. display: flex;
  176. justify-content: center;
  177. .tabs {
  178. display: flex;
  179. justify-content: center;
  180. align-items: center;
  181. gap: 40rem;
  182. .tab {
  183. transition: color 0.3s;
  184. color: rgba(255, 255, 255, 0.7);
  185. font-size: 17rem;
  186. cursor: pointer;
  187. white-space: nowrap;
  188. &.active {
  189. color: white;
  190. }
  191. }
  192. }
  193. .indicator {
  194. position: absolute;
  195. bottom: -6rem;
  196. height: 2.6rem;
  197. width: 26rem;
  198. background: #fff;
  199. border-radius: 5rem;
  200. }
  201. }
  202. .search {
  203. position: absolute;
  204. top: 0;
  205. bottom: 0;
  206. margin: auto 0;
  207. height: 24rem;
  208. display: flex;
  209. align-items: center;
  210. color: white;
  211. font-size: 24rem;
  212. cursor: pointer;
  213. &:first-of-type {
  214. left: 15rem;
  215. }
  216. &:last-of-type {
  217. right: 15rem;
  218. }
  219. }
  220. }
  221. }
  222. </style>