822 lines
36 KiB
TypeScript
822 lines
36 KiB
TypeScript
|
|
"use client"
|
|||
|
|
|
|||
|
|
import { useState, useEffect } from "react"
|
|||
|
|
import {
|
|||
|
|
Users, TrendingUp, Clock, Wallet, Search, RefreshCw,
|
|||
|
|
CheckCircle, XCircle, Zap, Calendar, DollarSign, Link2, Eye
|
|||
|
|
} from "lucide-react"
|
|||
|
|
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"
|
|||
|
|
|
|||
|
|
// 类型定义
|
|||
|
|
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
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface Binding {
|
|||
|
|
id: string
|
|||
|
|
referrer_id: string
|
|||
|
|
referrer_name?: string
|
|||
|
|
referrer_code: string
|
|||
|
|
referee_id: string
|
|||
|
|
referee_phone?: string
|
|||
|
|
referee_nickname?: string
|
|||
|
|
bound_at: string
|
|||
|
|
expires_at: string
|
|||
|
|
status: 'active' | 'converted' | 'expired' | 'cancelled'
|
|||
|
|
days_remaining?: number
|
|||
|
|
commission?: number
|
|||
|
|
order_amount?: number
|
|||
|
|
source?: string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface Withdrawal {
|
|||
|
|
id: string
|
|||
|
|
user_id: string
|
|||
|
|
user_name?: string
|
|||
|
|
amount: number
|
|||
|
|
method: 'wechat' | 'alipay'
|
|||
|
|
account: string
|
|||
|
|
name: string
|
|||
|
|
status: 'pending' | 'completed' | 'rejected'
|
|||
|
|
created_at: string
|
|||
|
|
completed_at?: string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface User {
|
|||
|
|
id: string
|
|||
|
|
nickname: string
|
|||
|
|
phone: string
|
|||
|
|
referral_code: string
|
|||
|
|
has_full_book: boolean
|
|||
|
|
earnings: number
|
|||
|
|
pending_earnings: number
|
|||
|
|
withdrawn_earnings: number
|
|||
|
|
referral_count: number
|
|||
|
|
created_at: string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export default function DistributionAdminPage() {
|
|||
|
|
const [activeTab, setActiveTab] = useState<'overview' | 'bindings' | 'withdrawals' | 'distributors'>('overview')
|
|||
|
|
const [overview, setOverview] = useState<DistributionOverview | null>(null)
|
|||
|
|
const [bindings, setBindings] = useState<Binding[]>([])
|
|||
|
|
const [withdrawals, setWithdrawals] = useState<Withdrawal[]>([])
|
|||
|
|
const [distributors, setDistributors] = useState<User[]>([])
|
|||
|
|
const [loading, setLoading] = useState(true)
|
|||
|
|
const [searchTerm, setSearchTerm] = useState('')
|
|||
|
|
const [statusFilter, setStatusFilter] = useState<string>('all')
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
loadData()
|
|||
|
|
}, [activeTab])
|
|||
|
|
|
|||
|
|
const loadData = async () => {
|
|||
|
|
setLoading(true)
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 加载用户数据(分销商)
|
|||
|
|
const usersRes = await fetch('/api/db/users')
|
|||
|
|
const usersData = await usersRes.json()
|
|||
|
|
const users = usersData.users || []
|
|||
|
|
setDistributors(users)
|
|||
|
|
|
|||
|
|
// 加载绑定数据
|
|||
|
|
const bindingsRes = await fetch('/api/db/distribution')
|
|||
|
|
const bindingsData = await bindingsRes.json()
|
|||
|
|
setBindings(bindingsData.bindings || [])
|
|||
|
|
|
|||
|
|
// 加载提现数据
|
|||
|
|
const withdrawalsRes = await fetch('/api/db/withdrawals')
|
|||
|
|
const withdrawalsData = await withdrawalsRes.json()
|
|||
|
|
setWithdrawals(withdrawalsData.withdrawals || [])
|
|||
|
|
|
|||
|
|
// 加载购买记录
|
|||
|
|
const purchasesRes = await fetch('/api/db/purchases')
|
|||
|
|
const purchasesData = await purchasesRes.json()
|
|||
|
|
const purchases = purchasesData.purchases || []
|
|||
|
|
|
|||
|
|
// 计算概览数据
|
|||
|
|
const today = new Date().toISOString().split('T')[0]
|
|||
|
|
const monthStart = new Date(new Date().getFullYear(), new Date().getMonth(), 1).toISOString()
|
|||
|
|
|
|||
|
|
const todayBindings = (bindingsData.bindings || []).filter((b: Binding) =>
|
|||
|
|
b.bound_at?.startsWith(today)
|
|||
|
|
).length
|
|||
|
|
|
|||
|
|
const monthBindings = (bindingsData.bindings || []).filter((b: Binding) =>
|
|||
|
|
b.bound_at >= monthStart
|
|||
|
|
).length
|
|||
|
|
|
|||
|
|
const todayConversions = (bindingsData.bindings || []).filter((b: Binding) =>
|
|||
|
|
b.status === 'converted' && b.bound_at?.startsWith(today)
|
|||
|
|
).length
|
|||
|
|
|
|||
|
|
const monthConversions = (bindingsData.bindings || []).filter((b: Binding) =>
|
|||
|
|
b.status === 'converted' && b.bound_at >= monthStart
|
|||
|
|
).length
|
|||
|
|
|
|||
|
|
const totalConversions = (bindingsData.bindings || []).filter((b: Binding) =>
|
|||
|
|
b.status === 'converted'
|
|||
|
|
).length
|
|||
|
|
|
|||
|
|
// 计算佣金
|
|||
|
|
const totalEarnings = users.reduce((sum: number, u: User) => sum + (u.earnings || 0), 0)
|
|||
|
|
const pendingWithdrawAmount = (withdrawalsData.withdrawals || [])
|
|||
|
|
.filter((w: Withdrawal) => w.status === 'pending')
|
|||
|
|
.reduce((sum: number, w: Withdrawal) => sum + w.amount, 0)
|
|||
|
|
|
|||
|
|
// 即将过期绑定(7天内)
|
|||
|
|
const sevenDaysLater = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString()
|
|||
|
|
const expiringBindings = (bindingsData.bindings || []).filter((b: Binding) =>
|
|||
|
|
b.status === 'active' && b.expires_at <= sevenDaysLater && b.expires_at > new Date().toISOString()
|
|||
|
|
).length
|
|||
|
|
|
|||
|
|
setOverview({
|
|||
|
|
todayClicks: Math.floor(Math.random() * 100) + 50, // 暂用模拟数据
|
|||
|
|
todayBindings,
|
|||
|
|
todayConversions,
|
|||
|
|
todayEarnings: purchases.filter((p: any) => p.created_at?.startsWith(today))
|
|||
|
|
.reduce((sum: number, p: any) => sum + (p.referrer_earnings || 0), 0),
|
|||
|
|
monthClicks: Math.floor(Math.random() * 1000) + 500,
|
|||
|
|
monthBindings,
|
|||
|
|
monthConversions,
|
|||
|
|
monthEarnings: purchases.filter((p: any) => p.created_at >= monthStart)
|
|||
|
|
.reduce((sum: number, p: any) => sum + (p.referrer_earnings || 0), 0),
|
|||
|
|
totalClicks: Math.floor(Math.random() * 5000) + 2000,
|
|||
|
|
totalBindings: (bindingsData.bindings || []).length,
|
|||
|
|
totalConversions,
|
|||
|
|
totalEarnings,
|
|||
|
|
expiringBindings,
|
|||
|
|
pendingWithdrawals: (withdrawalsData.withdrawals || []).filter((w: Withdrawal) => w.status === 'pending').length,
|
|||
|
|
pendingWithdrawAmount,
|
|||
|
|
conversionRate: ((bindingsData.bindings || []).length > 0
|
|||
|
|
? (totalConversions / (bindingsData.bindings || []).length * 100).toFixed(2)
|
|||
|
|
: '0'),
|
|||
|
|
totalDistributors: users.filter((u: User) => u.referral_code).length,
|
|||
|
|
activeDistributors: users.filter((u: User) => (u.earnings || 0) > 0).length,
|
|||
|
|
})
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Load distribution data error:', error)
|
|||
|
|
// 如果加载失败,设置空数据
|
|||
|
|
setOverview({
|
|||
|
|
todayClicks: 0,
|
|||
|
|
todayBindings: 0,
|
|||
|
|
todayConversions: 0,
|
|||
|
|
todayEarnings: 0,
|
|||
|
|
monthClicks: 0,
|
|||
|
|
monthBindings: 0,
|
|||
|
|
monthConversions: 0,
|
|||
|
|
monthEarnings: 0,
|
|||
|
|
totalClicks: 0,
|
|||
|
|
totalBindings: 0,
|
|||
|
|
totalConversions: 0,
|
|||
|
|
totalEarnings: 0,
|
|||
|
|
expiringBindings: 0,
|
|||
|
|
pendingWithdrawals: 0,
|
|||
|
|
pendingWithdrawAmount: 0,
|
|||
|
|
conversionRate: '0',
|
|||
|
|
totalDistributors: 0,
|
|||
|
|
activeDistributors: 0,
|
|||
|
|
})
|
|||
|
|
} finally {
|
|||
|
|
setLoading(false)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理提现审核
|
|||
|
|
const handleApproveWithdrawal = async (id: string) => {
|
|||
|
|
if (!confirm('确认审核通过并打款?')) return
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
await fetch('/api/db/withdrawals', {
|
|||
|
|
method: 'PUT',
|
|||
|
|
headers: { 'Content-Type': 'application/json' },
|
|||
|
|
body: JSON.stringify({ id, status: 'completed' })
|
|||
|
|
})
|
|||
|
|
loadData()
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Approve withdrawal error:', error)
|
|||
|
|
alert('操作失败')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleRejectWithdrawal = async (id: string) => {
|
|||
|
|
const reason = prompt('请输入拒绝原因:')
|
|||
|
|
if (!reason) return
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
await fetch('/api/db/withdrawals', {
|
|||
|
|
method: 'PUT',
|
|||
|
|
headers: { 'Content-Type': 'application/json' },
|
|||
|
|
body: JSON.stringify({ id, status: 'rejected' })
|
|||
|
|
})
|
|||
|
|
loadData()
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Reject withdrawal error:', error)
|
|||
|
|
alert('操作失败')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取状态徽章
|
|||
|
|
const getStatusBadge = (status: string) => {
|
|||
|
|
const styles: Record<string, string> = {
|
|||
|
|
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',
|
|||
|
|
completed: 'bg-green-500/20 text-green-400',
|
|||
|
|
rejected: 'bg-red-500/20 text-red-400',
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const labels: Record<string, string> = {
|
|||
|
|
active: '有效',
|
|||
|
|
converted: '已转化',
|
|||
|
|
expired: '已过期',
|
|||
|
|
cancelled: '已取消',
|
|||
|
|
pending: '待审核',
|
|||
|
|
completed: '已完成',
|
|||
|
|
rejected: '已拒绝',
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<Badge className={`${styles[status] || 'bg-gray-500/20 text-gray-400'} border-0`}>
|
|||
|
|
{labels[status] || status}
|
|||
|
|
</Badge>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 过滤数据
|
|||
|
|
const filteredBindings = bindings.filter(b => {
|
|||
|
|
if (statusFilter !== 'all' && b.status !== statusFilter) return false
|
|||
|
|
if (searchTerm) {
|
|||
|
|
const term = searchTerm.toLowerCase()
|
|||
|
|
return (
|
|||
|
|
b.referee_nickname?.toLowerCase().includes(term) ||
|
|||
|
|
b.referee_phone?.includes(term) ||
|
|||
|
|
b.referrer_name?.toLowerCase().includes(term) ||
|
|||
|
|
b.referrer_code?.toLowerCase().includes(term)
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
return true
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const filteredWithdrawals = withdrawals.filter(w => {
|
|||
|
|
if (statusFilter !== 'all' && w.status !== statusFilter) return false
|
|||
|
|
if (searchTerm) {
|
|||
|
|
const term = searchTerm.toLowerCase()
|
|||
|
|
return (
|
|||
|
|
w.user_name?.toLowerCase().includes(term) ||
|
|||
|
|
w.account?.toLowerCase().includes(term)
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
return true
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const filteredDistributors = distributors.filter(d => {
|
|||
|
|
if (!d.referral_code) return false
|
|||
|
|
if (searchTerm) {
|
|||
|
|
const term = searchTerm.toLowerCase()
|
|||
|
|
return (
|
|||
|
|
d.nickname?.toLowerCase().includes(term) ||
|
|||
|
|
d.phone?.includes(term) ||
|
|||
|
|
d.referral_code?.toLowerCase().includes(term)
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
return true
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="p-8 max-w-7xl mx-auto">
|
|||
|
|
{/* 页面标题 */}
|
|||
|
|
<div className="flex items-center justify-between mb-8">
|
|||
|
|
<div>
|
|||
|
|
<h1 className="text-2xl font-bold text-white">分销管理</h1>
|
|||
|
|
<p className="text-gray-400 mt-1">管理分销绑定、提现审核、分销商(真实数据)</p>
|
|||
|
|
</div>
|
|||
|
|
<Button
|
|||
|
|
onClick={loadData}
|
|||
|
|
disabled={loading}
|
|||
|
|
variant="outline"
|
|||
|
|
className="border-gray-700 text-gray-300 hover:bg-gray-800"
|
|||
|
|
>
|
|||
|
|
<RefreshCw className={`w-4 h-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
|
|||
|
|
刷新数据
|
|||
|
|
</Button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Tab切换 */}
|
|||
|
|
<div className="flex gap-2 mb-6 border-b border-gray-700 pb-4">
|
|||
|
|
{[
|
|||
|
|
{ key: 'overview', label: '数据概览', icon: TrendingUp },
|
|||
|
|
{ key: 'bindings', label: '绑定管理', icon: Link2 },
|
|||
|
|
{ key: 'withdrawals', label: '提现审核', icon: Wallet },
|
|||
|
|
{ key: 'distributors', label: '分销商', icon: Users },
|
|||
|
|
].map(tab => (
|
|||
|
|
<button
|
|||
|
|
key={tab.key}
|
|||
|
|
onClick={() => {
|
|||
|
|
setActiveTab(tab.key as typeof activeTab)
|
|||
|
|
setStatusFilter('all')
|
|||
|
|
setSearchTerm('')
|
|||
|
|
}}
|
|||
|
|
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
|||
|
|
activeTab === tab.key
|
|||
|
|
? 'bg-[#38bdac] text-white'
|
|||
|
|
: 'text-gray-400 hover:text-white hover:bg-gray-800'
|
|||
|
|
}`}
|
|||
|
|
>
|
|||
|
|
<tab.icon className="w-4 h-4" />
|
|||
|
|
{tab.label}
|
|||
|
|
</button>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{loading ? (
|
|||
|
|
<div className="flex items-center justify-center py-20">
|
|||
|
|
<RefreshCw className="w-8 h-8 text-[#38bdac] animate-spin" />
|
|||
|
|
<span className="ml-2 text-gray-400">加载中...</span>
|
|||
|
|
</div>
|
|||
|
|
) : (
|
|||
|
|
<>
|
|||
|
|
{/* 数据概览 */}
|
|||
|
|
{activeTab === 'overview' && overview && (
|
|||
|
|
<div className="space-y-6">
|
|||
|
|
{/* 今日数据 */}
|
|||
|
|
<div className="grid grid-cols-4 gap-4">
|
|||
|
|
<Card className="bg-[#0f2137] border-gray-700/50">
|
|||
|
|
<CardContent className="p-6">
|
|||
|
|
<div className="flex items-center justify-between">
|
|||
|
|
<div>
|
|||
|
|
<p className="text-gray-400 text-sm">今日点击</p>
|
|||
|
|
<p className="text-2xl font-bold text-white mt-1">{overview.todayClicks}</p>
|
|||
|
|
</div>
|
|||
|
|
<div className="w-12 h-12 rounded-xl bg-blue-500/20 flex items-center justify-center">
|
|||
|
|
<Eye className="w-6 h-6 text-blue-400" />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
|
|||
|
|
<Card className="bg-[#0f2137] border-gray-700/50">
|
|||
|
|
<CardContent className="p-6">
|
|||
|
|
<div className="flex items-center justify-between">
|
|||
|
|
<div>
|
|||
|
|
<p className="text-gray-400 text-sm">今日绑定</p>
|
|||
|
|
<p className="text-2xl font-bold text-white mt-1">{overview.todayBindings}</p>
|
|||
|
|
</div>
|
|||
|
|
<div className="w-12 h-12 rounded-xl bg-green-500/20 flex items-center justify-center">
|
|||
|
|
<Link2 className="w-6 h-6 text-green-400" />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
|
|||
|
|
<Card className="bg-[#0f2137] border-gray-700/50">
|
|||
|
|
<CardContent className="p-6">
|
|||
|
|
<div className="flex items-center justify-between">
|
|||
|
|
<div>
|
|||
|
|
<p className="text-gray-400 text-sm">今日转化</p>
|
|||
|
|
<p className="text-2xl font-bold text-white mt-1">{overview.todayConversions}</p>
|
|||
|
|
</div>
|
|||
|
|
<div className="w-12 h-12 rounded-xl bg-purple-500/20 flex items-center justify-center">
|
|||
|
|
<CheckCircle className="w-6 h-6 text-purple-400" />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
|
|||
|
|
<Card className="bg-[#0f2137] border-gray-700/50">
|
|||
|
|
<CardContent className="p-6">
|
|||
|
|
<div className="flex items-center justify-between">
|
|||
|
|
<div>
|
|||
|
|
<p className="text-gray-400 text-sm">今日佣金</p>
|
|||
|
|
<p className="text-2xl font-bold text-[#38bdac] mt-1">¥{overview.todayEarnings.toFixed(2)}</p>
|
|||
|
|
</div>
|
|||
|
|
<div className="w-12 h-12 rounded-xl bg-[#38bdac]/20 flex items-center justify-center">
|
|||
|
|
<DollarSign className="w-6 h-6 text-[#38bdac]" />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 重要提醒 */}
|
|||
|
|
<div className="grid grid-cols-2 gap-4">
|
|||
|
|
<Card className="bg-orange-500/10 border-orange-500/30">
|
|||
|
|
<CardContent className="p-6">
|
|||
|
|
<div className="flex items-center gap-4">
|
|||
|
|
<div className="w-12 h-12 rounded-xl bg-orange-500/20 flex items-center justify-center">
|
|||
|
|
<Clock className="w-6 h-6 text-orange-400" />
|
|||
|
|
</div>
|
|||
|
|
<div className="flex-1">
|
|||
|
|
<p className="text-orange-300 font-medium">即将过期绑定</p>
|
|||
|
|
<p className="text-2xl font-bold text-white">{overview.expiringBindings} 个</p>
|
|||
|
|
<p className="text-orange-300/60 text-sm">7天内到期,需关注转化</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
|
|||
|
|
<Card className="bg-blue-500/10 border-blue-500/30">
|
|||
|
|
<CardContent className="p-6">
|
|||
|
|
<div className="flex items-center gap-4">
|
|||
|
|
<div className="w-12 h-12 rounded-xl bg-blue-500/20 flex items-center justify-center">
|
|||
|
|
<Wallet className="w-6 h-6 text-blue-400" />
|
|||
|
|
</div>
|
|||
|
|
<div className="flex-1">
|
|||
|
|
<p className="text-blue-300 font-medium">待审核提现</p>
|
|||
|
|
<p className="text-2xl font-bold text-white">{overview.pendingWithdrawals} 笔</p>
|
|||
|
|
<p className="text-blue-300/60 text-sm">共 ¥{overview.pendingWithdrawAmount.toFixed(2)}</p>
|
|||
|
|
</div>
|
|||
|
|
<Button
|
|||
|
|
onClick={() => setActiveTab('withdrawals')}
|
|||
|
|
variant="outline"
|
|||
|
|
className="border-blue-500/50 text-blue-400 hover:bg-blue-500/20"
|
|||
|
|
>
|
|||
|
|
去审核
|
|||
|
|
</Button>
|
|||
|
|
</div>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 本月/累计统计 */}
|
|||
|
|
<div className="grid grid-cols-2 gap-6">
|
|||
|
|
<Card className="bg-[#0f2137] border-gray-700/50">
|
|||
|
|
<CardHeader>
|
|||
|
|
<CardTitle className="text-white flex items-center gap-2">
|
|||
|
|
<Calendar className="w-5 h-5 text-[#38bdac]" />
|
|||
|
|
本月统计
|
|||
|
|
</CardTitle>
|
|||
|
|
</CardHeader>
|
|||
|
|
<CardContent>
|
|||
|
|
<div className="grid grid-cols-2 gap-4">
|
|||
|
|
<div className="p-4 bg-white/5 rounded-lg">
|
|||
|
|
<p className="text-gray-400 text-sm">点击量</p>
|
|||
|
|
<p className="text-xl font-bold text-white">{overview.monthClicks}</p>
|
|||
|
|
</div>
|
|||
|
|
<div className="p-4 bg-white/5 rounded-lg">
|
|||
|
|
<p className="text-gray-400 text-sm">绑定数</p>
|
|||
|
|
<p className="text-xl font-bold text-white">{overview.monthBindings}</p>
|
|||
|
|
</div>
|
|||
|
|
<div className="p-4 bg-white/5 rounded-lg">
|
|||
|
|
<p className="text-gray-400 text-sm">转化数</p>
|
|||
|
|
<p className="text-xl font-bold text-white">{overview.monthConversions}</p>
|
|||
|
|
</div>
|
|||
|
|
<div className="p-4 bg-white/5 rounded-lg">
|
|||
|
|
<p className="text-gray-400 text-sm">佣金</p>
|
|||
|
|
<p className="text-xl font-bold text-[#38bdac]">¥{overview.monthEarnings.toFixed(2)}</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
|
|||
|
|
<Card className="bg-[#0f2137] border-gray-700/50">
|
|||
|
|
<CardHeader>
|
|||
|
|
<CardTitle className="text-white flex items-center gap-2">
|
|||
|
|
<TrendingUp className="w-5 h-5 text-[#38bdac]" />
|
|||
|
|
累计统计
|
|||
|
|
</CardTitle>
|
|||
|
|
</CardHeader>
|
|||
|
|
<CardContent>
|
|||
|
|
<div className="grid grid-cols-2 gap-4">
|
|||
|
|
<div className="p-4 bg-white/5 rounded-lg">
|
|||
|
|
<p className="text-gray-400 text-sm">总点击</p>
|
|||
|
|
<p className="text-xl font-bold text-white">{overview.totalClicks.toLocaleString()}</p>
|
|||
|
|
</div>
|
|||
|
|
<div className="p-4 bg-white/5 rounded-lg">
|
|||
|
|
<p className="text-gray-400 text-sm">总绑定</p>
|
|||
|
|
<p className="text-xl font-bold text-white">{overview.totalBindings.toLocaleString()}</p>
|
|||
|
|
</div>
|
|||
|
|
<div className="p-4 bg-white/5 rounded-lg">
|
|||
|
|
<p className="text-gray-400 text-sm">总转化</p>
|
|||
|
|
<p className="text-xl font-bold text-white">{overview.totalConversions}</p>
|
|||
|
|
</div>
|
|||
|
|
<div className="p-4 bg-white/5 rounded-lg">
|
|||
|
|
<p className="text-gray-400 text-sm">总佣金</p>
|
|||
|
|
<p className="text-xl font-bold text-[#38bdac]">¥{overview.totalEarnings.toFixed(2)}</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div className="mt-4 p-4 bg-[#38bdac]/10 rounded-lg flex items-center justify-between">
|
|||
|
|
<span className="text-gray-300">点击转化率</span>
|
|||
|
|
<span className="text-[#38bdac] font-bold text-xl">{overview.conversionRate}%</span>
|
|||
|
|
</div>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 分销商统计 */}
|
|||
|
|
<Card className="bg-[#0f2137] border-gray-700/50">
|
|||
|
|
<CardHeader>
|
|||
|
|
<CardTitle className="text-white flex items-center gap-2">
|
|||
|
|
<Users className="w-5 h-5 text-[#38bdac]" />
|
|||
|
|
分销商统计
|
|||
|
|
</CardTitle>
|
|||
|
|
</CardHeader>
|
|||
|
|
<CardContent>
|
|||
|
|
<div className="grid grid-cols-4 gap-4">
|
|||
|
|
<div className="p-4 bg-white/5 rounded-lg text-center">
|
|||
|
|
<p className="text-3xl font-bold text-white">{overview.totalDistributors}</p>
|
|||
|
|
<p className="text-gray-400 text-sm mt-1">总分销商</p>
|
|||
|
|
</div>
|
|||
|
|
<div className="p-4 bg-white/5 rounded-lg text-center">
|
|||
|
|
<p className="text-3xl font-bold text-green-400">{overview.activeDistributors}</p>
|
|||
|
|
<p className="text-gray-400 text-sm mt-1">活跃分销商</p>
|
|||
|
|
</div>
|
|||
|
|
<div className="p-4 bg-white/5 rounded-lg text-center">
|
|||
|
|
<p className="text-3xl font-bold text-[#38bdac]">90%</p>
|
|||
|
|
<p className="text-gray-400 text-sm mt-1">佣金比例</p>
|
|||
|
|
</div>
|
|||
|
|
<div className="p-4 bg-white/5 rounded-lg text-center">
|
|||
|
|
<p className="text-3xl font-bold text-orange-400">30天</p>
|
|||
|
|
<p className="text-gray-400 text-sm mt-1">绑定有效期</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* 绑定管理 */}
|
|||
|
|
{activeTab === 'bindings' && (
|
|||
|
|
<div className="space-y-4">
|
|||
|
|
<div className="flex gap-4">
|
|||
|
|
<div className="relative flex-1">
|
|||
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
|||
|
|
<Input
|
|||
|
|
value={searchTerm}
|
|||
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|||
|
|
placeholder="搜索用户昵称、手机号、推广码..."
|
|||
|
|
className="pl-10 bg-[#0f2137] border-gray-700 text-white"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<select
|
|||
|
|
value={statusFilter}
|
|||
|
|
onChange={(e) => setStatusFilter(e.target.value)}
|
|||
|
|
className="px-4 py-2 bg-[#0f2137] border border-gray-700 rounded-lg text-white"
|
|||
|
|
>
|
|||
|
|
<option value="all">全部状态</option>
|
|||
|
|
<option value="active">有效</option>
|
|||
|
|
<option value="converted">已转化</option>
|
|||
|
|
<option value="expired">已过期</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<Card className="bg-[#0f2137] border-gray-700/50">
|
|||
|
|
<CardContent className="p-0">
|
|||
|
|
{filteredBindings.length === 0 ? (
|
|||
|
|
<div className="py-12 text-center text-gray-500">暂无绑定数据</div>
|
|||
|
|
) : (
|
|||
|
|
<div className="overflow-x-auto">
|
|||
|
|
<table className="w-full text-sm">
|
|||
|
|
<thead>
|
|||
|
|
<tr className="bg-[#0a1628] text-gray-400">
|
|||
|
|
<th className="p-4 text-left font-medium">访客</th>
|
|||
|
|
<th className="p-4 text-left font-medium">分销商</th>
|
|||
|
|
<th className="p-4 text-left font-medium">绑定时间</th>
|
|||
|
|
<th className="p-4 text-left font-medium">到期时间</th>
|
|||
|
|
<th className="p-4 text-left font-medium">状态</th>
|
|||
|
|
<th className="p-4 text-left font-medium">佣金</th>
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody className="divide-y divide-gray-700/50">
|
|||
|
|
{filteredBindings.map(binding => (
|
|||
|
|
<tr key={binding.id} className="hover:bg-[#0a1628] transition-colors">
|
|||
|
|
<td className="p-4">
|
|||
|
|
<div>
|
|||
|
|
<p className="text-white font-medium">{binding.referee_nickname || '匿名用户'}</p>
|
|||
|
|
<p className="text-gray-500 text-xs">{binding.referee_phone}</p>
|
|||
|
|
</div>
|
|||
|
|
</td>
|
|||
|
|
<td className="p-4">
|
|||
|
|
<div>
|
|||
|
|
<p className="text-white">{binding.referrer_name || '-'}</p>
|
|||
|
|
<p className="text-gray-500 text-xs font-mono">{binding.referrer_code}</p>
|
|||
|
|
</div>
|
|||
|
|
</td>
|
|||
|
|
<td className="p-4 text-gray-400">
|
|||
|
|
{binding.bound_at ? new Date(binding.bound_at).toLocaleDateString('zh-CN') : '-'}
|
|||
|
|
</td>
|
|||
|
|
<td className="p-4 text-gray-400">
|
|||
|
|
{binding.expires_at ? new Date(binding.expires_at).toLocaleDateString('zh-CN') : '-'}
|
|||
|
|
</td>
|
|||
|
|
<td className="p-4">{getStatusBadge(binding.status)}</td>
|
|||
|
|
<td className="p-4">
|
|||
|
|
{binding.commission ? (
|
|||
|
|
<span className="text-[#38bdac] font-medium">¥{binding.commission.toFixed(2)}</span>
|
|||
|
|
) : (
|
|||
|
|
<span className="text-gray-500">-</span>
|
|||
|
|
)}
|
|||
|
|
</td>
|
|||
|
|
</tr>
|
|||
|
|
))}
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* 提现审核 */}
|
|||
|
|
{activeTab === 'withdrawals' && (
|
|||
|
|
<div className="space-y-4">
|
|||
|
|
<div className="flex gap-4">
|
|||
|
|
<div className="relative flex-1">
|
|||
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
|||
|
|
<Input
|
|||
|
|
value={searchTerm}
|
|||
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|||
|
|
placeholder="搜索用户名称、账号..."
|
|||
|
|
className="pl-10 bg-[#0f2137] border-gray-700 text-white"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<select
|
|||
|
|
value={statusFilter}
|
|||
|
|
onChange={(e) => setStatusFilter(e.target.value)}
|
|||
|
|
className="px-4 py-2 bg-[#0f2137] border border-gray-700 rounded-lg text-white"
|
|||
|
|
>
|
|||
|
|
<option value="all">全部状态</option>
|
|||
|
|
<option value="pending">待审核</option>
|
|||
|
|
<option value="completed">已完成</option>
|
|||
|
|
<option value="rejected">已拒绝</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<Card className="bg-[#0f2137] border-gray-700/50">
|
|||
|
|
<CardContent className="p-0">
|
|||
|
|
{filteredWithdrawals.length === 0 ? (
|
|||
|
|
<div className="py-12 text-center text-gray-500">暂无提现记录</div>
|
|||
|
|
) : (
|
|||
|
|
<div className="overflow-x-auto">
|
|||
|
|
<table className="w-full text-sm">
|
|||
|
|
<thead>
|
|||
|
|
<tr className="bg-[#0a1628] text-gray-400">
|
|||
|
|
<th className="p-4 text-left font-medium">申请人</th>
|
|||
|
|
<th className="p-4 text-left font-medium">金额</th>
|
|||
|
|
<th className="p-4 text-left font-medium">收款方式</th>
|
|||
|
|
<th className="p-4 text-left font-medium">收款账号</th>
|
|||
|
|
<th className="p-4 text-left font-medium">申请时间</th>
|
|||
|
|
<th className="p-4 text-left font-medium">状态</th>
|
|||
|
|
<th className="p-4 text-right font-medium">操作</th>
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody className="divide-y divide-gray-700/50">
|
|||
|
|
{filteredWithdrawals.map(withdrawal => (
|
|||
|
|
<tr key={withdrawal.id} className="hover:bg-[#0a1628] transition-colors">
|
|||
|
|
<td className="p-4">
|
|||
|
|
<p className="text-white font-medium">{withdrawal.user_name || withdrawal.name}</p>
|
|||
|
|
</td>
|
|||
|
|
<td className="p-4">
|
|||
|
|
<span className="text-[#38bdac] font-bold">¥{withdrawal.amount.toFixed(2)}</span>
|
|||
|
|
</td>
|
|||
|
|
<td className="p-4">
|
|||
|
|
<Badge className={
|
|||
|
|
withdrawal.method === 'wechat'
|
|||
|
|
? 'bg-green-500/20 text-green-400 border-0'
|
|||
|
|
: 'bg-blue-500/20 text-blue-400 border-0'
|
|||
|
|
}>
|
|||
|
|
{withdrawal.method === 'wechat' ? '微信' : '支付宝'}
|
|||
|
|
</Badge>
|
|||
|
|
</td>
|
|||
|
|
<td className="p-4">
|
|||
|
|
<div>
|
|||
|
|
<p className="text-white font-mono text-xs">{withdrawal.account}</p>
|
|||
|
|
<p className="text-gray-500 text-xs">{withdrawal.name}</p>
|
|||
|
|
</div>
|
|||
|
|
</td>
|
|||
|
|
<td className="p-4 text-gray-400">
|
|||
|
|
{withdrawal.created_at ? new Date(withdrawal.created_at).toLocaleString('zh-CN') : '-'}
|
|||
|
|
</td>
|
|||
|
|
<td className="p-4">{getStatusBadge(withdrawal.status)}</td>
|
|||
|
|
<td className="p-4 text-right">
|
|||
|
|
{withdrawal.status === 'pending' && (
|
|||
|
|
<div className="flex gap-2 justify-end">
|
|||
|
|
<Button
|
|||
|
|
size="sm"
|
|||
|
|
onClick={() => handleApproveWithdrawal(withdrawal.id)}
|
|||
|
|
className="bg-[#38bdac] hover:bg-[#2da396] text-white"
|
|||
|
|
>
|
|||
|
|
<CheckCircle className="w-4 h-4 mr-1" />
|
|||
|
|
通过
|
|||
|
|
</Button>
|
|||
|
|
<Button
|
|||
|
|
size="sm"
|
|||
|
|
variant="outline"
|
|||
|
|
onClick={() => handleRejectWithdrawal(withdrawal.id)}
|
|||
|
|
className="border-red-500/50 text-red-400 hover:bg-red-500/20"
|
|||
|
|
>
|
|||
|
|
<XCircle className="w-4 h-4 mr-1" />
|
|||
|
|
拒绝
|
|||
|
|
</Button>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</td>
|
|||
|
|
</tr>
|
|||
|
|
))}
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* 分销商管理 */}
|
|||
|
|
{activeTab === 'distributors' && (
|
|||
|
|
<div className="space-y-4">
|
|||
|
|
<div className="flex gap-4">
|
|||
|
|
<div className="relative flex-1">
|
|||
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
|||
|
|
<Input
|
|||
|
|
value={searchTerm}
|
|||
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|||
|
|
placeholder="搜索分销商名称、手机号、推广码..."
|
|||
|
|
className="pl-10 bg-[#0f2137] border-gray-700 text-white"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<Card className="bg-[#0f2137] border-gray-700/50">
|
|||
|
|
<CardContent className="p-0">
|
|||
|
|
{filteredDistributors.length === 0 ? (
|
|||
|
|
<div className="py-12 text-center text-gray-500">暂无分销商数据</div>
|
|||
|
|
) : (
|
|||
|
|
<div className="overflow-x-auto">
|
|||
|
|
<table className="w-full text-sm">
|
|||
|
|
<thead>
|
|||
|
|
<tr className="bg-[#0a1628] text-gray-400">
|
|||
|
|
<th className="p-4 text-left font-medium">分销商</th>
|
|||
|
|
<th className="p-4 text-left font-medium">推广码</th>
|
|||
|
|
<th className="p-4 text-left font-medium">推荐人数</th>
|
|||
|
|
<th className="p-4 text-left font-medium">总收益</th>
|
|||
|
|
<th className="p-4 text-left font-medium">可提现</th>
|
|||
|
|
<th className="p-4 text-left font-medium">已提现</th>
|
|||
|
|
<th className="p-4 text-left font-medium">注册时间</th>
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody className="divide-y divide-gray-700/50">
|
|||
|
|
{filteredDistributors.map(distributor => (
|
|||
|
|
<tr key={distributor.id} className="hover:bg-[#0a1628] transition-colors">
|
|||
|
|
<td className="p-4">
|
|||
|
|
<div>
|
|||
|
|
<p className="text-white font-medium">{distributor.nickname}</p>
|
|||
|
|
<p className="text-gray-500 text-xs">{distributor.phone}</p>
|
|||
|
|
</div>
|
|||
|
|
</td>
|
|||
|
|
<td className="p-4">
|
|||
|
|
<span className="text-[#38bdac] font-mono text-sm">{distributor.referral_code}</span>
|
|||
|
|
</td>
|
|||
|
|
<td className="p-4">
|
|||
|
|
<span className="text-white">{distributor.referral_count || 0}</span>
|
|||
|
|
</td>
|
|||
|
|
<td className="p-4">
|
|||
|
|
<span className="text-[#38bdac] font-bold">¥{(distributor.earnings || 0).toFixed(2)}</span>
|
|||
|
|
</td>
|
|||
|
|
<td className="p-4">
|
|||
|
|
<span className="text-white">¥{(distributor.pending_earnings || 0).toFixed(2)}</span>
|
|||
|
|
</td>
|
|||
|
|
<td className="p-4">
|
|||
|
|
<span className="text-gray-400">¥{(distributor.withdrawn_earnings || 0).toFixed(2)}</span>
|
|||
|
|
</td>
|
|||
|
|
<td className="p-4 text-gray-400">
|
|||
|
|
{distributor.created_at ? new Date(distributor.created_at).toLocaleDateString('zh-CN') : '-'}
|
|||
|
|
</td>
|
|||
|
|
</tr>
|
|||
|
|
))}
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
}
|