EMS-vite/src/api/javaApi.ts
miaoda 95098991f3 # 技术实现详情
## 架构设计

**前端技术栈**
- React 18 + TypeScript - 提供类型安全的现代化前端开发
- Tailwind CSS + shadcn/ui - 实现美观一致的用户界面
- Recharts - 专业的数据可视化图表库
- React Router - 单页应用路由管理
- React Hook Form - 高效的表单状态管理

**后端数据层**
- Supabase - 现代化的后端即服务平台
- PostgreSQL - 可靠的关系型数据库
- 实时订阅 - 支持数据变更的实时推送

## 数据库设计

创建了完整的数据库架构,包含5个核心表:

1. **devices** - 设备管理表,存储BBox设备信息、状态和配置
2. **battery_data** - 电池数据表,记录电压、电流、温度等实时数据
3. **ota_tasks** - OTA升级任务表,管理固件升级流程和状态
4. **mqtt_logs** - MQTT通信日志表,记录设备通信历史
5. **system_config** - 系统配置表,存储报警阈值和系统参数

## 功能实现

**设备管理模块**
- 实现了完整的CRUD操作,支持设备的创建、查看、编辑和删除
- 设备状态实时监控,包括在线/离线/维护/故障四种状态
- 设备详情页面展示最新电池数据和设备信息
- 智能搜索和状态筛选功能

**实时监控模块**
- 多维度数据图表展示,支持电压、电流、温度、电量趋势分析
- 可配置时间范围查询(1小时到7天)
- 实时数据更新机制,支持自动刷新
- 数据趋势指示器,显示数值变化方向

**OTA管理模块**
- 升级任务创建和管理,支持批量设备升级
- 实时进度跟踪,模拟真实的下载和安装过程
- 详细的升级日志查看,便于故障排查
- 任务状态管理,支持重试和删除操作

**MQTT管理模块**
- 连接状态监控和自动重连功能
- 消息日志实时查看和筛选
- 通信统计分析,包括消息总数和错误率
- 日志导出功能,支持CSV格式

**系统设置模块**
- 分类配置管理,包括常规设置、MQTT配置、报警设置
- 表单验证和错误处理
- 系统信息展示,包括运行状态和存储统计
- 通知设置和权限管理

## 代码质量

- 完整的TypeScript类型定义,确保类型安全
- 模块化的组件设计,便于维护和扩展
- 统一的错误处理和用户反馈机制
- 响应式设计,适配各种屏幕尺寸
- 代码通过ESLint检查,符合最佳实践

## 数据初始化

系统包含丰富的示例数据:
- 5个示例BBox设备,涵盖各种状态
- 历史电池数据,用于图表展示
- OTA升级任务示例,展示不同阶段状态
- MQTT通信日志,模拟真实设备通信
- 完整的系统配置参数

应用已完全实现需求文档中的所有功能,提供了专业级的电池管理解决方案。
2025-11-17 16:52:12 +08:00

