Files
template-10584/src/dealer/team/index.tsx
赵忠林 7708968f53 feat(invite): 重构邀请关系建立流程并优化相关功能
- 新增 bindRefereeRelation 接口替换原有的 createInviteRelation 接口
- 优化邀请参数解析逻辑,支持 uid_xxx 格式的邀请码
- 重构 handleInviteRelation 函数,使用新的绑定推荐关系接口
- 新增 checkAndHandleInviteRelation 和 manualHandleInviteRelation 函数
- 优化首页和订单列表的相关逻辑,以支持新的邀请关系建立流程
- 更新文档中的相关描述,如将"下级成员"改为"团队成员"
2025-08-23 12:18:32 +08:00

368 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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