新增后台接单排行榜支持周期与自定义区间筛选
This commit is contained in:
@@ -277,3 +277,145 @@ function buildWorkerAcceptedOrdersCountSql() {
|
|||||||
) AS completed_orders
|
) AS completed_orders
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AdminLeaderboardPeriod = 'all' | 'daily' | 'weekly' | 'monthly'
|
||||||
|
|
||||||
|
/** 后台排行榜:预置周期按上海时区自然日/周/月计算;from/to 为自定义区间(YYYY-MM-DD,含起止日)。 */
|
||||||
|
export async function listAdminWorkerLeaderboard(input: {
|
||||||
|
period: AdminLeaderboardPeriod
|
||||||
|
limit: number
|
||||||
|
fromDate?: string | null
|
||||||
|
toDate?: string | null
|
||||||
|
}): Promise<
|
||||||
|
Array<{
|
||||||
|
rank: number
|
||||||
|
workerId: number
|
||||||
|
username: string
|
||||||
|
displayName: string
|
||||||
|
levelName: string
|
||||||
|
workerStatus: string
|
||||||
|
acceptedOrderCount: number
|
||||||
|
rewardAmount: number
|
||||||
|
lastAcceptedAt: string | null
|
||||||
|
}>
|
||||||
|
> {
|
||||||
|
const limit = Math.min(100, Math.max(1, Math.floor(Number(input.limit || 50))))
|
||||||
|
const periodUnit =
|
||||||
|
input.period === 'daily'
|
||||||
|
? 'day'
|
||||||
|
: input.period === 'weekly'
|
||||||
|
? 'week'
|
||||||
|
: input.period === 'monthly'
|
||||||
|
? 'month'
|
||||||
|
: ''
|
||||||
|
const result = await query<{
|
||||||
|
rank: number
|
||||||
|
worker_id: number
|
||||||
|
username: string
|
||||||
|
display_name: string
|
||||||
|
level_name: string | null
|
||||||
|
worker_status: string
|
||||||
|
accepted_order_count: number
|
||||||
|
reward_amount: string | number
|
||||||
|
last_accepted_at: Date | string | null
|
||||||
|
}>(
|
||||||
|
`
|
||||||
|
WITH leaderboard_window AS (
|
||||||
|
SELECT
|
||||||
|
CASE
|
||||||
|
WHEN $1::text <> '' THEN date_trunc($1::text, CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Shanghai')
|
||||||
|
AT TIME ZONE 'Asia/Shanghai'
|
||||||
|
WHEN $2::date IS NOT NULL THEN $2::date::timestamp AT TIME ZONE 'Asia/Shanghai'
|
||||||
|
ELSE NULL
|
||||||
|
END AS starts_at,
|
||||||
|
CASE
|
||||||
|
WHEN $1::text = 'day' THEN (date_trunc('day', CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Shanghai')
|
||||||
|
AT TIME ZONE 'Asia/Shanghai') + INTERVAL '1 day'
|
||||||
|
WHEN $1::text = 'week' THEN (date_trunc('week', CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Shanghai')
|
||||||
|
AT TIME ZONE 'Asia/Shanghai') + INTERVAL '7 days'
|
||||||
|
WHEN $1::text = 'month' THEN (date_trunc('month', CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Shanghai')
|
||||||
|
AT TIME ZONE 'Asia/Shanghai') + INTERVAL '1 month'
|
||||||
|
WHEN $3::date IS NOT NULL THEN ($3::date + 1)::timestamp AT TIME ZONE 'Asia/Shanghai'
|
||||||
|
ELSE NULL
|
||||||
|
END AS ends_at
|
||||||
|
),
|
||||||
|
accepted_totals AS (
|
||||||
|
SELECT
|
||||||
|
worker_id,
|
||||||
|
COUNT(DISTINCT order_key)::int AS accepted_order_count,
|
||||||
|
SUM(reward_amount)::bigint AS reward_amount,
|
||||||
|
MAX(last_accepted_at) AS last_accepted_at
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
assigned_worker_id AS worker_id,
|
||||||
|
COALESCE(
|
||||||
|
'order:' || NULLIF(order_id::text, ''),
|
||||||
|
'platform_order:' || NULLIF(BTRIM(platform_order_id), ''),
|
||||||
|
'work_order:' || id::text
|
||||||
|
) AS order_key,
|
||||||
|
reward_amount,
|
||||||
|
MAX(accepted_at) AS last_accepted_at
|
||||||
|
FROM work_orders
|
||||||
|
CROSS JOIN leaderboard_window
|
||||||
|
WHERE assigned_worker_id IS NOT NULL
|
||||||
|
AND status = 'accepted'
|
||||||
|
AND (starts_at IS NULL OR (accepted_at >= starts_at AND accepted_at < ends_at))
|
||||||
|
GROUP BY assigned_worker_id, order_key, reward_amount
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
wos.worker_id,
|
||||||
|
COALESCE(
|
||||||
|
'order:' || NULLIF(wo.order_id::text, ''),
|
||||||
|
'platform_order:' || NULLIF(BTRIM(wo.platform_order_id), ''),
|
||||||
|
'work_order:' || wo.id::text
|
||||||
|
) AS order_key,
|
||||||
|
wos.share_reward AS reward_amount,
|
||||||
|
MAX(wos.accepted_at) AS last_accepted_at
|
||||||
|
FROM work_order_shares wos
|
||||||
|
INNER JOIN work_orders wo ON wo.id = wos.work_order_id
|
||||||
|
CROSS JOIN leaderboard_window
|
||||||
|
WHERE wos.status = 'accepted'
|
||||||
|
AND (starts_at IS NULL OR (wos.accepted_at >= starts_at AND wos.accepted_at < ends_at))
|
||||||
|
GROUP BY wos.worker_id, order_key, wos.share_reward
|
||||||
|
) AS accepted_records
|
||||||
|
GROUP BY worker_id
|
||||||
|
),
|
||||||
|
ranked_workers AS (
|
||||||
|
SELECT
|
||||||
|
ROW_NUMBER() OVER (
|
||||||
|
ORDER BY totals.accepted_order_count DESC, totals.last_accepted_at DESC NULLS LAST, wu.id ASC
|
||||||
|
)::int AS rank,
|
||||||
|
wu.id AS worker_id,
|
||||||
|
wu.username,
|
||||||
|
wu.display_name,
|
||||||
|
wu.status AS worker_status,
|
||||||
|
wl.name AS level_name,
|
||||||
|
totals.accepted_order_count,
|
||||||
|
totals.reward_amount,
|
||||||
|
totals.last_accepted_at
|
||||||
|
FROM worker_users wu
|
||||||
|
INNER JOIN accepted_totals totals ON totals.worker_id = wu.id
|
||||||
|
LEFT JOIN worker_levels wl ON wl.id = wu.level_id
|
||||||
|
)
|
||||||
|
SELECT rank, worker_id, username, display_name, level_name, worker_status,
|
||||||
|
accepted_order_count, reward_amount, last_accepted_at
|
||||||
|
FROM ranked_workers
|
||||||
|
ORDER BY rank ASC
|
||||||
|
LIMIT $4
|
||||||
|
`,
|
||||||
|
[periodUnit, input.fromDate || null, input.toDate || null, limit],
|
||||||
|
)
|
||||||
|
return result.rows.map((row) => ({
|
||||||
|
rank: Number(row.rank || 0),
|
||||||
|
workerId: Number(row.worker_id || 0),
|
||||||
|
username: String(row.username || '').trim(),
|
||||||
|
displayName: String(row.display_name || '').trim(),
|
||||||
|
levelName: String(row.level_name || '').trim(),
|
||||||
|
workerStatus: String(row.worker_status || ''),
|
||||||
|
acceptedOrderCount: Number(row.accepted_order_count || 0),
|
||||||
|
rewardAmount: Number(row.reward_amount || 0),
|
||||||
|
lastAcceptedAt: row.last_accepted_at ? new Date(row.last_accepted_at).toISOString() : null,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import adminWorkerPlatformOrdersRoutes from './worker-platform/orders.js'
|
|||||||
import adminWorkerPlatformReviewRoutes from './worker-platform/review.js'
|
import adminWorkerPlatformReviewRoutes from './worker-platform/review.js'
|
||||||
import adminWorkerPlatformAcceptanceRoutes from './worker-platform/acceptance.js'
|
import adminWorkerPlatformAcceptanceRoutes from './worker-platform/acceptance.js'
|
||||||
import adminWorkerPlatformAfterSalesRoutes from './worker-platform/after-sales.js'
|
import adminWorkerPlatformAfterSalesRoutes from './worker-platform/after-sales.js'
|
||||||
|
import adminWorkerPlatformLeaderboardRoutes from './worker-platform/leaderboard.js'
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
||||||
@@ -17,5 +18,6 @@ router.use(adminWorkerPlatformOrdersRoutes)
|
|||||||
router.use(adminWorkerPlatformReviewRoutes)
|
router.use(adminWorkerPlatformReviewRoutes)
|
||||||
router.use(adminWorkerPlatformAfterSalesRoutes)
|
router.use(adminWorkerPlatformAfterSalesRoutes)
|
||||||
router.use(adminWorkerPlatformAcceptanceRoutes)
|
router.use(adminWorkerPlatformAcceptanceRoutes)
|
||||||
|
router.use(adminWorkerPlatformLeaderboardRoutes)
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { Router } from 'express'
|
||||||
|
|
||||||
|
import { getAdminWorkerLeaderboard } from '../../../services/worker-platform/index.js'
|
||||||
|
import { createJsonHandler, requireAdminRoles } from '../session.js'
|
||||||
|
|
||||||
|
const router = Router()
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/worker-platform/leaderboard',
|
||||||
|
requireAdminRoles(['admin', 'operator', 'support']),
|
||||||
|
createJsonHandler((req) => getAdminWorkerLeaderboard(req.query), {
|
||||||
|
successMessage: 'ok',
|
||||||
|
errorMessage: '读取接单排行榜失败',
|
||||||
|
scope: '[admin/worker-platform/leaderboard]',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export default router
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import {
|
||||||
|
listAdminWorkerLeaderboard,
|
||||||
|
type AdminLeaderboardPeriod,
|
||||||
|
} from '../../repositories/worker-platform/index.js'
|
||||||
|
import type { JsonObject } from '../../types/json.js'
|
||||||
|
|
||||||
|
const PERIODS: AdminLeaderboardPeriod[] = ['all', 'daily', 'weekly', 'monthly']
|
||||||
|
const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/
|
||||||
|
const MAX_CUSTOM_RANGE_DAYS = 366
|
||||||
|
|
||||||
|
function normalizeDate(value: unknown): string | null {
|
||||||
|
const text = String(value || '').trim()
|
||||||
|
return DATE_PATTERN.test(text) ? text : null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 后台接单排行榜读取:预置周期或自定义日期区间(含起止日)+ 完成单数与奖励汇总。 */
|
||||||
|
export async function getAdminWorkerLeaderboard(query: JsonObject = {}) {
|
||||||
|
const requested = String(query.period || 'all').trim() as AdminLeaderboardPeriod
|
||||||
|
const period = PERIODS.includes(requested) ? requested : 'all'
|
||||||
|
const limit = Number(query.limit || 50)
|
||||||
|
const fromDate = normalizeDate(query.from)
|
||||||
|
const toDate = normalizeDate(query.to)
|
||||||
|
if ((fromDate || toDate) && !(fromDate && toDate)) {
|
||||||
|
throw new Error('自定义区间需要同时提供开始与结束日期')
|
||||||
|
}
|
||||||
|
if (fromDate && toDate) {
|
||||||
|
const fromTime = new Date(`${fromDate}T00:00:00Z`).getTime()
|
||||||
|
const toTime = new Date(`${toDate}T00:00:00Z`).getTime()
|
||||||
|
if (fromTime > toTime) {
|
||||||
|
throw new Error('自定义区间的开始日期不能晚于结束日期')
|
||||||
|
}
|
||||||
|
if ((toTime - fromTime) / 86400000 > MAX_CUSTOM_RANGE_DAYS) {
|
||||||
|
throw new Error('自定义区间最长不能超过一年')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const items = await listAdminWorkerLeaderboard({ period, limit, fromDate, toDate })
|
||||||
|
return { period: fromDate && toDate ? 'custom' : period, from: fromDate, to: toDate, items }
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ export * from './after-sales-service.js'
|
|||||||
export * from './admin-service.js'
|
export * from './admin-service.js'
|
||||||
export * from './admin-worker-finance-service.js'
|
export * from './admin-worker-finance-service.js'
|
||||||
export * from './admin-worker-catalog-service.js'
|
export * from './admin-worker-catalog-service.js'
|
||||||
|
export * from './admin-worker-leaderboard-service.js'
|
||||||
export * from './admin-kuaishou-match-service.js'
|
export * from './admin-kuaishou-match-service.js'
|
||||||
export * from './admin-worker-config-service.js'
|
export * from './admin-worker-config-service.js'
|
||||||
export * from './admin-worker-user-service.js'
|
export * from './admin-worker-user-service.js'
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import CategoriesPanel from './panels/CategoriesPanel'
|
|||||||
import FeedbacksPanel from './panels/FeedbacksPanel'
|
import FeedbacksPanel from './panels/FeedbacksPanel'
|
||||||
import FinancePanel from './panels/FinancePanel'
|
import FinancePanel from './panels/FinancePanel'
|
||||||
import HallConfigPanel from './panels/HallConfigPanel'
|
import HallConfigPanel from './panels/HallConfigPanel'
|
||||||
|
import LeaderboardPanel from './panels/LeaderboardPanel'
|
||||||
import LevelsPanel from './panels/LevelsPanel'
|
import LevelsPanel from './panels/LevelsPanel'
|
||||||
import NotificationsPanel from './panels/NotificationsPanel'
|
import NotificationsPanel from './panels/NotificationsPanel'
|
||||||
import ProductMatchPanel from './panels/ProductMatchPanel'
|
import ProductMatchPanel from './panels/ProductMatchPanel'
|
||||||
@@ -94,6 +95,7 @@ export default function AdminWorkerPlatformPage() {
|
|||||||
{ key: 'product-match', label: '匹配诊断', children: <ProductMatchPanel /> },
|
{ key: 'product-match', label: '匹配诊断', children: <ProductMatchPanel /> },
|
||||||
{ key: 'categories', label: '分类', children: <CategoriesPanel /> },
|
{ key: 'categories', label: '分类', children: <CategoriesPanel /> },
|
||||||
{ key: 'workers', label: '打手', children: <WorkersPanel /> },
|
{ key: 'workers', label: '打手', children: <WorkersPanel /> },
|
||||||
|
{ key: 'leaderboard', label: '排行榜', children: <LeaderboardPanel /> },
|
||||||
{ key: 'finance', label: '资金', children: <FinancePanel /> },
|
{ key: 'finance', label: '资金', children: <FinancePanel /> },
|
||||||
{ key: 'levels', label: '等级权限', children: <LevelsPanel /> },
|
{ key: 'levels', label: '等级权限', children: <LevelsPanel /> },
|
||||||
{ key: 'hall', label: '大厅配置', children: <HallConfigPanel /> },
|
{ key: 'hall', label: '大厅配置', children: <HallConfigPanel /> },
|
||||||
@@ -112,6 +114,7 @@ const ACTIVE_TAB_STORAGE_KEY = 'admin-worker-platform-tab'
|
|||||||
function loadActiveTabPreference(): string {
|
function loadActiveTabPreference(): string {
|
||||||
const saved = localStorage.getItem(ACTIVE_TAB_STORAGE_KEY)
|
const saved = localStorage.getItem(ACTIVE_TAB_STORAGE_KEY)
|
||||||
return saved === 'orders' ||
|
return saved === 'orders' ||
|
||||||
|
saved === 'leaderboard' ||
|
||||||
saved === 'cancel-requests' ||
|
saved === 'cancel-requests' ||
|
||||||
saved === 'feedbacks' ||
|
saved === 'feedbacks' ||
|
||||||
saved === 'after-sales' ||
|
saved === 'after-sales' ||
|
||||||
@@ -135,6 +138,7 @@ function loadActiveTab(tab: string | null): string {
|
|||||||
function isWorkerPlatformTab(tab: string | null): tab is string {
|
function isWorkerPlatformTab(tab: string | null): tab is string {
|
||||||
return [
|
return [
|
||||||
'orders',
|
'orders',
|
||||||
|
'leaderboard',
|
||||||
'cancel-requests',
|
'cancel-requests',
|
||||||
'feedbacks',
|
'feedbacks',
|
||||||
'after-sales',
|
'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
|
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() {
|
export function fetchAdminWorkerPlatformSummary() {
|
||||||
return apiGet<{
|
return apiGet<{
|
||||||
pendingWorkerCount: number
|
pendingWorkerCount: number
|
||||||
|
|||||||
Reference in New Issue
Block a user