'use client'; import { useCallback, useEffect, useState } from 'react'; import { DatePicker, Descriptions, Radio, Space, Table, Tag, Typography, message, } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import dayjs, { type Dayjs } from 'dayjs'; import type { CursorPage, UserCoinRecord, UserRewardStats, WithdrawUserSnapshot } from '@/lib/types'; import { api, errMsg } from '@/lib/api'; const { Text } = Typography; const { RangePicker } = DatePicker; const yuan = (c: number) => `¥${(c / 100).toFixed(2)}`; // 后端时间为 UTC(无时区串补 Z),按北京时区展示(与提现列表口径一致) const apiTime = (v: string) => { const hasTz = /(?:Z|[+-]\d{2}:?\d{2})$/i.test(v); return dayjs(hasTz ? v : `${v}Z`); }; const dt = (v: string) => apiTime(v).format('YYYY-MM-DD HH:mm'); const SOURCE_COLOR: Record = { reward_video: 'blue', signin_boost: 'cyan', feed: 'purple', signin: 'green', }; const PAGE_SIZE = 10; // 金币记录每页条数 interface Props { userId: number; user: WithdrawUserSnapshot | null; // 统计区字段集:withdraw(默认,提现详情:含提现细分) / ad(广告收益详情:只看看广告观看统计,对齐参考图 6 项) statsVariant?: 'withdraw' | 'ad'; } /** 用户看广告/提现详情:基本信息 + 互斥时间筛选 + 统计区(按 statsVariant 取字段集)+ 金币发放记录表。 */ export default function UserRewardPanel({ userId, user, statsVariant = 'withdraw' }: Props) { const [mode, setMode] = useState<'all' | 'range'>('all'); // all=注册至今 / range=自定义区间 const [range, setRange] = useState<[Dayjs, Dayjs] | null>(null); const [stats, setStats] = useState(null); const [statsLoading, setStatsLoading] = useState(false); const [records, setRecords] = useState([]); const [page, setPage] = useState(1); const [total, setTotal] = useState(0); const [recordsLoading, setRecordsLoading] = useState(false); // 时间窗口参数:注册至今=不传;自定义区间(选齐两端)=date_from/date_to。 const params = mode === 'range' && range?.[0] && range?.[1] ? { date_from: range[0].startOf('day').toISOString(), date_to: range[1].endOf('day').toISOString(), } : {}; const paramsKey = JSON.stringify(params); const loadStats = useCallback(async () => { setStatsLoading(true); try { const { data } = await api.get(`/admin/api/users/${userId}/reward-stats`, { params: JSON.parse(paramsKey), }); setStats(data); } catch (e) { message.error(errMsg(e)); } finally { setStatsLoading(false); } }, [userId, paramsKey]); const loadRecords = useCallback( async (p: number) => { setRecordsLoading(true); try { const { data } = await api.get>( `/admin/api/users/${userId}/coin-records`, { params: { ...JSON.parse(paramsKey), limit: PAGE_SIZE, cursor: (p - 1) * PAGE_SIZE } }, ); setRecords(data.items); setTotal(data.total ?? 0); } catch (e) { message.error(errMsg(e)); } finally { setRecordsLoading(false); } }, [userId, paramsKey], ); // 用户或时间筛选变化:回到第 1 页重新拉取 useEffect(() => { loadStats(); setPage(1); loadRecords(1); }, [loadStats, loadRecords]); const regDays = user?.created_at ? dayjs().diff(apiTime(user.created_at), 'day') : null; const columns: ColumnsType = [ { title: '日期时间', dataIndex: 'created_at', width: 150, render: dt }, { title: '赚取途径', dataIndex: 'source_label', width: 120, render: (label: string, r) => {label}, }, { title: 'ECPM', dataIndex: 'ecpm', width: 100, render: (v: string | null) => v ?? -, }, { title: '发放金币数', dataIndex: 'coin', width: 100, render: (v: number) => {v} }, ]; return (
{/* 用户基本信息 + 互斥时间筛选(同一行) */} 手机号 {user?.phone || '-'} 昵称 {user?.nickname || '-'} 微信昵称 {user?.wechat_nickname || '-'} 注册天数 {regDays != null ? `${regDays} 天` : '-'} setMode(e.target.value)} optionType="button" buttonStyle="solid" > 注册至今 自定义区间 setRange(v as [Dayjs, Dayjs] | null)} /> {/* 统计区(受时间筛选;现金余额为当前快照)。ad 视图只保留看广告观看统计(对齐参考图 6 项) */} {stats ? yuan(stats.withdraw_success_cents) : '-'} {stats ? yuan(stats.cash_balance_cents) : '-'} {statsVariant === 'ad' ? ( <> {stats?.reward_video_count ?? '-'} {/* eCPM 均按元展示(reward_video / feed avg_ecpm 原始为分/千次,÷100 转元) */} {stats ? `${(stats.reward_video_avg_ecpm / 100).toFixed(2)} 元/千` : '-'} {stats?.feed_count ?? '-'} {stats ? `${(stats.feed_avg_ecpm / 100).toFixed(2)} 元/千` : '-'} ) : ( <> {stats?.withdraw_total ?? '-'} {stats ? yuan(stats.traditional_task_cash_cents) : '-'} {stats?.reward_video_count ?? '-'} {stats ? `${stats.reward_video_avg_ecpm} 分/千` : '-'} {stats ? yuan(stats.reward_video_cash_cents) : '-'} {stats?.feed_count ?? '-'} {stats ? `${stats.feed_avg_ecpm} 分/千` : '-'} {stats ? yuan(stats.feed_cash_cents) : '-'} )} {statsVariant === 'ad' ? '统计随上方时间筛选(现金余额除外,为当前快照);eCPM 均按元展示(原始分/千次 ÷100)。' : '统计随上方时间筛选(现金余额除外,为当前快照);eCPM 单位分/千次,同金币审计。'} {/* 金币记录 */}

金币记录

`${r.source}-${r.created_at}-${r.coin}-${r.ecpm ?? ''}`} size="small" columns={columns} dataSource={records} loading={recordsLoading || statsLoading} pagination={{ current: page, pageSize: PAGE_SIZE, total, showSizeChanger: false, onChange: (p) => { setPage(p); loadRecords(p); }, showTotal: (t) => `共 ${t} 条`, }} /> ); }