新增后台接单排行榜支持周期与自定义区间筛选

This commit is contained in:
yml2213
2026-08-22 09:48:09 +08:00
parent ed81122047
commit f1e219d1ef
8 changed files with 383 additions and 0 deletions
@@ -14,6 +14,7 @@ import CategoriesPanel from './panels/CategoriesPanel'
import FeedbacksPanel from './panels/FeedbacksPanel'
import FinancePanel from './panels/FinancePanel'
import HallConfigPanel from './panels/HallConfigPanel'
import LeaderboardPanel from './panels/LeaderboardPanel'
import LevelsPanel from './panels/LevelsPanel'
import NotificationsPanel from './panels/NotificationsPanel'
import ProductMatchPanel from './panels/ProductMatchPanel'
@@ -94,6 +95,7 @@ export default function AdminWorkerPlatformPage() {
{ key: 'product-match', label: '匹配诊断', children: <ProductMatchPanel /> },
{ key: 'categories', label: '分类', children: <CategoriesPanel /> },
{ key: 'workers', label: '打手', children: <WorkersPanel /> },
{ key: 'leaderboard', label: '排行榜', children: <LeaderboardPanel /> },
{ key: 'finance', label: '资金', children: <FinancePanel /> },
{ key: 'levels', label: '等级权限', children: <LevelsPanel /> },
{ key: 'hall', label: '大厅配置', children: <HallConfigPanel /> },
@@ -112,6 +114,7 @@ const ACTIVE_TAB_STORAGE_KEY = 'admin-worker-platform-tab'
function loadActiveTabPreference(): string {
const saved = localStorage.getItem(ACTIVE_TAB_STORAGE_KEY)
return saved === 'orders' ||
saved === 'leaderboard' ||
saved === 'cancel-requests' ||
saved === 'feedbacks' ||
saved === 'after-sales' ||
@@ -135,6 +138,7 @@ function loadActiveTab(tab: string | null): string {
function isWorkerPlatformTab(tab: string | null): tab is string {
return [
'orders',
'leaderboard',
'cancel-requests',
'feedbacks',
'after-sales',
@@ -0,0 +1,147 @@
import { useQuery } from '@tanstack/react-query'
import { Card, Segmented, Space, Table, Tag, Typography } from 'antd'
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>
<Table<AdminWorkerLeaderboardEntry>
rowKey="workerId"
columns={columns}
dataSource={items}
loading={leaderboardQuery.isLoading}
pagination={false}
scroll={{ x: 820 }}
/>
</Card>
)
}
@@ -36,6 +36,37 @@ type WorkOrderVoucherConsumeResult = {
errorMessage: string
}
export type AdminLeaderboardPeriod = 'all' | 'daily' | 'weekly' | 'monthly'
export type AdminWorkerLeaderboardEntry = {
rank: number
workerId: number
username: string
displayName: string
levelName: string
workerStatus: string
acceptedOrderCount: number
rewardAmount: number
lastAcceptedAt: string | null
}
export function fetchAdminWorkerLeaderboard(
period: AdminLeaderboardPeriod,
limit = 50,
range?: { from: string; to: string },
) {
return apiGet<{
period: AdminLeaderboardPeriod | 'custom'
from: string | null
to: string | null
items: AdminWorkerLeaderboardEntry[]
}>('/api/v1/admin/worker-platform/leaderboard', {
period,
limit,
...(range ? { from: range.from, to: range.to } : {}),
})
}
export function fetchAdminWorkerPlatformSummary() {
return apiGet<{
pendingWorkerCount: number