BaseFooter.vue 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. <template>
  2. <Teleport to="body">
  3. <div v-if="visible" class="footer-wrap">
  4. <nav class="footer" :class="{ isWhite }">
  5. <div
  6. v-for="item in tabs"
  7. :key="item.id"
  8. class="footer-item"
  9. :class="{ active: currentTab === item.id }"
  10. @click="refresh(item.id)"
  11. >
  12. <template v-if="!isRefreshing(item.id)">
  13. <div class="icon-placeholder">
  14. <img
  15. class="icon-img"
  16. :src="currentTab === item.id ? item.iconActive : item.icon"
  17. :alt="item.label"
  18. />
  19. </div>
  20. <span class="label" :class="{ active: currentTab === item.id }">{{ item.label }}</span>
  21. </template>
  22. </div>
  23. </nav>
  24. </div>
  25. </Teleport>
  26. </template>
  27. <script setup lang="ts">
  28. import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
  29. import { useRoute, useRouter } from 'vue-router'
  30. import bus, { EVENT_KEY } from '@/utils/bus'
  31. import tab1 from '@/assets/img/tabbar/tab-1.png'
  32. import tabAct1 from '@/assets/img/tabbar/tab-act-1.png'
  33. import tab2 from '@/assets/img/tabbar/tab-2.png'
  34. import tabAct2 from '@/assets/img/tabbar/tab-act-2.png'
  35. import tab3 from '@/assets/img/tabbar/tab-3.png'
  36. import tabAct3 from '@/assets/img/tabbar/tab-act-3.png'
  37. const props = defineProps({
  38. initTab: { type: Number, default: 1 },
  39. isWhite: { type: Boolean, default: false },
  40. })
  41. const router = useRouter()
  42. const route = useRoute()
  43. const currentTab = ref(props.initTab)
  44. /** douyin:bus 控制;扩展:仅 footerTab 路由 + 首页子层可隐藏 */
  45. const panelVisible = ref(true)
  46. const fullscreenHidden = ref(false)
  47. const hasFooterTab = computed(() => route.meta.footerTab != null)
  48. const visible = computed(
  49. () => hasFooterTab.value && panelVisible.value && !fullscreenHidden.value,
  50. )
  51. const tabs = [
  52. {
  53. id: 1,
  54. path: '/',
  55. label: '农场美景',
  56. icon: tab1,
  57. iconActive: tabAct1,
  58. },
  59. {
  60. id: 2,
  61. path: '/guard-map',
  62. label: '守护地图',
  63. icon: tab2,
  64. iconActive: tabAct2,
  65. },
  66. {
  67. id: 3,
  68. path: '/my-guard',
  69. label: '我的守护',
  70. icon: tab3,
  71. iconActive: tabAct3,
  72. },
  73. ]
  74. function syncTabFromRoute() {
  75. const tab = route.meta.footerTab as number | undefined
  76. if (tab) {
  77. currentTab.value = tab
  78. return
  79. }
  80. const item = tabs.find((t) => t.path === route.path)
  81. if (item) currentTab.value = item.id
  82. }
  83. const isRefresh1 = ref(false)
  84. const isRefresh2 = ref(false)
  85. const isRefresh3 = ref(false)
  86. function isRefreshing(index: number) {
  87. if (index === 1) return isRefresh1.value
  88. if (index === 2) return isRefresh2.value
  89. if (index === 3) return isRefresh3.value
  90. return false
  91. }
  92. function setRefreshing(index: number, val: boolean) {
  93. if (index === 1) isRefresh1.value = val
  94. else if (index === 2) isRefresh2.value = val
  95. else if (index === 3) isRefresh3.value = val
  96. }
  97. function tab(index: number) {
  98. const item = tabs.find((t) => t.id === index)
  99. if (!item) return
  100. currentTab.value = index
  101. if (route.path !== item.path) {
  102. router.push(item.path)
  103. }
  104. }
  105. /** 与 douyin-vue 一致:当前 Tab 再点进入 refresh 态,否则切换 Tab */
  106. function refresh(index: number) {
  107. if (currentTab.value === index) {
  108. setRefreshing(index, !isRefreshing(index))
  109. setTimeout(() => {
  110. setRefreshing(index, !isRefreshing(index))
  111. }, 2000)
  112. } else {
  113. tab(index)
  114. }
  115. }
  116. watch(
  117. () => route.path,
  118. () => {
  119. panelVisible.value = true
  120. fullscreenHidden.value = false
  121. syncTabFromRoute()
  122. },
  123. )
  124. onMounted(() => {
  125. syncTabFromRoute()
  126. bus.on('setFooterVisible', (e) => {
  127. panelVisible.value = e as boolean
  128. })
  129. bus.on(EVENT_KEY.ENTER_FULLSCREEN, () => {
  130. fullscreenHidden.value = true
  131. })
  132. bus.on(EVENT_KEY.EXIT_FULLSCREEN, () => {
  133. fullscreenHidden.value = false
  134. })
  135. })
  136. onUnmounted(() => {
  137. bus.off('setFooterVisible')
  138. bus.off(EVENT_KEY.ENTER_FULLSCREEN)
  139. bus.off(EVENT_KEY.EXIT_FULLSCREEN)
  140. })
  141. </script>
  142. <style scoped lang="less">
  143. .footer-wrap {
  144. position: fixed;
  145. left: 50%;
  146. bottom: var(--footer-bottom-offset, 30rem);
  147. z-index: 10;
  148. transform: translateX(-50%);
  149. width: calc(100vw - 32rem);
  150. max-width: 360rem;
  151. pointer-events: none;
  152. box-sizing: border-box;
  153. }
  154. .footer {
  155. pointer-events: auto;
  156. display: flex;
  157. align-items: center;
  158. justify-content: space-evenly;
  159. width: 100%;
  160. height: 56rem;
  161. padding: 8rem 12rem;
  162. box-sizing: border-box;
  163. background: rgba(0, 0, 0, 0.61);
  164. border: 1px solid rgba(255, 255, 255, 0.2);
  165. border-radius: 25rem;
  166. &.isWhite {
  167. background: #f5f5f5;
  168. border-color: rgba(0, 0, 0, 0.12);
  169. }
  170. }
  171. .footer-item {
  172. flex: 1;
  173. display: flex;
  174. flex-direction: column;
  175. align-items: center;
  176. justify-content: center;
  177. gap: 4rem;
  178. cursor: pointer;
  179. min-width: 0;
  180. text-align: center;
  181. }
  182. .icon-placeholder {
  183. width: 28rem;
  184. height: 28rem;
  185. display: flex;
  186. align-items: center;
  187. justify-content: center;
  188. flex-shrink: 0;
  189. }
  190. .icon-img {
  191. display: block;
  192. width: 100%;
  193. height: 100%;
  194. object-fit: contain;
  195. opacity: 0.4;
  196. }
  197. .footer-item.active .icon-img {
  198. opacity: 1;
  199. }
  200. .label {
  201. width: 100%;
  202. font-size: 11rem;
  203. line-height: 1.2;
  204. text-align: center;
  205. color: rgba(255, 255, 255, 0.35);
  206. white-space: nowrap;
  207. &.active {
  208. color: #ffb400;
  209. font-weight: 500;
  210. }
  211. }
  212. .footer.isWhite .label {
  213. color: rgba(0, 0, 0, 0.35);
  214. &.active {
  215. color: #e6a000;
  216. }
  217. }
  218. </style>