mqtt消息记录优化,控制台页面样式优化

This commit is contained in:
2025-05-23 11:47:59 +08:00
parent 49f1220310
commit 021f9f0e0d
6 changed files with 337 additions and 301 deletions

View File

@@ -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;

View File

@@ -47,7 +47,7 @@ export default function AuthenticatedLayout({
};
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">
<nav className="container mx-auto px-6 py-3 flex justify-between items-center">
<div>
@@ -77,7 +77,7 @@ export default function AuthenticatedLayout({
</div>
</nav>
</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}
</main>
<footer className="bg-gray-800 text-white text-center p-4 mt-auto w-full">

View File

@@ -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;