功能迭代:用户管理与存客宝同步、管理后台与小程序优化、开发文档更新

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
卡若
2026-01-29 17:15:00 +08:00
parent 8f01de4f9a
commit d87fa5c175
18 changed files with 2693 additions and 149 deletions

View File

@@ -75,12 +75,30 @@ interface User {
created_at: string
}
// 订单类型(用于交易中心的订单管理标签)
interface Order {
id: string
userId: string
userNickname?: string
userPhone?: string
type: 'section' | 'fullbook' | 'match'
sectionId?: string
sectionTitle?: string
amount: number
status: 'pending' | 'completed' | 'failed'
paymentMethod?: string
referrerEarnings?: number
createdAt: string
}
export default function DistributionAdminPage() {
const [activeTab, setActiveTab] = useState<'overview' | 'bindings' | 'withdrawals' | 'distributors'>('overview')
// 标签页:数据概览、订单管理、绑定管理、提现审核
const [activeTab, setActiveTab] = useState<'overview' | 'orders' | 'bindings' | 'withdrawals'>('overview')
const [orders, setOrders] = useState<Order[]>([])
const [overview, setOverview] = useState<DistributionOverview | null>(null)
const [bindings, setBindings] = useState<Binding[]>([])
const [withdrawals, setWithdrawals] = useState<Withdrawal[]>([])
const [distributors, setDistributors] = useState<User[]>([])
const [users, setUsers] = useState<User[]>([])
const [loading, setLoading] = useState(true)
const [searchTerm, setSearchTerm] = useState('')
const [statusFilter, setStatusFilter] = useState<string>('all')
@@ -93,11 +111,27 @@ export default function DistributionAdminPage() {
setLoading(true)
try {
// 加载用户数据(分销商)
// 加载用户数据
const usersRes = await fetch('/api/db/users')
const usersData = await usersRes.json()
const users = usersData.users || []
setDistributors(users)
const usersArr = usersData.users || []
setUsers(usersArr)
// 加载订单数据
const ordersRes = await fetch('/api/orders')
const ordersData = await ordersRes.json()
if (ordersData.success && ordersData.orders) {
// 补充用户信息
const enrichedOrders = ordersData.orders.map((order: Order) => {
const user = usersArr.find((u: User) => u.id === order.userId)
return {
...order,
userNickname: user?.nickname || '未知用户',
userPhone: user?.phone || '-'
}
})
setOrders(enrichedOrders)
}
// 加载绑定数据
const bindingsRes = await fetch('/api/db/distribution')
@@ -139,7 +173,7 @@ export default function DistributionAdminPage() {
).length
// 计算佣金
const totalEarnings = users.reduce((sum: number, u: User) => sum + (u.earnings || 0), 0)
const totalEarnings = usersArr.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)
@@ -171,8 +205,8 @@ export default function DistributionAdminPage() {
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,
totalDistributors: usersArr.filter((u: User) => u.referral_code).length,
activeDistributors: usersArr.filter((u: User) => (u.earnings || 0) > 0).length,
})
} catch (error) {
console.error('Load distribution data error:', error)
@@ -292,26 +326,14 @@ export default function DistributionAdminPage() {
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>
<h1 className="text-2xl font-bold text-white"></h1>
<p className="text-gray-400 mt-1"></p>
</div>
<Button
onClick={loadData}
@@ -324,13 +346,13 @@ export default function DistributionAdminPage() {
</Button>
</div>
{/* Tab切换 */}
{/* Tab切换 - 交易中心:合并分销+订单+提现 */}
<div className="flex gap-2 mb-6 border-b border-gray-700 pb-4">
{[
{ key: 'overview', label: '数据概览', icon: TrendingUp },
{ key: 'orders', label: '订单管理', icon: DollarSign },
{ key: 'bindings', label: '绑定管理', icon: Link2 },
{ key: 'withdrawals', label: '提现审核', icon: Wallet },
{ key: 'distributors', label: '分销商', icon: Users },
].map(tab => (
<button
key={tab.key}
@@ -525,23 +547,23 @@ export default function DistributionAdminPage() {
</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>
<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>
<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>
@@ -557,6 +579,123 @@ export default function DistributionAdminPage() {
</div>
)}
{/* 订单管理 - 新增标签页 */}
{activeTab === 'orders' && (
<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="completed"></option>
<option value="pending"></option>
<option value="failed"></option>
</select>
</div>
<Card className="bg-[#0f2137] border-gray-700/50">
<CardContent className="p-0">
{orders.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>
<th className="p-4 text-left font-medium"></th>
</tr>
</thead>
<tbody className="divide-y divide-gray-700/50">
{orders
.filter(order => {
if (statusFilter !== 'all' && order.status !== statusFilter) return false
if (searchTerm) {
const term = searchTerm.toLowerCase()
return (
order.id?.toLowerCase().includes(term) ||
order.userNickname?.toLowerCase().includes(term) ||
order.userPhone?.includes(term) ||
order.sectionTitle?.toLowerCase().includes(term)
)
}
return true
})
.map(order => (
<tr key={order.id} className="hover:bg-[#0a1628] transition-colors">
<td className="p-4 font-mono text-xs text-gray-400">
{order.id?.slice(0, 12)}...
</td>
<td className="p-4">
<div>
<p className="text-white text-sm">{order.userNickname}</p>
<p className="text-gray-500 text-xs">{order.userPhone}</p>
</div>
</td>
<td className="p-4">
<div>
<p className="text-white text-sm">
{order.type === 'fullbook' ? '整本购买' :
order.type === 'match' ? '匹配次数' :
order.sectionTitle || `章节${order.sectionId}`}
</p>
<p className="text-gray-500 text-xs">
{order.type === 'fullbook' ? '全书' :
order.type === 'match' ? '功能' : '单章'}
</p>
</div>
</td>
<td className="p-4 text-[#38bdac] font-bold">
¥{(order.amount || 0).toFixed(2)}
</td>
<td className="p-4 text-gray-300">
{order.paymentMethod === 'wechat' ? '微信支付' :
order.paymentMethod === 'alipay' ? '支付宝' :
order.paymentMethod || '微信支付'}
</td>
<td className="p-4">
{order.status === 'completed' ? (
<Badge className="bg-green-500/20 text-green-400 border-0"></Badge>
) : order.status === 'pending' ? (
<Badge className="bg-yellow-500/20 text-yellow-400 border-0"></Badge>
) : (
<Badge className="bg-red-500/20 text-red-400 border-0"></Badge>
)}
</td>
<td className="p-4 text-[#FFD700]">
{order.referrerEarnings ? `¥${order.referrerEarnings.toFixed(2)}` : '-'}
</td>
<td className="p-4 text-gray-400 text-sm">
{order.createdAt ? new Date(order.createdAt).toLocaleString('zh-CN') : '-'}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</CardContent>
</Card>
</div>
)}
{/* 绑定管理 */}
{activeTab === 'bindings' && (
<div className="space-y-4">
@@ -744,76 +883,6 @@ export default function DistributionAdminPage() {
</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>