重命名前端目录
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
import { ReloadOutlined, SearchOutlined } from '@ant-design/icons'
|
||||
import { Alert, Button, Card, DatePicker, Empty, Form, Input, Select, Space, Table, Typography } from 'antd'
|
||||
import type { TableColumnsType } from 'antd'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import dayjs from 'dayjs'
|
||||
import { useMemo } from 'react'
|
||||
import { useSearchParams } from 'react-router'
|
||||
|
||||
import PageHeader from '@/components/admin/PageHeader'
|
||||
import StatusTag from '@/components/admin/StatusTag'
|
||||
import { fetchAdminAuditLogs } from '@/services/admin'
|
||||
import type { AdminAuditLogItem } from '@/types/admin'
|
||||
import { hasAdminRole } from '@/utils/admin-auth'
|
||||
import { formatAuditAction, formatAuditTargetType } from '@/utils/admin-display'
|
||||
import { adminAuditActionOptions, adminAuditTargetTypeOptions } from '@/utils/admin-options'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
import { stringifyDisplayJson } from '@/utils/date-time'
|
||||
|
||||
type AuditFilterForm = {
|
||||
actorUsername?: string
|
||||
action?: string
|
||||
targetType?: string
|
||||
dateRange?: unknown
|
||||
}
|
||||
|
||||
export default function AdminAuditLogsPage() {
|
||||
const isAdmin = hasAdminRole('admin')
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const page = Number(searchParams.get('page') || 1) || 1
|
||||
const pageSize = Number(searchParams.get('pageSize') || 20) || 20
|
||||
const filters = {
|
||||
actorUsername: searchParams.get('actorUsername') || '',
|
||||
action: searchParams.get('action') || '',
|
||||
targetType: searchParams.get('targetType') || '',
|
||||
dateFrom: searchParams.get('dateFrom') || '',
|
||||
dateTo: searchParams.get('dateTo') || '',
|
||||
}
|
||||
const queryParams = useMemo(
|
||||
() => ({ page, pageSize, ...filters }),
|
||||
[
|
||||
filters.action,
|
||||
filters.actorUsername,
|
||||
filters.dateFrom,
|
||||
filters.dateTo,
|
||||
filters.targetType,
|
||||
page,
|
||||
pageSize,
|
||||
],
|
||||
)
|
||||
const query = useQuery({
|
||||
queryKey: ['admin-audit-logs', queryParams],
|
||||
enabled: isAdmin,
|
||||
queryFn: () => fetchAdminAuditLogs(queryParams),
|
||||
})
|
||||
const data = query.data?.data
|
||||
|
||||
const columns: TableColumnsType<AdminAuditLogItem> = [
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'createdAt',
|
||||
width: 180,
|
||||
render: (value) => formatAdminDateTime(value),
|
||||
},
|
||||
{
|
||||
title: '操作人',
|
||||
minWidth: 140,
|
||||
render: (_, row) => (
|
||||
<div className="cell-stack">
|
||||
<Typography.Text strong>{row.actorUsername || '-'}</Typography.Text>
|
||||
<StatusTag value={row.actorRole} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '动作',
|
||||
dataIndex: 'action',
|
||||
minWidth: 180,
|
||||
render: (value) => <Typography.Text strong>{formatAuditAction(value)}</Typography.Text>,
|
||||
},
|
||||
{
|
||||
title: '目标',
|
||||
minWidth: 180,
|
||||
render: (_, row) => (
|
||||
<Typography.Text strong>
|
||||
{formatAuditTargetType(row.targetType)} #{row.targetId || '-'}
|
||||
</Typography.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '摘要',
|
||||
minWidth: 260,
|
||||
render: (_, row) => <span className="payload-text">{formatPayload(row.payload)}</span>,
|
||||
},
|
||||
]
|
||||
|
||||
function applyFilters(values: AuditFilterForm) {
|
||||
const [dateFrom, dateTo] = Array.isArray(values.dateRange)
|
||||
? values.dateRange.map((item: { format?: (pattern: string) => string }) =>
|
||||
item?.format ? item.format('YYYY-MM-DD') : '',
|
||||
)
|
||||
: ['', '']
|
||||
|
||||
setSearchParams(
|
||||
cleanParams({
|
||||
actorUsername: values.actorUsername,
|
||||
action: values.action,
|
||||
targetType: values.targetType,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
page: 1,
|
||||
pageSize,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
setSearchParams(cleanParams({ page: 1, pageSize }))
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="page-stack">
|
||||
<PageHeader
|
||||
title="操作审计"
|
||||
description="集中查看高风险后台动作,便于排查谁在什么时间改了什么。"
|
||||
extra={<Typography.Text type="secondary">共 {data?.pagination.total || 0} 条审计记录</Typography.Text>}
|
||||
/>
|
||||
|
||||
{!isAdmin ? (
|
||||
<Alert type="warning" showIcon message="仅管理员可以访问操作审计" />
|
||||
) : (
|
||||
<>
|
||||
<Card title="筛选审计日志">
|
||||
<Form<AuditFilterForm>
|
||||
layout="inline"
|
||||
initialValues={{
|
||||
actorUsername: filters.actorUsername,
|
||||
action: filters.action,
|
||||
targetType: filters.targetType,
|
||||
dateRange:
|
||||
filters.dateFrom && filters.dateTo
|
||||
? [dayjs(filters.dateFrom), dayjs(filters.dateTo)]
|
||||
: undefined,
|
||||
}}
|
||||
onFinish={applyFilters}
|
||||
className="filter-form"
|
||||
>
|
||||
<Form.Item name="actorUsername">
|
||||
<Input allowClear placeholder="操作账号" />
|
||||
</Form.Item>
|
||||
<Form.Item name="action">
|
||||
<Select
|
||||
allowClear
|
||||
placeholder="动作"
|
||||
options={adminAuditActionOptions}
|
||||
style={{ minWidth: 180 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="targetType">
|
||||
<Select
|
||||
allowClear
|
||||
placeholder="目标类型"
|
||||
options={adminAuditTargetTypeOptions}
|
||||
style={{ minWidth: 150 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="dateRange">
|
||||
<DatePicker.RangePicker />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button type="primary" icon={<SearchOutlined />} htmlType="submit">
|
||||
查询
|
||||
</Button>
|
||||
<Button icon={<ReloadOutlined />} onClick={resetFilters}>
|
||||
重置
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
{query.error ? (
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
message={query.error instanceof Error ? query.error.message : '读取审计日志失败'}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Card>
|
||||
<Table<AdminAuditLogItem>
|
||||
rowKey="logId"
|
||||
loading={query.isLoading || query.isFetching}
|
||||
columns={columns}
|
||||
dataSource={data?.items || []}
|
||||
locale={{ emptyText: <Empty description="暂无审计记录" /> }}
|
||||
scroll={{ x: 980 }}
|
||||
pagination={{
|
||||
current: data?.pagination.page || page,
|
||||
pageSize: data?.pagination.pageSize || pageSize,
|
||||
total: data?.pagination.total || 0,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
onChange: (nextPage, nextPageSize) => {
|
||||
setSearchParams(
|
||||
cleanParams({ ...filters, page: nextPage, pageSize: nextPageSize }),
|
||||
)
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function formatPayload(payload: Record<string, unknown>) {
|
||||
const text = stringifyDisplayJson(payload, 0)
|
||||
return text.length > 140 ? `${text.slice(0, 140)}...` : text
|
||||
}
|
||||
|
||||
function cleanParams(params: Record<string, unknown>) {
|
||||
const next = new URLSearchParams()
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
const normalized = String(value ?? '').trim()
|
||||
if (normalized) {
|
||||
next.set(key, normalized)
|
||||
}
|
||||
})
|
||||
return next
|
||||
}
|
||||
Reference in New Issue
Block a user