1234 lines
54 KiB
TypeScript
1234 lines
54 KiB
TypeScript
import { useState, useEffect } from 'react'
|
||
import {
|
||
Users,
|
||
TrendingUp,
|
||
Clock,
|
||
Wallet,
|
||
Search,
|
||
RefreshCw,
|
||
CheckCircle,
|
||
XCircle,
|
||
Calendar,
|
||
DollarSign,
|
||
Link2,
|
||
Eye,
|
||
Undo2,
|
||
Settings,
|
||
} from 'lucide-react'
|
||
import { ReferralSettingsPage } from '@/pages/referral-settings/ReferralSettingsPage'
|
||
import { Pagination } from '@/components/ui/Pagination'
|
||
import { Button } from '@/components/ui/button'
|
||
import { Input } from '@/components/ui/input'
|
||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||
import { Badge } from '@/components/ui/badge'
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogHeader,
|
||
DialogFooter,
|
||
DialogTitle,
|
||
} from '@/components/ui/dialog'
|
||
import { get, put } from '@/api/client'
|
||
|
||
interface TodayClicksByPageItem {
|
||
page: string
|
||
clicks: number
|
||
}
|
||
|
||
interface DistributionOverview {
|
||
todayClicks: number
|
||
todayBindings: number
|
||
todayConversions: number
|
||
todayEarnings: number
|
||
monthClicks: number
|
||
monthBindings: number
|
||
monthConversions: number
|
||
monthEarnings: number
|
||
totalClicks: number
|
||
totalBindings: number
|
||
totalConversions: number
|
||
totalEarnings: number
|
||
expiringBindings: number
|
||
pendingWithdrawals: number
|
||
pendingWithdrawAmount: number
|
||
conversionRate: string
|
||
totalDistributors: number
|
||
activeDistributors: number
|
||
todayUniqueVisitors?: number
|
||
todayClickRate?: number
|
||
todayClicksByPage?: TodayClicksByPageItem[]
|
||
}
|
||
|
||
interface Binding {
|
||
id: string
|
||
referrerId: string
|
||
referrerName?: string
|
||
referrerCode: string
|
||
refereeId: string
|
||
refereePhone?: string
|
||
refereeNickname?: string
|
||
boundAt: string
|
||
expiresAt: string
|
||
status: 'active' | 'converted' | 'expired' | 'cancelled'
|
||
commission?: number
|
||
}
|
||
|
||
interface Withdrawal {
|
||
id: string
|
||
userId?: string
|
||
userName?: string
|
||
userPhone?: string
|
||
userAvatar?: string
|
||
amount: number
|
||
method?: 'wechat' | 'alipay'
|
||
account?: string
|
||
name?: string
|
||
status: string
|
||
createdAt?: string
|
||
processedAt?: string
|
||
}
|
||
|
||
interface User {
|
||
id: string
|
||
nickname: string
|
||
phone: string
|
||
referralCode?: string
|
||
}
|
||
|
||
interface Order {
|
||
id: string
|
||
userId: string
|
||
userNickname?: string
|
||
userPhone?: string
|
||
productType?: string
|
||
type?: string
|
||
productId?: string
|
||
sectionId?: string
|
||
bookName?: string
|
||
chapterTitle?: string
|
||
sectionTitle?: string
|
||
amount: number
|
||
status: string
|
||
paymentMethod?: string
|
||
referrerEarnings?: number
|
||
referrerId?: string | null
|
||
referrerNickname?: string | null
|
||
referrerCode?: string | null
|
||
referralCode?: string | null
|
||
orderSn?: string
|
||
refundReason?: string
|
||
createdAt: string
|
||
}
|
||
|
||
export function DistributionPage() {
|
||
const [activeTab, setActiveTab] = useState<'overview' | 'orders' | 'bindings' | 'withdrawals' | 'settings'>(
|
||
'overview',
|
||
)
|
||
const [orders, setOrders] = useState<Order[]>([])
|
||
const [overview, setOverview] = useState<DistributionOverview | null>(null)
|
||
const [bindings, setBindings] = useState<Binding[]>([])
|
||
const [withdrawals, setWithdrawals] = useState<Withdrawal[]>([])
|
||
const [users, setUsers] = useState<User[]>([])
|
||
const [loading, setLoading] = useState(true)
|
||
const [error, setError] = useState<string | null>(null)
|
||
const [searchTerm, setSearchTerm] = useState('')
|
||
const [statusFilter, setStatusFilter] = useState<string>('all')
|
||
const [page, setPage] = useState(1)
|
||
const [pageSize, setPageSize] = useState(10)
|
||
const [total, setTotal] = useState(0)
|
||
const [loadedTabs, setLoadedTabs] = useState<Set<string>>(new Set())
|
||
const [refundOrder, setRefundOrder] = useState<Order | null>(null)
|
||
const [refundReason, setRefundReason] = useState('')
|
||
const [refundLoading, setRefundLoading] = useState(false)
|
||
|
||
useEffect(() => {
|
||
loadInitialData()
|
||
}, [])
|
||
|
||
useEffect(() => {
|
||
setPage(1)
|
||
}, [activeTab, statusFilter])
|
||
|
||
useEffect(() => {
|
||
loadTabData(activeTab)
|
||
}, [activeTab])
|
||
|
||
useEffect(() => {
|
||
if (['orders', 'bindings', 'withdrawals'].includes(activeTab)) {
|
||
loadTabData(activeTab, true)
|
||
}
|
||
}, [page, pageSize, statusFilter, searchTerm])
|
||
|
||
async function loadInitialData() {
|
||
setError(null)
|
||
try {
|
||
const overviewData = await get<{ success?: boolean; overview?: DistributionOverview }>(
|
||
'/api/admin/distribution/overview',
|
||
)
|
||
if (overviewData?.success && overviewData.overview) setOverview(overviewData.overview)
|
||
} catch (e) {
|
||
console.error('[Admin] 概览接口异常:', e)
|
||
setError('加载概览失败')
|
||
}
|
||
try {
|
||
const usersData = await get<{ success?: boolean; users?: User[] }>('/api/db/users')
|
||
setUsers(usersData?.users || [])
|
||
} catch (e) {
|
||
console.error('[Admin] 用户数据加载失败:', e)
|
||
}
|
||
}
|
||
|
||
async function loadTabData(tab: string, force = false) {
|
||
if (!force && loadedTabs.has(tab)) return
|
||
setLoading(true)
|
||
try {
|
||
const usersArr = users
|
||
switch (tab) {
|
||
case 'overview':
|
||
break
|
||
case 'orders': {
|
||
try {
|
||
const params = new URLSearchParams({
|
||
page: String(page),
|
||
pageSize: String(pageSize),
|
||
...(statusFilter !== 'all' && { status: statusFilter }),
|
||
...(searchTerm && { search: searchTerm }),
|
||
})
|
||
const ordersData = await get<{ success?: boolean; orders?: Order[]; total?: number }>(`/api/orders?${params}`)
|
||
if (ordersData?.success && ordersData.orders) {
|
||
const enriched = ordersData.orders.map((order) => {
|
||
const user = usersArr.find((u) => u.id === order.userId)
|
||
const referrer = order.referrerId
|
||
? usersArr.find((u) => u.id === order.referrerId)
|
||
: null
|
||
return {
|
||
...order,
|
||
amount: parseFloat(String(order.amount)) || 0,
|
||
userNickname: user?.nickname || order.userNickname || '未知用户',
|
||
userPhone: user?.phone || order.userPhone || '-',
|
||
referrerNickname: referrer?.nickname || null,
|
||
referrerCode: referrer?.referralCode ?? null,
|
||
type: order.productType || order.type,
|
||
}
|
||
})
|
||
setOrders(enriched)
|
||
setTotal(ordersData.total ?? enriched.length)
|
||
} else {
|
||
setOrders([])
|
||
setTotal(0)
|
||
}
|
||
} catch (e) {
|
||
console.error(e)
|
||
setError('加载订单失败')
|
||
setOrders([])
|
||
}
|
||
break
|
||
}
|
||
case 'bindings': {
|
||
try {
|
||
const params = new URLSearchParams({
|
||
page: String(page),
|
||
pageSize: String(pageSize),
|
||
...(statusFilter !== 'all' && { status: statusFilter }),
|
||
})
|
||
const bindingsData = await get<{ success?: boolean; bindings?: Binding[]; total?: number }>(
|
||
`/api/db/distribution?${params}`,
|
||
)
|
||
setBindings(bindingsData?.bindings || [])
|
||
setTotal(bindingsData?.total ?? bindingsData?.bindings?.length ?? 0)
|
||
} catch (e) {
|
||
console.error(e)
|
||
setError('加载绑定数据失败')
|
||
setBindings([])
|
||
}
|
||
break
|
||
}
|
||
case 'withdrawals': {
|
||
try {
|
||
const statusParam = statusFilter === 'completed' ? 'success' : statusFilter === 'rejected' ? 'failed' : statusFilter
|
||
const params = new URLSearchParams({
|
||
...(statusParam && statusParam !== 'all' && { status: statusParam }),
|
||
page: String(page),
|
||
pageSize: String(pageSize),
|
||
})
|
||
const withdrawalsData = await get<{
|
||
success?: boolean
|
||
withdrawals?: Withdrawal[]
|
||
total?: number
|
||
error?: string
|
||
}>(`/api/admin/withdrawals?${params}`)
|
||
if (withdrawalsData?.success && withdrawalsData.withdrawals) {
|
||
const formatted = withdrawalsData.withdrawals.map((w) => ({
|
||
...w,
|
||
account: w.account ?? '未绑定微信号',
|
||
status:
|
||
w.status === 'success' ? 'completed' : w.status === 'failed' ? 'rejected' : w.status,
|
||
}))
|
||
setWithdrawals(formatted)
|
||
setTotal(withdrawalsData?.total ?? formatted.length)
|
||
} else {
|
||
if (!withdrawalsData?.success)
|
||
setError(`获取提现记录失败: ${(withdrawalsData as { error?: string })?.error || '未知错误'}`)
|
||
setWithdrawals([])
|
||
}
|
||
} catch (e) {
|
||
console.error(e)
|
||
setError('加载提现数据失败')
|
||
setWithdrawals([])
|
||
}
|
||
break
|
||
}
|
||
}
|
||
setLoadedTabs((prev) => new Set(prev).add(tab))
|
||
} catch (e) {
|
||
console.error(e)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
async function refreshCurrentTab() {
|
||
setError(null)
|
||
setLoadedTabs((prev) => {
|
||
const next = new Set(prev)
|
||
next.delete(activeTab)
|
||
return next
|
||
})
|
||
if (activeTab === 'overview') loadInitialData()
|
||
await loadTabData(activeTab, true)
|
||
}
|
||
|
||
async function handleApproveWithdrawal(id: string) {
|
||
if (!confirm('确认审核通过并打款?')) return
|
||
try {
|
||
const res = await put<{ success?: boolean; error?: string; message?: string }>(
|
||
'/api/admin/withdrawals',
|
||
{ id, action: 'approve' },
|
||
)
|
||
if (!res?.success) {
|
||
const detail = res?.message || res?.error || '操作失败'
|
||
alert(detail)
|
||
return
|
||
}
|
||
await refreshCurrentTab()
|
||
} catch (e) {
|
||
console.error(e)
|
||
alert('操作失败')
|
||
}
|
||
}
|
||
|
||
async function handleRejectWithdrawal(id: string) {
|
||
const reason = prompt('请输入拒绝原因:')
|
||
if (!reason) return
|
||
try {
|
||
const res = await put<{ success?: boolean; error?: string }>(
|
||
'/api/admin/withdrawals',
|
||
{ id, action: 'reject', errorMessage: reason },
|
||
)
|
||
if (!res?.success) {
|
||
alert(res?.error || '操作失败')
|
||
return
|
||
}
|
||
await refreshCurrentTab()
|
||
} catch (e) {
|
||
console.error(e)
|
||
alert('操作失败')
|
||
}
|
||
}
|
||
|
||
async function handleRefund() {
|
||
if (!refundOrder?.orderSn && !refundOrder?.id) return
|
||
setRefundLoading(true)
|
||
setError(null)
|
||
try {
|
||
const res = await put<{ success?: boolean; error?: string }>('/api/admin/orders/refund', {
|
||
orderSn: refundOrder.orderSn || refundOrder.id,
|
||
reason: refundReason || undefined,
|
||
})
|
||
if (res?.success) {
|
||
setRefundOrder(null)
|
||
setRefundReason('')
|
||
await loadTabData('orders', true)
|
||
} else {
|
||
setError(res?.error || '退款失败')
|
||
}
|
||
} catch (e) {
|
||
const err = e as Error & { data?: { error?: string } }
|
||
setError(err?.data?.error || '退款失败,请检查网络后重试')
|
||
} finally {
|
||
setRefundLoading(false)
|
||
}
|
||
}
|
||
|
||
function getStatusBadge(status: string) {
|
||
const styles: Record<string, string> = {
|
||
active: 'bg-green-500/20 text-green-400',
|
||
converted: 'bg-blue-500/20 text-blue-400',
|
||
expired: 'bg-gray-500/20 text-gray-400',
|
||
cancelled: 'bg-red-500/20 text-red-400',
|
||
pending: 'bg-orange-500/20 text-orange-400',
|
||
pending_confirm: 'bg-orange-500/20 text-orange-400',
|
||
processing: 'bg-blue-500/20 text-blue-400',
|
||
completed: 'bg-green-500/20 text-green-400',
|
||
rejected: 'bg-red-500/20 text-red-400',
|
||
}
|
||
const labels: Record<string, string> = {
|
||
active: '有效',
|
||
converted: '已转化',
|
||
expired: '已过期',
|
||
cancelled: '已取消',
|
||
pending: '待审核',
|
||
pending_confirm: '待用户确认',
|
||
processing: '处理中',
|
||
completed: '已完成',
|
||
rejected: '已拒绝',
|
||
}
|
||
return (
|
||
<Badge className={`${styles[status] || 'bg-gray-500/20 text-gray-400'} border-0`}>
|
||
{labels[status] || status}
|
||
</Badge>
|
||
)
|
||
}
|
||
|
||
const totalPages = Math.ceil(total / pageSize) || 1
|
||
const displayOrders = orders
|
||
const displayBindings = bindings.filter((b) => {
|
||
if (!searchTerm) return true
|
||
const term = searchTerm.toLowerCase()
|
||
return (
|
||
b.refereeNickname?.toLowerCase().includes(term) ||
|
||
b.refereePhone?.includes(term) ||
|
||
b.referrerName?.toLowerCase().includes(term) ||
|
||
b.referrerCode?.toLowerCase().includes(term)
|
||
)
|
||
})
|
||
const displayWithdrawals = withdrawals.filter((w) => {
|
||
if (!searchTerm) return true
|
||
const term = searchTerm.toLowerCase()
|
||
return (
|
||
w.userName?.toLowerCase().includes(term) || (w.account && w.account.toLowerCase().includes(term))
|
||
)
|
||
})
|
||
|
||
return (
|
||
<div className="p-8 w-full">
|
||
{error && (
|
||
<div className="mb-4 px-4 py-3 rounded-lg bg-red-500/20 border border-red-500/50 text-red-400 text-sm flex items-center justify-between">
|
||
<span>{error}</span>
|
||
<button type="button" onClick={() => setError(null)} className="hover:text-red-300">
|
||
×
|
||
</button>
|
||
</div>
|
||
)}
|
||
<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>
|
||
</div>
|
||
<Button
|
||
onClick={refreshCurrentTab}
|
||
disabled={loading}
|
||
variant="outline"
|
||
className="border-gray-700 text-gray-300 hover:bg-gray-800"
|
||
>
|
||
<RefreshCw className={`w-4 h-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
|
||
刷新数据
|
||
</Button>
|
||
</div>
|
||
|
||
<div className="flex gap-2 mb-6 border-b border-gray-700 pb-4 flex-wrap">
|
||
{[
|
||
{ key: 'overview', label: '数据概览', icon: TrendingUp },
|
||
{ key: 'orders', label: '订单管理', icon: DollarSign },
|
||
{ key: 'bindings', label: '绑定管理', icon: Link2 },
|
||
{ key: 'withdrawals', label: '提现审核', icon: Wallet },
|
||
{ key: 'settings', label: '推广设置', icon: Settings },
|
||
].map((tab) => (
|
||
<button
|
||
key={tab.key}
|
||
type="button"
|
||
onClick={() => {
|
||
setActiveTab(tab.key as typeof activeTab)
|
||
setStatusFilter('all')
|
||
setSearchTerm('')
|
||
}}
|
||
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||
activeTab === tab.key
|
||
? 'bg-[#38bdac] text-white'
|
||
: 'text-gray-400 hover:text-white hover:bg-gray-800'
|
||
}`}
|
||
>
|
||
<tab.icon className="w-4 h-4" />
|
||
{tab.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{loading ? (
|
||
<div className="flex items-center justify-center py-20">
|
||
<RefreshCw className="w-8 h-8 text-[#38bdac] animate-spin" />
|
||
<span className="ml-2 text-gray-400">加载中...</span>
|
||
</div>
|
||
) : (
|
||
<>
|
||
{activeTab === 'overview' && overview && (
|
||
<div className="space-y-6">
|
||
<div className="grid grid-cols-4 gap-4">
|
||
<Card className="bg-[#0f2137] border-gray-700/50">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-gray-400 text-sm">今日点击</p>
|
||
<p className="text-2xl font-bold text-white mt-1">{overview.todayClicks}</p>
|
||
<p className="text-xs text-gray-500 mt-0.5">总点击次数(实时)</p>
|
||
</div>
|
||
<div className="w-12 h-12 rounded-xl bg-blue-500/20 flex items-center justify-center">
|
||
<Eye className="w-6 h-6 text-blue-400" />
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
<Card className="bg-[#0f2137] border-gray-700/50">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-gray-400 text-sm">今日独立用户</p>
|
||
<p className="text-2xl font-bold text-white mt-1">{overview.todayUniqueVisitors ?? 0}</p>
|
||
<p className="text-xs text-gray-500 mt-0.5">去重访客数(实时)</p>
|
||
</div>
|
||
<div className="w-12 h-12 rounded-xl bg-cyan-500/20 flex items-center justify-center">
|
||
<Users className="w-6 h-6 text-cyan-400" />
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
<Card className="bg-[#0f2137] border-gray-700/50">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-gray-400 text-sm">今日总文章点击率</p>
|
||
<p className="text-2xl font-bold text-white mt-1">
|
||
{(overview.todayClickRate ?? 0).toFixed(2)}
|
||
</p>
|
||
<p className="text-xs text-gray-500 mt-0.5">人均点击(总点击/独立用户)</p>
|
||
</div>
|
||
<div className="w-12 h-12 rounded-xl bg-amber-500/20 flex items-center justify-center">
|
||
<TrendingUp className="w-6 h-6 text-amber-400" />
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
<Card className="bg-[#0f2137] border-gray-700/50">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-gray-400 text-sm">今日绑定</p>
|
||
<p className="text-2xl font-bold text-white mt-1">{overview.todayBindings}</p>
|
||
</div>
|
||
<div className="w-12 h-12 rounded-xl bg-green-500/20 flex items-center justify-center">
|
||
<Link2 className="w-6 h-6 text-green-400" />
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
<Card className="bg-[#0f2137] border-gray-700/50">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-gray-400 text-sm">今日转化</p>
|
||
<p className="text-2xl font-bold text-white mt-1">{overview.todayConversions}</p>
|
||
</div>
|
||
<div className="w-12 h-12 rounded-xl bg-purple-500/20 flex items-center justify-center">
|
||
<CheckCircle className="w-6 h-6 text-purple-400" />
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
<Card className="bg-[#0f2137] border-gray-700/50">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-gray-400 text-sm">今日佣金</p>
|
||
<p className="text-2xl font-bold text-[#38bdac] mt-1">
|
||
¥{overview.todayEarnings.toFixed(2)}
|
||
</p>
|
||
</div>
|
||
<div className="w-12 h-12 rounded-xl bg-[#38bdac]/20 flex items-center justify-center">
|
||
<DollarSign className="w-6 h-6 text-[#38bdac]" />
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
|
||
{/* 每篇文章今日点击 */}
|
||
{(overview.todayClicksByPage?.length ?? 0) > 0 && (
|
||
<Card className="bg-[#0f2137] border-gray-700/50">
|
||
<CardHeader>
|
||
<CardTitle className="text-white flex items-center gap-2">
|
||
<Eye className="w-5 h-5 text-[#38bdac]" />
|
||
每篇文章今日点击(按来源页/文章统计)
|
||
</CardTitle>
|
||
<p className="text-gray-400 text-sm mt-1">实际用户与实际文章的点击均计入;今日总点击与上表一致</p>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full text-sm">
|
||
<thead>
|
||
<tr className="border-b border-gray-700 text-left text-gray-400">
|
||
<th className="pb-3 pr-4">来源页/文章</th>
|
||
<th className="pb-3 pr-4 text-right">今日点击</th>
|
||
<th className="pb-3 text-right">占比</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{[...(overview.todayClicksByPage ?? [])]
|
||
.sort((a, b) => b.clicks - a.clicks)
|
||
.map((row, i) => (
|
||
<tr key={i} className="border-b border-gray-700/50">
|
||
<td className="py-2 pr-4 text-white font-mono">{row.page || '(未区分)'}</td>
|
||
<td className="py-2 pr-4 text-right text-white">{row.clicks}</td>
|
||
<td className="py-2 text-right text-gray-400">
|
||
{overview.todayClicks > 0
|
||
? ((row.clicks / overview.todayClicks) * 100).toFixed(1)
|
||
: 0}
|
||
%
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<Card className="bg-orange-500/10 border-orange-500/30">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center gap-4">
|
||
<div className="w-12 h-12 rounded-xl bg-orange-500/20 flex items-center justify-center">
|
||
<Clock className="w-6 h-6 text-orange-400" />
|
||
</div>
|
||
<div className="flex-1">
|
||
<p className="text-orange-300 font-medium">即将过期绑定</p>
|
||
<p className="text-2xl font-bold text-white">{overview.expiringBindings} 个</p>
|
||
<p className="text-orange-300/60 text-sm">7天内到期,需关注转化</p>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
<Card className="bg-blue-500/10 border-blue-500/30">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center gap-4">
|
||
<div className="w-12 h-12 rounded-xl bg-blue-500/20 flex items-center justify-center">
|
||
<Wallet className="w-6 h-6 text-blue-400" />
|
||
</div>
|
||
<div className="flex-1">
|
||
<p className="text-blue-300 font-medium">待审核提现</p>
|
||
<p className="text-2xl font-bold text-white">{overview.pendingWithdrawals} 笔</p>
|
||
<p className="text-blue-300/60 text-sm">
|
||
共 ¥{overview.pendingWithdrawAmount.toFixed(2)}
|
||
</p>
|
||
</div>
|
||
<Button
|
||
onClick={() => setActiveTab('withdrawals')}
|
||
variant="outline"
|
||
className="border-blue-500/50 text-blue-400 hover:bg-blue-500/20"
|
||
>
|
||
去审核
|
||
</Button>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-6">
|
||
<Card className="bg-[#0f2137] border-gray-700/50">
|
||
<CardHeader>
|
||
<CardTitle className="text-white flex items-center gap-2">
|
||
<Calendar className="w-5 h-5 text-[#38bdac]" />
|
||
本月统计
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="p-4 bg-white/5 rounded-lg">
|
||
<p className="text-gray-400 text-sm">点击量</p>
|
||
<p className="text-xl font-bold text-white">{overview.monthClicks}</p>
|
||
</div>
|
||
<div className="p-4 bg-white/5 rounded-lg">
|
||
<p className="text-gray-400 text-sm">绑定数</p>
|
||
<p className="text-xl font-bold text-white">{overview.monthBindings}</p>
|
||
</div>
|
||
<div className="p-4 bg-white/5 rounded-lg">
|
||
<p className="text-gray-400 text-sm">转化数</p>
|
||
<p className="text-xl font-bold text-white">{overview.monthConversions}</p>
|
||
</div>
|
||
<div className="p-4 bg-white/5 rounded-lg">
|
||
<p className="text-gray-400 text-sm">佣金</p>
|
||
<p className="text-xl font-bold text-[#38bdac]">
|
||
¥{overview.monthEarnings.toFixed(2)}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
<Card className="bg-[#0f2137] border-gray-700/50">
|
||
<CardHeader>
|
||
<CardTitle className="text-white flex items-center gap-2">
|
||
<TrendingUp className="w-5 h-5 text-[#38bdac]" />
|
||
累计统计
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="p-4 bg-white/5 rounded-lg">
|
||
<p className="text-gray-400 text-sm">总点击</p>
|
||
<p className="text-xl font-bold text-white">
|
||
{overview.totalClicks.toLocaleString()}
|
||
</p>
|
||
</div>
|
||
<div className="p-4 bg-white/5 rounded-lg">
|
||
<p className="text-gray-400 text-sm">总绑定</p>
|
||
<p className="text-xl font-bold text-white">
|
||
{overview.totalBindings.toLocaleString()}
|
||
</p>
|
||
</div>
|
||
<div className="p-4 bg-white/5 rounded-lg">
|
||
<p className="text-gray-400 text-sm">总转化</p>
|
||
<p className="text-xl font-bold text-white">{overview.totalConversions}</p>
|
||
</div>
|
||
<div className="p-4 bg-white/5 rounded-lg">
|
||
<p className="text-gray-400 text-sm">总佣金</p>
|
||
<p className="text-xl font-bold text-[#38bdac]">
|
||
¥{overview.totalEarnings.toFixed(2)}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<div className="mt-4 p-4 bg-[#38bdac]/10 rounded-lg flex items-center justify-between">
|
||
<span className="text-gray-300">点击转化率</span>
|
||
<span className="text-[#38bdac] font-bold text-xl">
|
||
{overview.conversionRate}%
|
||
</span>
|
||
</div>
|
||
</CardContent>
|
||
</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>
|
||
</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>
|
||
</div>
|
||
<div className="p-4 bg-white/5 rounded-lg text-center">
|
||
<p className="text-3xl font-bold text-[#38bdac]">90%</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-orange-400">30天</p>
|
||
<p className="text-gray-400 text-sm mt-1">绑定有效期</p>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</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>
|
||
<option value="refunded">已退款</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>
|
||
<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">
|
||
{displayOrders.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">
|
||
{(() => {
|
||
const type = order.productType || order.type
|
||
if (type === 'fullbook')
|
||
return `${order.bookName || '《底层逻辑》'} - 全本`
|
||
if (type === 'match') return '匹配次数购买'
|
||
return `${order.bookName || '《底层逻辑》'} - ${order.sectionTitle || order.chapterTitle || `章节${order.productId || order.sectionId || ''}`}`
|
||
})()}
|
||
</p>
|
||
<p className="text-gray-500 text-xs">
|
||
{(() => {
|
||
const type = order.productType || order.type
|
||
if (type === 'fullbook') return '全书解锁'
|
||
if (type === 'match') return '功能权益'
|
||
return order.chapterTitle || '单章购买'
|
||
})()}
|
||
</p>
|
||
</div>
|
||
</td>
|
||
<td className="p-4 text-[#38bdac] font-bold">
|
||
¥{typeof order.amount === 'number' ? order.amount.toFixed(2) : parseFloat(String(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 === 'refunded' ? (
|
||
<Badge className="bg-gray-500/20 text-gray-400 border-0">
|
||
已退款
|
||
</Badge>
|
||
) : order.status === 'completed' || order.status === 'paid' ? (
|
||
<Badge className="bg-green-500/20 text-green-400 border-0">
|
||
已完成
|
||
</Badge>
|
||
) : order.status === 'pending' || order.status === 'created' ? (
|
||
<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-gray-400 text-sm max-w-[120px]" title={order.refundReason}>
|
||
{order.status === 'refunded' && order.refundReason ? order.refundReason : '-'}
|
||
</td>
|
||
<td className="p-4 text-gray-300 text-sm">
|
||
{order.referrerId || order.referralCode ? (
|
||
<span
|
||
title={
|
||
order.referralCode ||
|
||
order.referrerCode ||
|
||
order.referrerId ||
|
||
''
|
||
}
|
||
>
|
||
{order.referrerNickname ||
|
||
order.referralCode ||
|
||
order.referrerCode ||
|
||
order.referrerId?.slice(0, 8)}
|
||
{(order.referralCode || order.referrerCode) &&
|
||
` (${order.referralCode || order.referrerCode})`}
|
||
</span>
|
||
) : (
|
||
'-'
|
||
)}
|
||
</td>
|
||
<td className="p-4 text-[#FFD700]">
|
||
{order.referrerEarnings
|
||
? `¥${(typeof order.referrerEarnings === 'number' ? order.referrerEarnings : parseFloat(String(order.referrerEarnings))).toFixed(2)}`
|
||
: '-'}
|
||
</td>
|
||
<td className="p-4 text-gray-400 text-sm">
|
||
{order.createdAt
|
||
? new Date(order.createdAt).toLocaleString('zh-CN')
|
||
: '-'}
|
||
</td>
|
||
<td className="p-4">
|
||
{(order.status === 'paid' || order.status === 'completed') && (
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
className="border-orange-500/50 text-orange-400 hover:bg-orange-500/20"
|
||
onClick={() => {
|
||
setRefundOrder(order)
|
||
setRefundReason('')
|
||
}}
|
||
>
|
||
<Undo2 className="w-3 h-3 mr-1" />
|
||
退款
|
||
</Button>
|
||
)}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
)}
|
||
{activeTab === 'orders' && (
|
||
<Pagination
|
||
page={page}
|
||
totalPages={totalPages}
|
||
total={total}
|
||
pageSize={pageSize}
|
||
onPageChange={setPage}
|
||
onPageSizeChange={(n) => {
|
||
setPageSize(n)
|
||
setPage(1)
|
||
}}
|
||
/>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
)}
|
||
|
||
{activeTab === 'bindings' && (
|
||
<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="active">有效</option>
|
||
<option value="converted">已转化</option>
|
||
<option value="expired">已过期</option>
|
||
</select>
|
||
</div>
|
||
<Card className="bg-[#0f2137] border-gray-700/50">
|
||
<CardContent className="p-0">
|
||
{displayBindings.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>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-gray-700/50">
|
||
{displayBindings.map((binding) => (
|
||
<tr key={binding.id} className="hover:bg-[#0a1628] transition-colors">
|
||
<td className="p-4">
|
||
<div>
|
||
<p className="text-white font-medium">
|
||
{binding.refereeNickname || '匿名用户'}
|
||
</p>
|
||
<p className="text-gray-500 text-xs">{binding.refereePhone}</p>
|
||
</div>
|
||
</td>
|
||
<td className="p-4">
|
||
<div>
|
||
<p className="text-white">{binding.referrerName || '-'}</p>
|
||
<p className="text-gray-500 text-xs font-mono">
|
||
{binding.referrerCode}
|
||
</p>
|
||
</div>
|
||
</td>
|
||
<td className="p-4 text-gray-400">
|
||
{binding.boundAt
|
||
? new Date(binding.boundAt).toLocaleDateString('zh-CN')
|
||
: '-'}
|
||
</td>
|
||
<td className="p-4 text-gray-400">
|
||
{binding.expiresAt
|
||
? new Date(binding.expiresAt).toLocaleDateString('zh-CN')
|
||
: '-'}
|
||
</td>
|
||
<td className="p-4">{getStatusBadge(binding.status)}</td>
|
||
<td className="p-4">
|
||
{binding.commission ? (
|
||
<span className="text-[#38bdac] font-medium">
|
||
¥{binding.commission.toFixed(2)}
|
||
</span>
|
||
) : (
|
||
<span className="text-gray-500">-</span>
|
||
)}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
)}
|
||
{activeTab === 'bindings' && (
|
||
<Pagination
|
||
page={page}
|
||
totalPages={totalPages}
|
||
total={total}
|
||
pageSize={pageSize}
|
||
onPageChange={setPage}
|
||
onPageSizeChange={(n) => {
|
||
setPageSize(n)
|
||
setPage(1)
|
||
}}
|
||
/>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
)}
|
||
|
||
{activeTab === 'withdrawals' && (
|
||
<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="pending">待审核</option>
|
||
<option value="completed">已完成</option>
|
||
<option value="rejected">已拒绝</option>
|
||
</select>
|
||
</div>
|
||
<Card className="bg-[#0f2137] border-gray-700/50">
|
||
<CardContent className="p-0">
|
||
{displayWithdrawals.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-right font-medium">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-gray-700/50">
|
||
{displayWithdrawals.map((withdrawal) => (
|
||
<tr key={withdrawal.id} className="hover:bg-[#0a1628] transition-colors">
|
||
<td className="p-4">
|
||
<div className="flex items-center gap-2">
|
||
{withdrawal.userAvatar ? (
|
||
<img
|
||
src={withdrawal.userAvatar}
|
||
alt=""
|
||
className="w-8 h-8 rounded-full object-cover"
|
||
/>
|
||
) : (
|
||
<div className="w-8 h-8 rounded-full bg-gray-600 flex items-center justify-center text-white text-sm font-medium">
|
||
{(withdrawal.userName || withdrawal.name || '?').slice(0, 1)}
|
||
</div>
|
||
)}
|
||
<p className="text-white font-medium">
|
||
{withdrawal.userName || withdrawal.name}
|
||
</p>
|
||
</div>
|
||
</td>
|
||
<td className="p-4">
|
||
<span className="text-[#38bdac] font-bold">
|
||
¥{withdrawal.amount.toFixed(2)}
|
||
</span>
|
||
</td>
|
||
<td className="p-4">
|
||
<Badge
|
||
className={
|
||
withdrawal.method === 'wechat'
|
||
? 'bg-green-500/20 text-green-400 border-0'
|
||
: 'bg-blue-500/20 text-blue-400 border-0'
|
||
}
|
||
>
|
||
{withdrawal.method === 'wechat' ? '微信' : '支付宝'}
|
||
</Badge>
|
||
</td>
|
||
<td className="p-4">
|
||
<div>
|
||
<p className="text-white font-mono text-xs">
|
||
{withdrawal.account}
|
||
</p>
|
||
<p className="text-gray-500 text-xs">{withdrawal.name}</p>
|
||
</div>
|
||
</td>
|
||
<td className="p-4 text-gray-400">
|
||
{withdrawal.createdAt
|
||
? new Date(withdrawal.createdAt).toLocaleString('zh-CN')
|
||
: '-'}
|
||
</td>
|
||
<td className="p-4">{getStatusBadge(withdrawal.status)}</td>
|
||
<td className="p-4 text-right">
|
||
{withdrawal.status === 'pending' && (
|
||
<div className="flex gap-2 justify-end">
|
||
<Button
|
||
size="sm"
|
||
onClick={() => handleApproveWithdrawal(withdrawal.id)}
|
||
className="bg-[#38bdac] hover:bg-[#2da396] text-white"
|
||
>
|
||
<CheckCircle className="w-4 h-4 mr-1" />
|
||
通过
|
||
</Button>
|
||
<Button
|
||
size="sm"
|
||
variant="outline"
|
||
onClick={() => handleRejectWithdrawal(withdrawal.id)}
|
||
className="border-red-500/50 text-red-400 hover:bg-red-500/20"
|
||
>
|
||
<XCircle className="w-4 h-4 mr-1" />
|
||
拒绝
|
||
</Button>
|
||
</div>
|
||
)}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
)}
|
||
{activeTab === 'withdrawals' && (
|
||
<Pagination
|
||
page={page}
|
||
totalPages={totalPages}
|
||
total={total}
|
||
pageSize={pageSize}
|
||
onPageChange={setPage}
|
||
onPageSizeChange={(n) => {
|
||
setPageSize(n)
|
||
setPage(1)
|
||
}}
|
||
/>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
)}
|
||
</>
|
||
)}
|
||
|
||
<Dialog open={!!refundOrder} onOpenChange={(open) => !open && setRefundOrder(null)}>
|
||
<DialogContent className="bg-[#0f2137] border-gray-700 text-white max-w-md">
|
||
<DialogHeader>
|
||
<DialogTitle className="text-white">订单退款</DialogTitle>
|
||
</DialogHeader>
|
||
{refundOrder && (
|
||
<div className="space-y-4">
|
||
<p className="text-gray-400 text-sm">
|
||
订单号:{refundOrder.orderSn || refundOrder.id}
|
||
</p>
|
||
<p className="text-gray-400 text-sm">
|
||
退款金额:¥{typeof refundOrder.amount === 'number' ? refundOrder.amount.toFixed(2) : parseFloat(String(refundOrder.amount || '0')).toFixed(2)}
|
||
</p>
|
||
<div>
|
||
<label className="text-sm text-gray-400 block mb-2">退款原因(选填)</label>
|
||
<div className="form-input">
|
||
<Input
|
||
className="bg-[#0a1628] border-gray-700 text-white placeholder:text-gray-500"
|
||
placeholder="如:用户申请退款"
|
||
value={refundReason}
|
||
onChange={(e) => setRefundReason(e.target.value)}
|
||
/>
|
||
</div>
|
||
</div>
|
||
<p className="text-orange-400/80 text-xs">
|
||
退款将原路退回至用户微信,且无法撤销,请确认后再操作。
|
||
</p>
|
||
</div>
|
||
)}
|
||
<DialogFooter>
|
||
<Button
|
||
variant="outline"
|
||
className="border-gray-600 text-gray-300"
|
||
onClick={() => setRefundOrder(null)}
|
||
disabled={refundLoading}
|
||
>
|
||
取消
|
||
</Button>
|
||
<Button
|
||
className="bg-orange-500 hover:bg-orange-600 text-white"
|
||
onClick={handleRefund}
|
||
disabled={refundLoading}
|
||
>
|
||
{refundLoading ? '退款中...' : '确认退款'}
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
|
||
{/* 推广设置 Tab */}
|
||
{activeTab === 'settings' && (
|
||
<div className="-mx-8 -mt-6">
|
||
<ReferralSettingsPage embedded />
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|