diff --git a/.env b/.env index 89222c7..d0dd837 100644 --- a/.env +++ b/.env @@ -1,4 +1,11 @@ -VITE_SUPABASE_URL=https://backend.appmiaoda.com/projects/supabase233847254023151616 -VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoyMDc1MTgwNzMwLCJpc3MiOiJzdXBhYmFzZSIsInJvbGUiOiJhbm9uIiwic3ViIjoiYW5vbiJ9.eBXTY-SvuVf-rynRmP4VgudnhbUtqYquoEPbGp_l-HM +# 使用本地/自有 Supabase(默认本地 CLI 端口) +VITE_SUPABASE_URL=http://192.168.5.200:54321 +# 将下面的 anon key 替换为你本地 supabase 启动时输出的 Anonymous Key +# 示例:在终端执行 `npx supabase@latest start` 后,CLI 会打印 Anonymous API Key +VITE_SUPABASE_ANON_KEY=REPLACE_WITH_LOCAL_SUPABASE_ANON_KEY + VITE_APP_ID=app-6qcydjtbzwu9 +# Java 后端 API 基础地址(若依服务),开发环境可指向 http://localhost:8080 +VITE_JAVA_API_BASE_URL=http://192.168.5.200:8080 +VITE_COMPANY_LOGO_URL=/images/logo/evo-tech.png diff --git a/.trae/documents/为修改部分添加注释.md b/.trae/documents/为修改部分添加注释.md new file mode 100644 index 0000000..ebb00c9 --- /dev/null +++ b/.trae/documents/为修改部分添加注释.md @@ -0,0 +1,111 @@ +## 目标 + +为本次新增/修改的代码片段补充清晰、中文的注释,说明数据来源、字段含义、单位及接口用途,便于后续联调和维护。 + +## 注释范围 + +1. `src/types/types.ts` 中新增的五个接口:`ExtremeValues`、`VehicleLocation`、`VehicleData`、`SubsystemVoltage`、`SubsystemTemperature` +2. `src/api/javaApi.ts` 中新增的五个 API 模块:`javaExtremeValuesApi`、`javaVehicleLocationApi`、`javaVehicleDataApi`、`javaSubsystemVoltageApi`、`javaSubsystemTemperatureApi`,以及聚合导出对象处的说明 +3. `.env` 中的 `VITE_JAVA_API_BASE_URL` + +## 注释规范 + +* 语言:中文,与现有文件注释风格一致 + +* 形式: + + * 文件/模块级 JSDoc(`/** ... */`)用于整体说明与路由清单 + + * 字段级行注释(`// ...`)用于标注单位、含义、取值约束 + +* 不引入冗余内容,仅覆盖本次修改部分 + +## 具体变更(按文件) + +### 1) src/types/types.ts + +* 在每个新增接口顶部添加 JSDoc,标注与 SQL 表的映射关系与单位(如电压 V、温度 ℃、经纬度小数位精度)。 + +* 为关键字段添加行注释: + + * `timestamp`:UTC ISO 字符串时间戳 + + * `longitude`/`latitude`:经纬度,保留 7 位小数 + + * `vehicleStatus`/`chargingStatus`/`operationMode`:取值含义(按后端约定) + + * `batteryVoltages`/`temperatureValues`:数组单位与含义 + +示例(片段): + +```ts +/** + * 电池极值数据(表:extreme_values) + * 电压单位:V;温度单位:℃;时间戳:UTC ISO 字符串 + */ +export interface ExtremeValues { + id: string; + deviceId: string; // 设备ID + timestamp: string; // 数据时间戳(UTC ISO) + maxVoltageSubsystemNo?: number; // 最高电压子系统号 + maxVoltageBatteryNo?: number; // 最高电压单体代号 + maxVoltageValue?: number; // 最高电压值(V) + ... +} +``` + +### 2) src/api/javaApi.ts + +* 在每个新增 API 模块定义前添加模块级 JSDoc,列出路由、用途、返回结构(`ApiResponse` 或数组),并注明 404 时返回 `null` 的约定。 + +* 在各方法上添加简短 JSDoc:参数说明(`deviceId`、`subsystemNo`、`start/end` 格式)、返回类型含义、列表 `limit` 默认值。 + +* 在聚合导出对象处添加一行注释,说明已包含设备/电池/OTA/MQTT/系统以及新增数据域模块。 + +示例(片段): + +```ts +/** + * 极值数据 API + * 路由: + * - GET /extreme-values/{deviceId}/latest 最新记录 + * - GET /extreme-values/{deviceId}?limit= 按设备查询 + * - GET /extreme-values/{deviceId}/range?start=&end= 时间范围查询 + * - POST /extreme-values 新增记录 + */ +export const javaExtremeValuesApi = { + /** 获取设备最新极值;404 返回 null */ + async getLatestByDevice(deviceId: string): Promise { ... }, + /** 按设备查询极值列表,limit 默认 100 */ + async getByDevice(deviceId: string, limit = 100): Promise { ... }, + /** 按时间范围查询,start/end 为 ISO 字符串 */ + async getRange(deviceId: string, start: string, end: string): Promise { ... }, + /** 新增一条极值记录 */ + async addRecord(payload: Omit): Promise { ... } +}; + +// 聚合导出:设备/电池/OTA/MQTT/系统 + 极值/位置/整车/子系统电压/子系统温度 +export const javaApi = { ... }; +``` + +### 3) .env + +* 在 `VITE_JAVA_API_BASE_URL` 上方添加一行注释,说明为若依后端基础地址,可按环境切换。 + +示例(片段): + +```env +# Java 后端 API 基础地址(若依服务),开发环境可指向 http://localhost:8080 +VITE_JAVA_API_BASE_URL=http://localhost:8080 +``` + +## 验证与影响 + +* 仅添加注释,不改动逻辑与类型签名;不会影响编译与运行。 + +* 重新运行类型检查,确保无语法问题。 + +## 需要确认 + +* 是否按上述规范为本次新增的所有片段添加注释;如需 + diff --git a/.trae/documents/修复登录页 Logo 不显示.md b/.trae/documents/修复登录页 Logo 不显示.md new file mode 100644 index 0000000..d902903 --- /dev/null +++ b/.trae/documents/修复登录页 Logo 不显示.md @@ -0,0 +1,24 @@ +## 原因分析 +- 代码使用的静态资源路径是 `/logo/...`,但实际文件位于 `public/images/logo/...`,导致 404 并触发 `onError` 的文本回退。 +- 参照 `src/pages/Login.tsx:8-10` 的候选路径与 `.env` 的 `VITE_COMPANY_LOGO_URL=/logo/auth-logo.svg` 都指向错误目录。 +- `public` 目录结构为:`public/images/logo/{auth-logo.svg,evo-tech.png,logo-dark.svg,logo-icon.svg}`。 + +## 修改方案 +- 统一将所有 Logo 路径改为 `public/images/logo/...`。 +- 更新环境变量默认值与候选列表,确保回退链条都指向存在的文件。 +- 提升兼容性:使用 `import.meta.env.BASE_URL` 拼接公共目录,避免非根路径部署时的资源 404。 + +## 具体改动 +- `.env`:将 `VITE_COMPANY_LOGO_URL` 改为 `/images/logo/auth-logo.svg`。 +- `src/pages/Login.tsx:8-10`: + - 将 `const companyLogo = (import.meta.env as any).VITE_COMPANY_LOGO_URL || '/logo/evo-tech.png';` 改为使用 BASE_URL 并指向 `images/logo/evo-tech.png`。 + - 将 `candidates` 改为 `['/images/logo/evo-tech.png','/images/logo/auth-logo.svg','/images/logo/logo-dark.svg']` 的 BASE_URL 版本。 +- 构造公共前缀:`const publicBase = (import.meta.env as any).BASE_URL || '/';`,候选路径使用 ``${publicBase}images/logo/...``。 + +## 验证步骤 +- 启动开发服务器,打开登录页,确认图片请求返回 200 且 Logo 正常显示。 +- 暂时将 `VITE_COMPANY_LOGO_URL` 设为一个不存在的路径,验证回退顺序依次生效,最终仍能显示有效 Logo;全部失败时回退到文本徽标 `EVO`。 +- 检查控制台与网络面板,确保无 `404` 与无跨域错误。 + +## 影响范围 +- 仅登录页 Logo 显示路径与默认值,其他功能不受影响。 \ No newline at end of file diff --git a/.trae/documents/修复首页图表接口返回兼容问题.md b/.trae/documents/修复首页图表接口返回兼容问题.md new file mode 100644 index 0000000..63b8027 --- /dev/null +++ b/.trae/documents/修复首页图表接口返回兼容问题.md @@ -0,0 +1,25 @@ +## 目标 +让 `javaBatteryDataApi.getChartData` 同时兼容两种后端返回: +- 直接数组 `List` +- 包装对象 `{ data: ChartDataPoint[] }` +从而首页刷新时能正常拿到数据并绘图。 + +## 修改点 +- 文件:`src/api/javaApi.ts` +- 方法:`javaBatteryDataApi.getChartData` +- 调整实现: +```ts +async getChartData(deviceId: string, hours = 24): Promise { + const response: any = await apiClient.get(`/battery-data/${deviceId}/chart`, { params: { hours } }); + if (Array.isArray(response)) return response; // 兼容直接数组 + return response?.data ?? []; // 兼容 { data: [] } +} +``` +- 添加简短注释(中文),说明该方法兼容两种返回结构。 + +## 验证 +- 重新加载首页,Network 面板应出现 `GET /battery-data/{deviceId}/chart?hours=24` +- 电压/电流两张图能显示数据,无控制台错误。 + +## 影响范围 +- 仅修改图表数据接口读取逻辑;不影响其他模块。 \ No newline at end of file diff --git a/.trae/documents/前端兼容最新接口的返回结构.md b/.trae/documents/前端兼容最新接口的返回结构.md new file mode 100644 index 0000000..bb3c656 --- /dev/null +++ b/.trae/documents/前端兼容最新接口的返回结构.md @@ -0,0 +1,23 @@ +## 目标 +让前端 Java API 封装兼容你后端“直接返回对象/数组”的最新接口,从而实时监控页面能正确显示整车、位置、极值、子系统数据。 + +## 修改点(src/api/javaApi.ts) +- 更新以下方法的返回值处理逻辑: + - `javaExtremeValuesApi.getLatestByDevice` → 兼容返回对象或 `{data:对象}` + - `javaVehicleLocationApi.getLatestByDevice` → 兼容返回对象或 `{data:对象}` + - `javaVehicleDataApi.getLatestByDevice` → 兼容返回对象或 `{data:对象}` + - `javaSubsystemVoltageApi.getLatest` → 兼容返回对象或 `{data:对象}` + - `javaSubsystemTemperatureApi.getLatest` → 兼容返回对象或 `{data:对象}` + - `javaVehicleLocationApi.getTrack` → 兼容返回数组或 `{data:数组}` + +## 具体实现 +- 对象型返回:`const r: any = await apiClient.get(...); return r?.data ?? r ?? null;` +- 数组型返回:`const r: any = await apiClient.get(...); return Array.isArray(r) ? r : (r?.data ?? []);` +- 保留 404 → `null` 的处理逻辑。 + +## 验证 +- 打开实时监控页,切换设备与子系统号,确认卡片与子系统帧图均有数据。 +- Network 中对应接口返回为 JSON(非被 401/登录页替代)。 + +## 影响范围 +- 仅调整返回解析逻辑;不改变接口路径或页面结构。 \ No newline at end of file diff --git a/.trae/documents/完善实时监控页面接口对接与展示.md b/.trae/documents/完善实时监控页面接口对接与展示.md new file mode 100644 index 0000000..f30a19e --- /dev/null +++ b/.trae/documents/完善实时监控页面接口对接与展示.md @@ -0,0 +1,49 @@ +## 目标 +在实时监控页面接入并展示五类数据:整车数据、车辆位置、极值数据、子系统温度、子系统电压;支持设备选择与时间范围刷新。 + +## 接口与数据源 +- 整车数据:`javaVehicleDataApi.getLatestByDevice(deviceId)`、`getByDevice(deviceId, limit)` +- 车辆位置:`javaVehicleLocationApi.getLatestByDevice(deviceId)`、`getTrack(deviceId, start, end, limit)` +- 极值数据:`javaExtremeValuesApi.getLatestByDevice(deviceId)` +- 子系统电压:`javaSubsystemVoltageApi.getLatest(deviceId, subsystemNo)`、`getBySubsystem(deviceId, subsystemNo, limit)` +- 子系统温度:`javaSubsystemTemperatureApi.getLatest(deviceId, subsystemNo)`、`getBySubsystem(deviceId, subsystemNo, limit)` + +## 页面改造 +1. 修复设备选择值 +- 下拉 `value` 使用 `device.deviceId` +- 选择回调按 `deviceId` 查找设备并刷新数据 + +2. 新增状态 +- `vehicleLatest: VehicleData | null` +- `locationLatest: VehicleLocation | null` +- `extremeLatest: ExtremeValues | null` +- `subsystemNoVolt: number`、`subsystemNoTemp: number` +- `subsystemVoltLatest: SubsystemVoltage | null` +- `subsystemTempLatest: SubsystemTemperature | null` + +3. 加载逻辑 +- 在 `loadDeviceData` 并行请求:整车/位置/极值/子系统(默认子系统号 1)最新数据 +- 在 `loadChartData` 继续使用 `javaBatteryDataApi.getChartData(deviceId, timeRange)` 用于电压/电流趋势 +- 根据 `timeRange` 可选调用 `getTrack` 获取位置轨迹(后续可选) + +4. 展示布局 +- 顶部卡片: + - 展示整车数据:总电压(V)、总电流(A)、SOC(%)、车速(km/h)、里程(km) + - 极值数据:最高/最低单体电压与温度(值与所属子系统/探针) +- 图表分区: + - 电压/电流趋势(已接入) + - 子系统温度:折线图展示 `temperatureValues`(℃) + - 子系统电压:列表或柱状图展示 `batteryVoltages`(V) +- 位置分区(可选): + - 若配置了百度地图 AK(`VITE_BAIDU_MAP_AK`),使用现有 `Map` 组件展示最新位置;否则展示经纬度与定位状态 + +5. 注释 +- 在新增接口调用处与关键渲染处添加简短中文注释(来源、单位、参数说明) + +## 验证 +- 设备切换与时间范围刷新可见对应数据与图表更新 +- Network 面板出现五类接口请求;无 401/跨域错误 + +## 可选后续 +- 在页面上增加选择子系统号的下拉,便于查看指定子系统的温度与电压 +- 在位置分区展示最近轨迹折线(调用 `getTrack`) \ No newline at end of file diff --git a/.trae/documents/首页电压_电流图表对接 Java 后端.md b/.trae/documents/首页电压_电流图表对接 Java 后端.md new file mode 100644 index 0000000..709b953 --- /dev/null +++ b/.trae/documents/首页电压_电流图表对接 Java 后端.md @@ -0,0 +1,34 @@ +## 目标 + +将首页(Dashboard)电压/电流趋势图数据源改为后端 Java 图表接口:`javaBatteryDataApi.getChartData(deviceId, hours)`,展示 24 小时的 `voltage` 与 `current`。 + +## 修改点 + +1. `src/pages/Dashboard.tsx` + +* 引入 `ChartDataPoint` 类型 + +* 将 `recentBatteryData` 状态替换为 `chartData: ChartDataPoint[]` + +* 在 `loadDashboardData` 里,获取设备后调用 `javaBatteryDataApi.getChartData(devicesData[0].deviceId, 24)` 赋值给 `chartData` + +* 将第二张图改为“电流趋势”,`dataKey="current"`,单位 `A` + +* 在上述改动处添加中文注释(说明数据来源、单位与时间范围) + +## 接口约定 + +* 路由:`GET /battery-data/{deviceId}/chart?hours=`(已封装在 `javaBatteryDataApi.getChartData`) + +* 返回:`ChartDataPoint[]`(含 `timestamp`, `voltage`, `current`) + +* 参数:使用设备列表中的第一个设备,时间范围默认 24 小时 + +## 验证 + +* 类型检查通过,首页能显示电压与电流两张趋势图;无影响其他页面逻辑 + +## 后续可选 + +* 增加设备与时间范围选择器,以便用户切换设备与窗口大小 + diff --git a/README.md b/README.md index 55f60b7..b34e80a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +# 欢迎使用你的秒哒应用代码包 +秒哒应用链接 + URL:https://www.miaoda.cn/projects/app-6qcydjtbzwu9 + # 电池管理系统 (Battery Management System) ## 项目介绍 diff --git a/docs/prd.md b/docs/prd.md index 63fe72b..222f9d0 100644 --- a/docs/prd.md +++ b/docs/prd.md @@ -49,7 +49,39 @@ VUE 3框架 - 便捷的设备管理和操作界面 - 完善升级任务监控和日志查看功能 -## 4. 参考文件 +## 4. 接口文档 + +### 4.1 设备管理接口 +- 设备登录授权接口 +- 设备状态查询接口 +- 设备列表获取接口 +- 设备信息更新接口 + +### 4.2 数据传输接口 +- 实时电池数据获取接口 +- 历史数据查询接口 +- 数据统计分析接口 +- 设备状态数据接口 + +### 4.3 OTA升级接口 +- 固件版本查询接口 +- OTA升级任务创建接口 +- 升级进度查询接口 +- 升级日志获取接口 + +### 4.4 通信管理接口 +- MQTT连接状态接口 +- 消息发送接口 +- 消息接收状态查询接口 +- 通信日志接口 + +### 4.5 系统配置接口 +- 系统参数配置接口 +- 配置信息获取接口 +- 用户权限管理接口 +- 系统状态监控接口 + +## 5. 参考文件 参考文件路径: 1. 动力电池端采集设备与平台交互接口规范 - 羿动新能源科技有限公司技术文档 \ No newline at end of file diff --git a/package.json b/package.json index 3527a8b..57e3ff7 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "scripts": { "dev": "echo 'Do not use this command, only use lint to check'", "build": "echo 'Do not use this command, only use lint to check'", - "lint": "tsgo -p tsconfig.check.json; npx biome lint; rules/check.sh" + "lint": "tsgo -p tsconfig.check.json; biome lint --only=correctness/noUndeclaredDependencies; ast-grep scan" }, "dependencies": { "@radix-ui/react-accordion": "^1.2.8", @@ -81,7 +81,8 @@ "tailwindcss": "^3.4.11", "typescript": "~5.7.2", "vite": "^5.1.4", - "vite-plugin-svgr": "^4.3.0" + "vite-plugin-svgr": "^4.3.0", + "miaoda-sc-plugin": "^1.0.4" }, "overrides": { "react-helmet-async": { diff --git a/public/images/logo/evo-tech.png b/public/images/logo/evo-tech.png new file mode 100644 index 0000000..bc227bf Binary files /dev/null and b/public/images/logo/evo-tech.png differ diff --git a/rules/check.sh b/rules/check.sh old mode 100755 new mode 100644 diff --git a/src/App.tsx b/src/App.tsx index c338e30..3546c43 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,29 +1,33 @@ import React from 'react'; -import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; +import { BrowserRouter as Router, Routes, Route, Navigate, useLocation } from 'react-router-dom'; import { Toaster } from 'sonner'; import routes from './routes'; import Header from './components/common/Header'; +const AppContent: React.FC = () => { + const location = useLocation(); + const hideHeader = location.pathname === '/login'; + return ( +
+ {!hideHeader &&
} +
+ + {routes.map((route, index) => ( + + ))} + } /> + +
+ +
+ ); +}; + const App: React.FC = () => { return ( -
-
-
- - {routes.map((route, index) => ( - - ))} - } /> - -
- -
+
); }; diff --git a/src/api/javaApi.ts b/src/api/javaApi.ts new file mode 100644 index 0000000..7ae2f02 --- /dev/null +++ b/src/api/javaApi.ts @@ -0,0 +1,529 @@ +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 { + const response: any = await apiClient.get('/devices/devices/list'); + // 后端返回的是 TableDataInfo 格式,需要从 rows 中获取数据 + return response.rows || []; + }, + + // 获取设备详情(包含最新电池数据) + async getDeviceDetail(deviceId: string): Promise { + try { + const response: ApiResponse = 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 { + // 将表单的 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 = await apiClient.post('/devices/devices', payload); + return response.data; + }, + + // 更新设备 + async updateDevice(id: string, updates: UpdateDeviceRequest): Promise { + // 将更新请求的 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 = await apiClient.put(`/devices/devices`, payload); + return response.data; + }, + + // 删除设备 - 使用数据库主键id + async deleteDevice(id: string): Promise { + await apiClient.delete(`/devices/devices/${id}`); + }, + + // 批量删除设备 - 使用数据库主键id数组 + async batchDeleteDevices(ids: string[]): Promise { + await apiClient.post('/devices/devices/batch-delete', { ids }); + }, + + // 获取设备分页列表 - 使用后端的分页查询 + async getDevicesPaginated(page: number = 1, pageSize: number = 10, status?: string, keyword?: string): Promise> { + 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 { + const response: ApiResponse = await apiClient.get(`/battery-data/${deviceId}`, { + params: { limit } + }); + return response.data; + }, + + // 获取实时图表数据 + /** + * 获取实时图表数据(兼容两种返回): + * - 直接数组 List + * - 包装对象 { data: ChartDataPoint[] } + */ + async getChartData(deviceId: string, hours = 24): Promise { + 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): Promise { + const response: ApiResponse = await apiClient.post('/battery-data', data); + return response.data; + }, + + // 获取设备最新电池数据 + async getLatestBatteryData(deviceId: string): Promise { + 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 { + 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 { + 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 { + const response: any = await apiClient.get(`/extreme-values/${deviceId}/range`, { params: { start, end } }); + return Array.isArray(response) ? response : (response?.data ?? []); + }, + /** 新增一条极值记录 */ + async addRecord(payload: Omit): Promise { + const response: ApiResponse = 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 { + 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 { + 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): Promise { + const response: ApiResponse = 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 { + 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 { + 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 { + const response: any = await apiClient.get(`/vehicle-data/${deviceId}/range`, { params: { start, end } }); + return Array.isArray(response) ? response : (response?.data ?? []); + }, + /** 新增整车数据记录 */ + async addRecord(payload: Omit): Promise { + const response: ApiResponse = 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 { + 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 { + 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 { + 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): Promise { + const response: ApiResponse = 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 { + 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 { + 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 { + 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): Promise { + const response: ApiResponse = await apiClient.post('/subsystem-temperature', payload); + return response.data; + } +}; + +// OTA任务API +export const javaOtaApi = { + // 列表查询(后端返回 TableDataInfo,需要从 rows 取数组) + async getTasks(params?: Record): Promise { + const response: any = await apiClient.get('/ota/tasks/list', { params }); + return response.rows || []; + }, + + // 获取任务详情 + async getTaskById(id: string): Promise { + const response: any = await apiClient.get(`/ota/tasks/${id}`); + return response.data ?? response; + }, + + // 创建任务 + async createTask(task: CreateOtaTaskRequest): Promise { + // 将表单的 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 & { id: string }): Promise { + const response: any = await apiClient.put('/ota/tasks', task); + return response; + }, + + // 批量删除 + async deleteTasks(ids: string[]): Promise { + const idPath = ids.join(','); + const response: any = await apiClient.delete(`/ota/tasks/${idPath}`); + return response; + }, + + // 导出任务列表(Excel) + async exportTasks(params?: Record): Promise { + 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 { + const params: any = { limit }; + if (deviceId) params.deviceId = deviceId; + + const response: ApiResponse = await apiClient.get('/mqtt/logs', { params }); + return response.data; + }, + + // 添加MQTT日志 + async addLog(log: Omit): Promise { + const response: ApiResponse = await apiClient.post('/mqtt/logs', log); + return response.data; + } +}; + +// 系统配置API +export const javaSystemApi = { + // 获取所有配置 + async getConfigs(): Promise { + const response: ApiResponse = await apiClient.get('/system/configs'); + return response.data; + }, + + // 获取单个配置 + async getConfig(key: string): Promise { + try { + const response: ApiResponse = 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 { + const response: ApiResponse = await apiClient.put(`/system/configs/${key}`, updates); + return response.data; + }, + + // 获取仪表盘统计数据 + async getDashboardStats(): Promise { + const response: ApiResponse = await apiClient.get('/system/dashboard-stats'); + return response.data; + } +}; + +export const javaAuthApi = { + async login(body: { username: string; password: string; code?: string; uuid?: string }): Promise { + 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 { + const response: any = await apiClient.get('/getInfo'); + return response?.data ?? response; + }, + async getRouters(): Promise { + 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; \ No newline at end of file diff --git a/src/components/ui/map.tsx b/src/components/ui/map.tsx index a7066e6..c4a4a0e 100644 --- a/src/components/ui/map.tsx +++ b/src/components/ui/map.tsx @@ -99,19 +99,21 @@ let BMapGLLoadingPromise: Promise | null = null; * @param {ReactNode} children - Child components, usually MapTitle */ const Map = ({ -ak, -option, -className, -children, -...props + ak, + option, + path, + className, + children, + ...props }: React.ComponentProps<"div"> & { -ak: string; -option?: { + ak: string; + option?: { zoom: number; lng: number; lat: number; address: string; -}; + }; + path?: Array<{ lng: number; lat: number }>; }) => { const mapRef = useRef(null); const currentRef = useRef(null); @@ -141,18 +143,27 @@ const initMap = useCallback(() => { // Clear overlays map.clearOverlays(); - // Set map center coordinates and map level - const center = new (window as any).BMapGL.Point( - _options?.lng, - _options?.lat - ); + const centerPoint = (() => { + if (path && path.length > 0) { + const last = path[path.length - 1]; + return new (window as any).BMapGL.Point(last.lng, last.lat); + } + return new (window as any).BMapGL.Point(_options?.lng, _options?.lat); + })(); - map.centerAndZoom(center, _options?.zoom); + map.centerAndZoom(centerPoint, _options?.zoom); - // Add marker - const marker = new (window as any).BMapGL.Marker(center); + if (path && path.length > 0) { + const points = path.map(p => new (window as any).BMapGL.Point(p.lng, p.lat)); + const polyline = new (window as any).BMapGL.Polyline(points, { strokeColor: "#0ea5e9", strokeWeight: 4, strokeOpacity: 0.8 }); + map.addOverlay(polyline); + const marker = new (window as any).BMapGL.Marker(points[points.length - 1]); map.addOverlay(marker); -}, [_options]); + } else { + const marker = new (window as any).BMapGL.Marker(centerPoint); + map.addOverlay(marker); + } +}, [_options, path]); useEffect(() => { // Check if Baidu Map API is loaded diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index db73257..f2856be 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -2,13 +2,12 @@ import React, { useState, useEffect } from 'react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Progress } from '@/components/ui/progress'; -import { - Battery, - Zap, - Thermometer, - Activity, - Wifi, - WifiOff, +import { + Battery, + Zap, + Activity, + Wifi, + WifiOff, AlertTriangle, CheckCircle, Clock, @@ -16,15 +15,18 @@ import { Download, Server } from 'lucide-react'; -import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar } from 'recharts'; -import { dashboardApi, deviceApi, batteryDataApi, otaApi } from '@/db/api'; -import type { DashboardStats, Device, BatteryData, OtaTask } from '@/types/types'; +import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; +import { javaDeviceApi, javaBatteryDataApi, javaOtaApi, javaVehicleLocationApi } from '@/api/javaApi'; +import { Map } from '@/components/ui/map'; +import type { DashboardStats, Device, ChartDataPoint, OtaTask } from '@/types/types'; const Dashboard: React.FC = () => { const [stats, setStats] = useState(null); const [devices, setDevices] = useState([]); - const [recentBatteryData, setRecentBatteryData] = useState([]); + // 首页图表数据源:Java 图表接口返回的 24 小时数据(timestamp, voltage, current) + const [chartData, setChartData] = useState([]); const [recentTasks, setRecentTasks] = useState([]); + const [trackPoints, setTrackPoints] = useState<{ lng: number; lat: number }[]>([]); const [loading, setLoading] = useState(true); useEffect(() => { @@ -34,20 +36,40 @@ const Dashboard: React.FC = () => { const loadDashboardData = async () => { try { setLoading(true); - const [statsData, devicesData, tasksData] = await Promise.all([ - dashboardApi.getStats(), - deviceApi.getDevices(), - otaApi.getTasks() + const [devicesData, tasksData] = await Promise.all([ + javaDeviceApi.getDevices(), + javaOtaApi.getTasks() ]); - setStats(statsData); + const computedStats: DashboardStats = { + totalDevices: devicesData.length, + onlineDevices: devicesData.filter(d => d.status === 'online').length, + offlineDevices: devicesData.filter(d => d.status === 'offline').length, + maintenanceDevices: devicesData.filter(d => d.status === 'maintenance').length, + errorDevices: devicesData.filter(d => d.status === 'error').length, + activeTasks: tasksData.filter(t => t.status !== 'completed' && t.status !== 'failed').length, + completedTasks: tasksData.filter(t => t.status === 'completed').length, + failedTasks: tasksData.filter(t => t.status === 'failed').length + }; + setStats(computedStats); setDevices(devicesData); setRecentTasks(tasksData.slice(0, 5)); - // 获取最新的电池数据用于图表展示 + // 获取图表数据(24小时窗口) if (devicesData.length > 0) { - const batteryData = await batteryDataApi.getBatteryData(devicesData[0].device_id, 10); - setRecentBatteryData(batteryData.reverse()); + const data = await javaBatteryDataApi.getChartData(devicesData[0].deviceId, 24); + setChartData(Array.isArray(data) ? data : []); + + const end = Date.now(); + const start = end - 6 * 3600_000; + const locs = await javaVehicleLocationApi.getTrack(devicesData[0].deviceId, start.toString(), end.toString(), 500); + const pts = (Array.isArray(locs) ? locs : []) + .filter(l => l.positioningStatus === 1 && typeof l.longitude === 'number' && typeof l.latitude === 'number') + .map(l => ({ lng: Number(l.longitude), lat: Number(l.latitude) })); + setTrackPoints(pts); + } else { + setChartData([]); + setTrackPoints([]); } } catch (error) { console.error('Failed to load dashboard data:', error); @@ -78,7 +100,7 @@ const Dashboard: React.FC = () => { maintenance: "outline", error: "destructive" }; - + const labels: Record = { online: "在线", offline: "离线", @@ -194,14 +216,14 @@ const Dashboard: React.FC = () => {
{getStatusIcon(device.status)}
-
{device.device_name}
-
{device.device_id}
+
{device.deviceName}
+
{device.deviceId}
{getStatusBadge(device.status)}
- {device.firmware_version} + {device.firmwareVersion}
@@ -225,8 +247,8 @@ const Dashboard: React.FC = () => {
{getTaskStatusIcon(task.status)}
-
{task.task_name}
-
{task.device_id}
+
{task.taskName}
+
{task.deviceId}
@@ -240,76 +262,111 @@ const Dashboard: React.FC = () => {
+
+ + + + + 位置轨迹(最近6小时) + + + + {trackPoints.length > 1 ? ( + + ) : ( +
暂无轨迹数据
+ )} +
+
+
+ {/* 电池数据图表 */} - {recentBatteryData.length > 0 && ( -
- - - - - 电压趋势 - - - +
+ + + + + 电压趋势 + + + + {/* 电压趋势图:单位 V,数据来源 chartData */} + {chartData.length > 0 ? ( - + - new Date(value).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })} /> - new Date(value).toLocaleString('zh-CN')} formatter={(value: number) => [`${value}V`, '电压']} /> - - - + ) : ( +
+ 暂无数据 +
+ )} + + - - - - - 温度趋势 - - - + + + + + 电流趋势 + + + + {/* 电流趋势图:单位 A,数据来源 chartData */} + {chartData.length > 0 ? ( - + - new Date(value).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })} /> - new Date(value).toLocaleString('zh-CN')} - formatter={(value: number) => [`${value}°C`, '温度']} + formatter={(value: number) => [`${value}A`, '电流']} /> - - - -
- )} + ) : ( +
+ 暂无数据 +
+ )} +
+
+
); }; -export default Dashboard; \ No newline at end of file +export default Dashboard; diff --git a/src/pages/DeviceManagement.tsx b/src/pages/DeviceManagement.tsx index ced2ef2..145905c 100644 --- a/src/pages/DeviceManagement.tsx +++ b/src/pages/DeviceManagement.tsx @@ -23,7 +23,7 @@ import { } from 'lucide-react'; import { useForm } from 'react-hook-form'; import { toast } from 'sonner'; -import { deviceApi, batteryDataApi } from '@/db/api'; +import { javaDeviceApi, javaBatteryDataApi } from '@/api/javaApi'; import type { Device, DeviceStatus, CreateDeviceRequest, UpdateDeviceRequest, BatteryData } from '@/types/types'; const DeviceManagement: React.FC = () => { @@ -52,7 +52,8 @@ const DeviceManagement: React.FC = () => { const loadDevices = async () => { try { setLoading(true); - const data = await deviceApi.getDevices(); + //加载设备列表 + const data = await javaDeviceApi.getDevices(); setDevices(data); } catch (error) { console.error('Failed to load devices:', error); @@ -67,8 +68,8 @@ const DeviceManagement: React.FC = () => { if (searchTerm) { filtered = filtered.filter(device => - device.device_name.toLowerCase().includes(searchTerm.toLowerCase()) || - device.device_id.toLowerCase().includes(searchTerm.toLowerCase()) + device.deviceName.toLowerCase().includes(searchTerm.toLowerCase()) || + device.deviceId.toLowerCase().includes(searchTerm.toLowerCase()) ); } @@ -81,7 +82,8 @@ const DeviceManagement: React.FC = () => { const handleCreateDevice = async (data: CreateDeviceRequest) => { try { - await deviceApi.createDevice(data); + //获取设备新增 + await javaDeviceApi.createDevice(data); toast.success('设备创建成功'); setIsCreateDialogOpen(false); createForm.reset(); @@ -96,7 +98,8 @@ const DeviceManagement: React.FC = () => { if (!selectedDevice) return; try { - await deviceApi.updateDevice(selectedDevice.device_id, data); + //设备更新 + await javaDeviceApi.updateDevice(selectedDevice.id, data); toast.success('设备更新成功'); setIsEditDialogOpen(false); editForm.reset(); @@ -108,11 +111,12 @@ const DeviceManagement: React.FC = () => { } }; - const handleDeleteDevice = async (deviceId: string) => { + const handleDeleteDevice = async (id: string) => { if (!confirm('确定要删除这个设备吗?')) return; try { - await deviceApi.deleteDevice(deviceId); + //删除设备 + await javaDeviceApi.deleteDevice(id); toast.success('设备删除成功'); loadDevices(); } catch (error) { @@ -124,7 +128,7 @@ const DeviceManagement: React.FC = () => { const handleViewDetails = async (device: Device) => { setSelectedDevice(device); try { - const batteryData = await batteryDataApi.getBatteryData(device.device_id, 1); + const batteryData = await javaBatteryDataApi.getBatteryData(device.deviceId, 1); setDeviceBatteryData(batteryData[0] || null); } catch (error) { console.error('Failed to load battery data:', error); @@ -221,6 +225,32 @@ const DeviceManagement: React.FC = () => { )} /> + ( + + 设备SN码 + + + + + + )} + /> + ( + + 设备类型 + + + + + + )} + /> { )} /> + ( + + 设备状态 + + + + )} + /> {
- {device.device_name} + {device.deviceName} {getStatusIcon(device.status)}
-
{device.device_id}
+
{device.deviceId}
@@ -309,28 +362,34 @@ const DeviceManagement: React.FC = () => { {getStatusBadge(device.status)}
- {device.ip_address && ( -
- IP地址 - {device.ip_address} -
- )} - - {device.firmware_version && ( -
- 固件版本 - {device.firmware_version} -
- )} - - {device.last_online && ( -
- 最后在线 - - {new Date(device.last_online).toLocaleString('zh-CN')} - -
- )} + {device.ipAddress && ( +
+ IP地址 + {device.ipAddress} +
+ )} + {device.deviceSn && ( +
+ 设备SN码 + {device.deviceSn} +
+ )} + + {device.firmwareVersion && ( +
+ 固件版本 + {device.firmwareVersion} +
+ )} + + {device.lastOnline && ( +
+ 最后在线 + + {new Date(device.lastOnline).toLocaleString('zh-CN')} + +
+ )}
+ +
+
+
+
+
+ {showTextBrand ? ( + EVO + ) : ( + logo { + if (logoIndex < candidates.length - 1) { + const next = logoIndex + 1; + setLogoIndex(next); + setLogoSrc(candidates[next]); + } else { + setShowTextBrand(true); + } + }} + /> + )} +
+

伊特电池管理系统

+

统一设备、数据与轨迹的可视化管理平台

+ +
+
+
+ + + ); +}; + +export default Login; \ No newline at end of file diff --git a/src/pages/MqttManagement.tsx b/src/pages/MqttManagement.tsx index d0b3fc2..3707645 100644 --- a/src/pages/MqttManagement.tsx +++ b/src/pages/MqttManagement.tsx @@ -20,7 +20,7 @@ import { Download } from 'lucide-react'; import { toast } from 'sonner'; -import { mqttLogApi, deviceApi, configApi } from '@/db/api'; +import { javaMqttApi, javaDeviceApi, javaSystemApi } from '@/api/javaApi'; import type { MqttLog, Device, SystemConfig } from '@/types/types'; const MqttManagement: React.FC = () => { @@ -45,14 +45,15 @@ const MqttManagement: React.FC = () => { const loadData = async () => { try { setLoading(true); - const [logsData, devicesData, configData] = await Promise.all([ - mqttLogApi.getLogs(200), - deviceApi.getDevices(), - configApi.getConfig('mqtt_server') + const [logsData, devicesData, configs] = await Promise.all([ + javaMqttApi.getLogs(undefined, 200), + javaDeviceApi.getDevices(), + javaSystemApi.getConfigs() ]); setLogs(logsData); setDevices(devicesData); - setMqttConfig(configData); + const cfg = configs.find(c => c.config_key === 'mqtt_server') || null; + setMqttConfig(cfg); } catch (error) { console.error('Failed to load data:', error); toast.error('加载数据失败'); @@ -122,8 +123,8 @@ const MqttManagement: React.FC = () => { }; const getDeviceName = (deviceId: string) => { - const device = devices.find(d => d.device_id === deviceId); - return device ? device.device_name : deviceId; + const device = devices.find(d => d.deviceId === deviceId); + return device ? device.deviceName : deviceId; }; const exportLogs = () => { @@ -285,8 +286,8 @@ const MqttManagement: React.FC = () => { 全部设备 {devices.map((device) => ( - - {device.device_name} + + {device.deviceName} ))} diff --git a/src/pages/OtaManagement.tsx b/src/pages/OtaManagement.tsx index e491236..9134b0e 100644 --- a/src/pages/OtaManagement.tsx +++ b/src/pages/OtaManagement.tsx @@ -24,7 +24,7 @@ import { } from 'lucide-react'; import { useForm } from 'react-hook-form'; import { toast } from 'sonner'; -import { otaApi, deviceApi } from '@/db/api'; +import { javaOtaApi, javaDeviceApi } from '@/api/javaApi'; import type { OtaTask, Device, CreateOtaTaskRequest, OtaStatus } from '@/types/types'; const OtaManagement: React.FC = () => { @@ -52,27 +52,30 @@ const OtaManagement: React.FC = () => { try { setLoading(true); const [tasksData, devicesData] = await Promise.all([ - otaApi.getTasks(), - deviceApi.getDevices() + javaOtaApi.getTasks(), + javaDeviceApi.getDevices() ]); - setTasks(tasksData); - setDevices(devicesData); + setTasks(tasksData || []); // 确保数据不为undefined + setDevices(devicesData || []); // 确保数据不为undefined } catch (error) { console.error('Failed to load data:', error); toast.error('加载数据失败'); + // 设置默认空数组,防止undefined错误 + setTasks([]); + setDevices([]); } finally { setLoading(false); } }; const filterTasks = () => { - let filtered = tasks; + let filtered = tasks || []; // 确保tasks不为undefined if (searchTerm) { filtered = filtered.filter(task => - task.task_name.toLowerCase().includes(searchTerm.toLowerCase()) || - task.device_id.toLowerCase().includes(searchTerm.toLowerCase()) || - task.firmware_version.toLowerCase().includes(searchTerm.toLowerCase()) + task.taskName.toLowerCase().includes(searchTerm.toLowerCase()) || + task.deviceId.toLowerCase().includes(searchTerm.toLowerCase()) || + task.firmwareVersion.toLowerCase().includes(searchTerm.toLowerCase()) ); } @@ -85,7 +88,7 @@ const OtaManagement: React.FC = () => { const handleCreateTask = async (data: CreateOtaTaskRequest) => { try { - await otaApi.createTask(data); + await javaOtaApi.createTask(data); toast.success('OTA任务创建成功'); setIsCreateDialogOpen(false); createForm.reset(); @@ -98,7 +101,7 @@ const OtaManagement: React.FC = () => { const handleUpdateTaskStatus = async (taskId: string, status: OtaStatus, progress?: number) => { try { - await otaApi.updateTaskStatus(taskId, status, progress); + await javaOtaApi.updateTask({ id: taskId, status, progress }); toast.success('任务状态更新成功'); loadData(); } catch (error) { @@ -227,8 +230,8 @@ const OtaManagement: React.FC = () => { {devices.map((device) => ( - - {device.device_name} ({device.device_id}) + + {device.deviceName} ({device.id}) ))} @@ -320,9 +323,9 @@ const OtaManagement: React.FC = () => {
{getStatusIcon(task.status)}
-

{task.task_name}

+

{task.taskName}

- 设备: {task.device_id} | 目标版本: {task.firmware_version} + 设备: {task.deviceId} | 目标版本: {task.firmwareVersion}

@@ -347,42 +350,42 @@ const OtaManagement: React.FC = () => {
创建时间:
- {new Date(task.created_at).toLocaleString('zh-CN')} + {new Date(task.createTime).toLocaleString('zh-CN')}
- {task.start_time && ( + {task.startTime && (
开始时间:
- {new Date(task.start_time).toLocaleString('zh-CN')} + {new Date(task.startTime).toLocaleString('zh-CN')}
)} - {task.end_time && ( + {task.endTime && (
结束时间:
- {new Date(task.end_time).toLocaleString('zh-CN')} + {new Date(task.endTime).toLocaleString('zh-CN')}
)} - {task.end_time && task.start_time && ( + {task.endTime && task.startTime && (
耗时:
- {Math.round((new Date(task.end_time).getTime() - new Date(task.start_time).getTime()) / 60000)}分钟 + {Math.round((new Date(task.endTime).getTime() - new Date(task.startTime).getTime()) / 60000)}分钟
)} - {task.error_message && ( + {task.errorMessage && (
错误信息:
-

{task.error_message}

+

{task.errorMessage}

)} @@ -466,15 +469,15 @@ const OtaManagement: React.FC = () => {
任务名称: -
{selectedTask.task_name}
+
{selectedTask.taskName}
设备ID: -
{selectedTask.device_id}
+
{selectedTask.deviceId}
固件版本: -
{selectedTask.firmware_version}
+
{selectedTask.firmwareVersion}
当前状态: @@ -485,9 +488,9 @@ const OtaManagement: React.FC = () => {
-
[{new Date(selectedTask.created_at).toLocaleString()}] 任务创建成功
- {selectedTask.start_time && ( -
[{new Date(selectedTask.start_time).toLocaleString()}] 开始下载固件包...
+
[{new Date(selectedTask.createTime).toLocaleString()}] 任务创建成功
+ {selectedTask.startTime && ( +
[{new Date(selectedTask.startTime).toLocaleString()}] 开始下载固件包...
)} {selectedTask.status === 'downloading' && ( <> @@ -504,15 +507,15 @@ const OtaManagement: React.FC = () => { )} {selectedTask.status === 'completed' && ( <> -
[{new Date(selectedTask.end_time!).toLocaleString()}] 固件安装完成
-
[{new Date(selectedTask.end_time!).toLocaleString()}] 设备重启中...
-
[{new Date(selectedTask.end_time!).toLocaleString()}] 升级任务完成
+
[{new Date(selectedTask.endTime!).toLocaleString()}] 固件安装完成
+
[{new Date(selectedTask.endTime!).toLocaleString()}] 设备重启中...
+
[{new Date(selectedTask.endTime!).toLocaleString()}] 升级任务完成
)} - {selectedTask.status === 'failed' && selectedTask.error_message && ( + {selectedTask.status === 'failed' && selectedTask.errorMessage && ( <> -
[{new Date(selectedTask.end_time!).toLocaleString()}] 错误: {selectedTask.error_message}
-
[{new Date(selectedTask.end_time!).toLocaleString()}] 升级任务失败
+
[{new Date(selectedTask.endTime!).toLocaleString()}] 错误: {selectedTask.errorMessage}
+
[{new Date(selectedTask.endTime!).toLocaleString()}] 升级任务失败
)}
diff --git a/src/pages/RealTimeMonitoring.tsx b/src/pages/RealTimeMonitoring.tsx index 66feaaf..69898e8 100644 --- a/src/pages/RealTimeMonitoring.tsx +++ b/src/pages/RealTimeMonitoring.tsx @@ -15,9 +15,9 @@ import { Play, Pause } from 'lucide-react'; -import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, AreaChart, Area } from 'recharts'; -import { deviceApi, batteryDataApi } from '@/db/api'; -import type { Device, BatteryData, ChartDataPoint } from '@/types/types'; +import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, AreaChart, Area, BarChart, Bar } from 'recharts'; +import { javaDeviceApi, javaBatteryDataApi, javaVehicleDataApi, javaVehicleLocationApi, javaExtremeValuesApi, javaSubsystemVoltageApi, javaSubsystemTemperatureApi } from '@/api/javaApi'; +import type { Device, BatteryData, ChartDataPoint, VehicleData, VehicleLocation, ExtremeValues, SubsystemVoltage, SubsystemTemperature } from '@/types/types'; const RealTimeMonitoring: React.FC = () => { const [devices, setDevices] = useState([]); @@ -25,6 +25,13 @@ const RealTimeMonitoring: React.FC = () => { const [selectedDevice, setSelectedDevice] = useState(null); const [chartData, setChartData] = useState([]); const [latestData, setLatestData] = useState(null); + const [vehicleLatest, setVehicleLatest] = useState(null); + const [locationLatest, setLocationLatest] = useState(null); + const [extremeLatest, setExtremeLatest] = useState(null); + const [subsystemNoVolt, setSubsystemNoVolt] = useState(1); + const [subsystemNoTemp, setSubsystemNoTemp] = useState(1); + const [subsystemVoltLatest, setSubsystemVoltLatest] = useState(null); + const [subsystemTempLatest, setSubsystemTempLatest] = useState(null); const [isRealTime, setIsRealTime] = useState(false); const [timeRange, setTimeRange] = useState(24); const [loading, setLoading] = useState(true); @@ -41,7 +48,7 @@ const RealTimeMonitoring: React.FC = () => { }, [selectedDeviceId, timeRange]); useEffect(() => { - let interval: NodeJS.Timeout; + let interval: ReturnType; if (isRealTime && selectedDeviceId) { interval = setInterval(() => { loadDeviceData(); @@ -53,15 +60,21 @@ const RealTimeMonitoring: React.FC = () => { }; }, [isRealTime, selectedDeviceId]); + useEffect(() => { + if (selectedDeviceId) { + loadSubsystemLatest(); + } + }, [selectedDeviceId, subsystemNoVolt, subsystemNoTemp]); + const loadDevices = async () => { try { setLoading(true); - const data = await deviceApi.getDevices(); + const data = await javaDeviceApi.getDevices(); setDevices(data.filter(d => d.status === 'online')); // 只显示在线设备 if (data.length > 0) { const onlineDevice = data.find(d => d.status === 'online'); if (onlineDevice) { - setSelectedDeviceId(onlineDevice.device_id); + setSelectedDeviceId(onlineDevice.deviceId); setSelectedDevice(onlineDevice); } } @@ -76,29 +89,65 @@ const RealTimeMonitoring: React.FC = () => { if (!selectedDeviceId) return; try { - const batteryData = await batteryDataApi.getBatteryData(selectedDeviceId, 1); - if (batteryData.length > 0) { - setLatestData(batteryData[0]); - } + // 整车最新 + const vLatest = await javaVehicleDataApi.getLatestByDevice(selectedDeviceId); + setVehicleLatest(vLatest); + + // 位置最新 + const lLatest = await javaVehicleLocationApi.getLatestByDevice(selectedDeviceId); + setLocationLatest(lLatest); + + // 极值最新 + const eLatest = await javaExtremeValuesApi.getLatestByDevice(selectedDeviceId); + setExtremeLatest(eLatest); } catch (error) { console.error('Failed to load device data:', error); } }; + const loadSubsystemLatest = async () => { + if (!selectedDeviceId) return; + try { + const sv = await javaSubsystemVoltageApi.getLatest(selectedDeviceId, subsystemNoVolt); + setSubsystemVoltLatest(sv); + const st = await javaSubsystemTemperatureApi.getLatest(selectedDeviceId, subsystemNoTemp); + setSubsystemTempLatest(st); + } catch (error) { + console.error('Failed to load subsystem latest:', error); + } + }; + const loadChartData = async () => { if (!selectedDeviceId) return; try { - const data = await batteryDataApi.getChartData(selectedDeviceId, timeRange); + const data = await javaBatteryDataApi.getChartData(selectedDeviceId, timeRange); setChartData(data); + if (Array.isArray(data) && data.length > 0) { + const p = data[data.length - 1]; + setLatestData({ + id: '', + device_id: selectedDeviceId, + voltage: p.voltage, + current: p.current, + temperature: p.temperature, + soc: p.soc, + soh: undefined, + power: undefined, + timestamp: p.timestamp + }); + } else { + setLatestData(null); + } } catch (error) { console.error('Failed to load chart data:', error); } }; + // 设备选择:使用 deviceId 作为值 const handleDeviceChange = (deviceId: string) => { setSelectedDeviceId(deviceId); - const device = devices.find(d => d.device_id === deviceId); + const device = devices.find(d => d.deviceId === deviceId); setSelectedDevice(device || null); }; @@ -184,8 +233,8 @@ const RealTimeMonitoring: React.FC = () => { {devices.map((device) => ( - - {device.device_name} ({device.device_id}) + + {device.deviceName} ({device.deviceId}) ))} @@ -206,11 +255,37 @@ const RealTimeMonitoring: React.FC = () => {
+
+ + +
+
+ + +
- {selectedDevice && latestData && ( + {selectedDevice && ( <> {/* 实时数据卡片 */}
@@ -223,9 +298,9 @@ const RealTimeMonitoring: React.FC = () => {
-
{latestData.voltage?.toFixed(2) || '--'}V
+
{latestData?.voltage?.toFixed(2) || '--'}V

- {new Date(latestData.timestamp).toLocaleTimeString('zh-CN')} + {latestData?.timestamp ? new Date(latestData.timestamp).toLocaleTimeString('zh-CN') : '--'}

@@ -239,9 +314,9 @@ const RealTimeMonitoring: React.FC = () => {
-
{latestData.current?.toFixed(2) || '--'}A
+
{latestData?.current?.toFixed(2) || '--'}A

- 功率: {latestData.power?.toFixed(2) || '--'}W + 功率: {latestData?.power?.toFixed(2) || '--'}W

@@ -255,9 +330,9 @@ const RealTimeMonitoring: React.FC = () => { -
{latestData.temperature?.toFixed(1) || '--'}°C
+
{latestData?.temperature?.toFixed(1) || '--'}°C

- {latestData.temperature && latestData.temperature > 50 ? '高温警告' : '温度正常'} + {latestData?.temperature && latestData?.temperature > 50 ? '高温警告' : '温度正常'}

@@ -271,9 +346,9 @@ const RealTimeMonitoring: React.FC = () => { -
{latestData.soc?.toFixed(1) || '--'}%
+
{latestData?.soc?.toFixed(1) || '--'}%

- {latestData.soc && latestData.soc < 20 ? '电量不足' : '电量充足'} + {latestData?.soc && latestData?.soc < 20 ? '电量不足' : '电量充足'}

@@ -284,12 +359,52 @@ const RealTimeMonitoring: React.FC = () => { -
{latestData.soh?.toFixed(1) || '--'}%
+
{latestData?.soh?.toFixed(1) || '--'}%

- {latestData.soh && latestData.soh > 95 ? '状态良好' : '需要关注'} + {latestData?.soh && latestData?.soh > 95 ? '状态良好' : '需要关注'}

+ {vehicleLatest && ( + + + 整车 + + + +
总电压: {vehicleLatest.totalVoltage ?? '--'} V
+
总电流: {vehicleLatest.totalCurrent ?? '--'} A
+
SOC: {vehicleLatest.soc ?? '--'} %
+
速度: {vehicleLatest.vehicleSpeed ?? '--'} km/h | 里程: {vehicleLatest.totalMileage ?? '--'} km
+
+
+ )} + {extremeLatest && ( + + + 极值 + + + +
最高电压: {extremeLatest.maxVoltageValue ?? '--'} V (子系统 {extremeLatest.maxVoltageSubsystemNo ?? '--'} / 单体 {extremeLatest.maxVoltageBatteryNo ?? '--'})
+
最低电压: {extremeLatest.minVoltageValue ?? '--'} V (子系统 {extremeLatest.minVoltageSubsystemNo ?? '--'} / 单体 {extremeLatest.minVoltageBatteryNo ?? '--'})
+
最高温度: {extremeLatest.maxTempValue ?? '--'} ℃ (子系统 {extremeLatest.maxTempSubsystemNo ?? '--'} / 探针 {extremeLatest.maxTempProbeNo ?? '--'})
+
最低温度: {extremeLatest.minTempValue ?? '--'} ℃ (子系统 {extremeLatest.minTempSubsystemNo ?? '--'} / 探针 {extremeLatest.minTempProbeNo ?? '--'})
+
+
+ )} + {locationLatest && ( + + + 位置 + + + +
经度: {locationLatest.longitude ?? '--'} | 纬度: {locationLatest.latitude ?? '--'}
+
定位: {locationLatest.positioningStatus === 1 ? '有效' : '无效'}
+
+
+ )} {/* 图表区域 */} @@ -439,6 +554,49 @@ const RealTimeMonitoring: React.FC = () => { + {/* 子系统电压/温度最新帧展示 */} +
+ + + + + 子系统电压帧 + + + + + ({ index: i + 1, value: v }))}> + + + + [`${Number(value).toFixed(3)}V`, '电压']} /> + + + + + + + + + + + 子系统温度帧 + + + + + ({ index: i + 1, value: v }))}> + + + + [`${Number(value).toFixed(2)}℃`, '温度']} /> + + + + + +
+ {/* 设备信息 */} @@ -448,19 +606,19 @@ const RealTimeMonitoring: React.FC = () => {
设备名称
-
{selectedDevice.device_name}
+
{selectedDevice.deviceName}
设备ID
-
{selectedDevice.device_id}
+
{selectedDevice.id}
IP地址
-
{selectedDevice.ip_address || '未设置'}
+
{selectedDevice.ipAddress || '未设置'}
固件版本
-
{selectedDevice.firmware_version || '未知'}
+
{selectedDevice.firmwareVersion || '未知'}
diff --git a/src/pages/SystemSettings.tsx b/src/pages/SystemSettings.tsx index 6ddf45d..cc82897 100644 --- a/src/pages/SystemSettings.tsx +++ b/src/pages/SystemSettings.tsx @@ -22,7 +22,7 @@ import { } from 'lucide-react'; import { useForm } from 'react-hook-form'; import { toast } from 'sonner'; -import { configApi } from '@/db/api'; +import { javaSystemApi } from '@/api/javaApi'; import type { SystemConfig } from '@/types/types'; interface ConfigForm { @@ -55,7 +55,7 @@ const SystemSettings: React.FC = () => { const loadConfigs = async () => { try { setLoading(true); - const data = await configApi.getConfigs(); + const data = await javaSystemApi.getConfigs(); setConfigs(data); // 填充表单 @@ -79,7 +79,7 @@ const SystemSettings: React.FC = () => { // 更新所有配置 const updatePromises = Object.entries(data).map(([key, value]) => - configApi.updateConfig(key, { config_value: value }) + javaSystemApi.updateConfig(key, { config_value: value }) ); await Promise.all(updatePromises); diff --git a/src/routes.tsx b/src/routes.tsx index cf045c0..e82a9a3 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -4,6 +4,7 @@ import RealTimeMonitoring from './pages/RealTimeMonitoring'; import OtaManagement from './pages/OtaManagement'; import MqttManagement from './pages/MqttManagement'; import SystemSettings from './pages/SystemSettings'; +import Login from './pages/Login'; import type { ReactNode } from 'react'; interface RouteConfig { @@ -19,6 +20,12 @@ const routes: RouteConfig[] = [ path: '/', element: }, + { + name: '登录', + path: '/login', + element: , + visible: false + }, { name: '设备管理', path: '/devices', diff --git a/src/types/types.ts b/src/types/types.ts index 2c53922..70f4720 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -3,8 +3,26 @@ export type DeviceStatus = 'online' | 'offline' | 'maintenance' | 'error'; export type OtaStatus = 'pending' | 'downloading' | 'installing' | 'completed' | 'failed'; -// 设备信息 +// 设备信息 - 匹配Java后端返回格式 export interface Device { + id: string; + deviceId: string; + deviceName: string; + deviceSn: string; + deviceType: string; + status: DeviceStatus; + ipAddress?: string; + firmwareVersion?: string; + lastOnline?: string; + createBy?: string; + createTime?: string; + updateBy?: string; + updateTime?: string; + remark?: string; +} + +// 兼容旧版本的设备接口(下划线命名) +export interface DeviceLegacy { id: string; device_id: string; device_name: string; @@ -33,15 +51,18 @@ export interface BatteryData { // OTA升级任务 export interface OtaTask { id: string; - device_id: string; - task_name: string; - firmware_version: string; + deviceId: string; + taskName: string; + firmwareVersion: string; status: OtaStatus; progress: number; - start_time?: string; - end_time?: string; - error_message?: string; - created_at: string; + startTime?: string; + endTime?: string; + errorMessage?: string; + createTime: string; + updateTime?: string; + createBy?: string; + updateBy?: string; } // MQTT日志 @@ -110,6 +131,8 @@ export interface CreateDeviceRequest { device_id: string; device_name: string; device_type?: string; + device_sn?: string; + status?: DeviceStatus; ip_address?: string; firmware_version?: string; } @@ -118,6 +141,8 @@ export interface CreateDeviceRequest { export interface UpdateDeviceRequest { device_name?: string; status?: DeviceStatus; + device_type?: string; + device_sn?: string; ip_address?: string; firmware_version?: string; } @@ -132,4 +157,105 @@ export interface CreateOtaTaskRequest { // 更新系统配置请求 export interface UpdateConfigRequest { config_value: string; +} + +/** + * 电池极值数据(表:extreme_values) + * 电压单位:V;温度单位:℃;时间戳:UTC ISO 字符串 + */ +export interface ExtremeValues { + id: string; + deviceId: string; // 设备ID + timestamp: string; // 数据时间戳(UTC ISO) + maxVoltageSubsystemNo?: number; // 最高电压子系统号 + maxVoltageBatteryNo?: number; // 最高电压单体代号 + maxVoltageValue?: number; // 电池单体电压最高值(V) + minVoltageSubsystemNo?: number; // 最低电压子系统号 + minVoltageBatteryNo?: number; // 最低电压单体代号 + minVoltageValue?: number; // 电池单体电压最低值(V) + maxTempSubsystemNo?: number; // 最高温度子系统号 + maxTempProbeNo?: number; // 最高温度探针序号 + maxTempValue?: number; // 最高温度值(℃) + minTempSubsystemNo?: number; // 最低温度子系统号 + minTempProbeNo?: number; // 最低温度探针序号 + minTempValue?: number; // 最低温度值(℃) + createTime?: string; + updateTime?: string; + createBy?: string; + updateBy?: string; +} + +/** + * 车辆位置数据(表:vehicle_location) + * 经纬度保留 7 位小数;定位状态:0 无效,1 有效 + */ +export interface VehicleLocation { + id: string; + deviceId: string; // 设备ID + timestamp: string; // 数据时间戳(UTC ISO) + positioningStatus?: number; // 定位状态(0:无效 1:有效) + longitude?: number; // 经度(decimal(10,7)) + latitude?: number; // 纬度(decimal(10,7)) + createdAt?: string; + updatedAt?: string; +} + +/** + * 整车数据(表:vehicle_data) + * 速度 km/h;里程 km;电压 V;电流 A;SOC %;绝缘电阻 kΩ + */ +export interface VehicleData { + id: string; + deviceId: string; // 设备ID + timestamp: string; // 数据时间戳(UTC ISO) + vehicleStatus?: number; // 车辆状态(1:启动 2:熄火 3:其他 254:异常 255:无效) + chargingStatus?: number; // 充电状态(1:停车充电 2:行驶充电 3:未充电 4:充电完成 254:异常 255:无效) + operationMode?: number; // 运行模式(1:纯电 2:混动 3:燃油 254:异常 255:无效) + vehicleSpeed?: number; // 车速(km/h) + totalMileage?: number; // 累计里程(km) + totalVoltage?: number; // 总电压(V) + totalCurrent?: number; // 总电流(A) + soc?: number; // SOC(%) + dcdcStatus?: number; // DC-DC 状态(1:工作 2:断开) + gearPosition?: number; // 档位(0:空档 1-6:1-6档 13:倒档 14:D档 15:P档) + insulationResistance?: number; // 绝缘电阻(kΩ) + reservedData?: string; // 预留字段 + createdAt?: string; + updatedAt?: string; +} + +/** + * 子系统电压信息(表:subsystem_voltage) + * 电压单位:V;电流单位:A;batteryVoltages 为单体电压数组(V) + */ +export interface SubsystemVoltage { + id: string; + deviceId: string; // 设备ID + timestamp: string; // 数据时间戳(UTC ISO) + subsystemCount: number; // 子系统总数 + subsystemNo: number; // 子系统号 + subsystemVoltage?: number; // 子系统电压(V) + subsystemCurrent?: number; // 子系统电流(A) + totalBatteryCount?: number; // 单体电池总数 + frameStartBatteryNo?: number; // 本帧起始电池序号 + frameBatteryCount?: number; // 本帧单体电池总数 + batteryVoltages?: number[]; // 单体电池电压数组(V) + createdAt?: string; + updatedAt?: string; +} + +/** + * 子系统温度信息(表:subsystem_temperature) + * temperatureValues 为各温度探针检测值数组(℃) + */ +export interface SubsystemTemperature { + id: string; + deviceId: string; // 设备ID + timestamp: string; // 数据时间戳(UTC ISO) + subsystemCount: number; // 子系统总数 + subsystemNo: number; // 子系统号 + tempProbeCount?: number; // 温度探针个数 + temperatureValues?: number[]; // 温度值数组(℃) + createdAt?: string; + updatedAt?: string; } \ No newline at end of file diff --git a/test_data/bms_devices_test_data.sql b/test_data/bms_devices_test_data.sql new file mode 100644 index 0000000..f76686a --- /dev/null +++ b/test_data/bms_devices_test_data.sql @@ -0,0 +1,74 @@ +-- BMS设备管理表测试数据 +-- 清空现有数据(可选) +-- TRUNCATE TABLE bms_devices; + +-- 插入测试数据 +INSERT INTO `bms_devices` ( + `id`, + `device_id`, + `device_name`, + `device_type`, + `status`, + `ip_address`, + `firmware_version`, + `last_online`, + `create_by`, + `create_time`, + `update_time`, + `update_by` +) VALUES +-- 在线设备 +('550e8400-e29b-41d4-a716-446655440001', 'BBOX-001', '1号充电桩BBox', 'BBox', 'online', '192.168.1.101', 'v2.1.3', '2024-01-15 14:30:25', 'admin', '2024-01-10 09:00:00', '2024-01-15 14:30:25', '2024-01-15 14:30:25'), +('550e8400-e29b-41d4-a716-446655440002', 'BBOX-002', '2号充电桩BBox', 'BBox', 'online', '192.168.1.102', 'v2.1.3', '2024-01-15 14:28:15', 'admin', '2024-01-10 09:15:00', '2024-01-15 14:28:15', '2024-01-15 14:28:15'), +('550e8400-e29b-41d4-a716-446655440003', 'BBOX-003', '3号充电桩BBox', 'BBox', 'online', '192.168.1.103', 'v2.1.2', '2024-01-15 14:25:40', 'admin', '2024-01-10 09:30:00', '2024-01-15 14:25:40', '2024-01-15 14:25:40'), +('550e8400-e29b-41d4-a716-446655440004', 'BBOX-004', '4号充电桩BBox', 'BBox', 'online', '192.168.1.104', 'v2.1.3', '2024-01-15 14:32:10', 'admin', '2024-01-10 10:00:00', '2024-01-15 14:32:10', '2024-01-15 14:32:10'), +('550e8400-e29b-41d4-a716-446655440005', 'BBOX-005', '5号充电桩BBox', 'BBox', 'online', '192.168.1.105', 'v2.1.3', '2024-01-15 14:29:55', 'admin', '2024-01-10 10:15:00', '2024-01-15 14:29:55', '2024-01-15 14:29:55'), + +-- 离线设备 +('550e8400-e29b-41d4-a716-446655440006', 'BBOX-006', '6号充电桩BBox', 'BBox', 'offline', '192.168.1.106', 'v2.1.1', '2024-01-14 18:45:30', 'admin', '2024-01-10 10:30:00', '2024-01-14 18:45:30', '2024-01-14 18:45:30'), +('550e8400-e29b-41d4-a716-446655440007', 'BBOX-007', '7号充电桩BBox', 'BBox', 'offline', '192.168.1.107', 'v2.0.8', '2024-01-13 22:15:20', 'admin', '2024-01-10 11:00:00', '2024-01-13 22:15:20', '2024-01-13 22:15:20'), +('550e8400-e29b-41d4-a716-446655440008', 'BBOX-008', '8号充电桩BBox', 'BBox', 'offline', '192.168.1.108', 'v2.1.0', '2024-01-12 16:30:45', 'admin', '2024-01-10 11:15:00', '2024-01-12 16:30:45', '2024-01-12 16:30:45'), + +-- 维护中设备 +('550e8400-e29b-41d4-a716-446655440009', 'BBOX-009', '9号充电桩BBox', 'BBox', 'maintenance', '192.168.1.109', 'v2.1.2', '2024-01-15 08:00:00', 'admin', '2024-01-10 11:30:00', '2024-01-15 08:00:00', '2024-01-15 08:00:00'), +('550e8400-e29b-41d4-a716-446655440010', 'BBOX-010', '10号充电桩BBox', 'BBox', 'maintenance', '192.168.1.110', 'v2.1.1', '2024-01-15 07:30:00', 'admin', '2024-01-10 12:00:00', '2024-01-15 07:30:00', '2024-01-15 07:30:00'), + +-- 不同区域的设备 +('550e8400-e29b-41d4-a716-446655440011', 'BBOX-A01', 'A区1号BBox', 'BBox', 'online', '192.168.2.101', 'v2.1.3', '2024-01-15 14:31:20', 'admin', '2024-01-11 09:00:00', '2024-01-15 14:31:20', '2024-01-15 14:31:20'), +('550e8400-e29b-41d4-a716-446655440012', 'BBOX-A02', 'A区2号BBox', 'BBox', 'online', '192.168.2.102', 'v2.1.3', '2024-01-15 14:27:35', 'admin', '2024-01-11 09:30:00', '2024-01-15 14:27:35', '2024-01-15 14:27:35'), +('550e8400-e29b-41d4-a716-446655440013', 'BBOX-B01', 'B区1号BBox', 'BBox', 'offline', '192.168.3.101', 'v2.0.9', '2024-01-14 20:15:10', 'admin', '2024-01-11 10:00:00', '2024-01-14 20:15:10', '2024-01-14 20:15:10'), +('550e8400-e29b-41d4-a716-446655440014', 'BBOX-B02', 'B区2号BBox', 'BBox', 'online', '192.168.3.102', 'v2.1.2', '2024-01-15 14:26:50', 'admin', '2024-01-11 10:30:00', '2024-01-15 14:26:50', '2024-01-15 14:26:50'), +('550e8400-e29b-41d4-a716-446655440015', 'BBOX-C01', 'C区1号BBox', 'BBox', 'maintenance', '192.168.4.101', 'v2.1.1', '2024-01-15 06:00:00', 'admin', '2024-01-11 11:00:00', '2024-01-15 06:00:00', '2024-01-15 06:00:00'), + +-- 测试设备(用于开发测试) +('550e8400-e29b-41d4-a716-446655440016', 'BBOX-TEST01', '测试设备01', 'BBox', 'online', '192.168.100.101', 'v2.2.0-beta', '2024-01-15 14:33:00', 'developer', '2024-01-12 14:00:00', '2024-01-15 14:33:00', '2024-01-15 14:33:00'), +('550e8400-e29b-41d4-a716-446655440017', 'BBOX-TEST02', '测试设备02', 'BBox', 'offline', '192.168.100.102', 'v2.2.0-alpha', '2024-01-14 16:20:30', 'developer', '2024-01-12 14:30:00', '2024-01-14 16:20:30', '2024-01-14 16:20:30'), + +-- 老版本固件设备 +('550e8400-e29b-41d4-a716-446655440018', 'BBOX-OLD01', '老版本设备01', 'BBox', 'offline', '192.168.1.201', 'v1.8.5', '2024-01-10 12:00:00', 'admin', '2023-12-15 10:00:00', '2024-01-10 12:00:00', '2024-01-10 12:00:00'), +('550e8400-e29b-41d4-a716-446655440019', 'BBOX-OLD02', '老版本设备02', 'BBox', 'maintenance', '192.168.1.202', 'v1.9.2', '2024-01-11 08:30:00', 'admin', '2023-12-20 11:00:00', '2024-01-11 08:30:00', '2024-01-11 08:30:00'), + +-- 高负载区域设备 +('550e8400-e29b-41d4-a716-446655440020', 'BBOX-HUB01', '枢纽站1号BBox', 'BBox', 'online', '192.168.5.101', 'v2.1.3', '2024-01-15 14:34:15', 'admin', '2024-01-08 08:00:00', '2024-01-15 14:34:15', '2024-01-15 14:34:15'); + +-- 查询验证数据 +SELECT + COUNT(*) as total_devices, + SUM(CASE WHEN status = 'online' THEN 1 ELSE 0 END) as online_count, + SUM(CASE WHEN status = 'offline' THEN 1 ELSE 0 END) as offline_count, + SUM(CASE WHEN status = 'maintenance' THEN 1 ELSE 0 END) as maintenance_count +FROM bms_devices; + +-- 按固件版本统计 +SELECT + firmware_version, + COUNT(*) as device_count, + GROUP_CONCAT(device_name) as devices +FROM bms_devices +GROUP BY firmware_version +ORDER BY firmware_version DESC; + +-- 按状态查看设备 +SELECT device_id, device_name, status, ip_address, firmware_version, last_online +FROM bms_devices +ORDER BY status, last_online DESC; \ No newline at end of file diff --git a/vite.config.dev.ts b/vite.config.dev.ts new file mode 100644 index 0000000..7060c54 --- /dev/null +++ b/vite.config.dev.ts @@ -0,0 +1,197 @@ + + import { defineConfig, loadConfigFromFile } from "vite"; + import type { Plugin, ConfigEnv } from "vite"; + import tailwindcss from "tailwindcss"; + import autoprefixer from "autoprefixer"; + import fs from "fs/promises"; + import path from "path"; + import { + makeTagger, + injectedGuiListenerPlugin, + injectOnErrorPlugin + } from "miaoda-sc-plugin"; + + const tailwindConfig = { + plugins: [ + function ({ addUtilities }) { + addUtilities( + { + ".border-t-solid": { "border-top-style": "solid" }, + ".border-r-solid": { "border-right-style": "solid" }, + ".border-b-solid": { "border-bottom-style": "solid" }, + ".border-l-solid": { "border-left-style": "solid" }, + ".border-t-dashed": { "border-top-style": "dashed" }, + ".border-r-dashed": { "border-right-style": "dashed" }, + ".border-b-dashed": { "border-bottom-style": "dashed" }, + ".border-l-dashed": { "border-left-style": "dashed" }, + ".border-t-dotted": { "border-top-style": "dotted" }, + ".border-r-dotted": { "border-right-style": "dotted" }, + ".border-b-dotted": { "border-bottom-style": "dotted" }, + ".border-l-dotted": { "border-left-style": "dotted" }, + }, + ["responsive"] + ); + }, + ], + }; + + export async function tryLoadConfigFromFile( + filePath: string, + env: ConfigEnv = { command: "serve", mode: "development" } + ): Promise { + try { + const result = await loadConfigFromFile(env, filePath); + return result ? result.config : null; + } catch (error) { + console.warn(`加载配置文件失败: ${filePath},尝试加载cjs版本`); + console.warn(error); + + // 👇 创建 .cjs 临时文件重试 + const tempFilePath = + filePath.replace(/\.(js|ts|mjs|mts)$/, "") + `.temp.cjs`; + try { + const originalContent = await fs.readFile(filePath, "utf-8"); + + // 补充逻辑:如果是 ESM 语法,无法直接 require,会失败 + if (/^\s*import\s+/m.test(originalContent)) { + console.error( + `配置文件包含 import 语法,无法自动转为 CommonJS: ${filePath}` + ); + return null; + } + + await fs.writeFile(tempFilePath, originalContent, "utf-8"); + + const result = await loadConfigFromFile(env, tempFilePath); + return result ? result.config : null; + } catch (innerError) { + console.error(`重试加载临时 .cjs 文件失败: ${tempFilePath}`); + console.error(innerError); + return null; + } finally { + // 🧹 尝试删除临时文件 + try { + await fs.unlink(tempFilePath); + } catch (_) {} + } + } + } + + const env: ConfigEnv = { command: "serve", mode: "development" }; + const configFile = path.resolve(__dirname, "vite.config.ts"); + const result = await loadConfigFromFile(env, configFile); + const userConfig = result?.config; + const tailwindConfigFile = path.resolve(__dirname, "tailwind.config.js"); + const tailwindResult = await tryLoadConfigFromFile(tailwindConfigFile, env); + const root = path.resolve(__dirname); + + export default defineConfig({ + ...userConfig, + plugins: [ + makeTagger(), + injectedGuiListenerPlugin({ + path: 'https://resource-static.cdn.bcebos.com/common/v2/injected.js' + }), + injectOnErrorPlugin(), + ...(userConfig?.plugins || []), + +{ + name: 'hmr-toggle', + configureServer(server) { + let hmrEnabled = true; + + // 包装原来的 send 方法 + const _send = server.ws.send; + server.ws.send = (payload) => { + if (hmrEnabled) { + return _send.call(server.ws, payload); + } else { + console.log('[HMR disabled] skipped payload:', payload.type); + } + }; + + // 提供接口切换 HMR + server.middlewares.use('/innerapi/v1/sourcecode/__hmr_off', (req, res) => { + hmrEnabled = false; + let body = { + status: 0, + msg: 'HMR disabled' + }; + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify(body)); + }); + + server.middlewares.use('/innerapi/v1/sourcecode/__hmr_on', (req, res) => { + hmrEnabled = true; + let body = { + status: 0, + msg: 'HMR enabled' + }; + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify(body)); + }); + + // 注册一个 HTTP API,用来手动触发一次整体刷新 + server.middlewares.use('/innerapi/v1/sourcecode/__hmr_reload', (req, res) => { + if (hmrEnabled) { + server.ws.send({ + type: 'full-reload', + path: '*', // 整页刷新 + }); + } + res.statusCode = 200; + let body = { + status: 0, + msg: 'Manual full reload triggered' + }; + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify(body)); + }); + }, + load(id) { + if (id === 'virtual:after-update') { + return ` + if (import.meta.hot) { + import.meta.hot.on('vite:afterUpdate', () => { + window.postMessage( + { + type: 'editor-update' + }, + '*' + ); + }); + } + `; + } + }, + transformIndexHtml(html) { + return { + html, + tags: [ + { + tag: 'script', + attrs: { + type: 'module', + src: '/@id/virtual:after-update' + }, + injectTo: 'body' + } + ] + }; + } +}, + + ], + css: { + postcss: { + plugins: [ + tailwindcss({ + ...(tailwindResult as any), + content: [`${root}/index.html`, `${root}/src/**/*.{js,ts,jsx,tsx}`], + }), + autoprefixer(), + ], + }, + } + }); + \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index b9798ff..e7f27f4 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,25 +1,39 @@ -import { defineConfig } from "vite"; +import { defineConfig, loadEnv } from 'vite'; +import type { UserConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import svgr from 'vite-plugin-svgr'; +import path from 'path'; + import { miaodaDevPlugin } from "miaoda-sc-plugin"; -import react from "@vitejs/plugin-react"; -import svgr from "vite-plugin-svgr"; -import path from "path"; // https://vite.dev/config/ -export default defineConfig({ - plugins: [ - react(), - miaodaDevPlugin(), - svgr({ +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd()); + const target = env.VITE_JAVA_API_BASE_URL || 'http://192.168.5.200:8080'; + const config: UserConfig = { + plugins: [react(), svgr({ svgrOptions: { - icon: true, - exportType: "named", - namedExport: "ReactComponent", - }, - }), - ], + icon: true, + exportType: 'named', + namedExport: 'ReactComponent' + } + }), miaodaDevPlugin()], resolve: { alias: { - "@": path.resolve(__dirname, "./src"), + '@': path.resolve(__dirname, './src'), }, }, + server: { + host: '0.0.0.0', + port: 5174, + proxy: { + '/dev-api': { + target, + changeOrigin: true, + rewrite: (p) => p.replace(/^\/dev-api/, ''), + }, + }, + }, + }; + return config; });