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