forked from wangziqi/gongxue-base
Squash merge PR #23. Included changes: - complete occupancy check-in required fields/default payload - improve responsive admin management pages - fix attendance edge cases and attendance period config - refine wallet/finance-related workflow handling Checks: - npm run typecheck -w apps/admin - npm run typecheck -w apps/server
182 lines
5.1 KiB
TypeScript
182 lines
5.1 KiB
TypeScript
import React, { useEffect, useState, useMemo, useCallback } from 'react';
|
|
import { Table, Select, DatePicker, Space, Tag, Tooltip } from 'antd';
|
|
import dayjs from 'dayjs';
|
|
import api from '../../api';
|
|
import { message } from '../../ui/app-message';
|
|
|
|
const { RangePicker } = DatePicker;
|
|
|
|
const moduleColorMap: Record<string, string> = {
|
|
学生: 'blue',
|
|
宿舍: 'green',
|
|
入住: 'cyan',
|
|
费用: 'orange',
|
|
账单: 'red',
|
|
账号: 'purple',
|
|
认证: 'magenta',
|
|
};
|
|
|
|
const statusMap: Record<string, { text: string; color: string }> = {
|
|
success: { text: '成功', color: 'green' },
|
|
fail: { text: '失败', color: 'red' },
|
|
};
|
|
|
|
const OperationLogsPage: React.FC = () => {
|
|
const [data, setData] = useState<any[]>([]);
|
|
const [total, setTotal] = useState(0);
|
|
const [loading, setLoading] = useState(false);
|
|
const [page, setPage] = useState(1);
|
|
const [pageSize, setPageSize] = useState(20);
|
|
const [filterModule, setFilterModule] = useState<string | undefined>();
|
|
const [dateRange, setDateRange] = useState<[string, string] | null>(null);
|
|
|
|
const fetchData = useCallback(async () => {
|
|
setLoading(true);
|
|
try {
|
|
const params: any = { page, pageSize };
|
|
if (filterModule) params.module = filterModule;
|
|
if (dateRange) {
|
|
params.startDate = dateRange[0];
|
|
params.endDate = dateRange[1];
|
|
}
|
|
const res: any = await api.get('/operation-logs', { params });
|
|
setData(res.data);
|
|
setTotal(res.total);
|
|
} catch (e: unknown) {
|
|
const err = e as { message?: string };
|
|
message.error(err?.message || '加载失败,请稍后重试');
|
|
}
|
|
setLoading(false);
|
|
}, [page, pageSize, filterModule, dateRange]);
|
|
|
|
useEffect(() => {
|
|
fetchData();
|
|
}, [fetchData]);
|
|
|
|
const columns = useMemo(
|
|
() => [
|
|
{
|
|
title: '时间',
|
|
dataIndex: 'createdAt',
|
|
width: 170,
|
|
render: (v: string) => dayjs(v).format('YYYY-MM-DD HH:mm:ss'),
|
|
},
|
|
{ title: '操作人', dataIndex: 'username', width: 100 },
|
|
{
|
|
title: '模块',
|
|
dataIndex: 'module',
|
|
width: 100,
|
|
render: (v: string) => <Tag color={moduleColorMap[v] || 'default'}>{v}</Tag>,
|
|
},
|
|
{ title: '操作', dataIndex: 'action', width: 150 },
|
|
{
|
|
title: '状态',
|
|
dataIndex: 'status',
|
|
width: 80,
|
|
render: (v: string) => {
|
|
const s = statusMap[v] || statusMap['success'];
|
|
return <Tag color={s.color}>{s.text}</Tag>;
|
|
},
|
|
},
|
|
{
|
|
title: '详情',
|
|
width: 200,
|
|
dataIndex: 'detail',
|
|
ellipsis: true,
|
|
render: (v: string) =>
|
|
v ? (
|
|
<Tooltip title={v}>
|
|
<span>{v}</span>
|
|
</Tooltip>
|
|
) : (
|
|
'-'
|
|
),
|
|
},
|
|
{ title: 'IP地址', dataIndex: 'ipAddress', width: 130, render: (v: string) => v || '-' },
|
|
{
|
|
title: '终端',
|
|
dataIndex: 'userAgent',
|
|
width: 120,
|
|
ellipsis: true,
|
|
render: (v: string) => {
|
|
if (!v) return '-';
|
|
if (v.includes('Mobile')) return <Tag color="blue">手机</Tag>;
|
|
if (v.includes('Windows')) return <Tag>Windows</Tag>;
|
|
if (v.includes('Mac')) return <Tag>Mac</Tag>;
|
|
if (v.includes('Linux')) return <Tag>Linux</Tag>;
|
|
return (
|
|
<Tooltip title={v}>
|
|
<Tag>其他</Tag>
|
|
</Tooltip>
|
|
);
|
|
},
|
|
},
|
|
],
|
|
[],
|
|
);
|
|
|
|
return (
|
|
<div>
|
|
<div
|
|
style={{
|
|
marginBottom: 16,
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
flexWrap: 'wrap',
|
|
gap: 12,
|
|
}}
|
|
>
|
|
<h2 style={{ margin: 0 }}>操作日志</h2>
|
|
<Space wrap>
|
|
<Select
|
|
allowClear
|
|
placeholder="筛选模块"
|
|
style={{ width: 140 }}
|
|
value={filterModule}
|
|
onChange={(v) => {
|
|
setFilterModule(v);
|
|
setPage(1);
|
|
}}
|
|
options={['认证', '学生', '宿舍', '入住', '费用', '账单', '账号'].map((m) => ({
|
|
value: m,
|
|
label: m,
|
|
}))}
|
|
/>
|
|
<RangePicker
|
|
placeholder={['开始日期', '结束日期']}
|
|
format="YYYY-MM-DD"
|
|
onChange={(dates) => {
|
|
if (dates)
|
|
setDateRange([dates[0]!.format('YYYY-MM-DD'), dates[1]!.format('YYYY-MM-DD')]);
|
|
else setDateRange(null);
|
|
setPage(1);
|
|
}}
|
|
/>
|
|
</Space>
|
|
</div>
|
|
<Table
|
|
columns={columns}
|
|
dataSource={data}
|
|
rowKey="id"
|
|
loading={loading}
|
|
scroll={{ x: 1100 }}
|
|
pagination={{
|
|
current: page,
|
|
total,
|
|
pageSize,
|
|
showSizeChanger: true,
|
|
pageSizeOptions: [20, 50, 100],
|
|
onChange: (nextPage, nextPageSize) => {
|
|
setPage(nextPage);
|
|
setPageSize(nextPageSize);
|
|
},
|
|
showTotal: (t) => `共 ${t} 条`,
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default OperationLogsPage;
|