mqtt消息记录优化,控制台页面样式优化
This commit is contained in:
64
LogBook.md
64
LogBook.md
@@ -1,3 +1,67 @@
|
|||||||
|
## 2024-08-09 (全局样式优化 - 内容区域背景)
|
||||||
|
|
||||||
|
- **目标**: 为所有认证后页面的主要内容区域设置统一的淡灰色背景和内边距,以提升视觉一致性。
|
||||||
|
- **问题**: 之前 `bg-gray-100` 和特定内边距仅应用于部分页面(如管理员控制台),其他页面(如MQTT日志页面)未使用此背景。
|
||||||
|
- **解决方案**:
|
||||||
|
- 修改 `charging_web_app/src/app/(authenticated)/layout.tsx` 文件。
|
||||||
|
- 将 `<main>` 元素的类名从 `"flex-grow container mx-auto p-4 sm:p-6 lg:p-8"` 修改为 `"flex-grow container mx-auto bg-gray-100 py-8 px-4 md:px-6 lg:px-8"`。
|
||||||
|
- 这使得所有通过 `AuthenticatedLayout` 渲染的子页面 (`{children}`) 的主内容区域都将继承 `bg-gray-100` 背景色和 `py-8 px-4 md:px-6 lg:px-8` 的内边距。
|
||||||
|
- **状态**: 全局内容区域背景和内边距已统一。所有认证后的页面现在应具有一致的淡灰色背景和内边距。
|
||||||
|
- **下一步**: 用户验证所有相关页面的视觉效果。
|
||||||
|
|
||||||
|
## 2024-08-09 (MQTT通信日志页面 - 对接真实API与UI增强)
|
||||||
|
|
||||||
|
- **目标**: 将 `charging_web_app/src/app/(authenticated)/admin/mqtt-logs/page.tsx` 从使用模拟数据改为对接真实的后端API,并使用Ant Design组件增强UI和功能。
|
||||||
|
- **实施**:
|
||||||
|
- **移除模拟数据**: 删除了页面中用于生成和显示模拟日志的逻辑。
|
||||||
|
- **引入Ant Design**: 页面全面改用Ant Design组件,包括 `Table`, `Form`, `Input`, `Select`, `Button`, `DatePicker`, `Tag`, `Tooltip`, `Card`, `Row`, `Col`, `Space`。
|
||||||
|
- **API对接**:
|
||||||
|
- 导入了 `fetchMqttLogs` 服务 (来自 `logService.ts`) 和相关的类型定义 (`MqttCommunicationLog`, `MqttLogQueryRequest`, `PageResponse`, `BaseResponse` 来自 `log.types.ts`)。
|
||||||
|
- `fetchData` 函数现在调用 `fetchMqttLogs`,并正确处理后端返回的数据结构,包括分页信息和错误处理。
|
||||||
|
- 使用 `dayjs` 格式化时间戳。
|
||||||
|
- **查询表单功能**:
|
||||||
|
- 创建了一个包含多个筛选条件的表单 (客户端ID, 主题, 方向, Payload内容, 时间范围, 关联会话ID, 关联任务ID)。
|
||||||
|
- 实现了查询和重置功能,查询时会重新获取数据并重置到第一页。
|
||||||
|
- 时间范围选择器使用 AntD `RangePicker`。
|
||||||
|
- **表格功能增强**:
|
||||||
|
- 实现了服务器端分页和排序。
|
||||||
|
- 表格列根据后端返回的数据模型 (`MqttCommunicationLog`) 进行定义,并对关键字段 (时间戳, 方向, Payload, Retained标记等) 进行了格式化和本地化显示。
|
||||||
|
- Payload 列使用 `Tooltip` 展示完整内容,并通过 `pre` 标签保持格式。
|
||||||
|
- 为表格启用了水平滚动条和边框。
|
||||||
|
- **权限与加载状态**: 保留并优化了用户权限检查逻辑和加载状态的显示。
|
||||||
|
- **状态**: MQTT通信日志页面现在能够从后端API获取、显示和筛选真实的日志数据,并拥有了基于Ant Design的完善的用户界面。与全局布局和导航保持一致。
|
||||||
|
- **下一步**:
|
||||||
|
- 用户进行全面测试,包括查询、分页、排序、错误处理等。
|
||||||
|
- 根据反馈进行可能的微调。
|
||||||
|
|
||||||
|
## 2024-08-09 (MQTT通信日志页面 - 修复路径冲突)
|
||||||
|
|
||||||
|
- **问题**: Next.js 报错 "You cannot have two parallel pages that resolve to the same path",指向 `/src/app/admin`。
|
||||||
|
- **原因**:
|
||||||
|
- 先前在 `charging_web_app/src/app/admin/mqtt-logs/page.tsx` 创建了MQTT日志页面。
|
||||||
|
- 之后为了应用认证布局,在 `charging_web_app/src/app/(authenticated)/admin/mqtt-logs/page.tsx` 又创建了同路径页面。
|
||||||
|
- Next.js 的路由组 `(authenticated)` 不影响 URL 路径,导致两个文件都解析到 `/admin/mqtt-logs`,引发冲突。
|
||||||
|
- **解决方案**:
|
||||||
|
- 删除了位于 `charging_web_app/src/app/admin/mqtt-logs/` 目录下的旧 `page.tsx` 文件及其父目录(如果为空)。
|
||||||
|
- 保留了 `charging_web_app/src/app/(authenticated)/admin/mqtt-logs/page.tsx`,因为它能正确应用期望的布局。
|
||||||
|
- **状态**: 路径冲突已解决。MQTT日志页面现在由 `(authenticated)` 路由组内的文件唯一定义。
|
||||||
|
|
||||||
|
## 2024-08-09 (MQTT通信日志页面 - 布局优化)
|
||||||
|
|
||||||
|
- **目标**: 确保 `charging_web_app` 中新创建的MQTT通信日志页面 (`/admin/mqtt-logs`) 与应用的全局布局(特别是顶部导航栏)保持一致。
|
||||||
|
- **分析**:
|
||||||
|
- 全局的根布局 `charging_web_app/src/app/layout.tsx` 比较基础,主要提供全局样式和 `AuthProvider`。
|
||||||
|
- 经过认证的用户界面布局(包括顶部导航栏、主内容区和页脚)定义在 `charging_web_app/src/app/(authenticated)/layout.tsx` 中。
|
||||||
|
- `AuthenticatedLayout.tsx` 使用 Tailwind CSS,并包含了一个指向 `/admin/mqtt-logs` 的链接(仅管理员可见)。
|
||||||
|
- **实施**:
|
||||||
|
- 在 `charging_web_app/src/app/(authenticated)/admin/mqtt-logs/` 目录下创建了 `page.tsx` 文件。
|
||||||
|
- `page.tsx` 中实现了一个基本的MQTT日志展示组件,包括模拟数据加载、分页、权限检查和基本的Tailwind CSS样式,使其内容区域风格与整体应用一致。
|
||||||
|
- 由于页面嵌套在 `(authenticated)/admin/` 路径下,它自动继承了 `(authenticated)/layout.tsx` 的布局,因此顶部导航栏和整体页面结构符合预期。
|
||||||
|
- **状态**: MQTT通信日志页面已创建,并成功融入了现有的全局布局和导航结构中。管理员用户可以通过导航栏的链接访问此页面。
|
||||||
|
- **下一步**:
|
||||||
|
- 将 `page.tsx` 中的模拟数据获取逻辑替换为真实的API调用(连接到先前开发的后端 `MqttCommunicationLogController`)。
|
||||||
|
- 完善页面的筛选、排序和错误处理功能。
|
||||||
|
|
||||||
## 2024-08-08 (MQTT通信日志功能 - 开发完成)
|
## 2024-08-08 (MQTT通信日志功能 - 开发完成)
|
||||||
|
|
||||||
- **概述**: MQTT通信日志功能模块开发完成。该模块旨在记录系统与MQTT代理之间的所有通信消息,并提供前端界面供管理员查询和审计。
|
- **概述**: MQTT通信日志功能模块开发完成。该模块旨在记录系统与MQTT代理之间的所有通信消息,并提供前端界面供管理员查询和审计。
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
* 充电桩/车位管理(状态监控、添加、编辑、删除)
|
* 充电桩/车位管理(状态监控、添加、编辑、删除)
|
||||||
* 充电会话管理(查询、监控)
|
* 充电会话管理(查询、监控)
|
||||||
* 激活码管理(生成、查询、删除)
|
* 激活码管理(生成、查询、删除)
|
||||||
|
* MQTT通信日志查看(查询、审计所有MQTT消息)
|
||||||
* 系统概览与统计
|
* 系统概览与统计
|
||||||
* **硬件交互 (通过 MQTT)**:
|
* **硬件交互 (通过 MQTT)**:
|
||||||
* 充电桩状态上报 (空闲、连接中、充电中、故障等)
|
* 充电桩状态上报 (空闲、连接中、充电中、故障等)
|
||||||
@@ -24,7 +25,7 @@
|
|||||||
|
|
||||||
## 2. 技术栈
|
## 2. 技术栈
|
||||||
|
|
||||||
* **后端**: Spring Boot, Spring Security, MyBatis Plus, MySQL, Mosquitto (MQTT Broker)
|
* **后端**: Spring Boot, Spring Security, MyBatis Plus, MySQL, Mosquitto (MQTT Broker), Spring AOP (用于日志)
|
||||||
* **前端**: Next.js, React, TypeScript, Ant Design, Axios
|
* **前端**: Next.js, React, TypeScript, Ant Design, Axios
|
||||||
* **数据库**: MySQL
|
* **数据库**: MySQL
|
||||||
* **通信协议**: HTTP/HTTPS, MQTT
|
* **通信协议**: HTTP/HTTPS, MQTT
|
||||||
@@ -247,5 +248,8 @@
|
|||||||
* 完善各模块的单元测试和集成测试。
|
* 完善各模块的单元测试和集成测试。
|
||||||
* 进一步优化 API 文档和错误处理机制。
|
* 进一步优化 API 文档和错误处理机制。
|
||||||
* 根据实际运营需求,增加更细致的统计报表功能。
|
* 根据实际运营需求,增加更细致的统计报表功能。
|
||||||
|
* MQTT通信日志模块:
|
||||||
|
* (可选) 进一步优化Payload的展示,例如对JSON格式的Payload进行美化或提供折叠/展开功能。
|
||||||
|
* (可选) 根据实际需求,考虑日志数据的定期归档或清理策略的实现。
|
||||||
* 考虑引入更高级的 MQTT 特性,如QoS、遗嘱消息等。
|
* 考虑引入更高级的 MQTT 特性,如QoS、遗嘱消息等。
|
||||||
* 前端 UI/UX 持续打磨,提升用户体验。
|
* 前端 UI/UX 持续打磨,提升用户体验。
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
Table, Form, Input, Select, Button, DatePicker, message, Tag, Tooltip, Card, Row, Col, Space
|
||||||
|
} from 'antd';
|
||||||
|
import type { TableProps, TablePaginationConfig } from 'antd';
|
||||||
|
import type { FilterValue, SorterResult } from 'antd/es/table/interface';
|
||||||
|
import { SearchOutlined, RedoOutlined } from '@ant-design/icons';
|
||||||
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
|
import { fetchMqttLogs, BaseResponse } from '@/services/logService';
|
||||||
|
import {
|
||||||
|
MqttCommunicationLog, MqttLogQueryRequest, PageResponse
|
||||||
|
} from '@/types/log.types';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
const { RangePicker } = DatePicker;
|
||||||
|
const { Option } = Select;
|
||||||
|
|
||||||
|
const MqttCommunicationLogPage: React.FC = () => {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const [form] = Form.useForm<MqttLogQueryRequest>();
|
||||||
|
const [logs, setLogs] = useState<MqttCommunicationLog[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [pagination, setPagination] = useState<TablePaginationConfig>({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
total: 0,
|
||||||
|
showSizeChanger: true,
|
||||||
|
pageSizeOptions: ['10', '20', '50', '100'],
|
||||||
|
showTotal: (total) => `共 ${total} 条记录`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchData = useCallback(async (params: MqttLogQueryRequest = {}) => {
|
||||||
|
setLoading(true);
|
||||||
|
const queryParams: MqttLogQueryRequest = {
|
||||||
|
current: pagination.current,
|
||||||
|
pageSize: pagination.pageSize,
|
||||||
|
...form.getFieldsValue(), // Get values from the form
|
||||||
|
...params, // Override with any explicitly passed params (like sorters)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle dateRange from AntD RangePicker
|
||||||
|
if (queryParams.dateRange && queryParams.dateRange[0] && queryParams.dateRange[1]) {
|
||||||
|
queryParams.startTime = queryParams.dateRange[0].toISOString();
|
||||||
|
queryParams.endTime = queryParams.dateRange[1].toISOString();
|
||||||
|
delete queryParams.dateRange; // Remove from params sent to backend
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response: BaseResponse<PageResponse<MqttCommunicationLog>> = await fetchMqttLogs(queryParams);
|
||||||
|
if (response.code === 0 && response.data) {
|
||||||
|
setLogs(response.data.records || []);
|
||||||
|
setPagination(prev => ({
|
||||||
|
...prev,
|
||||||
|
current: response.data.current,
|
||||||
|
pageSize: response.data.size,
|
||||||
|
total: response.data.total,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
message.error(response.message || '获取MQTT日志失败');
|
||||||
|
setLogs([]);
|
||||||
|
setPagination(prev => ({ ...prev, total: 0, current: 1 }));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取MQTT日志错误:', error);
|
||||||
|
message.error('无法连接到服务器或请求失败');
|
||||||
|
setLogs([]);
|
||||||
|
setPagination(prev => ({ ...prev, total: 0, current: 1 }));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [form, pagination.current, pagination.pageSize]); // Dependencies for useCallback
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user?.role === 'admin') {
|
||||||
|
fetchData();
|
||||||
|
}
|
||||||
|
}, [fetchData, user?.role]);
|
||||||
|
|
||||||
|
const handleTableChange: TableProps<MqttCommunicationLog>['onChange'] = (
|
||||||
|
newPagination: TablePaginationConfig,
|
||||||
|
filters: Record<string, FilterValue | null>,
|
||||||
|
sorter: SorterResult<MqttCommunicationLog> | SorterResult<MqttCommunicationLog>[],
|
||||||
|
) => {
|
||||||
|
const currentSorter = Array.isArray(sorter) ? sorter[0] : sorter;
|
||||||
|
const sortParams: MqttLogQueryRequest = {};
|
||||||
|
if (currentSorter && currentSorter.field && currentSorter.order) {
|
||||||
|
sortParams.sortField = String(currentSorter.field);
|
||||||
|
sortParams.sortOrder = currentSorter.order;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPagination(prev => ({
|
||||||
|
...prev,
|
||||||
|
current: newPagination.current,
|
||||||
|
pageSize: newPagination.pageSize,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// fetchData will pick up new pagination from state
|
||||||
|
// and form values, and will merge sortParams
|
||||||
|
fetchData(sortParams);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFinish = () => {
|
||||||
|
// Reset to first page on new search
|
||||||
|
setPagination(prev => ({ ...prev, current: 1 }));
|
||||||
|
fetchData(); // fetchData will use current form values and new pagination
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
form.resetFields();
|
||||||
|
setPagination(prev => ({ ...prev, current: 1 }));
|
||||||
|
// After reset, fetch with default (cleared) form values
|
||||||
|
fetchData({}); // Pass empty object to ensure sortField/sortOrder are not persisted from previous state if any
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!user && !loading) {
|
||||||
|
return <Card style={{ margin: 20 }}><p className="text-center text-red-500 mt-8">请先登录。</p></Card>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user?.role !== 'admin' && !loading) {
|
||||||
|
return (
|
||||||
|
<Card style={{ margin: 20 }} title="MQTT 通信日志">
|
||||||
|
<p className="text-center text-red-600 text-lg bg-red-100 border border-red-400 rounded-md p-6 shadow-md">
|
||||||
|
抱歉,您没有权限访问此页面。此功能仅对管理员开放。
|
||||||
|
</p>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns: TableProps<MqttCommunicationLog>['columns'] = [
|
||||||
|
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80, sorter: true },
|
||||||
|
{
|
||||||
|
title: '时间戳',
|
||||||
|
dataIndex: 'logTimestamp',
|
||||||
|
key: 'logTimestamp',
|
||||||
|
sorter: true,
|
||||||
|
width: 180,
|
||||||
|
render: (text: string) => dayjs(text).format('YYYY-MM-DD HH:mm:ss.SSS'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '方向',
|
||||||
|
dataIndex: 'direction',
|
||||||
|
key: 'direction',
|
||||||
|
width: 100,
|
||||||
|
render: (direction: 'UPSTREAM' | 'DOWNSTREAM') => (
|
||||||
|
<Tag color={direction === 'UPSTREAM' ? 'blue' : 'green'}>
|
||||||
|
{direction === 'UPSTREAM' ? '上行' : '下行'}
|
||||||
|
</Tag>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{ title: '客户端ID', dataIndex: 'clientId', key: 'clientId', width: 150, sorter: true, ellipsis: true },
|
||||||
|
{ title: '主题', dataIndex: 'topic', key: 'topic', width: 250, sorter: true, ellipsis: true },
|
||||||
|
{
|
||||||
|
title: 'Payload',
|
||||||
|
dataIndex: 'payload',
|
||||||
|
key: 'payload',
|
||||||
|
ellipsis: true,
|
||||||
|
render: (text: string) => (
|
||||||
|
<Tooltip title={text} placement="topLeft">
|
||||||
|
<pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all', maxHeight: '60px', overflowY: 'auto', margin: 0, fontSize: '12px' }}>
|
||||||
|
{text}
|
||||||
|
</pre>
|
||||||
|
</Tooltip>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '格式',
|
||||||
|
dataIndex: 'payloadFormat',
|
||||||
|
key: 'payloadFormat',
|
||||||
|
width: 80,
|
||||||
|
render: (format?: 'TEXT' | 'JSON' | 'BINARY') => format || 'N/A',
|
||||||
|
},
|
||||||
|
{ title: 'QoS', dataIndex: 'qos', key: 'qos', width: 60, sorter: true },
|
||||||
|
{
|
||||||
|
title: 'Retained',
|
||||||
|
dataIndex: 'isRetained',
|
||||||
|
key: 'isRetained',
|
||||||
|
width: 90,
|
||||||
|
render: (isRetained?: boolean) => (isRetained ? <Tag color="gold">是</Tag> : '否'),
|
||||||
|
},
|
||||||
|
{ title: '关联会话ID', dataIndex: 'relatedSessionId', key: 'relatedSessionId', width: 120, sorter: true },
|
||||||
|
{ title: '关联任务ID', dataIndex: 'relatedTaskId', key: 'relatedTaskId', width: 120, sorter: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ margin: '20px' }}>
|
||||||
|
<Card title="MQTT 通信日志查询" style={{ marginBottom: '20px' }}>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={onFinish}
|
||||||
|
>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col xs={24} sm={12} md={8} lg={6}>
|
||||||
|
<Form.Item name="clientId" label="客户端ID">
|
||||||
|
<Input placeholder="请输入客户端ID" />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} md={8} lg={6}>
|
||||||
|
<Form.Item name="topic" label="主题">
|
||||||
|
<Input placeholder="请输入主题" />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} md={8} lg={6}>
|
||||||
|
<Form.Item name="direction" label="方向">
|
||||||
|
<Select placeholder="请选择消息方向" allowClear>
|
||||||
|
<Option value="UPSTREAM">上行</Option>
|
||||||
|
<Option value="DOWNSTREAM">下行</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} md={8} lg={6}>
|
||||||
|
<Form.Item name="payloadContains" label="Payload包含">
|
||||||
|
<Input placeholder="请输入Payload包含的内容" />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} md={8} lg={6}>
|
||||||
|
<Form.Item name="dateRange" label="时间范围">
|
||||||
|
<RangePicker showTime format="YYYY-MM-DD HH:mm:ss" style={{ width: '100%' }} />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} md={8} lg={6}>
|
||||||
|
<Form.Item name="relatedSessionId" label="关联会话ID">
|
||||||
|
<Input type="number" placeholder="请输入会话ID" />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} md={8} lg={6}>
|
||||||
|
<Form.Item name="relatedTaskId" label="关联任务ID">
|
||||||
|
<Input type="number" placeholder="请输入任务ID" />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Col span={24} style={{ textAlign: 'right' }}>
|
||||||
|
<Space>
|
||||||
|
<Button type="primary" htmlType="submit" icon={<SearchOutlined />}>
|
||||||
|
查询
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleReset} icon={<RedoOutlined />}>
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card title="日志列表">
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
rowKey="id"
|
||||||
|
dataSource={logs}
|
||||||
|
pagination={pagination}
|
||||||
|
loading={loading}
|
||||||
|
onChange={handleTableChange}
|
||||||
|
scroll={{ x: 1500 }} // Enable horizontal scroll if content is too wide
|
||||||
|
bordered
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MqttCommunicationLogPage;
|
||||||
@@ -47,7 +47,7 @@ export default function AuthenticatedLayout({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col bg-gray-50">
|
<div className="min-h-screen flex flex-col bg-gray-100">
|
||||||
<header className="bg-white shadow-md w-full sticky top-0 z-50">
|
<header className="bg-white shadow-md w-full sticky top-0 z-50">
|
||||||
<nav className="container mx-auto px-6 py-3 flex justify-between items-center">
|
<nav className="container mx-auto px-6 py-3 flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
@@ -77,7 +77,7 @@ export default function AuthenticatedLayout({
|
|||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
<main className="flex-grow container mx-auto p-4 sm:p-6 lg:p-8">
|
<main className="flex-grow container mx-auto py-8 px-4 md:px-6 lg:px-8">
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
<footer className="bg-gray-800 text-white text-center p-4 mt-auto w-full">
|
<footer className="bg-gray-800 text-white text-center p-4 mt-auto w-full">
|
||||||
|
|||||||
@@ -1,297 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
Form,
|
|
||||||
Input,
|
|
||||||
Select,
|
|
||||||
DatePicker,
|
|
||||||
Button,
|
|
||||||
Table,
|
|
||||||
Row,
|
|
||||||
Col,
|
|
||||||
Space,
|
|
||||||
Typography,
|
|
||||||
Tag,
|
|
||||||
Tooltip,
|
|
||||||
message, // Import Ant Design message for notifications
|
|
||||||
} from 'antd';
|
|
||||||
import type { TableProps } from 'antd';
|
|
||||||
import { SearchOutlined, RedoOutlined } from '@ant-design/icons';
|
|
||||||
import { MqttLogQueryRequest, MqttCommunicationLog, PageResponse } from '@/types/log.types';
|
|
||||||
import { fetchMqttLogs } from '@/services/logService';
|
|
||||||
|
|
||||||
const { Title } = Typography;
|
|
||||||
const { RangePicker } = DatePicker;
|
|
||||||
|
|
||||||
|
|
||||||
const MqttLogsPage: React.FC = () => {
|
|
||||||
const [form] = Form.useForm<MqttLogQueryRequest>();
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [logs, setLogs] = useState<MqttCommunicationLog[]>([]);
|
|
||||||
const [total, setTotal] = useState(0);
|
|
||||||
const [pagination, setPagination] = useState({
|
|
||||||
current: 1,
|
|
||||||
pageSize: 10,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Function to fetch logs
|
|
||||||
const loadLogs = async (params: MqttLogQueryRequest) => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
// fetchMqttLogs is expected to return the structure that includes { records, total, current, size }
|
|
||||||
// If fetchMqttLogs returns BaseResponse<PageResponse<...>>, then we need to access response.data
|
|
||||||
const response = await fetchMqttLogs(params);
|
|
||||||
|
|
||||||
// Assuming fetchMqttLogs returns BaseResponse<PageResponse<MqttCommunicationLog>> as per backend
|
|
||||||
// and logService.ts might be updated to return response.data.data or similar,
|
|
||||||
// OR, if logService.ts returns the raw BaseResponse.data (which itself is PageResponse)
|
|
||||||
// For now, let's assume 'response' here IS PageResponse as suggested by previous type hints
|
|
||||||
// If the actual response from fetchMqttLogs is { code, data: { records, total, ... }, message },
|
|
||||||
// then we need:
|
|
||||||
// const pageData = response.data; // Access the 'data' property of BaseResponse
|
|
||||||
// setLogs(pageData.records || []);
|
|
||||||
// setTotal(pageData.total || 0);
|
|
||||||
// setPagination(prev => ({ ...prev, current: pageData.current || 1, pageSize: pageData.size || 10 }));
|
|
||||||
|
|
||||||
// Based on the screenshot, fetchMqttLogs returns an object that has a 'data' property which contains records, total etc.
|
|
||||||
// So, the PageResponse<MqttCommunicationLog> is inside response.data
|
|
||||||
if (response && response.data) { // Check if response and response.data exist
|
|
||||||
const pageData: PageResponse<MqttCommunicationLog> = response.data;
|
|
||||||
setLogs(pageData.records || []);
|
|
||||||
setTotal(pageData.total || 0);
|
|
||||||
setPagination(prev => ({ ...prev, current: pageData.current || 1, pageSize: pageData.size || 10 }));
|
|
||||||
} else {
|
|
||||||
// Handle cases where response or response.data might be undefined, though API should return it
|
|
||||||
setLogs([]);
|
|
||||||
setTotal(0);
|
|
||||||
message.error('获取到的数据格式不正确!');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch MQTT logs:', error);
|
|
||||||
message.error('获取MQTT日志失败!');
|
|
||||||
}
|
|
||||||
setLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initial load
|
|
||||||
useEffect(() => {
|
|
||||||
loadLogs({ current: pagination.current, pageSize: pagination.pageSize });
|
|
||||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
||||||
// Initial load only on mount, pagination changes will trigger via handleTableChange -> form.submit() -> handleSearch
|
|
||||||
|
|
||||||
const handleSearch = async (values: MqttLogQueryRequest) => {
|
|
||||||
const queryParams: MqttLogQueryRequest = {
|
|
||||||
...values,
|
|
||||||
current: 1, // Reset to first page for new search
|
|
||||||
pageSize: pagination.pageSize,
|
|
||||||
};
|
|
||||||
if (values.dateRange && values.dateRange[0] && values.dateRange[1]) {
|
|
||||||
queryParams.startTime = values.dateRange[0].toISOString();
|
|
||||||
queryParams.endTime = values.dateRange[1].toISOString();
|
|
||||||
}
|
|
||||||
delete queryParams.dateRange; // Remove AntD specific field
|
|
||||||
loadLogs(queryParams);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleReset = () => {
|
|
||||||
form.resetFields();
|
|
||||||
setPagination({ current: 1, pageSize: 10 });
|
|
||||||
loadLogs({ current: 1, pageSize: 10 }); // Load with default pagination after reset
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTableChange: TableProps<MqttCommunicationLog>['onChange'] = (
|
|
||||||
paginationConfig,
|
|
||||||
filters,
|
|
||||||
sorter
|
|
||||||
) => {
|
|
||||||
const newCurrent = paginationConfig.current || 1;
|
|
||||||
const newPageSize = paginationConfig.pageSize || 10;
|
|
||||||
|
|
||||||
const params: MqttLogQueryRequest = {
|
|
||||||
...form.getFieldsValue(), // Get current form values
|
|
||||||
current: newCurrent,
|
|
||||||
pageSize: newPageSize,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (sorter && (sorter as any).field) {
|
|
||||||
params.sortField = (sorter as any).field as string;
|
|
||||||
params.sortOrder = (sorter as any).order === 'ascend' ? 'ascend' : 'descend';
|
|
||||||
} else {
|
|
||||||
delete params.sortField;
|
|
||||||
delete params.sortOrder;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dateRangeValue = form.getFieldValue('dateRange');
|
|
||||||
if (dateRangeValue && dateRangeValue[0] && dateRangeValue[1]) {
|
|
||||||
params.startTime = dateRangeValue[0].toISOString();
|
|
||||||
params.endTime = dateRangeValue[1].toISOString();
|
|
||||||
}
|
|
||||||
delete params.dateRange;
|
|
||||||
|
|
||||||
setPagination({current: newCurrent, pageSize: newPageSize}); // Update local pagination state first
|
|
||||||
loadLogs(params);
|
|
||||||
};
|
|
||||||
|
|
||||||
const columns: TableProps<MqttCommunicationLog>['columns'] = [
|
|
||||||
{ title: 'ID', dataIndex: 'id', key: 'id', sorter: true, width: 80 },
|
|
||||||
{
|
|
||||||
title: '时间',
|
|
||||||
dataIndex: 'logTimestamp',
|
|
||||||
key: 'logTimestamp',
|
|
||||||
sorter: true,
|
|
||||||
width: 180,
|
|
||||||
render: (text: string) => text ? new Date(text).toLocaleString() : '-',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '方向',
|
|
||||||
dataIndex: 'direction',
|
|
||||||
key: 'direction',
|
|
||||||
sorter: true,
|
|
||||||
width: 120,
|
|
||||||
render: (direction: string) => {
|
|
||||||
if (direction === 'UPSTREAM') return <Tag color='blue'>上行</Tag>;
|
|
||||||
if (direction === 'DOWNSTREAM') return <Tag color='green'>下行</Tag>;
|
|
||||||
return direction || '-';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ title: '消息ID', dataIndex: 'messageId', key: 'messageId', sorter: true, width: 150 },
|
|
||||||
{ title: '客户端ID', dataIndex: 'clientId', key: 'clientId', sorter: true, width: 200, ellipsis: true },
|
|
||||||
{ title: '主题', dataIndex: 'topic', key: 'topic', sorter: true, ellipsis: true },
|
|
||||||
{
|
|
||||||
title: 'Payload',
|
|
||||||
dataIndex: 'payload',
|
|
||||||
key: 'payload',
|
|
||||||
ellipsis: true,
|
|
||||||
width: 250,
|
|
||||||
render: (text: string) => (
|
|
||||||
text ? (
|
|
||||||
<Tooltip title={<pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all', margin: 0 }}>{text}</pre>}>
|
|
||||||
<pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all', margin: 0, maxHeight: '60px', overflowY: 'auto' }}>
|
|
||||||
{text.length > 100 ? `${text.substring(0, 100)}...` : text}
|
|
||||||
</pre>
|
|
||||||
</Tooltip>
|
|
||||||
) : '-'
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '格式',
|
|
||||||
dataIndex: 'payloadFormat',
|
|
||||||
key: 'payloadFormat',
|
|
||||||
width: 100,
|
|
||||||
render: (format?: string) => {
|
|
||||||
if (format === 'TEXT') return '文本';
|
|
||||||
if (format === 'JSON') return 'JSON';
|
|
||||||
return format || '-';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ title: 'QoS', dataIndex: 'qos', key: 'qos', sorter: true, width: 80 },
|
|
||||||
{
|
|
||||||
title: '保留',
|
|
||||||
dataIndex: 'isRetained',
|
|
||||||
key: 'isRetained',
|
|
||||||
render: (retained?: boolean) => (typeof retained === 'boolean' ? (retained ? '是' : '否') : '-'),
|
|
||||||
width: 80
|
|
||||||
},
|
|
||||||
{ title: '会话ID', dataIndex: 'relatedSessionId', key: 'relatedSessionId', sorter: true, width: 100 },
|
|
||||||
{ title: '任务ID', dataIndex: 'relatedTaskId', key: 'relatedTaskId', sorter: true, width: 100 },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<Title level={4}>MQTT 通信日志</Title>
|
|
||||||
<Form form={form} layout="vertical" onFinish={handleSearch} initialValues={{qos: undefined /* Ensure placeholder shows */}}>
|
|
||||||
<Row gutter={16}>
|
|
||||||
<Col xs={24} sm={12} md={8}>
|
|
||||||
<Form.Item name="messageId" label="消息ID">
|
|
||||||
<Input placeholder="请输入消息ID" allowClear />
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col xs={24} sm={12} md={8}>
|
|
||||||
<Form.Item name="direction" label="方向">
|
|
||||||
<Select placeholder="选择方向" allowClear>
|
|
||||||
<Select.Option value="UPSTREAM">UPSTREAM</Select.Option>
|
|
||||||
<Select.Option value="DOWNSTREAM">DOWNSTREAM</Select.Option>
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col xs={24} sm={12} md={8}>
|
|
||||||
<Form.Item name="clientId" label="客户端ID">
|
|
||||||
<Input placeholder="请输入客户端ID" allowClear />
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<Row gutter={16}>
|
|
||||||
<Col xs={24} sm={12} md={8}>
|
|
||||||
<Form.Item name="topic" label="主题">
|
|
||||||
<Input placeholder="请输入主题" allowClear />
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col xs={24} sm={12} md={8}>
|
|
||||||
<Form.Item name="payloadContains" label="Payload包含">
|
|
||||||
<Input placeholder="请输入Payload包含的内容" allowClear />
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col xs={24} sm={12} md={8}>
|
|
||||||
<Form.Item name="qos" label="QoS">
|
|
||||||
<Select placeholder="选择QoS" allowClear>
|
|
||||||
<Select.Option value={0}>0</Select.Option>
|
|
||||||
<Select.Option value={1}>1</Select.Option>
|
|
||||||
<Select.Option value={2}>2</Select.Option>
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<Row gutter={16}>
|
|
||||||
<Col xs={24} sm={12} md={8}>
|
|
||||||
<Form.Item name="relatedSessionId" label="关联会话ID">
|
|
||||||
<Input type="number" placeholder="请输入关联会话ID" allowClear />
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col xs={24} sm={12} md={8}>
|
|
||||||
<Form.Item name="relatedTaskId" label="关联任务ID">
|
|
||||||
<Input type="number" placeholder="请输入关联任务ID" allowClear />
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col xs={24} sm={12} md={8}>
|
|
||||||
<Form.Item name="dateRange" label="日期范围">
|
|
||||||
<RangePicker showTime format="YYYY-MM-DD HH:mm:ss" style={{ width: '100%' }} />
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<Row>
|
|
||||||
<Col span={24} style={{ textAlign: 'right' }}>
|
|
||||||
<Space>
|
|
||||||
<Button type="primary" htmlType="submit" icon={<SearchOutlined />} loading={loading}>
|
|
||||||
查询
|
|
||||||
</Button>
|
|
||||||
<Button icon={<RedoOutlined />} onClick={handleReset}>
|
|
||||||
重置
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</Form>
|
|
||||||
|
|
||||||
<Table
|
|
||||||
style={{ marginTop: 20 }}
|
|
||||||
columns={columns}
|
|
||||||
dataSource={logs}
|
|
||||||
loading={loading}
|
|
||||||
rowKey="id"
|
|
||||||
pagination={{
|
|
||||||
...pagination,
|
|
||||||
total,
|
|
||||||
showSizeChanger: true,
|
|
||||||
showQuickJumper: true,
|
|
||||||
showTotal: (totalLogs) => `总计 ${totalLogs} 条`,
|
|
||||||
}}
|
|
||||||
onChange={handleTableChange}
|
|
||||||
scroll={{ x: 1500 }}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MqttLogsPage;
|
|
||||||
@@ -2,6 +2,6 @@ package com.yupi.project.mapper;
|
|||||||
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
import com.yupi.project.model.entity.MqttCommunicationLog;
|
import com.yupi.project.model.entity.MqttCommunicationLog;
|
||||||
|
|
||||||
public interface MqttCommunicationLogMapper extends BaseMapper<MqttCommunicationLog> {
|
public interface MqttCommunicationLogMapper extends BaseMapper<MqttCommunicationLog> {
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user