Files
order_site/apps/frontend/src/pages/admin/AdminDashboardPage.tsx
T
yml2213 c665ef5851 优化登录记录角色展示
将角色从账号列拆出为独立列,避免 Tag 叠在用户名下显示异常。
2026-07-10 21:57:54 +08:00

254 lines
7.0 KiB
TypeScript

import { ReloadOutlined, SearchOutlined } from '@ant-design/icons'
import {
Alert,
Button,
Card,
Col,
Form,
Input,
Row,
Skeleton,
Space,
Statistic,
Table,
Tag,
Typography,
} from 'antd'
import type { TableColumnsType } from 'antd'
import { useQuery } from '@tanstack/react-query'
import { useMemo, useState } from 'react'
import { AdminRangePicker } from '@/components/admin/AdminDatePicker'
import PageHeader from '@/components/admin/PageHeader'
import StatusTag from '@/components/admin/StatusTag'
import dayjs from '@/lib/dayjs'
import { fetchAdminDashboardSummary, fetchAdminLoginLogs } from '@/services/admin'
import type { AdminLoginLogItem } from '@/types/admin'
import {
ADMIN_DEFAULT_PAGE_SIZE,
buildAdminTablePagination,
} from '@/utils/admin-pagination'
import { formatAdminDateTime } from '@/utils/admin-time'
const cards = [
{ key: 'todayOrders', label: '今日订单' },
{ key: 'paidPendingClaim', label: '待领取' },
{ key: 'claimingTasks', label: '领取中' },
{ key: 'redeemedToday', label: '今日成功' },
{ key: 'abnormalTasks', label: '异常任务' },
] as const
type LoginFilterForm = {
username?: string
dateRange?: unknown
}
export default function AdminDashboardPage() {
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(ADMIN_DEFAULT_PAGE_SIZE)
const [filters, setFilters] = useState({
username: '',
dateFrom: '',
dateTo: '',
})
const summaryQuery = useQuery({
queryKey: ['admin-dashboard-summary'],
queryFn: () => fetchAdminDashboardSummary(),
})
const summary = summaryQuery.data?.data
const loginQueryParams = useMemo(
() => ({
page,
pageSize,
username: filters.username || undefined,
dateFrom: filters.dateFrom || undefined,
dateTo: filters.dateTo || undefined,
}),
[filters.dateFrom, filters.dateTo, filters.username, page, pageSize],
)
const loginQuery = useQuery({
queryKey: ['admin-login-logs', loginQueryParams],
queryFn: () => fetchAdminLoginLogs(loginQueryParams),
})
const loginData = loginQuery.data?.data
const columns: TableColumnsType<AdminLoginLogItem> = [
{
title: '登录时间',
dataIndex: 'createdAt',
width: 180,
render: (value) => formatAdminDateTime(value),
},
{
title: '账号',
dataIndex: 'username',
minWidth: 120,
render: (value) => <Typography.Text strong>{value || '-'}</Typography.Text>,
},
{
title: '角色',
dataIndex: 'role',
width: 110,
render: (value) => <StatusTag value={value} kind="role" />,
},
{
title: '结果',
width: 90,
render: (_, row) =>
row.success ? (
<Tag color="green">成功</Tag>
) : (
<Tag color="red">失败</Tag>
),
},
{
title: '登录地点',
dataIndex: 'location',
minWidth: 140,
render: (value) => value || '-',
},
{
title: '登录 IP',
dataIndex: 'ip',
width: 150,
render: (value) => value || '-',
},
{
title: '设备信息',
dataIndex: 'userAgent',
minWidth: 280,
render: (value) => (
<Typography.Text ellipsis={{ tooltip: value || '-' }} style={{ maxWidth: 420 }}>
{value || '-'}
</Typography.Text>
),
},
]
function applyLoginFilters(values: LoginFilterForm) {
const range = Array.isArray(values.dateRange) ? values.dateRange : []
const dateFrom =
range[0] && dayjs(range[0]).isValid() ? dayjs(range[0]).format('YYYY-MM-DD') : ''
const dateTo =
range[1] && dayjs(range[1]).isValid() ? dayjs(range[1]).format('YYYY-MM-DD') : ''
setPage(1)
setFilters({
username: String(values.username || '').trim(),
dateFrom,
dateTo,
})
}
return (
<section className="page-stack">
<PageHeader title="系统概览" description="快速查看订单和交付链路的运行状态。" />
{summaryQuery.error ? (
<Alert
type="error"
showIcon
message={
summaryQuery.error instanceof Error ? summaryQuery.error.message : '读取概览失败'
}
/>
) : null}
{summaryQuery.isLoading ? (
<Card>
<Skeleton active paragraph={{ rows: 8 }} />
</Card>
) : (
<Row gutter={[16, 16]}>
{cards.map((card) => (
<Col xs={24} md={12} xl={8} key={card.key}>
<Card>
<Statistic title={card.label} value={summary?.[card.key] ?? 0} />
</Card>
</Col>
))}
</Row>
)}
<Card
title="登录记录"
extra={
<Space wrap>
<Typography.Text type="secondary">全部后台账号的登录信息</Typography.Text>
<Button
icon={<ReloadOutlined />}
loading={loginQuery.isFetching}
onClick={() => void loginQuery.refetch()}
>
刷新
</Button>
</Space>
}
>
<Form<LoginFilterForm>
layout="inline"
className="filter-form"
onFinish={applyLoginFilters}
initialValues={{ username: filters.username }}
style={{ marginBottom: 16 }}
>
<Form.Item name="username" label="账号">
<Input allowClear placeholder="用户名" style={{ width: 180 }} />
</Form.Item>
<Form.Item name="dateRange" label="登录时段">
<AdminRangePicker />
</Form.Item>
<Form.Item>
<Space>
<Button
onClick={() => {
setPage(1)
setFilters({ username: '', dateFrom: '', dateTo: '' })
}}
>
重置
</Button>
<Button type="primary" htmlType="submit" icon={<SearchOutlined />}>
查询
</Button>
</Space>
</Form.Item>
</Form>
{loginQuery.error ? (
<Alert
type="error"
showIcon
style={{ marginBottom: 16 }}
message={
loginQuery.error instanceof Error
? loginQuery.error.message
: '读取登录记录失败'
}
/>
) : null}
<Table<AdminLoginLogItem>
rowKey="logId"
loading={loginQuery.isLoading || loginQuery.isFetching}
columns={columns}
dataSource={loginData?.items || []}
scroll={{ x: 1100 }}
pagination={buildAdminTablePagination({
current: loginData?.pagination.page || page,
pageSize: loginData?.pagination.pageSize || pageSize,
total: loginData?.pagination.total || 0,
onChange: (nextPage, nextPageSize) => {
setPage(nextPage)
setPageSize(nextPageSize)
},
})}
locale={{ emptyText: '暂无登录记录(新登录成功后会自动写入)' }}
/>
</Card>
</section>
)
}