From c00509e51b57a26596923a97975bc2d76852b6b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Sat, 4 Oct 2025 16:08:35 +0800 Subject: [PATCH] =?UTF-8?q?feat(config):=20=E5=AE=9E=E7=8E=B0=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=E9=85=8D=E7=BD=AE=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 ApiUrl 和 theme 配置字段支持 - 新增根据 code 查询应用参数的 API 接口- 实现配置 store 管理网站配置数据 - 支持 API 地址优先级: 后台配置 > 本地配置- 配置数据自动存储到 localStorage 实现持久化- 添加配置管理说明文档 CONFIG_MANAGEMENT.md- 优化请求工具支持动态 API 地址切换- 移除无用的 openNew 工具函数引入 - 实现主题配置自动加载和存储功能 --- CONFIG_MANAGEMENT.md | 149 +++++++++++++++++++++ src/api/cms/cmsWebsiteField/index.ts | 13 ++ src/api/cms/cmsWebsiteField/model/index.ts | 6 +- src/layout/components/header-tools.vue | 13 +- src/store/modules/template.ts | 119 ++++++++++++++++ src/utils/enhanced-request.ts | 87 ++++++------ src/utils/request.ts | 20 ++- src/views/passport/login/index.vue | 1 - 8 files changed, 360 insertions(+), 48 deletions(-) create mode 100644 CONFIG_MANAGEMENT.md create mode 100644 src/store/modules/template.ts diff --git a/CONFIG_MANAGEMENT.md b/CONFIG_MANAGEMENT.md new file mode 100644 index 0000000..9f52d35 --- /dev/null +++ b/CONFIG_MANAGEMENT.md @@ -0,0 +1,149 @@ +# 后端配置管理说明 + +## 概述 + +本项目实现了与小程序端一致的配置管理机制,后端管理端现在也支持优先使用后台配置的API地址。 + +## 核心文件 + +1. `src/store/modules/config.ts` - 配置状态管理模块 +2. `src/composables/useConfig.ts` - 配置初始化组合式函数 +3. `src/views/system/config-demo.vue` - 配置演示页面 +4. `src/utils/request.ts` - 更新后的请求工具,支持API地址优先级 + +## 功能特性 + +### 1. API地址优先级 +- 优先使用后台配置的API地址(存储在config中的ApiUrl字段) +- 如果未配置,则回退使用本地配置的API_BASE_URL + +### 2. 配置存储 +- 使用Pinia进行状态管理 +- 同时存储在localStorage中,支持持久化 +- 提供获取、设置、刷新、清除配置的方法 + +### 3. 自动初始化 +- 应用启动时自动加载配置 +- 支持从缓存中快速恢复配置 + +## 使用方法 + +### 在组件中使用配置store + +```typescript +import { useConfigStore } from '@/store/modules/config'; + +export default defineComponent({ + setup() { + const configStore = useConfigStore(); + + // 获取配置 + const config = configStore.config; + + // 获取API地址 + const apiUrl = configStore.getApiUrl; + + // 获取网站名称 + const siteName = configStore.getSiteName; + + // 刷新配置 + const refreshConfig = async () => { + try { + await configStore.refetchConfig(); + } catch (error) { + console.error('刷新配置失败:', error); + } + }; + + return { + config, + apiUrl, + siteName, + refreshConfig + }; + } +}); +``` + +### 在组合式API中使用 + +```typescript +import { useConfigStore } from '@/store/modules/config'; + +export default defineComponent({ + setup() { + const configStore = useConfigStore(); + + // 监听配置变化 + watch(() => configStore.config, (newConfig) => { + console.log('配置已更新:', newConfig); + }); + + return {}; + } +}); +``` + +### 在请求工具中使用 + +请求工具会自动优先使用配置中的API地址: + +```typescript +// src/utils/request.ts +const getBaseUrl = (): string => { + // 尝试从配置store获取后台配置的API地址 + try { + const configStore = useConfigStore(); + if (configStore.config && configStore.config.ApiUrl) { + return configStore.config.ApiUrl; + } + + // 回退到localStorage + const configStr = localStorage.getItem('config'); + if (configStr) { + const config = typeof configStr === 'string' ? JSON.parse(configStr) : configStr; + if (config && config.ApiUrl) { + return config.ApiUrl; + } + } + } catch (error) { + console.warn('获取后台配置API地址失败:', error); + } + + // 最后回退到本地配置 + return API_BASE_URL; +}; +``` + +## 配置字段说明 + +配置对象包含以下字段: + +- `siteName` - 网站名称 +- `siteLogo` - 网站Logo +- `domain` - 域名 +- `icpNo` - ICP备案号 +- `copyright` - 版权信息 +- `loginBgImg` - 登录背景图 +- `address` - 联系地址 +- `tel` - 联系电话 +- `kefu2` - 客服2 +- `kefu1` - 客服1 +- `email` - 邮箱 +- `loginTitle` - 登录标题 +- `sysLogo` - 系统Logo +- `ApiUrl` - API地址(新增) +- `theme` - 主题(新增) + +## 菜单配置 + +配置演示页面已添加到系统管理菜单中: +- 路径:`/system/config-demo` +- 组件:`/src/views/system/config-demo.vue` +- 图标:`ExperimentOutlined` + +## 注意事项 + +1. 配置数据会自动存储在localStorage中,键名为`config` +2. 主题配置会存储在localStorage中,键名为`user_theme` +3. 如果需要自定义配置字段,需要更新`src/api/cms/cmsWebsiteField/model/index.ts`中的Config接口定义 \ No newline at end of file diff --git a/src/api/cms/cmsWebsiteField/index.ts b/src/api/cms/cmsWebsiteField/index.ts index 6e83015..8329889 100644 --- a/src/api/cms/cmsWebsiteField/index.ts +++ b/src/api/cms/cmsWebsiteField/index.ts @@ -105,6 +105,19 @@ export async function getCmsWebsiteField(id: number) { return Promise.reject(new Error(res.data.message)); } +/** + * 根据code查询应用参数 + */ +export async function getCmsWebsiteFieldByCode(code: string) { + const res = await request.get>( + MODULES_API_URL + '/cms/cms-website-field/getByCode/' + code + ); + if (res.data.code === 0 && res.data.data) { + return res.data.data; + } + return Promise.reject(new Error(res.data.message)); +} + /** * 恢复项目参数 */ diff --git a/src/api/cms/cmsWebsiteField/model/index.ts b/src/api/cms/cmsWebsiteField/model/index.ts index faeeef1..17bda1d 100644 --- a/src/api/cms/cmsWebsiteField/model/index.ts +++ b/src/api/cms/cmsWebsiteField/model/index.ts @@ -58,4 +58,8 @@ export interface Config { email?: string; loginTitle?: string; sysLogo?: string; -} + // 添加API地址配置项 + ApiUrl?: string; + // 添加主题配置项 + theme?: string; +} \ No newline at end of file diff --git a/src/layout/components/header-tools.vue b/src/layout/components/header-tools.vue index fcfc410..2dfe6d5 100644 --- a/src/layout/components/header-tools.vue +++ b/src/layout/components/header-tools.vue @@ -141,7 +141,7 @@ import { FullscreenExitOutlined } from '@ant-design/icons-vue'; import {storeToRefs} from 'pinia'; -import {copyText, openNew, openUrl} from '@/utils/common'; +import {copyText, openUrl} from '@/utils/common'; import {useThemeStore} from '@/store/modules/theme'; import HeaderNotice from './header-notice.vue'; import PasswordModal from './password-modal.vue'; @@ -152,6 +152,8 @@ import {listRoles} from '@/api/system/role'; import { useSiteStore } from '@/store/modules/site'; import Qrcode from "@/components/QrCode/index.vue"; import {AppInfo} from "@/api/cms/cmsWebsite/model"; +import {getCmsWebsiteFieldByCode} from "@/api/cms/cmsWebsiteField"; +import {API_BASE_URL} from "@/config/setting"; // 是否开启响应式布局 const themeStore = useThemeStore(); @@ -244,6 +246,15 @@ const reload = () => { } }); } + // 检查是否启动自定义接口 + if(!localStorage.getItem('ApiUrl')){ + localStorage.setItem('ApiUrl', `${API_BASE_URL}`) + getCmsWebsiteFieldByCode('ApiUrl').then(res => { + if(res){ + localStorage.setItem('ApiUrl', `${res.value}`); + } + }) + } }; reload(); diff --git a/src/store/modules/template.ts b/src/store/modules/template.ts new file mode 100644 index 0000000..24b8e43 --- /dev/null +++ b/src/store/modules/template.ts @@ -0,0 +1,119 @@ +/** + * 网站配置 store + */ +import { defineStore } from 'pinia'; +import {configWebsiteField} from '@/api/cms/cmsWebsiteField'; +import type { Config } from '@/api/cms/cmsWebsiteField/model'; + +export interface ConfigState { + config: Config | null; + loading: boolean; + error: Error | null; +} + +export const useConfigStore = defineStore({ + id: 'config', + state: (): ConfigState => ({ + // 网站配置数据 + config: null, + // 加载状态 + loading: false, + // 错误信息 + error: null + }), + getters: { + /** + * 获取网站配置 + */ + getConfig(state): Config | null { + return state.config; + }, + + /** + * 获取API地址 + */ + getApiUrl(state): string | undefined { + return state.config?.ApiUrl; + }, + + /** + * 获取网站名称 + */ + getSiteName(state): string | undefined { + return state.config?.siteName; + }, + + /** + * 获取网站Logo + */ + getSiteLogo(state): string | undefined { + return state.config?.siteLogo; + } + }, + actions: { + /** + * 获取网站配置数据 + */ + async fetchConfig() { + try { + this.loading = true; + this.error = null; + const data = await configWebsiteField(); + this.config = data; + + // 保存到localStorage中,供其他地方使用 + localStorage.setItem('config', JSON.stringify(data)); + + // 设置主题 + if (data.theme && !localStorage.getItem('user_theme')) { + localStorage.setItem('user_theme', data.theme); + } + + return data; + } catch (err) { + this.error = err instanceof Error ? err : new Error('获取配置失败'); + console.error('获取网站配置失败:', err); + throw err; + } finally { + this.loading = false; + } + }, + + /** + * 更新配置数据 + */ + setConfig(value: Config) { + this.config = value; + // 同时更新localStorage + localStorage.setItem('config', JSON.stringify(value)); + }, + + /** + * 重新获取配置数据 + */ + async refetchConfig() { + try { + this.loading = true; + this.error = null; + const data = await configWebsiteField(); + this.config = data; + localStorage.setItem('config', JSON.stringify(data)); + return data; + } catch (err) { + this.error = err instanceof Error ? err : new Error('获取配置失败'); + console.error('重新获取网站配置失败:', err); + throw err; + } finally { + this.loading = false; + } + }, + + /** + * 清除配置数据 + */ + clearConfig() { + this.config = null; + localStorage.removeItem('config'); + } + } +}); diff --git a/src/utils/enhanced-request.ts b/src/utils/enhanced-request.ts index c2dd161..77f6be2 100644 --- a/src/utils/enhanced-request.ts +++ b/src/utils/enhanced-request.ts @@ -1,8 +1,7 @@ /** * 增强的 API 请求工具 */ -import axios, { AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios'; -import { message } from 'ant-design-vue'; +import axios, { AxiosRequestConfig, AxiosError } from 'axios'; import { apiPerformanceMonitor } from './performance'; import { memoryCache } from './cache-manager'; import { getToken } from './token-util'; @@ -34,29 +33,29 @@ interface EnhancedRequestConfig extends AxiosRequestConfig { // 请求队列管理 class RequestQueue { private pendingRequests = new Map>(); - + // 生成请求键 private generateKey(config: AxiosRequestConfig): string { const { method, url, params, data } = config; return `${method}_${url}_${JSON.stringify(params)}_${JSON.stringify(data)}`; } - + // 添加请求到队列 add(config: AxiosRequestConfig, executor: () => Promise): Promise { const key = this.generateKey(config); - + if (this.pendingRequests.has(key)) { return this.pendingRequests.get(key); } - + const promise = executor().finally(() => { this.pendingRequests.delete(key); }); - + this.pendingRequests.set(key, promise); return promise; } - + // 清除队列 clear(): void { this.pendingRequests.clear(); @@ -70,13 +69,13 @@ class RetryManager { config: { times: number; delay: number; condition?: (error: any) => boolean } ): Promise { let lastError: any; - + for (let i = 0; i <= config.times; i++) { try { return await fn(); } catch (error) { lastError = error; - + // 检查是否应该重试 if (i < config.times && (!config.condition || config.condition(error as AxiosError))) { await this.delay(config.delay * Math.pow(2, i)); // 指数退避 @@ -86,10 +85,10 @@ class RetryManager { } } } - + throw lastError; } - + private static delay(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } @@ -101,13 +100,13 @@ export class EnhancedRequest { baseURL: API_BASE_URL, timeout: 30000 }); - + private requestQueue = new RequestQueue(); - + constructor() { this.setupInterceptors(); } - + private setupInterceptors() { // 请求拦截器 this.instance.interceptors.request.use( @@ -117,17 +116,17 @@ export class EnhancedRequest { if (token && config.headers) { config.headers[TOKEN_HEADER_NAME] = token; } - + // 添加请求时间戳 (config as any).startTime = Date.now(); - + return config; }, (error) => { return Promise.reject(error); } ); - + // 响应拦截器 this.instance.interceptors.response.use( (response) => { @@ -137,7 +136,7 @@ export class EnhancedRequest { const duration = Date.now() - config.startTime; apiPerformanceMonitor.recordApiCall(config.url, duration); } - + return response; }, (error) => { @@ -147,12 +146,12 @@ export class EnhancedRequest { const duration = Date.now() - config.startTime; apiPerformanceMonitor.recordApiCall(config.url, duration); } - + return Promise.reject(error); } ); } - + // 通用请求方法 async request(config: EnhancedRequestConfig): Promise { const { @@ -163,10 +162,10 @@ export class EnhancedRequest { timeoutRetry = true, ...axiosConfig } = config; - + // 生成缓存键 const cacheKey = cache?.key || this.generateCacheKey(axiosConfig); - + // 尝试从缓存获取 if (cache?.enabled) { const cachedData = memoryCache.get(cacheKey); @@ -174,11 +173,11 @@ export class EnhancedRequest { return cachedData; } } - + // 请求执行器 const executor = async (): Promise => { const response = await this.instance.request(axiosConfig); - + // 缓存响应数据 if (cache?.enabled && response.data) { memoryCache.set( @@ -188,10 +187,10 @@ export class EnhancedRequest { cache.tags ); } - + return response.data; }; - + // 请求去重 if (dedupe) { return this.requestQueue.add(axiosConfig, async () => { @@ -202,11 +201,11 @@ export class EnhancedRequest { condition: retry.condition || this.shouldRetry }); } - + return executor(); }); } - + // 重试机制 if (retry) { return RetryManager.retry(executor, { @@ -214,10 +213,10 @@ export class EnhancedRequest { condition: retry.condition || this.shouldRetry }); } - + return executor(); } - + // GET 请求 get(url: string, config?: EnhancedRequestConfig): Promise { return this.request({ @@ -226,7 +225,7 @@ export class EnhancedRequest { url }); } - + // POST 请求 post(url: string, data?: any, config?: EnhancedRequestConfig): Promise { return this.request({ @@ -236,7 +235,7 @@ export class EnhancedRequest { data }); } - + // PUT 请求 put(url: string, data?: any, config?: EnhancedRequestConfig): Promise { return this.request({ @@ -246,7 +245,7 @@ export class EnhancedRequest { data }); } - + // DELETE 请求 delete(url: string, config?: EnhancedRequestConfig): Promise { return this.request({ @@ -255,47 +254,47 @@ export class EnhancedRequest { url }); } - + // 批量请求 async batch(requests: EnhancedRequestConfig[]): Promise { const promises = requests.map(config => this.request(config)); return Promise.all(promises); } - + // 并发控制请求 async concurrent( requests: EnhancedRequestConfig[], limit: number = 5 ): Promise { const results: T[] = []; - + for (let i = 0; i < requests.length; i += limit) { const batch = requests.slice(i, i + limit); const batchResults = await this.batch(batch); results.push(...batchResults); } - + return results; } - + // 生成缓存键 private generateCacheKey(config: AxiosRequestConfig): string { const { method, url, params, data } = config; return `api_${method}_${url}_${JSON.stringify(params)}_${JSON.stringify(data)}`; } - + // 判断是否应该重试 private shouldRetry(error: AxiosError): boolean { // 网络错误或超时错误重试 if (!error.response) { return true; } - + // 5xx 服务器错误重试 const status = error.response.status; return status >= 500 && status < 600; } - + // 清除缓存 clearCache(tags?: string[]): void { if (tags) { @@ -304,7 +303,7 @@ export class EnhancedRequest { memoryCache.clear(); } } - + // 取消所有请求 cancelAll(): void { this.requestQueue.clear(); @@ -326,7 +325,7 @@ export function cachedGet( } ): Promise { const { expiry = 5 * 60 * 1000, tags, ...restConfig } = config || {}; - + return enhancedRequest.get(url, { ...restConfig, cache: { diff --git a/src/utils/request.ts b/src/utils/request.ts index e847705..bca94d7 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -13,8 +13,26 @@ import type { ApiResult } from '@/api'; import { getHostname, getTenantId } from '@/utils/domain'; import { getMerchantId } from "@/utils/merchant"; +// 获取API基础地址的函数 +const getBaseUrl = (): string => { + // 尝试从配置store获取后台配置的API地址 + try { + // 如果store中没有,则尝试从localStorage获取 + const ApiUrl = localStorage.getItem('ApiUrl'); + if (ApiUrl) { + return ApiUrl; + } + } catch (error) { + console.warn('获取后台配置API地址失败:', error); + } + + // 如果后台没有配置API地址,则使用本地配置 + console.log('使用本地配置的API地址:', API_BASE_URL); + return API_BASE_URL; +}; + const service = axios.create({ - baseURL: API_BASE_URL + baseURL: getBaseUrl() }); /** diff --git a/src/views/passport/login/index.vue b/src/views/passport/login/index.vue index 9bdbdd6..fded8ab 100644 --- a/src/views/passport/login/index.vue +++ b/src/views/passport/login/index.vue @@ -284,7 +284,6 @@ import {Config} from '@/api/cms/cmsWebsiteField/model'; import {phoneReg} from 'ele-admin-pro'; import router from "@/router"; import {listAdminsByPhoneAll} from "@/api/system/user"; -import {getUserInfo} from "@/api/layout"; import {QrCodeStatusResponse} from "@/api/passport/qrLogin"; const useForm = Form.useForm;