feat: And surface switching loading optimization
parent
6afed34437
commit
24aab5b4bb
|
@ -22,17 +22,21 @@ function setupCommonGuard(router: Router) {
|
||||||
const loadedPaths = new Set<string>();
|
const loadedPaths = new Set<string>();
|
||||||
|
|
||||||
router.beforeEach(async (to) => {
|
router.beforeEach(async (to) => {
|
||||||
|
to.meta.loaded = loadedPaths.has(to.path);
|
||||||
|
|
||||||
// 页面加载进度条
|
// 页面加载进度条
|
||||||
if (preferences.transition.progress) {
|
if (!to.meta.loaded && preferences.transition.progress) {
|
||||||
startProgress();
|
startProgress();
|
||||||
}
|
}
|
||||||
to.meta.loaded = loadedPaths.has(to.path);
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
router.afterEach((to) => {
|
router.afterEach((to) => {
|
||||||
// 记录页面是否加载,如果已经加载,后续的页面切换动画等效果不在重复执行
|
// 记录页面是否加载,如果已经加载,后续的页面切换动画等效果不在重复执行
|
||||||
loadedPaths.add(to.path);
|
|
||||||
|
if (preferences.tabbar.enable) {
|
||||||
|
loadedPaths.add(to.path);
|
||||||
|
}
|
||||||
|
|
||||||
// 关闭页面加载进度条
|
// 关闭页面加载进度条
|
||||||
if (preferences.transition.progress) {
|
if (preferences.transition.progress) {
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
// import { echartsInstance as echarts } from '@vben/chart-ui';
|
// import { echartsInstance as echarts } from '@vben/chart-ui';
|
||||||
|
|
||||||
defineOptions({ name: 'WelCome' });
|
defineOptions({ name: 'Welcome' });
|
||||||
|
|
||||||
// const cardList = ref([
|
// const cardList = ref([
|
||||||
// {
|
// {
|
||||||
|
@ -247,5 +247,4 @@ defineOptions({ name: 'WelCome' });
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>dashboard</div>
|
<div>dashboard</div>
|
||||||
<!-- <Dashboard :card-list="cardList" :chart-tabs="chartTabs" /> -->
|
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -3,7 +3,9 @@
|
||||||
"language": "en,en-US",
|
"language": "en,en-US",
|
||||||
"allowCompoundWords": true,
|
"allowCompoundWords": true,
|
||||||
"words": [
|
"words": [
|
||||||
|
"clsx",
|
||||||
"esno",
|
"esno",
|
||||||
|
"taze",
|
||||||
"acmr",
|
"acmr",
|
||||||
"antd",
|
"antd",
|
||||||
"brotli",
|
"brotli",
|
||||||
|
|
|
@ -38,7 +38,7 @@
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-eslint-comments": "^3.2.0",
|
"eslint-plugin-eslint-comments": "^3.2.0",
|
||||||
"eslint-plugin-i": "^2.29.1",
|
"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-jsonc": "^2.16.0",
|
||||||
"eslint-plugin-n": "^17.9.0",
|
"eslint-plugin-n": "^17.9.0",
|
||||||
"eslint-plugin-no-only-tests": "^3.1.0",
|
"eslint-plugin-no-only-tests": "^3.1.0",
|
||||||
|
|
|
@ -105,11 +105,11 @@ export default {
|
||||||
DEFAULT: 'hsl(var(--card))',
|
DEFAULT: 'hsl(var(--card))',
|
||||||
foreground: 'hsl(var(--card-foreground))',
|
foreground: 'hsl(var(--card-foreground))',
|
||||||
},
|
},
|
||||||
|
|
||||||
destructive: {
|
destructive: {
|
||||||
...createColorsPattern('destructive'),
|
...createColorsPattern('destructive'),
|
||||||
DEFAULT: 'hsl(var(--destructive))',
|
DEFAULT: 'hsl(var(--destructive))',
|
||||||
},
|
},
|
||||||
|
|
||||||
foreground: 'hsl(var(--foreground))',
|
foreground: 'hsl(var(--foreground))',
|
||||||
green: {
|
green: {
|
||||||
...createColorsPattern('green'),
|
...createColorsPattern('green'),
|
||||||
|
@ -146,7 +146,6 @@ export default {
|
||||||
desc: 'hsl(var(--secondary-desc))',
|
desc: 'hsl(var(--secondary-desc))',
|
||||||
foreground: 'hsl(var(--secondary-foreground))',
|
foreground: 'hsl(var(--secondary-foreground))',
|
||||||
},
|
},
|
||||||
|
|
||||||
success: {
|
success: {
|
||||||
...createColorsPattern('success'),
|
...createColorsPattern('success'),
|
||||||
DEFAULT: 'hsl(var(--success))',
|
DEFAULT: 'hsl(var(--success))',
|
||||||
|
|
|
@ -46,7 +46,7 @@
|
||||||
"rollup": "^4.18.0",
|
"rollup": "^4.18.0",
|
||||||
"rollup-plugin-visualizer": "^5.12.0",
|
"rollup-plugin-visualizer": "^5.12.0",
|
||||||
"sass": "^1.77.6",
|
"sass": "^1.77.6",
|
||||||
"unplugin-turbo-console": "^1.8.7",
|
"unplugin-turbo-console": "^1.8.8-beta.1",
|
||||||
"vite": "^5.3.1",
|
"vite": "^5.3.1",
|
||||||
"vite-plugin-compression": "^0.5.1",
|
"vite-plugin-compression": "^0.5.1",
|
||||||
"vite-plugin-dts": "^3.9.1",
|
"vite-plugin-dts": "^3.9.1",
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
margin-top: 66px;
|
margin-top: 66px;
|
||||||
font-size: 30px;
|
font-size: 28px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: rgb(0 0 0 / 85%);
|
color: rgb(0 0 0 / 85%);
|
||||||
}
|
}
|
||||||
|
@ -56,7 +56,7 @@
|
||||||
width: 48px;
|
width: 48px;
|
||||||
height: 5px;
|
height: 5px;
|
||||||
content: '';
|
content: '';
|
||||||
background: #0065cc50;
|
background: hsl(var(--primary) / 50%);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
animation: shadow-ani 0.5s linear infinite;
|
animation: shadow-ani 0.5s linear infinite;
|
||||||
}
|
}
|
||||||
|
@ -68,7 +68,7 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
content: '';
|
content: '';
|
||||||
background: #0065cc;
|
background: hsl(var(--primary));
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
animation: jump-ani 0.5s linear infinite;
|
animation: jump-ani 0.5s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,7 +68,7 @@
|
||||||
"is-ci": "^3.0.1",
|
"is-ci": "^3.0.1",
|
||||||
"jsdom": "^24.1.0",
|
"jsdom": "^24.1.0",
|
||||||
"rimraf": "^5.0.7",
|
"rimraf": "^5.0.7",
|
||||||
"taze": "^0.13.8",
|
"taze": "^0.13.9",
|
||||||
"turbo": "^2.0.5",
|
"turbo": "^2.0.5",
|
||||||
"typescript": "^5.5.2",
|
"typescript": "^5.5.2",
|
||||||
"unbuild": "^2.0.0",
|
"unbuild": "^2.0.0",
|
||||||
|
|
|
@ -76,6 +76,7 @@ const defaultPreferences: Preferences = {
|
||||||
},
|
},
|
||||||
transition: {
|
transition: {
|
||||||
enable: true,
|
enable: true,
|
||||||
|
loading: true,
|
||||||
name: 'fade-slide',
|
name: 'fade-slide',
|
||||||
progress: true,
|
progress: true,
|
||||||
},
|
},
|
||||||
|
|
|
@ -341,9 +341,9 @@ class PreferenceManager {
|
||||||
// 保存重置后的偏好设置
|
// 保存重置后的偏好设置
|
||||||
this.savePreferences(this.state);
|
this.savePreferences(this.state);
|
||||||
// 从存储中移除偏好设置项
|
// 从存储中移除偏好设置项
|
||||||
this.cache?.removeItem(STORAGE_KEY);
|
[STORAGE_KEY, STORAGE_KEY_THEME, STORAGE_KEY_LOCALE].forEach((key) => {
|
||||||
this.cache?.removeItem(STORAGE_KEY_THEME);
|
this.cache?.removeItem(key);
|
||||||
this.cache?.removeItem(STORAGE_KEY_LOCALE);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -150,6 +150,8 @@ interface ThemePreferences {
|
||||||
interface TransitionPreferences {
|
interface TransitionPreferences {
|
||||||
/** 页面切换动画是否启用 */
|
/** 页面切换动画是否启用 */
|
||||||
enable: boolean;
|
enable: boolean;
|
||||||
|
// /** 是否开启页面加载loading */
|
||||||
|
loading: boolean;
|
||||||
/** 页面切换动画 */
|
/** 页面切换动画 */
|
||||||
name: PageTransitionType | string;
|
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 { ContentCompactType } from '@vben-core/typings';
|
||||||
|
|
||||||
import type { CSSProperties } from 'vue';
|
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 {
|
interface Props {
|
||||||
/**
|
/**
|
||||||
|
@ -52,6 +54,14 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
paddingTop: 16,
|
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 style = computed((): CSSProperties => {
|
||||||
const {
|
const {
|
||||||
contentCompact,
|
contentCompact,
|
||||||
|
@ -76,10 +86,18 @@ const style = computed((): CSSProperties => {
|
||||||
paddingTop: `${paddingTop}px`,
|
paddingTop: `${paddingTop}px`,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch([height, width], () => {
|
||||||
|
debouncedCalcHeight();
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
debouncedCalcHeight();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main :style="style">
|
<main ref="domElement" :style="style">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -20,7 +20,7 @@ defineOptions({
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
minLoadingTime: 50,
|
minLoadingTime: 50,
|
||||||
});
|
});
|
||||||
const startTime = ref(0);
|
// const startTime = ref(0);
|
||||||
const showSpinner = ref(false);
|
const showSpinner = ref(false);
|
||||||
const renderSpinner = ref(true);
|
const renderSpinner = ref(true);
|
||||||
const timer = ref<ReturnType<typeof setTimeout>>();
|
const timer = ref<ReturnType<typeof setTimeout>>();
|
||||||
|
@ -33,11 +33,12 @@ watch(
|
||||||
clearTimeout(timer.value);
|
clearTimeout(timer.value);
|
||||||
return;
|
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) {
|
if (showSpinner.value) {
|
||||||
renderSpinner.value = true;
|
renderSpinner.value = true;
|
||||||
}
|
}
|
||||||
|
@ -49,13 +50,14 @@ watch(
|
||||||
);
|
);
|
||||||
|
|
||||||
function onTransitionEnd() {
|
function onTransitionEnd() {
|
||||||
renderSpinner.value = false;
|
if (!showSpinner.value) {
|
||||||
|
renderSpinner.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="renderSpinner"
|
|
||||||
:class="{
|
:class="{
|
||||||
'invisible opacity-0': !showSpinner,
|
'invisible opacity-0': !showSpinner,
|
||||||
}"
|
}"
|
||||||
|
|
|
@ -2,15 +2,18 @@
|
||||||
import type { RouteLocationNormalizedLoaded } from 'vue-router';
|
import type { RouteLocationNormalizedLoaded } from 'vue-router';
|
||||||
|
|
||||||
import { preferences, usePreferences } from '@vben-core/preferences';
|
import { preferences, usePreferences } from '@vben-core/preferences';
|
||||||
|
import { Spinner } from '@vben-core/shadcn-ui';
|
||||||
import { storeToRefs, useTabsStore } from '@vben-core/stores';
|
import { storeToRefs, useTabsStore } from '@vben-core/stores';
|
||||||
|
|
||||||
import { IFrameRouterView } from '../../iframe';
|
import { IFrameRouterView } from '../../iframe';
|
||||||
|
import { useContentSpinner } from './use-content-spinner';
|
||||||
|
|
||||||
defineOptions({ name: 'LayoutContent' });
|
defineOptions({ name: 'LayoutContent' });
|
||||||
|
|
||||||
const { keepAlive } = usePreferences();
|
const { keepAlive } = usePreferences();
|
||||||
|
|
||||||
const tabsStore = useTabsStore();
|
const tabsStore = useTabsStore();
|
||||||
|
const { onTransitionEnd, spinning } = useContentSpinner();
|
||||||
|
|
||||||
const { getCacheTabs, getExcludeTabs, renderRouteView } =
|
const { getCacheTabs, getExcludeTabs, renderRouteView } =
|
||||||
storeToRefs(tabsStore);
|
storeToRefs(tabsStore);
|
||||||
|
|
||||||
|
@ -29,32 +32,45 @@ function getTransitionName(route: RouteLocationNormalizedLoaded) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果页面已经加载过,则不使用动画
|
// 如果页面已经加载过,则不使用动画
|
||||||
if (route.meta.loaded) {
|
// if (route.meta.loaded) {
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
// 已经打开且已经加载过的页面不使用动画
|
// 已经打开且已经加载过的页面不使用动画
|
||||||
const inTabs = getCacheTabs.value.includes(route.name as string);
|
const inTabs = getCacheTabs.value.includes(route.name as string);
|
||||||
|
|
||||||
return inTabs && route.meta.loaded ? undefined : transitionName;
|
return inTabs && route.meta.loaded ? undefined : transitionName;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<IFrameRouterView />
|
<div class="relative h-full">
|
||||||
<RouterView v-slot="{ Component, route }">
|
<Spinner
|
||||||
<Transition :name="getTransitionName(route)" appear mode="out-in">
|
v-if="preferences.transition.loading"
|
||||||
<KeepAlive
|
:spinning="spinning"
|
||||||
v-if="keepAlive"
|
class="h-[var(--vben-content-client-height)]"
|
||||||
:exclude="getExcludeTabs"
|
/>
|
||||||
:include="getCacheTabs"
|
<IFrameRouterView />
|
||||||
|
<RouterView v-slot="{ Component, route }">
|
||||||
|
<Transition
|
||||||
|
:name="getTransitionName(route)"
|
||||||
|
appear
|
||||||
|
mode="out-in"
|
||||||
|
@transitionend="onTransitionEnd"
|
||||||
>
|
>
|
||||||
<component
|
<KeepAlive
|
||||||
:is="Component"
|
v-if="keepAlive"
|
||||||
v-if="renderRouteView"
|
:exclude="getExcludeTabs"
|
||||||
v-show="!route.meta.iframeSrc"
|
:include="getCacheTabs"
|
||||||
:key="route.fullPath"
|
>
|
||||||
/>
|
<component
|
||||||
</KeepAlive>
|
:is="Component"
|
||||||
<component :is="Component" v-else :key="route.fullPath" />
|
v-if="renderRouteView"
|
||||||
</Transition>
|
v-show="!route.meta.iframeSrc"
|
||||||
</RouterView>
|
:key="route.fullPath"
|
||||||
|
/>
|
||||||
|
</KeepAlive>
|
||||||
|
<component :is="Component" v-else :key="route.fullPath" />
|
||||||
|
</Transition>
|
||||||
|
</RouterView>
|
||||||
|
</div>
|
||||||
</template>
|
</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({
|
defineOptions({
|
||||||
name: 'PreferenceAnimation',
|
name: 'PreferenceAnimation',
|
||||||
});
|
});
|
||||||
|
|
||||||
const transitionProgress = defineModel<boolean>('transitionProgress', {
|
const transitionProgress = defineModel<boolean>('transitionProgress', {
|
||||||
// 默认值
|
// 默认值
|
||||||
default: false,
|
default: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const transitionName = defineModel<string>('transitionName');
|
const transitionName = defineModel<string>('transitionName');
|
||||||
const transitionEnable = defineModel<boolean>('transitionEnable');
|
const transitionEnable = defineModel<boolean>('transitionEnable');
|
||||||
|
const transitionLoading = defineModel<boolean>('transitionLoading');
|
||||||
|
|
||||||
const transitionPreset = ['fade', 'fade-slide', 'fade-up', 'fade-down'];
|
const transitionPreset = ['fade', 'fade-slide', 'fade-up', 'fade-down'];
|
||||||
|
|
||||||
|
@ -23,10 +24,13 @@ function handleClick(value: string) {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SwitchItem v-model="transitionProgress">
|
<SwitchItem v-model="transitionProgress">
|
||||||
{{ $t('preferences.page-progress') }}
|
{{ $t('preferences.animation.progress') }}
|
||||||
|
</SwitchItem>
|
||||||
|
<SwitchItem v-model="transitionLoading">
|
||||||
|
{{ $t('preferences.animation.loading') }}
|
||||||
</SwitchItem>
|
</SwitchItem>
|
||||||
<SwitchItem v-model="transitionEnable">
|
<SwitchItem v-model="transitionEnable">
|
||||||
{{ $t('preferences.page-transition') }}
|
{{ $t('preferences.animation.transition') }}
|
||||||
</SwitchItem>
|
</SwitchItem>
|
||||||
<div
|
<div
|
||||||
v-if="transitionEnable"
|
v-if="transitionEnable"
|
||||||
|
|
|
@ -42,6 +42,7 @@ import Preferences from './preferences.vue';
|
||||||
:theme-mode="preferences.theme.mode"
|
:theme-mode="preferences.theme.mode"
|
||||||
:theme-radius="preferences.theme.radius"
|
:theme-radius="preferences.theme.radius"
|
||||||
:transition-enable="preferences.transition.enable"
|
:transition-enable="preferences.transition.enable"
|
||||||
|
:transition-loading="preferences.transition.loading"
|
||||||
:transition-name="preferences.transition.name"
|
:transition-name="preferences.transition.name"
|
||||||
:transition-progress="preferences.transition.progress"
|
:transition-progress="preferences.transition.progress"
|
||||||
@update:app-ai-assistant="
|
@update:app-ai-assistant="
|
||||||
|
@ -143,6 +144,9 @@ import Preferences from './preferences.vue';
|
||||||
@update:transition-enable="
|
@update:transition-enable="
|
||||||
(val) => updatePreferences({ transition: { enable: val } })
|
(val) => updatePreferences({ transition: { enable: val } })
|
||||||
"
|
"
|
||||||
|
@update:transition-loading="
|
||||||
|
(val) => updatePreferences({ transition: { loading: val } })
|
||||||
|
"
|
||||||
@update:transition-name="
|
@update:transition-name="
|
||||||
(val) => updatePreferences({ transition: { name: val } })
|
(val) => updatePreferences({ transition: { name: val } })
|
||||||
"
|
"
|
||||||
|
|
|
@ -64,6 +64,7 @@ const appContentCompact = defineModel<ContentCompactType>('appContentCompact');
|
||||||
|
|
||||||
const transitionProgress = defineModel<boolean>('transitionProgress');
|
const transitionProgress = defineModel<boolean>('transitionProgress');
|
||||||
const transitionName = defineModel<string>('transitionName');
|
const transitionName = defineModel<string>('transitionName');
|
||||||
|
const transitionLoading = defineModel<boolean>('transitionLoading');
|
||||||
const transitionEnable = defineModel<boolean>('transitionEnable');
|
const transitionEnable = defineModel<boolean>('transitionEnable');
|
||||||
|
|
||||||
const themeColorPrimary = defineModel<string>('themeColorPrimary');
|
const themeColorPrimary = defineModel<string>('themeColorPrimary');
|
||||||
|
@ -209,9 +210,10 @@ function handleReset() {
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
|
|
||||||
<Block :title="$t('preferences.animation')">
|
<Block :title="$t('preferences.animation.name')">
|
||||||
<Animation
|
<Animation
|
||||||
v-model:transition-enable="transitionEnable"
|
v-model:transition-enable="transitionEnable"
|
||||||
|
v-model:transition-loading="transitionLoading"
|
||||||
v-model:transition-name="transitionName"
|
v-model:transition-name="transitionName"
|
||||||
v-model:transition-progress="transitionProgress"
|
v-model:transition-progress="transitionProgress"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -147,7 +147,7 @@ preferences:
|
||||||
full-content-tip: Display only the main content, no menus
|
full-content-tip: Display only the main content, no menus
|
||||||
weak-mode: Color Weak Mode
|
weak-mode: Color Weak Mode
|
||||||
gray-mode: Gray Mode
|
gray-mode: Gray Mode
|
||||||
animation: Animation
|
|
||||||
language: Language
|
language: Language
|
||||||
dynamic-title: Dynamic Title
|
dynamic-title: Dynamic Title
|
||||||
normal: Normal
|
normal: Normal
|
||||||
|
@ -168,8 +168,7 @@ preferences:
|
||||||
breadcrumb-background: background
|
breadcrumb-background: background
|
||||||
breadcrumb-style: Breadcrumb Type
|
breadcrumb-style: Breadcrumb Type
|
||||||
breadcrumb-hide-only-one: Hidden when only one left
|
breadcrumb-hide-only-one: Hidden when only one left
|
||||||
page-progress: Loading progress bar
|
|
||||||
page-transition: Page transition animation
|
|
||||||
copy: Copy Preferences
|
copy: Copy Preferences
|
||||||
copy-success: Copy successful. Please replace in `src/preferences.ts` of the app
|
copy-success: Copy successful. Please replace in `src/preferences.ts` of the app
|
||||||
reset-success: Preferences reset successfully
|
reset-success: Preferences reset successfully
|
||||||
|
@ -180,6 +179,11 @@ preferences:
|
||||||
tabs-icon: Display Tabbar Icon
|
tabs-icon: Display Tabbar Icon
|
||||||
mode: Mode
|
mode: Mode
|
||||||
logo-visible: Display Logo
|
logo-visible: Display Logo
|
||||||
|
animation:
|
||||||
|
name: Animation
|
||||||
|
loading: Page transition loading
|
||||||
|
transition: Page transition animation
|
||||||
|
progress: Page transition progress
|
||||||
theme:
|
theme:
|
||||||
name: Theme
|
name: Theme
|
||||||
builtin: Built-in
|
builtin: Built-in
|
||||||
|
|
|
@ -150,7 +150,7 @@ preferences:
|
||||||
follow-system: 跟随系统
|
follow-system: 跟随系统
|
||||||
weak-mode: 色弱模式
|
weak-mode: 色弱模式
|
||||||
gray-mode: 灰色模式
|
gray-mode: 灰色模式
|
||||||
animation: 动画
|
|
||||||
navigation-menu: 导航菜单
|
navigation-menu: 导航菜单
|
||||||
navigation-style: 导航菜单风格
|
navigation-style: 导航菜单风格
|
||||||
navigation-accordion: 侧边导航菜单手风琴模式
|
navigation-accordion: 侧边导航菜单手风琴模式
|
||||||
|
@ -167,8 +167,6 @@ preferences:
|
||||||
breadcrumb-style: 面包屑风格
|
breadcrumb-style: 面包屑风格
|
||||||
breadcrumb-hide-only-one: 只有一个时隐藏
|
breadcrumb-hide-only-one: 只有一个时隐藏
|
||||||
breadcrumb-background: 背景
|
breadcrumb-background: 背景
|
||||||
page-progress: 加载进度条
|
|
||||||
page-transition: 页面切换动画
|
|
||||||
copy: 复制偏好设置
|
copy: 复制偏好设置
|
||||||
copy-success: 拷贝成功,请在 app 下的 `src/preferences.ts`内进行覆盖
|
copy-success: 拷贝成功,请在 app 下的 `src/preferences.ts`内进行覆盖
|
||||||
reset-success: 重置偏好设置成功
|
reset-success: 重置偏好设置成功
|
||||||
|
@ -179,6 +177,11 @@ preferences:
|
||||||
tabs-icon: 显示标签栏图标
|
tabs-icon: 显示标签栏图标
|
||||||
mode: 模式
|
mode: 模式
|
||||||
logo-visible: 显示 Logo
|
logo-visible: 显示 Logo
|
||||||
|
animation:
|
||||||
|
name: 动画
|
||||||
|
loading: 页面切换 Loading
|
||||||
|
transition: 页面切换动画
|
||||||
|
progress: 页面加载进度条
|
||||||
theme:
|
theme:
|
||||||
name: 主题
|
name: 主题
|
||||||
builtin: 内置主题
|
builtin: 内置主题
|
||||||
|
|
1799
pnpm-lock.yaml
1799
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue