feat(invite): 添加邀请统计功能

- 新增邀请统计页面,包含统计概览、邀请记录和排行榜三个标签页
- 实现邀请统计数据的获取和展示,包括总邀请数、成功注册数、转化率等
- 添加邀请记录的查询和展示功能
- 实现邀请排行榜的查询和展示功能
- 新增生成小程序码和处理邀请场景值的接口
This commit is contained in:
2025-08-19 01:38:37 +08:00
parent 9d9762ef17
commit f928264e2c
23 changed files with 2406 additions and 261 deletions

View File

@@ -1,8 +0,0 @@
/* 添加这段样式后Primary Button 会变成绿色 */
:root {
--nutui-color-primary: green;
--nutui-color-primary-stop1: green;
--nutui-color-primary-stop2: green;
// 间隔线/容错线,用于结构或信息分割
--nutui-black-2: rgba(255, 0, 0, 0.08);
}

View File

@@ -7,7 +7,8 @@ import {
Dongdong,
ArrowRight,
Purse,
People
People,
Chart
} from '@nutui/icons-react-taro'
import {useDealerUser} from '@/hooks/useDealerUser'
import { useThemeStyles } from '@/hooks/useTheme'
@@ -69,7 +70,7 @@ const DealerIndex: React.FC = () => {
}
return (
<View className="bg-gray-50 min-h-screen">
<View className="bg-gray-100 min-h-screen">
<View>
{/*头部信息*/}
{dealerUser && (
@@ -162,9 +163,9 @@ const DealerIndex: React.FC = () => {
{/* 团队统计 */}
{dealerUser && (
<View className="bg-white mx-4 mt-4 rounded-xl p-4">
<View className="bg-white mx-4 mt-4 rounded-xl p-4 hidden">
<View className="flex items-center justify-between mb-4">
<Text className="font-semibold text-gray-800"></Text>
<Text className="font-semibold text-gray-800"></Text>
<View
className="text-gray-400 text-sm flex items-center"
onClick={() => navigateToPage('/dealer/team/index')}
@@ -200,7 +201,15 @@ const DealerIndex: React.FC = () => {
<View className="bg-white mx-4 mt-4 rounded-xl p-4">
<View className="font-semibold mb-4 text-gray-800"></View>
<ConfigProvider>
<Grid columns={4}>
<Grid
columns={4}
className="no-border-grid"
style={{
'--nutui-grid-border-color': 'transparent',
'--nutui-grid-item-border-width': '0px',
border: 'none'
} as React.CSSProperties}
>
<Grid.Item text="分销订单" onClick={() => navigateToPage('/dealer/orders/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2">
@@ -209,30 +218,68 @@ const DealerIndex: React.FC = () => {
</View>
</Grid.Item>
<Grid.Item onClick={() => navigateToPage('/dealer/withdraw/index')}>
<Grid.Item text={'提现申请'} onClick={() => navigateToPage('/dealer/withdraw/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-green-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Purse color="#10b981" size="20"/>
</View>
<Text className="text-xs text-gray-600"></Text>
</View>
</Grid.Item>
<Grid.Item onClick={() => navigateToPage('/dealer/team/index')}>
<Grid.Item text={'我的邀请'} onClick={() => navigateToPage('/dealer/team/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-purple-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<People color="#8b5cf6" size="20"/>
</View>
<Text className="text-xs text-gray-600"></Text>
</View>
</Grid.Item>
<Grid.Item onClick={() => navigateToPage('/dealer/qrcode/index')}>
<Grid.Item text={'我的邀请码'} onClick={() => navigateToPage('/dealer/qrcode/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-orange-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Dongdong color="#f59e0b" size="20"/>
</View>
<Text className="text-xs text-gray-600">广</Text>
</View>
</Grid.Item>
</Grid>
{/* 第二行功能 */}
<Grid
columns={4}
className="no-border-grid mt-4"
style={{
'--nutui-grid-border-color': 'transparent',
'--nutui-grid-item-border-width': '0px',
border: 'none'
} as React.CSSProperties}
>
<Grid.Item text={'邀请统计'} onClick={() => navigateToPage('/dealer/invite-stats/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-indigo-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Chart color="#6366f1" size="20"/>
</View>
</View>
</Grid.Item>
{/* 预留其他功能位置 */}
<Grid.Item text={''}>
<View className="text-center">
<View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">
</View>
</View>
</Grid.Item>
<Grid.Item text={''}>
<View className="text-center">
<View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">
</View>
</View>
</Grid.Item>
<Grid.Item text={''}>
<View className="text-center">
<View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">
</View>
</View>
</Grid.Item>
</Grid>

View File

@@ -0,0 +1,7 @@
export default definePageConfig({
navigationBarTitleText: '邀请统计',
navigationBarBackgroundColor: '#ffffff',
navigationBarTextStyle: 'black',
backgroundColor: '#f5f5f5',
enablePullDownRefresh: true
})

View File

@@ -0,0 +1,343 @@
import React, { useState, useEffect, useCallback } from 'react'
import { View, Text } from '@tarojs/components'
import {
Empty,
Tabs,
Progress,
Loading,
PullToRefresh,
Card,
Button,
DatePicker
} from '@nutui/nutui-react-taro'
import {
User,
Star,
TrendingUp,
Calendar,
Share,
Target,
Award
} from '@nutui/icons-react-taro'
import Taro from '@tarojs/taro'
import { useDealerUser } from '@/hooks/useDealerUser'
import {
getInviteStats,
getMyInviteRecords,
getInviteRanking
} from '@/api/invite'
import type {
InviteStats,
InviteRecord,
InviteRanking
} from '@/api/invite'
import { businessGradients } from '@/styles/gradients'
const InviteStatsPage: React.FC = () => {
const [activeTab, setActiveTab] = useState<string>('stats')
const [loading, setLoading] = useState<boolean>(false)
const [refreshing, setRefreshing] = useState<boolean>(false)
const [inviteStats, setInviteStats] = useState<InviteStats | null>(null)
const [inviteRecords, setInviteRecords] = useState<InviteRecord[]>([])
const [ranking, setRanking] = useState<InviteRanking[]>([])
const [dateRange, setDateRange] = useState<string>('month')
const { dealerUser } = useDealerUser()
// 获取邀请统计数据
const fetchInviteStats = useCallback(async () => {
if (!dealerUser?.userId) return
try {
setLoading(true)
const stats = await getInviteStats(dealerUser.userId)
setInviteStats(stats)
} catch (error) {
console.error('获取邀请统计失败:', error)
Taro.showToast({
title: '获取统计数据失败',
icon: 'error'
})
} finally {
setLoading(false)
}
}, [dealerUser?.userId])
// 获取邀请记录
const fetchInviteRecords = useCallback(async () => {
if (!dealerUser?.userId) return
try {
const result = await getMyInviteRecords({
page: 1,
limit: 50,
inviterId: dealerUser.userId
})
setInviteRecords(result?.list || [])
} catch (error) {
console.error('获取邀请记录失败:', error)
}
}, [dealerUser?.userId])
// 获取邀请排行榜
const fetchRanking = useCallback(async () => {
try {
const result = await getInviteRanking({
limit: 20,
period: dateRange as 'day' | 'week' | 'month'
})
setRanking(result || [])
} catch (error) {
console.error('获取排行榜失败:', error)
}
}, [dateRange])
// 刷新数据
const handleRefresh = async () => {
setRefreshing(true)
await Promise.all([
fetchInviteStats(),
fetchInviteRecords(),
fetchRanking()
])
setRefreshing(false)
}
// 初始化数据
useEffect(() => {
if (dealerUser?.userId) {
fetchInviteStats()
fetchInviteRecords()
fetchRanking()
}
}, [fetchInviteStats, fetchInviteRecords, fetchRanking])
// 获取状态显示文本
const getStatusText = (status: string) => {
const statusMap: Record<string, string> = {
'pending': '待注册',
'registered': '已注册',
'activated': '已激活'
}
return statusMap[status] || status
}
// 获取状态颜色
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
'pending': 'text-orange-500',
'registered': 'text-blue-500',
'activated': 'text-green-500'
}
return colorMap[status] || 'text-gray-500'
}
// 渲染统计概览
const renderStatsOverview = () => (
<View className="px-4 space-y-4">
{/* 核心数据卡片 */}
<Card className="bg-white rounded-2xl shadow-sm">
<View className="p-4">
<Text className="text-lg font-semibold text-gray-800 mb-4"></Text>
{loading ? (
<View className="flex items-center justify-center py-8">
<Loading />
</View>
) : inviteStats ? (
<View className="grid grid-cols-2 gap-4">
<View className="text-center p-4 bg-blue-50 rounded-xl">
<TrendingUp size="24" className="text-blue-500 mx-auto mb-2" />
<Text className="text-2xl font-bold text-blue-600">
{inviteStats.totalInvites || 0}
</Text>
<Text className="text-sm text-gray-600"></Text>
</View>
<View className="text-center p-4 bg-green-50 rounded-xl">
<User size="24" className="text-green-500 mx-auto mb-2" />
<Text className="text-2xl font-bold text-green-600">
{inviteStats.successfulRegistrations || 0}
</Text>
<Text className="text-sm text-gray-600"></Text>
</View>
<View className="text-center p-4 bg-purple-50 rounded-xl">
<Target size="24" className="text-purple-500 mx-auto mb-2" />
<Text className="text-2xl font-bold text-purple-600">
{inviteStats.conversionRate ? `${(inviteStats.conversionRate * 100).toFixed(1)}%` : '0%'}
</Text>
<Text className="text-sm text-gray-600"></Text>
</View>
<View className="text-center p-4 bg-orange-50 rounded-xl">
<Calendar size="24" className="text-orange-500 mx-auto mb-2" />
<Text className="text-2xl font-bold text-orange-600">
{inviteStats.todayInvites || 0}
</Text>
<Text className="text-sm text-gray-600"></Text>
</View>
</View>
) : (
<View className="text-center py-8">
<Text className="text-gray-500"></Text>
</View>
)}
</View>
</Card>
{/* 邀请来源分析 */}
{inviteStats?.sourceStats && inviteStats.sourceStats.length > 0 && (
<Card className="bg-white rounded-2xl shadow-sm">
<View className="p-4">
<Text className="text-lg font-semibold text-gray-800 mb-4"></Text>
<View className="space-y-3">
{inviteStats.sourceStats.map((source, index) => (
<View key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<View className="flex items-center">
<Share size="16" className="text-blue-500 mr-2" />
<Text className="font-medium text-gray-800">{source.source}</Text>
</View>
<View className="text-right">
<Text className="text-lg font-bold text-gray-800">{source.count}</Text>
<Text className="text-sm text-gray-500">
{source.conversionRate ? `${(source.conversionRate * 100).toFixed(1)}%` : '0%'}
</Text>
</View>
</View>
))}
</View>
</View>
</Card>
)}
</View>
)
// 渲染邀请记录
const renderInviteRecords = () => (
<View className="px-4">
{inviteRecords.length > 0 ? (
<View className="space-y-3">
{inviteRecords.map((record, index) => (
<Card key={record.id || index} className="bg-white rounded-xl shadow-sm">
<View className="p-4">
<View className="flex items-center justify-between mb-2">
<Text className="font-medium text-gray-800">
{record.inviteeName || `用户${record.inviteeId}`}
</Text>
<Text className={`text-sm font-medium ${getStatusColor(record.status || 'pending')}`}>
{getStatusText(record.status || 'pending')}
</Text>
</View>
<View className="flex items-center justify-between text-sm text-gray-500">
<Text>: {record.source || '未知'}</Text>
<Text>{record.inviteTime ? new Date(record.inviteTime).toLocaleDateString() : ''}</Text>
</View>
{record.registerTime && (
<Text className="text-xs text-green-600 mt-1">
: {new Date(record.registerTime).toLocaleString()}
</Text>
)}
</View>
</Card>
))}
</View>
) : (
<Empty description="暂无邀请记录" />
)}
</View>
)
// 渲染排行榜
const renderRanking = () => (
<View className="px-4">
<View className="mb-4">
<Tabs value={dateRange} onChange={setDateRange}>
<Tabs.TabPane title="日榜" value="day" />
<Tabs.TabPane title="周榜" value="week" />
<Tabs.TabPane title="月榜" value="month" />
</Tabs>
</View>
{ranking.length > 0 ? (
<View className="space-y-3">
{ranking.map((item, index) => (
<Card key={item.inviterId} className="bg-white rounded-xl shadow-sm">
<View className="p-4 flex items-center">
<View className="flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 mr-3">
{index < 3 ? (
<Award size="16" className={index === 0 ? 'text-yellow-500' : index === 1 ? 'text-gray-400' : 'text-orange-400'} />
) : (
<Text className="text-sm font-bold text-gray-600">{index + 1}</Text>
)}
</View>
<View className="flex-1">
<Text className="font-medium text-gray-800">{item.inviterName}</Text>
<Text className="text-sm text-gray-500">
{item.inviteCount} · {item.conversionRate ? `${(item.conversionRate * 100).toFixed(1)}%` : '0%'}
</Text>
</View>
<Text className="text-lg font-bold text-blue-600">{item.successCount}</Text>
</View>
</Card>
))}
</View>
) : (
<Empty description="暂无排行数据" />
)}
</View>
)
if (!dealerUser) {
return (
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
<Loading />
<Text className="text-gray-500 mt-2">...</Text>
</View>
)
}
return (
<View className="bg-gray-50 min-h-screen">
{/* 头部 */}
<View className="rounded-b-3xl p-6 mb-6 text-white relative overflow-hidden" style={{
background: businessGradients.dealer.header
}}>
<View className="absolute w-32 h-32 rounded-full" style={{
backgroundColor: 'rgba(255, 255, 255, 0.1)',
top: '-16px',
right: '-16px'
}}></View>
<View className="relative z-10">
<Text className="text-2xl font-bold mb-2 text-white"></Text>
<Text className="text-white text-opacity-80">
广
</Text>
</View>
</View>
{/* 标签页 */}
<View className="px-4 mb-4">
<Tabs value={activeTab} onChange={setActiveTab}>
<Tabs.TabPane title="统计概览" value="stats" />
<Tabs.TabPane title="邀请记录" value="records" />
<Tabs.TabPane title="排行榜" value="ranking" />
</Tabs>
</View>
{/* 内容区域 */}
<PullToRefresh onRefresh={handleRefresh} loading={refreshing}>
<View className="pb-6">
{activeTab === 'stats' && renderStatsOverview()}
{activeTab === 'records' && renderInviteRecords()}
{activeTab === 'ranking' && renderRanking()}
</View>
</PullToRefresh>
</View>
)
}
export default InviteStatsPage

View File

@@ -1,60 +1,197 @@
import React, { useState } from 'react'
import React, { useState, useEffect, useCallback } from 'react'
import { View, Text } from '@tarojs/components'
import { Empty, Tabs, Tag, PullToRefresh } from '@nutui/nutui-react-taro'
import { Empty, Tabs, Tag, PullToRefresh, Loading } from '@nutui/nutui-react-taro'
import Taro from '@tarojs/taro'
import { pageShopDealerOrder } from '@/api/shop/shopDealerOrder'
import { useDealerUser } from '@/hooks/useDealerUser'
import type { ShopDealerOrder } from '@/api/shop/shopDealerOrder/model'
interface OrderWithDetails extends ShopDealerOrder {
orderNo?: string
customerName?: string
totalCommission?: string
// 当前用户在此订单中的层级和佣金
userLevel?: 1 | 2 | 3
userCommission?: string
}
const DealerOrders: React.FC = () => {
const [activeTab, setActiveTab] = useState<string>('0')
const [refreshing, setRefreshing] = useState<boolean>(false)
const [loading, setLoading] = useState<boolean>(false)
const [orders, setOrders] = useState<OrderWithDetails[]>([])
const [statistics, setStatistics] = useState({
totalOrders: 0,
totalCommission: '0.00',
pendingCommission: '0.00',
// 分层统计
level1: { orders: 0, commission: '0.00' },
level2: { orders: 0, commission: '0.00' },
level3: { orders: 0, commission: '0.00' }
})
// 模拟订单数据
const mockOrders = [
{
id: '1',
orderNo: 'DD202412180001',
customerName: '张三',
amount: '299.00',
commission: '29.90',
status: 'completed',
createTime: '2024-12-18 10:30:00'
},
{
id: '2',
orderNo: 'DD202412180002',
customerName: '李四',
amount: '599.00',
commission: '59.90',
status: 'pending',
createTime: '2024-12-18 14:20:00'
const { dealerUser } = useDealerUser()
// 获取订单数据 - 查询当前用户作为各层级分销商的所有订单
const fetchOrders = useCallback(async () => {
if (!dealerUser?.userId) return
try {
setLoading(true)
// 并行查询三个层级的订单
const [level1Result, level2Result, level3Result] = await Promise.all([
// 一级分销商订单
pageShopDealerOrder({
page: 1,
limit: 100,
firstUserId: dealerUser.userId
}),
// 二级分销商订单
pageShopDealerOrder({
page: 1,
limit: 100,
secondUserId: dealerUser.userId
}),
// 三级分销商订单
pageShopDealerOrder({
page: 1,
limit: 100,
thirdUserId: dealerUser.userId
})
])
const allOrders: OrderWithDetails[] = []
const stats = {
totalOrders: 0,
totalCommission: '0.00',
pendingCommission: '0.00',
level1: { orders: 0, commission: '0.00' },
level2: { orders: 0, commission: '0.00' },
level3: { orders: 0, commission: '0.00' }
}
// 处理一级分销订单
if (level1Result?.list) {
const level1Orders = level1Result.list.map(order => ({
...order,
orderNo: `DD${order.orderId}`,
customerName: `用户${order.userId}`,
userLevel: 1 as const,
userCommission: order.firstMoney || '0.00',
totalCommission: (
parseFloat(order.firstMoney || '0') +
parseFloat(order.secondMoney || '0') +
parseFloat(order.thirdMoney || '0')
).toFixed(2)
}))
allOrders.push(...level1Orders)
stats.level1.orders = level1Orders.length
stats.level1.commission = level1Orders.reduce((sum, order) =>
sum + parseFloat(order.userCommission || '0'), 0
).toFixed(2)
}
// 处理二级分销订单
if (level2Result?.list) {
const level2Orders = level2Result.list.map(order => ({
...order,
orderNo: `DD${order.orderId}`,
customerName: `用户${order.userId}`,
userLevel: 2 as const,
userCommission: order.secondMoney || '0.00',
totalCommission: (
parseFloat(order.firstMoney || '0') +
parseFloat(order.secondMoney || '0') +
parseFloat(order.thirdMoney || '0')
).toFixed(2)
}))
allOrders.push(...level2Orders)
stats.level2.orders = level2Orders.length
stats.level2.commission = level2Orders.reduce((sum, order) =>
sum + parseFloat(order.userCommission || '0'), 0
).toFixed(2)
}
// 处理三级分销订单
if (level3Result?.list) {
const level3Orders = level3Result.list.map(order => ({
...order,
orderNo: `DD${order.orderId}`,
customerName: `用户${order.userId}`,
userLevel: 3 as const,
userCommission: order.thirdMoney || '0.00',
totalCommission: (
parseFloat(order.firstMoney || '0') +
parseFloat(order.secondMoney || '0') +
parseFloat(order.thirdMoney || '0')
).toFixed(2)
}))
allOrders.push(...level3Orders)
stats.level3.orders = level3Orders.length
stats.level3.commission = level3Orders.reduce((sum, order) =>
sum + parseFloat(order.userCommission || '0'), 0
).toFixed(2)
}
// 去重(同一个订单可能在多个层级中出现)
const uniqueOrders = allOrders.filter((order, index, self) =>
index === self.findIndex(o => o.orderId === order.orderId)
)
// 计算总统计
stats.totalOrders = uniqueOrders.length
stats.totalCommission = (
parseFloat(stats.level1.commission) +
parseFloat(stats.level2.commission) +
parseFloat(stats.level3.commission)
).toFixed(2)
stats.pendingCommission = allOrders
.filter(order => order.isSettled === 0)
.reduce((sum, order) => sum + parseFloat(order.userCommission || '0'), 0)
.toFixed(2)
setOrders(uniqueOrders)
setStatistics(stats)
} catch (error) {
console.error('获取分销订单失败:', error)
Taro.showToast({
title: '获取订单失败',
icon: 'error'
})
} finally {
setLoading(false)
}
]
const getStatusText = (status: string) => {
switch (status) {
case 'completed': return '已完成'
case 'pending': return '待结算'
case 'cancelled': return '已取消'
default: return '未知'
}
}
const getStatusColor = (status: string) => {
switch (status) {
case 'completed': return 'success'
case 'pending': return 'warning'
case 'cancelled': return 'danger'
default: return 'default'
}
}
}, [dealerUser?.userId])
// 刷新数据
const handleRefresh = async () => {
setRefreshing(true)
// 模拟刷新
setTimeout(() => {
setRefreshing(false)
}, 1000)
await fetchOrders()
}
const renderOrderItem = (order: any) => (
// 初始化加载数据
useEffect(() => {
if (dealerUser?.userId) {
fetchOrders().then()
}
}, [fetchOrders])
const getStatusText = (isSettled?: number, isInvalid?: number) => {
if (isInvalid === 1) return '已失效'
if (isSettled === 1) return '已结算'
return '待结算'
}
const getStatusColor = (isSettled?: number, isInvalid?: number) => {
if (isInvalid === 1) return 'danger'
if (isSettled === 1) return 'success'
return 'warning'
}
const renderOrderItem = (order: OrderWithDetails) => (
<View key={order.id} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
<View className="flex justify-between items-start mb-3">
<View>
@@ -64,19 +201,28 @@ const DealerOrders: React.FC = () => {
<Text className="text-sm text-gray-500">
{order.customerName}
</Text>
{/* 显示用户在此订单中的层级 */}
<Text className="text-xs text-blue-500">
{order.userLevel === 1 && '一级分销'}
{order.userLevel === 2 && '二级分销'}
{order.userLevel === 3 && '三级分销'}
</Text>
</View>
<Tag type={getStatusColor(order.status)}>
{getStatusText(order.status)}
<Tag type={getStatusColor(order.isSettled, order.isInvalid)}>
{getStatusText(order.isSettled, order.isInvalid)}
</Tag>
</View>
<View className="flex justify-between items-center">
<View>
<Text className="text-sm text-gray-600">
¥{order.amount}
¥{order.orderPrice || '0.00'}
</Text>
<Text className="text-sm text-orange-500 font-semibold">
¥{order.commission}
¥{order.userCommission}
</Text>
<Text className="text-xs text-gray-400">
¥{order.totalCommission}
</Text>
</View>
<Text className="text-xs text-gray-400">
@@ -86,37 +232,92 @@ const DealerOrders: React.FC = () => {
</View>
)
// 根据状态和层级过滤订单
const getFilteredOrders = (filter: string) => {
switch (filter) {
case '1': // 一级分销
return orders.filter(order => order.userLevel === 1)
case '2': // 二级分销
return orders.filter(order => order.userLevel === 2)
case '3': // 三级分销
return orders.filter(order => order.userLevel === 3)
case '4': // 待结算
return orders.filter(order => order.isSettled === 0 && order.isInvalid === 0)
case '5': // 已结算
return orders.filter(order => order.isSettled === 1)
case '6': // 已失效
return orders.filter(order => order.isInvalid === 1)
default: // 全部
return orders
}
}
if (!dealerUser) {
return (
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
<Loading />
<Text className="text-gray-500 mt-2">...</Text>
</View>
)
}
return (
<View className="bg-gray-50 min-h-screen">
{/* 统计卡片 */}
<View className="bg-white p-4 mb-4">
<View className="grid grid-cols-3 gap-4">
{/* 总体统计 */}
<View className="grid grid-cols-3 gap-4 mb-4">
<View className="text-center">
<Text className="text-lg font-bold text-blue-500">2</Text>
<Text className="text-lg font-bold text-blue-500">{statistics.totalOrders}</Text>
<Text className="text-xs text-gray-500"></Text>
</View>
<View className="text-center">
<Text className="text-lg font-bold text-green-500">¥89.80</Text>
<Text className="text-lg font-bold text-green-500">¥{statistics.totalCommission}</Text>
<Text className="text-xs text-gray-500"></Text>
</View>
<View className="text-center">
<Text className="text-lg font-bold text-orange-500">¥29.90</Text>
<Text className="text-lg font-bold text-orange-500">¥{statistics.pendingCommission}</Text>
<Text className="text-xs text-gray-500"></Text>
</View>
</View>
{/* 分层统计 */}
<View className="border-t pt-3">
<Text className="text-sm text-gray-600 mb-2"></Text>
<View className="grid grid-cols-3 gap-2">
<View className="text-center bg-gray-50 rounded p-2">
<Text className="text-sm font-semibold text-red-500">{statistics.level1.orders}</Text>
<Text className="text-xs text-gray-500"></Text>
<Text className="text-xs text-red-500">¥{statistics.level1.commission}</Text>
</View>
<View className="text-center bg-gray-50 rounded p-2">
<Text className="text-sm font-semibold text-blue-500">{statistics.level2.orders}</Text>
<Text className="text-xs text-gray-500"></Text>
<Text className="text-xs text-blue-500">¥{statistics.level2.commission}</Text>
</View>
<View className="text-center bg-gray-50 rounded p-2">
<Text className="text-sm font-semibold text-purple-500">{statistics.level3.orders}</Text>
<Text className="text-xs text-gray-500"></Text>
<Text className="text-xs text-purple-500">¥{statistics.level3.commission}</Text>
</View>
</View>
</View>
</View>
{/* 订单列表 */}
<Tabs value={activeTab} onChange={() => setActiveTab}>
<Tabs.TabPane title="全部" value="0">
<PullToRefresh
// @ts-ignore
loading={refreshing}
onRefresh={handleRefresh}
>
<View className="p-4">
{mockOrders.length > 0 ? (
mockOrders.map(renderOrderItem)
{loading ? (
<View className="text-center py-8">
<Loading />
<Text className="text-gray-500 mt-2">...</Text>
</View>
) : getFilteredOrders('0').length > 0 ? (
getFilteredOrders('0').map(renderOrderItem)
) : (
<Empty description="暂无分销订单" />
)}
@@ -124,15 +325,63 @@ const DealerOrders: React.FC = () => {
</PullToRefresh>
</Tabs.TabPane>
<Tabs.TabPane title="待结算" value="1">
<Tabs.TabPane title="一级分销" value="1">
<View className="p-4">
{mockOrders.filter(o => o.status === 'pending').map(renderOrderItem)}
{getFilteredOrders('1').length > 0 ? (
getFilteredOrders('1').map(renderOrderItem)
) : (
<Empty description="暂无一级分销订单" />
)}
</View>
</Tabs.TabPane>
<Tabs.TabPane title="已完成" value="2">
<Tabs.TabPane title="二级分销" value="2">
<View className="p-4">
{mockOrders.filter(o => o.status === 'completed').map(renderOrderItem)}
{getFilteredOrders('2').length > 0 ? (
getFilteredOrders('2').map(renderOrderItem)
) : (
<Empty description="暂无二级分销订单" />
)}
</View>
</Tabs.TabPane>
<Tabs.TabPane title="三级分销" value="3">
<View className="p-4">
{getFilteredOrders('3').length > 0 ? (
getFilteredOrders('3').map(renderOrderItem)
) : (
<Empty description="暂无三级分销订单" />
)}
</View>
</Tabs.TabPane>
<Tabs.TabPane title="待结算" value="4">
<View className="p-4">
{getFilteredOrders('4').length > 0 ? (
getFilteredOrders('4').map(renderOrderItem)
) : (
<Empty description="暂无待结算订单" />
)}
</View>
</Tabs.TabPane>
<Tabs.TabPane title="已结算" value="5">
<View className="p-4">
{getFilteredOrders('5').length > 0 ? (
getFilteredOrders('5').map(renderOrderItem)
) : (
<Empty description="暂无已结算订单" />
)}
</View>
</Tabs.TabPane>
<Tabs.TabPane title="已失效" value="6">
<View className="p-4">
{getFilteredOrders('6').length > 0 ? (
getFilteredOrders('6').map(renderOrderItem)
) : (
<Empty description="暂无失效订单" />
)}
</View>
</Tabs.TabPane>
</Tabs>

View File

@@ -1,28 +1,370 @@
import React from 'react'
import React, { useState, useEffect } from 'react'
import { View, Text, Image } from '@tarojs/components'
import { Cell, Button } from '@nutui/nutui-react-taro'
import { Button, Loading } from '@nutui/nutui-react-taro'
import { Share, Download, Copy, QrCode } from '@nutui/icons-react-taro'
import Taro from '@tarojs/taro'
import { useDealerUser } from '@/hooks/useDealerUser'
import { generateInviteCode, getInviteStats } from '@/api/invite'
import type { InviteStats } from '@/api/invite'
import { businessGradients } from '@/styles/gradients'
const DealerQrcode: React.FC = () => {
const [miniProgramCodeUrl, setMiniProgramCodeUrl] = useState<string>('')
const [loading, setLoading] = useState<boolean>(false)
const [inviteStats, setInviteStats] = useState<InviteStats | null>(null)
const [statsLoading, setStatsLoading] = useState<boolean>(false)
const { dealerUser } = useDealerUser()
// 生成小程序码
const generateMiniProgramCode = async () => {
if (!dealerUser?.userId) return
try {
setLoading(true)
// 生成邀请小程序码
const codeUrl = await generateInviteCode(dealerUser.userId, 'qrcode')
if (codeUrl) {
setMiniProgramCodeUrl(codeUrl)
}
} catch (error) {
console.error('生成小程序码失败:', error)
Taro.showToast({
title: '生成小程序码失败',
icon: 'error'
})
} finally {
setLoading(false)
}
}
// 获取邀请统计数据
const fetchInviteStats = async () => {
if (!dealerUser?.userId) return
try {
setStatsLoading(true)
const stats = await getInviteStats(dealerUser.userId)
stats && setInviteStats(stats)
} catch (error) {
console.error('获取邀请统计失败:', error)
} finally {
setStatsLoading(false)
}
}
// 初始化生成小程序码和获取统计数据
useEffect(() => {
if (dealerUser?.userId) {
generateMiniProgramCode()
fetchInviteStats()
}
}, [dealerUser?.userId])
// 保存小程序码到相册
const saveMiniProgramCode = async () => {
if (!miniProgramCodeUrl) {
Taro.showToast({
title: '小程序码未生成',
icon: 'error'
})
return
}
try {
// 先下载图片到本地
const res = await Taro.downloadFile({
url: miniProgramCodeUrl
})
if (res.statusCode === 200) {
// 保存到相册
await Taro.saveImageToPhotosAlbum({
filePath: res.tempFilePath
})
Taro.showToast({
title: '保存成功',
icon: 'success'
})
}
} catch (error: any) {
if (error.errMsg?.includes('auth deny')) {
Taro.showModal({
title: '提示',
content: '需要您授权保存图片到相册',
success: (res) => {
if (res.confirm) {
Taro.openSetting()
}
}
})
} else {
Taro.showToast({
title: '保存失败',
icon: 'error'
})
}
}
}
// 复制邀请信息
const copyInviteInfo = () => {
if (!dealerUser?.userId) {
Taro.showToast({
title: '用户信息未加载',
icon: 'error'
})
return
}
const inviteText = `🎉 邀请您加入我的团队!
扫描小程序码或搜索"网宿小店"小程序,即可享受优质商品和服务!
💰 成为我的下级分销商,一起赚取丰厚佣金
🎁 新用户专享优惠等你来拿
邀请码:${dealerUser.userId}
快来加入我们吧!`
Taro.setClipboardData({
data: inviteText,
success: () => {
Taro.showToast({
title: '邀请信息已复制',
icon: 'success'
})
}
})
}
// 分享小程序码
const shareMiniProgramCode = () => {
if (!dealerUser?.userId) {
Taro.showToast({
title: '用户信息未加载',
icon: 'error'
})
return
}
// 小程序分享
Taro.showShareMenu({
withShareTicket: true,
menus: ['shareAppMessage', 'shareTimeline']
})
}
if (!dealerUser) {
return (
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
<Loading />
<Text className="text-gray-500 mt-2">...</Text>
</View>
)
}
return (
<View className="p-4">
<Text className="text-lg font-bold mb-4">广</Text>
<View className="text-center">
<View className="bg-gray-100 w-48 h-48 mx-auto mb-4 flex items-center justify-center">
<Text className="text-gray-500"></Text>
<View className="bg-gray-50 min-h-screen">
{/* 头部卡片 */}
<View className="rounded-b-3xl p-6 mb-6 text-white relative overflow-hidden" style={{
background: businessGradients.dealer.header
}}>
{/* 装饰背景 */}
<View className="absolute w-32 h-32 rounded-full" style={{
backgroundColor: 'rgba(255, 255, 255, 0.1)',
top: '-16px',
right: '-16px'
}}></View>
<View className="relative z-10">
<Text className="text-2xl font-bold mb-2 text-white"></Text>
<Text className="text-white text-opacity-80">
</Text>
</View>
</View>
<View className="px-4">
{/* 小程序码展示区 */}
<View className="bg-white rounded-2xl p-6 mb-6 shadow-sm">
<View className="text-center">
{loading ? (
<View className="w-48 h-48 mx-auto mb-4 flex items-center justify-center bg-gray-50 rounded-xl">
<Loading />
<Text className="text-gray-500 mt-2">...</Text>
</View>
) : miniProgramCodeUrl ? (
<View className="w-48 h-48 mx-auto mb-4 bg-white rounded-xl shadow-sm p-4">
<Image
src={miniProgramCodeUrl}
className="w-full h-full"
mode="aspectFit"
/>
</View>
) : (
<View className="w-48 h-48 mx-auto mb-4 flex items-center justify-center bg-gray-50 rounded-xl">
<QrCode size="48" className="text-gray-400 mb-2"/>
<Text className="text-gray-500"></Text>
<Button
size="small"
type="primary"
className="mt-2"
onClick={generateMiniProgramCode}
>
</Button>
</View>
)}
<Text className="text-lg font-semibold text-gray-800 mb-2">
</Text>
<Text className="text-sm text-gray-500 mb-6">
</Text>
</View>
</View>
{/* 操作按钮 */}
<View className="space-y-3">
<Button
type="primary"
size="large"
block
icon={<Download />}
onClick={saveMiniProgramCode}
disabled={!miniProgramCodeUrl || loading}
>
</Button>
<Button
size="large"
block
icon={<Copy />}
onClick={copyInviteInfo}
disabled={!dealerUser?.userId || loading}
>
</Button>
<Button
size="large"
block
fill="outline"
icon={<Share />}
onClick={shareMiniProgramCode}
disabled={!dealerUser?.userId || loading}
>
</Button>
</View>
{/* 推广说明 */}
<View className="bg-white rounded-2xl p-4 mt-6">
<Text className="font-semibold text-gray-800 mb-3">广</Text>
<View className="space-y-2">
<View className="flex items-start">
<View className="w-2 h-2 bg-blue-500 rounded-full mt-2 mr-3 flex-shrink-0"></View>
<Text className="text-sm text-gray-600">
</Text>
</View>
<View className="flex items-start">
<View className="w-2 h-2 bg-green-500 rounded-full mt-2 mr-3 flex-shrink-0"></View>
<Text className="text-sm text-gray-600">
</Text>
</View>
<View className="flex items-start">
<View className="w-2 h-2 bg-purple-500 rounded-full mt-2 mr-3 flex-shrink-0"></View>
<Text className="text-sm text-gray-600">
</Text>
</View>
</View>
</View>
{/* 邀请统计数据 */}
<View className="bg-white rounded-2xl p-4 mt-4 mb-6">
<Text className="font-semibold text-gray-800 mb-3"></Text>
{statsLoading ? (
<View className="flex items-center justify-center py-8">
<Loading />
<Text className="text-gray-500 mt-2">...</Text>
</View>
) : inviteStats ? (
<View className="space-y-4">
<View className="grid grid-cols-2 gap-4">
<View className="text-center">
<Text className="text-2xl font-bold text-blue-500">
{inviteStats.totalInvites || 0}
</Text>
<Text className="text-sm text-gray-500"></Text>
</View>
<View className="text-center">
<Text className="text-2xl font-bold text-green-500">
{inviteStats.successfulRegistrations || 0}
</Text>
<Text className="text-sm text-gray-500"></Text>
</View>
</View>
<View className="grid grid-cols-2 gap-4">
<View className="text-center">
<Text className="text-2xl font-bold text-purple-500">
{inviteStats.conversionRate ? `${(inviteStats.conversionRate * 100).toFixed(1)}%` : '0%'}
</Text>
<Text className="text-sm text-gray-500"></Text>
</View>
<View className="text-center">
<Text className="text-2xl font-bold text-orange-500">
{inviteStats.todayInvites || 0}
</Text>
<Text className="text-sm text-gray-500"></Text>
</View>
</View>
{/* 邀请来源统计 */}
{inviteStats.sourceStats && inviteStats.sourceStats.length > 0 && (
<View className="mt-4">
<Text className="text-sm font-medium text-gray-700 mb-2"></Text>
<View className="space-y-2">
{inviteStats.sourceStats.map((source, index) => (
<View key={index} className="flex items-center justify-between py-2 px-3 bg-gray-50 rounded-lg">
<View className="flex items-center">
<View className="w-3 h-3 rounded-full bg-blue-500 mr-2"></View>
<Text className="text-sm text-gray-700">{source.source}</Text>
</View>
<View className="text-right">
<Text className="text-sm font-medium text-gray-800">{source.count}</Text>
<Text className="text-xs text-gray-500">
{source.conversionRate ? `${(source.conversionRate * 100).toFixed(1)}%` : '0%'}
</Text>
</View>
</View>
))}
</View>
</View>
)}
</View>
) : (
<View className="text-center py-8">
<Text className="text-gray-500"></Text>
<Button
size="small"
type="primary"
className="mt-2"
onClick={fetchInviteStats}
>
</Button>
</View>
)}
</View>
<Text className="text-sm text-gray-600 mb-4">
</Text>
<Button type="primary" className="mb-2">
</Button>
<Button>
</Button>
</View>
</View>
)

View File

@@ -1,55 +1,144 @@
import React, { useState } from 'react'
import React, { useState, useEffect, useCallback } from 'react'
import { View, Text } from '@tarojs/components'
import { Empty, Tabs, Avatar, Tag, Progress } from '@nutui/nutui-react-taro'
import { Empty, Tabs, Avatar, Tag, Progress, Loading, PullToRefresh } from '@nutui/nutui-react-taro'
import { User, Star, StarFill } from '@nutui/icons-react-taro'
import Taro from '@tarojs/taro'
import { useDealerUser } from '@/hooks/useDealerUser'
import { listShopDealerReferee } from '@/api/shop/shopDealerReferee'
import { pageShopDealerOrder } from '@/api/shop/shopDealerOrder'
import type { ShopDealerReferee } from '@/api/shop/shopDealerReferee/model'
interface TeamMemberWithStats extends ShopDealerReferee {
name?: string
avatar?: string
orderCount?: number
commission?: string
status?: 'active' | 'inactive'
subMembers?: number
joinTime?: string
}
const DealerTeam: React.FC = () => {
const [activeTab, setActiveTab] = useState('0')
const [loading, setLoading] = useState<boolean>(false)
const [refreshing, setRefreshing] = useState<boolean>(false)
const [teamMembers, setTeamMembers] = useState<TeamMemberWithStats[]>([])
const [teamStats, setTeamStats] = useState({
total: 0,
firstLevel: 0,
secondLevel: 0,
thirdLevel: 0,
monthlyCommission: '0.00'
})
// 模拟团队数据
const teamStats = {
total: 28,
firstLevel: 12,
secondLevel: 10,
thirdLevel: 6,
monthlyCommission: '2,580.50'
const { dealerUser } = useDealerUser()
// 获取团队数据
const fetchTeamData = useCallback(async () => {
if (!dealerUser?.userId) return
try {
setLoading(true)
// 获取团队成员关系
const refereeResult = await listShopDealerReferee({
dealerId: dealerUser.userId
})
if (refereeResult) {
// 处理团队成员数据
const processedMembers: TeamMemberWithStats[] = refereeResult.map(member => ({
...member,
name: `用户${member.userId}`,
avatar: '',
orderCount: 0,
commission: '0.00',
status: 'active' as const,
subMembers: 0,
joinTime: member.createTime
}))
// 并行获取每个成员的订单统计
const memberStats = await Promise.all(
processedMembers.map(async (member) => {
try {
const orderResult = await pageShopDealerOrder({
page: 1,
limit: 100,
userId: member.userId
})
if (orderResult?.list) {
const orders = orderResult.list
const orderCount = orders.length
const commission = orders.reduce((sum, order) => {
const levelCommission = member.level === 1 ? order.firstMoney :
member.level === 2 ? order.secondMoney :
order.thirdMoney
return sum + parseFloat(levelCommission || '0')
}, 0).toFixed(2)
// 判断活跃状态30天内有订单为活跃
const thirtyDaysAgo = new Date()
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30)
const hasRecentOrder = orders.some(order =>
new Date(order.createTime || '') > thirtyDaysAgo
)
return {
...member,
orderCount,
commission,
status: hasRecentOrder ? 'active' as const : 'inactive' as const
}
}
return member
} catch (error) {
console.error(`获取成员${member.userId}订单失败:`, error)
return member
}
})
)
setTeamMembers(memberStats)
// 计算统计数据
const stats = {
total: memberStats.length,
firstLevel: memberStats.filter(m => m.level === 1).length,
secondLevel: memberStats.filter(m => m.level === 2).length,
thirdLevel: memberStats.filter(m => m.level === 3).length,
monthlyCommission: memberStats.reduce((sum, member) =>
sum + parseFloat(member.commission || '0'), 0
).toFixed(2)
}
setTeamStats(stats)
}
} catch (error) {
console.error('获取团队数据失败:', error)
Taro.showToast({
title: '获取团队数据失败',
icon: 'error'
})
} finally {
setLoading(false)
}
}, [dealerUser?.userId])
// 刷新数据
const handleRefresh = async () => {
setRefreshing(true)
await fetchTeamData()
setRefreshing(false)
}
const teamMembers = [
{
id: '1',
name: '张小明',
level: 1,
joinTime: '2024-11-15',
orderCount: 15,
commission: '580.50',
status: 'active',
avatar: '',
subMembers: 3
},
{
id: '2',
name: '李小红',
level: 1,
joinTime: '2024-12-01',
orderCount: 8,
commission: '320.00',
status: 'active',
avatar: '',
subMembers: 2
},
{
id: '3',
name: '王小华',
level: 2,
joinTime: '2024-12-10',
orderCount: 5,
commission: '150.00',
status: 'inactive',
avatar: '',
subMembers: 0
// 初始化加载数据
useEffect(() => {
if (dealerUser?.userId) {
fetchTeamData().then()
}
]
}, [fetchTeamData])
const getLevelColor = (level: number) => {
switch (level) {
@@ -69,7 +158,7 @@ const DealerTeam: React.FC = () => {
}
}
const renderMemberItem = (member: any) => (
const renderMemberItem = (member: TeamMemberWithStats) => (
<View key={member.id} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
<View className="flex items-center mb-3">
<Avatar
@@ -83,7 +172,7 @@ const DealerTeam: React.FC = () => {
<Text className="font-semibold text-gray-800 mr-2">
{member.name}
</Text>
{getLevelIcon(member.level)}
{getLevelIcon(Number(member.level))}
<Text className="text-xs text-gray-500 ml-1">
{member.level}
</Text>
@@ -171,7 +260,7 @@ const DealerTeam: React.FC = () => {
<Progress
percent={(teamStats.firstLevel / teamStats.total) * 100}
strokeWidth="6"
background="#f59e0b"
background={'#f59e0b'}
className="w-20"
/>
</View>
@@ -187,7 +276,7 @@ const DealerTeam: React.FC = () => {
<Progress
percent={(teamStats.secondLevel / teamStats.total) * 100}
strokeWidth="6"
background="#8b5cf6"
background={'#8b5cf6'}
className="w-20"
/>
</View>
@@ -203,7 +292,7 @@ const DealerTeam: React.FC = () => {
<Progress
percent={(teamStats.thirdLevel / teamStats.total) * 100}
strokeWidth="6"
background="#ec4899"
background={'#ec4899'}
className="w-20"
/>
</View>
@@ -220,17 +309,38 @@ const DealerTeam: React.FC = () => {
)
const renderMemberList = (level?: number) => (
<View className="p-4">
{teamMembers
.filter(member => !level || member.level === level)
.map(renderMemberItem)}
{teamMembers.filter(member => !level || member.level === level).length === 0 && (
<Empty description={`暂无${level ? level + '级' : ''}团队成员`} />
)}
</View>
<PullToRefresh
loading={refreshing}
onRefresh={handleRefresh}
>
<View className="p-4">
{loading ? (
<View className="text-center py-8">
<Loading />
<Text className="text-gray-500 mt-2">...</Text>
</View>
) : teamMembers
.filter(member => !level || member.level === level)
.length > 0 ? (
teamMembers
.filter(member => !level || member.level === level)
.map(renderMemberItem)
) : (
<Empty description={`暂无${level ? level + '级' : ''}团队成员`} />
)}
</View>
</PullToRefresh>
)
if (!dealerUser) {
return (
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
<Loading />
<Text className="text-gray-500 mt-2">...</Text>
</View>
)
}
return (
<View className="bg-gray-50 min-h-screen">
<Tabs value={activeTab} onChange={() => setActiveTab}>

View File

@@ -1,4 +1,4 @@
import React, { useState, useRef } from 'react'
import React, { useState, useRef, useEffect, useCallback } from 'react'
import { View, Text } from '@tarojs/components'
import {
Cell,
@@ -9,63 +9,196 @@ import {
Radio,
Tabs,
Tag,
Empty
Empty,
Loading,
PullToRefresh
} from '@nutui/nutui-react-taro'
import { Wallet } from '@nutui/icons-react-taro'
import { businessGradients } from '@/styles/gradients'
import Taro from '@tarojs/taro'
import { useDealerUser } from '@/hooks/useDealerUser'
import { pageShopDealerWithdraw, addShopDealerWithdraw } from '@/api/shop/shopDealerWithdraw'
import type { ShopDealerWithdraw } from '@/api/shop/shopDealerWithdraw/model'
interface WithdrawRecordWithDetails extends ShopDealerWithdraw {
accountDisplay?: string
}
const DealerWithdraw: React.FC = () => {
const [activeTab, setActiveTab] = useState('0')
const [selectedAccount, setSelectedAccount] = useState('')
const [loading, setLoading] = useState<boolean>(false)
const [refreshing, setRefreshing] = useState<boolean>(false)
const [submitting, setSubmitting] = useState<boolean>(false)
const [availableAmount, setAvailableAmount] = useState<string>('0.00')
const [withdrawRecords, setWithdrawRecords] = useState<WithdrawRecordWithDetails[]>([])
const formRef = useRef<any>(null)
// 模拟可提现金额
const availableAmount = '1,288.50'
const { dealerUser } = useDealerUser()
// 模拟提现记录
const withdrawRecords = [
{
id: '1',
amount: '500.00',
account: '尾号1234',
status: 'completed',
createTime: '2024-12-15 10:30:00',
completeTime: '2024-12-15 16:20:00'
},
{
id: '2',
amount: '300.00',
account: '尾号1234',
status: 'pending',
createTime: '2024-12-18 09:15:00'
// 获取可提现余额
const fetchBalance = useCallback(async () => {
try {
setAvailableAmount(dealerUser?.money || '0.00')
} catch (error) {
console.error('获取余额失败:', error)
}
]
}, [])
const getStatusText = (status: string) => {
// 获取提现记录
const fetchWithdrawRecords = useCallback(async () => {
if (!dealerUser?.userId) return
try {
setLoading(true)
const result = await pageShopDealerWithdraw({
page: 1,
limit: 100,
userId: dealerUser.userId
})
if (result?.list) {
const processedRecords = result.list.map(record => ({
...record,
accountDisplay: getAccountDisplay(record)
}))
setWithdrawRecords(processedRecords)
}
} catch (error) {
console.error('获取提现记录失败:', error)
Taro.showToast({
title: '获取提现记录失败',
icon: 'error'
})
} finally {
setLoading(false)
}
}, [dealerUser?.userId])
// 格式化账户显示
const getAccountDisplay = (record: ShopDealerWithdraw) => {
if (record.payType === 10) {
return '微信钱包'
} else if (record.payType === 20 && record.alipayAccount) {
return `支付宝(${record.alipayAccount.slice(-4)})`
} else if (record.payType === 30 && record.bankCard) {
return `${record.bankName || '银行卡'}(尾号${record.bankCard.slice(-4)})`
}
return '未知账户'
}
// 刷新数据
const handleRefresh = async () => {
setRefreshing(true)
await Promise.all([fetchBalance(), fetchWithdrawRecords()])
setRefreshing(false)
}
// 初始化加载数据
useEffect(() => {
if (dealerUser?.userId) {
fetchBalance().then()
fetchWithdrawRecords().then()
}
}, [fetchBalance, fetchWithdrawRecords])
const getStatusText = (status?: number) => {
switch (status) {
case 'completed': return '已到账'
case 'pending': return '处理中'
case 'rejected': return '已拒绝'
case 40: return '已到账'
case 20: return '审核通过'
case 10: return '待审核'
case 30: return '已驳回'
default: return '未知'
}
}
const getStatusColor = (status: string) => {
const getStatusColor = (status?: number) => {
switch (status) {
case 'completed': return 'success'
case 'pending': return 'warning'
case 'rejected': return 'danger'
case 40: return 'success'
case 20: return 'success'
case 10: return 'warning'
case 30: return 'danger'
default: return 'default'
}
}
const handleSubmit = (values: any) => {
console.log('提现申请:', values)
Taro.showToast({
title: '提现申请已提交',
icon: 'success'
})
const handleSubmit = async (values: any) => {
if (!dealerUser?.userId) {
Taro.showToast({
title: '用户信息获取失败',
icon: 'error'
})
return
}
// 验证提现金额
const amount = parseFloat(values.amount)
const available = parseFloat(availableAmount.replace(',', ''))
if (amount < 100) {
Taro.showToast({
title: '最低提现金额为100元',
icon: 'error'
})
return
}
if (amount > available) {
Taro.showToast({
title: '提现金额超过可用余额',
icon: 'error'
})
return
}
try {
setSubmitting(true)
const withdrawData: ShopDealerWithdraw = {
userId: dealerUser.userId,
money: values.amount,
payType: values.accountType === 'wechat' ? 10 :
values.accountType === 'alipay' ? 20 : 30,
applyStatus: 10, // 待审核
platform: 'MiniProgram'
}
// 根据提现方式设置账户信息
if (values.accountType === 'alipay') {
withdrawData.alipayAccount = values.account
withdrawData.alipayName = values.accountName
} else if (values.accountType === 'bank') {
withdrawData.bankCard = values.account
withdrawData.bankAccount = values.accountName
withdrawData.bankName = values.bankName || '银行卡'
}
await addShopDealerWithdraw(withdrawData)
Taro.showToast({
title: '提现申请已提交',
icon: 'success'
})
// 重置表单
formRef.current?.resetFields()
setSelectedAccount('')
// 刷新数据
await handleRefresh()
// 切换到提现记录页面
setActiveTab('1')
} catch (error: any) {
console.error('提现申请失败:', error)
Taro.showToast({
title: error.message || '提现申请失败',
icon: 'error'
})
} finally {
setSubmitting(false)
}
}
const quickAmounts = ['100', '300', '500', '1000']
@@ -165,22 +298,49 @@ const DealerWithdraw: React.FC = () => {
</Radio.Group>
</Form.Item>
<Form.Item name="account" label="账户信息" required>
<Input placeholder="请输入账户号码" />
</Form.Item>
{selectedAccount === 'alipay' && (
<>
<Form.Item name="account" label="支付宝账号" required>
<Input placeholder="请输入支付宝账号" />
</Form.Item>
<Form.Item name="accountName" label="支付宝姓名" required>
<Input placeholder="请输入支付宝实名姓名" />
</Form.Item>
</>
)}
<Form.Item name="accountName" label="账户姓名" required>
<Input placeholder="请输入账户姓名" />
</Form.Item>
{selectedAccount === 'bank' && (
<>
<Form.Item name="bankName" label="开户银行" required>
<Input placeholder="请输入开户银行名称" />
</Form.Item>
<Form.Item name="account" label="银行卡号" required>
<Input placeholder="请输入银行卡号" />
</Form.Item>
<Form.Item name="accountName" label="开户姓名" required>
<Input placeholder="请输入银行卡开户姓名" />
</Form.Item>
</>
)}
<Form.Item name="remark" label="备注">
<Input placeholder="请输入备注信息(可选)" />
</Form.Item>
{selectedAccount === 'wechat' && (
<View className="px-4 py-2">
<Text className="text-sm text-gray-500">
</Text>
</View>
)}
</CellGroup>
<View className="mt-6 px-4">
<Button block type="primary" nativeType="submit">
<Button
block
type="primary"
nativeType="submit"
loading={submitting}
disabled={submitting || !selectedAccount}
>
{submitting ? '提交中...' : '申请提现'}
</Button>
</View>
</Form>
@@ -188,40 +348,64 @@ const DealerWithdraw: React.FC = () => {
)
const renderWithdrawRecords = () => (
<View className="p-4">
{withdrawRecords.length > 0 ? (
withdrawRecords.map(record => (
<View key={record.id} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
<View className="flex justify-between items-start mb-3">
<View>
<Text className="font-semibold text-gray-800 mb-1">
¥{record.amount}
</Text>
<Text className="text-sm text-gray-500">
{record.account}
</Text>
</View>
<Tag type={getStatusColor(record.status)}>
{getStatusText(record.status)}
</Tag>
</View>
<View className="text-xs text-gray-400">
<Text>{record.createTime}</Text>
{record.completeTime && (
<Text className="block mt-1">
{record.completeTime}
</Text>
)}
</View>
<PullToRefresh
loading={refreshing}
onRefresh={handleRefresh}
>
<View className="p-4">
{loading ? (
<View className="text-center py-8">
<Loading />
<Text className="text-gray-500 mt-2">...</Text>
</View>
))
) : (
<Empty description="暂无提现记录" />
)}
</View>
) : withdrawRecords.length > 0 ? (
withdrawRecords.map(record => (
<View key={record.id} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
<View className="flex justify-between items-start mb-3">
<View>
<Text className="font-semibold text-gray-800 mb-1">
¥{record.money}
</Text>
<Text className="text-sm text-gray-500">
{record.accountDisplay}
</Text>
</View>
<Tag type={getStatusColor(record.applyStatus)}>
{getStatusText(record.applyStatus)}
</Tag>
</View>
<View className="text-xs text-gray-400">
<Text>{record.createTime}</Text>
{record.auditTime && (
<Text className="block mt-1">
{new Date(record.auditTime).toLocaleString()}
</Text>
)}
{record.rejectReason && (
<Text className="block mt-1 text-red-500">
{record.rejectReason}
</Text>
)}
</View>
</View>
))
) : (
<Empty description="暂无提现记录" />
)}
</View>
</PullToRefresh>
)
if (!dealerUser) {
return (
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
<Loading />
<Text className="text-gray-500 mt-2">...</Text>
</View>
)
}
return (
<View className="bg-gray-50 min-h-screen">
<Tabs value={activeTab} onChange={() => setActiveTab}>