254 lines
7.0 KiB
TypeScript
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>
|
|
)
|
|
}
|