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