forked from gxwebsoft/mp-10550
feat(invite): 添加邀请统计功能
- 新增邀请统计页面,包含统计概览、邀请记录和排行榜三个标签页 - 实现邀请统计数据的获取和展示,包括总邀请数、成功注册数、转化率等 - 添加邀请记录的查询和展示功能 - 实现邀请排行榜的查询和展示功能 - 新增生成小程序码和处理邀请场景值的接口
This commit is contained in:
7
src/dealer/invite-stats/index.config.ts
Normal file
7
src/dealer/invite-stats/index.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '邀请统计',
|
||||
navigationBarBackgroundColor: '#ffffff',
|
||||
navigationBarTextStyle: 'black',
|
||||
backgroundColor: '#f5f5f5',
|
||||
enablePullDownRefresh: true
|
||||
})
|
||||
343
src/dealer/invite-stats/index.tsx
Normal file
343
src/dealer/invite-stats/index.tsx
Normal file
@@ -0,0 +1,343 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import {
|
||||
Empty,
|
||||
Tabs,
|
||||
Progress,
|
||||
Loading,
|
||||
PullToRefresh,
|
||||
Card,
|
||||
Button,
|
||||
DatePicker
|
||||
} from '@nutui/nutui-react-taro'
|
||||
import {
|
||||
User,
|
||||
Star,
|
||||
TrendingUp,
|
||||
Calendar,
|
||||
Share,
|
||||
Target,
|
||||
Award
|
||||
} from '@nutui/icons-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useDealerUser } from '@/hooks/useDealerUser'
|
||||
import {
|
||||
getInviteStats,
|
||||
getMyInviteRecords,
|
||||
getInviteRanking
|
||||
} from '@/api/invite'
|
||||
import type {
|
||||
InviteStats,
|
||||
InviteRecord,
|
||||
InviteRanking
|
||||
} from '@/api/invite'
|
||||
import { businessGradients } from '@/styles/gradients'
|
||||
|
||||
const InviteStatsPage: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState<string>('stats')
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [refreshing, setRefreshing] = useState<boolean>(false)
|
||||
const [inviteStats, setInviteStats] = useState<InviteStats | null>(null)
|
||||
const [inviteRecords, setInviteRecords] = useState<InviteRecord[]>([])
|
||||
const [ranking, setRanking] = useState<InviteRanking[]>([])
|
||||
const [dateRange, setDateRange] = useState<string>('month')
|
||||
const { dealerUser } = useDealerUser()
|
||||
|
||||
// 获取邀请统计数据
|
||||
const fetchInviteStats = useCallback(async () => {
|
||||
if (!dealerUser?.userId) return
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
const stats = await getInviteStats(dealerUser.userId)
|
||||
setInviteStats(stats)
|
||||
} catch (error) {
|
||||
console.error('获取邀请统计失败:', error)
|
||||
Taro.showToast({
|
||||
title: '获取统计数据失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [dealerUser?.userId])
|
||||
|
||||
// 获取邀请记录
|
||||
const fetchInviteRecords = useCallback(async () => {
|
||||
if (!dealerUser?.userId) return
|
||||
|
||||
try {
|
||||
const result = await getMyInviteRecords({
|
||||
page: 1,
|
||||
limit: 50,
|
||||
inviterId: dealerUser.userId
|
||||
})
|
||||
setInviteRecords(result?.list || [])
|
||||
} catch (error) {
|
||||
console.error('获取邀请记录失败:', error)
|
||||
}
|
||||
}, [dealerUser?.userId])
|
||||
|
||||
// 获取邀请排行榜
|
||||
const fetchRanking = useCallback(async () => {
|
||||
try {
|
||||
const result = await getInviteRanking({
|
||||
limit: 20,
|
||||
period: dateRange as 'day' | 'week' | 'month'
|
||||
})
|
||||
setRanking(result || [])
|
||||
} catch (error) {
|
||||
console.error('获取排行榜失败:', error)
|
||||
}
|
||||
}, [dateRange])
|
||||
|
||||
// 刷新数据
|
||||
const handleRefresh = async () => {
|
||||
setRefreshing(true)
|
||||
await Promise.all([
|
||||
fetchInviteStats(),
|
||||
fetchInviteRecords(),
|
||||
fetchRanking()
|
||||
])
|
||||
setRefreshing(false)
|
||||
}
|
||||
|
||||
// 初始化数据
|
||||
useEffect(() => {
|
||||
if (dealerUser?.userId) {
|
||||
fetchInviteStats()
|
||||
fetchInviteRecords()
|
||||
fetchRanking()
|
||||
}
|
||||
}, [fetchInviteStats, fetchInviteRecords, fetchRanking])
|
||||
|
||||
// 获取状态显示文本
|
||||
const getStatusText = (status: string) => {
|
||||
const statusMap: Record<string, string> = {
|
||||
'pending': '待注册',
|
||||
'registered': '已注册',
|
||||
'activated': '已激活'
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
'pending': 'text-orange-500',
|
||||
'registered': 'text-blue-500',
|
||||
'activated': 'text-green-500'
|
||||
}
|
||||
return colorMap[status] || 'text-gray-500'
|
||||
}
|
||||
|
||||
// 渲染统计概览
|
||||
const renderStatsOverview = () => (
|
||||
<View className="px-4 space-y-4">
|
||||
{/* 核心数据卡片 */}
|
||||
<Card className="bg-white rounded-2xl shadow-sm">
|
||||
<View className="p-4">
|
||||
<Text className="text-lg font-semibold text-gray-800 mb-4">邀请概览</Text>
|
||||
{loading ? (
|
||||
<View className="flex items-center justify-center py-8">
|
||||
<Loading />
|
||||
</View>
|
||||
) : inviteStats ? (
|
||||
<View className="grid grid-cols-2 gap-4">
|
||||
<View className="text-center p-4 bg-blue-50 rounded-xl">
|
||||
<TrendingUp size="24" className="text-blue-500 mx-auto mb-2" />
|
||||
<Text className="text-2xl font-bold text-blue-600">
|
||||
{inviteStats.totalInvites || 0}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-600">总邀请数</Text>
|
||||
</View>
|
||||
|
||||
<View className="text-center p-4 bg-green-50 rounded-xl">
|
||||
<User size="24" className="text-green-500 mx-auto mb-2" />
|
||||
<Text className="text-2xl font-bold text-green-600">
|
||||
{inviteStats.successfulRegistrations || 0}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-600">成功注册</Text>
|
||||
</View>
|
||||
|
||||
<View className="text-center p-4 bg-purple-50 rounded-xl">
|
||||
<Target size="24" className="text-purple-500 mx-auto mb-2" />
|
||||
<Text className="text-2xl font-bold text-purple-600">
|
||||
{inviteStats.conversionRate ? `${(inviteStats.conversionRate * 100).toFixed(1)}%` : '0%'}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-600">转化率</Text>
|
||||
</View>
|
||||
|
||||
<View className="text-center p-4 bg-orange-50 rounded-xl">
|
||||
<Calendar size="24" className="text-orange-500 mx-auto mb-2" />
|
||||
<Text className="text-2xl font-bold text-orange-600">
|
||||
{inviteStats.todayInvites || 0}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-600">今日邀请</Text>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<View className="text-center py-8">
|
||||
<Text className="text-gray-500">暂无统计数据</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</Card>
|
||||
|
||||
{/* 邀请来源分析 */}
|
||||
{inviteStats?.sourceStats && inviteStats.sourceStats.length > 0 && (
|
||||
<Card className="bg-white rounded-2xl shadow-sm">
|
||||
<View className="p-4">
|
||||
<Text className="text-lg font-semibold text-gray-800 mb-4">邀请来源分析</Text>
|
||||
<View className="space-y-3">
|
||||
{inviteStats.sourceStats.map((source, index) => (
|
||||
<View key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||
<View className="flex items-center">
|
||||
<Share size="16" className="text-blue-500 mr-2" />
|
||||
<Text className="font-medium text-gray-800">{source.source}</Text>
|
||||
</View>
|
||||
<View className="text-right">
|
||||
<Text className="text-lg font-bold text-gray-800">{source.count}</Text>
|
||||
<Text className="text-sm text-gray-500">
|
||||
转化率 {source.conversionRate ? `${(source.conversionRate * 100).toFixed(1)}%` : '0%'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</Card>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
|
||||
// 渲染邀请记录
|
||||
const renderInviteRecords = () => (
|
||||
<View className="px-4">
|
||||
{inviteRecords.length > 0 ? (
|
||||
<View className="space-y-3">
|
||||
{inviteRecords.map((record, index) => (
|
||||
<Card key={record.id || index} className="bg-white rounded-xl shadow-sm">
|
||||
<View className="p-4">
|
||||
<View className="flex items-center justify-between mb-2">
|
||||
<Text className="font-medium text-gray-800">
|
||||
{record.inviteeName || `用户${record.inviteeId}`}
|
||||
</Text>
|
||||
<Text className={`text-sm font-medium ${getStatusColor(record.status || 'pending')}`}>
|
||||
{getStatusText(record.status || 'pending')}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className="flex items-center justify-between text-sm text-gray-500">
|
||||
<Text>来源: {record.source || '未知'}</Text>
|
||||
<Text>{record.inviteTime ? new Date(record.inviteTime).toLocaleDateString() : ''}</Text>
|
||||
</View>
|
||||
|
||||
{record.registerTime && (
|
||||
<Text className="text-xs text-green-600 mt-1">
|
||||
注册时间: {new Date(record.registerTime).toLocaleString()}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</Card>
|
||||
))}
|
||||
</View>
|
||||
) : (
|
||||
<Empty description="暂无邀请记录" />
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
|
||||
// 渲染排行榜
|
||||
const renderRanking = () => (
|
||||
<View className="px-4">
|
||||
<View className="mb-4">
|
||||
<Tabs value={dateRange} onChange={setDateRange}>
|
||||
<Tabs.TabPane title="日榜" value="day" />
|
||||
<Tabs.TabPane title="周榜" value="week" />
|
||||
<Tabs.TabPane title="月榜" value="month" />
|
||||
</Tabs>
|
||||
</View>
|
||||
|
||||
{ranking.length > 0 ? (
|
||||
<View className="space-y-3">
|
||||
{ranking.map((item, index) => (
|
||||
<Card key={item.inviterId} className="bg-white rounded-xl shadow-sm">
|
||||
<View className="p-4 flex items-center">
|
||||
<View className="flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 mr-3">
|
||||
{index < 3 ? (
|
||||
<Award size="16" className={index === 0 ? 'text-yellow-500' : index === 1 ? 'text-gray-400' : 'text-orange-400'} />
|
||||
) : (
|
||||
<Text className="text-sm font-bold text-gray-600">{index + 1}</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View className="flex-1">
|
||||
<Text className="font-medium text-gray-800">{item.inviterName}</Text>
|
||||
<Text className="text-sm text-gray-500">
|
||||
邀请 {item.inviteCount} 人 · 转化率 {item.conversionRate ? `${(item.conversionRate * 100).toFixed(1)}%` : '0%'}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Text className="text-lg font-bold text-blue-600">{item.successCount}</Text>
|
||||
</View>
|
||||
</Card>
|
||||
))}
|
||||
</View>
|
||||
) : (
|
||||
<Empty description="暂无排行数据" />
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
|
||||
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">
|
||||
{/* 头部 */}
|
||||
<View className="rounded-b-3xl p-6 mb-6 text-white relative overflow-hidden" style={{
|
||||
background: businessGradients.dealer.header
|
||||
}}>
|
||||
<View className="absolute w-32 h-32 rounded-full" style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
top: '-16px',
|
||||
right: '-16px'
|
||||
}}></View>
|
||||
|
||||
<View className="relative z-10">
|
||||
<Text className="text-2xl font-bold mb-2 text-white">邀请统计</Text>
|
||||
<Text className="text-white text-opacity-80">
|
||||
查看您的邀请效果和推广数据
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 标签页 */}
|
||||
<View className="px-4 mb-4">
|
||||
<Tabs value={activeTab} onChange={setActiveTab}>
|
||||
<Tabs.TabPane title="统计概览" value="stats" />
|
||||
<Tabs.TabPane title="邀请记录" value="records" />
|
||||
<Tabs.TabPane title="排行榜" value="ranking" />
|
||||
</Tabs>
|
||||
</View>
|
||||
|
||||
{/* 内容区域 */}
|
||||
<PullToRefresh onRefresh={handleRefresh} loading={refreshing}>
|
||||
<View className="pb-6">
|
||||
{activeTab === 'stats' && renderStatsOverview()}
|
||||
{activeTab === 'records' && renderInviteRecords()}
|
||||
{activeTab === 'ranking' && renderRanking()}
|
||||
</View>
|
||||
</PullToRefresh>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default InviteStatsPage
|
||||
Reference in New Issue
Block a user