05a1d06e61
review 发现 4 处仍用 new Date(v).toLocaleString 渲染后端 UTC 口径时间戳—— 正是 lib/format.ts 头注释明令消灭的反模式(SQLite 本地/非中国浏览器会差 8h, 生产靠 +00:00 偏移侥幸正确)。统一改用 formatUtcTime 按北京显示: - admins: last_login_at - audit-logs: created_at - devices: completed_at - dashboard/HomeStatsConfig: ops_stat_config.updated_at 均为 server_default now()/datetime.now(utc) 的 UTC 口径字段。 HomeStatsConfig 的「下个更新时刻」用 epoch ms + 强制 Asia/Shanghai,正确,不动。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: OuYingJun1024 <1034284404@qq.com> Reviewed-on: #10 Co-authored-by: ouzhou <ouzhou@wonderable.ai> Co-committed-by: ouzhou <ouzhou@wonderable.ai>
74 lines
2.3 KiB
TypeScript
74 lines
2.3 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import type { ColumnsType } from 'antd/es/table';
|
|
import { Button, Input, Space, Table, Tag } from 'antd';
|
|
import { formatUtcTime } from '@/lib/format';
|
|
import { usePagedList } from '@/lib/usePagedList';
|
|
import type { AuditLog } from '@/lib/types';
|
|
|
|
// 审计 created_at 为 UTC 口径(server_default now()),按北京显示(勿用 new Date().toLocaleString)
|
|
const dt = (v: string) => formatUtcTime(v, 'YYYY-MM-DD HH:mm:ss');
|
|
|
|
export default function AuditLogsPage() {
|
|
const [action, setAction] = useState('');
|
|
const [filters, setFilters] = useState<Record<string, unknown>>({});
|
|
const { items, total, page, pageSize, loading, onChange: onPageChange } =
|
|
usePagedList<AuditLog>('/admin/api/audit-logs', filters, 50);
|
|
|
|
const search = () => setFilters({ action: action || undefined });
|
|
|
|
const columns: ColumnsType<AuditLog> = [
|
|
{ title: '时间', dataIndex: 'created_at', render: dt, width: 180 },
|
|
{ title: '操作者', dataIndex: 'admin_username', width: 120 },
|
|
{ title: '动作', dataIndex: 'action', render: (a: string) => <Tag>{a}</Tag>, width: 170 },
|
|
{
|
|
title: '对象',
|
|
key: 'target',
|
|
width: 140,
|
|
render: (_: unknown, l: AuditLog) => `${l.target_type}${l.target_id ? '#' + l.target_id : ''}`,
|
|
},
|
|
{
|
|
title: '详情',
|
|
dataIndex: 'detail',
|
|
render: (d: Record<string, unknown> | null) =>
|
|
d ? <code style={{ fontSize: 12 }}>{JSON.stringify(d)}</code> : '-',
|
|
},
|
|
{ title: 'IP', dataIndex: 'ip', render: (v: string | null) => v || '-', width: 130 },
|
|
];
|
|
|
|
return (
|
|
<div>
|
|
<h2>审计日志</h2>
|
|
<Space style={{ marginBottom: 16 }}>
|
|
<Input
|
|
placeholder="动作(如 user.coins.grant)"
|
|
value={action}
|
|
onChange={(e) => setAction(e.target.value)}
|
|
onPressEnter={search}
|
|
allowClear
|
|
style={{ width: 240 }}
|
|
/>
|
|
<Button type="primary" onClick={search}>
|
|
查询
|
|
</Button>
|
|
</Space>
|
|
<Table
|
|
rowKey="id"
|
|
columns={columns}
|
|
dataSource={items}
|
|
loading={loading}
|
|
pagination={{
|
|
current: page,
|
|
pageSize,
|
|
total,
|
|
showSizeChanger: true,
|
|
showTotal: (t) => `共 ${t} 条`,
|
|
onChange: onPageChange,
|
|
}}
|
|
scroll={{ x: 1000 }}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|