mqtt消息记录开发完成
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useRouter } from 'next/navigation';
|
||||
// import Link from 'next/link'; // Temporarily remove Link to test a plain anchor
|
||||
import Link from 'next/link';
|
||||
import LoadingSpinner from '@/components/LoadingSpinner';
|
||||
|
||||
export default function AuthenticatedLayout({
|
||||
@@ -11,18 +11,16 @@ export default function AuthenticatedLayout({
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { isAuthenticated, isLoading, user, logout } = useAuth(); // 获取 user 和 logout
|
||||
const { isAuthenticated, isLoading, user, logout } = useAuth();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
// Only redirect if loading is complete and user is not authenticated
|
||||
if (!isLoading && !isAuthenticated) {
|
||||
console.log('AuthenticatedLayout: User not authenticated, redirecting to login.');
|
||||
router.replace('/login');
|
||||
}
|
||||
}, [isAuthenticated, isLoading, router]);
|
||||
|
||||
// If still loading, show spinner
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen bg-gray-100">
|
||||
@@ -31,9 +29,7 @@ export default function AuthenticatedLayout({
|
||||
);
|
||||
}
|
||||
|
||||
// If loading is complete but user is not authenticated,
|
||||
// useEffect will handle redirection. Render null or spinner to avoid flashing content.
|
||||
if (!isAuthenticated || !user) { // Also check if user object is available
|
||||
if (!isAuthenticated || !user) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen bg-gray-100">
|
||||
<LoadingSpinner />
|
||||
@@ -41,16 +37,24 @@ export default function AuthenticatedLayout({
|
||||
);
|
||||
}
|
||||
|
||||
// If authenticated, render children
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await logout();
|
||||
router.push('/login');
|
||||
} catch (error) {
|
||||
console.error('注销失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col bg-gray-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">
|
||||
<div>
|
||||
{/* Temporarily replaced Link with a plain anchor tag for testing */}
|
||||
<a href={user.role === 'admin' ? '/admin/dashboard' : '/dashboard'} className="text-xl font-bold text-blue-700 hover:text-blue-900 transition-colors">
|
||||
<Link href={user.role === 'admin' ? '/admin/dashboard' : '/dashboard'}
|
||||
className="text-xl font-bold text-blue-700 hover:text-blue-900 transition-colors">
|
||||
充电机器人管理系统
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
{user && (
|
||||
@@ -58,16 +62,14 @@ export default function AuthenticatedLayout({
|
||||
欢迎, <span className="font-medium">{user.username}</span> ({user.role === 'admin' ? '管理员' : '用户'})
|
||||
</span>
|
||||
)}
|
||||
{user.role === 'admin' && (
|
||||
<Link href="/admin/mqtt-logs"
|
||||
className="text-sm text-blue-600 hover:text-blue-800 hover:underline transition-colors">
|
||||
MQTT日志
|
||||
</Link>
|
||||
)}
|
||||
<button
|
||||
onClick={async () => {
|
||||
try {
|
||||
await logout();
|
||||
router.push('/login');
|
||||
} catch (error) {
|
||||
console.error('注销失败:', error);
|
||||
// 可以选择性地通知用户注销失败
|
||||
}
|
||||
}}
|
||||
onClick={handleLogout}
|
||||
className="bg-red-600 hover:bg-red-700 text-white py-2 px-4 rounded-md text-sm font-semibold transition duration-150 ease-in-out shadow-sm hover:shadow-md focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-opacity-50"
|
||||
>
|
||||
注销
|
||||
|
||||
297
charging_web_app/src/app/admin/mqtt-logs/page.tsx
Normal file
297
charging_web_app/src/app/admin/mqtt-logs/page.tsx
Normal file
@@ -0,0 +1,297 @@
|
||||
'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;
|
||||
48
charging_web_app/src/services/axiosInstance.ts
Normal file
48
charging_web_app/src/services/axiosInstance.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const axiosInstance = axios.create({
|
||||
// baseURL: process.env.NEXT_PUBLIC_API_BASE_URL || '/api', // Default to /api if not set for Next.js proxy
|
||||
// Since your Spring Boot backend is at /api, and Next.js dev server can proxy,
|
||||
// for client-side requests, you might not need a full baseURL if using relative paths correctly,
|
||||
// or ensure your Next.js proxy is set up for /api paths.
|
||||
// If Spring Boot is running on a different port during dev (e.g. 7529) and not proxied,
|
||||
// you'd need something like: baseURL: 'http://localhost:7529/api'
|
||||
// However, the backend controller is already mapped to /api/admin/mqtt-log, so calling /api/admin/... should work if proxied.
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// Optional: Add interceptors for request/response, e.g., for auth tokens
|
||||
axiosInstance.interceptors.request.use(
|
||||
(config) => {
|
||||
// Example: Add auth token if available
|
||||
// if (typeof window !== 'undefined') {
|
||||
// const token = localStorage.getItem('token');
|
||||
// if (token) {
|
||||
// config.headers.Authorization = `Bearer ${token}`;
|
||||
// }
|
||||
// }
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
axiosInstance.interceptors.response.use(
|
||||
(response) => {
|
||||
// Any status code that lie within the range of 2xx cause this function to trigger
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
// Any status codes that falls outside the range of 2xx cause this function to trigger
|
||||
// You can add global error handling here, e.g., for 401 Unauthorized redirect to login
|
||||
// if (error.response && error.response.status === 401) {
|
||||
// // redirect to login page
|
||||
// }
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default axiosInstance;
|
||||
65
charging_web_app/src/services/logService.ts
Normal file
65
charging_web_app/src/services/logService.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import axiosInstance from '@/services/axiosInstance'; // Try using alias path
|
||||
import { MqttLogQueryRequest, MqttCommunicationLog, PageResponse } from '@/types/log.types';
|
||||
|
||||
// Define a generic BaseResponse type to match the backend structure
|
||||
export interface BaseResponse<T> {
|
||||
code: number;
|
||||
data: T;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch MQTT communication logs from the backend.
|
||||
* @param params - The query parameters for fetching logs.
|
||||
* @returns A promise that resolves to a BaseResponse containing a page of MQTT communication logs.
|
||||
*/
|
||||
export const fetchMqttLogs = async (
|
||||
params: MqttLogQueryRequest
|
||||
): Promise<BaseResponse<PageResponse<MqttCommunicationLog>>> => {
|
||||
try {
|
||||
// Clean up dateRange before sending to backend
|
||||
const backendParams = { ...params };
|
||||
if (backendParams.dateRange) {
|
||||
delete backendParams.dateRange;
|
||||
}
|
||||
|
||||
const response = await axiosInstance.post<BaseResponse<PageResponse<MqttCommunicationLog>>>(
|
||||
'/api/admin/mqtt-log/list/page',
|
||||
backendParams
|
||||
);
|
||||
return response.data; // Axios wraps the actual server response in its own 'data' property
|
||||
// So, response.data here IS the BaseResponse from the backend.
|
||||
} catch (error) {
|
||||
console.error('Error fetching MQTT logs:', error);
|
||||
// Consider throwing a more specific error or handling it as per your app's error strategy
|
||||
// For now, rethrow to be caught by the caller
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// You might need to create axiosInstance.ts if it doesn't exist
|
||||
// Example for axiosInstance.ts:
|
||||
// import axios from 'axios';
|
||||
//
|
||||
// const axiosInstance = axios.create({
|
||||
// baseURL: process.env.NEXT_PUBLIC_API_BASE_URL || '', // Your API base URL
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/json',
|
||||
// },
|
||||
// });
|
||||
//
|
||||
// // Optional: Add interceptors for request/response, e.g., for auth tokens
|
||||
// axiosInstance.interceptors.request.use(
|
||||
// (config) => {
|
||||
// // const token = localStorage.getItem('token'); // Or however you store your token
|
||||
// // if (token) {
|
||||
// // config.headers.Authorization = `Bearer ${token}`;
|
||||
// // }
|
||||
// return config;
|
||||
// },
|
||||
// (error) => {
|
||||
// return Promise.reject(error);
|
||||
// }
|
||||
// );
|
||||
//
|
||||
// export default axiosInstance;
|
||||
43
charging_web_app/src/types/log.types.ts
Normal file
43
charging_web_app/src/types/log.types.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
export interface MqttCommunicationLog {
|
||||
id: number;
|
||||
logTimestamp: string;
|
||||
direction: 'UPSTREAM' | 'DOWNSTREAM';
|
||||
messageId?: string;
|
||||
clientId?: string;
|
||||
topic: string;
|
||||
payload: string;
|
||||
payloadFormat?: 'TEXT' | 'JSON' | 'BINARY';
|
||||
qos?: number;
|
||||
isRetained?: boolean;
|
||||
backendProcessingStatus?: string;
|
||||
backendProcessingInfo?: string;
|
||||
relatedSessionId?: number;
|
||||
relatedTaskId?: number;
|
||||
}
|
||||
|
||||
export interface MqttLogQueryRequest {
|
||||
messageId?: string;
|
||||
direction?: 'UPSTREAM' | 'DOWNSTREAM';
|
||||
clientId?: string;
|
||||
topic?: string;
|
||||
payloadContains?: string;
|
||||
qos?: number;
|
||||
startTime?: string; // ISO string for backend
|
||||
endTime?: string; // ISO string for backend
|
||||
relatedSessionId?: number;
|
||||
relatedTaskId?: number;
|
||||
current?: number;
|
||||
pageSize?: number;
|
||||
sortField?: string;
|
||||
sortOrder?: 'ascend' | 'descend';
|
||||
// For AntD RangePicker in Form, converted to startTime/endTime before API call
|
||||
dateRange?: [any, any];
|
||||
}
|
||||
|
||||
export interface PageResponse<T> {
|
||||
records: T[];
|
||||
total: number;
|
||||
current: number;
|
||||
size: number;
|
||||
// Add other pagination fields if your backend returns them
|
||||
}
|
||||
Reference in New Issue
Block a user