import { useState, useEffect } from 'react' import { Users, TrendingUp, Clock, Wallet, Search, RefreshCw, CheckCircle, XCircle, Calendar, DollarSign, Link2, Eye, Undo2, Settings, } from 'lucide-react' import { ReferralSettingsPage } from '@/pages/referral-settings/ReferralSettingsPage' import { Pagination } from '@/components/ui/Pagination' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Dialog, DialogContent, DialogHeader, DialogFooter, DialogTitle, } from '@/components/ui/dialog' import { get, put } from '@/api/client' interface TodayClicksByPageItem { page: string clicks: number } interface DistributionOverview { todayClicks: number todayBindings: number todayConversions: number todayEarnings: number monthClicks: number monthBindings: number monthConversions: number monthEarnings: number totalClicks: number totalBindings: number totalConversions: number totalEarnings: number expiringBindings: number pendingWithdrawals: number pendingWithdrawAmount: number conversionRate: string totalDistributors: number activeDistributors: number todayUniqueVisitors?: number todayClickRate?: number todayClicksByPage?: TodayClicksByPageItem[] } interface Binding { id: string referrerId: string referrerName?: string referrerCode: string refereeId: string refereePhone?: string refereeNickname?: string boundAt: string expiresAt: string status: 'active' | 'converted' | 'expired' | 'cancelled' commission?: number } interface Withdrawal { id: string userId?: string userName?: string userPhone?: string userAvatar?: string amount: number method?: 'wechat' | 'alipay' account?: string name?: string status: string createdAt?: string processedAt?: string } interface User { id: string nickname: string phone: string referralCode?: string } interface Order { id: string userId: string userNickname?: string userPhone?: string productType?: string type?: string productId?: string sectionId?: string bookName?: string chapterTitle?: string sectionTitle?: string amount: number status: string paymentMethod?: string referrerEarnings?: number referrerId?: string | null referrerNickname?: string | null referrerCode?: string | null referralCode?: string | null orderSn?: string refundReason?: string createdAt: string } export function DistributionPage() { const [activeTab, setActiveTab] = useState<'overview' | 'orders' | 'bindings' | 'withdrawals' | 'settings'>( 'overview', ) const [orders, setOrders] = useState([]) const [overview, setOverview] = useState(null) const [bindings, setBindings] = useState([]) const [withdrawals, setWithdrawals] = useState([]) const [users, setUsers] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [searchTerm, setSearchTerm] = useState('') const [statusFilter, setStatusFilter] = useState('all') const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(10) const [total, setTotal] = useState(0) const [loadedTabs, setLoadedTabs] = useState>(new Set()) const [refundOrder, setRefundOrder] = useState(null) const [refundReason, setRefundReason] = useState('') const [refundLoading, setRefundLoading] = useState(false) useEffect(() => { loadInitialData() }, []) useEffect(() => { setPage(1) }, [activeTab, statusFilter]) useEffect(() => { loadTabData(activeTab) }, [activeTab]) useEffect(() => { if (['orders', 'bindings', 'withdrawals'].includes(activeTab)) { loadTabData(activeTab, true) } }, [page, pageSize, statusFilter, searchTerm]) async function loadInitialData() { setError(null) try { const overviewData = await get<{ success?: boolean; overview?: DistributionOverview }>( '/api/admin/distribution/overview', ) if (overviewData?.success && overviewData.overview) setOverview(overviewData.overview) } catch (e) { console.error('[Admin] 概览接口异常:', e) setError('加载概览失败') } try { const usersData = await get<{ success?: boolean; users?: User[] }>('/api/db/users') setUsers(usersData?.users || []) } catch (e) { console.error('[Admin] 用户数据加载失败:', e) } } async function loadTabData(tab: string, force = false) { if (!force && loadedTabs.has(tab)) return setLoading(true) try { const usersArr = users switch (tab) { case 'overview': break case 'orders': { try { const params = new URLSearchParams({ page: String(page), pageSize: String(pageSize), ...(statusFilter !== 'all' && { status: statusFilter }), ...(searchTerm && { search: searchTerm }), }) const ordersData = await get<{ success?: boolean; orders?: Order[]; total?: number }>(`/api/orders?${params}`) if (ordersData?.success && ordersData.orders) { const enriched = ordersData.orders.map((order) => { const user = usersArr.find((u) => u.id === order.userId) const referrer = order.referrerId ? usersArr.find((u) => u.id === order.referrerId) : null return { ...order, amount: parseFloat(String(order.amount)) || 0, userNickname: user?.nickname || order.userNickname || '未知用户', userPhone: user?.phone || order.userPhone || '-', referrerNickname: referrer?.nickname || null, referrerCode: referrer?.referralCode ?? null, type: order.productType || order.type, } }) setOrders(enriched) setTotal(ordersData.total ?? enriched.length) } else { setOrders([]) setTotal(0) } } catch (e) { console.error(e) setError('加载订单失败') setOrders([]) } break } case 'bindings': { try { const params = new URLSearchParams({ page: String(page), pageSize: String(pageSize), ...(statusFilter !== 'all' && { status: statusFilter }), }) const bindingsData = await get<{ success?: boolean; bindings?: Binding[]; total?: number }>( `/api/db/distribution?${params}`, ) setBindings(bindingsData?.bindings || []) setTotal(bindingsData?.total ?? bindingsData?.bindings?.length ?? 0) } catch (e) { console.error(e) setError('加载绑定数据失败') setBindings([]) } break } case 'withdrawals': { try { const statusParam = statusFilter === 'completed' ? 'success' : statusFilter === 'rejected' ? 'failed' : statusFilter const params = new URLSearchParams({ ...(statusParam && statusParam !== 'all' && { status: statusParam }), page: String(page), pageSize: String(pageSize), }) const withdrawalsData = await get<{ success?: boolean withdrawals?: Withdrawal[] total?: number error?: string }>(`/api/admin/withdrawals?${params}`) if (withdrawalsData?.success && withdrawalsData.withdrawals) { const formatted = withdrawalsData.withdrawals.map((w) => ({ ...w, account: w.account ?? '未绑定微信号', status: w.status === 'success' ? 'completed' : w.status === 'failed' ? 'rejected' : w.status, })) setWithdrawals(formatted) setTotal(withdrawalsData?.total ?? formatted.length) } else { if (!withdrawalsData?.success) setError(`获取提现记录失败: ${(withdrawalsData as { error?: string })?.error || '未知错误'}`) setWithdrawals([]) } } catch (e) { console.error(e) setError('加载提现数据失败') setWithdrawals([]) } break } } setLoadedTabs((prev) => new Set(prev).add(tab)) } catch (e) { console.error(e) } finally { setLoading(false) } } async function refreshCurrentTab() { setError(null) setLoadedTabs((prev) => { const next = new Set(prev) next.delete(activeTab) return next }) if (activeTab === 'overview') loadInitialData() await loadTabData(activeTab, true) } async function handleApproveWithdrawal(id: string) { if (!confirm('确认审核通过并打款?')) return try { const res = await put<{ success?: boolean; error?: string; message?: string }>( '/api/admin/withdrawals', { id, action: 'approve' }, ) if (!res?.success) { const detail = res?.message || res?.error || '操作失败' alert(detail) return } await refreshCurrentTab() } catch (e) { console.error(e) alert('操作失败') } } async function handleRejectWithdrawal(id: string) { const reason = prompt('请输入拒绝原因:') if (!reason) return try { const res = await put<{ success?: boolean; error?: string }>( '/api/admin/withdrawals', { id, action: 'reject', errorMessage: reason }, ) if (!res?.success) { alert(res?.error || '操作失败') return } await refreshCurrentTab() } catch (e) { console.error(e) alert('操作失败') } } async function handleRefund() { if (!refundOrder?.orderSn && !refundOrder?.id) return setRefundLoading(true) setError(null) try { const res = await put<{ success?: boolean; error?: string }>('/api/admin/orders/refund', { orderSn: refundOrder.orderSn || refundOrder.id, reason: refundReason || undefined, }) if (res?.success) { setRefundOrder(null) setRefundReason('') await loadTabData('orders', true) } else { setError(res?.error || '退款失败') } } catch (e) { const err = e as Error & { data?: { error?: string } } setError(err?.data?.error || '退款失败,请检查网络后重试') } finally { setRefundLoading(false) } } function getStatusBadge(status: string) { const styles: Record = { active: 'bg-green-500/20 text-green-400', converted: 'bg-blue-500/20 text-blue-400', expired: 'bg-gray-500/20 text-gray-400', cancelled: 'bg-red-500/20 text-red-400', pending: 'bg-orange-500/20 text-orange-400', pending_confirm: 'bg-orange-500/20 text-orange-400', processing: 'bg-blue-500/20 text-blue-400', completed: 'bg-green-500/20 text-green-400', rejected: 'bg-red-500/20 text-red-400', } const labels: Record = { active: '有效', converted: '已转化', expired: '已过期', cancelled: '已取消', pending: '待审核', pending_confirm: '待用户确认', processing: '处理中', completed: '已完成', rejected: '已拒绝', } return ( {labels[status] || status} ) } const totalPages = Math.ceil(total / pageSize) || 1 const displayOrders = orders const displayBindings = bindings.filter((b) => { if (!searchTerm) return true const term = searchTerm.toLowerCase() return ( b.refereeNickname?.toLowerCase().includes(term) || b.refereePhone?.includes(term) || b.referrerName?.toLowerCase().includes(term) || b.referrerCode?.toLowerCase().includes(term) ) }) const displayWithdrawals = withdrawals.filter((w) => { if (!searchTerm) return true const term = searchTerm.toLowerCase() return ( w.userName?.toLowerCase().includes(term) || (w.account && w.account.toLowerCase().includes(term)) ) }) return (
{error && (
{error}
)}

推广中心

统一管理:订单、分销绑定、提现审核

{[ { key: 'overview', label: '数据概览', icon: TrendingUp }, { key: 'orders', label: '订单管理', icon: DollarSign }, { key: 'bindings', label: '绑定管理', icon: Link2 }, { key: 'withdrawals', label: '提现审核', icon: Wallet }, { key: 'settings', label: '推广设置', icon: Settings }, ].map((tab) => ( ))}
{loading ? (
加载中...
) : ( <> {activeTab === 'overview' && overview && (

今日点击

{overview.todayClicks}

总点击次数(实时)

今日独立用户

{overview.todayUniqueVisitors ?? 0}

去重访客数(实时)

今日总文章点击率

{(overview.todayClickRate ?? 0).toFixed(2)}

人均点击(总点击/独立用户)

今日绑定

{overview.todayBindings}

今日转化

{overview.todayConversions}

今日佣金

¥{overview.todayEarnings.toFixed(2)}

{/* 每篇文章今日点击 */} {(overview.todayClicksByPage?.length ?? 0) > 0 && ( 每篇文章今日点击(按来源页/文章统计)

实际用户与实际文章的点击均计入;今日总点击与上表一致

{[...(overview.todayClicksByPage ?? [])] .sort((a, b) => b.clicks - a.clicks) .map((row, i) => ( ))}
来源页/文章 今日点击 占比
{row.page || '(未区分)'} {row.clicks} {overview.todayClicks > 0 ? ((row.clicks / overview.todayClicks) * 100).toFixed(1) : 0} %
)}

即将过期绑定

{overview.expiringBindings} 个

7天内到期,需关注转化

待审核提现

{overview.pendingWithdrawals} 笔

共 ¥{overview.pendingWithdrawAmount.toFixed(2)}

本月统计

点击量

{overview.monthClicks}

绑定数

{overview.monthBindings}

转化数

{overview.monthConversions}

佣金

¥{overview.monthEarnings.toFixed(2)}

累计统计

总点击

{overview.totalClicks.toLocaleString()}

总绑定

{overview.totalBindings.toLocaleString()}

总转化

{overview.totalConversions}

总佣金

¥{overview.totalEarnings.toFixed(2)}

点击转化率 {overview.conversionRate}%
推广统计

{overview.totalDistributors}

推广用户数

{overview.activeDistributors}

有收益用户

90%

佣金比例

30天

绑定有效期

)} {activeTab === 'orders' && (
setSearchTerm(e.target.value)} placeholder="搜索订单号、用户名、手机号..." className="pl-10 bg-[#0f2137] border-gray-700 text-white" />
{orders.length === 0 ? (
暂无订单数据
) : (
{displayOrders.map((order) => ( ))}
订单号 用户 商品 金额 支付方式 状态 退款原因 推荐人/邀请码 分销佣金 下单时间 操作
{order.id?.slice(0, 12)}...

{order.userNickname}

{order.userPhone}

{(() => { const type = order.productType || order.type if (type === 'fullbook') return `${order.bookName || '《底层逻辑》'} - 全本` if (type === 'match') return '匹配次数购买' return `${order.bookName || '《底层逻辑》'} - ${order.sectionTitle || order.chapterTitle || `章节${order.productId || order.sectionId || ''}`}` })()}

{(() => { const type = order.productType || order.type if (type === 'fullbook') return '全书解锁' if (type === 'match') return '功能权益' return order.chapterTitle || '单章购买' })()}

¥{typeof order.amount === 'number' ? order.amount.toFixed(2) : parseFloat(String(order.amount || '0')).toFixed(2)} {order.paymentMethod === 'wechat' ? '微信支付' : order.paymentMethod === 'alipay' ? '支付宝' : order.paymentMethod || '微信支付'} {order.status === 'refunded' ? ( 已退款 ) : order.status === 'completed' || order.status === 'paid' ? ( 已完成 ) : order.status === 'pending' || order.status === 'created' ? ( 待支付 ) : ( 已失败 )} {order.status === 'refunded' && order.refundReason ? order.refundReason : '-'} {order.referrerId || order.referralCode ? ( {order.referrerNickname || order.referralCode || order.referrerCode || order.referrerId?.slice(0, 8)} {(order.referralCode || order.referrerCode) && ` (${order.referralCode || order.referrerCode})`} ) : ( '-' )} {order.referrerEarnings ? `¥${(typeof order.referrerEarnings === 'number' ? order.referrerEarnings : parseFloat(String(order.referrerEarnings))).toFixed(2)}` : '-'} {order.createdAt ? new Date(order.createdAt).toLocaleString('zh-CN') : '-'} {(order.status === 'paid' || order.status === 'completed') && ( )}
)} {activeTab === 'orders' && ( { setPageSize(n) setPage(1) }} /> )}
)} {activeTab === 'bindings' && (
setSearchTerm(e.target.value)} placeholder="搜索用户昵称、手机号、推广码..." className="pl-10 bg-[#0f2137] border-gray-700 text-white" />
{displayBindings.length === 0 ? (
暂无绑定数据
) : (
{displayBindings.map((binding) => ( ))}
访客 分销商 绑定时间 到期时间 状态 佣金

{binding.refereeNickname || '匿名用户'}

{binding.refereePhone}

{binding.referrerName || '-'}

{binding.referrerCode}

{binding.boundAt ? new Date(binding.boundAt).toLocaleDateString('zh-CN') : '-'} {binding.expiresAt ? new Date(binding.expiresAt).toLocaleDateString('zh-CN') : '-'} {getStatusBadge(binding.status)} {binding.commission ? ( ¥{binding.commission.toFixed(2)} ) : ( - )}
)} {activeTab === 'bindings' && ( { setPageSize(n) setPage(1) }} /> )}
)} {activeTab === 'withdrawals' && (
setSearchTerm(e.target.value)} placeholder="搜索用户名称、账号..." className="pl-10 bg-[#0f2137] border-gray-700 text-white" />
{displayWithdrawals.length === 0 ? (
暂无提现记录
) : (
{displayWithdrawals.map((withdrawal) => ( ))}
申请人 金额 收款方式 收款账号 申请时间 状态 操作
{withdrawal.userAvatar ? ( ) : (
{(withdrawal.userName || withdrawal.name || '?').slice(0, 1)}
)}

{withdrawal.userName || withdrawal.name}

¥{withdrawal.amount.toFixed(2)} {withdrawal.method === 'wechat' ? '微信' : '支付宝'}

{withdrawal.account}

{withdrawal.name}

{withdrawal.createdAt ? new Date(withdrawal.createdAt).toLocaleString('zh-CN') : '-'} {getStatusBadge(withdrawal.status)} {withdrawal.status === 'pending' && (
)}
)} {activeTab === 'withdrawals' && ( { setPageSize(n) setPage(1) }} /> )}
)} )} !open && setRefundOrder(null)}> 订单退款 {refundOrder && (

订单号:{refundOrder.orderSn || refundOrder.id}

退款金额:¥{typeof refundOrder.amount === 'number' ? refundOrder.amount.toFixed(2) : parseFloat(String(refundOrder.amount || '0')).toFixed(2)}

setRefundReason(e.target.value)} />

退款将原路退回至用户微信,且无法撤销,请确认后再操作。

)}
{/* 推广设置 Tab */} {activeTab === 'settings' && (
)}
) }