193 lines
6.1 KiB
JavaScript
193 lines
6.1 KiB
JavaScript
import React, { useState, useEffect, useCallback } from 'react';
|
|
import { Container, Row, Col, Table, Spinner, Alert, Form, Button } from 'react-bootstrap';
|
|
import { adminAPI } from '../../services/api';
|
|
import { toast } from 'react-toastify';
|
|
import { debounce } from '../../utils/debounce';
|
|
|
|
const OperationLog = () => {
|
|
const [logs, setLogs] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState(null);
|
|
const [page, setPage] = useState(1);
|
|
const [hasMore, setHasMore] = useState(true);
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [selectedUserId, setSelectedUserId] = useState('');
|
|
const [startDate, setStartDate] = useState('');
|
|
const [endDate, setEndDate] = useState('');
|
|
const [users, setUsers] = useState([]); // 用于用户筛选下拉框
|
|
|
|
const fetchLogs = async (pageNumber = page, search = searchQuery, userId = selectedUserId, start = startDate, end = endDate) => {
|
|
try {
|
|
setLoading(true);
|
|
const params = {
|
|
page: pageNumber,
|
|
search: search,
|
|
user_id: userId || undefined,
|
|
start_date: start || undefined,
|
|
end_date: end || undefined,
|
|
};
|
|
const res = await adminAPI.getOperationLogs(params);
|
|
if (pageNumber === 1) {
|
|
setLogs(res.data.logs);
|
|
} else {
|
|
setLogs((prevLogs) => [...prevLogs, ...res.data.logs]);
|
|
}
|
|
setHasMore(res.data.has_more);
|
|
setPage(pageNumber);
|
|
} catch (err) {
|
|
setError('获取操作日志失败');
|
|
toast.error('获取操作日志失败');
|
|
console.error(err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const fetchUsersForFilter = async () => {
|
|
try {
|
|
const res = await adminAPI.getUsers({ page: 1, per_page: 9999 }); // 获取所有用户用于筛选
|
|
setUsers(res.data.users);
|
|
} catch (err) {
|
|
console.error('获取用户列表失败', err);
|
|
}
|
|
};
|
|
|
|
const loadMore = () => {
|
|
if (hasMore && !loading) {
|
|
fetchLogs(page + 1);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchUsersForFilter();
|
|
}, []);
|
|
|
|
// 防抖搜索函数
|
|
const debouncedSearch = useCallback(
|
|
debounce((search, userId, start, end) => {
|
|
fetchLogs(1, search, userId, start, end);
|
|
}, 500),
|
|
[]
|
|
);
|
|
|
|
useEffect(() => {
|
|
debouncedSearch(searchQuery, selectedUserId, startDate, endDate);
|
|
}, [searchQuery, selectedUserId, startDate, endDate, debouncedSearch]);
|
|
|
|
return (
|
|
<Container fluid className="p-4">
|
|
<Row className="mb-4">
|
|
<Col>
|
|
<h2 className="text-center">操作日志</h2>
|
|
</Col>
|
|
</Row>
|
|
|
|
<Row className="mb-4">
|
|
<Col>
|
|
<Form className="d-flex flex-wrap align-items-center">
|
|
<Form.Group controlId="searchQuery" className="me-2 mb-2">
|
|
<Form.Control
|
|
type="text"
|
|
placeholder="搜索操作类型或详情..."
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
/>
|
|
</Form.Group>
|
|
<Form.Group controlId="userFilter" className="me-2 mb-2">
|
|
<Form.Select
|
|
value={selectedUserId}
|
|
onChange={(e) => setSelectedUserId(e.target.value)}
|
|
>
|
|
<option value="">所有用户</option>
|
|
{users.map((user) => (
|
|
<option key={user.id} value={user.id}>
|
|
{user.username}
|
|
</option>
|
|
))}
|
|
</Form.Select>
|
|
</Form.Group>
|
|
<Form.Group controlId="startDate" className="me-2 mb-2">
|
|
<Form.Control
|
|
type="date"
|
|
value={startDate}
|
|
onChange={(e) => setStartDate(e.target.value)}
|
|
/>
|
|
</Form.Group>
|
|
<Form.Group controlId="endDate" className="me-2 mb-2">
|
|
<Form.Control
|
|
type="date"
|
|
value={endDate}
|
|
onChange={(e) => setEndDate(e.target.value)}
|
|
/>
|
|
</Form.Group>
|
|
</Form>
|
|
</Col>
|
|
</Row>
|
|
|
|
{/* 统一处理加载状态 */}
|
|
{loading && (
|
|
<Row className="justify-content-center">
|
|
<Col xs="auto">
|
|
<Spinner animation="border" role="status">
|
|
<span className="visually-hidden">加载中...</span>
|
|
</Spinner>
|
|
</Col>
|
|
</Row>
|
|
)}
|
|
|
|
{error && <Alert variant="danger">{error}</Alert>}
|
|
|
|
{/* 无数据状态(仅在非加载且无错误时显示) */}
|
|
{!loading && !error && logs.length === 0 && (
|
|
<Alert variant="info">没有找到操作日志。</Alert>
|
|
)}
|
|
|
|
{/* 日志表格(仅在非加载、无错误且有数据时显示) */}
|
|
{!loading && !error && logs.length > 0 && (
|
|
<Row>
|
|
<Col>
|
|
<Table striped bordered hover responsive className="shadow-sm">
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>用户</th>
|
|
<th>操作类型</th>
|
|
<th>文件ID</th>
|
|
<th>文件名</th>
|
|
<th>时间</th>
|
|
<th>详情</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{logs.map((log) => (
|
|
<tr key={log.id}>
|
|
<td>{log.id}</td>
|
|
<td>{log.username}</td>
|
|
<td>{log.operation_type}</td>
|
|
<td>{log.file_id || 'N/A'}</td>
|
|
<td>{log.original_filename || 'N/A'}</td>
|
|
<td>{new Date(log.timestamp).toLocaleString()}</td>
|
|
<td>{log.details}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</Table>
|
|
{hasMore && (
|
|
<div className="d-flex justify-content-center mt-4">
|
|
<Button
|
|
variant="primary"
|
|
onClick={loadMore}
|
|
disabled={loading}
|
|
>
|
|
{loading ? '加载中...' : '加载更多'}
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</Col>
|
|
</Row>
|
|
)}
|
|
</Container>
|
|
);
|
|
};
|
|
|
|
export default OperationLog; |