Browse Source
- 新增邀请统计页面,包含统计概览、邀请记录和排行榜三个标签页 - 实现邀请统计数据的获取和展示,包括总邀请数、成功注册数、转化率等 - 添加邀请记录的查询和展示功能 - 实现邀请排行榜的查询和展示功能 - 新增生成小程序码和处理邀请场景值的接口dev
23 changed files with 2403 additions and 258 deletions
@ -0,0 +1,239 @@ |
|||
import request from '@/utils/request'; |
|||
import type { ApiResult, PageResult } from '@/api/index'; |
|||
import { SERVER_API_URL } from '@/utils/server'; |
|||
|
|||
/** |
|||
* 小程序码生成参数 |
|||
*/ |
|||
export interface MiniProgramCodeParam { |
|||
// 小程序页面路径
|
|||
page?: string; |
|||
// 场景值,最大32个可见字符
|
|||
scene: string; |
|||
// 二维码宽度,单位 px,最小 280px,最大 1280px
|
|||
width?: number; |
|||
// 是否检查页面是否存在
|
|||
checkPath?: boolean; |
|||
// 环境版本
|
|||
envVersion?: 'release' | 'trial' | 'develop'; |
|||
} |
|||
|
|||
/** |
|||
* 邀请关系参数 |
|||
*/ |
|||
export interface InviteRelationParam { |
|||
// 邀请人ID
|
|||
inviterId: number; |
|||
// 被邀请人ID
|
|||
inviteeId: number; |
|||
// 邀请来源
|
|||
source: string; |
|||
// 场景值
|
|||
scene?: string; |
|||
// 邀请时间
|
|||
inviteTime?: string; |
|||
} |
|||
|
|||
/** |
|||
* 邀请统计数据 |
|||
*/ |
|||
export interface InviteStats { |
|||
// 总邀请数
|
|||
totalInvites: number; |
|||
// 成功注册数
|
|||
successfulRegistrations: number; |
|||
// 转化率
|
|||
conversionRate: number; |
|||
// 今日邀请数
|
|||
todayInvites: number; |
|||
// 本月邀请数
|
|||
monthlyInvites: number; |
|||
// 邀请来源统计
|
|||
sourceStats: InviteSourceStat[]; |
|||
} |
|||
|
|||
/** |
|||
* 邀请来源统计 |
|||
*/ |
|||
export interface InviteSourceStat { |
|||
source: string; |
|||
count: number; |
|||
successCount: number; |
|||
conversionRate: number; |
|||
} |
|||
|
|||
/** |
|||
* 邀请记录 |
|||
*/ |
|||
export interface InviteRecord { |
|||
id?: number; |
|||
inviterId?: number; |
|||
inviteeId?: number; |
|||
inviterName?: string; |
|||
inviteeName?: string; |
|||
source?: string; |
|||
scene?: string; |
|||
status?: 'pending' | 'registered' | 'activated'; |
|||
inviteTime?: string; |
|||
registerTime?: string; |
|||
activateTime?: string; |
|||
} |
|||
|
|||
/** |
|||
* 邀请记录查询参数 |
|||
*/ |
|||
export interface InviteRecordParam { |
|||
page?: number; |
|||
limit?: number; |
|||
inviterId?: number; |
|||
status?: string; |
|||
source?: string; |
|||
startTime?: string; |
|||
endTime?: string; |
|||
} |
|||
|
|||
/** |
|||
* 生成小程序码 |
|||
*/ |
|||
export async function generateMiniProgramCode(data: MiniProgramCodeParam) { |
|||
const res = await request.post<ApiResult<string>>( |
|||
SERVER_API_URL + '/invite/generate-miniprogram-code', |
|||
data |
|||
); |
|||
if (res.code === 0) { |
|||
return res.data; |
|||
} |
|||
return Promise.reject(new Error(res.message)); |
|||
} |
|||
|
|||
/** |
|||
* 生成邀请小程序码 |
|||
*/ |
|||
export async function generateInviteCode(inviterId: number, source: string = 'qrcode') { |
|||
const scene = `inviter=${inviterId}&source=${source}&t=${Date.now()}`; |
|||
|
|||
return generateMiniProgramCode({ |
|||
page: 'pages/index/index', |
|||
scene: scene, |
|||
width: 430, |
|||
checkPath: true, |
|||
envVersion: 'release' |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 建立邀请关系 |
|||
*/ |
|||
export async function createInviteRelation(data: InviteRelationParam) { |
|||
const res = await request.post<ApiResult<unknown>>( |
|||
SERVER_API_URL + '/invite/create-relation', |
|||
data |
|||
); |
|||
if (res.code === 0) { |
|||
return res.message; |
|||
} |
|||
return Promise.reject(new Error(res.message)); |
|||
} |
|||
|
|||
/** |
|||
* 处理邀请场景值 |
|||
*/ |
|||
export async function processInviteScene(scene: string, userId: number) { |
|||
const res = await request.post<ApiResult<unknown>>( |
|||
SERVER_API_URL + '/invite/process-scene', |
|||
{ scene, userId } |
|||
); |
|||
if (res.code === 0) { |
|||
return res.data; |
|||
} |
|||
return Promise.reject(new Error(res.message)); |
|||
} |
|||
|
|||
/** |
|||
* 获取邀请统计数据 |
|||
*/ |
|||
export async function getInviteStats(inviterId: number) { |
|||
const res = await request.get<ApiResult<InviteStats>>( |
|||
SERVER_API_URL + `/invite/stats/${inviterId}` |
|||
); |
|||
if (res.code === 0) { |
|||
return res.data; |
|||
} |
|||
return Promise.reject(new Error(res.message)); |
|||
} |
|||
|
|||
/** |
|||
* 分页查询邀请记录 |
|||
*/ |
|||
export async function pageInviteRecords(params: InviteRecordParam) { |
|||
const res = await request.get<ApiResult<PageResult<InviteRecord>>>( |
|||
SERVER_API_URL + '/invite/records/page', |
|||
params |
|||
); |
|||
if (res.code === 0) { |
|||
return res.data; |
|||
} |
|||
return Promise.reject(new Error(res.message)); |
|||
} |
|||
|
|||
/** |
|||
* 获取我的邀请记录 |
|||
*/ |
|||
export async function getMyInviteRecords(params: InviteRecordParam) { |
|||
const res = await request.get<ApiResult<PageResult<InviteRecord>>>( |
|||
SERVER_API_URL + '/invite/my-records', |
|||
params |
|||
); |
|||
if (res.code === 0) { |
|||
return res.data; |
|||
} |
|||
return Promise.reject(new Error(res.message)); |
|||
} |
|||
|
|||
/** |
|||
* 验证邀请码有效性 |
|||
*/ |
|||
export async function validateInviteCode(scene: string) { |
|||
const res = await request.post<ApiResult<{ valid: boolean; inviterId?: number; source?: string }>>( |
|||
SERVER_API_URL + '/invite/validate-code', |
|||
{ scene } |
|||
); |
|||
if (res.code === 0) { |
|||
return res.data; |
|||
} |
|||
return Promise.reject(new Error(res.message)); |
|||
} |
|||
|
|||
/** |
|||
* 更新邀请状态 |
|||
*/ |
|||
export async function updateInviteStatus(inviteId: number, status: 'registered' | 'activated') { |
|||
const res = await request.put<ApiResult<unknown>>( |
|||
SERVER_API_URL + `/invite/update-status/${inviteId}`, |
|||
{ status } |
|||
); |
|||
if (res.code === 0) { |
|||
return res.message; |
|||
} |
|||
return Promise.reject(new Error(res.message)); |
|||
} |
|||
|
|||
/** |
|||
* 获取邀请排行榜 |
|||
*/ |
|||
export async function getInviteRanking(params?: { limit?: number; period?: 'day' | 'week' | 'month' }) { |
|||
const res = await request.get<ApiResult<Array<{ |
|||
inviterId: number; |
|||
inviterName: string; |
|||
inviteCount: number; |
|||
successCount: number; |
|||
conversionRate: number; |
|||
}>>>( |
|||
SERVER_API_URL + '/invite/ranking', |
|||
params |
|||
); |
|||
if (res.code === 0) { |
|||
return res.data; |
|||
} |
|||
return Promise.reject(new Error(res.message)); |
|||
} |
@ -0,0 +1,279 @@ |
|||
import type { PageParam } from '@/api/index'; |
|||
|
|||
/** |
|||
* 邀请记录表 |
|||
*/ |
|||
export interface InviteRecord { |
|||
// 主键ID
|
|||
id?: number; |
|||
// 邀请人ID
|
|||
inviterId?: number; |
|||
// 被邀请人ID
|
|||
inviteeId?: number; |
|||
// 邀请人姓名
|
|||
inviterName?: string; |
|||
// 被邀请人姓名
|
|||
inviteeName?: string; |
|||
// 邀请来源 (qrcode, link, share等)
|
|||
source?: string; |
|||
// 场景值
|
|||
scene?: string; |
|||
// 邀请状态: pending-待注册, registered-已注册, activated-已激活
|
|||
status?: 'pending' | 'registered' | 'activated'; |
|||
// 邀请时间
|
|||
inviteTime?: string; |
|||
// 注册时间
|
|||
registerTime?: string; |
|||
// 激活时间
|
|||
activateTime?: string; |
|||
// 备注
|
|||
comments?: string; |
|||
// 是否删除, 0否, 1是
|
|||
deleted?: number; |
|||
// 租户id
|
|||
tenantId?: number; |
|||
// 创建时间
|
|||
createTime?: string; |
|||
// 修改时间
|
|||
updateTime?: string; |
|||
} |
|||
|
|||
/** |
|||
* 邀请统计表 |
|||
*/ |
|||
export interface InviteStats { |
|||
// 主键ID
|
|||
id?: number; |
|||
// 邀请人ID
|
|||
inviterId?: number; |
|||
// 统计日期
|
|||
statDate?: string; |
|||
// 总邀请数
|
|||
totalInvites?: number; |
|||
// 成功注册数
|
|||
successfulRegistrations?: number; |
|||
// 激活用户数
|
|||
activatedUsers?: number; |
|||
// 转化率
|
|||
conversionRate?: number; |
|||
// 今日邀请数
|
|||
todayInvites?: number; |
|||
// 本周邀请数
|
|||
weeklyInvites?: number; |
|||
// 本月邀请数
|
|||
monthlyInvites?: number; |
|||
// 租户id
|
|||
tenantId?: number; |
|||
// 创建时间
|
|||
createTime?: string; |
|||
// 修改时间
|
|||
updateTime?: string; |
|||
} |
|||
|
|||
/** |
|||
* 邀请来源统计表 |
|||
*/ |
|||
export interface InviteSourceStats { |
|||
// 主键ID
|
|||
id?: number; |
|||
// 邀请人ID
|
|||
inviterId?: number; |
|||
// 来源类型
|
|||
source?: string; |
|||
// 来源名称
|
|||
sourceName?: string; |
|||
// 邀请数量
|
|||
inviteCount?: number; |
|||
// 成功数量
|
|||
successCount?: number; |
|||
// 转化率
|
|||
conversionRate?: number; |
|||
// 统计日期
|
|||
statDate?: string; |
|||
// 租户id
|
|||
tenantId?: number; |
|||
// 创建时间
|
|||
createTime?: string; |
|||
// 修改时间
|
|||
updateTime?: string; |
|||
} |
|||
|
|||
/** |
|||
* 小程序码记录表 |
|||
*/ |
|||
export interface MiniProgramCode { |
|||
// 主键ID
|
|||
id?: number; |
|||
// 邀请人ID
|
|||
inviterId?: number; |
|||
// 场景值
|
|||
scene?: string; |
|||
// 小程序码URL
|
|||
codeUrl?: string; |
|||
// 页面路径
|
|||
pagePath?: string; |
|||
// 二维码宽度
|
|||
width?: number; |
|||
// 环境版本
|
|||
envVersion?: string; |
|||
// 过期时间
|
|||
expireTime?: string; |
|||
// 使用次数
|
|||
useCount?: number; |
|||
// 最后使用时间
|
|||
lastUseTime?: string; |
|||
// 状态: active-有效, expired-过期, disabled-禁用
|
|||
status?: 'active' | 'expired' | 'disabled'; |
|||
// 租户id
|
|||
tenantId?: number; |
|||
// 创建时间
|
|||
createTime?: string; |
|||
// 修改时间
|
|||
updateTime?: string; |
|||
} |
|||
|
|||
/** |
|||
* 邀请记录搜索条件 |
|||
*/ |
|||
export interface InviteRecordParam extends PageParam { |
|||
// 邀请人ID
|
|||
inviterId?: number; |
|||
// 被邀请人ID
|
|||
inviteeId?: number; |
|||
// 邀请状态
|
|||
status?: string; |
|||
// 邀请来源
|
|||
source?: string; |
|||
// 开始时间
|
|||
startTime?: string; |
|||
// 结束时间
|
|||
endTime?: string; |
|||
// 关键词搜索
|
|||
keywords?: string; |
|||
} |
|||
|
|||
/** |
|||
* 邀请统计搜索条件 |
|||
*/ |
|||
export interface InviteStatsParam extends PageParam { |
|||
// 邀请人ID
|
|||
inviterId?: number; |
|||
// 统计开始日期
|
|||
startDate?: string; |
|||
// 统计结束日期
|
|||
endDate?: string; |
|||
} |
|||
|
|||
/** |
|||
* 邀请来源统计搜索条件 |
|||
*/ |
|||
export interface InviteSourceStatsParam extends PageParam { |
|||
// 邀请人ID
|
|||
inviterId?: number; |
|||
// 来源类型
|
|||
source?: string; |
|||
// 统计开始日期
|
|||
startDate?: string; |
|||
// 统计结束日期
|
|||
endDate?: string; |
|||
} |
|||
|
|||
/** |
|||
* 小程序码搜索条件 |
|||
*/ |
|||
export interface MiniProgramCodeParam extends PageParam { |
|||
// 邀请人ID
|
|||
inviterId?: number; |
|||
// 状态
|
|||
status?: string; |
|||
// 场景值
|
|||
scene?: string; |
|||
} |
|||
|
|||
/** |
|||
* 邀请排行榜数据 |
|||
*/ |
|||
export interface InviteRanking { |
|||
// 邀请人ID
|
|||
inviterId?: number; |
|||
// 邀请人姓名
|
|||
inviterName?: string; |
|||
// 邀请人头像
|
|||
inviterAvatar?: string; |
|||
// 邀请数量
|
|||
inviteCount?: number; |
|||
// 成功数量
|
|||
successCount?: number; |
|||
// 转化率
|
|||
conversionRate?: number; |
|||
// 排名
|
|||
rank?: number; |
|||
// 奖励金额
|
|||
rewardAmount?: number; |
|||
} |
|||
|
|||
/** |
|||
* 邀请奖励配置 |
|||
*/ |
|||
export interface InviteRewardConfig { |
|||
// 主键ID
|
|||
id?: number; |
|||
// 奖励类型: register-注册奖励, activate-激活奖励, order-订单奖励
|
|||
rewardType?: string; |
|||
// 奖励名称
|
|||
rewardName?: string; |
|||
// 奖励金额
|
|||
rewardAmount?: number; |
|||
// 奖励积分
|
|||
rewardPoints?: number; |
|||
// 奖励优惠券ID
|
|||
couponId?: number; |
|||
// 是否启用
|
|||
enabled?: boolean; |
|||
// 生效时间
|
|||
effectTime?: string; |
|||
// 失效时间
|
|||
expireTime?: string; |
|||
// 备注
|
|||
comments?: string; |
|||
// 租户id
|
|||
tenantId?: number; |
|||
// 创建时间
|
|||
createTime?: string; |
|||
// 修改时间
|
|||
updateTime?: string; |
|||
} |
|||
|
|||
/** |
|||
* 邀请奖励记录 |
|||
*/ |
|||
export interface InviteRewardRecord { |
|||
// 主键ID
|
|||
id?: number; |
|||
// 邀请记录ID
|
|||
inviteRecordId?: number; |
|||
// 邀请人ID
|
|||
inviterId?: number; |
|||
// 被邀请人ID
|
|||
inviteeId?: number; |
|||
// 奖励类型
|
|||
rewardType?: string; |
|||
// 奖励金额
|
|||
rewardAmount?: number; |
|||
// 奖励积分
|
|||
rewardPoints?: number; |
|||
// 优惠券ID
|
|||
couponId?: number; |
|||
// 发放状态: pending-待发放, issued-已发放, failed-发放失败
|
|||
status?: 'pending' | 'issued' | 'failed'; |
|||
// 发放时间
|
|||
issueTime?: string; |
|||
// 失败原因
|
|||
failReason?: string; |
|||
// 租户id
|
|||
tenantId?: number; |
|||
// 创建时间
|
|||
createTime?: string; |
|||
// 修改时间
|
|||
updateTime?: string; |
|||
} |
@ -1,8 +0,0 @@ |
|||
/* 添加这段样式后,Primary Button 会变成绿色 */ |
|||
:root { |
|||
--nutui-color-primary: green; |
|||
--nutui-color-primary-stop1: green; |
|||
--nutui-color-primary-stop2: green; |
|||
// 间隔线/容错线,用于结构或信息分割 |
|||
--nutui-black-2: rgba(255, 0, 0, 0.08); |
|||
} |
@ -0,0 +1,7 @@ |
|||
export default definePageConfig({ |
|||
navigationBarTitleText: '邀请统计', |
|||
navigationBarBackgroundColor: '#ffffff', |
|||
navigationBarTextStyle: 'black', |
|||
backgroundColor: '#f5f5f5', |
|||
enablePullDownRefresh: true |
|||
}) |
@ -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 |
@ -0,0 +1,228 @@ |
|||
import Taro from '@tarojs/taro' |
|||
import { processInviteScene, createInviteRelation } from '@/api/invite' |
|||
|
|||
/** |
|||
* 邀请参数接口 |
|||
*/ |
|||
export interface InviteParams { |
|||
inviter?: string; |
|||
source?: string; |
|||
t?: string; |
|||
} |
|||
|
|||
/** |
|||
* 解析小程序启动参数中的邀请信息 |
|||
*/ |
|||
export function parseInviteParams(options: any): InviteParams | null { |
|||
try { |
|||
// 从 scene 参数中解析邀请信息
|
|||
if (options.scene) { |
|||
const params: InviteParams = {} |
|||
const pairs = options.scene.split('&') |
|||
|
|||
pairs.forEach((pair: string) => { |
|||
const [key, value] = pair.split('=') |
|||
if (key && value) { |
|||
switch (key) { |
|||
case 'inviter': |
|||
params.inviter = decodeURIComponent(value) |
|||
break |
|||
case 'source': |
|||
params.source = decodeURIComponent(value) |
|||
break |
|||
case 't': |
|||
params.t = decodeURIComponent(value) |
|||
break |
|||
} |
|||
} |
|||
}) |
|||
|
|||
return params.inviter ? params : null |
|||
} |
|||
|
|||
// 从 query 参数中解析邀请信息(兼容旧版本)
|
|||
if (options.referrer) { |
|||
return { |
|||
inviter: options.referrer, |
|||
source: 'link' |
|||
} |
|||
} |
|||
|
|||
return null |
|||
} catch (error) { |
|||
console.error('解析邀请参数失败:', error) |
|||
return null |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 保存邀请信息到本地存储 |
|||
*/ |
|||
export function saveInviteParams(params: InviteParams) { |
|||
try { |
|||
Taro.setStorageSync('invite_params', { |
|||
...params, |
|||
timestamp: Date.now() |
|||
}) |
|||
console.log('邀请参数已保存:', params) |
|||
} catch (error) { |
|||
console.error('保存邀请参数失败:', error) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 获取本地存储的邀请信息 |
|||
*/ |
|||
export function getStoredInviteParams(): InviteParams | null { |
|||
try { |
|||
const stored = Taro.getStorageSync('invite_params') |
|||
if (stored && stored.inviter) { |
|||
// 检查是否过期(24小时)
|
|||
const now = Date.now() |
|||
const expireTime = 24 * 60 * 60 * 1000 // 24小时
|
|||
|
|||
if (now - stored.timestamp < expireTime) { |
|||
return { |
|||
inviter: stored.inviter, |
|||
source: stored.source || 'unknown', |
|||
t: stored.t |
|||
} |
|||
} else { |
|||
// 过期则清除
|
|||
clearInviteParams() |
|||
} |
|||
} |
|||
return null |
|||
} catch (error) { |
|||
console.error('获取邀请参数失败:', error) |
|||
return null |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 清除本地存储的邀请信息 |
|||
*/ |
|||
export function clearInviteParams() { |
|||
try { |
|||
Taro.removeStorageSync('invite_params') |
|||
console.log('邀请参数已清除') |
|||
} catch (error) { |
|||
console.error('清除邀请参数失败:', error) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 处理邀请关系建立 |
|||
*/ |
|||
export async function handleInviteRelation(userId: number): Promise<boolean> { |
|||
try { |
|||
const inviteParams = getStoredInviteParams() |
|||
if (!inviteParams || !inviteParams.inviter) { |
|||
return false |
|||
} |
|||
|
|||
const inviterId = parseInt(inviteParams.inviter) |
|||
if (isNaN(inviterId) || inviterId === userId) { |
|||
// 邀请人ID无效或自己邀请自己
|
|||
clearInviteParams() |
|||
return false |
|||
} |
|||
|
|||
// 建立邀请关系
|
|||
await createInviteRelation({ |
|||
inviterId: inviterId, |
|||
inviteeId: userId, |
|||
source: inviteParams.source || 'unknown', |
|||
scene: `inviter=${inviterId}&source=${inviteParams.source}&t=${inviteParams.t}`, |
|||
inviteTime: new Date().toISOString() |
|||
}) |
|||
|
|||
// 清除本地存储的邀请参数
|
|||
clearInviteParams() |
|||
|
|||
console.log(`邀请关系建立成功: ${inviterId} -> ${userId}`) |
|||
return true |
|||
} catch (error) { |
|||
console.error('建立邀请关系失败:', error) |
|||
return false |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 检查是否有待处理的邀请 |
|||
*/ |
|||
export function hasPendingInvite(): boolean { |
|||
const params = getStoredInviteParams() |
|||
return !!(params && params.inviter) |
|||
} |
|||
|
|||
/** |
|||
* 获取邀请来源的显示名称 |
|||
*/ |
|||
export function getSourceDisplayName(source: string): string { |
|||
const sourceMap: Record<string, string> = { |
|||
'qrcode': '小程序码', |
|||
'link': '分享链接', |
|||
'share': '好友分享', |
|||
'poster': '海报分享', |
|||
'unknown': '未知来源' |
|||
} |
|||
|
|||
return sourceMap[source] || source |
|||
} |
|||
|
|||
/** |
|||
* 验证邀请码格式 |
|||
*/ |
|||
export function validateInviteCode(scene: string): boolean { |
|||
try { |
|||
if (!scene) return false |
|||
|
|||
// 检查是否包含必要的参数
|
|||
const hasInviter = scene.includes('inviter=') |
|||
const hasSource = scene.includes('source=') |
|||
|
|||
return hasInviter && hasSource |
|||
} catch (error) { |
|||
return false |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 生成邀请场景值 |
|||
*/ |
|||
export function generateInviteScene(inviterId: number, source: string): string { |
|||
const timestamp = Date.now() |
|||
return `inviter=${inviterId}&source=${source}&t=${timestamp}` |
|||
} |
|||
|
|||
/** |
|||
* 统计邀请来源 |
|||
*/ |
|||
export function trackInviteSource(source: string, inviterId?: number) { |
|||
try { |
|||
// 记录邀请来源统计
|
|||
const trackData = { |
|||
source, |
|||
inviterId, |
|||
timestamp: Date.now(), |
|||
userAgent: Taro.getSystemInfoSync() |
|||
} |
|||
|
|||
// 可以发送到统计服务
|
|||
console.log('邀请来源统计:', trackData) |
|||
|
|||
// 暂存到本地,后续可批量上报
|
|||
const existingTracks = Taro.getStorageSync('invite_tracks') || [] |
|||
existingTracks.push(trackData) |
|||
|
|||
// 只保留最近100条记录
|
|||
if (existingTracks.length > 100) { |
|||
existingTracks.splice(0, existingTracks.length - 100) |
|||
} |
|||
|
|||
Taro.setStorageSync('invite_tracks', existingTracks) |
|||
} catch (error) { |
|||
console.error('统计邀请来源失败:', error) |
|||
} |
|||
} |
Loading…
Reference in new issue