feat(src): 新增文章、经销商申请、用户地址和礼物添加功能
- 新增文章添加页面,支持文章基本信息、设置、高级设置和图片上传 - 新增经销商申请页面,支持申请信息填写和审核状态显示 - 新增用户地址添加页面,支持地址信息填写和地址识别功能 - 新增礼物添加页面,功能与文章添加类似 - 统一使用 .tsx 文件格式 - 添加 .editorconfig、.eslintrc 和 .gitignore 文件,规范代码风格和项目结构
This commit is contained in:
3
src/dealer/team/index.config.ts
Normal file
3
src/dealer/team/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '我的团队'
|
||||
})
|
||||
367
src/dealer/team/index.tsx
Normal file
367
src/dealer/team/index.tsx
Normal file
@@ -0,0 +1,367 @@
|
||||
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
|
||||
Reference in New Issue
Block a user