149 lines
4.8 KiB
TypeScript
149 lines
4.8 KiB
TypeScript
import { useQuery } from '@tanstack/react-query'
|
|
import { Card, Segmented, Space, Tag, Typography } from 'antd'
|
|
import PercentTable from '@/components/common/PercentTable'
|
|
import type { TableColumnsType } from 'antd'
|
|
import { useState } from 'react'
|
|
|
|
import { AdminRangePicker } from '@/components/admin/AdminDatePicker'
|
|
import dayjs, { ADMIN_DATE_FORMAT } from '@/lib/dayjs'
|
|
import {
|
|
fetchAdminWorkerLeaderboard,
|
|
type AdminLeaderboardPeriod,
|
|
type AdminWorkerLeaderboardEntry,
|
|
} from '@/services/admin'
|
|
import { formatAdminDateTime } from '@/utils/admin-time'
|
|
import { formatMoney } from './shared'
|
|
|
|
const PERIOD_OPTIONS: { label: string; value: AdminLeaderboardPeriod | 'custom' }[] = [
|
|
{ label: '今日', value: 'daily' },
|
|
{ label: '本周', value: 'weekly' },
|
|
{ label: '本月', value: 'monthly' },
|
|
{ label: '全部', value: 'all' },
|
|
{ label: '自定义', value: 'custom' },
|
|
]
|
|
|
|
const WORKER_STATUS_LABELS: Record<string, string> = {
|
|
active: '已启用',
|
|
pending_review: '待处理',
|
|
rejected: '已冻结',
|
|
disabled: '已停用',
|
|
}
|
|
|
|
export default function LeaderboardPanel() {
|
|
const [period, setPeriod] = useState<AdminLeaderboardPeriod | 'custom'>('daily')
|
|
const [range, setRange] = useState<[string, string] | null>(null)
|
|
|
|
const leaderboardQuery = useQuery({
|
|
queryKey: ['admin-worker-leaderboard', period, range],
|
|
queryFn: () =>
|
|
fetchAdminWorkerLeaderboard(
|
|
period === 'custom' ? 'all' : period,
|
|
50,
|
|
period === 'custom' && range ? { from: range[0], to: range[1] } : undefined,
|
|
),
|
|
enabled: period !== 'custom' || Boolean(range),
|
|
})
|
|
|
|
const items = leaderboardQuery.data?.data.items || []
|
|
|
|
const columns: TableColumnsType<AdminWorkerLeaderboardEntry> = [
|
|
{
|
|
title: '排名',
|
|
dataIndex: 'rank',
|
|
width: 90,
|
|
render: (_, row) => (
|
|
<Typography.Text strong={row.rank <= 3} type={row.rank === 1 ? 'warning' : undefined}>
|
|
{row.rank <= 3 ? `🏅 ${row.rank}` : row.rank}
|
|
</Typography.Text>
|
|
),
|
|
},
|
|
{
|
|
title: '打手',
|
|
dataIndex: 'displayName',
|
|
minWidth: 200,
|
|
render: (_, row) => (
|
|
<div className="cell-stack">
|
|
<Typography.Text strong>{row.displayName || row.username}</Typography.Text>
|
|
<Typography.Text type="secondary">{row.username}</Typography.Text>
|
|
</div>
|
|
),
|
|
},
|
|
{ title: '等级', dataIndex: 'levelName', width: 120, render: (value) => value || '-' },
|
|
{
|
|
title: '状态',
|
|
dataIndex: 'workerStatus',
|
|
width: 100,
|
|
render: (value: string) => (
|
|
<Tag color={value === 'active' ? 'green' : value === 'disabled' ? 'red' : 'orange'}>
|
|
{WORKER_STATUS_LABELS[value] || value}
|
|
</Tag>
|
|
),
|
|
},
|
|
{
|
|
title: '完成单数',
|
|
dataIndex: 'acceptedOrderCount',
|
|
width: 110,
|
|
align: 'right',
|
|
sorter: (a, b) => a.acceptedOrderCount - b.acceptedOrderCount,
|
|
render: (value) => <Typography.Text strong>{value}</Typography.Text>,
|
|
},
|
|
{
|
|
title: '奖励总额',
|
|
dataIndex: 'rewardAmount',
|
|
width: 120,
|
|
align: 'right',
|
|
render: (value: number) => formatMoney(value),
|
|
},
|
|
{
|
|
title: '最近完成',
|
|
dataIndex: 'lastAcceptedAt',
|
|
width: 170,
|
|
render: (value: string | null) => formatAdminDateTime(value),
|
|
},
|
|
]
|
|
|
|
return (
|
|
<Card size="small" bordered={false}>
|
|
<Space direction="vertical" size={12} style={{ width: '100%', marginBottom: 12 }}>
|
|
<Space wrap>
|
|
<Segmented
|
|
value={period}
|
|
options={PERIOD_OPTIONS}
|
|
onChange={(value) => setPeriod(value as AdminLeaderboardPeriod | 'custom')}
|
|
/>
|
|
{period === 'custom' ? (
|
|
<AdminRangePicker
|
|
value={range ? [dayjs(range[0]), dayjs(range[1])] : null}
|
|
onChange={(dates) => {
|
|
const [start, end] = dates || []
|
|
setRange(
|
|
start && end
|
|
? [start.format(ADMIN_DATE_FORMAT), end.format(ADMIN_DATE_FORMAT)]
|
|
: null,
|
|
)
|
|
}}
|
|
disabledDate={(current) => current && current.isAfter(dayjs(), 'day')}
|
|
/>
|
|
) : null}
|
|
</Space>
|
|
<Typography.Text type="secondary">
|
|
按验收完成单数排名,同一订单的拼单份数只计入一单;奖励为所选时间段内验收奖励合计。
|
|
{period === 'custom'
|
|
? range
|
|
? ` 当前区间:${range[0]} 至 ${range[1]}(含起止日)。`
|
|
: ' 请选择统计区间。'
|
|
: ''}
|
|
</Typography.Text>
|
|
</Space>
|
|
<PercentTable<AdminWorkerLeaderboardEntry>
|
|
rowKey="workerId"
|
|
columns={columns}
|
|
dataSource={items}
|
|
loading={leaderboardQuery.isLoading}
|
|
pagination={false}
|
|
scroll={{ x: 820 }}
|
|
/>
|
|
</Card>
|
|
)
|
|
}
|