Files
gongxue-base/apps/admin/src/pages/OperationLogs/index.tsx
wangziqi 375c7ec60b feat: refine admin forms, attendance and finance workflows
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
2026-07-18 12:54:10 +00:00

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;