529 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import axios from 'axios';
import type {
Device,
BatteryData,
OtaTask,
MqttLog,
SystemConfig,
DashboardStats,
CreateDeviceRequest,
UpdateDeviceRequest,
CreateOtaTaskRequest,
UpdateConfigRequest,
DeviceDetail,
ChartDataPoint,
ApiResponse,
PaginatedResponse,
ExtremeValues,
VehicleLocation,
VehicleData,
SubsystemVoltage,
SubsystemTemperature
} from "@/types/types";
// Java后端API基础配置开发环境走代理生产环境用环境变量
const API_BASE_URL = import.meta.env.DEV ? '/dev-api' : import.meta.env.VITE_JAVA_API_BASE_URL;
if (!API_BASE_URL) {
throw new Error('Missing VITE_JAVA_API_BASE_URL. Please set it in .env to your backend IP, e.g., http://192.168.5.200:8080');
}
// 创建axios实例
const apiClient = axios.create({
baseURL: API_BASE_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
// 请求拦截器 - 添加认证token等
apiClient.interceptors.request.use(
(config) => {
const token = localStorage.getItem('auth_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 响应拦截器 - 统一处理响应
apiClient.interceptors.response.use(
(response) => {
return response.data;
},
(error) => {
console.error('API Error:', error);
if (error.response?.status === 401) {
// 处理认证失败
localStorage.removeItem('auth_token');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
// 设备管理API
export const javaDeviceApi = {
// 获取所有设备 - 对应后端 /devices/devices/list
async getDevices(): Promise<Device[]> {
const response: any = await apiClient.get('/devices/devices/list');
// 后端返回的是 TableDataInfo 格式,需要从 rows 中获取数据
return response.rows || [];
},
// 获取设备详情(包含最新电池数据)
async getDeviceDetail(deviceId: string): Promise<DeviceDetail | null> {
try {
const response: ApiResponse<DeviceDetail> = await apiClient.get(`/devices/devices/${deviceId}/detail`);
return response.data;
} catch (error: any) {
if (error.response?.status === 404) {
return null;
}
throw error;
}
},
// 创建设备
async createDevice(device: CreateDeviceRequest): Promise<Device> {
// 将表单的 snake_case 字段映射为后端期望的 camelCase 字段
const payload = {
deviceId: device.device_id,
deviceName: device.device_name,
deviceType: device.device_type,
deviceSn: device.device_sn,
status: device.status,
ipAddress: device.ip_address,
firmwareVersion: device.firmware_version,
};
const response: ApiResponse<Device> = await apiClient.post('/devices/devices', payload);
return response.data;
},
// 更新设备
async updateDevice(id: string, updates: UpdateDeviceRequest): Promise<Device> {
// 将更新请求的 snake_case 字段映射为后端期望的 camelCase 字段,并移除未定义的字段
const payload: any = { id };
if (updates.device_name !== undefined) payload.deviceName = updates.device_name;
if (updates.status !== undefined) payload.status = updates.status;
if (updates.device_type !== undefined) payload.deviceType = updates.device_type;
if (updates.device_sn !== undefined) payload.deviceSn = updates.device_sn;
if (updates.ip_address !== undefined) payload.ipAddress = updates.ip_address;
if (updates.firmware_version !== undefined) payload.firmwareVersion = updates.firmware_version;
const response: ApiResponse<Device> = await apiClient.put(`/devices/devices`, payload);
return response.data;
},
// 删除设备 - 使用数据库主键id
async deleteDevice(id: string): Promise<void> {
await apiClient.delete(`/devices/devices/${id}`);
},
// 批量删除设备 - 使用数据库主键id数组
async batchDeleteDevices(ids: string[]): Promise<void> {
await apiClient.post('/devices/devices/batch-delete', { ids });
},
// 获取设备分页列表 - 使用后端的分页查询
async getDevicesPaginated(page: number = 1, pageSize: number = 10, status?: string, keyword?: string): Promise<PaginatedResponse<Device>> {
const params: any = {
pageNum: page, // 后端使用 pageNum
pageSize: pageSize
};
if (status && status !== 'all') params.status = status;
if (keyword) params.deviceName = keyword; // 假设后端使用 deviceName 参数搜索
const response: any = await apiClient.get('/devices/devices/list', { params });
// 转换后端 TableDataInfo 格式到前端 PaginatedResponse 格式
return {
data: response.rows || [],
total: response.total || 0,
page: page,
pageSize: pageSize
};
}
};
// 电池数据API
export const javaBatteryDataApi = {
// 获取设备的电池数据
async getBatteryData(deviceId: string, limit = 100): Promise<BatteryData[]> {
const response: ApiResponse<BatteryData[]> = await apiClient.get(`/battery-data/${deviceId}`, {
params: { limit }
});
return response.data;
},
// 获取实时图表数据
/**
* 获取实时图表数据(兼容两种返回):
* - 直接数组 List<ChartDataPoint>
* - 包装对象 { data: ChartDataPoint[] }
*/
async getChartData(deviceId: string, hours = 24): Promise<ChartDataPoint[]> {
const response: any = await apiClient.get(`/battery-data/${deviceId}/chart`, {
params: { hours }
});
if (Array.isArray(response)) return response;
return response?.data ?? [];
},
// 添加电池数据
async addBatteryData(data: Omit<BatteryData, "id" | "timestamp">): Promise<BatteryData> {
const response: ApiResponse<BatteryData> = await apiClient.post('/battery-data', data);
return response.data;
},
// 获取设备最新电池数据
async getLatestBatteryData(deviceId: string): Promise<BatteryData | null> {
try {
const response: any = await apiClient.get(`/battery-data/${deviceId}/latest`);
return response?.data ?? response ?? null;
} catch (error: any) {
if (error.response?.status === 404) {
return null;
}
throw error;
}
}
};
/**
* 极值数据 API
* 路由:
* - GET /extreme-values/{deviceId}/latest 最新记录
* - GET /extreme-values/{deviceId}?limit= 按设备查询
* - GET /extreme-values/{deviceId}/range?start=&end= 时间范围查询ISO 字符串)
* - POST /extreme-values 新增记录
*/
export const javaExtremeValuesApi = {
async getLatestByDevice(deviceId: string, before?: number | string | Date): Promise<ExtremeValues | null> {
try {
let paramBefore: any = undefined;
if (before !== undefined) {
if (before instanceof Date) paramBefore = before.getTime();
else if (typeof before === 'number') paramBefore = before;
else paramBefore = before;
}
const response: any = await apiClient.get(`/extreme-values/${deviceId}/latest`, { params: paramBefore !== undefined ? { before: paramBefore } : undefined });
return response?.data ?? response ?? null;
} catch (error: any) {
if (error.response?.status === 404) {
return null;
}
throw error;
}
},
/** 按设备查询极值列表limit 默认 100 */
async getByDevice(deviceId: string, limit = 100): Promise<ExtremeValues[]> {
const response: any = await apiClient.get(`/extreme-values/${deviceId}`, { params: { limit } });
return Array.isArray(response) ? response : (response?.data ?? []);
},
/** 按时间范围查询start/end 为 ISO 字符串 */
async getRange(deviceId: string, start: string, end: string): Promise<ExtremeValues[]> {
const response: any = await apiClient.get(`/extreme-values/${deviceId}/range`, { params: { start, end } });
return Array.isArray(response) ? response : (response?.data ?? []);
},
/** 新增一条极值记录 */
async addRecord(payload: Omit<ExtremeValues, "id" | "createTime" | "updateTime" | "createBy" | "updateBy">): Promise<ExtremeValues> {
const response: ApiResponse<ExtremeValues> = await apiClient.post('/extreme-values', payload);
return response.data;
}
};
/**
* 车辆位置 API
* 路由:
* - GET /vehicle-location/{deviceId}/latest 最新位置
* - GET /vehicle-location/{deviceId}?limit=&start=&end= 轨迹查询
* - POST /vehicle-location 新增位置
*/
export const javaVehicleLocationApi = {
/** 获取设备最新位置404 返回 null */
async getLatestByDevice(deviceId: string): Promise<VehicleLocation | null> {
try {
const response: any = await apiClient.get(`/vehicle-location/${deviceId}/latest`);
return response?.data ?? response ?? null;
} catch (error: any) {
if (error.response?.status === 404) {
return null;
}
throw error;
}
},
/** 按时间范围或限制数量查询轨迹start/end 为 ISO 字符串 */
async getTrack(deviceId: string, start?: string, end?: string, limit = 500): Promise<VehicleLocation[]> {
const params: any = { limit };
if (start) params.start = start;
if (end) params.end = end;
const response: any = await apiClient.get(`/vehicle-location/${deviceId}`, { params });
return Array.isArray(response) ? response : (response?.data ?? []);
},
/** 新增位置记录 */
async addLocation(payload: Omit<VehicleLocation, "id" | "createdAt" | "updatedAt">): Promise<VehicleLocation> {
const response: ApiResponse<VehicleLocation> = await apiClient.post('/vehicle-location', payload);
return response.data;
}
};
/**
* 整车数据 API
* 路由:
* - GET /vehicle-data/{deviceId}/latest 最新整车数据
* - GET /vehicle-data/{deviceId}?limit= 按设备查询
* - GET /vehicle-data/{deviceId}/range?start=&end= 时间范围查询
* - POST /vehicle-data 新增记录
*/
export const javaVehicleDataApi = {
/** 获取设备最新整车数据404 返回 null */
async getLatestByDevice(deviceId: string): Promise<VehicleData | null> {
try {
const response: any = await apiClient.get(`/vehicle-data/${deviceId}/latest`);
return response?.data ?? response ?? null;
} catch (error: any) {
if (error.response?.status === 404) {
return null;
}
throw error;
}
},
/** 按设备查询整车数据列表limit 默认 100 */
async getByDevice(deviceId: string, limit = 100): Promise<VehicleData[]> {
const response: any = await apiClient.get(`/vehicle-data/${deviceId}`, { params: { limit } });
return Array.isArray(response) ? response : (response?.data ?? []);
},
/** 按时间范围查询start/end 为 ISO 字符串 */
async getRange(deviceId: string, start: string, end: string): Promise<VehicleData[]> {
const response: any = await apiClient.get(`/vehicle-data/${deviceId}/range`, { params: { start, end } });
return Array.isArray(response) ? response : (response?.data ?? []);
},
/** 新增整车数据记录 */
async addRecord(payload: Omit<VehicleData, "id" | "createdAt" | "updatedAt">): Promise<VehicleData> {
const response: ApiResponse<VehicleData> = await apiClient.post('/vehicle-data', payload);
return response.data;
}
};
/**
* 子系统电压 API
* 路由:
* - GET /subsystem-voltage/{deviceId}/{subsystemNo}/latest 最新帧
* - GET /subsystem-voltage/{deviceId}/{subsystemNo}?limit= 按子系统查询
* - GET /subsystem-voltage/{deviceId}/{subsystemNo}/range?start=&end= 时间范围查询
* - POST /subsystem-voltage 新增帧
*/
export const javaSubsystemVoltageApi = {
/** 获取指定子系统最新电压帧404 返回 null */
async getLatest(deviceId: string, subsystemNo: number): Promise<SubsystemVoltage | null> {
try {
const response: any = await apiClient.get(`/subsystem-voltage/${deviceId}/${subsystemNo}/latest`);
return response?.data ?? response ?? null;
} catch (error: any) {
if (error.response?.status === 404) {
return null;
}
throw error;
}
},
/** 按子系统查询电压帧列表limit 默认 100 */
async getBySubsystem(deviceId: string, subsystemNo: number, limit = 100): Promise<SubsystemVoltage[]> {
const response: any = await apiClient.get(`/subsystem-voltage/${deviceId}/${subsystemNo}`, { params: { limit } });
return Array.isArray(response) ? response : (response?.data ?? []);
},
/** 按时间范围查询start/end 为 ISO 字符串 */
async getRange(deviceId: string, subsystemNo: number, start: string, end: string): Promise<SubsystemVoltage[]> {
const response: any = await apiClient.get(`/subsystem-voltage/${deviceId}/${subsystemNo}/range`, { params: { start, end } });
return Array.isArray(response) ? response : (response?.data ?? []);
},
/** 新增电压帧 */
async addFrame(payload: Omit<SubsystemVoltage, "id" | "createdAt" | "updatedAt">): Promise<SubsystemVoltage> {
const response: ApiResponse<SubsystemVoltage> = await apiClient.post('/subsystem-voltage', payload);
return response.data;
}
};
/**
* 子系统温度 API
* 路由:
* - GET /subsystem-temperature/{deviceId}/{subsystemNo}/latest 最新帧
* - GET /subsystem-temperature/{deviceId}/{subsystemNo}?limit= 按子系统查询
* - GET /subsystem-temperature/{deviceId}/{subsystemNo}/range?start=&end= 时间范围查询
* - POST /subsystem-temperature 新增帧
*/
export const javaSubsystemTemperatureApi = {
/** 获取指定子系统最新温度帧404 返回 null */
async getLatest(deviceId: string, subsystemNo: number): Promise<SubsystemTemperature | null> {
try {
const response: any = await apiClient.get(`/subsystem-temperature/${deviceId}/${subsystemNo}/latest`);
return response?.data ?? response ?? null;
} catch (error: any) {
if (error.response?.status === 404) {
return null;
}
throw error;
}
},
/** 按子系统查询温度帧列表limit 默认 100 */
async getBySubsystem(deviceId: string, subsystemNo: number, limit = 100): Promise<SubsystemTemperature[]> {
const response: any = await apiClient.get(`/subsystem-temperature/${deviceId}/${subsystemNo}`, { params: { limit } });
return Array.isArray(response) ? response : (response?.data ?? []);
},
/** 按时间范围查询start/end 为 ISO 字符串 */
async getRange(deviceId: string, subsystemNo: number, start: string, end: string): Promise<SubsystemTemperature[]> {
const response: any = await apiClient.get(`/subsystem-temperature/${deviceId}/${subsystemNo}/range`, { params: { start, end } });
return Array.isArray(response) ? response : (response?.data ?? []);
},
/** 新增温度帧 */
async addFrame(payload: Omit<SubsystemTemperature, "id" | "createdAt" | "updatedAt">): Promise<SubsystemTemperature> {
const response: ApiResponse<SubsystemTemperature> = await apiClient.post('/subsystem-temperature', payload);
return response.data;
}
};
// OTA任务API
export const javaOtaApi = {
// 列表查询(后端返回 TableDataInfo需要从 rows 取数组)
async getTasks(params?: Record<string, any>): Promise<OtaTask[]> {
const response: any = await apiClient.get('/ota/tasks/list', { params });
return response.rows || [];
},
// 获取任务详情
async getTaskById(id: string): Promise<OtaTask> {
const response: any = await apiClient.get(`/ota/tasks/${id}`);
return response.data ?? response;
},
// 创建任务
async createTask(task: CreateOtaTaskRequest): Promise<any> {
// 将表单的 snake_case 字段映射为后端期望的 camelCase 字段
const payload = {
deviceId: task.device_id,
taskName: task.task_name,
firmwareVersion: task.firmware_version,
};
const response: any = await apiClient.post('/ota/tasks', payload);
return response;
},
// 修改任务
async updateTask(task: Partial<OtaTask> & { id: string }): Promise<any> {
const response: any = await apiClient.put('/ota/tasks', task);
return response;
},
// 批量删除
async deleteTasks(ids: string[]): Promise<any> {
const idPath = ids.join(',');
const response: any = await apiClient.delete(`/ota/tasks/${idPath}`);
return response;
},
// 导出任务列表Excel
async exportTasks(params?: Record<string, any>): Promise<Blob> {
const response = await apiClient.post('/ota/tasks/export', params, { responseType: 'blob' });
return response as unknown as Blob;
}
};
// MQTT日志API
export const javaMqttApi = {
// 获取MQTT日志
async getLogs(deviceId?: string, limit = 100): Promise<MqttLog[]> {
const params: any = { limit };
if (deviceId) params.deviceId = deviceId;
const response: ApiResponse<MqttLog[]> = await apiClient.get('/mqtt/logs', { params });
return response.data;
},
// 添加MQTT日志
async addLog(log: Omit<MqttLog, "id" | "timestamp">): Promise<MqttLog> {
const response: ApiResponse<MqttLog> = await apiClient.post('/mqtt/logs', log);
return response.data;
}
};
// 系统配置API
export const javaSystemApi = {
// 获取所有配置
async getConfigs(): Promise<SystemConfig[]> {
const response: ApiResponse<SystemConfig[]> = await apiClient.get('/system/configs');
return response.data;
},
// 获取单个配置
async getConfig(key: string): Promise<SystemConfig | null> {
try {
const response: ApiResponse<SystemConfig> = await apiClient.get(`/system/configs/${key}`);
return response.data;
} catch (error: any) {
if (error.response?.status === 404) {
return null;
}
throw error;
}
},
// 更新配置
async updateConfig(key: string, updates: UpdateConfigRequest): Promise<SystemConfig> {
const response: ApiResponse<SystemConfig> = await apiClient.put(`/system/configs/${key}`, updates);
return response.data;
},
// 获取仪表盘统计数据
async getDashboardStats(): Promise<DashboardStats> {
const response: ApiResponse<DashboardStats> = await apiClient.get('/system/dashboard-stats');
return response.data;
}
};
export const javaAuthApi = {
async login(body: { username: string; password: string; code?: string; uuid?: string }): Promise<string> {
const response: any = await apiClient.post('/login', body);
const token = response?.token ?? response?.data?.token ?? response?.data;
if (!token || typeof token !== 'string') throw new Error('Login failed: token missing');
localStorage.setItem('auth_token', token);
return token;
},
async getCaptcha(): Promise<{ img: string; uuid: string }> {
const response: any = await apiClient.get('/captchaImage');
const data = response?.data ?? response;
const img = data?.img || data?.captcha || '';
const uuid = data?.uuid || data?.codeKey || '';
if (!img || !uuid) throw new Error('Captcha fetch failed');
const dataUrl = img.startsWith('data:') ? img : `data:image/jpeg;base64,${img}`;
return { img: dataUrl, uuid };
},
async getInfo(): Promise<any> {
const response: any = await apiClient.get('/getInfo');
return response?.data ?? response;
},
async getRouters(): Promise<any> {
const response: any = await apiClient.get('/getRouters');
return response?.data ?? response;
}
};
// 导出所有API
// 聚合导出:设备/电池/OTA/MQTT/系统 + 极值/位置/整车/子系统电压/子系统温度
export const javaApi = {
device: javaDeviceApi,
batteryData: javaBatteryDataApi,
ota: javaOtaApi,
mqtt: javaMqttApi,
system: javaSystemApi,
auth: javaAuthApi,
extremeValues: javaExtremeValuesApi,
vehicleLocation: javaVehicleLocationApi,
vehicleData: javaVehicleDataApi,
subsystemVoltage: javaSubsystemVoltageApi,
subsystemTemperature: javaSubsystemTemperatureApi
};
export default javaApi;