forked from gxwebsoft/mp-10550
- 新增 bindRefereeRelation 接口替换原有的 createInviteRelation 接口 - 优化邀请参数解析逻辑,支持 uid_xxx 格式的邀请码 - 重构 handleInviteRelation 函数,使用新的绑定推荐关系接口 - 新增 checkAndHandleInviteRelation 和 manualHandleInviteRelation 函数 - 优化首页和订单列表的相关逻辑,以支持新的邀请关系建立流程 - 更新文档中的相关描述,如将"下级成员"改为"团队成员"
368 lines
12 KiB
TypeScript
368 lines
12 KiB
TypeScript
import React, { useState, useEffect, useCallback } from 'react'
|
||
import { View, Text } from '@tarojs/components'
|
||
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 { 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)
|
||
}
|
||
|
||
// 初始化加载数据
|
||
useEffect(() => {
|
||
if (dealerUser?.userId) {
|
||
fetchTeamData().then()
|
||
}
|
||
}, [fetchTeamData])
|
||
|
||
const getLevelColor = (level: number) => {
|
||
switch (level) {
|
||
case 1: return '#f59e0b'
|
||
case 2: return '#8b5cf6'
|
||
case 3: return '#ec4899'
|
||
default: return '#6b7280'
|
||
}
|
||
}
|
||
|
||
const getLevelIcon = (level: number) => {
|
||
switch (level) {
|
||
case 1: return <StarFill color={getLevelColor(level)} size="16" />
|
||
case 2: return <Star color={getLevelColor(level)} size="16" />
|
||
case 3: return <User color={getLevelColor(level)} size="16" />
|
||
default: return <User color={getLevelColor(level)} size="16" />
|
||
}
|
||
}
|
||
|
||
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
|
||
size="40"
|
||
src={member.avatar}
|
||
icon={<User />}
|
||
className="mr-3"
|
||
/>
|
||
<View className="flex-1">
|
||
<View className="flex items-center mb-1">
|
||
<Text className="font-semibold text-gray-800 mr-2">
|
||
{member.name}
|
||
</Text>
|
||
{getLevelIcon(Number(member.level))}
|
||
<Text className="text-xs text-gray-500 ml-1">
|
||
{member.level}级
|
||
</Text>
|
||
</View>
|
||
<Text className="text-xs text-gray-500">
|
||
加入时间:{member.joinTime}
|
||
</Text>
|
||
</View>
|
||
<View className="text-right">
|
||
<Tag
|
||
type={member.status === 'active' ? 'success' : 'default'}
|
||
>
|
||
{member.status === 'active' ? '活跃' : '沉默'}
|
||
</Tag>
|
||
</View>
|
||
</View>
|
||
|
||
<View className="grid grid-cols-3 gap-4 text-center">
|
||
<View>
|
||
<Text className="text-sm font-semibold text-blue-600">
|
||
{member.orderCount}
|
||
</Text>
|
||
<Text className="text-xs text-gray-500">订单数</Text>
|
||
</View>
|
||
<View>
|
||
<Text className="text-sm font-semibold text-green-600">
|
||
¥{member.commission}
|
||
</Text>
|
||
<Text className="text-xs text-gray-500">贡献佣金</Text>
|
||
</View>
|
||
<View>
|
||
<Text className="text-sm font-semibold text-purple-600">
|
||
{member.subMembers}
|
||
</Text>
|
||
<Text className="text-xs text-gray-500">团队成员</Text>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
)
|
||
|
||
const renderOverview = () => (
|
||
<View className="p-4">
|
||
{/* 团队统计卡片 */}
|
||
<View className="rounded-xl p-6 mb-6 text-white relative overflow-hidden" style={{
|
||
background: 'linear-gradient(135deg, #8b5cf6 0%, #ec4899 100%)'
|
||
}}>
|
||
{/* 装饰背景 - 小程序兼容版本 */}
|
||
<View className="absolute w-32 h-32 rounded-full" style={{
|
||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||
top: '-16px',
|
||
right: '-16px'
|
||
}}></View>
|
||
<View className="absolute w-20 h-20 rounded-full" style={{
|
||
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||
bottom: '-10px',
|
||
left: '-10px'
|
||
}}></View>
|
||
|
||
<View className="relative z-10">
|
||
<Text className="text-lg font-bold mb-4 text-white">团队总览</Text>
|
||
<View className="grid grid-cols-2 gap-4">
|
||
<View>
|
||
<Text className="text-2xl font-bold mb-1 text-white">{teamStats.total}</Text>
|
||
<Text className="text-sm" style={{ color: 'rgba(255, 255, 255, 0.8)' }}>团队总人数</Text>
|
||
</View>
|
||
<View>
|
||
<Text className="text-2xl font-bold mb-1 text-white">¥{teamStats.monthlyCommission}</Text>
|
||
<Text className="text-sm" style={{ color: 'rgba(255, 255, 255, 0.8)' }}>本月团队佣金</Text>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
|
||
{/* 层级分布 */}
|
||
<View className="bg-white rounded-xl p-4 mb-4">
|
||
<Text className="font-semibold mb-4 text-gray-800">层级分布</Text>
|
||
<View className="gap-2">
|
||
<View className="flex items-center justify-between">
|
||
<View className="flex items-center">
|
||
<StarFill color="#f59e0b" size="16" className="mr-2" />
|
||
<Text className="text-sm">一级成员</Text>
|
||
</View>
|
||
<View className="flex items-center">
|
||
<Text className="text-sm font-semibold mr-2">{teamStats.firstLevel}</Text>
|
||
<Progress
|
||
percent={(teamStats.firstLevel / teamStats.total) * 100}
|
||
strokeWidth="6"
|
||
background={'#f59e0b'}
|
||
className="w-20"
|
||
/>
|
||
</View>
|
||
</View>
|
||
|
||
<View className="flex items-center justify-between">
|
||
<View className="flex items-center">
|
||
<Star color="#8b5cf6" size="16" className="mr-2" />
|
||
<Text className="text-sm">二级成员</Text>
|
||
</View>
|
||
<View className="flex items-center">
|
||
<Text className="text-sm font-semibold mr-2">{teamStats.secondLevel}</Text>
|
||
<Progress
|
||
percent={(teamStats.secondLevel / teamStats.total) * 100}
|
||
strokeWidth="6"
|
||
background={'#8b5cf6'}
|
||
className="w-20"
|
||
/>
|
||
</View>
|
||
</View>
|
||
|
||
<View className="flex items-center justify-between">
|
||
<View className="flex items-center">
|
||
<User color="#ec4899" size="16" className="mr-2" />
|
||
<Text className="text-sm">三级成员</Text>
|
||
</View>
|
||
<View className="flex items-center">
|
||
<Text className="text-sm font-semibold mr-2">{teamStats.thirdLevel}</Text>
|
||
<Progress
|
||
percent={(teamStats.thirdLevel / teamStats.total) * 100}
|
||
strokeWidth="6"
|
||
background={'#ec4899'}
|
||
className="w-20"
|
||
/>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
|
||
{/* 最新成员 */}
|
||
<View className="bg-white rounded-xl p-4">
|
||
<Text className="font-semibold mb-4 text-gray-800">最新成员</Text>
|
||
{teamMembers.slice(0, 3).map(renderMemberItem)}
|
||
</View>
|
||
</View>
|
||
)
|
||
|
||
const renderMemberList = (level?: number) => (
|
||
<PullToRefresh
|
||
disabled={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}>
|
||
<Tabs.TabPane title="团队总览" value="0">
|
||
{renderOverview()}
|
||
</Tabs.TabPane>
|
||
|
||
<Tabs.TabPane title="一级成员" value="1">
|
||
{renderMemberList(1)}
|
||
</Tabs.TabPane>
|
||
|
||
<Tabs.TabPane title="二级成员" value="2">
|
||
{renderMemberList(2)}
|
||
</Tabs.TabPane>
|
||
|
||
<Tabs.TabPane title="三级成员" value="3">
|
||
{renderMemberList(3)}
|
||
</Tabs.TabPane>
|
||
</Tabs>
|
||
</View>
|
||
)
|
||
}
|
||
|
||
export default DealerTeam
|