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,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}>