@@ -1,444 +1,256 @@
import React , { useState , useEffect , useCallback } from 'react'
import { useRef , useState } from 'react'
import Taro , { useDidShow } from '@tarojs/taro'
import { View , Text } from '@tarojs/components'
import { View , Text } from '@tarojs/components'
import { Phone , Edit , Message } from '@nutui/icons-react-taro'
import {
import { Space , Empty , Avatar , Button } from '@nutui/nutui-react-taro'
Avatar ,
import Taro from '@tarojs/taro'
Button ,
import { useDealerUser } from '@/hooks/useDealerUser'
Empty ,
import { listShopDealerReferee } from '@/api/shop/shopDealerReferee'
InfiniteLoading ,
import { pageShopDealerOrder } from '@/api/shop/shopDealerOrder'
Loading ,
import type { ShopDealer Refe ree } from '@/api/shop/shopDealerReferee/model'
PullTo Refresh ,
import FixedButton from "@/components/FixedButton" ;
SearchBar ,
import navTo from "@/utils/common" ;
Tag
import { updateUser } from "@/api/system/user" ;
} from '@nutui/nutui-react-taro'
import type { User } from '@/api/system/user/model'
import { pageUsers } from '@/api/system/user'
import { listUserRole , updateUserRole } from '@/api/system/userRole'
import { listRoles } from '@/api/system/role'
interface TeamMemberWithStats extends ShopDealerReferee {
const PAGE_SIZE = 10
name? : string
avatar? : string
nickname? : string ;
alias? : string ;
phone? : string ;
orderCount? : number
commission? : string
status ? : 'active' | 'inactive'
subMembers? : number
joinTime? : string
dealerAvatar? : string ;
dealerName? : string ;
dealerPhone? : string ;
}
// 层级信息接口
const AdminUsers = ( ) = > {
interface LevelInfo {
const [ searchValue , setSearchValue ] = useState ( '' )
dealerId : number
dealerName? : string
level : number
}
const DealerTeam : React.FC = ( ) = > {
const [ users , setUsers ] = useState < User [ ] > ( [ ] )
const [ teamMembers , setTeamMembers ] = useState < TeamMemberWithStats [ ] > ( [ ] )
const { dealerUser } = useDealerUser ( )
const [ dealerId , setDealerId ] = useState < number > ( )
// 层级栈,用于支持返回上一层
const [ levelStack , setLevelStack ] = useState < LevelInfo [ ] > ( [ ] )
const [ loading , setLoading ] = useState ( false )
const [ loading , setLoading ] = useState ( false )
// 当前查看的用户名称
const [ hasMore , setHasMore ] = useState ( true )
const [ currentDealerName , setCurrentDealerName ] = useState < string > ( '' )
const [ page , setPage ] = useState ( 1 )
const [ total , setTotal ] = useState ( 0 )
// 异步加载成员统计数据
const roleIdMapRef = useRef < Record < string , number > > ( { } )
const loadMemberStats = async ( members : TeamMemberWithStats [ ] ) = > {
const roleMapLoadedRef = useRef ( false )
// 分批处理,避免过多并发请求
const batchSize = 3
for ( let i = 0 ; i < members . length ; i += batchSize ) {
const batch = members . slice ( i , i + batchSize )
const batchStats = await Promise . all (
const getRoleIdByCode = async ( roleCode : string ) = > {
batch . map ( async ( member ) = > {
if ( ! roleMapLoadedRef . current ) {
try {
const roles = await listRoles ( )
// 并行获取订单统计和下级成员数量
const nextMap : Record < string , number > = { }
const [ orderResult , subMembersResult ] = await Promise . all ( [
roles ? . f orEach ( role = > {
pageShopDealerOr der ( {
if ( role . roleCode && role . roleId ) nextMap [ role . roleCo de] = role . roleId
page : 1 ,
userId : member.userId
} ) ,
listShopDealerReferee ( {
dealerId : member.userId ,
deleted : 0
} )
} )
] )
roleIdMapRef . current = nextMap
roleMapLoadedRef . current = true
let orderCount = 0
}
let commission = '0.00'
return roleIdMapRef . current [ roleCode ]
let status : 'active' | 'inactive' = 'inactive'
if ( orderResult ? . list ) {
const orders = orderResult . list
orderCount = orders . length
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
)
status = hasRecentOrder ? 'active' : 'inactive'
}
}
return {
const reload = async ( isRefresh = false , overrideKeywords? : string ) = > {
. . . member ,
if ( loading ) return
orderCount ,
commission ,
status ,
subMembers : subMembersResult?.length || 0
}
} catch ( error ) {
console . error ( ` 获取成员 ${ member . userId } 数据失败: ` , error )
return {
. . . member ,
orderCount : 0 ,
commission : '0.00' ,
status : 'inactive' as const ,
subMembers : 0
}
}
} )
)
// 更新这一批成员的数据
if ( isRefresh ) {
setTeamMembers ( prevMembers = > {
setPage ( 1 )
const updatedMembers = [ . . . prevMembers ]
setUsers ( [ ] )
batchStats . forEach ( updatedMember = > {
setHasMore ( true )
const index = updatedMembers . findIndex ( m = > m . userId === updatedMember . userId )
if ( index !== - 1 ) {
updatedMembers [ index ] = updatedMember
}
} )
return updatedMembers
} )
// 添加小延迟,避免请求过于密集
if ( i + batchSize < members . length ) {
await new Promise ( resolve = > setTimeout ( resolve , 100 ) )
}
}
}
}
// 获取团队数据
const fetchTeamData = useCallback ( async ( ) = > {
if ( ! dealerUser ? . userId && ! dealerId ) return
try {
setLoading ( true )
setLoading ( true )
console . log ( dealerId , 'dealerId>>>>>>>>>' )
try {
// 获取团队成员关系
const currentPage = isRefresh ? 1 : page
const refereeResult = await listShopDealerReferee ( {
const res = await pageUsers ( {
dealerId : dealerId ? dealerId : dealerUser?.userId
page : currentPage ,
limit : PAGE_SIZE ,
keywords : overrideKeywords ? ? searchValue
} )
} )
if ( refereeResul t ) {
if ( res ? . lis t ) {
console . log ( '团队成员原始数据:' , refereeResul t)
const nextUsers = isRefresh ? res . list : [ . . . users , . . . res . lis t]
// 处理团队成员数据
setUsers ( nextUsers )
const processedMembers : TeamMemberWithStats [ ] = refereeResult . map ( member = > ( {
setTotal ( res . count || 0 )
. . . member ,
setHasMore ( res . list . length === PAGE_SIZE )
name : ` ${ member . userId } ` ,
setPage ( isRefresh ? 2 : currentPage + 1 )
orderCount : 0 ,
} else {
commission : '0.00' ,
setUsers ( [ ] )
status : 'active' as const ,
setTotal ( 0 )
subMembers : 0 ,
setHasMore ( false )
joinTime : member.createTime
} ) )
// 先显示基础数据,然后异步加载详细统计
setTeamMembers ( processedMembers )
setLoading ( false )
// 异步加载每个成员的详细统计数据
loadMemberStats ( processedMembers )
}
}
} catch ( error ) {
} catch ( error ) {
console . error ( '获取团队数据 失败:' , error )
console . error ( '获取用户列表 失败:' , error )
Taro . showToast ( {
Taro . showToast ( { title : '获取用户列表失败' , icon : 'error' } )
title : '获取团队数据失败' ,
icon : 'error'
} )
} finally {
} finally {
setLoading ( false )
setLoading ( false )
}
}
} , [ dealerUser ? . userId , dealerId ] )
// 查看下级成员
const getNextUser = ( item : TeamMemberWithStats ) = > {
// 检查层级限制: 最多只能查看2层( levelStack.length >= 1 表示已经是第2层了)
if ( levelStack . length >= 1 ) {
return
}
}
// 如果没有下级成员,不允许点击
const getUserRoleCodes = ( target : User ) : string [ ] = > {
if ( ! item . subMembers || item . subMembers === 0 ) {
const fromRoles = target . roles ? . map ( r = > r . roleCode ) . filter ( Boolean ) as string [ ] | undefined
return
const fromSingle = target . roleCode ? [ target . roleCode ] : [ ]
const merged = [ . . . ( fromRoles || [ ] ) , . . . fromSingle ] . filter ( Boolean )
return Array . from ( new Set ( merged ) )
}
}
console . log ( '点击用户:' , item . userId , item . name )
const getPrimaryRoleCode = ( target : User ) : string | undefined = > {
const codes = getUserRoleCodes ( target )
// 将当前层级信息推入栈中
if ( codes . includes ( 'superAdmin' ) ) return 'superAdmin'
const currentLevel : LevelInfo = {
if ( codes . includes ( 'admin' ) ) return 'admin'
dealerId : dealerId || dealerUser ? . userId || 0 ,
if ( codes . includes ( 'dealer' ) ) return 'dealer'
dealerName : currentDealerName || ( dealerId ? '上级' : dealerUser ? . realName || '我' ) ,
if ( codes . includes ( 'user' ) ) return 'user'
level : levelStack.length
return codes [ 0 ]
}
setLevelStack ( prev = > [ . . . prev , currentLevel ] )
// 切换到下级
setDealerId ( item . userId )
setCurrentDealerName ( item . nickname || item . dealerName || ` 用户 ${ item . userId } ` )
}
}
// 返回上一层
const renderRoleTag = ( target : User ) = > {
const goBack = ( ) = > {
const code = getPrimaryRoleCode ( target )
if ( levelStack . length === 0 ) {
if ( code === 'superAdmin' ) return < Tag type = "danger" > 超 级 管 理 员 < / Tag >
// 如果栈为空,返回首页或上一页
if ( code === 'admin' ) return < Tag type = "danger" > 管 理 员 < / Tag >
Taro . navigateBack ( )
if ( code === 'dealer' ) return < Tag type = "primary" > 业 务 员 < / Tag >
return
if ( code === 'user' ) return < Tag type = "success" > 注 册 会 员 < / Tag >
return < Tag > 未 知 < / Tag >
}
}
// 从栈中弹出上一层信息
const toggleRole = async ( target : User ) = > {
const prevLevel = levelStack [ levelStack . length - 1 ]
const current = getPrimaryRoleCode ( target )
setLevelStack ( prev = > prev . slice ( 0 , - 1 ) )
const nextRoleCode = current === 'dealer' ? 'user' : 'dealer'
const nextRoleName = nextRoleCode === 'user' ? '注册会员' : '业务员'
if ( prevLevel . dealerId === ( dealerUser ? . userId || 0 ) ) {
const confirmRes = await Taro . showModal ( {
// 返回到根层级
title : '确认切换角色' ,
setDealerId ( undefined )
content : ` 确定将该用户切换为「 ${ nextRoleName } 」吗? `
setCurrentDealerName ( '' )
} )
} else {
if ( ! confirmRes . confirm ) return
setDealerId ( prevLevel . dealerId )
setCurrentDealerName ( prevLevel . dealerName || '' )
}
}
// 一键拨打
const makePhoneCall = ( phone : string ) = > {
Taro . makePhoneCall ( {
phoneNumber : phone ,
fail : ( ) = > {
Taro . showToast ( {
title : '拨打取消' ,
icon : 'error'
} ) ;
}
} ) ;
} ;
// 别名备注
const editAlias = ( item : any , index : number ) = > {
Taro . showModal ( {
title : '备注' ,
// @ts-ignore
editable : true ,
placeholderText : '真实姓名' ,
content : item.alias || '' ,
success : async ( res : any ) = > {
if ( res . confirm && res . content !== undefined ) {
try {
try {
// 更新跟进情况
const userId = target . userId
await updateUser ( {
if ( ! userId ) return
userId : item.userId ,
alias : res.content.trim ( )
const nextRoleId = await getRoleIdByCode ( nextRoleCode )
} ) ;
if ( ! nextRoleId ) {
teamMembers [ index ] . alias = res . content . trim ( )
throw new Error ( ` 未找到角色配置: ${ nextRoleCode } ` )
setTeamMembers ( teamMembers )
}
const roles = await listUserRole ( { userId } )
const candidate = roles ? . find ( r = > r . roleCode === 'dealer' || r . roleCode === 'user' )
if ( candidate ) {
await updateUserRole ( {
. . . candidate ,
roleId : nextRoleId
} )
} else {
await updateUserRole ( {
userId ,
roleId : nextRoleId
} )
}
Taro . showToast ( { title : '切换成功' , icon : 'success' } )
await reload ( true )
} catch ( error ) {
} catch ( error ) {
console . error ( '备注 失败:' , error ) ;
console . error ( '切换角色 失败:' , error )
Taro . showToast ( {
Taro . showToast ( { title : '切换失败' , icon : 'error' } )
title : '备注失败,请重试' ,
icon : 'error'
} ) ;
}
}
}
}
}
} ) ;
} ;
// 发送消息
const sendMessage = ( item : TeamMemberWithStats ) = > {
return navTo ( ` /user/chat/message/add?id= ${ item . userId } ` , true )
}
// 监听数据变化,获取团队数据
const handleSearch = ( value : string ) = > {
u seEffect ( ( ) = > {
setSearchValue ( value )
if ( dealerUser ? . userId || dealerId ) {
reload ( true , value ) . then ( )
fetchTeamData ( ) . then ( )
}
}
} , [ fetchTeamData ] )
// 初始化当前用户名称
const loadMore = async ( ) = > {
useEffect ( ( ) = > {
if ( ! loading && hasMore ) {
if ( ! dealerId && dealerUser ? . realName && ! currentDealerName ) {
await reload ( false )
setCurrentDealerName ( dealerUser . realName )
}
}
}
} , [ dealerUser , dealerId , currentDealerName ] )
const renderMemberItem = ( member : TeamMemberWithStats , index : number ) = > {
useDidShow ( ( ) = > {
// 判断是否可以点击:有下级成员且未达到层级限制
const init = async ( ) = > {
const canClick = member . subMembers && member . subMembers > 0 && levelStack . length < 1
try {
// 判断是否显示手机号: 只有本级( levelStack.length === 0) 才显示
await reload ( true )
const showPhone = levelStack . length === 0
} catch ( error ) {
// 判断数据是否还在加载中( 初始值都是0或'0.00')
console . error ( '初始化失败:' , error )
const isStatsLoading = member . orderCount === 0 && member . commission === '0.00' && member . subMembers === 0
Taro . showToast ( { title : '初始化失败' , icon : 'error' } )
}
}
init ( ) . then ( )
} )
return (
return (
< View
< View className = "bg-gray-50 min-h-screen" >
key = { member . id }
className = { ` bg-white rounded-lg p-4 mb-3 shadow-sm ${
< View className = "py-2 px-3" >
canClick ? 'cursor-pointer' : 'cursor-default opacity-75'
< SearchBar
} ` }
placeholder = "搜索昵称/手机号/UID"
onClick = { ( ) = > getNextUser ( member ) }
value = { searchValue }
>
onChange = { setSearchValue }
< View className = "flex items-center mb-3" >
onSearch = { handleSearch }
< Avatar
size = "40"
src = { member . avatar }
className = "mr-3"
/ >
/ >
< View className = "flex-1" >
< View className = "flex items-center justify-between mb-1" >
< View className = "flex items-center" >
< Space >
{ member . alias ? < Text className = "font-semibold text-blue-700 mr-2" > { member . alias } < / Text > :
< Text className = "font-semibold text-gray-800 mr-2" > { member . nickname } < / Text > }
{ /*别名备注*/ }
< Edit size = { 16 } className = { 'text-blue-500 mr-2' } onClick = { ( e ) = > {
e . stopPropagation ( )
editAlias ( member , index )
} } / >
{ /*发送消息*/ }
< Message size = { 16 } className = { 'text-orange-500 mr-2' } onClick = { ( e ) = > {
e . stopPropagation ( )
sendMessage ( member )
} } / >
< / Space >
< / View >
< / View >
{ /* 显示手机号(仅本级可见) */ }
{ showPhone && member . phone && (
{ total > 0 && (
< Text className = "text-sm text-gray-500" onClick = { ( e ) = > {
< View className = "px-4 py-2 text-sm text-gray-500" >
e . s topPropagation ( ) ;
共 找 到 { total } 个 成 员
makePhoneCall ( member . phone || '' ) ;
< / View >
} } >
{ member . phone }
< Phone size = { 12 } className = "ml-1 text-green-500" / >
< / Text >
) }
) }
< / View >
< Space >
< Text >
< Text className = "text-xs text-gray-500" > UID : { member . userId } < / Text >
< / Text >
< Text className = "text-xs text-gray-500" >
加 入 时 间 : { member . joinTime }
< / Text >
< / Space >
< / View >
< / View >
< View className = "grid grid-cols-3 gap-4 text-center" >
< PullToRefresh onRefresh = { ( ) = > reload ( true ) } headHeight = { 60 } >
< Space >
< View className = "px-4" style = { { height : 'calc(100vh - 190px)' , overflowY : 'auto' } } id = "users-scroll" >
< Text className = "text-xs text-gray-500" > 订 单 数 < / Text >
{ users . length === 0 && ! loading ? (
< Text className = "text-sm font-semibold text-blue-600" >
< View className = "flex flex-col justify-center items-center" style = { { height : 'calc(100vh - 260px)' } } >
{ isStatsLoading ? '-' : member . orderCount }
< Empty description = "暂无成员数据" style = { { backgroundColor : 'transparent' } } / >
< / Text >
< / Space >
< Space >
< Text className = "text-xs text-gray-500" > 贡 献 佣 金 < / Text >
< Text className = "text-sm font-semibold text-green-600" >
{ isStatsLoading ? '-' : ` ¥ ${ member . commission } ` }
< / Text >
< / Space >
< Space >
< Text className = "text-xs text-gray-500" > 团 队 成 员 < / Text >
< Text className = { ` text-sm font-semibold ${
canClick ? 'text-purple-600' : 'text-gray-400'
} ` } >
{ isStatsLoading ? '-' : ( member . subMembers || 0 ) }
< / Text >
< / Space >
< / View >
< / View >
) : (
< InfiniteLoading
target = "users-scroll"
hasMore = { hasMore }
onLoadMore = { loadMore }
loadingText = {
< View className = "flex justify-center items-center py-4" >
< Loading / >
< View className = "ml-2" > 加 载 中 . . . < / View >
< / View >
< / View >
)
}
}
loadMoreText = {
const r end erOverview = ( ) = > (
< View className = "text-c ent er py-4 text-gray-500" >
< View className = "rounded-xl p-4" >
{ users . length === 0 ? '暂无数据' : '没有更多了' }
< View
className = { 'bg-white rounded-lg py-2 px-4 mb-3 shadow-sm text-right text-sm font-medium flex justify-between items-center' } >
< Text className = "text-lg font-semibold" > 我 的 团 队 成 员 < / Text >
< Text className = { 'text-gray-500 ' } > 成 员 数 : { teamMembers . length } < / Text >
< / View >
< / View >
{ teamMembers . map ( renderMemberItem ) }
}
< / View >
>
)
{ users . map ( ( item , index ) = > {
const displayName = item . alias || item . nickname || item . realName || item . username || ` 用户 ${ item . userId || '' } `
// 渲染顶部导航栏
const phone = item . phone || item . mobile || '-'
const renderHeader = ( ) = > {
const primaryRole = getPrimaryRoleCode ( item )
if ( levelStack . length === 0 ) return null
const toggleText = primaryRole === 'dealer' ? '设为注册会员' : '设为业务员'
return (
return (
< View className = "bg-white p-4 mb-3 shadow-sm" >
< View key = { item . userId || index } className= "bg-white rounded-lg p-4 mb-3 shadow-sm" >
< View className = "flex items-center justify-between" >
< View className = "flex items-center" >
< View className = "flex items-center" >
< Text className = "text-lg font-semibold ">
< Avatar size = "40" src = { item . avatar || item . avatarUrl } className = "mr-3 " / >
{ currentDealerName } 的 团 队 成 员
< View className = "flex-1" >
< / Text >
< View className = "flex items-center justify-between" >
< Text className = "font-semibold text-gray-800" > { displayName } < / Text >
{ renderRoleTag ( item ) }
< / View >
< / View >
< View className = "text-xs text-gray-500 mt-1" >
UID : { item . userId || '-' } · 手 机 : { phone }
< / View >
< / View >
< / View >
< View className = "flex justify-end gap-2 pt-3 mt-3 border-t border-gray-100" >
< Button
< Button
size = "small"
size = "small"
type = "primary "
fill = "outline "
onClick = { goBack }
onClick = { ( ) = > toggleRole ( item ) }
className = "bg-blue-500"
>
>
返 回 上 一 层
{ toggleText }
< / Button >
< / Button >
< / View >
< / View >
< / View >
< / View >
)
)
}
} ) }
< / InfiniteLoading >
if ( ! dealerUser ) {
return (
< Space className = "flex items-center justify-center" >
< Empty description = "您还不是业务人员" style = { {
backgroundColor : 'transparent'
} } actions = { [ { text : '立即申请' , onClick : ( ) = > navTo ( ` /dealer/apply/add ` , true ) } ] }
/ >
< / Space >
)
}
return (
< >
{ renderHeader ( ) }
{ loading ? (
< View className = "flex items-center justify-center mt-20" >
< Text className = "text-gray-500" > 加 载 中 . . . < / Text >
< / View >
) : teamMembers . length > 0 ? (
renderOverview ( )
) : (
< View className = "flex items-center justify-center mt-20" >
< Empty description = "暂无成员" style = { {
backgroundColor : 'transparent'
} } / >
< / View >
) }
) }
< / View >
< FixedButton text = { '立即邀请' } onClick = { ( ) = > navTo ( ` /dealer/qrcode/index ` , true ) } / >
< / PullToRefresh >
< / >
< / View >
)
)
}
}
export default DealerTeam ;
export default AdminUsers