feat(invite): 添加邀请统计功能
- 新增邀请统计页面,包含统计概览、邀请记录和排行榜三个标签页 - 实现邀请统计数据的获取和展示,包括总邀请数、成功注册数、转化率等 - 添加邀请记录的查询和展示功能 - 实现邀请排行榜的查询和展示功能 - 新增生成小程序码和处理邀请场景值的接口
This commit is contained in:
239
src/api/invite/index.ts
Normal file
239
src/api/invite/index.ts
Normal file
@@ -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));
|
||||
}
|
||||
279
src/api/invite/model/index.ts
Normal file
279
src/api/invite/model/index.ts
Normal file
@@ -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;
|
||||
}
|
||||
@@ -43,5 +43,9 @@ export interface ShopDealerOrder {
|
||||
*/
|
||||
export interface ShopDealerOrderParam extends PageParam {
|
||||
id?: number;
|
||||
firstUserId?: number;
|
||||
secondUserId?: number;
|
||||
thirdUserId?: number;
|
||||
userId?: number;
|
||||
keywords?: string;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { PageParam } from '@/api/index';
|
||||
import type { PageParam } from '@/api';
|
||||
|
||||
/**
|
||||
* 分销商推荐关系表
|
||||
@@ -25,5 +25,6 @@ export interface ShopDealerReferee {
|
||||
*/
|
||||
export interface ShopDealerRefereeParam extends PageParam {
|
||||
id?: number;
|
||||
dealerId?: number;
|
||||
keywords?: string;
|
||||
}
|
||||
|
||||
@@ -43,5 +43,6 @@ export interface ShopDealerWithdraw {
|
||||
*/
|
||||
export interface ShopDealerWithdrawParam extends PageParam {
|
||||
id?: number;
|
||||
userId?: number;
|
||||
keywords?: string;
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ export default defineAppConfig({
|
||||
"orders/index",
|
||||
"team/index",
|
||||
"qrcode/index",
|
||||
"invite-stats/index",
|
||||
"info"
|
||||
]
|
||||
},
|
||||
|
||||
21
src/app.scss
21
src/app.scss
@@ -17,6 +17,27 @@ button {
|
||||
}
|
||||
}
|
||||
|
||||
// 去掉 Grid 组件的边框
|
||||
.no-border-grid {
|
||||
.nut-grid-item {
|
||||
border: none !important;
|
||||
border-right: none !important;
|
||||
border-bottom: none !important;
|
||||
|
||||
&::after {
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.nut-grid {
|
||||
border: none !important;
|
||||
|
||||
&::after {
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 微信授权按钮的特殊样式
|
||||
button[open-type="getPhoneNumber"] {
|
||||
background: none !important;
|
||||
|
||||
48
src/app.ts
48
src/app.ts
@@ -6,6 +6,7 @@ import './app.scss'
|
||||
import {loginByOpenId} from "@/api/layout";
|
||||
import {TenantId} from "@/config/app";
|
||||
import {saveStorageByLoginUser} from "@/utils/server";
|
||||
import {parseInviteParams, saveInviteParams, trackInviteSource, handleInviteRelation} from "@/utils/invite";
|
||||
|
||||
function App(props: { children: any; }) {
|
||||
const reload = () => {
|
||||
@@ -14,9 +15,21 @@ function App(props: { children: any; }) {
|
||||
loginByOpenId({
|
||||
code: res.code,
|
||||
tenantId: TenantId
|
||||
}).then(data => {
|
||||
}).then(async data => {
|
||||
if (data) {
|
||||
saveStorageByLoginUser(data.access_token, data.user)
|
||||
|
||||
// 处理邀请关系
|
||||
if (data.user?.userId) {
|
||||
try {
|
||||
const inviteSuccess = await handleInviteRelation(data.user.userId)
|
||||
if (inviteSuccess) {
|
||||
console.log('自动登录时邀请关系建立成功')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('自动登录时处理邀请关系失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -36,8 +49,41 @@ function App(props: { children: any; }) {
|
||||
|
||||
// 对应 onShow
|
||||
useDidShow(() => {
|
||||
// 处理小程序启动参数中的邀请信息
|
||||
const options = Taro.getLaunchOptionsSync()
|
||||
handleLaunchOptions(options)
|
||||
})
|
||||
|
||||
// 处理启动参数
|
||||
const handleLaunchOptions = (options: any) => {
|
||||
try {
|
||||
console.log('小程序启动参数:', options)
|
||||
|
||||
// 解析邀请参数
|
||||
const inviteParams = parseInviteParams(options)
|
||||
if (inviteParams) {
|
||||
console.log('检测到邀请参数:', inviteParams)
|
||||
|
||||
// 保存邀请参数到本地存储
|
||||
saveInviteParams(inviteParams)
|
||||
|
||||
// 统计邀请来源
|
||||
trackInviteSource(inviteParams.source || 'unknown', parseInt(inviteParams.inviter || '0'))
|
||||
|
||||
// 显示邀请提示
|
||||
setTimeout(() => {
|
||||
Taro.showToast({
|
||||
title: '检测到邀请信息',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
}, 1000)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('处理启动参数失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 对应 onHide
|
||||
useDidHide(() => {
|
||||
})
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ import {
|
||||
Dongdong,
|
||||
ArrowRight,
|
||||
Purse,
|
||||
People
|
||||
People,
|
||||
Chart
|
||||
} from '@nutui/icons-react-taro'
|
||||
import {useDealerUser} from '@/hooks/useDealerUser'
|
||||
import { useThemeStyles } from '@/hooks/useTheme'
|
||||
@@ -69,7 +70,7 @@ const DealerIndex: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen">
|
||||
<View className="bg-gray-100 min-h-screen">
|
||||
<View>
|
||||
{/*头部信息*/}
|
||||
{dealerUser && (
|
||||
@@ -162,9 +163,9 @@ const DealerIndex: React.FC = () => {
|
||||
|
||||
{/* 团队统计 */}
|
||||
{dealerUser && (
|
||||
<View className="bg-white mx-4 mt-4 rounded-xl p-4">
|
||||
<View className="bg-white mx-4 mt-4 rounded-xl p-4 hidden">
|
||||
<View className="flex items-center justify-between mb-4">
|
||||
<Text className="font-semibold text-gray-800">我的团队</Text>
|
||||
<Text className="font-semibold text-gray-800">我的邀请</Text>
|
||||
<View
|
||||
className="text-gray-400 text-sm flex items-center"
|
||||
onClick={() => navigateToPage('/dealer/team/index')}
|
||||
@@ -200,7 +201,15 @@ const DealerIndex: React.FC = () => {
|
||||
<View className="bg-white mx-4 mt-4 rounded-xl p-4">
|
||||
<View className="font-semibold mb-4 text-gray-800">分销工具</View>
|
||||
<ConfigProvider>
|
||||
<Grid columns={4}>
|
||||
<Grid
|
||||
columns={4}
|
||||
className="no-border-grid"
|
||||
style={{
|
||||
'--nutui-grid-border-color': 'transparent',
|
||||
'--nutui-grid-item-border-width': '0px',
|
||||
border: 'none'
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
<Grid.Item text="分销订单" onClick={() => navigateToPage('/dealer/orders/index')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
@@ -209,30 +218,68 @@ const DealerIndex: React.FC = () => {
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item onClick={() => navigateToPage('/dealer/withdraw/index')}>
|
||||
<Grid.Item text={'提现申请'} onClick={() => navigateToPage('/dealer/withdraw/index')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-green-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Purse color="#10b981" size="20"/>
|
||||
</View>
|
||||
<Text className="text-xs text-gray-600">提现申请</Text>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item onClick={() => navigateToPage('/dealer/team/index')}>
|
||||
<Grid.Item text={'我的邀请'} onClick={() => navigateToPage('/dealer/team/index')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-purple-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<People color="#8b5cf6" size="20"/>
|
||||
</View>
|
||||
<Text className="text-xs text-gray-600">我的团队</Text>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item onClick={() => navigateToPage('/dealer/qrcode/index')}>
|
||||
<Grid.Item text={'我的邀请码'} onClick={() => navigateToPage('/dealer/qrcode/index')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-orange-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Dongdong color="#f59e0b" size="20"/>
|
||||
</View>
|
||||
<Text className="text-xs text-gray-600">推广二维码</Text>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
</Grid>
|
||||
|
||||
{/* 第二行功能 */}
|
||||
<Grid
|
||||
columns={4}
|
||||
className="no-border-grid mt-4"
|
||||
style={{
|
||||
'--nutui-grid-border-color': 'transparent',
|
||||
'--nutui-grid-item-border-width': '0px',
|
||||
border: 'none'
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
<Grid.Item text={'邀请统计'} onClick={() => navigateToPage('/dealer/invite-stats/index')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-indigo-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Chart color="#6366f1" size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
{/* 预留其他功能位置 */}
|
||||
<Grid.Item text={''}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text={''}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text={''}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
</Grid>
|
||||
|
||||
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
|
||||
@@ -1,60 +1,197 @@
|
||||
import React, { useState } from 'react'
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Empty, Tabs, Tag, PullToRefresh } from '@nutui/nutui-react-taro'
|
||||
import { Empty, Tabs, Tag, PullToRefresh, Loading } from '@nutui/nutui-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { pageShopDealerOrder } from '@/api/shop/shopDealerOrder'
|
||||
import { useDealerUser } from '@/hooks/useDealerUser'
|
||||
import type { ShopDealerOrder } from '@/api/shop/shopDealerOrder/model'
|
||||
|
||||
interface OrderWithDetails extends ShopDealerOrder {
|
||||
orderNo?: string
|
||||
customerName?: string
|
||||
totalCommission?: string
|
||||
// 当前用户在此订单中的层级和佣金
|
||||
userLevel?: 1 | 2 | 3
|
||||
userCommission?: string
|
||||
}
|
||||
|
||||
const DealerOrders: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState<string>('0')
|
||||
const [refreshing, setRefreshing] = useState<boolean>(false)
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [orders, setOrders] = useState<OrderWithDetails[]>([])
|
||||
const [statistics, setStatistics] = useState({
|
||||
totalOrders: 0,
|
||||
totalCommission: '0.00',
|
||||
pendingCommission: '0.00',
|
||||
// 分层统计
|
||||
level1: { orders: 0, commission: '0.00' },
|
||||
level2: { orders: 0, commission: '0.00' },
|
||||
level3: { orders: 0, commission: '0.00' }
|
||||
})
|
||||
|
||||
// 模拟订单数据
|
||||
const mockOrders = [
|
||||
{
|
||||
id: '1',
|
||||
orderNo: 'DD202412180001',
|
||||
customerName: '张三',
|
||||
amount: '299.00',
|
||||
commission: '29.90',
|
||||
status: 'completed',
|
||||
createTime: '2024-12-18 10:30:00'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
orderNo: 'DD202412180002',
|
||||
customerName: '李四',
|
||||
amount: '599.00',
|
||||
commission: '59.90',
|
||||
status: 'pending',
|
||||
createTime: '2024-12-18 14:20:00'
|
||||
const { dealerUser } = useDealerUser()
|
||||
|
||||
// 获取订单数据 - 查询当前用户作为各层级分销商的所有订单
|
||||
const fetchOrders = useCallback(async () => {
|
||||
if (!dealerUser?.userId) return
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
|
||||
// 并行查询三个层级的订单
|
||||
const [level1Result, level2Result, level3Result] = await Promise.all([
|
||||
// 一级分销商订单
|
||||
pageShopDealerOrder({
|
||||
page: 1,
|
||||
limit: 100,
|
||||
firstUserId: dealerUser.userId
|
||||
}),
|
||||
// 二级分销商订单
|
||||
pageShopDealerOrder({
|
||||
page: 1,
|
||||
limit: 100,
|
||||
secondUserId: dealerUser.userId
|
||||
}),
|
||||
// 三级分销商订单
|
||||
pageShopDealerOrder({
|
||||
page: 1,
|
||||
limit: 100,
|
||||
thirdUserId: dealerUser.userId
|
||||
})
|
||||
])
|
||||
|
||||
const allOrders: OrderWithDetails[] = []
|
||||
const stats = {
|
||||
totalOrders: 0,
|
||||
totalCommission: '0.00',
|
||||
pendingCommission: '0.00',
|
||||
level1: { orders: 0, commission: '0.00' },
|
||||
level2: { orders: 0, commission: '0.00' },
|
||||
level3: { orders: 0, commission: '0.00' }
|
||||
}
|
||||
|
||||
// 处理一级分销订单
|
||||
if (level1Result?.list) {
|
||||
const level1Orders = level1Result.list.map(order => ({
|
||||
...order,
|
||||
orderNo: `DD${order.orderId}`,
|
||||
customerName: `用户${order.userId}`,
|
||||
userLevel: 1 as const,
|
||||
userCommission: order.firstMoney || '0.00',
|
||||
totalCommission: (
|
||||
parseFloat(order.firstMoney || '0') +
|
||||
parseFloat(order.secondMoney || '0') +
|
||||
parseFloat(order.thirdMoney || '0')
|
||||
).toFixed(2)
|
||||
}))
|
||||
|
||||
allOrders.push(...level1Orders)
|
||||
stats.level1.orders = level1Orders.length
|
||||
stats.level1.commission = level1Orders.reduce((sum, order) =>
|
||||
sum + parseFloat(order.userCommission || '0'), 0
|
||||
).toFixed(2)
|
||||
}
|
||||
|
||||
// 处理二级分销订单
|
||||
if (level2Result?.list) {
|
||||
const level2Orders = level2Result.list.map(order => ({
|
||||
...order,
|
||||
orderNo: `DD${order.orderId}`,
|
||||
customerName: `用户${order.userId}`,
|
||||
userLevel: 2 as const,
|
||||
userCommission: order.secondMoney || '0.00',
|
||||
totalCommission: (
|
||||
parseFloat(order.firstMoney || '0') +
|
||||
parseFloat(order.secondMoney || '0') +
|
||||
parseFloat(order.thirdMoney || '0')
|
||||
).toFixed(2)
|
||||
}))
|
||||
|
||||
allOrders.push(...level2Orders)
|
||||
stats.level2.orders = level2Orders.length
|
||||
stats.level2.commission = level2Orders.reduce((sum, order) =>
|
||||
sum + parseFloat(order.userCommission || '0'), 0
|
||||
).toFixed(2)
|
||||
}
|
||||
|
||||
// 处理三级分销订单
|
||||
if (level3Result?.list) {
|
||||
const level3Orders = level3Result.list.map(order => ({
|
||||
...order,
|
||||
orderNo: `DD${order.orderId}`,
|
||||
customerName: `用户${order.userId}`,
|
||||
userLevel: 3 as const,
|
||||
userCommission: order.thirdMoney || '0.00',
|
||||
totalCommission: (
|
||||
parseFloat(order.firstMoney || '0') +
|
||||
parseFloat(order.secondMoney || '0') +
|
||||
parseFloat(order.thirdMoney || '0')
|
||||
).toFixed(2)
|
||||
}))
|
||||
|
||||
allOrders.push(...level3Orders)
|
||||
stats.level3.orders = level3Orders.length
|
||||
stats.level3.commission = level3Orders.reduce((sum, order) =>
|
||||
sum + parseFloat(order.userCommission || '0'), 0
|
||||
).toFixed(2)
|
||||
}
|
||||
|
||||
// 去重(同一个订单可能在多个层级中出现)
|
||||
const uniqueOrders = allOrders.filter((order, index, self) =>
|
||||
index === self.findIndex(o => o.orderId === order.orderId)
|
||||
)
|
||||
|
||||
// 计算总统计
|
||||
stats.totalOrders = uniqueOrders.length
|
||||
stats.totalCommission = (
|
||||
parseFloat(stats.level1.commission) +
|
||||
parseFloat(stats.level2.commission) +
|
||||
parseFloat(stats.level3.commission)
|
||||
).toFixed(2)
|
||||
stats.pendingCommission = allOrders
|
||||
.filter(order => order.isSettled === 0)
|
||||
.reduce((sum, order) => sum + parseFloat(order.userCommission || '0'), 0)
|
||||
.toFixed(2)
|
||||
|
||||
setOrders(uniqueOrders)
|
||||
setStatistics(stats)
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取分销订单失败:', error)
|
||||
Taro.showToast({
|
||||
title: '获取订单失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
]
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case 'completed': return '已完成'
|
||||
case 'pending': return '待结算'
|
||||
case 'cancelled': return '已取消'
|
||||
default: return '未知'
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'completed': return 'success'
|
||||
case 'pending': return 'warning'
|
||||
case 'cancelled': return 'danger'
|
||||
default: return 'default'
|
||||
}
|
||||
}
|
||||
}, [dealerUser?.userId])
|
||||
|
||||
// 刷新数据
|
||||
const handleRefresh = async () => {
|
||||
setRefreshing(true)
|
||||
// 模拟刷新
|
||||
setTimeout(() => {
|
||||
setRefreshing(false)
|
||||
}, 1000)
|
||||
await fetchOrders()
|
||||
}
|
||||
|
||||
const renderOrderItem = (order: any) => (
|
||||
// 初始化加载数据
|
||||
useEffect(() => {
|
||||
if (dealerUser?.userId) {
|
||||
fetchOrders().then()
|
||||
}
|
||||
}, [fetchOrders])
|
||||
|
||||
const getStatusText = (isSettled?: number, isInvalid?: number) => {
|
||||
if (isInvalid === 1) return '已失效'
|
||||
if (isSettled === 1) return '已结算'
|
||||
return '待结算'
|
||||
}
|
||||
|
||||
const getStatusColor = (isSettled?: number, isInvalid?: number) => {
|
||||
if (isInvalid === 1) return 'danger'
|
||||
if (isSettled === 1) return 'success'
|
||||
return 'warning'
|
||||
}
|
||||
|
||||
const renderOrderItem = (order: OrderWithDetails) => (
|
||||
<View key={order.id} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
|
||||
<View className="flex justify-between items-start mb-3">
|
||||
<View>
|
||||
@@ -64,19 +201,28 @@ const DealerOrders: React.FC = () => {
|
||||
<Text className="text-sm text-gray-500">
|
||||
客户:{order.customerName}
|
||||
</Text>
|
||||
{/* 显示用户在此订单中的层级 */}
|
||||
<Text className="text-xs text-blue-500">
|
||||
{order.userLevel === 1 && '一级分销'}
|
||||
{order.userLevel === 2 && '二级分销'}
|
||||
{order.userLevel === 3 && '三级分销'}
|
||||
</Text>
|
||||
</View>
|
||||
<Tag type={getStatusColor(order.status)}>
|
||||
{getStatusText(order.status)}
|
||||
<Tag type={getStatusColor(order.isSettled, order.isInvalid)}>
|
||||
{getStatusText(order.isSettled, order.isInvalid)}
|
||||
</Tag>
|
||||
</View>
|
||||
|
||||
<View className="flex justify-between items-center">
|
||||
<View>
|
||||
<Text className="text-sm text-gray-600">
|
||||
订单金额:¥{order.amount}
|
||||
订单金额:¥{order.orderPrice || '0.00'}
|
||||
</Text>
|
||||
<Text className="text-sm text-orange-500 font-semibold">
|
||||
预计佣金:¥{order.commission}
|
||||
我的佣金:¥{order.userCommission}
|
||||
</Text>
|
||||
<Text className="text-xs text-gray-400">
|
||||
总佣金:¥{order.totalCommission}
|
||||
</Text>
|
||||
</View>
|
||||
<Text className="text-xs text-gray-400">
|
||||
@@ -86,37 +232,92 @@ const DealerOrders: React.FC = () => {
|
||||
</View>
|
||||
)
|
||||
|
||||
// 根据状态和层级过滤订单
|
||||
const getFilteredOrders = (filter: string) => {
|
||||
switch (filter) {
|
||||
case '1': // 一级分销
|
||||
return orders.filter(order => order.userLevel === 1)
|
||||
case '2': // 二级分销
|
||||
return orders.filter(order => order.userLevel === 2)
|
||||
case '3': // 三级分销
|
||||
return orders.filter(order => order.userLevel === 3)
|
||||
case '4': // 待结算
|
||||
return orders.filter(order => order.isSettled === 0 && order.isInvalid === 0)
|
||||
case '5': // 已结算
|
||||
return orders.filter(order => order.isSettled === 1)
|
||||
case '6': // 已失效
|
||||
return orders.filter(order => order.isInvalid === 1)
|
||||
default: // 全部
|
||||
return orders
|
||||
}
|
||||
}
|
||||
|
||||
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="bg-white p-4 mb-4">
|
||||
<View className="grid grid-cols-3 gap-4">
|
||||
{/* 总体统计 */}
|
||||
<View className="grid grid-cols-3 gap-4 mb-4">
|
||||
<View className="text-center">
|
||||
<Text className="text-lg font-bold text-blue-500">2</Text>
|
||||
<Text className="text-lg font-bold text-blue-500">{statistics.totalOrders}</Text>
|
||||
<Text className="text-xs text-gray-500">总订单</Text>
|
||||
</View>
|
||||
<View className="text-center">
|
||||
<Text className="text-lg font-bold text-green-500">¥89.80</Text>
|
||||
<Text className="text-lg font-bold text-green-500">¥{statistics.totalCommission}</Text>
|
||||
<Text className="text-xs text-gray-500">总佣金</Text>
|
||||
</View>
|
||||
<View className="text-center">
|
||||
<Text className="text-lg font-bold text-orange-500">¥29.90</Text>
|
||||
<Text className="text-lg font-bold text-orange-500">¥{statistics.pendingCommission}</Text>
|
||||
<Text className="text-xs text-gray-500">待结算</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 分层统计 */}
|
||||
<View className="border-t pt-3">
|
||||
<Text className="text-sm text-gray-600 mb-2">分层统计</Text>
|
||||
<View className="grid grid-cols-3 gap-2">
|
||||
<View className="text-center bg-gray-50 rounded p-2">
|
||||
<Text className="text-sm font-semibold text-red-500">{statistics.level1.orders}</Text>
|
||||
<Text className="text-xs text-gray-500">一级订单</Text>
|
||||
<Text className="text-xs text-red-500">¥{statistics.level1.commission}</Text>
|
||||
</View>
|
||||
<View className="text-center bg-gray-50 rounded p-2">
|
||||
<Text className="text-sm font-semibold text-blue-500">{statistics.level2.orders}</Text>
|
||||
<Text className="text-xs text-gray-500">二级订单</Text>
|
||||
<Text className="text-xs text-blue-500">¥{statistics.level2.commission}</Text>
|
||||
</View>
|
||||
<View className="text-center bg-gray-50 rounded p-2">
|
||||
<Text className="text-sm font-semibold text-purple-500">{statistics.level3.orders}</Text>
|
||||
<Text className="text-xs text-gray-500">三级订单</Text>
|
||||
<Text className="text-xs text-purple-500">¥{statistics.level3.commission}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 订单列表 */}
|
||||
<Tabs value={activeTab} onChange={() => setActiveTab}>
|
||||
<Tabs.TabPane title="全部" value="0">
|
||||
<PullToRefresh
|
||||
// @ts-ignore
|
||||
loading={refreshing}
|
||||
onRefresh={handleRefresh}
|
||||
>
|
||||
<View className="p-4">
|
||||
{mockOrders.length > 0 ? (
|
||||
mockOrders.map(renderOrderItem)
|
||||
{loading ? (
|
||||
<View className="text-center py-8">
|
||||
<Loading />
|
||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||
</View>
|
||||
) : getFilteredOrders('0').length > 0 ? (
|
||||
getFilteredOrders('0').map(renderOrderItem)
|
||||
) : (
|
||||
<Empty description="暂无分销订单" />
|
||||
)}
|
||||
@@ -124,15 +325,63 @@ const DealerOrders: React.FC = () => {
|
||||
</PullToRefresh>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane title="待结算" value="1">
|
||||
<Tabs.TabPane title="一级分销" value="1">
|
||||
<View className="p-4">
|
||||
{mockOrders.filter(o => o.status === 'pending').map(renderOrderItem)}
|
||||
{getFilteredOrders('1').length > 0 ? (
|
||||
getFilteredOrders('1').map(renderOrderItem)
|
||||
) : (
|
||||
<Empty description="暂无一级分销订单" />
|
||||
)}
|
||||
</View>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane title="已完成" value="2">
|
||||
<Tabs.TabPane title="二级分销" value="2">
|
||||
<View className="p-4">
|
||||
{mockOrders.filter(o => o.status === 'completed').map(renderOrderItem)}
|
||||
{getFilteredOrders('2').length > 0 ? (
|
||||
getFilteredOrders('2').map(renderOrderItem)
|
||||
) : (
|
||||
<Empty description="暂无二级分销订单" />
|
||||
)}
|
||||
</View>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane title="三级分销" value="3">
|
||||
<View className="p-4">
|
||||
{getFilteredOrders('3').length > 0 ? (
|
||||
getFilteredOrders('3').map(renderOrderItem)
|
||||
) : (
|
||||
<Empty description="暂无三级分销订单" />
|
||||
)}
|
||||
</View>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane title="待结算" value="4">
|
||||
<View className="p-4">
|
||||
{getFilteredOrders('4').length > 0 ? (
|
||||
getFilteredOrders('4').map(renderOrderItem)
|
||||
) : (
|
||||
<Empty description="暂无待结算订单" />
|
||||
)}
|
||||
</View>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane title="已结算" value="5">
|
||||
<View className="p-4">
|
||||
{getFilteredOrders('5').length > 0 ? (
|
||||
getFilteredOrders('5').map(renderOrderItem)
|
||||
) : (
|
||||
<Empty description="暂无已结算订单" />
|
||||
)}
|
||||
</View>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane title="已失效" value="6">
|
||||
<View className="p-4">
|
||||
{getFilteredOrders('6').length > 0 ? (
|
||||
getFilteredOrders('6').map(renderOrderItem)
|
||||
) : (
|
||||
<Empty description="暂无失效订单" />
|
||||
)}
|
||||
</View>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
|
||||
@@ -1,28 +1,370 @@
|
||||
import React from 'react'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, Image } from '@tarojs/components'
|
||||
import { Cell, Button } from '@nutui/nutui-react-taro'
|
||||
import { Button, Loading } from '@nutui/nutui-react-taro'
|
||||
import { Share, Download, Copy, QrCode } from '@nutui/icons-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useDealerUser } from '@/hooks/useDealerUser'
|
||||
import { generateInviteCode, getInviteStats } from '@/api/invite'
|
||||
import type { InviteStats } from '@/api/invite'
|
||||
import { businessGradients } from '@/styles/gradients'
|
||||
|
||||
const DealerQrcode: React.FC = () => {
|
||||
const [miniProgramCodeUrl, setMiniProgramCodeUrl] = useState<string>('')
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [inviteStats, setInviteStats] = useState<InviteStats | null>(null)
|
||||
const [statsLoading, setStatsLoading] = useState<boolean>(false)
|
||||
const { dealerUser } = useDealerUser()
|
||||
|
||||
// 生成小程序码
|
||||
const generateMiniProgramCode = async () => {
|
||||
if (!dealerUser?.userId) return
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
|
||||
// 生成邀请小程序码
|
||||
const codeUrl = await generateInviteCode(dealerUser.userId, 'qrcode')
|
||||
|
||||
if (codeUrl) {
|
||||
setMiniProgramCodeUrl(codeUrl)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('生成小程序码失败:', error)
|
||||
Taro.showToast({
|
||||
title: '生成小程序码失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取邀请统计数据
|
||||
const fetchInviteStats = async () => {
|
||||
if (!dealerUser?.userId) return
|
||||
|
||||
try {
|
||||
setStatsLoading(true)
|
||||
const stats = await getInviteStats(dealerUser.userId)
|
||||
stats && setInviteStats(stats)
|
||||
} catch (error) {
|
||||
console.error('获取邀请统计失败:', error)
|
||||
} finally {
|
||||
setStatsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化生成小程序码和获取统计数据
|
||||
useEffect(() => {
|
||||
if (dealerUser?.userId) {
|
||||
generateMiniProgramCode()
|
||||
fetchInviteStats()
|
||||
}
|
||||
}, [dealerUser?.userId])
|
||||
|
||||
// 保存小程序码到相册
|
||||
const saveMiniProgramCode = async () => {
|
||||
if (!miniProgramCodeUrl) {
|
||||
Taro.showToast({
|
||||
title: '小程序码未生成',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 先下载图片到本地
|
||||
const res = await Taro.downloadFile({
|
||||
url: miniProgramCodeUrl
|
||||
})
|
||||
|
||||
if (res.statusCode === 200) {
|
||||
// 保存到相册
|
||||
await Taro.saveImageToPhotosAlbum({
|
||||
filePath: res.tempFilePath
|
||||
})
|
||||
|
||||
Taro.showToast({
|
||||
title: '保存成功',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.errMsg?.includes('auth deny')) {
|
||||
Taro.showModal({
|
||||
title: '提示',
|
||||
content: '需要您授权保存图片到相册',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
Taro.openSetting()
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Taro.showToast({
|
||||
title: '保存失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 复制邀请信息
|
||||
const copyInviteInfo = () => {
|
||||
if (!dealerUser?.userId) {
|
||||
Taro.showToast({
|
||||
title: '用户信息未加载',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const inviteText = `🎉 邀请您加入我的团队!
|
||||
|
||||
扫描小程序码或搜索"网宿小店"小程序,即可享受优质商品和服务!
|
||||
|
||||
💰 成为我的下级分销商,一起赚取丰厚佣金
|
||||
🎁 新用户专享优惠等你来拿
|
||||
|
||||
邀请码:${dealerUser.userId}
|
||||
快来加入我们吧!`
|
||||
|
||||
Taro.setClipboardData({
|
||||
data: inviteText,
|
||||
success: () => {
|
||||
Taro.showToast({
|
||||
title: '邀请信息已复制',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 分享小程序码
|
||||
const shareMiniProgramCode = () => {
|
||||
if (!dealerUser?.userId) {
|
||||
Taro.showToast({
|
||||
title: '用户信息未加载',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 小程序分享
|
||||
Taro.showShareMenu({
|
||||
withShareTicket: true,
|
||||
menus: ['shareAppMessage', 'shareTimeline']
|
||||
})
|
||||
}
|
||||
|
||||
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="p-4">
|
||||
<Text className="text-lg font-bold mb-4">推广二维码</Text>
|
||||
|
||||
<View className="text-center">
|
||||
<View className="bg-gray-100 w-48 h-48 mx-auto mb-4 flex items-center justify-center">
|
||||
<Text className="text-gray-500">二维码占位</Text>
|
||||
<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">
|
||||
{/* 小程序码展示区 */}
|
||||
<View className="bg-white rounded-2xl p-6 mb-6 shadow-sm">
|
||||
<View className="text-center">
|
||||
{loading ? (
|
||||
<View className="w-48 h-48 mx-auto mb-4 flex items-center justify-center bg-gray-50 rounded-xl">
|
||||
<Loading />
|
||||
<Text className="text-gray-500 mt-2">生成中...</Text>
|
||||
</View>
|
||||
) : miniProgramCodeUrl ? (
|
||||
<View className="w-48 h-48 mx-auto mb-4 bg-white rounded-xl shadow-sm p-4">
|
||||
<Image
|
||||
src={miniProgramCodeUrl}
|
||||
className="w-full h-full"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<View className="w-48 h-48 mx-auto mb-4 flex items-center justify-center bg-gray-50 rounded-xl">
|
||||
<QrCode size="48" className="text-gray-400 mb-2"/>
|
||||
<Text className="text-gray-500">小程序码生成失败</Text>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
className="mt-2"
|
||||
onClick={generateMiniProgramCode}
|
||||
>
|
||||
重新生成
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<Text className="text-lg font-semibold text-gray-800 mb-2">
|
||||
扫码加入我的团队
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-500 mb-6">
|
||||
好友扫描小程序码即可直接进入小程序并建立邀请关系
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<View className="space-y-3">
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
icon={<Download />}
|
||||
onClick={saveMiniProgramCode}
|
||||
disabled={!miniProgramCodeUrl || loading}
|
||||
>
|
||||
保存小程序码到相册
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="large"
|
||||
block
|
||||
icon={<Copy />}
|
||||
onClick={copyInviteInfo}
|
||||
disabled={!dealerUser?.userId || loading}
|
||||
>
|
||||
复制邀请信息
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="large"
|
||||
block
|
||||
fill="outline"
|
||||
icon={<Share />}
|
||||
onClick={shareMiniProgramCode}
|
||||
disabled={!dealerUser?.userId || loading}
|
||||
>
|
||||
分享给好友
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
{/* 推广说明 */}
|
||||
<View className="bg-white rounded-2xl p-4 mt-6">
|
||||
<Text className="font-semibold text-gray-800 mb-3">推广说明</Text>
|
||||
<View className="space-y-2">
|
||||
<View className="flex items-start">
|
||||
<View className="w-2 h-2 bg-blue-500 rounded-full mt-2 mr-3 flex-shrink-0"></View>
|
||||
<Text className="text-sm text-gray-600">
|
||||
好友通过您的二维码或链接注册成为您的下级分销商
|
||||
</Text>
|
||||
</View>
|
||||
<View className="flex items-start">
|
||||
<View className="w-2 h-2 bg-green-500 rounded-full mt-2 mr-3 flex-shrink-0"></View>
|
||||
<Text className="text-sm text-gray-600">
|
||||
好友购买商品时,您可获得相应层级的分销佣金
|
||||
</Text>
|
||||
</View>
|
||||
<View className="flex items-start">
|
||||
<View className="w-2 h-2 bg-purple-500 rounded-full mt-2 mr-3 flex-shrink-0"></View>
|
||||
<Text className="text-sm text-gray-600">
|
||||
支持三级分销,团队越大收益越多
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 邀请统计数据 */}
|
||||
<View className="bg-white rounded-2xl p-4 mt-4 mb-6">
|
||||
<Text className="font-semibold text-gray-800 mb-3">我的邀请数据</Text>
|
||||
{statsLoading ? (
|
||||
<View className="flex items-center justify-center py-8">
|
||||
<Loading />
|
||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||
</View>
|
||||
) : inviteStats ? (
|
||||
<View className="space-y-4">
|
||||
<View className="grid grid-cols-2 gap-4">
|
||||
<View className="text-center">
|
||||
<Text className="text-2xl font-bold text-blue-500">
|
||||
{inviteStats.totalInvites || 0}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-500">总邀请数</Text>
|
||||
</View>
|
||||
<View className="text-center">
|
||||
<Text className="text-2xl font-bold text-green-500">
|
||||
{inviteStats.successfulRegistrations || 0}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-500">成功注册</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="grid grid-cols-2 gap-4">
|
||||
<View className="text-center">
|
||||
<Text className="text-2xl font-bold text-purple-500">
|
||||
{inviteStats.conversionRate ? `${(inviteStats.conversionRate * 100).toFixed(1)}%` : '0%'}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-500">转化率</Text>
|
||||
</View>
|
||||
<View className="text-center">
|
||||
<Text className="text-2xl font-bold text-orange-500">
|
||||
{inviteStats.todayInvites || 0}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-500">今日邀请</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 邀请来源统计 */}
|
||||
{inviteStats.sourceStats && inviteStats.sourceStats.length > 0 && (
|
||||
<View className="mt-4">
|
||||
<Text className="text-sm font-medium text-gray-700 mb-2">邀请来源分布</Text>
|
||||
<View className="space-y-2">
|
||||
{inviteStats.sourceStats.map((source, index) => (
|
||||
<View key={index} className="flex items-center justify-between py-2 px-3 bg-gray-50 rounded-lg">
|
||||
<View className="flex items-center">
|
||||
<View className="w-3 h-3 rounded-full bg-blue-500 mr-2"></View>
|
||||
<Text className="text-sm text-gray-700">{source.source}</Text>
|
||||
</View>
|
||||
<View className="text-right">
|
||||
<Text className="text-sm font-medium text-gray-800">{source.count}</Text>
|
||||
<Text className="text-xs text-gray-500">
|
||||
{source.conversionRate ? `${(source.conversionRate * 100).toFixed(1)}%` : '0%'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
) : (
|
||||
<View className="text-center py-8">
|
||||
<Text className="text-gray-500">暂无邀请数据</Text>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
className="mt-2"
|
||||
onClick={fetchInviteStats}
|
||||
>
|
||||
刷新数据
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<Text className="text-sm text-gray-600 mb-4">
|
||||
扫描二维码或分享链接邀请好友
|
||||
</Text>
|
||||
|
||||
<Button type="primary" className="mb-2">
|
||||
保存二维码
|
||||
</Button>
|
||||
|
||||
<Button>
|
||||
分享链接
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
|
||||
@@ -1,55 +1,144 @@
|
||||
import React, { useState } from 'react'
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Empty, Tabs, Avatar, Tag, Progress } from '@nutui/nutui-react-taro'
|
||||
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 teamStats = {
|
||||
total: 28,
|
||||
firstLevel: 12,
|
||||
secondLevel: 10,
|
||||
thirdLevel: 6,
|
||||
monthlyCommission: '2,580.50'
|
||||
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)
|
||||
}
|
||||
|
||||
const teamMembers = [
|
||||
{
|
||||
id: '1',
|
||||
name: '张小明',
|
||||
level: 1,
|
||||
joinTime: '2024-11-15',
|
||||
orderCount: 15,
|
||||
commission: '580.50',
|
||||
status: 'active',
|
||||
avatar: '',
|
||||
subMembers: 3
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: '李小红',
|
||||
level: 1,
|
||||
joinTime: '2024-12-01',
|
||||
orderCount: 8,
|
||||
commission: '320.00',
|
||||
status: 'active',
|
||||
avatar: '',
|
||||
subMembers: 2
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: '王小华',
|
||||
level: 2,
|
||||
joinTime: '2024-12-10',
|
||||
orderCount: 5,
|
||||
commission: '150.00',
|
||||
status: 'inactive',
|
||||
avatar: '',
|
||||
subMembers: 0
|
||||
// 初始化加载数据
|
||||
useEffect(() => {
|
||||
if (dealerUser?.userId) {
|
||||
fetchTeamData().then()
|
||||
}
|
||||
]
|
||||
}, [fetchTeamData])
|
||||
|
||||
const getLevelColor = (level: number) => {
|
||||
switch (level) {
|
||||
@@ -69,7 +158,7 @@ const DealerTeam: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const renderMemberItem = (member: any) => (
|
||||
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
|
||||
@@ -83,7 +172,7 @@ const DealerTeam: React.FC = () => {
|
||||
<Text className="font-semibold text-gray-800 mr-2">
|
||||
{member.name}
|
||||
</Text>
|
||||
{getLevelIcon(member.level)}
|
||||
{getLevelIcon(Number(member.level))}
|
||||
<Text className="text-xs text-gray-500 ml-1">
|
||||
{member.level}级
|
||||
</Text>
|
||||
@@ -171,7 +260,7 @@ const DealerTeam: React.FC = () => {
|
||||
<Progress
|
||||
percent={(teamStats.firstLevel / teamStats.total) * 100}
|
||||
strokeWidth="6"
|
||||
background="#f59e0b"
|
||||
background={'#f59e0b'}
|
||||
className="w-20"
|
||||
/>
|
||||
</View>
|
||||
@@ -187,7 +276,7 @@ const DealerTeam: React.FC = () => {
|
||||
<Progress
|
||||
percent={(teamStats.secondLevel / teamStats.total) * 100}
|
||||
strokeWidth="6"
|
||||
background="#8b5cf6"
|
||||
background={'#8b5cf6'}
|
||||
className="w-20"
|
||||
/>
|
||||
</View>
|
||||
@@ -203,7 +292,7 @@ const DealerTeam: React.FC = () => {
|
||||
<Progress
|
||||
percent={(teamStats.thirdLevel / teamStats.total) * 100}
|
||||
strokeWidth="6"
|
||||
background="#ec4899"
|
||||
background={'#ec4899'}
|
||||
className="w-20"
|
||||
/>
|
||||
</View>
|
||||
@@ -220,17 +309,38 @@ const DealerTeam: React.FC = () => {
|
||||
)
|
||||
|
||||
const renderMemberList = (level?: number) => (
|
||||
<View className="p-4">
|
||||
{teamMembers
|
||||
.filter(member => !level || member.level === level)
|
||||
.map(renderMemberItem)}
|
||||
|
||||
{teamMembers.filter(member => !level || member.level === level).length === 0 && (
|
||||
<Empty description={`暂无${level ? level + '级' : ''}团队成员`} />
|
||||
)}
|
||||
</View>
|
||||
<PullToRefresh
|
||||
loading={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}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useRef } from 'react'
|
||||
import React, { useState, useRef, useEffect, useCallback } from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import {
|
||||
Cell,
|
||||
@@ -9,63 +9,196 @@ import {
|
||||
Radio,
|
||||
Tabs,
|
||||
Tag,
|
||||
Empty
|
||||
Empty,
|
||||
Loading,
|
||||
PullToRefresh
|
||||
} from '@nutui/nutui-react-taro'
|
||||
import { Wallet } from '@nutui/icons-react-taro'
|
||||
import { businessGradients } from '@/styles/gradients'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useDealerUser } from '@/hooks/useDealerUser'
|
||||
import { pageShopDealerWithdraw, addShopDealerWithdraw } from '@/api/shop/shopDealerWithdraw'
|
||||
import type { ShopDealerWithdraw } from '@/api/shop/shopDealerWithdraw/model'
|
||||
|
||||
interface WithdrawRecordWithDetails extends ShopDealerWithdraw {
|
||||
accountDisplay?: string
|
||||
}
|
||||
|
||||
const DealerWithdraw: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState('0')
|
||||
const [selectedAccount, setSelectedAccount] = useState('')
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [refreshing, setRefreshing] = useState<boolean>(false)
|
||||
const [submitting, setSubmitting] = useState<boolean>(false)
|
||||
const [availableAmount, setAvailableAmount] = useState<string>('0.00')
|
||||
const [withdrawRecords, setWithdrawRecords] = useState<WithdrawRecordWithDetails[]>([])
|
||||
const formRef = useRef<any>(null)
|
||||
|
||||
// 模拟可提现金额
|
||||
const availableAmount = '1,288.50'
|
||||
const { dealerUser } = useDealerUser()
|
||||
|
||||
// 模拟提现记录
|
||||
const withdrawRecords = [
|
||||
{
|
||||
id: '1',
|
||||
amount: '500.00',
|
||||
account: '尾号1234',
|
||||
status: 'completed',
|
||||
createTime: '2024-12-15 10:30:00',
|
||||
completeTime: '2024-12-15 16:20:00'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
amount: '300.00',
|
||||
account: '尾号1234',
|
||||
status: 'pending',
|
||||
createTime: '2024-12-18 09:15:00'
|
||||
// 获取可提现余额
|
||||
const fetchBalance = useCallback(async () => {
|
||||
try {
|
||||
setAvailableAmount(dealerUser?.money || '0.00')
|
||||
} catch (error) {
|
||||
console.error('获取余额失败:', error)
|
||||
}
|
||||
]
|
||||
}, [])
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
// 获取提现记录
|
||||
const fetchWithdrawRecords = useCallback(async () => {
|
||||
if (!dealerUser?.userId) return
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
const result = await pageShopDealerWithdraw({
|
||||
page: 1,
|
||||
limit: 100,
|
||||
userId: dealerUser.userId
|
||||
})
|
||||
|
||||
if (result?.list) {
|
||||
const processedRecords = result.list.map(record => ({
|
||||
...record,
|
||||
accountDisplay: getAccountDisplay(record)
|
||||
}))
|
||||
setWithdrawRecords(processedRecords)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取提现记录失败:', error)
|
||||
Taro.showToast({
|
||||
title: '获取提现记录失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [dealerUser?.userId])
|
||||
|
||||
// 格式化账户显示
|
||||
const getAccountDisplay = (record: ShopDealerWithdraw) => {
|
||||
if (record.payType === 10) {
|
||||
return '微信钱包'
|
||||
} else if (record.payType === 20 && record.alipayAccount) {
|
||||
return `支付宝(${record.alipayAccount.slice(-4)})`
|
||||
} else if (record.payType === 30 && record.bankCard) {
|
||||
return `${record.bankName || '银行卡'}(尾号${record.bankCard.slice(-4)})`
|
||||
}
|
||||
return '未知账户'
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
const handleRefresh = async () => {
|
||||
setRefreshing(true)
|
||||
await Promise.all([fetchBalance(), fetchWithdrawRecords()])
|
||||
setRefreshing(false)
|
||||
}
|
||||
|
||||
// 初始化加载数据
|
||||
useEffect(() => {
|
||||
if (dealerUser?.userId) {
|
||||
fetchBalance().then()
|
||||
fetchWithdrawRecords().then()
|
||||
}
|
||||
}, [fetchBalance, fetchWithdrawRecords])
|
||||
|
||||
const getStatusText = (status?: number) => {
|
||||
switch (status) {
|
||||
case 'completed': return '已到账'
|
||||
case 'pending': return '处理中'
|
||||
case 'rejected': return '已拒绝'
|
||||
case 40: return '已到账'
|
||||
case 20: return '审核通过'
|
||||
case 10: return '待审核'
|
||||
case 30: return '已驳回'
|
||||
default: return '未知'
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
const getStatusColor = (status?: number) => {
|
||||
switch (status) {
|
||||
case 'completed': return 'success'
|
||||
case 'pending': return 'warning'
|
||||
case 'rejected': return 'danger'
|
||||
case 40: return 'success'
|
||||
case 20: return 'success'
|
||||
case 10: return 'warning'
|
||||
case 30: return 'danger'
|
||||
default: return 'default'
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = (values: any) => {
|
||||
console.log('提现申请:', values)
|
||||
Taro.showToast({
|
||||
title: '提现申请已提交',
|
||||
icon: 'success'
|
||||
})
|
||||
const handleSubmit = async (values: any) => {
|
||||
if (!dealerUser?.userId) {
|
||||
Taro.showToast({
|
||||
title: '用户信息获取失败',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证提现金额
|
||||
const amount = parseFloat(values.amount)
|
||||
const available = parseFloat(availableAmount.replace(',', ''))
|
||||
|
||||
if (amount < 100) {
|
||||
Taro.showToast({
|
||||
title: '最低提现金额为100元',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (amount > available) {
|
||||
Taro.showToast({
|
||||
title: '提现金额超过可用余额',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setSubmitting(true)
|
||||
|
||||
const withdrawData: ShopDealerWithdraw = {
|
||||
userId: dealerUser.userId,
|
||||
money: values.amount,
|
||||
payType: values.accountType === 'wechat' ? 10 :
|
||||
values.accountType === 'alipay' ? 20 : 30,
|
||||
applyStatus: 10, // 待审核
|
||||
platform: 'MiniProgram'
|
||||
}
|
||||
|
||||
// 根据提现方式设置账户信息
|
||||
if (values.accountType === 'alipay') {
|
||||
withdrawData.alipayAccount = values.account
|
||||
withdrawData.alipayName = values.accountName
|
||||
} else if (values.accountType === 'bank') {
|
||||
withdrawData.bankCard = values.account
|
||||
withdrawData.bankAccount = values.accountName
|
||||
withdrawData.bankName = values.bankName || '银行卡'
|
||||
}
|
||||
|
||||
await addShopDealerWithdraw(withdrawData)
|
||||
|
||||
Taro.showToast({
|
||||
title: '提现申请已提交',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 重置表单
|
||||
formRef.current?.resetFields()
|
||||
setSelectedAccount('')
|
||||
|
||||
// 刷新数据
|
||||
await handleRefresh()
|
||||
|
||||
// 切换到提现记录页面
|
||||
setActiveTab('1')
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('提现申请失败:', error)
|
||||
Taro.showToast({
|
||||
title: error.message || '提现申请失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const quickAmounts = ['100', '300', '500', '1000']
|
||||
@@ -165,22 +298,49 @@ const DealerWithdraw: React.FC = () => {
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="account" label="账户信息" required>
|
||||
<Input placeholder="请输入账户号码" />
|
||||
</Form.Item>
|
||||
{selectedAccount === 'alipay' && (
|
||||
<>
|
||||
<Form.Item name="account" label="支付宝账号" required>
|
||||
<Input placeholder="请输入支付宝账号" />
|
||||
</Form.Item>
|
||||
<Form.Item name="accountName" label="支付宝姓名" required>
|
||||
<Input placeholder="请输入支付宝实名姓名" />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Form.Item name="accountName" label="账户姓名" required>
|
||||
<Input placeholder="请输入账户姓名" />
|
||||
</Form.Item>
|
||||
{selectedAccount === 'bank' && (
|
||||
<>
|
||||
<Form.Item name="bankName" label="开户银行" required>
|
||||
<Input placeholder="请输入开户银行名称" />
|
||||
</Form.Item>
|
||||
<Form.Item name="account" label="银行卡号" required>
|
||||
<Input placeholder="请输入银行卡号" />
|
||||
</Form.Item>
|
||||
<Form.Item name="accountName" label="开户姓名" required>
|
||||
<Input placeholder="请输入银行卡开户姓名" />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Form.Item name="remark" label="备注">
|
||||
<Input placeholder="请输入备注信息(可选)" />
|
||||
</Form.Item>
|
||||
{selectedAccount === 'wechat' && (
|
||||
<View className="px-4 py-2">
|
||||
<Text className="text-sm text-gray-500">
|
||||
微信钱包提现将直接转入您的微信零钱
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</CellGroup>
|
||||
|
||||
<View className="mt-6 px-4">
|
||||
<Button block type="primary" nativeType="submit">
|
||||
申请提现
|
||||
<Button
|
||||
block
|
||||
type="primary"
|
||||
nativeType="submit"
|
||||
loading={submitting}
|
||||
disabled={submitting || !selectedAccount}
|
||||
>
|
||||
{submitting ? '提交中...' : '申请提现'}
|
||||
</Button>
|
||||
</View>
|
||||
</Form>
|
||||
@@ -188,40 +348,64 @@ const DealerWithdraw: React.FC = () => {
|
||||
)
|
||||
|
||||
const renderWithdrawRecords = () => (
|
||||
<View className="p-4">
|
||||
{withdrawRecords.length > 0 ? (
|
||||
withdrawRecords.map(record => (
|
||||
<View key={record.id} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
|
||||
<View className="flex justify-between items-start mb-3">
|
||||
<View>
|
||||
<Text className="font-semibold text-gray-800 mb-1">
|
||||
提现金额:¥{record.amount}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-500">
|
||||
提现账户:{record.account}
|
||||
</Text>
|
||||
</View>
|
||||
<Tag type={getStatusColor(record.status)}>
|
||||
{getStatusText(record.status)}
|
||||
</Tag>
|
||||
</View>
|
||||
|
||||
<View className="text-xs text-gray-400">
|
||||
<Text>申请时间:{record.createTime}</Text>
|
||||
{record.completeTime && (
|
||||
<Text className="block mt-1">
|
||||
完成时间:{record.completeTime}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<PullToRefresh
|
||||
loading={refreshing}
|
||||
onRefresh={handleRefresh}
|
||||
>
|
||||
<View className="p-4">
|
||||
{loading ? (
|
||||
<View className="text-center py-8">
|
||||
<Loading />
|
||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||
</View>
|
||||
))
|
||||
) : (
|
||||
<Empty description="暂无提现记录" />
|
||||
)}
|
||||
</View>
|
||||
) : withdrawRecords.length > 0 ? (
|
||||
withdrawRecords.map(record => (
|
||||
<View key={record.id} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
|
||||
<View className="flex justify-between items-start mb-3">
|
||||
<View>
|
||||
<Text className="font-semibold text-gray-800 mb-1">
|
||||
提现金额:¥{record.money}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-500">
|
||||
提现账户:{record.accountDisplay}
|
||||
</Text>
|
||||
</View>
|
||||
<Tag type={getStatusColor(record.applyStatus)}>
|
||||
{getStatusText(record.applyStatus)}
|
||||
</Tag>
|
||||
</View>
|
||||
|
||||
<View className="text-xs text-gray-400">
|
||||
<Text>申请时间:{record.createTime}</Text>
|
||||
{record.auditTime && (
|
||||
<Text className="block mt-1">
|
||||
审核时间:{new Date(record.auditTime).toLocaleString()}
|
||||
</Text>
|
||||
)}
|
||||
{record.rejectReason && (
|
||||
<Text className="block mt-1 text-red-500">
|
||||
驳回原因:{record.rejectReason}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
))
|
||||
) : (
|
||||
<Empty description="暂无提现记录" />
|
||||
)}
|
||||
</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}>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {TenantId} from "@/config/app";
|
||||
import {getOrganization} from "@/api/system/organization";
|
||||
import {myUserVerify} from "@/api/system/userVerify";
|
||||
import { useShopInfo } from '@/hooks/useShopInfo';
|
||||
import {handleInviteRelation} from "@/utils/invite";
|
||||
import MySearch from "./MySearch";
|
||||
import './Header.scss';
|
||||
|
||||
@@ -105,7 +106,7 @@ const Header = (props: any) => {
|
||||
'content-type': 'application/json',
|
||||
TenantId
|
||||
},
|
||||
success: function (res) {
|
||||
success: async function (res) {
|
||||
if (res.data.code == 1) {
|
||||
Taro.showToast({
|
||||
title: res.data.message,
|
||||
@@ -118,6 +119,23 @@ const Header = (props: any) => {
|
||||
Taro.setStorageSync('access_token', res.data.data.access_token)
|
||||
Taro.setStorageSync('UserId', res.data.data.user.userId)
|
||||
setIsLogin(true)
|
||||
|
||||
// 处理邀请关系
|
||||
if (res.data.data.user?.userId) {
|
||||
try {
|
||||
const inviteSuccess = await handleInviteRelation(res.data.data.user.userId)
|
||||
if (inviteSuccess) {
|
||||
Taro.showToast({
|
||||
title: '邀请关系建立成功',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('处理邀请关系失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 重新加载小程序
|
||||
Taro.reLaunch({
|
||||
url: '/pages/index/index'
|
||||
|
||||
@@ -10,22 +10,23 @@ import {myUserVerify} from "@/api/system/userVerify";
|
||||
import {User} from "@/api/system/user/model";
|
||||
import { useShopInfo } from '@/hooks/useShopInfo';
|
||||
import { useUser } from '@/hooks/useUser';
|
||||
import {handleInviteRelation} from "@/utils/invite";
|
||||
import MySearch from "./MySearch";
|
||||
import './Header.scss';
|
||||
|
||||
const Header = (props: any) => {
|
||||
// 使用新的hooks
|
||||
const {
|
||||
shopInfo,
|
||||
loading: shopLoading,
|
||||
getWebsiteName,
|
||||
getWebsiteLogo
|
||||
const {
|
||||
shopInfo,
|
||||
loading: shopLoading,
|
||||
getWebsiteName,
|
||||
getWebsiteLogo
|
||||
} = useShopInfo();
|
||||
|
||||
const {
|
||||
user,
|
||||
isLoggedIn,
|
||||
loading: userLoading
|
||||
|
||||
const {
|
||||
user,
|
||||
isLoggedIn,
|
||||
loading: userLoading
|
||||
} = useUser();
|
||||
|
||||
const [showBasic, setShowBasic] = useState(false)
|
||||
@@ -37,10 +38,10 @@ const Header = (props: any) => {
|
||||
setStatusBarHeight(res.statusBarHeight)
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
// 注意:商店信息现在通过useShopInfo自动管理,不需要手动获取
|
||||
// 用户信息现在通过useUser自动管理,不需要手动获取
|
||||
|
||||
|
||||
// 如果需要获取openId,可以在用户登录后处理
|
||||
if (user && !user.openid) {
|
||||
Taro.login({
|
||||
@@ -88,7 +89,7 @@ const Header = (props: any) => {
|
||||
sceneType: 'save_referee',
|
||||
tenantId: TenantId
|
||||
},
|
||||
success: function (res) {
|
||||
success: async function (res) {
|
||||
if (res.data.code == 1) {
|
||||
Taro.showToast({
|
||||
title: res.data.message,
|
||||
@@ -100,7 +101,23 @@ const Header = (props: any) => {
|
||||
// 登录成功
|
||||
Taro.setStorageSync('access_token', res.data.data.access_token)
|
||||
Taro.setStorageSync('UserId', res.data.data.user.userId)
|
||||
|
||||
|
||||
// 处理邀请关系
|
||||
if (res.data.data.user?.userId) {
|
||||
try {
|
||||
const inviteSuccess = await handleInviteRelation(res.data.data.user.userId)
|
||||
if (inviteSuccess) {
|
||||
Taro.showToast({
|
||||
title: '邀请关系建立成功',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('处理邀请关系失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 重新加载小程序
|
||||
Taro.reLaunch({
|
||||
url: '/pages/index/index'
|
||||
@@ -179,7 +196,7 @@ const Header = (props: any) => {
|
||||
<h3>商店信息</h3>
|
||||
<div>网站名称: {getWebsiteName()}</div>
|
||||
<div>Logo: <img src={getWebsiteLogo()} alt="logo" style={{width: '50px', height: '50px'}} /></div>
|
||||
|
||||
|
||||
<h3>用户信息</h3>
|
||||
<div>登录状态: {isLoggedIn ? '已登录' : '未登录'}</div>
|
||||
{user && (
|
||||
@@ -189,8 +206,8 @@ const Header = (props: any) => {
|
||||
<div>昵称: {user.nickname}</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<button
|
||||
|
||||
<button
|
||||
onClick={() => setShowBasic(false)}
|
||||
style={{marginTop: '20px', padding: '10px 20px'}}
|
||||
>
|
||||
|
||||
@@ -4,6 +4,7 @@ import {Input, Radio, Button} from '@nutui/nutui-react-taro'
|
||||
import {TenantId} from "@/config/app";
|
||||
import './login.scss';
|
||||
import {saveStorageByLoginUser} from "@/utils/server";
|
||||
import {handleInviteRelation} from "@/utils/invite";
|
||||
|
||||
// 微信获取手机号回调参数类型
|
||||
interface GetPhoneNumberDetail {
|
||||
@@ -58,8 +59,25 @@ const Login = (props: LoginProps) => {
|
||||
'content-type': 'application/json',
|
||||
TenantId
|
||||
},
|
||||
success: function (res: LoginResponse) {
|
||||
success: async function (res: LoginResponse) {
|
||||
saveStorageByLoginUser(res.data.data.access_token, res.data.data.user)
|
||||
|
||||
// 处理邀请关系
|
||||
if (res.data.data.user?.userId) {
|
||||
try {
|
||||
const inviteSuccess = await handleInviteRelation(res.data.data.user.userId)
|
||||
if (inviteSuccess) {
|
||||
Taro.showToast({
|
||||
title: '邀请关系建立成功',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('处理邀请关系失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
props.done?.(res.data.data.user);
|
||||
}
|
||||
})
|
||||
|
||||
@@ -10,8 +10,6 @@ import {ShopUserAddress} from "@/api/shop/shopUserAddress/model";
|
||||
import './index.scss'
|
||||
import {useCart, CartItem} from "@/hooks/useCart";
|
||||
import Gap from "@/components/Gap";
|
||||
import {createOrder} from "@/api/shop/shopOrder";
|
||||
import {OrderCreateRequest} from "@/api/shop/shopOrder/model";
|
||||
import {Payment} from "@/api/system/payment/model";
|
||||
import {PaymentHandler, PaymentType, buildCartOrder} from "@/utils/payment";
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ export const cardGradients = {
|
||||
},
|
||||
|
||||
elevated: {
|
||||
background: 'linear-gradient(135deg, #ffffff 0%, #f1f5f9 100%)',
|
||||
background: 'linear-gradient(135deg, #ffffff 0%, #ffffff 100%)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.9)'
|
||||
// 注意:小程序不支持 boxShadow,使用边框和背景替代
|
||||
}
|
||||
|
||||
228
src/utils/invite.ts
Normal file
228
src/utils/invite.ts
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user