NewLife/cube-front

feat(配置模块): 增加多环境配置实现
笑笑 authored at 2025-06-18 11:29:05
27d1c83
Tree
1 Parent(s) 91f8130
Summary: 17 changed files with 730 additions and 155 deletions.
Added +14 -0
Added +14 -0
Added +16 -0
Modified +8 -0
Added +417 -0
Modified +1 -1
Modified +43 -39
Modified +20 -10
Modified +5 -3
Renamed +31 -15
core/pages/PageLogin.vue → core/pages/PageLogin.tsx
Modified +71 -2
Modified +1 -1
Modified +59 -56
Modified +20 -18
Modified +0 -1
Modified +6 -9
Modified +4 -0
Added +14 -0
diff --git a/configs/config.development.ts b/configs/config.development.ts
new file mode 100644
index 0000000..5e096b2
--- /dev/null
+++ b/configs/config.development.ts
@@ -0,0 +1,14 @@
+import type { EnvConfig } from '../core/configure/types';
+
+/**
+ * 开发环境特定配置
+ */
+export const config: EnvConfig = {
+  base: {
+    env: 'dev',
+    title: '魔方系统',
+  },
+  request: {
+    baseURL: 'https://cube.newlifex.com',
+  },
+};
Added +14 -0
diff --git a/configs/config.production.ts b/configs/config.production.ts
new file mode 100644
index 0000000..6f51329
--- /dev/null
+++ b/configs/config.production.ts
@@ -0,0 +1,14 @@
+import type { EnvConfig } from '../core/configure/types';
+
+/**
+ * 生产环境特定配置
+ */
+export const config: EnvConfig = {
+  base: {
+    env: 'production',
+    title: '魔方系统',
+  },
+  request: {
+    baseURL: 'https://cube.newlifex.com',
+  },
+};
Added +16 -0
diff --git a/configs/config.ts b/configs/config.ts
new file mode 100644
index 0000000..3bf0c94
--- /dev/null
+++ b/configs/config.ts
@@ -0,0 +1,16 @@
+import type { EnvConfig } from '../core/configure/types';
+
+/**
+ * 通用配置(所有环境共享)
+ */
+export const config: EnvConfig = {
+  base: {
+    title: '魔方系统',
+    footer: '版权所有 © 2025',
+  },
+  ui: {
+    theme: {
+      primaryColor: '#1890ff',
+    },
+  },
+};
Modified +8 -0
diff --git a/core/client.d.ts b/core/client.d.ts
index 612a670..62f0688 100644
--- a/core/client.d.ts
+++ b/core/client.d.ts
@@ -15,6 +15,14 @@ declare module 'virtual:cube-front-micro-apps' {
   export default appConfigs;
 }
 
