Files
soul/app/admin/withdrawals/page.tsx
卡若 4dd2f9f4a7 feat: 完善后台管理+搜索功能+分销系统
主要更新:
- 后台菜单精简(9项→6项)
- 新增搜索功能(敏感信息过滤)
- 分销绑定和提现系统完善
- 数据库初始化API(自动修复表结构)
- 用户管理:显示绑定关系详情
- 小程序:上下章导航优化、匹配页面重构
- 修复hydration和数据类型问题
2026-01-25 19:37:59 +08:00

306 lines
12 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
}
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>
)
}