Files
soul-yongping/next-project/app/admin/withdrawals/page.tsx
2026-02-09 14:43:35 +08:00

355 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { useState, useEffect } from "react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Check, X, Clock, Wallet, History, RefreshCw, AlertCircle, DollarSign } from "lucide-react"
interface Withdrawal {
id: string
userId: string
userNickname: string
userPhone?: string
userAvatar?: string
referralCode?: string
amount: number
status: 'pending' | 'processing' | 'success' | 'failed'
wechatOpenid?: string
transactionId?: string
errorMessage?: string
createdAt: string
processedAt?: string
userCommissionInfo?: {
totalCommission: number
withdrawnEarnings: number
pendingWithdrawals: number
availableAfterThis: number
}
}
interface Stats {
total: number
pendingCount: number
pendingAmount: number
successCount: number
successAmount: number
failedCount: number
}
export default function WithdrawalsPage() {
const [withdrawals, setWithdrawals] = useState<Withdrawal[]>([])
const [stats, setStats] = useState<Stats>({ total: 0, pendingCount: 0, pendingAmount: 0, successCount: 0, successAmount: 0, failedCount: 0 })
const [loading, setLoading] = useState(true)
const [filter, setFilter] = useState<'all' | 'pending' | 'success' | 'failed'>('all')
const [processing, setProcessing] = useState<string | null>(null)
// 加载提现记录
const loadWithdrawals = async () => {
setLoading(true)
try {
const res = await fetch(`/api/admin/withdrawals?status=${filter}`)
const data = await res.json()
if (data.success) {
setWithdrawals(data.withdrawals || [])
setStats(data.stats || {})
}
} catch (error) {
console.error('Load withdrawals error:', error)
} finally {
setLoading(false)
}
}
useEffect(() => {
loadWithdrawals()
}, [filter])
// 批准提现
const handleApprove = async (id: string) => {
// 检查是否存在超额提现风险
const withdrawal = withdrawals.find(w => w.id === id)
if (withdrawal?.userCommissionInfo && withdrawal.userCommissionInfo.availableAfterThis < 0) {
if (!confirm(`⚠️ 风险警告:该用户审核后余额为负数(¥${withdrawal.userCommissionInfo.availableAfterThis.toFixed(2)}),可能存在超额提现。\n\n确认已核实用户账户并完成打款`)) {
return
}
} else {
if (!confirm("确认已完成打款?批准后将更新用户提现记录。")) return
}
setProcessing(id)
try {
const res = await fetch('/api/admin/withdrawals', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, action: 'approve' })
})
const data = await res.json()
if (data.success) {
loadWithdrawals()
} else {
alert('操作失败: ' + data.error)
}
} catch (error) {
alert('操作失败')
} finally {
setProcessing(null)
}
}
// 拒绝提现
const handleReject = async (id: string) => {
const reason = prompt("请输入拒绝原因(将返还用户余额):")
if (!reason) return
setProcessing(id)
try {
const res = await fetch('/api/admin/withdrawals', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, action: 'reject', reason })
})
const data = await res.json()
if (data.success) {
loadWithdrawals()
} else {
alert('操作失败: ' + data.error)
}
} catch (error) {
alert('操作失败')
} finally {
setProcessing(null)
}
}
const getStatusBadge = (status: string) => {
switch (status) {
case 'pending':
return <Badge className="bg-orange-500/20 text-orange-400 hover:bg-orange-500/20 border-0"></Badge>
case 'processing':
return <Badge className="bg-blue-500/20 text-blue-400 hover:bg-blue-500/20 border-0"></Badge>
case 'success':
return <Badge className="bg-green-500/20 text-green-400 hover:bg-green-500/20 border-0"></Badge>
case 'failed':
return <Badge className="bg-red-500/20 text-red-400 hover:bg-red-500/20 border-0"></Badge>
default:
return <Badge className="bg-gray-500/20 text-gray-400 border-0">{status}</Badge>
}
}
return (
<div className="p-8 max-w-6xl mx-auto">
<div className="flex justify-between items-start mb-8">
<div>
<h1 className="text-2xl font-bold text-white"></h1>
<p className="text-gray-400 mt-1">
</p>
</div>
<Button
variant="outline"
onClick={loadWithdrawals}
disabled={loading}
className="border-gray-600 text-gray-300 hover:bg-gray-700/50 bg-transparent"
>
<RefreshCw className={`w-4 h-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
</Button>
</div>
{/* 分账规则说明 */}
<Card className="bg-gradient-to-r from-[#38bdac]/10 to-[#0f2137] border-[#38bdac]/30 mb-6">
<CardContent className="p-4">
<div className="flex items-start gap-3">
<DollarSign className="w-5 h-5 text-[#38bdac] mt-0.5" />
<div>
<h3 className="text-white font-medium mb-2"></h3>
<div className="text-sm text-gray-400 space-y-1">
<p> <span className="text-[#38bdac]"></span>广 <span className="text-white font-medium">90%</span></p>
<p> <span className="text-[#38bdac]"></span>广</p>
<p> <span className="text-[#38bdac]"></span></p>
<p> <span className="text-[#38bdac]"></span></p>
</div>
</div>
</div>
</CardContent>
</Card>
{/* 统计卡片 */}
<div className="grid grid-cols-4 gap-4 mb-6">
<Card className="bg-[#0f2137] border-gray-700/50">
<CardContent className="p-4 text-center">
<div className="text-3xl font-bold text-[#38bdac]">{stats.total}</div>
<div className="text-sm text-gray-400"></div>
</CardContent>
</Card>
<Card className="bg-[#0f2137] border-gray-700/50">
<CardContent className="p-4 text-center">
<div className="text-3xl font-bold text-orange-400">{stats.pendingCount}</div>
<div className="text-sm text-gray-400"></div>
<div className="text-xs text-orange-400 mt-1">¥{stats.pendingAmount.toFixed(2)}</div>
</CardContent>
</Card>
<Card className="bg-[#0f2137] border-gray-700/50">
<CardContent className="p-4 text-center">
<div className="text-3xl font-bold text-green-400">{stats.successCount}</div>
<div className="text-sm text-gray-400"></div>
<div className="text-xs text-green-400 mt-1">¥{stats.successAmount.toFixed(2)}</div>
</CardContent>
</Card>
<Card className="bg-[#0f2137] border-gray-700/50">
<CardContent className="p-4 text-center">
<div className="text-3xl font-bold text-red-400">{stats.failedCount}</div>
<div className="text-sm text-gray-400"></div>
</CardContent>
</Card>
</div>
{/* 筛选按钮 */}
<div className="flex gap-2 mb-4">
{(['all', 'pending', 'success', 'failed'] as const).map((f) => (
<Button
key={f}
variant={filter === f ? "default" : "outline"}
size="sm"
onClick={() => setFilter(f)}
className={filter === f
? "bg-[#38bdac] hover:bg-[#2da396] text-white"
: "border-gray-600 text-gray-300 hover:bg-gray-700/50 bg-transparent"
}
>
{f === 'all' ? '全部' : f === 'pending' ? '待处理' : f === 'success' ? '已完成' : '已拒绝'}
</Button>
))}
</div>
{/* 提现记录表格 */}
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
<CardContent className="p-0">
{loading ? (
<div className="flex items-center justify-center py-12">
<RefreshCw className="w-6 h-6 text-[#38bdac] animate-spin" />
<span className="ml-2 text-gray-400">...</span>
</div>
) : withdrawals.length === 0 ? (
<div className="text-center py-12">
<Wallet className="w-12 h-12 text-gray-600 mx-auto mb-3" />
<p className="text-gray-500"></p>
</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">
{withdrawals.map((w) => (
<tr key={w.id} className="hover:bg-[#0a1628] transition-colors">
<td className="p-4 text-gray-400">
{new Date(w.createdAt).toLocaleString()}
</td>
<td className="p-4">
<div className="flex items-center gap-2">
{w.userAvatar ? (
<img
src={w.userAvatar}
alt={w.userNickname}
className="w-8 h-8 rounded-full object-cover"
/>
) : (
<div className="w-8 h-8 rounded-full bg-[#38bdac]/20 flex items-center justify-center text-sm text-[#38bdac]">
{w.userNickname?.charAt(0) || "?"}
</div>
)}
<div>
<p className="font-medium text-white">{w.userNickname}</p>
<p className="text-xs text-gray-500">{w.userPhone || w.referralCode || w.userId.slice(0, 10)}</p>
</div>
</div>
</td>
<td className="p-4">
<span className="font-bold text-orange-400">¥{w.amount.toFixed(2)}</span>
</td>
<td className="p-4">
{w.userCommissionInfo ? (
<div className="text-xs space-y-1">
<div className="flex justify-between gap-4">
<span className="text-gray-500">:</span>
<span className="text-[#38bdac] font-medium">¥{w.userCommissionInfo.totalCommission.toFixed(2)}</span>
</div>
<div className="flex justify-between gap-4">
<span className="text-gray-500">:</span>
<span className="text-gray-400">¥{w.userCommissionInfo.withdrawnEarnings.toFixed(2)}</span>
</div>
<div className="flex justify-between gap-4">
<span className="text-gray-500">:</span>
<span className="text-orange-400">¥{w.userCommissionInfo.pendingWithdrawals.toFixed(2)}</span>
</div>
<div className="flex justify-between gap-4 pt-1 border-t border-gray-700/30">
<span className="text-gray-500">:</span>
<span className={w.userCommissionInfo.availableAfterThis >= 0 ? "text-green-400 font-medium" : "text-red-400 font-medium"}>
¥{w.userCommissionInfo.availableAfterThis.toFixed(2)}
</span>
</div>
</div>
) : (
<span className="text-gray-500 text-xs"></span>
)}
</td>
<td className="p-4">
{getStatusBadge(w.status)}
{w.errorMessage && (
<p className="text-xs text-red-400 mt-1">{w.errorMessage}</p>
)}
</td>
<td className="p-4 text-gray-400">
{w.processedAt ? new Date(w.processedAt).toLocaleString() : '-'}
</td>
<td className="p-4 text-right">
{w.status === 'pending' && (
<div className="flex items-center justify-end gap-2">
<Button
size="sm"
onClick={() => handleApprove(w.id)}
disabled={processing === w.id}
className="bg-green-600 hover:bg-green-700 text-white"
>
<Check className="w-4 h-4 mr-1" />
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleReject(w.id)}
disabled={processing === w.id}
className="border-red-500/50 text-red-400 hover:bg-red-500/10 bg-transparent"
>
<X className="w-4 h-4 mr-1" />
</Button>
</div>
)}
{w.status === 'success' && w.transactionId && (
<span className="text-xs text-gray-500 font-mono">{w.transactionId}</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</CardContent>
</Card>
</div>
)
}