Files
Go-Web-Template/web-admin/src/features/logs/LoginLogPage.tsx
2026-04-28 20:00:05 +08:00

223 lines
6.1 KiB
TypeScript

import { useCallback, useEffect, useState } from 'react'
import { Button, Card, Form, Input, Modal, Select, Space, Table, Tag, Typography, message } from 'antd'
import type { ColumnsType } from 'antd/es/table'
import { loginLogApi } from '@/lib/api'
import { formatDate } from '@/lib/date'
import type { LoginLog } from '@/types/system'
type LoginLogSearch = {
username?: string
status?: boolean
}
export function LoginLogPage() {
const [searchForm] = Form.useForm<LoginLogSearch>()
const [rows, setRows] = useState<LoginLog[]>([])
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([])
const [loading, setLoading] = useState(false)
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(10)
const [total, setTotal] = useState(0)
const reloadRows = useCallback(async () => {
setLoading(true)
try {
const values = searchForm.getFieldsValue()
const response = await loginLogApi.getLoginLogList({
page,
pageSize,
username: values.username,
...(typeof values.status === 'boolean' ? { status: values.status } : {}),
})
setRows(response.data.list)
setTotal(response.data.total)
} finally {
setLoading(false)
}
}, [page, pageSize, searchForm])
useEffect(() => {
reloadRows()
}, [reloadRows])
const refreshAfterDelete = (removedCount: number) => {
setSelectedRowKeys([])
if (rows.length <= removedCount && page > 1) {
setPage((current) => current - 1)
return
}
reloadRows()
}
const runSearch = () => {
if (page !== 1) {
setPage(1)
return
}
reloadRows()
}
const deleteRow = (record: LoginLog) => {
Modal.confirm({
title: `删除登录日志 #${record.ID}`,
content: '删除后无法恢复。',
okText: '删除',
okButtonProps: { danger: true },
onOk: async () => {
await loginLogApi.deleteLoginLog(record.ID)
message.success('登录日志已删除')
refreshAfterDelete(1)
},
})
}
const deleteSelectedRows = () => {
if (!selectedRowKeys.length) {
return
}
Modal.confirm({
title: `批量删除 ${selectedRowKeys.length} 条登录日志`,
content: '删除后无法恢复。',
okText: '删除',
okButtonProps: { danger: true },
onOk: async () => {
await loginLogApi.deleteLoginLogByIds(selectedRowKeys.map((item) => Number(item)))
message.success('登录日志已删除')
refreshAfterDelete(selectedRowKeys.length)
},
})
}
const columns: ColumnsType<LoginLog> = [
{
title: 'ID',
dataIndex: 'ID',
width: 80,
},
{
title: '用户名',
dataIndex: 'username',
width: 160,
},
{
title: '登录 IP',
dataIndex: 'ip',
width: 160,
},
{
title: '状态',
width: 110,
render: (_, record) => <Tag color={record.status ? 'green' : 'red'}>{record.status ? '成功' : '失败'}</Tag>,
},
{
title: '详情',
render: (_, record) => record.errorMessage || (record.status ? '登录成功' : '-'),
},
{
title: '浏览器/设备',
dataIndex: 'agent',
ellipsis: true,
},
{
title: '登录时间',
width: 180,
render: (_, record) => formatDate(record.CreatedAt),
},
{
title: '操作',
width: 100,
fixed: 'right',
render: (_, record) => (
<Button danger type="link" onClick={() => deleteRow(record)}>
</Button>
),
},
]
return (
<div className="page-stack">
<Card className="glass-panel page-panel">
<div className="section-heading">
<div>
<Typography.Title level={2} style={{ marginBottom: 8 }}>
</Typography.Title>
<Typography.Paragraph className="text-muted" style={{ marginBottom: 0 }}>
</Typography.Paragraph>
</div>
</div>
</Card>
<Card className="glass-panel page-panel">
<Space direction="vertical" size={16} style={{ width: '100%' }}>
<Form
form={searchForm}
layout="inline"
onFinish={runSearch}
>
<Form.Item name="username" label="用户名">
<Input allowClear placeholder="输入用户名" style={{ width: 220 }} />
</Form.Item>
<Form.Item name="status" label="状态">
<Select
allowClear
placeholder="全部状态"
style={{ width: 160 }}
options={[
{ label: '成功', value: true },
{ label: '失败', value: false },
]}
/>
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" htmlType="submit">
</Button>
<Button
onClick={() => {
searchForm.resetFields()
runSearch()
}}
>
</Button>
</Space>
</Form.Item>
</Form>
<Space>
<Button danger disabled={!selectedRowKeys.length} onClick={deleteSelectedRows}>
</Button>
</Space>
<Table
rowKey="ID"
loading={loading}
columns={columns}
dataSource={rows}
rowSelection={{
selectedRowKeys,
onChange: setSelectedRowKeys,
}}
scroll={{ x: 1100 }}
pagination={{
current: page,
pageSize,
total,
showSizeChanger: true,
onChange: (nextPage, nextPageSize) => {
setPage(nextPage)
setPageSize(nextPageSize)
},
}}
/>
</Space>
</Card>
</div>
)
}