Files
erp/apps/web/src/pages/settings/AuditLogViewer.tsx
iven 3b41e73f82 fix: resolve E2E audit findings and add Phase C frontend pages
- Fix audit_log handler multi-tenant bug: use Extension<TenantContext>
  instead of hardcoded default_tenant_id
- Fix sendMessage route mismatch: frontend /messages/send → /messages
- Add POST /users/{id}/roles backend route for role assignment
- Add task.completed event payload: started_by + instance_id for
  notification delivery
- Add audit log viewer frontend page (AuditLogViewer.tsx)
- Add language management frontend page (LanguageManager.tsx)
- Add api/auditLogs.ts and api/languages.ts modules
2026-04-12 15:57:33 +08:00

157 lines
4.1 KiB
TypeScript

import { useState, useEffect, useCallback } from 'react';
import { Table, Select, Input, Space, Card, Typography, Tag, message } from 'antd';
import type { ColumnsType, TablePaginationConfig } from 'antd/es/table';
import { listAuditLogs, type AuditLogItem, type AuditLogQuery } from '../../api/auditLogs';
const RESOURCE_TYPE_OPTIONS = [
{ value: 'user', label: '用户' },
{ value: 'role', label: '角色' },
{ value: 'organization', label: '组织' },
{ value: 'department', label: '部门' },
{ value: 'position', label: '岗位' },
{ value: 'process_instance', label: '流程实例' },
{ value: 'dictionary', label: '字典' },
{ value: 'menu', label: '菜单' },
{ value: 'setting', label: '设置' },
{ value: 'numbering_rule', label: '编号规则' },
];
const ACTION_COLOR_MAP: Record<string, string> = {
create: 'green',
update: 'blue',
delete: 'red',
};
function formatDateTime(value: string): string {
return new Date(value).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
}
export default function AuditLogViewer() {
const [logs, setLogs] = useState<AuditLogItem[]>([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const [query, setQuery] = useState<AuditLogQuery>({ page: 1, page_size: 20 });
const fetchLogs = useCallback(async (params: AuditLogQuery) => {
setLoading(true);
try {
const result = await listAuditLogs(params);
setLogs(result.data);
setTotal(result.total);
} catch {
message.error('加载审计日志失败');
}
setLoading(false);
}, []);
useEffect(() => {
fetchLogs(query);
}, [query, fetchLogs]);
const handleFilterChange = (field: keyof AuditLogQuery, value: string | undefined) => {
setQuery((prev) => ({
...prev,
[field]: value || undefined,
page: 1,
}));
};
const handleTableChange = (pagination: TablePaginationConfig) => {
setQuery((prev) => ({
...prev,
page: pagination.current,
page_size: pagination.pageSize,
}));
};
const columns: ColumnsType<AuditLogItem> = [
{
title: '操作',
dataIndex: 'action',
key: 'action',
width: 120,
render: (action: string) => (
<Tag color={ACTION_COLOR_MAP[action] ?? 'default'}>{action}</Tag>
),
},
{
title: '资源类型',
dataIndex: 'resource_type',
key: 'resource_type',
width: 140,
},
{
title: '资源 ID',
dataIndex: 'resource_id',
key: 'resource_id',
width: 200,
ellipsis: true,
},
{
title: '操作用户',
dataIndex: 'user_id',
key: 'user_id',
width: 200,
ellipsis: true,
},
{
title: '时间',
dataIndex: 'created_at',
key: 'created_at',
width: 200,
render: (value: string) => formatDateTime(value),
},
];
return (
<div>
<Typography.Title level={5} style={{ marginBottom: 16 }}>
</Typography.Title>
<Card size="small" style={{ marginBottom: 16 }}>
<Space wrap>
<Select
allowClear
placeholder="资源类型"
style={{ width: 160 }}
options={RESOURCE_TYPE_OPTIONS}
value={query.resource_type}
onChange={(value) => handleFilterChange('resource_type', value)}
/>
<Input
allowClear
placeholder="操作用户 ID"
style={{ width: 240 }}
value={query.user_id ?? ''}
onChange={(e) => handleFilterChange('user_id', e.target.value)}
/>
</Space>
</Card>
<Table
rowKey="id"
columns={columns}
dataSource={logs}
loading={loading}
onChange={handleTableChange}
pagination={{
current: query.page,
pageSize: query.page_size,
total,
showSizeChanger: true,
showTotal: (t) => `${t}`,
}}
scroll={{ x: 900 }}
/>
</div>
);
}