主需求:实现运行时可配置的后端 API 地址,支持 Electron、Cordova 两种环境(不包括 Web 端)
具体要求:
现有架构:
src/API/config.ts 使用 webpack 注入的常量src/API/interceptor.ts 中的 axiosInstancesrc/app.tsx 启动时调用 initializeProductState()fetchSoftwareInfo() 获取 /pub/software_info问题:
不包括 Web 端的原因:
src/features/serverConfig/
├── index.ts # 导出文件
├── types/
│ └── index.ts # 类型定义
├── storage/
│ ├── IStorageAdapter.ts # 存储适配器接口
│ ├── ElectronStorageAdapter.ts # Electron 存储实现
│ ├── CordovaStorageAdapter.ts # Cordova 存储实现
│ └── index.ts # 存储工厂
├── services/
│ └── ConfigService.ts # 配置服务
├── state/
│ └── serverConfigSlice.ts # Redux slice
└── components/
├── ServerConfigModal.tsx # 配置对话框
├── ServerConfigModal.module.css # 对话框样式
└── ServerSettingsPanel.tsx # 设置面板
配置管理模块 (ConfigService)
网络层改造 (axiosInstance)
UI 组件
ServerConfigModal)ServerSettingsPanel)serverConfigSlice)
setServerConfig, testConnection, resetConfig
StorageAdapter)
electron-store 或文件系统cordova-plugin-nativestorageConfigService (src/features/serverConfig/services/)
class ConfigService {
- getServerConfig(): Promise<ServerConfig>
- saveServerConfig(config: ServerConfig): Promise<void>
- testConnection(url: string): Promise<ConnectionTestResult>
- getDefaultConfig(): ServerConfig
- clearConfig(): Promise<void>
}
ServerConfigSlice (src/features/serverConfig/state/)
- State: { apiBaseUrl, mqttBrokerUrl, isConfigured, testing }
- Actions: setServerConfig, testConnection, resetConfig
- Thunks: initializeServerConfig, saveAndApplyConfig
ServerConfigModal (src/features/serverConfig/components/)
- Props: open, onSave, onCancel, defaultUrl
- Methods: handleTest, handleSave, validateUrl
- State: inputUrl, testing, testResult
StorageAdapter (src/features/serverConfig/storage/)
interface IStorageAdapter {
- getItem(key: string): Promise<string | null>
- setItem(key: string, value: string): Promise<void>
- removeItem(key: string): Promise<void>
}
class ElectronStorageAdapter implements IStorageAdapter
class CordovaStorageAdapter implements IStorageAdapter
src/features/serverConfig/types/index.ts - 类型定义src/features/serverConfig/storage/IStorageAdapter.ts - 存储接口src/features/serverConfig/storage/ElectronStorageAdapter.ts - Electron 存储src/features/serverConfig/storage/CordovaStorageAdapter.ts - Cordova 存储src/features/serverConfig/storage/index.ts - 存储工厂src/features/serverConfig/services/ConfigService.ts - 配置服务src/features/serverConfig/state/serverConfigSlice.ts - Redux slicesrc/features/serverConfig/components/ServerConfigModal.tsx - 配置对话框src/features/serverConfig/components/ServerConfigModal.module.css - 对话框样式src/features/serverConfig/components/ServerSettingsPanel.tsx - 设置面板src/features/serverConfig/index.ts - 导出文件src/API/config.ts - 改为从配置服务获取地址src/API/interceptor.ts - 支持动态修改 baseURLsrc/app.tsx - 添加配置初始化和错误处理逻辑src/states/store.ts - 注册新的 slicepackage.json - 添加依赖(如需要)sequenceDiagram
participant User as 用户
participant App as App.tsx
participant Config as ConfigService
participant Storage as StorageAdapter
participant Modal as ServerConfigModal
participant API as axiosInstance
participant Backend as 后端服务器
User->>App: 启动应用
App->>Config: 初始化配置
Config->>Storage: 读取本地配置
Storage-->>Config: 返回配置/null
alt 配置存在
Config-->>App: 返回已保存配置
App->>API: 设置 baseURL
App->>Backend: 请求 /software_info
alt 请求成功
Backend-->>App: 返回软件信息
App->>User: 正常显示应用
else 请求失败
Backend-->>App: 连接失败
App->>Modal: 显示配置对话框
Modal->>User: 请求输入服务器地址
end
else 配置不存在
Config-->>App: 返回默认配置
App->>Backend: 尝试默认地址
alt 默认地址可用
Backend-->>App: 连接成功
App->>User: 正常显示应用
else 默认地址不可用
Backend-->>App: 连接失败
App->>Modal: 显示配置对话框
Modal->>User: 请求输入服务器地址
end
end
User->>Modal: 输入服务器地址
User->>Modal: 点击"测试连接"
Modal->>API: 临时设置 baseURL
Modal->>Backend: 测试请求 /software_info
alt 测试成功
Backend-->>Modal: 返回成功
Modal->>User: 显示"连接成功"
User->>Modal: 点击"保存"
Modal->>Config: 保存配置
Config->>Storage: 持久化配置
Storage-->>Config: 保存成功
Modal->>App: 应用新配置
App->>API: 更新 baseURL
App->>User: 重新初始化应用
else 测试失败
Backend-->>Modal: 连接失败
Modal->>User: 显示错误信息
User->>Modal: 修正地址后重试
end
flowchart TD
Start([应用启动]) --> LoadConfig[加载本地配置]
LoadConfig --> HasConfig{配置存在?}
HasConfig -->|是| UseConfig[使用已保存配置]
HasConfig -->|否| UseDefault[使用默认配置]
UseConfig --> TestAPI[测试 API 连接]
UseDefault --> TestAPI
TestAPI --> APISuccess{连接成功?}
APISuccess -->|是| InitApp[初始化应用]
APISuccess -->|否| ShowModal[显示配置对话框]
ShowModal --> UserInput[用户输入地址]
UserInput --> TestConnection[测试连接]
TestConnection --> TestResult{测试结果}
TestResult -->|成功| SaveConfig[保存配置]
TestResult -->|失败| ShowError[显示错误]
ShowError --> UserInput
SaveConfig --> UpdateAxios[更新 axios baseURL]
UpdateAxios --> InitApp
InitApp --> AppRunning[应用正常运行]
AppRunning --> SettingsChange{用户修改配置?}
SettingsChange -->|是| ShowModal
SettingsChange -->|否| AppRunning
// src/features/serverConfig/types/index.ts
/**
* 服务器配置接口
*/
export interface ServerConfig {
/** API 基础地址 */
apiBaseUrl: string;
/** MQTT Broker 地址 */
mqttBrokerUrl: string;
/** 配置创建时间 */
createdAt?: string;
/** 配置更新时间 */
updatedAt?: string;
/** 配置名称(用于多环境) */
name?: string;
}
/**
* 连接测试结果
*/
export interface ConnectionTestResult {
success: boolean;
message: string;
serverInfo?: {
version: string;
product: string;
};
}
/**
* 配置验证结果
*/
export interface ConfigValidation {
valid: boolean;
errors: string[];
}
/**
* 环境预设
*/
export interface EnvironmentPreset {
name: string;
displayName: string;
apiBaseUrl: string;
mqttBrokerUrl: string;
}
/**
* 平台类型
*/
export type PlatformType = 'Electron' | 'Cordova';
// src/features/serverConfig/state/serverConfigSlice.ts
interface ServerConfigState {
/** 当前配置 */
current: ServerConfig | null;
/** 是否已配置 */
isConfigured: boolean;
/** 是否正在测试连接 */
testing: boolean;
/** 测试结果 */
testResult: ConnectionTestResult | null;
/** 加载状态 */
loading: boolean;
/** 错误信息 */
error: string | null;
/** 环境预设列表 */
presets: EnvironmentPreset[];
}
Electron (userData/config.json)
{
"serverConfig": {
"apiBaseUrl": "http://192.168.1.100:8080",
"mqttBrokerUrl": "ws://192.168.1.100:8083/mqtt",
"createdAt": "2025-01-11T09:00:00.000Z",
"updatedAt": "2025-01-11T10:30:00.000Z",
"name": "生产环境"
}
}
Cordova (SharedPreferences/NSUserDefaults)
Key: serverConfig
Value: {"apiBaseUrl":"http://192.168.1.100:8080","mqttBrokerUrl":"ws://192.168.1.100:8083/mqtt"}
flowchart TD
A[应用启动] --> B[App.tsx useEffect]
B --> C[dispatch initializeServerConfig]
C --> D[ConfigService.getServerConfig]
D --> E{本地有配置?}
E -->|有| F[返回本地配置]
E -->|无| G[返回默认配置]
F --> H[更新 Redux State]
G --> H
H --> I[更新 axios baseURL]
I --> J[dispatch initializeProductState]
J --> K[调用 fetchSoftwareInfo]
K --> L{API 请求成功?}
L -->|成功| M[加载应用内容]
L -->|失败| N[显示 ServerConfigModal]
N --> O[用户配置后重试]
O --> K
flowchart TD
A[用户输入地址] --> B[点击测试连接]
B --> C[临时创建 axios 实例]
C --> D[请求 /pub/software_info]
D --> E{请求成功?}
E -->|成功| F[显示成功提示]
E -->|失败| G[显示错误信息]
F --> H[用户点击保存]
H --> I[dispatch saveAndApplyConfig]
I --> J[ConfigService.saveServerConfig]
J --> K[StorageAdapter.setItem]
K --> L[更新 Redux State]
L --> M[更新全局 axios baseURL]
M --> N[关闭对话框]
N --> O[重新初始化应用]
自动触发:
手动触发:
测试步骤:
userData/config.jsonhttp://192.168.1.100:8080)测试步骤:
测试步骤:
测试步骤:
invalid-url)测试步骤:
http://192.168.999.999:8080)测试步骤:
| 平台 | 存储方式 | 测试要点 |
|---|---|---|
| Electron (Windows) | electron-store | 检查 %APPDATA%/app-name/config.json |
| Electron (macOS) | electron-store | 检查 ~/Library/Application Support/app-name/config.json |
| Cordova (Android) | SharedPreferences | 检查应用数据目录 |
| Cordova (iOS) | NSUserDefaults | 检查应用沙盒 |
描述:axios 实例在创建时设置了 baseURL,后续修改可能不生效
解决方案:
// 使用请求拦截器动态设置(推荐)
axiosInstance.interceptors.request.use((config) => {
const serverConfig = store.getState().serverConfig.current;
if (serverConfig) {
config.baseURL = serverConfig.apiBaseUrl + '/dr/api/v1/';
}
return config;
});
描述:MQTT 连接可能在应用启动时已经建立,修改配置后需要重连
解决方案:
// 在 ConfigService.saveServerConfig 中
async saveServerConfig(config: ServerConfig) {
await this.storage.setItem('serverConfig', JSON.stringify(config));
// 触发 MQTT 重连
if (config.mqttBrokerUrl !== this.currentConfig?.mqttBrokerUrl) {
emitter.emit('mqtt:reconnect', config.mqttBrokerUrl);
}
}
描述:某些系统中可能无法写入配置文件
解决方案:
app.getPath('userData') 确保有写入权限描述:需要安装 cordova-plugin-nativestorage
解决方案:
cordova plugin add cordova-plugin-nativestorage
描述:如果用户不配置,应用无法使用
解决方案:
描述:修改配置后,应用中已加载的数据可能过时
解决方案:
// 保存配置后强制刷新
async function applyNewConfig(config: ServerConfig) {
// 1. 保存配置
await ConfigService.save(config);
// 2. 清除所有缓存状态
store.dispatch(resetAllData());
// 3. 重新初始化
await store.dispatch(initializeProductState());
// 4. 显示成功提示
message.success('配置已更新,应用已重新加载');
}
描述:配置中可能包含内网地址等敏感信息
解决方案:
描述:用户可能输入恶意服务器地址
解决方案:
function validateServerUrl(url: string): ConfigValidation {
const errors: string[] = [];
try {
const parsed = new URL(url);
// 只允许 http 和 https
if (!['http:', 'https:'].includes(parsed.protocol)) {
errors.push('只支持 HTTP 和 HTTPS 协议');
}
// 检查端口范围
const port = parseInt(parsed.port);
if (port && (port < 1 || port > 65535)) {
errors.push('端口号必须在 1-65535 之间');
}
} catch (e) {
errors.push('URL 格式不正确');
}
return {
valid: errors.length === 0,
errors,
};
}
classDiagram
class IStorageAdapter {
<<interface>>
+getItem(key: string) Promise~string~
+setItem(key: string, value: string) Promise~void~
+removeItem(key: string) Promise~void~
}
class ElectronStorageAdapter {
-configPath: string
+getItem(key: string) Promise~string~
+setItem(key: string, value: string) Promise~void~
+removeItem(key: string) Promise~void~
}
class CordovaStorageAdapter {
+getItem(key: string) Promise~string~
+setItem(key: string, value: string) Promise~void~
+removeItem(key: string) Promise~void~
}
class ConfigService {
-storage: IStorageAdapter
-CONFIG_KEY: string
+getServerConfig() Promise~ServerConfig~
+saveServerConfig(config) Promise~void~
+testConnection(url) Promise~ConnectionTestResult~
+getDefaultConfig() ServerConfig
+clearConfig() Promise~void~
}
class ServerConfigSlice {
+state: ServerConfigState
+setServerConfig(config)
+setTesting(testing)
+setTestResult(result)
+initializeServerConfig()
+saveAndApplyConfig(config)
+testConnectionThunk(url)
}
class ServerConfigModal {
-props: ServerConfigModalProps
-state: ModalState
+handleTest()
+handleSave()
+validateUrl()
+render()
}
IStorageAdapter <|.. ElectronStorageAdapter
IStorageAdapter <|.. CordovaStorageAdapter
ConfigService --> IStorageAdapter
ServerConfigSlice --> ConfigService
ServerConfigModal --> ServerConfigSlice
┌─────────────────────────────────────────────┐
│ ⚙️ 服务器配置 ✕ │
├─────────────────────────────────────────────┤
│ │
│ ⚠️ 无法连接到服务器,请配置后端地址 │
│ │
│ 服务器地址: │
│ ┌───────────────────────────────────────┐ │
│ │ http://192.168.1.100:8080 │ │
│ └───────────────────────────────────────┘ │
│ 示例: http://192.168.1.100:8080 │
│ │
│ MQTT 地址: │
│ ┌───────────────────────────────────────┐ │
│ │ ws://192.168.1.100:8083/mqtt │ │
│ └───────────────────────────────────────┘ │
│ 示例: ws://192.168.1.100:8083/mqtt │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ 🧪 测试连接 │ │
│ └─────────────────────────────────────┘ │
│ │
│ ✅ 连接成功!服务器版本: v2.1.0 │
│ │
│ ┌──────────┐ ┌──────────┐ │
│ │ 取消 │ │ 保存 │ │
│ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────┘
设置
├── 通用设置
├── 语言设置
└── 🌐 服务器配置
├── 当前服务器: http://192.168.1.100:8080
├── 连接状态: ✅ 已连接
├── [修改配置] 按钮
└── 环境快捷切换:
○ 开发环境
○ 测试环境
● 生产环境
// src/features/serverConfig/types/index.ts
/**
* 服务器配置接口
*/
export interface ServerConfig {
/** API 基础地址 */
apiBaseUrl: string;
/** MQTT Broker 地址 */
mqttBrokerUrl: string;
/** 配置创建时间 */
createdAt?: string;
/** 配置更新时间 */
updatedAt?: string;
/** 配置名称(用于多环境) */
name?: string;
}
/**
* 连接测试结果
*/
export interface ConnectionTestResult {
success: boolean;
message: string;
serverInfo?: {
version: string;
product: string;
};
}
/**
* 配置验证结果
*/
export interface ConfigValidation {
valid: boolean;
errors: string[];
}
/**
* 环境预设
*/
export interface EnvironmentPreset {
name: string;
displayName: string;
apiBaseUrl: string;
mqttBrokerUrl: string;
}
/**
* 平台类型
*/
export type PlatformType = 'Electron' | 'Cordova';
// src/features/serverConfig/storage/IStorageAdapter.ts
/**
* 存储适配器接口
*/
export interface IStorageAdapter {
/**
* 获取存储项
*/
getItem(key: string): Promise<string | null>;
/**
* 设置存储项
*/
setItem(key: string, value: string): Promise<void>;
/**
* 删除存储项
*/
removeItem(key: string): Promise<void>;
}
// src/features/serverConfig/storage/ElectronStorageAdapter.ts
import { IStorageAdapter } from './IStorageAdapter';
import fs from 'fs';
import path from 'path';
/**
* Electron 存储适配器
* 使用文件系统存储配置
*/
export class ElectronStorageAdapter implements IStorageAdapter {
private configPath: string;
constructor() {
// 获取 userData 路径
const { app } = require('electron').remote || require('@electron/remote');
const userDataPath = app.getPath('userData');
this.configPath = path.join(userDataPath, 'config.json');
// 确保配置文件所在目录存在
const dir = path.dirname(this.configPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
async getItem(key: string): Promise<string | null> {
try {
if (!fs.existsSync(this.configPath)) {
return null;
}
const content = fs.readFileSync(this.configPath, 'utf-8');
const data = JSON.parse(content);
return data[key] || null;
} catch (error) {
console.error('读取配置文件失败:', error);
return null;
}
}
async setItem(key: string, value: string): Promise<void> {
try {
let data = {};
// 读取现有配置
if (fs.existsSync(this.configPath)) {
const content = fs.readFileSync(this.configPath, 'utf-8');
data = JSON.parse(content);
}
// 更新配置
data[key] = value;
// 写入文件
fs.writeFileSync(this.configPath, JSON.stringify(data, null, 2), 'utf-8');
} catch (error) {
console.error('保存配置文件失败:', error);
throw error;
}
}
async removeItem(key: string): Promise<void> {
try {
if (!fs.existsSync(this.configPath)) {
return;
}
const content = fs.readFileSync(this.configPath, 'utf-8');
const data = JSON.parse(content);
delete data[key];
fs.writeFileSync(this.configPath, JSON.stringify(data, null, 2), 'utf-8');
} catch (error) {
console.error('删除配置项失败:', error);
throw error;
}
}
}
// src/features/serverConfig/storage/CordovaStorageAdapter.ts
import { IStorageAdapter } from './IStorageAdapter';
/**
* Cordova 存储适配器
* 使用 cordova-plugin-nativestorage
*/
export class CordovaStorageAdapter implements IStorageAdapter {
private NativeStorage: any;
constructor() {
// @ts-ignore
this.NativeStorage = window.NativeStorage;
if (!this.NativeStorage) {
throw new Error('NativeStorage plugin not available');
}
}
async getItem(key: string): Promise<string | null> {
return new Promise((resolve) => {
this.NativeStorage.getItem(
key,
(value: string) => resolve(value),
(error: any) => {
console.error('读取存储失败:', error);
resolve(null);
}
);
});
}
async setItem(key: string, value: string): Promise<void> {
return new Promise((resolve, reject) => {
this.NativeStorage.setItem(
key,
value,
() => resolve(),
(error: any) => {
console.error('保存存储失败:', error);
reject(error);
}
);
});
}
async removeItem(key: string): Promise<void> {
return new Promise((resolve, reject) => {
this.NativeStorage.remove(
key,
() => resolve(),
(error: any) => {
console.error('删除存储失败:', error);
reject(error);
}
);
});
}
}
// src/features/serverConfig/storage/index.ts
import { IStorageAdapter } from './IStorageAdapter';
import { ElectronStorageAdapter } from './ElectronStorageAdapter';
import { CordovaStorageAdapter } from './CordovaStorageAdapter';
/**
* 检测是否在 Electron 环境
*/
function isElectron(): boolean {
return (
typeof window !== 'undefined' &&
typeof window.process === 'object' &&
(window.process as any).type === 'renderer'
);
}
/**
* 检测是否在 Cordova 环境
*/
function isCordova(): boolean {
return typeof window !== 'undefined' && !!(window as any).cordova;
}
/**
* 创建存储适配器
*/
export function createStorageAdapter(): IStorageAdapter {
if (isElectron()) {
console.log('使用 Electron 存储适配器');
return new ElectronStorageAdapter();
} else if (isCordova()) {
console.log('使用 Cordova 存储适配器');
return new CordovaStorageAdapter();
} else {
throw new Error('不支持的平台');
}
}
// src/features/serverConfig/services/ConfigService.ts
import { ServerConfig, ConnectionTestResult, ConfigValidation } from '../types';
import { createStorageAdapter } from '../storage';
import axios from 'axios';
export class ConfigService {
private static instance: ConfigService;
private storage = createStorageAdapter();
private readonly CONFIG_KEY = 'serverConfig';
private constructor() {}
static getInstance(): ConfigService {
if (!ConfigService.instance) {
ConfigService.instance = new ConfigService();
}
return ConfigService.instance;
}
/**
* 获取默认配置
*/
getDefaultConfig(): ServerConfig {
return {
apiBaseUrl: 'http://127.0.0.1:8080',
mqttBrokerUrl: 'ws://127.0.0.1:8083/mqtt',
name: '默认配置',
};
}
/**
* 获取服务器配置
*/
async getServerConfig(): Promise<ServerConfig> {
try {
const configJson = await this.storage.getItem(this.CONFIG_KEY);
if (configJson) {
return JSON.parse(configJson);
}
} catch (error) {
console.error('读取配置失败:', error);
}
return this.getDefaultConfig();
}
/**
* 保存服务器配置
*/
async saveServerConfig(config: ServerConfig): Promise<void> {
const configWithTimestamp: ServerConfig = {
...config,
updatedAt: new Date().toISOString(),
createdAt: config.createdAt || new Date().toISOString(),
};
await this.storage.setItem(
this.CONFIG_KEY,
JSON.stringify(configWithTimestamp)
);
}
/**
* 验证 URL 格式
*/
validateUrl(url: string): ConfigValidation {
const errors: string[] = [];
if (!url || url.trim() === '') {
errors.push('地址不能为空');
return { valid: false, errors };
}
try {
const parsed = new URL(url);
// 只允许 http 和 https
if (!['http:', 'https:'].includes(parsed.protocol)) {
errors.push('只支持 HTTP 和 HTTPS 协议');
}
// 检查端口范围
if (parsed.port) {
const port = parseInt(parsed.port);
if (port < 1 || port > 65535) {
errors.push('端口号必须在 1-65535 之间');
}
}
} catch (e) {
errors.push('URL 格式不正确');
}
return {
valid: errors.length === 0,
errors,
};
}
/**
* 验证 MQTT URL 格式
*/
validateMqttUrl(url: string): ConfigValidation {
const errors: string[] = [];
if (!url || url.trim() === '') {
errors.push('MQTT 地址不能为空');
return { valid: false, errors };
}
try {
const parsed = new URL(url);
// 只允许 ws 和 wss
if (!['ws:', 'wss:'].includes(parsed.protocol)) {
errors.push('只支持 WS 和 WSS 协议');
}
} catch (e) {
errors.push('MQTT URL 格式不正确');
}
return {
valid: errors.length === 0,
errors,
};
}
/**
* 测试连接
*/
async testConnection(apiBaseUrl: string): Promise<ConnectionTestResult> {
try {
const testInstance = axios.create({
baseURL: apiBaseUrl + '/dr/api/v1',
timeout: 10000,
});
const response = await testInstance.get('/pub/software_info');
if (response.data?.code === '0x000000') {
return {
success: true,
message: '连接成功',
serverInfo: {
version: response.data.data.server?.version || 'unknown',
product: response.data.data.product || 'unknown',
},
};
} else {
return {
success: false,
message: '服务器响应异常: ' + response.data?.description,
};
}
} catch (error: any) {
let message = '连接失败';
if (error.code === 'ECONNABORTED') {
message = '连接超时,请检查服务器地址';
} else if (error.code === 'ERR_NETWORK') {
message = '网络错误,请检查网络连接';
} else if (error.message) {
message = error.message;
}
return {
success: false,
message,
};
}
}
/**
* 清除配置
*/
async clearConfig(): Promise<void> {
await this.storage.removeItem(this.CONFIG_KEY);
}
}
export default ConfigService.getInstance();
// src/features/serverConfig/state/serverConfigSlice.ts
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import {
ServerConfig,
ConnectionTestResult,
EnvironmentPreset,
} from '../types';
import ConfigService from '../services/ConfigService';
interface ServerConfigState {
current: ServerConfig | null;
isConfigured: boolean;
testing: boolean;
testResult: ConnectionTestResult | null;
loading: boolean;
error: string | null;
presets: EnvironmentPreset[];
}
const initialState: ServerConfigState = {
current: null,
isConfigured: false,
testing: false,
testResult: null,
loading: false,
error: null,
presets: [
{
name: 'dev',
displayName: '开发环境',
apiBaseUrl: 'http://192.168.110.245:6001',
mqttBrokerUrl: 'ws://192.168.110.245:8083/mqtt',
},
{
name: 'test',
displayName: '测试环境',
apiBaseUrl: 'http://192.168.110.100:6001',
mqttBrokerUrl: 'ws://192.168.110.100:8083/mqtt',
},
{
name: 'prod',
displayName: '生产环境',
apiBaseUrl: 'http://127.0.0.1:8080',
mqttBrokerUrl: 'ws://127.0.0.1:8083/mqtt',
},
],
};
/**
* 初始化服务器配置
*/
export const initializeServerConfig = createAsyncThunk(
'serverConfig/initialize',
async () => {
const config = await ConfigService.getServerConfig();
return config;
}
);
/**
* 测试连接
*/
export const testConnectionThunk = createAsyncThunk(
'serverConfig/testConnection',
async (apiBaseUrl: string) => {
const result = await ConfigService.testConnection(apiBaseUrl);
return result;
}
);
/**
* 保存并应用配置
*/
export const saveAndApplyConfig = createAsyncThunk(
'serverConfig/saveAndApply',
async (config: ServerConfig) => {
await ConfigService.saveServerConfig(config);
return config;
}
);
const serverConfigSlice = createSlice({
name: 'serverConfig',
initialState,
reducers: {
setServerConfig: (state, action: PayloadAction<ServerConfig>) => {
state.current = action.payload;
state.isConfigured = true;
},
setTesting: (state, action: PayloadAction<boolean>) => {
state.testing = action.payload;
},
setTestResult: (
state,
action: PayloadAction<ConnectionTestResult | null>
) => {
state.testResult = action.payload;
},
clearTestResult: (state) => {
state.testResult = null;
},
resetConfig: (state) => {
state.current = null;
state.isConfigured = false;
state.testResult = null;
state.error = null;
},
},
extraReducers: (builder) => {
builder
// 初始化配置
.addCase(initializeServerConfig.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(initializeServerConfig.fulfilled, (state, action) => {
state.loading = false;
state.current = action.payload;
state.isConfigured = true;
})
.addCase(initializeServerConfig.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message || '加载配置失败';
})
// 测试连接
.addCase(testConnectionThunk.pending, (state) => {
state.testing = true;
state.testResult = null;
})
.addCase(testConnectionThunk.fulfilled, (state, action) => {
state.testing = false;
state.testResult = action.payload;
})
.addCase(testConnectionThunk.rejected, (state, action) => {
state.testing = false;
state.testResult = {
success: false,
message: action.error.message || '测试失败',
};
})
// 保存并应用配置
.addCase(saveAndApplyConfig.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(saveAndApplyConfig.fulfilled, (state, action) => {
state.loading = false;
state.current = action.payload;
state.isConfigured = true;
})
.addCase(saveAndApplyConfig.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message || '保存配置失败';
});
},
});
export const {
setServerConfig,
setTesting,
setTestResult,
clearTestResult,
resetConfig,
} = serverConfigSlice.actions;
export default serverConfigSlice.reducer;
// src/API/config.ts
import store from '../states/store';
/**
* 动态获取 API 基础地址
*/
export function getApiBaseUrl(): string {
const serverConfig = store.getState().serverConfig?.current;
if (serverConfig) {
return serverConfig.apiBaseUrl + '/dr/api/v1/';
}
// 降级到编译时配置
return `${API_BASE_URL_FROM_WEBPACK}/dr/api/v1/`;
}
/**
* 动态获取 MQTT Broker 地址
*/
export function getMqttBrokerUrl(): string {
const serverConfig = store.getState().serverConfig?.current;
if (serverConfig) {
return serverConfig.mqttBrokerUrl;
}
// 降级到编译时配置
return `${MQTT_BROKER_URL_FROM_WEBPACK}`;
}
// 保留原有导出以兼容现有代码,但标记为废弃
/** @deprecated 使用 getApiBaseUrl() 代替 */
export const API_BASE_URL = getApiBaseUrl();
/** @deprecated 使用 getMqttBrokerUrl() 代替 */
export const MQTT_BROKER_URL = getMqttBrokerUrl();
export const IP_PORT = `${API_BASE_URL_FROM_WEBPACK}`;
// src/API/interceptor.ts
import axios from 'axios';
import store from '../states/store';
import { getApiBaseUrl } from './config';
// ... 其他导入
const axiosInstance = axios.create({
// 初始 baseURL,会被拦截器动态更新
baseURL: getApiBaseUrl(),
timeout: 30000,
});
// 添加请求拦截器,动态设置 baseURL
axiosInstance.interceptors.request.use(
(config) => {
// 动态获取当前配置的 API 地址
config.baseURL = getApiBaseUrl();
return config;
},
(error) => {
return Promise.reject(error);
}
);
// ... 其他拦截器保持不变
// src/app.tsx
import { useState, useEffect, ReactNode } from 'react';
// ... 其他导入
import { initializeServerConfig } from './features/serverConfig/state/serverConfigSlice';
import ServerConfigModal from './features/serverConfig/components/ServerConfigModal';
function AppContent({ children }: { children: ReactNode }): JSX.Element {
const dispatch = useAppDispatch();
// ... 其他状态
const [isI18nReady, setIsI18nReady] = useState(false);
const [showConfigModal, setShowConfigModal] = useState(false);
const serverConfig = useAppSelector((state) => state.serverConfig);
useEffect(() => {
// 1. 先初始化服务器配置
dispatch(initializeServerConfig())
.unwrap()
.then((config) => {
console.log('服务器配置已加载:', config);
// 2. 尝试加载软件信息
return dispatch(initializeProductState()).unwrap();
})
.then((productState) => {
console.log(`初始化,拉取到产品信息:${JSON.stringify(productState)}`);
const languageCode = productState.language;
return dispatch(loadI18nMessages(languageCode)).unwrap();
})
.then(() => {
setIsI18nReady(true);
})
.catch((error) => {
console.error('初始化失败:', error);
// 显示配置对话框
setShowConfigModal(true);
});
}, [dispatch]);
// 配置保存成功后重新初始化
const handleConfigSaved = () => {
setShowConfigModal(false);
// 重新初始化应用
dispatch(initializeProductState())
.unwrap()
.then((productState) => {
const languageCode = productState.language;
return dispatch(loadI18nMessages(languageCode)).unwrap();
})
.then(() => {
setIsI18nReady(true);
})
.catch((error) => {
console.error('重新初始化失败:', error);
setShowConfigModal(true);
});
};
return (
<ConfigProvider theme={themeWithAlgorithm}>
<IntlProvider
locale={currentLocale ? currentLocale.split('_')[0] : 'en'}
messages={(messages as Record<string, string>) || {}}
>
{/* 配置对话框 */}
<ServerConfigModal
open={showConfigModal}
onSave={handleConfigSaved}
onCancel={() => {
// 用户可以选择稍后配置
setShowConfigModal(false);
}}
/>
{/* 其他内容保持不变 */}
{/* ... */}
</IntlProvider>
</ConfigProvider>
);
}
// src/states/store.ts
import { configureStore } from '@reduxjs/toolkit';
// ... 其他导入
import serverConfigReducer from '../features/serverConfig/state/serverConfigSlice';
const store = configureStore({
reducer: {
// ... 其他 reducers
serverConfig: serverConfigReducer,
},
});
export default store;
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
# 安装 NativeStorage 插件
cordova plugin add cordova-plugin-nativestorage
Electron 使用 Node.js 的 fs 模块,无需额外安装依赖。
如果使用 @electron/remote,需要安装:
npm install @electron/remote
src/features/serverConfig/文档版本: v1.0
创建日期: 2025-01-11
最后更新: 2025-01-11