123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309 |
- <script setup lang="ts">
- import { isEmpty } from 'lodash-es'
- import { computed, nextTick, onMounted, ref, watch } from 'vue'
- import { RouterView } from 'vue-router'
- import { useInjection } from '@/composable/useInjection'
- import { mediaQueryInjectionKey, layoutInjectionKey } from '@/injection'
- import router from '@/router'
- import { usePreferencesStore } from '@/stores'
- import { useTabsStore } from '@/stores'
- import type { Tab } from '@/stores'
- import type { RouteLocationNormalizedLoaded } from 'vue-router'
- defineOptions({
- name: 'MainLayout',
- })
- const { isSmallScreen } = useInjection(mediaQueryInjectionKey)
- const { shouldRefreshRoute, layoutSlideDirection, setLayoutSlideDirection } =
- useInjection(layoutInjectionKey)
- const layoutsRouteRedirect = router.getRoutes().find((item) => item.name === 'layouts')?.redirect
- const tabsStore = useTabsStore()
- const preferencesStore = usePreferencesStore()
- const { createTab, setTabActivePath } = tabsStore
- const navigationTransitionName = ref('scale')
- const isMounted = ref(false)
- const keepAliveTabs = computed(() => {
- return tabsStore.tabs.filter((tab) => tab.keepAlive).map((tab) => tab.componentName ?? '')
- })
- let oldTabs: Tab[] = []
- function createTabFromRoute(route: RouteLocationNormalizedLoaded) {
- const {
- icon = 'iconify ph--browser',
- title: label = '未命名标签',
- renderTabTitle,
- componentName,
- pinned,
- } = route.meta
- const { fullPath, name, params } = route
- const title = renderTabTitle ? renderTabTitle(params) : label
- createTab({
- path: fullPath,
- icon,
- title,
- name,
- componentName,
- pinned,
- })
- }
- watch(
- (): [Tab[], string] => [tabsStore.tabs, tabsStore.tabActivePath],
- ([newTabs, newTabActivePath], [, oldTabActivePath]) => {
- if (
- newTabActivePath &&
- newTabActivePath !== oldTabActivePath &&
- newTabActivePath !== router.currentRoute.value.fullPath
- ) {
- if (
- layoutsRouteRedirect &&
- layoutsRouteRedirect === router.currentRoute.value.path &&
- newTabActivePath === '/'
- ) {
- router.go(0)
- } else {
- router.push(newTabActivePath)
- }
- }
- if (!preferencesStore.preferences.enableNavigationTransition) return
- if (isSmallScreen.value) {
- navigationTransitionName.value = ''
- return
- }
- if (!preferencesStore.preferences.showTabs) {
- navigationTransitionName.value = 'scale'
- return
- }
- const oldActiveIndex = oldTabs.findIndex((item) => item.path === oldTabActivePath)
- const newActiveIndex = newTabs.findIndex((item) => item.path === newTabActivePath)
- if (oldTabs.length < newTabs.length || oldActiveIndex === -1 || newActiveIndex === -1) {
- navigationTransitionName.value = 'scale'
- } else if (oldTabs.length > newTabs.length) {
- navigationTransitionName.value =
- oldActiveIndex > newActiveIndex ? 'scale-slider-left' : 'scale-slider-right'
- } else if (oldTabActivePath !== newTabActivePath) {
- navigationTransitionName.value =
- oldActiveIndex > newActiveIndex ? 'slider-left' : 'slider-right'
- }
- oldTabs = [...newTabs]
- },
- )
- watch(
- () => shouldRefreshRoute.value,
- (shouldRefresh) => {
- if (shouldRefresh) {
- navigationTransitionName.value = 'shake'
- nextTick(() => {
- shouldRefreshRoute.value = false
- })
- }
- },
- )
- watch(
- () => router.currentRoute.value,
- (newRoute, oldRoute) => {
- if (layoutSlideDirection.value) {
- setLayoutSlideDirection(null)
- }
- if (newRoute.fullPath !== oldRoute?.fullPath) {
- const { showTab, enableMultiTab } = newRoute.meta
- const targetPath = enableMultiTab ? newRoute.fullPath : newRoute.path
- const findTab = tabsStore.tabs.find((item) => item.path === targetPath)
- if (!isEmpty(findTab)) {
- setTabActivePath(findTab.path)
- } else if (showTab) {
- createTabFromRoute(newRoute)
- } else {
- setTabActivePath('')
- }
- }
- },
- {
- immediate: true,
- },
- )
- onMounted(() => {
- oldTabs = [...tabsStore.tabs]
- isMounted.value = true
- })
- </script>
- <template>
- <RouterView
- v-if="preferencesStore.preferences.enableNavigationTransition"
- v-slot="{ Component, route }"
- >
- <Transition
- :type="navigationTransitionName === 'shake' ? 'animation' : 'transition'"
- :name="navigationTransitionName"
- >
- <KeepAlive :include="keepAliveTabs">
- <component
- :is="Component"
- v-if="isMounted && !shouldRefreshRoute"
- :key="route.path + JSON.stringify(route.query)"
- />
- </KeepAlive>
- </Transition>
- </RouterView>
- <RouterView v-else />
- </template>
- <style scoped>
- .slider-left-enter-active,
- .slider-right-enter-active,
- .slider-left-leave-active,
- .slider-right-leave-active {
- position: absolute;
- width: 100%;
- transition: translate 500ms var(--cubic-bezier-ease-in-out) 75ms;
- will-change: translate;
- }
- .slider-left-enter-from,
- .slider-right-leave-to {
- translate: -100%;
- }
- .slider-left-leave-to,
- .slider-right-enter-from {
- translate: 100%;
- }
- .slider-left-enter-to,
- .slider-left-leave-from,
- .slider-right-enter-to,
- .slider-right-leave-from {
- translate: 0;
- }
- .scale-slider-left-enter-active,
- .scale-slider-left-leave-active,
- .scale-slider-right-enter-active,
- .scale-slider-right-leave-active {
- position: absolute;
- width: 100%;
- transition:
- scale 500ms var(--cubic-bezier-ease-in-out) 75ms,
- translate 500ms var(--cubic-bezier-ease-in-out) 75ms,
- opacity 300ms var(--cubic-bezier-ease-in-out) 75ms;
- will-change: scale, translate, opacity;
- }
- .scale-slider-left-leave-to,
- .scale-slider-right-leave-to {
- scale: 0.5;
- opacity: 0;
- }
- .scale-slider-left-enter-from {
- translate: -100%;
- }
- .scale-slider-right-enter-from {
- translate: 100%;
- }
- .scale-enter-active,
- .scale-leave-active {
- position: absolute;
- width: 100%;
- transition:
- scale 300ms var(--cubic-bezier-ease-in-out) 75ms,
- opacity 300ms var(--cubic-bezier-ease-in-out) 75ms;
- will-change: scale, opacity;
- }
- .scale-enter-from,
- .scale-leave-to {
- scale: 0.5;
- opacity: 0;
- }
- .shake-enter-active {
- animation: shake var(--cubic-bezier-ease-in-out) 500ms;
- }
- @keyframes shake {
- 0% {
- translate: 0 0;
- rotate: 0deg;
- }
- 8% {
- translate: -2px -1px;
- rotate: -0.6deg;
- }
- 16% {
- translate: 1px -2px;
- rotate: 0.9deg;
- }
- 24% {
- translate: -1px 2px;
- rotate: -0.5deg;
- }
- 32% {
- translate: 2px -1px;
- rotate: 0.7deg;
- }
- 40% {
- translate: -1px -2px;
- rotate: -0.8deg;
- }
- 48% {
- translate: 1px 2px;
- rotate: 0.4deg;
- }
- 56% {
- translate: -2px 1px;
- rotate: -0.7deg;
- }
- 64% {
- translate: 2px -1px;
- rotate: 0.8deg;
- }
- 72% {
- translate: -1px -1px;
- rotate: -0.5deg;
- }
- 80% {
- translate: 2px 1px;
- rotate: 0.6deg;
- }
- 88% {
- translate: -1px 1px;
- rotate: -0.4deg;
- }
- 96% {
- translate: 1px -1px;
- rotate: 0.3deg;
- }
- 100% {
- translate: 0 0;
- rotate: 0deg;
- }
- }
- </style>
|