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',
+ },
+};
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',
+ },
+};
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',
+ },
+ },
+};
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;
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);
+}
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: () => {
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
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>;
+};
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);
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>
+ );
+ }
+});
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];
}
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',
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;
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>>
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';
/**
* 生成指定长度的随机十六进制字符串
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实例
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"