Browse Source

feat: And surface switching loading optimization

vben 10 months ago
parent
commit
24aab5b4bb

+ 7 - 3
apps/web-antd/src/router/guard.ts

@@ -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) {

+ 1 - 2
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' });
 
 <template>
   <div>dashboard</div>
-  <!-- <Dashboard :card-list="cardList" :chart-tabs="chartTabs" /> -->
 </template>

+ 2 - 0
cspell.json

@@ -3,7 +3,9 @@
   "language": "en,en-US",
   "allowCompoundWords": true,
   "words": [
+    "clsx",
     "esno",
+    "taze",
     "acmr",
     "antd",
     "brotli",

+ 1 - 1
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",

+ 1 - 2
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))',

+ 1 - 1
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",

+ 3 - 3
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;
   }

+ 1 - 1
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",

+ 1 - 0
packages/@core/forward/preferences/src/config.ts

@@ -76,6 +76,7 @@ const defaultPreferences: Preferences = {
   },
   transition: {
     enable: true,
+    loading: true,
     name: 'fade-slide',
     progress: true,
   },

+ 3 - 3
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);
+    });
   }
 
   /**

+ 2 - 0
packages/@core/forward/preferences/src/types.ts

@@ -150,6 +150,8 @@ interface ThemePreferences {
 interface TransitionPreferences {
   /** 页面切换动画是否启用 */
   enable: boolean;
+  // /** 是否开启页面加载loading */
+  loading: boolean;
   /** 页面切换动画 */
   name: PageTransitionType | string;
   /** 是否开启页面加载进度动画 */

+ 130 - 0
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();
+  });
+});

+ 118 - 0
packages/@core/shared/const/src/storage-manager.ts

@@ -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 };

+ 17 - 0
packages/@core/shared/const/src/types.ts

@@ -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 };

+ 20 - 2
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<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>

+ 8 - 6
packages/@core/uikit/shadcn-ui/src/components/spinner/spinner.vue

@@ -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();
+
+    // startTime.value = performance.now();
     timer.value = setTimeout(() => {
-      const loadingTime = performance.now() - startTime.value;
+      // const loadingTime = performance.now() - startTime.value;
 
-      showSpinner.value = loadingTime > props.minLoadingTime;
+      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,
     }"

+ 37 - 21
packages/business/layouts/src/basic/content/content.vue

@@ -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>

+ 56 - 0
packages/business/layouts/src/basic/content/use-content-spinner.ts

@@ -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 };

+ 7 - 3
packages/business/universal-ui/src/preferences/blocks/general/animation.vue

@@ -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"

+ 4 - 0
packages/business/universal-ui/src/preferences/preferences-widget.vue

@@ -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 } })
     "

+ 3 - 1
packages/business/universal-ui/src/preferences/preferences.vue

@@ -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"
               />

+ 7 - 3
packages/locales/src/langs/en-US.yaml

@@ -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

+ 6 - 3
packages/locales/src/langs/zh-CN.yaml

@@ -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: 内置主题

File diff suppressed because it is too large
+ 16 - 493
pnpm-lock.yaml


Some files were not shown because too many files changed in this diff