Files
soul/app/admin/withdrawals/page.tsx

306 lines
12 KiB
TypeScript
Raw Normal View History

"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
}
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) => {
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-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">
<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.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">
{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>
)
}