+declare module 'virtual:cube-front-config' {
+  const configData: Record<string, string>;
+  const currentEnv: string;
+  const config: { configData: Record<string, string>; currentEnv: string; };
+  export { configData, currentEnv };
+  export default config;
+}
+
 interface Window {
   router: import('vue-router').Router;
   store: import('pinia').Pinia;
Added +417 -0
diff --git a/core/composables/useProvideInject.ts b/core/composables/useProvideInject.ts
new file mode 100644
index 0000000..19a0eb1
--- /dev/null
+++ b/core/composables/useProvideInject.ts
@@ -0,0 +1,417 @@
+/**
+ * Vue 3 Provide/Inject 安全管理工具
+ *
+ * 这个模块提供了一套完整的 provide/inject 管理解决方案,解决了以下问题:
+ * 1. 检查某个 key 是否已经被注入过
+ * 2. 确保外部注入的值具有更高优先级
+ * 3. 类型安全的注入和提供
+ * 4. 防止重复注入和意外覆盖
+ *
+ * @example 基本使用
+ * ```typescript
+ * // 在应用初始化时
+ * appProvide(app, LayoutKey, MyLayout);
+ *
+ * // 在组件中使用
+ * const Layout = useLayoutRequired(DefaultLayout);
+ * ```
+ *
+ * @example 外部覆盖
+ * ```typescript
+ * // 外部可以覆盖内部提供的值
+ * appProvide(app, LayoutKey, CustomLayout, { override: true });
+ * ```
+ */
+import { inject, type InjectionKey, type App, type Component } from 'vue';
+
+// 全局跟踪已提供的 keys,用于避免重复注入和检查注入状态
+const globalProvidedKeys = new Set<string | symbol>();
+
+/**
+ * 检查某个 key 是否已经被 provide
+ *
+ * 这个函数用于判断某个注入键是否已经在应用中被提供过,
+ * 可以避免重复注入或在需要时进行条件性注入。
+ *
+ * @param key - 要检查的注入键,可以是字符串或 Symbol
+ * @returns 如果键已经被提供则返回 true,否则返回 false
+ *
+ * @example
+ * ```typescript
+ * if (!hasProvided(LayoutKey)) {
+ *   appProvide(app, LayoutKey, DefaultLayout);
+ * }
+ * ```
+ *
+ * @example 在配置函数中使用
+ * ```typescript
+ * const configure = (app, { hasProvided, provide }) => {
+ *   if (hasProvided(LayoutKey)) {
+ *     console.log('Layout already provided');
+ *   } else {
+ *     provide(app, LayoutKey, CustomLayout);
+ *   }
+ * };
+ * ```
+ */
+export function hasProvided(key: string | symbol): boolean {
+  return globalProvidedKeys.has(key);
+}
+
+/**
+ * 安全地使用 inject,提供默认值和类型安全
+ *
+ * 这个函数是 Vue 的 inject 函数的安全包装器,提供了更好的类型安全性
+ * 和错误处理。它可以处理工厂函数作为默认值的情况。
+ *
+ * @template T - 注入值的类型
+ * @param key - 注入键,可以是 InjectionKey、字符串或 Symbol
+ * @param defaultValue - 默认值,当注入失败时使用
+ * @param options - 配置选项
+ * @param options.treatDefaultAsFactory - 是否将默认值当作工厂函数处理
+ * @returns 注入的值或默认值,可能为 undefined
+ *
+ * @example 基本使用
+ * ```typescript
+ * const theme = safeInject(ThemeKey, { color: 'blue' });
+ * ```
+ *
+ * @example 使用工厂函数
+ * ```typescript
+ * const config = safeInject(
+ *   ConfigKey,
+ *   () => ({ mode: 'development' }),
+ *   { treatDefaultAsFactory: true }
+ * );
+ * ```
+ *
+ * @example 在组件中使用
+ * ```typescript
+ * const Layout = safeInject(LayoutKey, DefaultLayout);
+ * if (Layout) {
+ *   // 安全地使用 Layout 组件
+ * }
+ * ```
+ */
+export function safeInject<T>(
+  key: InjectionKey<T> | string | symbol,
+  defaultValue?: T,
+  options?: {
+    treatDefaultAsFactory?: boolean;
+  }
+): T | undefined {
+  const treatDefaultAsFactory = options?.treatDefaultAsFactory ?? false;
+
+  if (treatDefaultAsFactory && typeof defaultValue === 'function') {
+    return inject(key, defaultValue, true);
+  } else {
+    return inject(key, defaultValue);
+  }
+}
+
+/**
+ * 安全地使用 inject,确保返回值不为 undefined
+ *
+ * 这个函数是 safeInject 的严格版本,它保证返回值不会是 undefined。
+ * 如果注入失败且没有有效的默认值,它会抛出错误。
+ *
+ * @template T - 注入值的类型
+ * @param key - 注入键,可以是 InjectionKey、字符串或 Symbol
+ * @param defaultValue - 默认值,当注入失败时使用(必须提供)
+ * @param options - 配置选项
+ * @param options.treatDefaultAsFactory - 是否将默认值当作工厂函数处理
+ * @returns 注入的值或默认值,保证不为 undefined
+ * @throws {Error} 当注入失败且没有有效默认值时抛出错误
+ *
+ * @example 基本使用
+ * ```typescript
+ * // 确保总是有 Layout 组件可用
+ * const Layout = safeInjectRequired(LayoutKey, DefaultLayout);
+ * // Layout 现在保证不为 undefined
+ * ```
+ *
+ * @example 使用工厂函数
+ * ```typescript
+ * const config = safeInjectRequired(
+ *   ConfigKey,
+ *   () => ({ mode: 'production' }),
+ *   { treatDefaultAsFactory: true }
+ * );
+ * ```
+ *
+ * @example 在关键组件中使用
+ * ```typescript
+ * const router = safeInjectRequired(RouterKey, createRouter());
+ * // 如果没有提供 router 且 createRouter() 返回 undefined,会抛出错误
+ * ```
+ */
+export function safeInjectRequired<T>(
+  key: InjectionKey<T> | string | symbol,
+  defaultValue: T,
+  options?: {
+    treatDefaultAsFactory?: boolean;
+  }
+): T {
+  const result = safeInject(key, defaultValue, options);
+
+  if (result === undefined) {
+    throw new Error(`No provider found for injection "${String(key)}" and no valid default value provided`);
+  }
+
+  return result;
+}
+
+/**
+ * 应用级别的安全 provide
+ *
+ * 这个函数用于在应用级别安全地提供依赖注入值。它提供了重复检查、
+ * 覆盖控制和跟踪功能,避免意外的重复注入或未授权的覆盖。
+ *
+ * @param app - Vue 应用实例
+ * @param key - 注入键,可以是字符串或 Symbol
+ * @param value - 要提供的值
+ * @param options - 配置选项
+ * @param options.override - 是否允许覆盖已存在的值,默认为 false
+ * @param options.track - 是否跟踪这个键到全局记录中,默认为 true
+ * @returns 如果成功提供则返回 true,如果被阻止(已存在且不允许覆盖)则返回 false
+ *
+ * @example 基本使用
+ * ```typescript
+ * // 提供默认布局
+ * appProvide(app, LayoutKey, DefaultLayout);
+ * ```
+ *
+ * @example 允许覆盖
+ * ```typescript
+ * // 外部可以覆盖内部提供的布局
+ * appProvide(app, LayoutKey, CustomLayout, { override: true });
+ * ```
+ *
+ * @example 不跟踪的临时提供
+ * ```typescript
+ * // 提供临时值,不记录到全局跟踪中
+ * appProvide(app, 'temp-config', tempConfig, { track: false });
+ * ```
+ *
+ * @example 在配置函数中使用
+ * ```typescript
+ * const configure = (app, { provide }) => {
+ *   // 尝试提供自定义主题
+ *   const success = provide(app, ThemeKey, customTheme);
+ *   if (!success) {
+ *     console.log('Theme already provided by framework');
+ *   }
+ * };
+ * ```
+ */
+export function appProvide(
+  app: App,
+  key: string | symbol,
+  value: unknown,
+  options?: {
+    override?: boolean;
+    track?: boolean;
+  }
+): boolean {
+  const { override = false, track = true } = options || {};
+
+  if (!override && hasProvided(key)) {
+    console.warn(`Key "${String(key)}" has already been provided. Use override: true to replace it.`);
+    return false;
+  }
+
+  app.provide(key, value);
+
+  if (track) {
+    globalProvidedKeys.add(key);
+  }
+
+  return true;
+}
+
+/**
+ * 创建类型安全的 injection key
+ *
+ * 这个函数用于创建类型安全的注入键,返回一个带有类型信息的 Symbol。
+ * 使用 InjectionKey 可以提供更好的 TypeScript 类型推断和编译时检查。
+ *
+ * @template T - 注入值的类型
+ * @param description - 键的描述,用于调试和日志记录
+ * @returns 类型安全的 InjectionKey
+ *
+ * @example 创建基本类型键
+ * ```typescript
+ * const ConfigKey = createInjectionKey<AppConfig>('AppConfig');
+ * ```
+ *
+ * @example 创建组件类型键
+ * ```typescript
+ * const ModalKey = createInjectionKey<Component>('ModalComponent');
+ * ```
+ *
+ * @example 创建复杂类型键
+ * ```typescript
+ * interface ThemeConfig {
+ *   colors: Record<string, string>;
+ *   fonts: string[];
+ * }
+ * const ThemeKey = createInjectionKey<ThemeConfig>('Theme');
+ * ```
+ */
+export function createInjectionKey<T>(description: string): InjectionKey<T> {
+  return Symbol(description) as InjectionKey<T>;
+}
+
+/**
+ * 预定义的 injection keys
+ *
+ * 这些是框架内置的常用注入键,为常见的用例提供了开箱即用的类型安全性。
+ *
+ * @example 使用预定义键
+ * ```typescript
+ * // 在应用中提供布局
+ * appProvide(app, LayoutKey, MyLayout);
+ *
+ * // 在组件中使用
+ * const Layout = useLayoutRequired(DefaultLayout);
+ * ```
+ */
+
+/** 布局组件注入键 - 用于提供和注入应用的主要布局组件 */
+export const LayoutKey = createInjectionKey<Component>('Layout');
+
+/** 主题配置注入键 - 用于提供和注入应用的主题配置 */
+export const ThemeKey = createInjectionKey<Record<string, unknown>>('Theme');
+
+/** 应用配置注入键 - 用于提供和注入应用的全局配置 */
+export const ConfigKey = createInjectionKey<Record<string, unknown>>('Config');
+
+/**
+ * Layout 相关的专用 composable
+ *
+ * 这个函数提供了一个便捷的方式来注入布局组件,支持可选的默认值。
+ * 如果没有提供布局且没有默认值,将返回 undefined。
+ *
+ * @template T - 布局组件的类型,继承自 Component
+ * @param defaultLayout - 可选的默认布局组件
+ * @returns 注入的布局组件或默认布局组件,可能为 undefined
+ *
+ * @example 基本使用
+ * ```typescript
+ * const Layout = useLayout();
+ * if (Layout) {
+ *   // 使用布局组件
+ * }
+ * ```
+ *
+ * @example 使用默认布局
+ * ```typescript
+ * const Layout = useLayout(DefaultLayout);
+ * // Layout 现在是注入的布局或 DefaultLayout
+ * ```
+ *
+ * @example 在组件中条件渲染
+ * ```typescript
+ * <template>
+ *   <component :is="Layout" v-if="Layout">
+ *     <slot />
+ *   </component>
+ *   <div v-else>
+ *     <!-- 无布局时的回退内容 -->
+ *   </div>
+ * </template>
+ *
+ * <script setup>
+ * const Layout = useLayout();
+ * </script>
+ * ```
+ */
+export function useLayout<T extends Component = Component>(defaultLayout?: T): T | undefined {
+  return safeInject(LayoutKey, defaultLayout) as T | undefined;
+}
+
+/**
+ * Layout 相关的专用 composable - 必须返回值版本
+ *
+ * 这个函数是 useLayout 的严格版本,保证总是返回一个布局组件。
+ * 如果没有注入布局且没有提供有效的默认值,将抛出错误。
+ *
+ * @template T - 布局组件的类型,继承自 Component
+ * @param defaultLayout - 必需的默认布局组件
+ * @returns 注入的布局组件或默认布局组件,保证不为 undefined
+ * @throws {Error} 当没有注入布局且默认值无效时抛出错误
+ *
+ * @example 基本使用
+ * ```typescript
+ * const Layout = useLayoutRequired(DefaultLayout);
+ * // Layout 保证不为 undefined
+ * ```
+ *
+ * @example 在组件中安全使用
+ * ```typescript
+ * <template>
+ *   <component :is="Layout">
+ *     <slot />
+ *   </component>
+ * </template>
+ *
+ * <script setup>
+ * import DefaultLayout from './DefaultLayout.vue';
+ *
+ * const Layout = useLayoutRequired(DefaultLayout);
+ * // 可以安全地使用 Layout,无需检查 undefined
+ * </script>
+ * ```
+ *
+ * @example 在路由守卫中使用
+ * ```typescript
+ * router.beforeEach((to, from, next) => {
+ *   try {
+ *     const Layout = useLayoutRequired(DefaultLayout);
+ *     // 确保有可用的布局
+ *     next();
+ *   } catch (error) {
+ *     console.error('No layout available:', error);
+ *     next('/error');
+ *   }
+ * });
+ * ```
+ */
+export function useLayoutRequired<T extends Component = Component>(defaultLayout: T): T {
+  return safeInjectRequired(LayoutKey, defaultLayout) as T;
+}
+
+/**
+ * 获取所有已提供的 keys
+ *
+ * 这个函数返回一个只读数组,包含所有已经通过 appProvide 函数提供的注入键。
+ * 主要用于调试、日志记录或运行时检查。
+ *
+ * @returns 所有已提供的注入键的只读数组
+ *
+ * @example 调试使用
+ * ```typescript
+ * console.log('Provided keys:', getProvidedKeys());
+ * // 输出: ['Layout', 'Theme', 'Config']
+ * ```
+ *
+ * @example 运行时检查
+ * ```typescript
+ * const providedKeys = getProvidedKeys();
+ * const hasTheme = providedKeys.includes(ThemeKey);
+ * if (!hasTheme) {
+ *   console.warn('Theme not provided, using default');
+ * }
+ * ```
+ *
+ * @example 在开发工具中使用
+ * ```typescript
+ * // 开发模式下暴露到全局对象
+ * if (process.env.NODE_ENV === 'development') {
+ *   window.__DEBUG_PROVIDED_KEYS__ = getProvidedKeys;
+ * }
+ * ```
+ */
+export function getProvidedKeys(): readonly (string | symbol)[] {
+  return Array.from(globalProvidedKeys);
+}
Modified +1 -1
diff --git a/core/configure/defaultConfig/index.ts b/core/configure/defaultConfig/index.ts
index d14ffb4..3ec6607 100644
--- a/core/configure/defaultConfig/index.ts
+++ b/core/configure/defaultConfig/index.ts
@@ -6,7 +6,7 @@ export const defaultConfig: CubeFrontConfig = {
     title: '魔方系统',
     logo: '/logo.png',
     footer: '版权所有',
-    env: process.env.NODE_ENV as 'development' | 'production' | 'test',
+    env: import.meta.env as unknown as 'dev' | 'production' | 'test',
   },
   menu: {
     getMenuAxiosConfig: () => {
Modified +43 -39
diff --git a/core/configure/index.ts b/core/configure/index.ts
index 8987403..15fef62 100644
--- a/core/configure/index.ts
+++ b/core/configure/index.ts
@@ -1,56 +1,60 @@
-import { getEnvConfig } from './environments';
-import type { CubeFrontConfig } from './types';
+import type { CubeFrontConfig, EnvConfig } from './types';
 import { defaultConfig } from './defaultConfig';
+import { deepMerge } from '../utils/object';
+import { configData, currentEnv } from 'virtual:cube-front-config';
 
 /**
- * 获取当前环境配置
+ * 解析配置字符串为对象
+ * @param configStr 配置字符串
+ * @returns 配置对象
+ */
+function parseConfigString(configStr: string): EnvConfig {
+  try {
+    // 使用 Function 构造器安全地解析配置对象
+    return new Function('return ' + configStr)();
+  } catch (error) {
+    console.warn('解析配置字符串失败:', error);
+    return {};
+  }
+}
+
+/**
+ * 获取当前配置
+ * 从虚拟模块导入配置数据并进行合并处理
+ * @returns 当前配置
  */
 export function getConfig(): CubeFrontConfig {
-  return getEnvConfig();
+  let result = defaultConfig;
+
+  // 1. 先合并通用配置 config.ts
+  if (configData.general) {
+    const generalConfig = parseConfigString(configData.general);
+    result = deepMerge(result, generalConfig) as CubeFrontConfig;
+  }
+
+  // 2. 再合并环境特定配置 config.${env}.ts 或 ${env}.ts
+  if (configData[currentEnv]) {
+    const envConfig = parseConfigString(configData[currentEnv]);
+    result = deepMerge(result, envConfig) as CubeFrontConfig;
+  }
+
+  return result;
 }
 
 /**
  * 合并配置
+ * @param customConfig 自定义配置
+ * @returns 合并后的配置
  */
-export function mergeConfig(config?: Partial<CubeFrontConfig>): CubeFrontConfig {
-  // 深度合并配置
-  return {
-    ...defaultConfig,
-    ...config,
-    base: {
-      ...defaultConfig.base,
-      ...(config?.base || {}),
-    },
-    ui: {
-      ...defaultConfig.ui,
-      ...(config?.ui || {}),
-      layout: {
-        ...defaultConfig.ui.layout,
-        ...(config?.ui?.layout || {}),
-      },
-      theme: {
-        ...defaultConfig.ui.theme,
-        ...(config?.ui?.theme || {}),
-      },
-    },
-    request: {
-      ...defaultConfig.request,
-      ...(config?.request || {}),
-    },
-    auth: {
-      ...defaultConfig.auth,
-      ...(config?.auth || {}),
-      reLoginParams: {
-        ...(defaultConfig.auth.reLoginParams || {}),
-        ...(config?.auth?.reLoginParams || {}),
-      },
-    },
-  };
+export function mergeConfig(customConfig: EnvConfig): CubeFrontConfig {
+  const baseConfig = getConfig();
+  return deepMerge(baseConfig, customConfig) as CubeFrontConfig;
 }
 
 // 导出配置类型
 export type {
   CubeFrontConfig,
+  EnvConfig,
   BaseConfig, // 替代 AppConfig
   AuthConfig, // 替代 LoginConfig
   RequestConfig, // 替代 RequestConfig
@@ -58,4 +62,4 @@ export type {
 } from './types';
 
 // 导出默认配置
-export { defaultConfig } from './defaultConfig';
+export { defaultConfig } from './defaultConfig';
\ No newline at end of file
Modified +20 -10
diff --git a/core/configure/types.d.ts b/core/configure/types.d.ts
index 9f482d8..5b0e770 100644
--- a/core/configure/types.d.ts
+++ b/core/configure/types.d.ts
@@ -5,15 +5,15 @@ export interface BaseConfig {
   title: string;
   logo?: string;
   footer?: string;
-  env?: 'development' | 'production' | 'test';
+  env?: 'dev' | 'production' | 'test';
 }
 
 // 菜单相关配置
 export interface MenuConfig {
   getMenuAxiosConfig:
-    | AxiosRequestConfig
-    | (() => AxiosRequestConfig)
-    | (() => Promise<AxiosRequestConfig>);
+  | AxiosRequestConfig
+  | (() => AxiosRequestConfig)
+  | (() => Promise<AxiosRequestConfig>);
   isMenuTree: boolean;
   dataKey: string;
   idField: string;
@@ -29,9 +29,9 @@ export interface MenuConfig {
 // 用户相关配置
 export interface UserConfig {
   getUserInfoAxiosConfig:
-    | AxiosRequestConfig
-    | (() => AxiosRequestConfig)
-    | (() => Promise<AxiosRequestConfig>);
+  | AxiosRequestConfig
+  | (() => AxiosRequestConfig)
+  | (() => Promise<AxiosRequestConfig>);
 }
 
 // UI相关配置
@@ -89,9 +89,9 @@ export interface AuthConfig {
   pageTitle?: string;
   background?: string;
   logoutAxiosConfig?:
-    | AxiosRequestConfig
-    | (() => AxiosRequestConfig)
-    | (() => Promise<AxiosRequestConfig>);
+  | AxiosRequestConfig
+  | (() => AxiosRequestConfig)
+  | (() => Promise<AxiosRequestConfig>);
   reLoginParams?: {
     titleIntlCode?: string;
     titleIntlDefault?: string;
@@ -122,3 +122,13 @@ export interface CubeFrontConfig {
   menu: MenuConfig;
   user: UserConfig;
 }
+
+// 环境配置类型 - 所有字段都是可选的深度部分类型
+export type EnvConfig = {
+  base?: Partial<BaseConfig>;
+  ui?: Partial<UIConfig>;
+  request?: Partial<RequestConfig>;
+  auth?: Partial<AuthConfig>;
+  menu?: Partial<MenuConfig>;
+  user?: Partial<UserConfig>;
+};
Modified +5 -3
diff --git a/core/layouts/RootLayout.vue b/core/layouts/RootLayout.vue
index 74614d7..3f0dc0f 100644
--- a/core/layouts/RootLayout.vue
+++ b/core/layouts/RootLayout.vue
@@ -1,11 +1,13 @@
 <script setup lang="ts">
-import { onBeforeMount, onMounted, watch, computed, inject } from 'vue';
+import { onBeforeMount, onMounted, watch, computed } from 'vue';
 import { useRouter, useRoute } from 'vue-router';
 // import NotFound from '../pages/404.vue'
 // import Loading from '../pages/loading.vue'
 import { useUserStore } from '../stores/user';
 import { useMenuStore } from '../stores/menu';
 import { getUrlHashToken } from '../utils/token';
+import { useLayoutRequired } from '../composables/useProvideInject';
+import DefaultMainLayout from './MainLayout/index.vue';
 
 const router = useRouter();
 const route = useRoute();
@@ -13,8 +15,8 @@ const userStore = useUserStore();
 const menuStore = useMenuStore();
 console.log('routes', router.getRoutes());
 
-// 通过inject获取MainLayout组件
-const MainLayout = inject('Layout');
+// 通过useLayoutRequired获取MainLayout组件,提供默认值作为后备
+const MainLayout = useLayoutRequired(DefaultMainLayout);
 
 // 使用计算属性获取响应式的 meta 对象
 const meta = computed(() => route.meta);
Renamed +31 -15
core/pages/PageLogin.vue → core/pages/PageLogin.tsx
diff --git a/core/pages/PageLogin.vue b/core/pages/PageLogin.tsx
similarity index 66%
rename from core/pages/PageLogin.vue
rename to core/pages/PageLogin.tsx
index d006469..6d9db47 100644
--- a/core/pages/PageLogin.vue
+++ b/core/pages/PageLogin.tsx
@@ -1,4 +1,7 @@
-<script lang="ts" setup>
+import { defineComponent, onMounted } from 'vue';
+import { useRoute } from 'vue-router';
+import { getConfig } from '../configure';
+
 /**
  * 处理OAuth认证重定向的登录组件。
  *
@@ -8,20 +11,33 @@
  *
  * 由于立即重定向到配置中定义的外部认证服务,该组件不渲染任何内容。
  *
- * @returns {null} - 由于立即重定向,组件不渲染任何内容
+ * @returns {JSX.Element} - 显示加载中的组件
  */
-import { useRoute } from 'vue-router';
-import { getConfig } from '../configure';
+export default defineComponent({
+  name: 'PageLogin',
+  setup() {
+    const route = useRoute();
+
+    onMounted(() => {
+
+      const config = getConfig();
+      console.log('config', config);
+
+      const oauthUrl = config.auth.oauthUrl;
+      const baseURL = config.request.baseURL;
+
+      if (!baseURL) {
+        throw new Error('config.request.baseURL is not defined');
+      }
 
-const route = useRoute();
-const config = getConfig();
-const oauthUrl = config.auth.oauthUrl;
-const baseURL = config.request.baseURL;
-console.log('login', route);
-const redirect = `${location.origin}${route.query.redirect || '/'}`;
-location.href = `${baseURL}${oauthUrl}${encodeURIComponent(redirect)}`;
-</script>
+      console.log('login', route);
+      debugger;
+      const redirect = `${location.origin}${route.query.redirect || '/'}`;
+      location.href = `${baseURL}${oauthUrl}${encodeURIComponent(redirect)}`;
+    });
 
-<template>
-  <div>Loading...</div>
-</template>
+    return () => (
+      <div>Loading...</div>
+    );
+  }
+});
Modified +71 -2
diff --git a/core/plugin/index.ts b/core/plugin/index.ts
index a9147d7..20b1b29 100644
--- a/core/plugin/index.ts
+++ b/core/plugin/index.ts
@@ -10,9 +10,9 @@ export default function vitePluginCubeFront() {
   const resolvedVirtualModuleIdPrefix = '\0' + virtualModuleIdPrefix;
 
   // 虚拟模块名称常量
-
   const appName = 'app';
   const microAppsName = 'micro-apps';
+  const configName = 'config';
 
   /** 配置信息 */
   let config: ResolvedConfig & { routes: ConfigRoute[]; };
@@ -100,5 +100,74 @@ export default microAppConfigs
     },
   };
 
-  return [viteCubeApp, viteCubeAppNames];
+  // 配置虚拟模块插件
+  const viteCubeConfig: PluginOption = {
+    name: `vite:${virtualModuleNamePrefix}-${configName}`,
+    enforce: 'post',
+    configResolved: (cfg) => {
+      config = cfg as ResolvedConfig & { routes: ConfigRoute[]; };
+    },
+    resolveId(id: string) {
+      if (id === virtualModuleIdPrefix + configName) {
+        return resolvedVirtualModuleIdPrefix + configName;
+      }
+    },
+    load(id: string) {
+      if (id === resolvedVirtualModuleIdPrefix + configName) {
+        try {
+          const env = config.mode || 'dev';
+          const configsPath = path.resolve(config.root, 'configs');
+
+          // 查找所有配置文件
+          const configFiles: Record<string, string> = {};
+
+          // 查找通用配置文件 config.ts
+          const generalConfigPath = path.resolve(configsPath, 'config.ts');
+          if (fs.existsSync(generalConfigPath)) {
+            try {
+              const content = fs.readFileSync(generalConfigPath, 'utf-8');
+              const configMatch = content.match(/export\s+const\s+(\w+)\s*:\s*[^=]*=\s*({[\s\S]*?});/);
+              if (configMatch) {
+                configFiles.general = configMatch[2];
+              }
+            } catch (error) {
+              console.warn(`无法读取配置文件 config.ts:`, error);
+              throw new Error(`配置文件 config.ts 不存在或无法读取`);
+            }
+          }
+
+          // 查找环境特定配置文件 config.${env}.ts
+          const envConfigPath = path.resolve(configsPath, `config.${env}.ts`);
+          if (fs.existsSync(envConfigPath)) {
+            try {
+              const content = fs.readFileSync(envConfigPath, 'utf-8');
+              const configMatch = content.match(/export\s+const\s+(\w+)\s*:\s*[^=]*=\s*({[\s\S]*?});/);
+              if (configMatch) {
+                configFiles[env] = configMatch[2];
+              }
+            } catch (error) {
+              console.warn(`无法读取环境配置文件 config.${env}.ts:`, error);
+              throw new Error(`环境配置文件 config.${env}.ts 不存在或无法读取`);
+            }
+          }
+
+          return `
+// 导出配置对象和当前环境
+export const configData = ${JSON.stringify(configFiles)};
+export const currentEnv = '${env}';
+export default { configData, currentEnv };
+`;
+        } catch (error) {
+          console.error('Failed to load config', error);
+          return `
+export const configData = {};
+export const currentEnv = 'dev';
+export default { configData, currentEnv };
+`;
+        }
+      }
+    },
+  };
+
+  return [viteCubeApp, viteCubeAppNames, viteCubeConfig];
 }
Modified +1 -1
diff --git a/core/routes/index.ts b/core/routes/index.ts
index 1ff8c1c..42b3b70 100644
--- a/core/routes/index.ts
+++ b/core/routes/index.ts
@@ -17,7 +17,7 @@ const routes: ConfigRoute[] = [
       auth: false,
       layout: false,
     },
-    component: () => import('../pages/PageLogin.vue'),
+    component: () => import('../pages/PageLogin'),
   },
   {
     path: '/unauthorized',
Modified +59 -56
diff --git a/core/stores/menu.ts b/core/stores/menu.ts
index dbd9746..3c6faa5 100644
--- a/core/stores/menu.ts
+++ b/core/stores/menu.ts
@@ -1,6 +1,6 @@
 import { defineStore } from 'pinia';
 import { getConfig } from 'cube-front/core/configure';
-import { getDataByKey, isPromise } from '../utils/common';
+import { getDataByKey } from '../utils/common';
 import { type AxiosRequestConfig } from 'axios';
 import request from '../utils/request';
 
@@ -30,7 +30,7 @@ export interface TreeMenuItem {
 
 /** 平铺菜单转换成树形菜单 */
 export function convertFlatMenuToTreeMenu(flatMenu: FlatMenuItem[]): TreeMenuItem[] {
-  const menuMap: { [id: string]: TreeMenuItem } = {};
+  const menuMap: { [id: string]: TreeMenuItem; } = {};
 
   // Create a map of menu items using their IDs as keys
   flatMenu.forEach((item) => {
@@ -81,27 +81,24 @@ export function convertTreeMenuToFlatMenu(
 }
 
 /** 将菜单数据按照配置的字段名称进行转换,支持平铺数据和树形数据 */
-const covertMenu = (list: never[]) => {
+const covertMenu = (list: Record<string, unknown>[]): TreeMenuItem[] => {
   const menuConfig = getConfig().menu;
-  const newList = list.map((item) => {
-    const newItem: TreeMenuItem = {
-      ...(item as TreeMenuItem),
-      id: item[menuConfig.idField],
-      parentId: item[menuConfig.parentField],
-      name: item[menuConfig.nameField],
-      path: item[menuConfig.pathField],
-      title: item[menuConfig.titleField],
-      icon: item[menuConfig.iconField],
-      sort: item[menuConfig.sortField],
-      children: item[menuConfig.childrenField]
-        ? covertMenu(item[menuConfig.childrenField])
-        : undefined,
-    };
 
-    return newItem;
-  });
+  const convertItem = (item: Record<string, unknown>): TreeMenuItem => {
+    const children = item[menuConfig.childrenField] as Record<string, unknown>[] | undefined;
+    return {
+      id: item[menuConfig.idField] as string,
+      parentId: item[menuConfig.parentField] as string | null,
+      name: item[menuConfig.nameField] as string,
+      path: item[menuConfig.pathField] as string,
+      title: item[menuConfig.titleField] as string,
+      icon: item[menuConfig.iconField] as string,
+      sort: item[menuConfig.sortField] as number,
+      children: children ? covertMenu(children) : undefined,
+    };
+  };
 
-  return newList;
+  return list.map(convertItem);
 };
 
 const state: {
@@ -158,46 +155,52 @@ export const useMenuStore = defineStore('menu', {
       this.loading = true;
       // 如果没有菜单信息,则获取菜单信息
       if (!this.hasMenus) {
-        const menuConfig = getConfig().menu;
-        const getMenuAxiosConfig = menuConfig.getMenuAxiosConfig;
-
-        // 处理不同类型的 getMenuAxiosConfig
-        const processMenuRequestAsync = async (config: AxiosRequestConfig) => {
-          try {
-            const res = await request(config);
-            // 增加延时,避免下面的赋值触发更新再次请求
-            setTimeout(() => {
-              this.loading = false;
-            }, 1000);
-
-            const data = getDataByKey(res as never, menuConfig.dataKey);
-
-            if (menuConfig.isMenuTree) {
-              this.setTreeMenus(covertMenu(data as never[]));
+        try {
+          const config = getConfig();
+          const menuConfig = config.menu;
+          const getMenuAxiosConfig = menuConfig.getMenuAxiosConfig;
+
+          // 处理不同类型的 getMenuAxiosConfig
+          const processMenuRequestAsync = async (requestConfig: AxiosRequestConfig) => {
+            try {
+              const res = await request(requestConfig);
+              const data = getDataByKey(res as never, menuConfig.dataKey);
+
+              if (menuConfig.isMenuTree) {
+                const convertedData = covertMenu(data as Record<string, unknown>[]);
+                this.setTreeMenus(convertedData);
+              } else {
+                const convertedData = covertMenu(data as Record<string, unknown>[]);
+                // 转换树形菜单为平铺菜单
+                this.setFlatMenus(convertTreeMenuToFlatMenu(convertedData));
+              }
+              this.setActiveMenuByPath(window.location.pathname);
+            } catch (error) {
+              console.error('获取菜单失败:', error);
+              throw error;
+            }
+          };
+
+          // 根据类型执行不同操作
+          if (typeof getMenuAxiosConfig === 'function') {
+            const configResult = getMenuAxiosConfig();
+            if (configResult instanceof Promise) {
+              // 处理返回 Promise 类型的配置
+              await processMenuRequestAsync(await configResult);
             } else {
-              this.setFlatMenus(covertMenu(data as never[]));
+              await processMenuRequestAsync(configResult);
             }
-            this.setActiveMenuByPath(window.location.pathname);
-          } catch (error) {
-            // 如果报错,增加延时,避免下面的赋值触发更新再次请求
-            setTimeout(() => {
-              this.loading = false;
-            }, 1000);
-          }
-        };
-
-        // 根据类型执行不同操作
-        if (typeof getMenuAxiosConfig === 'function') {
-          const config = getMenuAxiosConfig();
-          if (config instanceof Promise) {
-            // 处理返回 Promise 类型的配置
-            await processMenuRequestAsync(await config);
           } else {
-            await processMenuRequestAsync(config);
+            // 处理直接配置对象
+            await processMenuRequestAsync(getMenuAxiosConfig);
           }
-        } else {
-          // 处理直接配置对象
-          await processMenuRequestAsync(getMenuAxiosConfig);
+        } catch (error) {
+          console.error('加载菜单配置失败:', error);
+        } finally {
+          // 增加延时,避免下面的赋值触发更新再次请求
+          setTimeout(() => {
+            this.loading = false;
+          }, 1000);
         }
       } else {
         this.loading = false;
Modified +20 -18
diff --git a/core/stores/user.ts b/core/stores/user.ts
index f891b8c..8b5a057 100644
--- a/core/stores/user.ts
+++ b/core/stores/user.ts
@@ -1,6 +1,6 @@
 import { defineStore } from 'pinia';
 import { getConfig } from 'cube-front/core/configure';
-import axios, { type AxiosRequestConfig, type AxiosResponse } from 'axios';
+import { type AxiosRequestConfig, type AxiosResponse } from 'axios';
 import request from '../utils/request';
 import { removeAccessToken } from '../utils/token';
 
@@ -41,18 +41,19 @@ export const useUserStore = defineStore('user', {
     async logout() {
       try {
         // 获取认证配置
-        const authConfig = getConfig().auth;
+        const config = getConfig();
+        const authConfig = config.auth;
         // 默认登出请求配置
         let axiosConfig: AxiosRequestConfig;
 
         // 如果有专门的登出配置,则使用该配置
         if (authConfig.logoutAxiosConfig) {
           if (typeof authConfig.logoutAxiosConfig === 'function') {
-            const config = authConfig.logoutAxiosConfig();
-            if (config instanceof Promise) {
-              axiosConfig = await authConfig.logoutAxiosConfig();
+            const configResult = authConfig.logoutAxiosConfig();
+            if (configResult instanceof Promise) {
+              axiosConfig = await configResult;
             } else {
-              axiosConfig = config;
+              axiosConfig = configResult;
             }
           } else {
             axiosConfig = authConfig.logoutAxiosConfig;
@@ -81,22 +82,23 @@ export const useUserStore = defineStore('user', {
       this.loading = true;
       // 如果没有用户信息,则获取获取用户信息、菜单信息
       if (!this.hasUserInfo) {
-        const userConfig = getConfig().user;
-        const getUserInfoAxiosConfig = userConfig.getUserInfoAxiosConfig;
-        let axiosConfig: AxiosRequestConfig;
+        try {
+          const config = getConfig();
+          const userConfig = config.user;
+          const getUserInfoAxiosConfig = userConfig.getUserInfoAxiosConfig;
+          let axiosConfig: AxiosRequestConfig;
 
-        if (typeof getUserInfoAxiosConfig === 'function') {
-          const config = getUserInfoAxiosConfig();
-          if (config instanceof Promise) {
-            axiosConfig = await getUserInfoAxiosConfig();
+          if (typeof getUserInfoAxiosConfig === 'function') {
+            const configResult = getUserInfoAxiosConfig();
+            if (configResult instanceof Promise) {
+              axiosConfig = await configResult;
+            } else {
+              axiosConfig = configResult;
+            }
           } else {
-            axiosConfig = config;
+            axiosConfig = getUserInfoAxiosConfig;
           }
-        } else {
-          axiosConfig = getUserInfoAxiosConfig;
-        }
 
-        try {
           const response: AxiosResponse<Partial<UserInfo>> = await request<
             Partial<UserInfo>,
             AxiosResponse<Partial<UserInfo>>
Modified +0 -1
diff --git a/core/utils/common.ts b/core/utils/common.ts
index 4279c02..8be515b 100644
--- a/core/utils/common.ts
+++ b/core/utils/common.ts
@@ -1,5 +1,4 @@
 import notification from '../components/Notification.ts';
-import { useTimestamp } from '@vueuse/core';
 
 /**
  * 生成指定长度的随机十六进制字符串
Modified +6 -9
diff --git a/core/utils/request.ts b/core/utils/request.ts
index c4c1111..6ab8959 100644
--- a/core/utils/request.ts
+++ b/core/utils/request.ts
@@ -4,8 +4,6 @@
  */
 import axios, {
   type AxiosError,
-  type AxiosInterceptorManager,
-  type AxiosRequestConfig,
   type AxiosRequestHeaders,
   type AxiosResponse,
   type InternalAxiosRequestConfig,
@@ -83,7 +81,7 @@ const INDEX_ROUTE_PATH = '/';
  * @param {Object} options - 配置选项
  * @param {string} options.loginPageUrl - 可选的登录页URL
  */
-export function redirectToLogin({ loginPageUrl }: { loginPageUrl?: string } = {}) {
+export function redirectToLogin({ loginPageUrl }: { loginPageUrl?: string; } = {}) {
   removeAccessToken();
   removeAllCookie();
 
@@ -91,6 +89,8 @@ export function redirectToLogin({ loginPageUrl }: { loginPageUrl?: string } = {}
     request: { baseURL: API_HOST },
     auth: { oauthUrl },
   } = getConfig();
+  console.log('redirectToLogin', API_HOST);
+  debugger;
   const LOGIN_URL = loginPageUrl || `${API_HOST}${oauthUrl}`;
 
   const sessionData = getSession('redirectUrl');
@@ -235,8 +235,8 @@ function authIntercept(status: number, config: InternalAxiosRequestConfig) {
 function handleResponseError(error: AxiosError) {
   // 移除debugger语句,优化错误日志记录
   console.error('Request error:', error.message, error.config?.url);
-
-  const { response, config, code } = error;
+  debugger;
+  const { response, code } = error;
 
   // 如果response为空,直接返回
   if (!response) {
@@ -308,7 +308,7 @@ function handleResponseError(error: AxiosError) {
  * @returns {any} - 处理后的响应数据,默认返回data
  */
 function handleResponseSuccess(response: AxiosResponse) {
-  const { status, data, config } = response;
+  const { data, config } = response;
 
   if (config.responseType === 'text' && typeof data !== 'string') {
     return JSON.stringify(data);
@@ -380,14 +380,11 @@ function handleRequestConfig(config: InternalAxiosRequestConfig) {
  * @returns {Promise} - 处理后的Promise
  */
 async function handleRequestError(error: AxiosError) {
-  const { config } = error;
-
   console.error('Request processing error:', error.message);
 
   return Promise.reject(error);
 }
 
-const requestHandlers = [handleRequestConfig, handleRequestError];
 cubeAxios.interceptors.request.use(handleRequestConfig, handleRequestError);
 
 // 导出配置好的axios实例
Modified +4 -0
diff --git a/tsconfig.app.json b/tsconfig.app.json
index af02613..e2e3c70 100644
--- a/tsconfig.app.json
+++ b/tsconfig.app.json
@@ -4,6 +4,7 @@
     "env.d.ts",
     "./**/*.vue",
     "./**/*.ts",
+    "./**/*.tsx",
     "./**/*.d.ts",
     "configs/microAppConfig.js",
     "core/pages/Login.ts"
@@ -12,6 +13,9 @@
     "src/**/__tests__/*"
   ],
   "compilerOptions": {
+    "jsx": "preserve",
+    "jsxFactory": "h",
+    "jsxFragmentFactory": "Fragment",
     "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
     "types": [
       "element-plus/global"