index.vue 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. <script setup lang="ts">
  2. import { isEmpty } from 'lodash-es'
  3. import { computed, nextTick, onMounted, ref, watch } from 'vue'
  4. import { RouterView } from 'vue-router'
  5. import { useInjection } from '@/composables'
  6. import { mediaQueryInjectionKey, layoutInjectionKey } from '@/injection'
  7. import router from '@/router'
  8. import { usePreferencesStore, useTabsStore } from '@/stores'
  9. import type { Tab } from '@/stores'
  10. import type { RouteLocationNormalizedLoaded } from 'vue-router'
  11. defineOptions({
  12. name: 'MainLayout',
  13. })
  14. const { isSmallScreen } = useInjection(mediaQueryInjectionKey)
  15. const { shouldRefreshRoute, layoutSlideDirection, setLayoutSlideDirection } =
  16. useInjection(layoutInjectionKey)
  17. const tabsStore = useTabsStore()
  18. const preferencesStore = usePreferencesStore()
  19. const { createTab, setTabActivePath } = tabsStore
  20. const isMounted = ref(false)
  21. const navigationTransitionName = ref('scale')
  22. const layoutRouteRedirect = router.getRoutes().find((item) => item.name === 'layout')?.redirect
  23. const keepAliveTabs = computed(() => {
  24. return tabsStore.tabs.filter((tab) => tab.keepAlive).map((tab) => tab.componentName ?? '')
  25. })
  26. let oldTabs: Tab[] = []
  27. function createTabFromRoute(route: RouteLocationNormalizedLoaded) {
  28. const {
  29. icon = 'iconify ph--browser',
  30. title: label = '未命名标签',
  31. renderTabTitle,
  32. componentName,
  33. pinned,
  34. } = route.meta
  35. const { fullPath, name, params } = route
  36. const title = renderTabTitle ? renderTabTitle(params) : label
  37. createTab({
  38. path: fullPath,
  39. icon,
  40. title,
  41. name,
  42. componentName,
  43. pinned,
  44. })
  45. }
  46. watch(
  47. [() => tabsStore.tabs, () => tabsStore.tabActivePath],
  48. ([newTabs, newTabActivePath], [, oldTabActivePath]) => {
  49. if (
  50. newTabActivePath &&
  51. newTabActivePath !== oldTabActivePath &&
  52. newTabActivePath !== router.currentRoute.value.fullPath
  53. ) {
  54. if (
  55. layoutRouteRedirect &&
  56. layoutRouteRedirect === router.currentRoute.value.path &&
  57. newTabActivePath === '/'
  58. ) {
  59. router.go(0)
  60. } else {
  61. router.push(newTabActivePath)
  62. }
  63. }
  64. if (isSmallScreen.value) {
  65. navigationTransitionName.value = ''
  66. return
  67. }
  68. if (!preferencesStore.preferences.enableNavigationTransition) return
  69. if (!preferencesStore.preferences.showTabs) {
  70. navigationTransitionName.value = 'scale'
  71. return
  72. }
  73. const oldActiveIndex = oldTabs.findIndex((item) => item.path === oldTabActivePath)
  74. const newActiveIndex = newTabs.findIndex((item) => item.path === newTabActivePath)
  75. if (oldTabs.length < newTabs.length || oldActiveIndex === -1 || newActiveIndex === -1) {
  76. navigationTransitionName.value = 'scale'
  77. } else if (oldTabs.length > newTabs.length) {
  78. navigationTransitionName.value =
  79. oldActiveIndex > newActiveIndex ? 'scale-slider-left' : 'scale-slider-right'
  80. } else if (oldTabActivePath !== newTabActivePath) {
  81. navigationTransitionName.value =
  82. oldActiveIndex > newActiveIndex ? 'slider-left' : 'slider-right'
  83. }
  84. oldTabs = [...newTabs]
  85. },
  86. )
  87. watch(
  88. () => shouldRefreshRoute.value,
  89. (shouldRefresh) => {
  90. if (shouldRefresh) {
  91. navigationTransitionName.value = 'shake'
  92. nextTick(() => {
  93. shouldRefreshRoute.value = false
  94. })
  95. }
  96. },
  97. )
  98. watch(
  99. () => router.currentRoute.value,
  100. (newRoute, oldRoute) => {
  101. if (layoutSlideDirection.value) {
  102. setLayoutSlideDirection(null)
  103. }
  104. if (newRoute.fullPath !== oldRoute?.fullPath) {
  105. const { showTab, enableMultiTab } = newRoute.meta
  106. const targetPath = enableMultiTab ? newRoute.fullPath : newRoute.path
  107. const findTab = tabsStore.tabs.find((item) => item.path === targetPath)
  108. if (!isEmpty(findTab)) {
  109. setTabActivePath(findTab.path)
  110. } else if (showTab) {
  111. createTabFromRoute(newRoute)
  112. } else {
  113. setTabActivePath('')
  114. }
  115. }
  116. },
  117. {
  118. immediate: true,
  119. },
  120. )
  121. onMounted(() => {
  122. oldTabs = [...tabsStore.tabs]
  123. isMounted.value = true
  124. })
  125. </script>
  126. <template>
  127. <RouterView
  128. v-if="preferencesStore.preferences.enableNavigationTransition"
  129. v-slot="{ Component, route }"
  130. >
  131. <Transition
  132. :type="navigationTransitionName === 'shake' ? 'animation' : 'transition'"
  133. :name="navigationTransitionName"
  134. >
  135. <KeepAlive :include="keepAliveTabs">
  136. <component
  137. :is="Component"
  138. v-if="isMounted && !shouldRefreshRoute"
  139. :key="route.path + JSON.stringify(route.query)"
  140. />
  141. </KeepAlive>
  142. </Transition>
  143. </RouterView>
  144. <RouterView v-else />
  145. </template>
  146. <style scoped>
  147. .slider-left-enter-active,
  148. .slider-right-enter-active,
  149. .slider-left-leave-active,
  150. .slider-right-leave-active {
  151. position: absolute;
  152. width: 100%;
  153. transition: translate 500ms var(--cubic-bezier-ease-in-out) 75ms;
  154. will-change: translate;
  155. }
  156. .slider-left-enter-from,
  157. .slider-right-leave-to {
  158. translate: -100%;
  159. }
  160. .slider-left-leave-to,
  161. .slider-right-enter-from {
  162. translate: 100%;
  163. }
  164. .slider-left-enter-to,
  165. .slider-left-leave-from,
  166. .slider-right-enter-to,
  167. .slider-right-leave-from {
  168. translate: 0;
  169. }
  170. .scale-slider-left-enter-active,
  171. .scale-slider-left-leave-active,
  172. .scale-slider-right-enter-active,
  173. .scale-slider-right-leave-active {
  174. position: absolute;
  175. width: 100%;
  176. transition:
  177. scale 500ms var(--cubic-bezier-ease-in-out) 75ms,
  178. translate 500ms var(--cubic-bezier-ease-in-out) 75ms,
  179. opacity 300ms var(--cubic-bezier-ease-in-out) 75ms;
  180. will-change: scale, translate, opacity;
  181. }
  182. .scale-slider-left-leave-to,
  183. .scale-slider-right-leave-to {
  184. scale: 0.5;
  185. opacity: 0;
  186. }
  187. .scale-slider-left-enter-from {
  188. translate: -100%;
  189. }
  190. .scale-slider-right-enter-from {
  191. translate: 100%;
  192. }
  193. .scale-enter-active,
  194. .scale-leave-active {
  195. position: absolute;
  196. width: 100%;
  197. transition:
  198. scale 300ms var(--cubic-bezier-ease-in-out) 75ms,
  199. opacity 300ms var(--cubic-bezier-ease-in-out) 75ms;
  200. will-change: scale, opacity;
  201. }
  202. .scale-enter-from,
  203. .scale-leave-to {
  204. scale: 0.5;
  205. opacity: 0;
  206. }
  207. .shake-enter-active {
  208. animation: shake var(--cubic-bezier-ease-in-out) 500ms;
  209. }
  210. @keyframes shake {
  211. 0% {
  212. translate: 0 0;
  213. rotate: 0deg;
  214. }
  215. 8% {
  216. translate: -2px -1px;
  217. rotate: -0.6deg;
  218. }
  219. 16% {
  220. translate: 1px -2px;
  221. rotate: 0.9deg;
  222. }
  223. 24% {
  224. translate: -1px 2px;
  225. rotate: -0.5deg;
  226. }
  227. 32% {
  228. translate: 2px -1px;
  229. rotate: 0.7deg;
  230. }
  231. 40% {
  232. translate: -1px -2px;
  233. rotate: -0.8deg;
  234. }
  235. 48% {
  236. translate: 1px 2px;
  237. rotate: 0.4deg;
  238. }
  239. 56% {
  240. translate: -2px 1px;
  241. rotate: -0.7deg;
  242. }
  243. 64% {
  244. translate: 2px -1px;
  245. rotate: 0.8deg;
  246. }
  247. 72% {
  248. translate: -1px -1px;
  249. rotate: -0.5deg;
  250. }
  251. 80% {
  252. translate: 2px 1px;
  253. rotate: 0.6deg;
  254. }
  255. 88% {
  256. translate: -1px 1px;
  257. rotate: -0.4deg;
  258. }
  259. 96% {
  260. translate: 1px -1px;
  261. rotate: 0.3deg;
  262. }
  263. 100% {
  264. translate: 0 0;
  265. rotate: 0deg;
  266. }
  267. }
  268. </style>