feat: 完善后台管理+搜索功能+分销系统
主要更新: - 后台菜单精简(9项→6项) - 新增搜索功能(敏感信息过滤) - 分销绑定和提现系统完善 - 数据库初始化API(自动修复表结构) - 用户管理:显示绑定关系详情 - 小程序:上下章导航优化、匹配页面重构 - 修复hydration和数据类型问题
This commit is contained in:
@@ -1,172 +1,305 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { useStore } from "@/lib/store"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Check, Clock, Wallet, History } from "lucide-react"
|
||||
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, completeWithdrawal } = useStore()
|
||||
const [mounted, setMounted] = useState(false)
|
||||
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(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
loadWithdrawals()
|
||||
}, [filter])
|
||||
|
||||
if (!mounted) return null
|
||||
// 批准提现
|
||||
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 pendingWithdrawals = withdrawals?.filter((w) => w.status === "pending") || []
|
||||
const historyWithdrawals =
|
||||
withdrawals
|
||||
?.filter((w) => w.status !== "pending")
|
||||
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) || []
|
||||
// 拒绝提现
|
||||
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 totalPending = pendingWithdrawals.reduce((sum, w) => sum + w.amount, 0)
|
||||
|
||||
const handleApprove = (id: string) => {
|
||||
if (confirm("确认打款并完成此提现申请吗?")) {
|
||||
completeWithdrawal(id)
|
||||
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="mb-8">
|
||||
<h1 className="text-2xl font-bold text-white">提现管理</h1>
|
||||
<p className="text-gray-400 mt-1">
|
||||
待处理 {pendingWithdrawals.length} 笔,共 ¥{totalPending.toFixed(2)}
|
||||
</p>
|
||||
<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>
|
||||
|
||||
<div className="grid gap-6">
|
||||
{/* 待处理申请 */}
|
||||
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-white">
|
||||
<div className="p-2 rounded-lg bg-orange-500/20">
|
||||
<Clock className="w-5 h-5 text-orange-400" />
|
||||
{/* 分账规则说明 */}
|
||||
<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>
|
||||
待处理申请 ({pendingWithdrawals.length})
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{pendingWithdrawals.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">
|
||||
{pendingWithdrawals.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>
|
||||
<p className="font-medium text-white">{w.name}</p>
|
||||
<p className="text-xs text-gray-500 font-mono">{w.userId.slice(0, 8)}...</p>
|
||||
</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>
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<Badge
|
||||
className={
|
||||
w.method === "wechat"
|
||||
? "bg-green-500/20 text-green-400 hover:bg-green-500/20 border-0"
|
||||
: "bg-blue-500/20 text-blue-400 hover:bg-blue-500/20 border-0"
|
||||
}
|
||||
>
|
||||
{w.method === "wechat" ? "微信" : "支付宝"}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="p-4 font-mono text-gray-300">{w.account}</td>
|
||||
<td className="p-4">
|
||||
<span className="font-bold text-orange-400">¥{w.amount.toFixed(2)}</span>
|
||||
</td>
|
||||
<td className="p-4 text-right">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => handleApprove(w.id)}
|
||||
className="bg-[#38bdac] hover:bg-[#2da396] text-white"
|
||||
>
|
||||
<Check className="w-4 h-4 mr-1" />
|
||||
确认打款
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 处理历史 */}
|
||||
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-white">
|
||||
<div className="p-2 rounded-lg bg-gray-700/50">
|
||||
<History className="w-5 h-5 text-gray-400" />
|
||||
</div>
|
||||
处理历史
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{historyWithdrawals.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<History 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>
|
||||
<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>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-700/50">
|
||||
{historyWithdrawals.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 text-gray-400">
|
||||
{w.completedAt ? new Date(w.completedAt).toLocaleString() : "-"}
|
||||
</td>
|
||||
<td className="p-4 font-medium text-white">{w.name}</td>
|
||||
<td className="p-4 text-gray-300">{w.method === "wechat" ? "微信" : "支付宝"}</td>
|
||||
<td className="p-4 font-medium text-white">¥{w.amount.toFixed(2)}</td>
|
||||
<td className="p-4">
|
||||
<Badge className="bg-green-500/20 text-green-400 hover:bg-green-500/20 border-0">
|
||||
已完成
|
||||
</Badge>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user