From 24aab5b4bb421a99a273f19d504474e1fef5e442 Mon Sep 17 00:00:00 2001 From: vben Date: Sun, 23 Jun 2024 19:39:44 +0800 Subject: [PATCH] feat: And surface switching loading optimization --- apps/web-antd/src/router/guard.ts | 10 +- apps/web-antd/src/views/dashboard/index.vue | 3 +- cspell.json | 2 + .../lint-configs/eslint-config/package.json | 2 +- internal/tailwind-config/src/index.ts | 3 +- internal/vite-config/package.json | 2 +- .../plugins/inject-app-loading/loading.html | 6 +- package.json | 2 +- .../@core/forward/preferences/src/config.ts | 1 + .../forward/preferences/src/preferences.ts | 6 +- .../@core/forward/preferences/src/types.ts | 2 + .../shared/const/src/storage-manager.test.ts | 130 ++ .../@core/shared/const/src/storage-manager.ts | 118 ++ packages/@core/shared/const/src/types.ts | 17 + .../src/components/layout-content.vue | 22 +- .../src/components/spinner/spinner.vue | 16 +- .../layouts/src/basic/content/content.vue | 58 +- .../src/basic/content/use-content-spinner.ts | 56 + .../preferences/blocks/general/animation.vue | 10 +- .../src/preferences/preferences-widget.vue | 4 + .../src/preferences/preferences.vue | 4 +- packages/locales/src/langs/en-US.yaml | 10 +- packages/locales/src/langs/zh-CN.yaml | 9 +- pnpm-lock.yaml | 1799 ++--------------- 24 files changed, 596 insertions(+), 1696 deletions(-) create mode 100644 packages/@core/shared/const/src/storage-manager.test.ts create mode 100644 packages/@core/shared/const/src/storage-manager.ts create mode 100644 packages/@core/shared/const/src/types.ts create mode 100644 packages/business/layouts/src/basic/content/use-content-spinner.ts diff --git a/apps/web-antd/src/router/guard.ts b/apps/web-antd/src/router/guard.ts index 1cd887b9..68e7e9e0 100644 --- a/apps/web-antd/src/router/guard.ts +++ b/apps/web-antd/src/router/guard.ts @@ -22,17 +22,21 @@ function setupCommonGuard(router: Router) { const loadedPaths = new Set(); 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) { diff --git a/apps/web-antd/src/views/dashboard/index.vue b/apps/web-antd/src/views/dashboard/index.vue index e1008047..2d4e650a 100644 --- a/apps/web-antd/src/views/dashboard/index.vue +++ b/apps/web-antd/src/views/dashboard/index.vue @@ -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' }); diff --git a/cspell.json b/cspell.json index 2b1344be..9396c7af 100644 --- a/cspell.json +++ b/cspell.json @@ -3,7 +3,9 @@ "language": "en,en-US", "allowCompoundWords": true, "words": [ + "clsx", "esno", + "taze", "acmr", "antd", "brotli", diff --git a/internal/lint-configs/eslint-config/package.json b/internal/lint-configs/eslint-config/package.json index dee0e928..88448545 100644 --- a/internal/lint-configs/eslint-config/package.json +++ b/internal/lint-configs/eslint-config/package.json @@ -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", diff --git a/internal/tailwind-config/src/index.ts b/internal/tailwind-config/src/index.ts index 2b2f09c5..59396133 100644 --- a/internal/tailwind-config/src/index.ts +++ b/internal/tailwind-config/src/index.ts @@ -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))', diff --git a/internal/vite-config/package.json b/internal/vite-config/package.json index d5c99c99..89c794ae 100644 --- a/internal/vite-config/package.json +++ b/internal/vite-config/package.json @@ -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", diff --git a/internal/vite-config/src/plugins/inject-app-loading/loading.html b/internal/vite-config/src/plugins/inject-app-loading/loading.html index 0e0802d9..e93b24f6 100644 --- a/internal/vite-config/src/plugins/inject-app-loading/loading.html +++ b/internal/vite-config/src/plugins/inject-app-loading/loading.html @@ -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; } diff --git a/package.json b/package.json index b663d02a..037ecef9 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/@core/forward/preferences/src/config.ts b/packages/@core/forward/preferences/src/config.ts index 792fa59e..bd62e18c 100644 --- a/packages/@core/forward/preferences/src/config.ts +++ b/packages/@core/forward/preferences/src/config.ts @@ -76,6 +76,7 @@ const defaultPreferences: Preferences = { }, transition: { enable: true, + loading: true, name: 'fade-slide', progress: true, }, diff --git a/packages/@core/forward/preferences/src/preferences.ts b/packages/@core/forward/preferences/src/preferences.ts index 7e89b51e..c0ed7f82 100644 --- a/packages/@core/forward/preferences/src/preferences.ts +++ b/packages/@core/forward/preferences/src/preferences.ts @@ -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); + }); } /** diff --git a/packages/@core/forward/preferences/src/types.ts b/packages/@core/forward/preferences/src/types.ts index 55d911d8..9ff29a2a 100644 --- a/packages/@core/forward/preferences/src/types.ts +++ b/packages/@core/forward/preferences/src/types.ts @@ -150,6 +150,8 @@ interface ThemePreferences { interface TransitionPreferences { /** 页面切换动画是否启用 */ enable: boolean; + // /** 是否开启页面加载loading */ + loading: boolean; /** 页面切换动画 */ name: PageTransitionType | string; /** 是否开启页面加载进度动画 */ diff --git a/packages/@core/shared/const/src/storage-manager.test.ts b/packages/@core/shared/const/src/storage-manager.test.ts new file mode 100644 index 00000000..b56d49e0 --- /dev/null +++ b/packages/@core/shared/const/src/storage-manager.test.ts @@ -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(); + }); +}); diff --git a/packages/@core/shared/const/src/storage-manager.ts b/packages/@core/shared/const/src/storage-manager.ts new file mode 100644 index 00000000..d3bfcb41 --- /dev/null +++ b/packages/@core/shared/const/src/storage-manager.ts @@ -0,0 +1,118 @@ +type StorageType = 'localStorage' | 'sessionStorage'; + +interface StorageManagerOptions { + prefix?: string; + storageType?: StorageType; +} + +interface StorageItem { + 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(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 = 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(key: string, value: T, ttl?: number): void { + const fullKey = this.getFullKey(key); + const expiry = ttl ? Date.now() + ttl : undefined; + const item: StorageItem = { expiry, value }; + try { + this.storage.setItem(fullKey, JSON.stringify(item)); + } catch (error) { + console.error(`Error setting item with key "${fullKey}":`, error); + } + } +} + +export { StorageManager }; diff --git a/packages/@core/shared/const/src/types.ts b/packages/@core/shared/const/src/types.ts new file mode 100644 index 00000000..869472f2 --- /dev/null +++ b/packages/@core/shared/const/src/types.ts @@ -0,0 +1,17 @@ +type StorageType = 'localStorage' | 'sessionStorage'; + +interface StorageValue { + data: T; + expiry: null | number; +} + +interface IStorageCache { + clear(): void; + getItem(key: string): T | null; + key(index: number): null | string; + length(): number; + removeItem(key: string): void; + setItem(key: string, value: T, expiryInMinutes?: number): void; +} + +export type { IStorageCache, StorageType, StorageValue }; diff --git a/packages/@core/uikit/layout-ui/src/components/layout-content.vue b/packages/@core/uikit/layout-ui/src/components/layout-content.vue index 146fe7b6..533faab7 100644 --- a/packages/@core/uikit/layout-ui/src/components/layout-content.vue +++ b/packages/@core/uikit/layout-ui/src/components/layout-content.vue @@ -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(), { paddingTop: 16, }); +const domElement = ref(); + +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(); +}); diff --git a/packages/@core/uikit/shadcn-ui/src/components/spinner/spinner.vue b/packages/@core/uikit/shadcn-ui/src/components/spinner/spinner.vue index 5c44e0ea..21dac585 100644 --- a/packages/@core/uikit/shadcn-ui/src/components/spinner/spinner.vue +++ b/packages/@core/uikit/shadcn-ui/src/components/spinner/spinner.vue @@ -20,7 +20,7 @@ defineOptions({ const props = withDefaults(defineProps(), { minLoadingTime: 50, }); -const startTime = ref(0); +// const startTime = ref(0); const showSpinner = ref(false); const renderSpinner = ref(true); const timer = ref>(); @@ -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; + } }