feat: And surface switching loading optimization
parent
6afed34437
commit
24aab5b4bb
|
@ -22,17 +22,21 @@ function setupCommonGuard(router: Router) {
|
|||
const loadedPaths = new Set<string>();
|
||||
|
||||
router.beforeEach(async (to) => {
|
||||
to.meta.loaded = loadedPaths.has(to.path);
|
||||
|
||||
// 页面加载进度条
|
||||
if (preferences.transition.progress) {
|
||||
if (!to.meta.loaded && preferences.transition.progress) {
|
||||
startProgress();
|
||||
}
|
||||
to.meta.loaded = loadedPaths.has(to.path);
|
||||
return true;
|
||||
});
|
||||
|
||||
router.afterEach((to) => {
|
||||
// 记录页面是否加载,如果已经加载,后续的页面切换动画等效果不在重复执行
|
||||
loadedPaths.add(to.path);
|
||||
|
||||
if (preferences.tabbar.enable) {
|
||||
loadedPaths.add(to.path);
|
||||
}
|
||||
|
||||
// 关闭页面加载进度条
|
||||
if (preferences.transition.progress) {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
// import { echartsInstance as echarts } from '@vben/chart-ui';
|
||||
|
||||
defineOptions({ name: 'WelCome' });
|
||||
defineOptions({ name: 'Welcome' });
|
||||
|
||||
// const cardList = ref([
|
||||
// {
|
||||
|
@ -247,5 +247,4 @@ defineOptions({ name: 'WelCome' });
|
|||
|
||||
<template>
|
||||
<div>dashboard</div>
|
||||
<!-- <Dashboard :card-list="cardList" :chart-tabs="chartTabs" /> -->
|
||||
</template>
|
||||
|
|
|
@ -3,7 +3,9 @@
|
|||
"language": "en,en-US",
|
||||
"allowCompoundWords": true,
|
||||
"words": [
|
||||
"clsx",
|
||||
"esno",
|
||||
"taze",
|
||||
"acmr",
|
||||
"antd",
|
||||
"brotli",
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-eslint-comments": "^3.2.0",
|
||||
"eslint-plugin-i": "^2.29.1",
|
||||
"eslint-plugin-jsdoc": "^48.4.0",
|
||||
"eslint-plugin-jsdoc": "^48.5.0",
|
||||
"eslint-plugin-jsonc": "^2.16.0",
|
||||
"eslint-plugin-n": "^17.9.0",
|
||||
"eslint-plugin-no-only-tests": "^3.1.0",
|
||||
|
|
|
@ -105,11 +105,11 @@ export default {
|
|||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))',
|
||||
},
|
||||
|
||||
destructive: {
|
||||
...createColorsPattern('destructive'),
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
},
|
||||
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
green: {
|
||||
...createColorsPattern('green'),
|
||||
|
@ -146,7 +146,6 @@ export default {
|
|||
desc: 'hsl(var(--secondary-desc))',
|
||||
foreground: 'hsl(var(--secondary-foreground))',
|
||||
},
|
||||
|
||||
success: {
|
||||
...createColorsPattern('success'),
|
||||
DEFAULT: 'hsl(var(--success))',
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
"rollup": "^4.18.0",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"sass": "^1.77.6",
|
||||
"unplugin-turbo-console": "^1.8.7",
|
||||
"unplugin-turbo-console": "^1.8.8-beta.1",
|
||||
"vite": "^5.3.1",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-dts": "^3.9.1",
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
|
||||
.title {
|
||||
margin-top: 66px;
|
||||
font-size: 30px;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: rgb(0 0 0 / 85%);
|
||||
}
|
||||
|
@ -56,7 +56,7 @@
|
|||
width: 48px;
|
||||
height: 5px;
|
||||
content: '';
|
||||
background: #0065cc50;
|
||||
background: hsl(var(--primary) / 50%);
|
||||
border-radius: 50%;
|
||||
animation: shadow-ani 0.5s linear infinite;
|
||||
}
|
||||
|
@ -68,7 +68,7 @@
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
content: '';
|
||||
background: #0065cc;
|
||||
background: hsl(var(--primary));
|
||||
border-radius: 4px;
|
||||
animation: jump-ani 0.5s linear infinite;
|
||||
}
|
||||
|
|
|
@ -68,7 +68,7 @@
|
|||
"is-ci": "^3.0.1",
|
||||
"jsdom": "^24.1.0",
|
||||
"rimraf": "^5.0.7",
|
||||
"taze": "^0.13.8",
|
||||
"taze": "^0.13.9",
|
||||
"turbo": "^2.0.5",
|
||||
"typescript": "^5.5.2",
|
||||
"unbuild": "^2.0.0",
|
||||
|
|
|
@ -76,6 +76,7 @@ const defaultPreferences: Preferences = {
|
|||
},
|
||||
transition: {
|
||||
enable: true,
|
||||
loading: true,
|
||||
name: 'fade-slide',
|
||||
progress: true,
|
||||
},
|
||||
|
|
|
@ -341,9 +341,9 @@ class PreferenceManager {
|
|||
// 保存重置后的偏好设置
|
||||
this.savePreferences(this.state);
|
||||
// 从存储中移除偏好设置项
|
||||
this.cache?.removeItem(STORAGE_KEY);
|
||||
this.cache?.removeItem(STORAGE_KEY_THEME);
|
||||
this.cache?.removeItem(STORAGE_KEY_LOCALE);
|
||||
[STORAGE_KEY, STORAGE_KEY_THEME, STORAGE_KEY_LOCALE].forEach((key) => {
|
||||
this.cache?.removeItem(key);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -150,6 +150,8 @@ interface ThemePreferences {
|
|||
interface TransitionPreferences {
|
||||
/** 页面切换动画是否启用 */
|
||||
enable: boolean;
|
||||
// /** 是否开启页面加载loading */
|
||||
loading: boolean;
|
||||
/** 页面切换动画 */
|
||||
name: PageTransitionType | string;
|
||||
/** 是否开启页面加载进度动画 */
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { StorageManager } from './storage-manager';
|
||||
|
||||
describe('storageManager', () => {
|
||||
let storageManager: StorageManager<{ age: number; name: string }>;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
localStorage.clear();
|
||||
storageManager = new StorageManager<{ age: number; name: string }>({
|
||||
prefix: 'test_',
|
||||
});
|
||||
});
|
||||
|
||||
it('should set and get an item', () => {
|
||||
storageManager.setItem('user', { age: 30, name: 'John Doe' });
|
||||
const user = storageManager.getItem('user');
|
||||
expect(user).toEqual({ age: 30, name: 'John Doe' });
|
||||
});
|
||||
|
||||
it('should return default value if item does not exist', () => {
|
||||
const user = storageManager.getItem('nonexistent', {
|
||||
age: 0,
|
||||
name: 'Default User',
|
||||
});
|
||||
expect(user).toEqual({ age: 0, name: 'Default User' });
|
||||
});
|
||||
|
||||
it('should remove an item', () => {
|
||||
storageManager.setItem('user', { age: 30, name: 'John Doe' });
|
||||
storageManager.removeItem('user');
|
||||
const user = storageManager.getItem('user');
|
||||
expect(user).toBeNull();
|
||||
});
|
||||
|
||||
it('should clear all items with the prefix', () => {
|
||||
storageManager.setItem('user1', { age: 30, name: 'John Doe' });
|
||||
storageManager.setItem('user2', { age: 25, name: 'Jane Doe' });
|
||||
storageManager.clear();
|
||||
expect(storageManager.getItem('user1')).toBeNull();
|
||||
expect(storageManager.getItem('user2')).toBeNull();
|
||||
});
|
||||
|
||||
it('should clear expired items', () => {
|
||||
storageManager.setItem('user', { age: 30, name: 'John Doe' }, 1000); // 1秒过期
|
||||
vi.advanceTimersByTime(1001); // 快进时间
|
||||
storageManager.clearExpiredItems();
|
||||
const user = storageManager.getItem('user');
|
||||
expect(user).toBeNull();
|
||||
});
|
||||
|
||||
it('should not clear non-expired items', () => {
|
||||
storageManager.setItem('user', { age: 30, name: 'John Doe' }, 10_000); // 10秒过期
|
||||
vi.advanceTimersByTime(5000); // 快进时间
|
||||
storageManager.clearExpiredItems();
|
||||
const user = storageManager.getItem('user');
|
||||
expect(user).toEqual({ age: 30, name: 'John Doe' });
|
||||
});
|
||||
|
||||
it('should handle JSON parse errors gracefully', () => {
|
||||
localStorage.setItem('test_user', '{ invalid JSON }');
|
||||
const user = storageManager.getItem('user', {
|
||||
age: 0,
|
||||
name: 'Default User',
|
||||
});
|
||||
expect(user).toEqual({ age: 0, name: 'Default User' });
|
||||
});
|
||||
it('should return null for non-existent items without default value', () => {
|
||||
const user = storageManager.getItem('nonexistent');
|
||||
expect(user).toBeNull();
|
||||
});
|
||||
|
||||
it('should overwrite existing items', () => {
|
||||
storageManager.setItem('user', { age: 30, name: 'John Doe' });
|
||||
storageManager.setItem('user', { age: 25, name: 'Jane Doe' });
|
||||
const user = storageManager.getItem('user');
|
||||
expect(user).toEqual({ age: 25, name: 'Jane Doe' });
|
||||
});
|
||||
|
||||
it('should handle items without expiry correctly', () => {
|
||||
storageManager.setItem('user', { age: 30, name: 'John Doe' });
|
||||
vi.advanceTimersByTime(5000);
|
||||
const user = storageManager.getItem('user');
|
||||
expect(user).toEqual({ age: 30, name: 'John Doe' });
|
||||
});
|
||||
|
||||
it('should remove expired items when accessed', () => {
|
||||
storageManager.setItem('user', { age: 30, name: 'John Doe' }, 1000); // 1秒过期
|
||||
vi.advanceTimersByTime(1001); // 快进时间
|
||||
const user = storageManager.getItem('user');
|
||||
expect(user).toBeNull();
|
||||
});
|
||||
|
||||
it('should not remove non-expired items when accessed', () => {
|
||||
storageManager.setItem('user', { age: 30, name: 'John Doe' }, 10_000); // 10秒过期
|
||||
vi.advanceTimersByTime(5000); // 快进时间
|
||||
const user = storageManager.getItem('user');
|
||||
expect(user).toEqual({ age: 30, name: 'John Doe' });
|
||||
});
|
||||
|
||||
it('should handle multiple items with different expiry times', () => {
|
||||
storageManager.setItem('user1', { age: 30, name: 'John Doe' }, 1000); // 1秒过期
|
||||
storageManager.setItem('user2', { age: 25, name: 'Jane Doe' }, 2000); // 2秒过期
|
||||
vi.advanceTimersByTime(1500); // 快进时间
|
||||
storageManager.clearExpiredItems();
|
||||
const user1 = storageManager.getItem('user1');
|
||||
const user2 = storageManager.getItem('user2');
|
||||
expect(user1).toBeNull();
|
||||
expect(user2).toEqual({ age: 25, name: 'Jane Doe' });
|
||||
});
|
||||
|
||||
it('should handle items with no expiry', () => {
|
||||
storageManager.setItem('user', { age: 30, name: 'John Doe' });
|
||||
vi.advanceTimersByTime(10_000); // 快进时间
|
||||
storageManager.clearExpiredItems();
|
||||
const user = storageManager.getItem('user');
|
||||
expect(user).toEqual({ age: 30, name: 'John Doe' });
|
||||
});
|
||||
|
||||
it('should clear all items correctly', () => {
|
||||
storageManager.setItem('user1', { age: 30, name: 'John Doe' });
|
||||
storageManager.setItem('user2', { age: 25, name: 'Jane Doe' });
|
||||
storageManager.clear();
|
||||
const user1 = storageManager.getItem('user1');
|
||||
const user2 = storageManager.getItem('user2');
|
||||
expect(user1).toBeNull();
|
||||
expect(user2).toBeNull();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,118 @@
|
|||
type StorageType = 'localStorage' | 'sessionStorage';
|
||||
|
||||
interface StorageManagerOptions {
|
||||
prefix?: string;
|
||||
storageType?: StorageType;
|
||||
}
|
||||
|
||||
interface StorageItem<T> {
|
||||
expiry?: number;
|
||||
value: T;
|
||||
}
|
||||
|
||||
class StorageManager {
|
||||
private prefix: string;
|
||||
private storage: Storage;
|
||||
|
||||
constructor({
|
||||
prefix = '',
|
||||
storageType = 'localStorage',
|
||||
}: StorageManagerOptions = {}) {
|
||||
this.prefix = prefix;
|
||||
this.storage =
|
||||
storageType === 'localStorage'
|
||||
? window.localStorage
|
||||
: window.sessionStorage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取完整的存储键
|
||||
* @param key 原始键
|
||||
* @returns 带前缀的完整键
|
||||
*/
|
||||
private getFullKey(key: string): string {
|
||||
return `${this.prefix}-${key}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有带前缀的存储项
|
||||
*/
|
||||
clear(): void {
|
||||
const keysToRemove: string[] = [];
|
||||
for (let i = 0; i < this.storage.length; i++) {
|
||||
const key = this.storage.key(i);
|
||||
if (key && key.startsWith(this.prefix)) {
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
}
|
||||
keysToRemove.forEach((key) => this.storage.removeItem(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有过期的存储项
|
||||
*/
|
||||
clearExpiredItems(): void {
|
||||
for (let i = 0; i < this.storage.length; i++) {
|
||||
const key = this.storage.key(i);
|
||||
if (key && key.startsWith(this.prefix)) {
|
||||
const shortKey = key.replace(this.prefix, '');
|
||||
this.getItem(shortKey); // 调用 getItem 方法检查并移除过期项
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取存储项
|
||||
* @param key 键
|
||||
* @param defaultValue 当项不存在或已过期时返回的默认值
|
||||
* @returns 值,如果项已过期或解析错误则返回默认值
|
||||
*/
|
||||
getItem<T>(key: string, defaultValue: T | null = null): T | null {
|
||||
const fullKey = this.getFullKey(key);
|
||||
const itemStr = this.storage.getItem(fullKey);
|
||||
if (!itemStr) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
try {
|
||||
const item: StorageItem<T> = JSON.parse(itemStr);
|
||||
if (item.expiry && Date.now() > item.expiry) {
|
||||
this.storage.removeItem(fullKey);
|
||||
return defaultValue;
|
||||
}
|
||||
return item.value;
|
||||
} catch (error) {
|
||||
console.error(`Error parsing item with key "${fullKey}":`, error);
|
||||
this.storage.removeItem(fullKey); // 如果解析失败,删除该项
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除存储项
|
||||
* @param key 键
|
||||
*/
|
||||
removeItem(key: string): void {
|
||||
const fullKey = this.getFullKey(key);
|
||||
this.storage.removeItem(fullKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置存储项
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
* @param ttl 存活时间(毫秒)
|
||||
*/
|
||||
setItem<T>(key: string, value: T, ttl?: number): void {
|
||||
const fullKey = this.getFullKey(key);
|
||||
const expiry = ttl ? Date.now() + ttl : undefined;
|
||||
const item: StorageItem<T> = { expiry, value };
|
||||
try {
|
||||
this.storage.setItem(fullKey, JSON.stringify(item));
|
||||
} catch (error) {
|
||||
console.error(`Error setting item with key "${fullKey}":`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { StorageManager };
|
|
@ -0,0 +1,17 @@
|
|||
type StorageType = 'localStorage' | 'sessionStorage';
|
||||
|
||||
interface StorageValue<T> {
|
||||
data: T;
|
||||
expiry: null | number;
|
||||
}
|
||||
|
||||
interface IStorageCache {
|
||||
clear(): void;
|
||||
getItem<T>(key: string): T | null;
|
||||
key(index: number): null | string;
|
||||
length(): number;
|
||||
removeItem(key: string): void;
|
||||
setItem<T>(key: string, value: T, expiryInMinutes?: number): void;
|
||||
}
|
||||
|
||||
export type { IStorageCache, StorageType, StorageValue };
|
|
@ -2,7 +2,9 @@
|
|||
import type { ContentCompactType } from '@vben-core/typings';
|
||||
|
||||
import type { CSSProperties } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { useCssVar, useDebounceFn, useWindowSize } from '@vueuse/core';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
|
@ -52,6 +54,14 @@ const props = withDefaults(defineProps<Props>(), {
|
|||
paddingTop: 16,
|
||||
});
|
||||
|
||||
const domElement = ref<HTMLDivElement | null>();
|
||||
|
||||
const { height, width } = useWindowSize();
|
||||
const contentClientHeight = useCssVar('--vben-content-client-height');
|
||||
const debouncedCalcHeight = useDebounceFn(() => {
|
||||
contentClientHeight.value = `${domElement.value?.clientHeight ?? window.innerHeight}px`;
|
||||
}, 200);
|
||||
|
||||
const style = computed((): CSSProperties => {
|
||||
const {
|
||||
contentCompact,
|
||||
|
@ -76,10 +86,18 @@ const style = computed((): CSSProperties => {
|
|||
paddingTop: `${paddingTop}px`,
|
||||
};
|
||||
});
|
||||
|
||||
watch([height, width], () => {
|
||||
debouncedCalcHeight();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
debouncedCalcHeight();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main :style="style">
|
||||
<main ref="domElement" :style="style">
|
||||
<slot></slot>
|
||||
</main>
|
||||
</template>
|
||||
|
|
|
@ -20,7 +20,7 @@ defineOptions({
|
|||
const props = withDefaults(defineProps<Props>(), {
|
||||
minLoadingTime: 50,
|
||||
});
|
||||
const startTime = ref(0);
|
||||
// const startTime = ref(0);
|
||||
const showSpinner = ref(false);
|
||||
const renderSpinner = ref(true);
|
||||
const timer = ref<ReturnType<typeof setTimeout>>();
|
||||
|
@ -33,11 +33,12 @@ watch(
|
|||
clearTimeout(timer.value);
|
||||
return;
|
||||
}
|
||||
startTime.value = performance.now();
|
||||
timer.value = setTimeout(() => {
|
||||
const loadingTime = performance.now() - startTime.value;
|
||||
|
||||
showSpinner.value = loadingTime > props.minLoadingTime;
|
||||
// startTime.value = performance.now();
|
||||
timer.value = setTimeout(() => {
|
||||
// const loadingTime = performance.now() - startTime.value;
|
||||
|
||||
showSpinner.value = true;
|
||||
if (showSpinner.value) {
|
||||
renderSpinner.value = true;
|
||||
}
|
||||
|
@ -49,13 +50,14 @@ watch(
|
|||
);
|
||||
|
||||
function onTransitionEnd() {
|
||||
renderSpinner.value = false;
|
||||
if (!showSpinner.value) {
|
||||
renderSpinner.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="renderSpinner"
|
||||
:class="{
|
||||
'invisible opacity-0': !showSpinner,
|
||||
}"
|
||||
|
|
|
@ -2,15 +2,18 @@
|
|||
import type { RouteLocationNormalizedLoaded } from 'vue-router';
|
||||
|
||||
import { preferences, usePreferences } from '@vben-core/preferences';
|
||||
import { Spinner } from '@vben-core/shadcn-ui';
|
||||
import { storeToRefs, useTabsStore } from '@vben-core/stores';
|
||||
|
||||
import { IFrameRouterView } from '../../iframe';
|
||||
import { useContentSpinner } from './use-content-spinner';
|
||||
|
||||
defineOptions({ name: 'LayoutContent' });
|
||||
|
||||
const { keepAlive } = usePreferences();
|
||||
|
||||
const tabsStore = useTabsStore();
|
||||
const { onTransitionEnd, spinning } = useContentSpinner();
|
||||
|
||||
const { getCacheTabs, getExcludeTabs, renderRouteView } =
|
||||
storeToRefs(tabsStore);
|
||||
|
||||
|
@ -29,32 +32,45 @@ function getTransitionName(route: RouteLocationNormalizedLoaded) {
|
|||
}
|
||||
|
||||
// 如果页面已经加载过,则不使用动画
|
||||
if (route.meta.loaded) {
|
||||
return;
|
||||
}
|
||||
// if (route.meta.loaded) {
|
||||
// return;
|
||||
// }
|
||||
// 已经打开且已经加载过的页面不使用动画
|
||||
const inTabs = getCacheTabs.value.includes(route.name as string);
|
||||
|
||||
return inTabs && route.meta.loaded ? undefined : transitionName;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<IFrameRouterView />
|
||||
<RouterView v-slot="{ Component, route }">
|
||||
<Transition :name="getTransitionName(route)" appear mode="out-in">
|
||||
<KeepAlive
|
||||
v-if="keepAlive"
|
||||
:exclude="getExcludeTabs"
|
||||
:include="getCacheTabs"
|
||||
<div class="relative h-full">
|
||||
<Spinner
|
||||
v-if="preferences.transition.loading"
|
||||
:spinning="spinning"
|
||||
class="h-[var(--vben-content-client-height)]"
|
||||
/>
|
||||
<IFrameRouterView />
|
||||
<RouterView v-slot="{ Component, route }">
|
||||
<Transition
|
||||
:name="getTransitionName(route)"
|
||||
appear
|
||||
mode="out-in"
|
||||
@transitionend="onTransitionEnd"
|
||||
>
|
||||
<component
|
||||
:is="Component"
|
||||
v-if="renderRouteView"
|
||||
v-show="!route.meta.iframeSrc"
|
||||
:key="route.fullPath"
|
||||
/>
|
||||
</KeepAlive>
|
||||
<component :is="Component" v-else :key="route.fullPath" />
|
||||
</Transition>
|
||||
</RouterView>
|
||||
<KeepAlive
|
||||
v-if="keepAlive"
|
||||
:exclude="getExcludeTabs"
|
||||
:include="getCacheTabs"
|
||||
>
|
||||
<component
|
||||
:is="Component"
|
||||
v-if="renderRouteView"
|
||||
v-show="!route.meta.iframeSrc"
|
||||
:key="route.fullPath"
|
||||
/>
|
||||
</KeepAlive>
|
||||
<component :is="Component" v-else :key="route.fullPath" />
|
||||
</Transition>
|
||||
</RouterView>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
import { computed, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { preferences } from '@vben-core/preferences';
|
||||
|
||||
function useContentSpinner() {
|
||||
const spinning = ref(false);
|
||||
const isStartTransition = ref(false);
|
||||
const startTime = ref(0);
|
||||
const router = useRouter();
|
||||
const minShowTime = 500;
|
||||
const enableLoading = computed(() => preferences.transition.loading);
|
||||
|
||||
const onEnd = () => {
|
||||
if (!enableLoading.value) {
|
||||
return;
|
||||
}
|
||||
const processTime = performance.now() - startTime.value;
|
||||
if (processTime < minShowTime) {
|
||||
setTimeout(() => {
|
||||
spinning.value = false;
|
||||
}, minShowTime - processTime);
|
||||
} else {
|
||||
spinning.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
router.beforeEach((to) => {
|
||||
if (to.meta.loaded || !enableLoading.value) {
|
||||
return true;
|
||||
}
|
||||
isStartTransition.value = false;
|
||||
startTime.value = performance.now();
|
||||
spinning.value = true;
|
||||
return true;
|
||||
});
|
||||
|
||||
router.afterEach((to) => {
|
||||
if (to.meta.loaded || !enableLoading.value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 未进入过渡动画
|
||||
if (!isStartTransition.value) {
|
||||
// 关闭加载动画
|
||||
onEnd();
|
||||
}
|
||||
|
||||
isStartTransition.value = false;
|
||||
return true;
|
||||
});
|
||||
|
||||
return { onTransitionEnd: onEnd, spinning };
|
||||
}
|
||||
|
||||
export { useContentSpinner };
|
|
@ -6,13 +6,14 @@ import SwitchItem from '../switch-item.vue';
|
|||
defineOptions({
|
||||
name: 'PreferenceAnimation',
|
||||
});
|
||||
|
||||
const transitionProgress = defineModel<boolean>('transitionProgress', {
|
||||
// 默认值
|
||||
default: false,
|
||||
});
|
||||
|
||||
const transitionName = defineModel<string>('transitionName');
|
||||
const transitionEnable = defineModel<boolean>('transitionEnable');
|
||||
const transitionLoading = defineModel<boolean>('transitionLoading');
|
||||
|
||||
const transitionPreset = ['fade', 'fade-slide', 'fade-up', 'fade-down'];
|
||||
|
||||
|
@ -23,10 +24,13 @@ function handleClick(value: string) {
|
|||
|
||||
<template>
|
||||
<SwitchItem v-model="transitionProgress">
|
||||
{{ $t('preferences.page-progress') }}
|
||||
{{ $t('preferences.animation.progress') }}
|
||||
</SwitchItem>
|
||||
<SwitchItem v-model="transitionLoading">
|
||||
{{ $t('preferences.animation.loading') }}
|
||||
</SwitchItem>
|
||||
<SwitchItem v-model="transitionEnable">
|
||||
{{ $t('preferences.page-transition') }}
|
||||
{{ $t('preferences.animation.transition') }}
|
||||
</SwitchItem>
|
||||
<div
|
||||
v-if="transitionEnable"
|
||||
|
|
|
@ -42,6 +42,7 @@ import Preferences from './preferences.vue';
|
|||
:theme-mode="preferences.theme.mode"
|
||||
:theme-radius="preferences.theme.radius"
|
||||
:transition-enable="preferences.transition.enable"
|
||||
:transition-loading="preferences.transition.loading"
|
||||
:transition-name="preferences.transition.name"
|
||||
:transition-progress="preferences.transition.progress"
|
||||
@update:app-ai-assistant="
|
||||
|
@ -143,6 +144,9 @@ import Preferences from './preferences.vue';
|
|||
@update:transition-enable="
|
||||
(val) => updatePreferences({ transition: { enable: val } })
|
||||
"
|
||||
@update:transition-loading="
|
||||
(val) => updatePreferences({ transition: { loading: val } })
|
||||
"
|
||||
@update:transition-name="
|
||||
(val) => updatePreferences({ transition: { name: val } })
|
||||
"
|
||||
|
|
|
@ -64,6 +64,7 @@ const appContentCompact = defineModel<ContentCompactType>('appContentCompact');
|
|||
|
||||
const transitionProgress = defineModel<boolean>('transitionProgress');
|
||||
const transitionName = defineModel<string>('transitionName');
|
||||
const transitionLoading = defineModel<boolean>('transitionLoading');
|
||||
const transitionEnable = defineModel<boolean>('transitionEnable');
|
||||
|
||||
const themeColorPrimary = defineModel<string>('themeColorPrimary');
|
||||
|
@ -209,9 +210,10 @@ function handleReset() {
|
|||
/>
|
||||
</Block>
|
||||
|
||||
<Block :title="$t('preferences.animation')">
|
||||
<Block :title="$t('preferences.animation.name')">
|
||||
<Animation
|
||||
v-model:transition-enable="transitionEnable"
|
||||
v-model:transition-loading="transitionLoading"
|
||||
v-model:transition-name="transitionName"
|
||||
v-model:transition-progress="transitionProgress"
|
||||
/>
|
||||
|
|
|
@ -147,7 +147,7 @@ preferences:
|
|||
full-content-tip: Display only the main content, no menus
|
||||
weak-mode: Color Weak Mode
|
||||
gray-mode: Gray Mode
|
||||
animation: Animation
|
||||
|
||||
language: Language
|
||||
dynamic-title: Dynamic Title
|
||||
normal: Normal
|
||||
|
@ -168,8 +168,7 @@ preferences:
|
|||
breadcrumb-background: background
|
||||
breadcrumb-style: Breadcrumb Type
|
||||
breadcrumb-hide-only-one: Hidden when only one left
|
||||
page-progress: Loading progress bar
|
||||
page-transition: Page transition animation
|
||||
|
||||
copy: Copy Preferences
|
||||
copy-success: Copy successful. Please replace in `src/preferences.ts` of the app
|
||||
reset-success: Preferences reset successfully
|
||||
|
@ -180,6 +179,11 @@ preferences:
|
|||
tabs-icon: Display Tabbar Icon
|
||||
mode: Mode
|
||||
logo-visible: Display Logo
|
||||
animation:
|
||||
name: Animation
|
||||
loading: Page transition loading
|
||||
transition: Page transition animation
|
||||
progress: Page transition progress
|
||||
theme:
|
||||
name: Theme
|
||||
builtin: Built-in
|
||||
|
|
|
@ -150,7 +150,7 @@ preferences:
|
|||
follow-system: 跟随系统
|
||||
weak-mode: 色弱模式
|
||||
gray-mode: 灰色模式
|
||||
animation: 动画
|
||||
|
||||
navigation-menu: 导航菜单
|
||||
navigation-style: 导航菜单风格
|
||||
navigation-accordion: 侧边导航菜单手风琴模式
|
||||
|
@ -167,8 +167,6 @@ preferences:
|
|||
breadcrumb-style: 面包屑风格
|
||||
breadcrumb-hide-only-one: 只有一个时隐藏
|
||||
breadcrumb-background: 背景
|
||||
page-progress: 加载进度条
|
||||
page-transition: 页面切换动画
|
||||
copy: 复制偏好设置
|
||||
copy-success: 拷贝成功,请在 app 下的 `src/preferences.ts`内进行覆盖
|
||||
reset-success: 重置偏好设置成功
|
||||
|
@ -179,6 +177,11 @@ preferences:
|
|||
tabs-icon: 显示标签栏图标
|
||||
mode: 模式
|
||||
logo-visible: 显示 Logo
|
||||
animation:
|
||||
name: 动画
|
||||
loading: 页面切换 Loading
|
||||
transition: 页面切换动画
|
||||
progress: 页面加载进度条
|
||||
theme:
|
||||
name: 主题
|
||||
builtin: 内置主题
|
||||
|
|
1799
pnpm-lock.yaml
1799
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue