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