feat(dealer): 添加客户列表功能并优化邀请流程
- 新增客户列表页面,实现客户数据获取和筛选功能 - 添加客户状态管理工具函数 - 优化邀请流程,支持绑定推荐关系 - 调整提现功能,增加调试组件 - 修复申请经销商功能中的推荐人ID逻辑
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
export const ENV_CONFIG = {
|
export const ENV_CONFIG = {
|
||||||
// 开发环境
|
// 开发环境
|
||||||
development: {
|
development: {
|
||||||
API_BASE_URL: 'http://127.0.0.1:9200/api',
|
API_BASE_URL: 'https://cms-api.websoft.top/api',
|
||||||
APP_NAME: '开发环境',
|
APP_NAME: '开发环境',
|
||||||
DEBUG: 'true',
|
DEBUG: 'true',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import type { ApiResult, PageResult } from '@/api';
|
import type { ApiResult, PageResult } from '@/api';
|
||||||
import { SERVER_API_URL } from '@/utils/server';
|
import { BaseUrl } from '@/config/app';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 小程序码生成参数
|
* 小程序码生成参数
|
||||||
@@ -34,6 +34,20 @@ export interface InviteRelationParam {
|
|||||||
inviteTime?: string;
|
inviteTime?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定推荐关系参数
|
||||||
|
*/
|
||||||
|
export interface BindRefereeParam {
|
||||||
|
// 推荐人ID
|
||||||
|
dealerId: number;
|
||||||
|
// 被推荐人ID (可选,如果不传则使用当前登录用户)
|
||||||
|
userId?: number;
|
||||||
|
// 推荐来源
|
||||||
|
source?: string;
|
||||||
|
// 场景值
|
||||||
|
scene?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 邀请统计数据
|
* 邀请统计数据
|
||||||
*/
|
*/
|
||||||
@@ -96,37 +110,36 @@ export interface InviteRecordParam {
|
|||||||
* 生成小程序码
|
* 生成小程序码
|
||||||
*/
|
*/
|
||||||
export async function generateMiniProgramCode(data: MiniProgramCodeParam) {
|
export async function generateMiniProgramCode(data: MiniProgramCodeParam) {
|
||||||
const res = await request.post<ApiResult<string>>(
|
try {
|
||||||
SERVER_API_URL + '/invite/generate-miniprogram-code',
|
const url = '/wx-login/getOrderQRCodeUnlimited/' + data.scene;
|
||||||
data
|
// 由于接口直接返回图片buffer,我们直接构建完整的URL
|
||||||
);
|
return `${BaseUrl}${url}`;
|
||||||
if (res.code === 0) {
|
} catch (error: any) {
|
||||||
return res.data;
|
throw new Error(error.message || '生成小程序码失败');
|
||||||
}
|
}
|
||||||
return Promise.reject(new Error(res.message));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成邀请小程序码
|
* 生成邀请小程序码
|
||||||
*/
|
*/
|
||||||
export async function generateInviteCode(inviterId: number, source: string = 'qrcode') {
|
export async function generateInviteCode(inviterId: number) {
|
||||||
const scene = `inviter=${inviterId}&source=${source}&t=${Date.now()}`;
|
const scene = `uid_${inviterId}`;
|
||||||
|
|
||||||
return generateMiniProgramCode({
|
return generateMiniProgramCode({
|
||||||
page: 'pages/index/index',
|
page: 'pages/index/index',
|
||||||
scene: scene,
|
scene: scene,
|
||||||
width: 430,
|
width: 180,
|
||||||
checkPath: true,
|
checkPath: true,
|
||||||
envVersion: 'release'
|
envVersion: 'trial'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 建立邀请关系
|
* 建立邀请关系 (旧接口,保留兼容性)
|
||||||
*/
|
*/
|
||||||
export async function createInviteRelation(data: InviteRelationParam) {
|
export async function createInviteRelation(data: InviteRelationParam) {
|
||||||
const res = await request.post<ApiResult<unknown>>(
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
SERVER_API_URL + '/invite/create-relation',
|
'/invite/create-relation',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
@@ -135,12 +148,38 @@ export async function createInviteRelation(data: InviteRelationParam) {
|
|||||||
return Promise.reject(new Error(res.message));
|
return Promise.reject(new Error(res.message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定推荐关系 (新接口)
|
||||||
|
*/
|
||||||
|
export async function bindRefereeRelation(data: BindRefereeParam) {
|
||||||
|
try {
|
||||||
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
|
'/shop/shop-dealer-referee',
|
||||||
|
{
|
||||||
|
dealerId: data.dealerId,
|
||||||
|
userId: data.userId,
|
||||||
|
source: data.source || 'qrcode',
|
||||||
|
scene: data.scene
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(res.message || '绑定推荐关系失败');
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('绑定推荐关系API调用失败:', error);
|
||||||
|
throw new Error(error.message || '绑定推荐关系失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理邀请场景值
|
* 处理邀请场景值
|
||||||
*/
|
*/
|
||||||
export async function processInviteScene(scene: string, userId: number) {
|
export async function processInviteScene(scene: string, userId: number) {
|
||||||
const res = await request.post<ApiResult<unknown>>(
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
SERVER_API_URL + '/invite/process-scene',
|
'/invite/process-scene',
|
||||||
{ scene, userId }
|
{ scene, userId }
|
||||||
);
|
);
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
@@ -154,7 +193,7 @@ export async function processInviteScene(scene: string, userId: number) {
|
|||||||
*/
|
*/
|
||||||
export async function getInviteStats(inviterId: number) {
|
export async function getInviteStats(inviterId: number) {
|
||||||
const res = await request.get<ApiResult<InviteStats>>(
|
const res = await request.get<ApiResult<InviteStats>>(
|
||||||
SERVER_API_URL + `/invite/stats/${inviterId}`
|
`/invite/stats/${inviterId}`
|
||||||
);
|
);
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
return res.data;
|
return res.data;
|
||||||
@@ -167,7 +206,7 @@ export async function getInviteStats(inviterId: number) {
|
|||||||
*/
|
*/
|
||||||
export async function pageInviteRecords(params: InviteRecordParam) {
|
export async function pageInviteRecords(params: InviteRecordParam) {
|
||||||
const res = await request.get<ApiResult<PageResult<InviteRecord>>>(
|
const res = await request.get<ApiResult<PageResult<InviteRecord>>>(
|
||||||
SERVER_API_URL + '/invite/records/page',
|
'/invite/records/page',
|
||||||
params
|
params
|
||||||
);
|
);
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
@@ -181,7 +220,7 @@ export async function pageInviteRecords(params: InviteRecordParam) {
|
|||||||
*/
|
*/
|
||||||
export async function getMyInviteRecords(params: InviteRecordParam) {
|
export async function getMyInviteRecords(params: InviteRecordParam) {
|
||||||
const res = await request.get<ApiResult<PageResult<InviteRecord>>>(
|
const res = await request.get<ApiResult<PageResult<InviteRecord>>>(
|
||||||
SERVER_API_URL + '/invite/my-records',
|
'/invite/my-records',
|
||||||
params
|
params
|
||||||
);
|
);
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
@@ -195,7 +234,7 @@ export async function getMyInviteRecords(params: InviteRecordParam) {
|
|||||||
*/
|
*/
|
||||||
export async function validateInviteCode(scene: string) {
|
export async function validateInviteCode(scene: string) {
|
||||||
const res = await request.post<ApiResult<{ valid: boolean; inviterId?: number; source?: string }>>(
|
const res = await request.post<ApiResult<{ valid: boolean; inviterId?: number; source?: string }>>(
|
||||||
SERVER_API_URL + '/invite/validate-code',
|
'/invite/validate-code',
|
||||||
{ scene }
|
{ scene }
|
||||||
);
|
);
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
@@ -209,7 +248,7 @@ export async function validateInviteCode(scene: string) {
|
|||||||
*/
|
*/
|
||||||
export async function updateInviteStatus(inviteId: number, status: 'registered' | 'activated') {
|
export async function updateInviteStatus(inviteId: number, status: 'registered' | 'activated') {
|
||||||
const res = await request.put<ApiResult<unknown>>(
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
SERVER_API_URL + `/invite/update-status/${inviteId}`,
|
`/invite/update-status/${inviteId}`,
|
||||||
{ status }
|
{ status }
|
||||||
);
|
);
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
@@ -229,7 +268,7 @@ export async function getInviteRanking(params?: { limit?: number; period?: 'day'
|
|||||||
successCount: number;
|
successCount: number;
|
||||||
conversionRate: number;
|
conversionRate: number;
|
||||||
}>>>(
|
}>>>(
|
||||||
SERVER_API_URL + '/invite/ranking',
|
'/invite/ranking',
|
||||||
params
|
params
|
||||||
);
|
);
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type { ShopDealerUser, ShopDealerUserParam } from './model';
|
|||||||
* 分页查询分销商用户记录表
|
* 分页查询分销商用户记录表
|
||||||
*/
|
*/
|
||||||
export async function pageShopDealerUser(params: ShopDealerUserParam) {
|
export async function pageShopDealerUser(params: ShopDealerUserParam) {
|
||||||
|
// 使用新的request方法,它会自动处理错误并返回完整的ApiResult
|
||||||
const res = await request.get<ApiResult<PageResult<ShopDealerUser>>>(
|
const res = await request.get<ApiResult<PageResult<ShopDealerUser>>>(
|
||||||
'/shop/shop-dealer-user/page',
|
'/shop/shop-dealer-user/page',
|
||||||
params
|
params
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import {SERVER_API_URL} from "@/utils/server";
|
|||||||
*/
|
*/
|
||||||
export async function pageUsers(params: UserParam) {
|
export async function pageUsers(params: UserParam) {
|
||||||
const res = await request.get<ApiResult<PageResult<User>>>(
|
const res = await request.get<ApiResult<PageResult<User>>>(
|
||||||
'/system/user/page',
|
SERVER_API_URL + '/system/user/page',
|
||||||
{params}
|
params
|
||||||
);
|
);
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
return res.data;
|
return res.data;
|
||||||
@@ -23,9 +23,7 @@ export async function pageUsers(params: UserParam) {
|
|||||||
export async function listUsers(params?: UserParam) {
|
export async function listUsers(params?: UserParam) {
|
||||||
const res = await request.get<ApiResult<User[]>>(
|
const res = await request.get<ApiResult<User[]>>(
|
||||||
'/system/user',
|
'/system/user',
|
||||||
{
|
params
|
||||||
params
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
if (res.code === 0 && res.data) {
|
if (res.code === 0 && res.data) {
|
||||||
return res.data;
|
return res.data;
|
||||||
|
|||||||
@@ -62,7 +62,8 @@ export default defineAppConfig({
|
|||||||
"team/index",
|
"team/index",
|
||||||
"qrcode/index",
|
"qrcode/index",
|
||||||
"invite-stats/index",
|
"invite-stats/index",
|
||||||
"info"
|
"info",
|
||||||
|
"customer/index",
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
// {
|
// {
|
||||||
|
|||||||
@@ -11,11 +11,12 @@ import {
|
|||||||
pageShopDealerApply,
|
pageShopDealerApply,
|
||||||
updateShopDealerApply
|
updateShopDealerApply
|
||||||
} from "@/api/shop/shopDealerApply";
|
} from "@/api/shop/shopDealerApply";
|
||||||
|
import {getShopDealerUser} from "@/api/shop/shopDealerUser";
|
||||||
|
|
||||||
const AddUserAddress = () => {
|
const AddUserAddress = () => {
|
||||||
const {user} = useUser()
|
const {user} = useUser()
|
||||||
const [loading, setLoading] = useState<boolean>(true)
|
const [loading, setLoading] = useState<boolean>(true)
|
||||||
const [FormData, setFormData] = useState<ShopDealerApply>({})
|
const [FormData, setFormData] = useState<ShopDealerApply>()
|
||||||
const formRef = useRef<any>(null)
|
const formRef = useRef<any>(null)
|
||||||
const [isEditMode, setIsEditMode] = useState<boolean>(false)
|
const [isEditMode, setIsEditMode] = useState<boolean>(false)
|
||||||
const [existingApply, setExistingApply] = useState<ShopDealerApply | null>(null)
|
const [existingApply, setExistingApply] = useState<ShopDealerApply | null>(null)
|
||||||
@@ -47,31 +48,34 @@ const AddUserAddress = () => {
|
|||||||
setExistingApply(res.list[0]);
|
setExistingApply(res.list[0]);
|
||||||
// 如果有记录,填充表单数据
|
// 如果有记录,填充表单数据
|
||||||
setFormData(res.list[0]);
|
setFormData(res.list[0]);
|
||||||
|
setLoading(false)
|
||||||
} else {
|
} else {
|
||||||
setFormData({})
|
|
||||||
setIsEditMode(false);
|
setIsEditMode(false);
|
||||||
setExistingApply(null);
|
setExistingApply(null);
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
setLoading(true)
|
||||||
console.error('查询申请记录失败:', error);
|
console.error('查询申请记录失败:', error);
|
||||||
setIsEditMode(false);
|
setIsEditMode(false);
|
||||||
setExistingApply(null);
|
setExistingApply(null);
|
||||||
setFormData({})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提交表单
|
// 提交表单
|
||||||
const submitSucceed = async (values: any) => {
|
const submitSucceed = async (values: any) => {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
// 准备提交的数据
|
// 准备提交的数据
|
||||||
const submitData = {
|
const submitData = {
|
||||||
...values,
|
...values,
|
||||||
realName: values.realName || user?.nickname,
|
realName: values.realName || user?.nickname,
|
||||||
mobile: user?.phone,
|
mobile: user?.phone,
|
||||||
refereeId: values.refereeId,
|
refereeId: values.refereeId || FormData?.refereeId,
|
||||||
applyStatus: 10,
|
applyStatus: 10,
|
||||||
auditTime: undefined
|
auditTime: undefined
|
||||||
};
|
};
|
||||||
|
await getShopDealerUser(submitData.refereeId);
|
||||||
|
|
||||||
// 如果是编辑模式,添加现有申请的id
|
// 如果是编辑模式,添加现有申请的id
|
||||||
if (isEditMode && existingApply?.applyId) {
|
if (isEditMode && existingApply?.applyId) {
|
||||||
@@ -86,7 +90,7 @@ const AddUserAddress = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
title: `${isEditMode ? '更新' : '提交'}成功`,
|
title: `${isEditMode ? '提交' : '提交'}成功`,
|
||||||
icon: 'success'
|
icon: 'success'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -95,9 +99,9 @@ const AddUserAddress = () => {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('提交失败:', error);
|
console.error('验证邀请人失败:', error);
|
||||||
Taro.showToast({
|
return Taro.showToast({
|
||||||
title: `${isEditMode ? '更新' : '提交'}失败`,
|
title: '邀请人ID不存在',
|
||||||
icon: 'error'
|
icon: 'error'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -141,8 +145,8 @@ const AddUserAddress = () => {
|
|||||||
<Form.Item name="mobile" label="手机号" initialValue={user?.mobile} required>
|
<Form.Item name="mobile" label="手机号" initialValue={user?.mobile} required>
|
||||||
<Input placeholder="请输入手机号" disabled={true} maxLength={11}/>
|
<Input placeholder="请输入手机号" disabled={true} maxLength={11}/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="refereeId" label="推荐人ID" initialValue={FormData?.refereeId} required>
|
<Form.Item name="refereeId" label="邀请人ID" initialValue={FormData?.refereeId} required>
|
||||||
<Input placeholder="推荐人ID"/>
|
<Input placeholder="邀请人ID"/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</CellGroup>
|
</CellGroup>
|
||||||
</Form>
|
</Form>
|
||||||
@@ -153,29 +157,29 @@ const AddUserAddress = () => {
|
|||||||
title={'审核状态'}
|
title={'审核状态'}
|
||||||
extra={
|
extra={
|
||||||
<span style={{
|
<span style={{
|
||||||
color: FormData.applyStatus === 20 ? '#52c41a' :
|
color: FormData?.applyStatus === 20 ? '#52c41a' :
|
||||||
FormData.applyStatus === 30 ? '#ff4d4f' : '#faad14'
|
FormData?.applyStatus === 30 ? '#ff4d4f' : '#faad14'
|
||||||
}}>
|
}}>
|
||||||
{getApplyStatusText(FormData.applyStatus)}
|
{getApplyStatusText(FormData?.applyStatus)}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{FormData.applyStatus === 20 && (
|
{FormData?.applyStatus === 20 && (
|
||||||
<Cell title={'审核时间'} extra={FormData.auditTime || '无'}/>
|
<Cell title={'审核时间'} extra={FormData?.auditTime || '无'}/>
|
||||||
)}
|
)}
|
||||||
{FormData.applyStatus === 30 && (
|
{FormData?.applyStatus === 30 && (
|
||||||
<Cell title={'驳回原因'} extra={FormData.rejectReason || '无'}/>
|
<Cell title={'驳回原因'} extra={FormData?.rejectReason || '无'}/>
|
||||||
)}
|
)}
|
||||||
</CellGroup>
|
</CellGroup>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{/* 底部浮动按钮 */}
|
{/* 底部浮动按钮 */}
|
||||||
{(!isEditMode || FormData.applyStatus === 10 || FormData.applyStatus === 30) && (
|
{(!isEditMode || FormData?.applyStatus === 10 || FormData?.applyStatus === 30) && (
|
||||||
<FixedButton
|
<FixedButton
|
||||||
icon={<Edit/>}
|
icon={<Edit/>}
|
||||||
text={isEditMode ? '保存修改' : '提交申请'}
|
text={isEditMode ? '保存修改' : '提交申请'}
|
||||||
disabled={FormData.applyStatus === 10}
|
disabled={FormData?.applyStatus === 10}
|
||||||
onClick={handleFixedButtonClick}
|
onClick={handleFixedButtonClick}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
108
src/dealer/customer/README.md
Normal file
108
src/dealer/customer/README.md
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
# 客户管理页面
|
||||||
|
|
||||||
|
## 功能概述
|
||||||
|
|
||||||
|
这是一个完整的客户管理页面,支持客户数据的展示、筛选和搜索功能。
|
||||||
|
|
||||||
|
## 主要功能
|
||||||
|
|
||||||
|
### 1. 数据源
|
||||||
|
- 使用 `pageUsers` API 从 User 表读取客户数据
|
||||||
|
- 支持按状态筛选用户(status: 0 表示正常状态)
|
||||||
|
|
||||||
|
### 2. 状态管理
|
||||||
|
客户状态包括:
|
||||||
|
- **全部** - 显示所有客户
|
||||||
|
- **跟进中** - 正在跟进的潜在客户
|
||||||
|
- **已签约** - 已经签约的客户
|
||||||
|
- **已取消** - 已取消合作的客户
|
||||||
|
|
||||||
|
### 3. 顶部Tabs筛选
|
||||||
|
- 支持按客户状态筛选
|
||||||
|
- 显示每个状态的客户数量统计
|
||||||
|
- 实时更新统计数据
|
||||||
|
|
||||||
|
### 4. 搜索功能
|
||||||
|
支持多字段搜索:
|
||||||
|
- 客户姓名(realName)
|
||||||
|
- 昵称(nickname)
|
||||||
|
- 用户名(username)
|
||||||
|
- 手机号(phone)
|
||||||
|
- 用户ID(userId)
|
||||||
|
|
||||||
|
### 5. 客户信息展示
|
||||||
|
每个客户卡片显示:
|
||||||
|
- 客户姓名和状态标签
|
||||||
|
- 手机号码
|
||||||
|
- 注册时间
|
||||||
|
- 用户ID、余额、积分等统计信息
|
||||||
|
|
||||||
|
## 技术实现
|
||||||
|
|
||||||
|
### 组件结构
|
||||||
|
```
|
||||||
|
CustomerManagement
|
||||||
|
├── 搜索栏 (SearchBar)
|
||||||
|
├── 状态筛选Tabs
|
||||||
|
└── 客户列表
|
||||||
|
└── 客户卡片项
|
||||||
|
```
|
||||||
|
|
||||||
|
### 主要状态
|
||||||
|
- `list`: 客户数据列表
|
||||||
|
- `loading`: 加载状态
|
||||||
|
- `activeTab`: 当前选中的状态Tab
|
||||||
|
- `searchValue`: 搜索关键词
|
||||||
|
|
||||||
|
### 工具函数
|
||||||
|
使用 `@/utils/customerStatus` 工具函数管理客户状态:
|
||||||
|
- `getStatusText()`: 获取状态文本
|
||||||
|
- `getStatusTagType()`: 获取状态标签类型
|
||||||
|
- `getStatusOptions()`: 获取状态选项列表
|
||||||
|
|
||||||
|
## 使用的组件
|
||||||
|
|
||||||
|
### NutUI 组件
|
||||||
|
- `Tabs` / `TabPane`: 状态筛选标签页
|
||||||
|
- `SearchBar`: 搜索输入框
|
||||||
|
- `Tag`: 状态标签
|
||||||
|
- `Loading`: 加载指示器
|
||||||
|
- `Space`: 间距布局
|
||||||
|
|
||||||
|
### 图标
|
||||||
|
- `Phone`: 手机号图标
|
||||||
|
- `User`: 用户图标
|
||||||
|
|
||||||
|
## 数据流
|
||||||
|
|
||||||
|
1. 页面初始化时调用 `fetchCustomerData()` 获取用户数据
|
||||||
|
2. 为每个用户添加客户状态(目前使用随机状态,实际项目中应从数据库获取)
|
||||||
|
3. 根据当前Tab和搜索条件筛选数据
|
||||||
|
4. 渲染客户列表
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
### 临时实现
|
||||||
|
- 当前使用 `getRandomStatus()` 生成随机客户状态
|
||||||
|
- 实际项目中应该:
|
||||||
|
1. 在数据库中添加客户状态字段
|
||||||
|
2. 修改后端API返回真实的客户状态
|
||||||
|
3. 删除随机状态生成函数
|
||||||
|
|
||||||
|
### 扩展建议
|
||||||
|
1. 添加客户详情页面
|
||||||
|
2. 支持客户状态的修改操作
|
||||||
|
3. 添加客户添加/编辑功能
|
||||||
|
4. 支持批量操作
|
||||||
|
5. 添加导出功能
|
||||||
|
6. 支持更多筛选条件(注册时间、地区等)
|
||||||
|
|
||||||
|
## 文件结构
|
||||||
|
```
|
||||||
|
src/dealer/customer/
|
||||||
|
├── index.tsx # 主页面组件
|
||||||
|
└── README.md # 说明文档
|
||||||
|
|
||||||
|
src/utils/
|
||||||
|
└── customerStatus.ts # 客户状态工具函数
|
||||||
|
```
|
||||||
3
src/dealer/customer/index.config.ts
Normal file
3
src/dealer/customer/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '客户列表'
|
||||||
|
})
|
||||||
184
src/dealer/customer/index.tsx
Normal file
184
src/dealer/customer/index.tsx
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
import React, {useState, useEffect, useCallback} from 'react'
|
||||||
|
import {View, Text} from '@tarojs/components'
|
||||||
|
import {Loading, Tabs, TabPane, Tag} from '@nutui/nutui-react-taro'
|
||||||
|
import {Phone} from '@nutui/icons-react-taro'
|
||||||
|
import {pageUsers} from "@/api/system/user";
|
||||||
|
import type {User as UserType} from "@/api/system/user/model";
|
||||||
|
import {
|
||||||
|
CustomerStatus,
|
||||||
|
getStatusText,
|
||||||
|
getStatusTagType,
|
||||||
|
getRandomStatus,
|
||||||
|
getStatusOptions
|
||||||
|
} from '@/utils/customerStatus';
|
||||||
|
|
||||||
|
// 扩展User类型,添加客户状态
|
||||||
|
interface CustomerUser extends UserType {
|
||||||
|
customerStatus?: CustomerStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CustomerManagement: React.FC = () => {
|
||||||
|
const [list, setList] = useState<CustomerUser[]>([])
|
||||||
|
const [loading, setLoading] = useState<boolean>(false)
|
||||||
|
const [activeTab, setActiveTab] = useState<CustomerStatus>('all')
|
||||||
|
const [searchValue, setSearchValue] = useState<string>('')
|
||||||
|
|
||||||
|
// Tab配置
|
||||||
|
const tabList = getStatusOptions();
|
||||||
|
|
||||||
|
// 获取客户数据
|
||||||
|
const fetchCustomerData = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
// 获取用户列表,status: 0 表示正常状态
|
||||||
|
const res = await pageUsers({ status: 0 });
|
||||||
|
if (res?.list) {
|
||||||
|
// 为每个用户添加随机的客户状态(实际项目中应该从后端获取真实状态)
|
||||||
|
const customersWithStatus: CustomerUser[] = res.list.map(user => ({
|
||||||
|
...user,
|
||||||
|
customerStatus: getRandomStatus() // 临时使用随机状态,实际应该从数据库获取
|
||||||
|
}));
|
||||||
|
setList(customersWithStatus);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取客户数据失败:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 根据当前Tab和搜索条件筛选数据
|
||||||
|
const getFilteredList = () => {
|
||||||
|
let filteredList = list;
|
||||||
|
|
||||||
|
// 按状态筛选
|
||||||
|
if (activeTab !== 'all') {
|
||||||
|
filteredList = filteredList.filter(customer => customer.customerStatus === activeTab);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按搜索关键词筛选
|
||||||
|
if (searchValue.trim()) {
|
||||||
|
const keyword = searchValue.trim().toLowerCase();
|
||||||
|
filteredList = filteredList.filter(customer =>
|
||||||
|
(customer.realName && customer.realName.toLowerCase().includes(keyword)) ||
|
||||||
|
(customer.nickname && customer.nickname.toLowerCase().includes(keyword)) ||
|
||||||
|
(customer.username && customer.username.toLowerCase().includes(keyword)) ||
|
||||||
|
(customer.phone && customer.phone.includes(keyword)) ||
|
||||||
|
(customer.userId && customer.userId.toString().includes(keyword))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredList;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取各状态的统计数量
|
||||||
|
const getStatusCounts = () => {
|
||||||
|
const counts = {
|
||||||
|
all: list.length,
|
||||||
|
pending: 0,
|
||||||
|
signed: 0,
|
||||||
|
cancelled: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
list.forEach(customer => {
|
||||||
|
if (customer.customerStatus && counts.hasOwnProperty(customer.customerStatus)) {
|
||||||
|
counts[customer.customerStatus]++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return counts;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化数据
|
||||||
|
useEffect(() => {
|
||||||
|
fetchCustomerData();
|
||||||
|
}, [fetchCustomerData]);
|
||||||
|
|
||||||
|
// 渲染客户项
|
||||||
|
const renderCustomerItem = (customer: CustomerUser) => (
|
||||||
|
<View key={customer.userId} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
|
||||||
|
<View className="flex items-center mb-3">
|
||||||
|
<View className="flex-1">
|
||||||
|
<View className="flex items-center justify-between mb-1">
|
||||||
|
<Text className="font-semibold text-gray-800 mr-2">
|
||||||
|
{customer.realName || customer.nickname || customer.username}
|
||||||
|
</Text>
|
||||||
|
{customer.customerStatus && (
|
||||||
|
<Tag type={getStatusTagType(customer.customerStatus)}>
|
||||||
|
{getStatusText(customer.customerStatus)}
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<View className="flex items-center mb-1">
|
||||||
|
<Phone size={12} className="mr-1" />
|
||||||
|
<Text className="text-xs text-gray-500">
|
||||||
|
{customer.phone || '未填写'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Text className="text-xs text-gray-500">
|
||||||
|
注册时间:{customer.createTime}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
// 渲染客户列表
|
||||||
|
const renderCustomerList = () => {
|
||||||
|
const filteredList = getFilteredList();
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<View className="flex items-center justify-center py-8">
|
||||||
|
<Loading />
|
||||||
|
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filteredList.length === 0) {
|
||||||
|
return (
|
||||||
|
<View className="flex items-center justify-center py-8">
|
||||||
|
<Text className="text-gray-500">暂无客户数据</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className="p-4">
|
||||||
|
{filteredList.map(renderCustomerItem)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className="min-h-screen bg-gray-50">
|
||||||
|
{/* 顶部Tabs */}
|
||||||
|
<View className="bg-white">
|
||||||
|
<Tabs
|
||||||
|
value={activeTab}
|
||||||
|
onChange={(value) => setActiveTab(value as CustomerStatus)}
|
||||||
|
>
|
||||||
|
{tabList.map(tab => {
|
||||||
|
const counts = getStatusCounts();
|
||||||
|
const count = counts[tab.value as keyof typeof counts] || 0;
|
||||||
|
return (
|
||||||
|
<TabPane
|
||||||
|
key={tab.value}
|
||||||
|
title={`${tab.label}${count > 0 ? `(${count})` : ''}`}
|
||||||
|
value={tab.value}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Tabs>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 客户列表 */}
|
||||||
|
{renderCustomerList()}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CustomerManagement;
|
||||||
@@ -7,8 +7,7 @@ import {
|
|||||||
Dongdong,
|
Dongdong,
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
Purse,
|
Purse,
|
||||||
People,
|
People
|
||||||
Presentation
|
|
||||||
} from '@nutui/icons-react-taro'
|
} from '@nutui/icons-react-taro'
|
||||||
import {useDealerUser} from '@/hooks/useDealerUser'
|
import {useDealerUser} from '@/hooks/useDealerUser'
|
||||||
import { useThemeStyles } from '@/hooks/useTheme'
|
import { useThemeStyles } from '@/hooks/useTheme'
|
||||||
@@ -132,28 +131,28 @@ const DealerIndex: React.FC = () => {
|
|||||||
<View className="mb-4">
|
<View className="mb-4">
|
||||||
<Text className="font-semibold text-gray-800">佣金统计</Text>
|
<Text className="font-semibold text-gray-800">佣金统计</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className="grid grid-cols-3 gap-4">
|
<View className="grid grid-cols-3 gap-3">
|
||||||
<View className="text-center p-3 rounded-lg" style={{
|
<View className="text-center p-3 rounded-lg" style={{
|
||||||
background: businessGradients.money.available
|
background: businessGradients.money.available
|
||||||
}}>
|
}}>
|
||||||
<Text className="text-2xl font-bold mb-1 text-white">
|
<Text className="text-lg font-bold mb-1 text-white">
|
||||||
¥{formatMoney(dealerUser.money)}
|
{formatMoney(dealerUser.money)}
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>可提现</Text>
|
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>可提现</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className="text-center p-3 rounded-lg" style={{
|
<View className="text-center p-3 rounded-lg" style={{
|
||||||
background: businessGradients.money.frozen
|
background: businessGradients.money.frozen
|
||||||
}}>
|
}}>
|
||||||
<Text className="text-2xl font-bold mb-1 text-white">
|
<Text className="text-lg font-bold mb-1 text-white">
|
||||||
¥{formatMoney(dealerUser.freezeMoney)}
|
{formatMoney(dealerUser.freezeMoney)}
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>冻结中</Text>
|
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>冻结中</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className="text-center p-3 rounded-lg" style={{
|
<View className="text-center p-3 rounded-lg" style={{
|
||||||
background: businessGradients.money.total
|
background: businessGradients.money.total
|
||||||
}}>
|
}}>
|
||||||
<Text className="text-2xl font-bold mb-1 text-white">
|
<Text className="text-lg font-bold mb-1 text-white">
|
||||||
¥{formatMoney(dealerUser.totalMoney)}
|
{formatMoney(dealerUser.totalMoney)}
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>累计收益</Text>
|
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>累计收益</Text>
|
||||||
</View>
|
</View>
|
||||||
@@ -244,45 +243,45 @@ const DealerIndex: React.FC = () => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/* 第二行功能 */}
|
{/* 第二行功能 */}
|
||||||
<Grid
|
{/*<Grid*/}
|
||||||
columns={4}
|
{/* columns={4}*/}
|
||||||
className="no-border-grid mt-4"
|
{/* className="no-border-grid mt-4"*/}
|
||||||
style={{
|
{/* style={{*/}
|
||||||
'--nutui-grid-border-color': 'transparent',
|
{/* '--nutui-grid-border-color': 'transparent',*/}
|
||||||
'--nutui-grid-item-border-width': '0px',
|
{/* '--nutui-grid-item-border-width': '0px',*/}
|
||||||
border: 'none'
|
{/* border: 'none'*/}
|
||||||
} as React.CSSProperties}
|
{/* } as React.CSSProperties}*/}
|
||||||
>
|
{/*>*/}
|
||||||
<Grid.Item text={'邀请统计'} onClick={() => navigateToPage('/dealer/invite-stats/index')}>
|
{/* <Grid.Item text={'邀请统计'} onClick={() => navigateToPage('/dealer/invite-stats/index')}>*/}
|
||||||
<View className="text-center">
|
{/* <View className="text-center">*/}
|
||||||
<View className="w-12 h-12 bg-indigo-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
{/* <View className="w-12 h-12 bg-indigo-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
|
||||||
<Presentation color="#6366f1" size="20"/>
|
{/* <Presentation color="#6366f1" size="20"/>*/}
|
||||||
</View>
|
{/* </View>*/}
|
||||||
</View>
|
{/* </View>*/}
|
||||||
</Grid.Item>
|
{/* </Grid.Item>*/}
|
||||||
|
|
||||||
{/* 预留其他功能位置 */}
|
{/* /!* 预留其他功能位置 *!/*/}
|
||||||
<Grid.Item text={''}>
|
{/* <Grid.Item text={''}>*/}
|
||||||
<View className="text-center">
|
{/* <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 className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
|
||||||
</View>
|
{/* </View>*/}
|
||||||
</View>
|
{/* </View>*/}
|
||||||
</Grid.Item>
|
{/* </Grid.Item>*/}
|
||||||
|
|
||||||
<Grid.Item text={''}>
|
{/* <Grid.Item text={''}>*/}
|
||||||
<View className="text-center">
|
{/* <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 className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
|
||||||
</View>
|
{/* </View>*/}
|
||||||
</View>
|
{/* </View>*/}
|
||||||
</Grid.Item>
|
{/* </Grid.Item>*/}
|
||||||
|
|
||||||
<Grid.Item text={''}>
|
{/* <Grid.Item text={''}>*/}
|
||||||
<View className="text-center">
|
{/* <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 className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
|
||||||
</View>
|
{/* </View>*/}
|
||||||
</View>
|
{/* </View>*/}
|
||||||
</Grid.Item>
|
{/* </Grid.Item>*/}
|
||||||
</Grid>
|
{/*</Grid>*/}
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,161 +1,63 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react'
|
import React, {useState, useEffect, useCallback} from 'react'
|
||||||
import { View, Text } from '@tarojs/components'
|
import {View, Text, ScrollView} from '@tarojs/components'
|
||||||
import { Empty, Tabs, Tag, PullToRefresh, Loading } from '@nutui/nutui-react-taro'
|
import {Empty, Tag, PullToRefresh, Loading} from '@nutui/nutui-react-taro'
|
||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
import { pageShopDealerOrder } from '@/api/shop/shopDealerOrder'
|
import {pageShopDealerOrder} from '@/api/shop/shopDealerOrder'
|
||||||
import { useDealerUser } from '@/hooks/useDealerUser'
|
import {useDealerUser} from '@/hooks/useDealerUser'
|
||||||
import type { ShopDealerOrder } from '@/api/shop/shopDealerOrder/model'
|
import type {ShopDealerOrder} from '@/api/shop/shopDealerOrder/model'
|
||||||
|
|
||||||
interface OrderWithDetails extends ShopDealerOrder {
|
interface OrderWithDetails extends ShopDealerOrder {
|
||||||
orderNo?: string
|
orderNo?: string
|
||||||
customerName?: string
|
customerName?: string
|
||||||
totalCommission?: string
|
|
||||||
// 当前用户在此订单中的层级和佣金
|
|
||||||
userLevel?: 1 | 2 | 3
|
|
||||||
userCommission?: string
|
userCommission?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const DealerOrders: React.FC = () => {
|
const DealerOrders: React.FC = () => {
|
||||||
const [activeTab, setActiveTab] = useState<string>('0')
|
|
||||||
const [loading, setLoading] = useState<boolean>(false)
|
const [loading, setLoading] = useState<boolean>(false)
|
||||||
|
const [refreshing, setRefreshing] = useState<boolean>(false)
|
||||||
|
const [loadingMore, setLoadingMore] = useState<boolean>(false)
|
||||||
const [orders, setOrders] = useState<OrderWithDetails[]>([])
|
const [orders, setOrders] = useState<OrderWithDetails[]>([])
|
||||||
const [statistics, setStatistics] = useState({
|
const [currentPage, setCurrentPage] = useState<number>(1)
|
||||||
totalOrders: 0,
|
const [hasMore, setHasMore] = useState<boolean>(true)
|
||||||
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 { dealerUser } = useDealerUser()
|
const {dealerUser} = useDealerUser()
|
||||||
|
|
||||||
// 获取订单数据 - 查询当前用户作为各层级分销商的所有订单
|
// 获取订单数据
|
||||||
const fetchOrders = useCallback(async () => {
|
const fetchOrders = useCallback(async (page: number = 1, isRefresh: boolean = false) => {
|
||||||
if (!dealerUser?.userId) return
|
if (!dealerUser?.userId) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true)
|
if (isRefresh) {
|
||||||
|
setRefreshing(true)
|
||||||
// 并行查询三个层级的订单
|
} else if (page === 1) {
|
||||||
const [level1Result, level2Result, level3Result] = await Promise.all([
|
setLoading(true)
|
||||||
// 一级分销商订单
|
} else {
|
||||||
pageShopDealerOrder({
|
setLoadingMore(true)
|
||||||
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' }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理一级分销订单
|
const result = await pageShopDealerOrder({
|
||||||
if (level1Result?.list) {
|
page,
|
||||||
const level1Orders = level1Result.list.map(order => ({
|
limit: 10
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result?.list) {
|
||||||
|
const newOrders = result.list.map(order => ({
|
||||||
...order,
|
...order,
|
||||||
orderNo: `DD${order.orderId}`,
|
orderNo: `${order.orderId}`,
|
||||||
customerName: `用户${order.userId}`,
|
customerName: `用户${order.userId}`,
|
||||||
userLevel: 1 as const,
|
userCommission: order.firstMoney || '0.00'
|
||||||
userCommission: order.firstMoney || '0.00',
|
|
||||||
totalCommission: (
|
|
||||||
parseFloat(order.firstMoney || '0') +
|
|
||||||
parseFloat(order.secondMoney || '0') +
|
|
||||||
parseFloat(order.thirdMoney || '0')
|
|
||||||
).toFixed(2)
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
allOrders.push(...level1Orders)
|
if (page === 1) {
|
||||||
stats.level1.orders = level1Orders.length
|
setOrders(newOrders)
|
||||||
stats.level1.commission = level1Orders.reduce((sum, order) =>
|
} else {
|
||||||
sum + parseFloat(order.userCommission || '0'), 0
|
setOrders(prev => [...prev, ...newOrders])
|
||||||
).toFixed(2)
|
}
|
||||||
|
|
||||||
|
setHasMore(newOrders.length === 10)
|
||||||
|
setCurrentPage(page)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理二级分销订单
|
|
||||||
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) {
|
} catch (error) {
|
||||||
console.error('获取分销订单失败:', error)
|
console.error('获取分销订单失败:', error)
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
@@ -164,18 +66,27 @@ const DealerOrders: React.FC = () => {
|
|||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
setRefreshing(false)
|
||||||
|
setLoadingMore(false)
|
||||||
}
|
}
|
||||||
}, [dealerUser?.userId])
|
}, [dealerUser?.userId])
|
||||||
|
|
||||||
// 刷新数据
|
// 下拉刷新
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
await fetchOrders()
|
await fetchOrders(1, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载更多
|
||||||
|
const handleLoadMore = async () => {
|
||||||
|
if (!loadingMore && hasMore) {
|
||||||
|
await fetchOrders(currentPage + 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化加载数据
|
// 初始化加载数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dealerUser?.userId) {
|
if (dealerUser?.userId) {
|
||||||
fetchOrders().then()
|
fetchOrders(1)
|
||||||
}
|
}
|
||||||
}, [fetchOrders])
|
}, [fetchOrders])
|
||||||
|
|
||||||
@@ -193,198 +104,87 @@ const DealerOrders: React.FC = () => {
|
|||||||
|
|
||||||
const renderOrderItem = (order: OrderWithDetails) => (
|
const renderOrderItem = (order: OrderWithDetails) => (
|
||||||
<View key={order.id} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
|
<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 className="flex justify-between items-start mb-1">
|
||||||
<View>
|
<Text className="font-semibold text-gray-800">
|
||||||
<Text className="font-semibold text-gray-800 mb-1">
|
订单号:{order.orderNo}
|
||||||
订单号:{order.orderNo}
|
</Text>
|
||||||
</Text>
|
|
||||||
<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.isSettled, order.isInvalid)}>
|
<Tag type={getStatusColor(order.isSettled, order.isInvalid)}>
|
||||||
{getStatusText(order.isSettled, order.isInvalid)}
|
{getStatusText(order.isSettled, order.isInvalid)}
|
||||||
</Tag>
|
</Tag>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
<View className="flex justify-between items-center mb-1">
|
||||||
|
<Text className="text-sm text-gray-400">
|
||||||
|
订单金额:¥{order.orderPrice || '0.00'}
|
||||||
|
</Text>
|
||||||
|
<Text className="text-sm text-orange-500 font-semibold">
|
||||||
|
我的佣金:¥{order.userCommission}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
<View className="flex justify-between items-center">
|
<View className="flex justify-between items-center">
|
||||||
<View>
|
<Text className="text-sm text-gray-400">
|
||||||
<Text className="text-sm text-gray-600">
|
客户:{order.customerName}
|
||||||
订单金额:¥{order.orderPrice || '0.00'}
|
</Text>
|
||||||
</Text>
|
<Text className="text-sm text-gray-400">
|
||||||
<Text className="text-sm text-orange-500 font-semibold">
|
|
||||||
我的佣金:¥{order.userCommission}
|
|
||||||
</Text>
|
|
||||||
<Text className="text-xs text-gray-400">
|
|
||||||
总佣金:¥{order.totalCommission}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<Text className="text-xs text-gray-400">
|
|
||||||
{order.createTime}
|
{order.createTime}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</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) {
|
if (!dealerUser) {
|
||||||
return (
|
return (
|
||||||
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
||||||
<Loading />
|
<Loading/>
|
||||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="bg-gray-50 min-h-screen">
|
<View className="min-h-screen bg-gray-50">
|
||||||
{/* 统计卡片 */}
|
<PullToRefresh
|
||||||
<View className="bg-white p-4 mb-4">
|
onRefresh={handleRefresh}
|
||||||
{/* 总体统计 */}
|
disabled={refreshing}
|
||||||
<View className="grid grid-cols-3 gap-4 mb-4">
|
pullingText="下拉刷新"
|
||||||
<View className="text-center">
|
canReleaseText="释放刷新"
|
||||||
<Text className="text-lg font-bold text-blue-500">{statistics.totalOrders}</Text>
|
refreshingText="刷新中..."
|
||||||
<Text className="text-xs text-gray-500">总订单</Text>
|
completeText="刷新完成"
|
||||||
</View>
|
>
|
||||||
<View className="text-center">
|
<ScrollView
|
||||||
<Text className="text-lg font-bold text-green-500">¥{statistics.totalCommission}</Text>
|
scrollY
|
||||||
<Text className="text-xs text-gray-500">总佣金</Text>
|
className="h-screen"
|
||||||
</View>
|
onScrollToLower={handleLoadMore}
|
||||||
<View className="text-center">
|
lowerThreshold={50}
|
||||||
<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
|
|
||||||
onRefresh={handleRefresh}
|
|
||||||
>
|
|
||||||
<View className="p-4">
|
|
||||||
{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="暂无分销订单" />
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</PullToRefresh>
|
|
||||||
</Tabs.TabPane>
|
|
||||||
|
|
||||||
<Tabs.TabPane title="一级分销" value="1">
|
|
||||||
<View className="p-4">
|
<View className="p-4">
|
||||||
{getFilteredOrders('1').length > 0 ? (
|
{loading && orders.length === 0 ? (
|
||||||
getFilteredOrders('1').map(renderOrderItem)
|
<View className="text-center py-8">
|
||||||
|
<Loading/>
|
||||||
|
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||||
|
</View>
|
||||||
|
) : orders.length > 0 ? (
|
||||||
|
<>
|
||||||
|
{orders.map(renderOrderItem)}
|
||||||
|
{loadingMore && (
|
||||||
|
<View className="text-center py-4">
|
||||||
|
<Loading/>
|
||||||
|
<Text className="text-gray-500 mt-1 text-sm">加载更多...</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
{!hasMore && orders.length > 0 && (
|
||||||
|
<View className="text-center py-4">
|
||||||
|
<Text className="text-gray-400 text-sm">没有更多数据了</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Empty description="暂无一级分销订单" />
|
<Empty description="暂无分销订单"/>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</Tabs.TabPane>
|
</ScrollView>
|
||||||
|
</PullToRefresh>
|
||||||
<Tabs.TabPane title="二级分销" value="2">
|
|
||||||
<View className="p-4">
|
|
||||||
{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>
|
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,64 +1,69 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React, {useState, useEffect} from 'react'
|
||||||
import { View, Text, Image } from '@tarojs/components'
|
import {View, Text, Image} from '@tarojs/components'
|
||||||
import { Button, Loading } from '@nutui/nutui-react-taro'
|
import {Button, Loading} from '@nutui/nutui-react-taro'
|
||||||
import { Share, Download, Copy, QrCode } from '@nutui/icons-react-taro'
|
import {Share, Download, Copy, QrCode} from '@nutui/icons-react-taro'
|
||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
import { useDealerUser } from '@/hooks/useDealerUser'
|
import {useDealerUser} from '@/hooks/useDealerUser'
|
||||||
import { generateInviteCode, getInviteStats } from '@/api/invite'
|
import {generateInviteCode} from '@/api/invite'
|
||||||
import type { InviteStats } from '@/api/invite'
|
// import type {InviteStats} from '@/api/invite'
|
||||||
import { businessGradients } from '@/styles/gradients'
|
import {businessGradients} from '@/styles/gradients'
|
||||||
|
|
||||||
const DealerQrcode: React.FC = () => {
|
const DealerQrcode: React.FC = () => {
|
||||||
const [miniProgramCodeUrl, setMiniProgramCodeUrl] = useState<string>('')
|
const [miniProgramCodeUrl, setMiniProgramCodeUrl] = useState<string>('')
|
||||||
const [loading, setLoading] = useState<boolean>(false)
|
const [loading, setLoading] = useState<boolean>(false)
|
||||||
const [inviteStats, setInviteStats] = useState<InviteStats | null>(null)
|
// const [inviteStats, setInviteStats] = useState<InviteStats | null>(null)
|
||||||
const [statsLoading, setStatsLoading] = useState<boolean>(false)
|
// const [statsLoading, setStatsLoading] = useState<boolean>(false)
|
||||||
const { dealerUser } = useDealerUser()
|
const {dealerUser} = useDealerUser()
|
||||||
|
|
||||||
// 生成小程序码
|
// 生成小程序码
|
||||||
const generateMiniProgramCode = async () => {
|
const generateMiniProgramCode = async () => {
|
||||||
if (!dealerUser?.userId) return
|
if (!dealerUser?.userId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
||||||
// 生成邀请小程序码
|
// 生成邀请小程序码
|
||||||
const codeUrl = await generateInviteCode(dealerUser.userId, 'qrcode')
|
const codeUrl = await generateInviteCode(dealerUser.userId)
|
||||||
|
|
||||||
if (codeUrl) {
|
if (codeUrl) {
|
||||||
setMiniProgramCodeUrl(codeUrl)
|
setMiniProgramCodeUrl(codeUrl)
|
||||||
|
} else {
|
||||||
|
throw new Error('返回的小程序码URL为空')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error('生成小程序码失败:', error)
|
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
title: '生成小程序码失败',
|
title: error.message || '生成小程序码失败',
|
||||||
icon: 'error'
|
icon: 'error'
|
||||||
})
|
})
|
||||||
|
// 清空之前的二维码
|
||||||
|
setMiniProgramCodeUrl('')
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取邀请统计数据
|
// 获取邀请统计数据
|
||||||
const fetchInviteStats = async () => {
|
// const fetchInviteStats = async () => {
|
||||||
if (!dealerUser?.userId) return
|
// if (!dealerUser?.userId) return
|
||||||
|
//
|
||||||
try {
|
// try {
|
||||||
setStatsLoading(true)
|
// setStatsLoading(true)
|
||||||
const stats = await getInviteStats(dealerUser.userId)
|
// const stats = await getInviteStats(dealerUser.userId)
|
||||||
stats && setInviteStats(stats)
|
// stats && setInviteStats(stats)
|
||||||
} catch (error) {
|
// } catch (error) {
|
||||||
console.error('获取邀请统计失败:', error)
|
// // 静默处理错误,不影响用户体验
|
||||||
} finally {
|
// } finally {
|
||||||
setStatsLoading(false)
|
// setStatsLoading(false)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// 初始化生成小程序码和获取统计数据
|
// 初始化生成小程序码和获取统计数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dealerUser?.userId) {
|
if (dealerUser?.userId) {
|
||||||
generateMiniProgramCode()
|
generateMiniProgramCode()
|
||||||
fetchInviteStats()
|
// fetchInviteStats()
|
||||||
}
|
}
|
||||||
}, [dealerUser?.userId])
|
}, [dealerUser?.userId])
|
||||||
|
|
||||||
@@ -121,9 +126,9 @@ const DealerQrcode: React.FC = () => {
|
|||||||
|
|
||||||
const inviteText = `🎉 邀请您加入我的团队!
|
const inviteText = `🎉 邀请您加入我的团队!
|
||||||
|
|
||||||
扫描小程序码或搜索"网宿小店"小程序,即可享受优质商品和服务!
|
扫描小程序码或搜索"时里院子市集"小程序,即可享受优质商品和服务!
|
||||||
|
|
||||||
💰 成为我的下级分销商,一起赚取丰厚佣金
|
💰 成为我的团队成员,一起赚取丰厚佣金
|
||||||
🎁 新用户专享优惠等你来拿
|
🎁 新用户专享优惠等你来拿
|
||||||
|
|
||||||
邀请码:${dealerUser.userId}
|
邀请码:${dealerUser.userId}
|
||||||
@@ -153,14 +158,14 @@ const DealerQrcode: React.FC = () => {
|
|||||||
// 小程序分享
|
// 小程序分享
|
||||||
Taro.showShareMenu({
|
Taro.showShareMenu({
|
||||||
withShareTicket: true,
|
withShareTicket: true,
|
||||||
showShareItems: ['shareAppMessage', 'shareTimeline']
|
showShareItems: ['shareAppMessage']
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!dealerUser) {
|
if (!dealerUser) {
|
||||||
return (
|
return (
|
||||||
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
||||||
<Loading />
|
<Loading/>
|
||||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
@@ -179,7 +184,7 @@ const DealerQrcode: React.FC = () => {
|
|||||||
right: '-16px'
|
right: '-16px'
|
||||||
}}></View>
|
}}></View>
|
||||||
|
|
||||||
<View className="relative z-10">
|
<View className="relative z-10 flex flex-col">
|
||||||
<Text className="text-2xl font-bold mb-2 text-white">我的邀请小程序码</Text>
|
<Text className="text-2xl font-bold mb-2 text-white">我的邀请小程序码</Text>
|
||||||
<Text className="text-white text-opacity-80">
|
<Text className="text-white text-opacity-80">
|
||||||
分享小程序码邀请好友,获得丰厚佣金奖励
|
分享小程序码邀请好友,获得丰厚佣金奖励
|
||||||
@@ -193,7 +198,7 @@ const DealerQrcode: React.FC = () => {
|
|||||||
<View className="text-center">
|
<View className="text-center">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<View className="w-48 h-48 mx-auto mb-4 flex items-center justify-center bg-gray-50 rounded-xl">
|
<View className="w-48 h-48 mx-auto mb-4 flex items-center justify-center bg-gray-50 rounded-xl">
|
||||||
<Loading />
|
<Loading/>
|
||||||
<Text className="text-gray-500 mt-2">生成中...</Text>
|
<Text className="text-gray-500 mt-2">生成中...</Text>
|
||||||
</View>
|
</View>
|
||||||
) : miniProgramCodeUrl ? (
|
) : miniProgramCodeUrl ? (
|
||||||
@@ -202,6 +207,20 @@ const DealerQrcode: React.FC = () => {
|
|||||||
src={miniProgramCodeUrl}
|
src={miniProgramCodeUrl}
|
||||||
className="w-full h-full"
|
className="w-full h-full"
|
||||||
mode="aspectFit"
|
mode="aspectFit"
|
||||||
|
onError={() => {
|
||||||
|
Taro.showModal({
|
||||||
|
title: '二维码加载失败',
|
||||||
|
content: '请检查网络连接或联系管理员',
|
||||||
|
showCancel: true,
|
||||||
|
confirmText: '重新生成',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
generateMiniProgramCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
@@ -219,58 +238,64 @@ const DealerQrcode: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Text className="text-lg font-semibold text-gray-800 mb-2">
|
<View className="text-lg font-semibold text-gray-800 mb-2">
|
||||||
扫码加入我的团队
|
扫码加入我的团队
|
||||||
</Text>
|
</View>
|
||||||
<Text className="text-sm text-gray-500 mb-6">
|
<View className="text-sm text-gray-500 mb-4">
|
||||||
好友扫描小程序码即可直接进入小程序并建立邀请关系
|
好友扫描小程序码即可直接进入小程序并建立邀请关系
|
||||||
</Text>
|
</View>
|
||||||
|
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 操作按钮 */}
|
{/* 操作按钮 */}
|
||||||
<View className="space-y-3">
|
<View className={'gap-2'}>
|
||||||
<Button
|
<View className={'my-2'}>
|
||||||
type="primary"
|
<Button
|
||||||
size="large"
|
type="primary"
|
||||||
block
|
size="large"
|
||||||
icon={<Download />}
|
block
|
||||||
onClick={saveMiniProgramCode}
|
icon={<Download/>}
|
||||||
disabled={!miniProgramCodeUrl || loading}
|
onClick={saveMiniProgramCode}
|
||||||
>
|
disabled={!miniProgramCodeUrl || loading}
|
||||||
保存小程序码到相册
|
>
|
||||||
</Button>
|
保存小程序码到相册
|
||||||
|
</Button>
|
||||||
<Button
|
</View>
|
||||||
size="large"
|
<View className={'my-2 bg-white'}>
|
||||||
block
|
<Button
|
||||||
icon={<Copy />}
|
size="large"
|
||||||
onClick={copyInviteInfo}
|
block
|
||||||
disabled={!dealerUser?.userId || loading}
|
icon={<Copy/>}
|
||||||
>
|
onClick={copyInviteInfo}
|
||||||
复制邀请信息
|
disabled={!dealerUser?.userId || loading}
|
||||||
</Button>
|
>
|
||||||
|
复制邀请信息
|
||||||
<Button
|
</Button>
|
||||||
size="large"
|
</View>
|
||||||
block
|
<View className={'my-2 bg-white'}>
|
||||||
fill="outline"
|
<Button
|
||||||
icon={<Share />}
|
size="large"
|
||||||
onClick={shareMiniProgramCode}
|
block
|
||||||
disabled={!dealerUser?.userId || loading}
|
fill="outline"
|
||||||
>
|
icon={<Share/>}
|
||||||
分享给好友
|
onClick={shareMiniProgramCode}
|
||||||
</Button>
|
disabled={!dealerUser?.userId || loading}
|
||||||
|
>
|
||||||
|
分享给好友
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 推广说明 */}
|
{/* 推广说明 */}
|
||||||
<View className="bg-white rounded-2xl p-4 mt-6">
|
<View className="bg-white rounded-2xl p-4 mt-6 hidden">
|
||||||
<Text className="font-semibold text-gray-800 mb-3">推广说明</Text>
|
<Text className="font-semibold text-gray-800 mb-3">推广说明</Text>
|
||||||
<View className="space-y-2">
|
<View className="space-y-2">
|
||||||
<View className="flex items-start">
|
<View className="flex items-start">
|
||||||
<View className="w-2 h-2 bg-blue-500 rounded-full mt-2 mr-3 flex-shrink-0"></View>
|
<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 className="text-sm text-gray-600">
|
||||||
好友通过您的二维码或链接注册成为您的下级分销商
|
好友通过您的二维码或链接注册成为您的团队成员
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className="flex items-start">
|
<View className="flex items-start">
|
||||||
@@ -289,82 +314,82 @@ const DealerQrcode: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 邀请统计数据 */}
|
{/* 邀请统计数据 */}
|
||||||
<View className="bg-white rounded-2xl p-4 mt-4 mb-6">
|
{/*<View className="bg-white rounded-2xl p-4 mt-4 mb-6">*/}
|
||||||
<Text className="font-semibold text-gray-800 mb-3">我的邀请数据</Text>
|
{/* <Text className="font-semibold text-gray-800 mb-3">我的邀请数据</Text>*/}
|
||||||
{statsLoading ? (
|
{/* {statsLoading ? (*/}
|
||||||
<View className="flex items-center justify-center py-8">
|
{/* <View className="flex items-center justify-center py-8">*/}
|
||||||
<Loading />
|
{/* <Loading/>*/}
|
||||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
{/* <Text className="text-gray-500 mt-2">加载中...</Text>*/}
|
||||||
</View>
|
{/* </View>*/}
|
||||||
) : inviteStats ? (
|
{/* ) : inviteStats ? (*/}
|
||||||
<View className="space-y-4">
|
{/* <View className="space-y-4">*/}
|
||||||
<View className="grid grid-cols-2 gap-4">
|
{/* <View className="grid grid-cols-2 gap-4">*/}
|
||||||
<View className="text-center">
|
{/* <View className="text-center">*/}
|
||||||
<Text className="text-2xl font-bold text-blue-500">
|
{/* <Text className="text-2xl font-bold text-blue-500">*/}
|
||||||
{inviteStats.totalInvites || 0}
|
{/* {inviteStats.totalInvites || 0}*/}
|
||||||
</Text>
|
{/* </Text>*/}
|
||||||
<Text className="text-sm text-gray-500">总邀请数</Text>
|
{/* <Text className="text-sm text-gray-500">总邀请数</Text>*/}
|
||||||
</View>
|
{/* </View>*/}
|
||||||
<View className="text-center">
|
{/* <View className="text-center">*/}
|
||||||
<Text className="text-2xl font-bold text-green-500">
|
{/* <Text className="text-2xl font-bold text-green-500">*/}
|
||||||
{inviteStats.successfulRegistrations || 0}
|
{/* {inviteStats.successfulRegistrations || 0}*/}
|
||||||
</Text>
|
{/* </Text>*/}
|
||||||
<Text className="text-sm text-gray-500">成功注册</Text>
|
{/* <Text className="text-sm text-gray-500">成功注册</Text>*/}
|
||||||
</View>
|
{/* </View>*/}
|
||||||
</View>
|
{/* </View>*/}
|
||||||
|
|
||||||
<View className="grid grid-cols-2 gap-4">
|
{/* <View className="grid grid-cols-2 gap-4">*/}
|
||||||
<View className="text-center">
|
{/* <View className="text-center">*/}
|
||||||
<Text className="text-2xl font-bold text-purple-500">
|
{/* <Text className="text-2xl font-bold text-purple-500">*/}
|
||||||
{inviteStats.conversionRate ? `${(inviteStats.conversionRate * 100).toFixed(1)}%` : '0%'}
|
{/* {inviteStats.conversionRate ? `${(inviteStats.conversionRate * 100).toFixed(1)}%` : '0%'}*/}
|
||||||
</Text>
|
{/* </Text>*/}
|
||||||
<Text className="text-sm text-gray-500">转化率</Text>
|
{/* <Text className="text-sm text-gray-500">转化率</Text>*/}
|
||||||
</View>
|
{/* </View>*/}
|
||||||
<View className="text-center">
|
{/* <View className="text-center">*/}
|
||||||
<Text className="text-2xl font-bold text-orange-500">
|
{/* <Text className="text-2xl font-bold text-orange-500">*/}
|
||||||
{inviteStats.todayInvites || 0}
|
{/* {inviteStats.todayInvites || 0}*/}
|
||||||
</Text>
|
{/* </Text>*/}
|
||||||
<Text className="text-sm text-gray-500">今日邀请</Text>
|
{/* <Text className="text-sm text-gray-500">今日邀请</Text>*/}
|
||||||
</View>
|
{/* </View>*/}
|
||||||
</View>
|
{/* </View>*/}
|
||||||
|
|
||||||
{/* 邀请来源统计 */}
|
{/* /!* 邀请来源统计 *!/*/}
|
||||||
{inviteStats.sourceStats && inviteStats.sourceStats.length > 0 && (
|
{/* {inviteStats.sourceStats && inviteStats.sourceStats.length > 0 && (*/}
|
||||||
<View className="mt-4">
|
{/* <View className="mt-4">*/}
|
||||||
<Text className="text-sm font-medium text-gray-700 mb-2">邀请来源分布</Text>
|
{/* <Text className="text-sm font-medium text-gray-700 mb-2">邀请来源分布</Text>*/}
|
||||||
<View className="space-y-2">
|
{/* <View className="space-y-2">*/}
|
||||||
{inviteStats.sourceStats.map((source, index) => (
|
{/* {inviteStats.sourceStats.map((source, index) => (*/}
|
||||||
<View key={index} className="flex items-center justify-between py-2 px-3 bg-gray-50 rounded-lg">
|
{/* <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="flex items-center">*/}
|
||||||
<View className="w-3 h-3 rounded-full bg-blue-500 mr-2"></View>
|
{/* <View className="w-3 h-3 rounded-full bg-blue-500 mr-2"></View>*/}
|
||||||
<Text className="text-sm text-gray-700">{source.source}</Text>
|
{/* <Text className="text-sm text-gray-700">{source.source}</Text>*/}
|
||||||
</View>
|
{/* </View>*/}
|
||||||
<View className="text-right">
|
{/* <View className="text-right">*/}
|
||||||
<Text className="text-sm font-medium text-gray-800">{source.count}</Text>
|
{/* <Text className="text-sm font-medium text-gray-800">{source.count}</Text>*/}
|
||||||
<Text className="text-xs text-gray-500">
|
{/* <Text className="text-xs text-gray-500">*/}
|
||||||
{source.conversionRate ? `${(source.conversionRate * 100).toFixed(1)}%` : '0%'}
|
{/* {source.conversionRate ? `${(source.conversionRate * 100).toFixed(1)}%` : '0%'}*/}
|
||||||
</Text>
|
{/* </Text>*/}
|
||||||
</View>
|
{/* </View>*/}
|
||||||
</View>
|
{/* </View>*/}
|
||||||
))}
|
{/* ))}*/}
|
||||||
</View>
|
{/* </View>*/}
|
||||||
</View>
|
{/* </View>*/}
|
||||||
)}
|
{/* )}*/}
|
||||||
</View>
|
{/* </View>*/}
|
||||||
) : (
|
{/* ) : (*/}
|
||||||
<View className="text-center py-8">
|
{/* <View className="text-center py-8">*/}
|
||||||
<Text className="text-gray-500">暂无邀请数据</Text>
|
{/* <View className="text-gray-500">暂无邀请数据</View>*/}
|
||||||
<Button
|
{/* <Button*/}
|
||||||
size="small"
|
{/* size="small"*/}
|
||||||
type="primary"
|
{/* type="primary"*/}
|
||||||
className="mt-2"
|
{/* className="mt-2"*/}
|
||||||
onClick={fetchInviteStats}
|
{/* onClick={fetchInviteStats}*/}
|
||||||
>
|
{/* >*/}
|
||||||
刷新数据
|
{/* 刷新数据*/}
|
||||||
</Button>
|
{/* </Button>*/}
|
||||||
</View>
|
{/* </View>*/}
|
||||||
)}
|
{/* )}*/}
|
||||||
</View>
|
{/*</View>*/}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react'
|
import React, {useState, useEffect, useCallback} from 'react'
|
||||||
import { View, Text } from '@tarojs/components'
|
import {View, Text} from '@tarojs/components'
|
||||||
import { Empty, Tabs, Avatar, Tag, Progress, Loading, PullToRefresh } from '@nutui/nutui-react-taro'
|
import {Space, Avatar, Loading} from '@nutui/nutui-react-taro'
|
||||||
import { User, Star, StarFill } from '@nutui/icons-react-taro'
|
import {User} from '@nutui/icons-react-taro'
|
||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
import { useDealerUser } from '@/hooks/useDealerUser'
|
import {useDealerUser} from '@/hooks/useDealerUser'
|
||||||
import { listShopDealerReferee } from '@/api/shop/shopDealerReferee'
|
import {listShopDealerReferee} from '@/api/shop/shopDealerReferee'
|
||||||
import { pageShopDealerOrder } from '@/api/shop/shopDealerOrder'
|
import {pageShopDealerOrder} from '@/api/shop/shopDealerOrder'
|
||||||
import type { ShopDealerReferee } from '@/api/shop/shopDealerReferee/model'
|
import type {ShopDealerReferee} from '@/api/shop/shopDealerReferee/model'
|
||||||
|
|
||||||
interface TeamMemberWithStats extends ShopDealerReferee {
|
interface TeamMemberWithStats extends ShopDealerReferee {
|
||||||
name?: string
|
name?: string
|
||||||
@@ -19,30 +19,19 @@ interface TeamMemberWithStats extends ShopDealerReferee {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DealerTeam: React.FC = () => {
|
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 [teamMembers, setTeamMembers] = useState<TeamMemberWithStats[]>([])
|
||||||
const [teamStats, setTeamStats] = useState({
|
const {dealerUser} = useDealerUser()
|
||||||
total: 0,
|
const [dealerId, setDealerId] = useState<number>()
|
||||||
firstLevel: 0,
|
|
||||||
secondLevel: 0,
|
|
||||||
thirdLevel: 0,
|
|
||||||
monthlyCommission: '0.00'
|
|
||||||
})
|
|
||||||
|
|
||||||
const { dealerUser } = useDealerUser()
|
|
||||||
|
|
||||||
// 获取团队数据
|
// 获取团队数据
|
||||||
const fetchTeamData = useCallback(async () => {
|
const fetchTeamData = useCallback(async () => {
|
||||||
if (!dealerUser?.userId) return
|
if (!dealerUser?.userId && !dealerId) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true)
|
console.log(dealerId, 'dealerId>>>>>>>>>')
|
||||||
|
|
||||||
// 获取团队成员关系
|
// 获取团队成员关系
|
||||||
const refereeResult = await listShopDealerReferee({
|
const refereeResult = await listShopDealerReferee({
|
||||||
dealerId: dealerUser.userId
|
dealerId: dealerId ? dealerId : dealerUser?.userId
|
||||||
})
|
})
|
||||||
|
|
||||||
if (refereeResult) {
|
if (refereeResult) {
|
||||||
@@ -73,8 +62,8 @@ const DealerTeam: React.FC = () => {
|
|||||||
const orderCount = orders.length
|
const orderCount = orders.length
|
||||||
const commission = orders.reduce((sum, order) => {
|
const commission = orders.reduce((sum, order) => {
|
||||||
const levelCommission = member.level === 1 ? order.firstMoney :
|
const levelCommission = member.level === 1 ? order.firstMoney :
|
||||||
member.level === 2 ? order.secondMoney :
|
member.level === 2 ? order.secondMoney :
|
||||||
order.thirdMoney
|
order.thirdMoney
|
||||||
return sum + parseFloat(levelCommission || '0')
|
return sum + parseFloat(levelCommission || '0')
|
||||||
}, 0).toFixed(2)
|
}, 0).toFixed(2)
|
||||||
|
|
||||||
@@ -102,18 +91,6 @@ const DealerTeam: React.FC = () => {
|
|||||||
|
|
||||||
setTeamMembers(memberStats)
|
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) {
|
} catch (error) {
|
||||||
console.error('获取团队数据失败:', error)
|
console.error('获取团队数据失败:', error)
|
||||||
@@ -121,50 +98,28 @@ const DealerTeam: React.FC = () => {
|
|||||||
title: '获取团队数据失败',
|
title: '获取团队数据失败',
|
||||||
icon: 'error'
|
icon: 'error'
|
||||||
})
|
})
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
}
|
||||||
}, [dealerUser?.userId])
|
}, [dealerUser?.userId, dealerId])
|
||||||
|
|
||||||
// 刷新数据
|
const getNextUser = (item: TeamMemberWithStats) => {
|
||||||
const handleRefresh = async () => {
|
console.log('点击用户:', item.userId, item.name)
|
||||||
setRefreshing(true)
|
setDealerId(item.userId)
|
||||||
await fetchTeamData()
|
|
||||||
setRefreshing(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化加载数据
|
// 监听数据变化,获取团队数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dealerUser?.userId) {
|
if (dealerUser?.userId || dealerId) {
|
||||||
fetchTeamData().then()
|
fetchTeamData().then()
|
||||||
}
|
}
|
||||||
}, [fetchTeamData])
|
}, [fetchTeamData])
|
||||||
|
|
||||||
const getLevelColor = (level: number) => {
|
|
||||||
switch (level) {
|
|
||||||
case 1: return '#f59e0b'
|
|
||||||
case 2: return '#8b5cf6'
|
|
||||||
case 3: return '#ec4899'
|
|
||||||
default: return '#6b7280'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getLevelIcon = (level: number) => {
|
|
||||||
switch (level) {
|
|
||||||
case 1: return <StarFill color={getLevelColor(level)} size="16" />
|
|
||||||
case 2: return <Star color={getLevelColor(level)} size="16" />
|
|
||||||
case 3: return <User color={getLevelColor(level)} size="16" />
|
|
||||||
default: return <User color={getLevelColor(level)} size="16" />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderMemberItem = (member: TeamMemberWithStats) => (
|
const renderMemberItem = (member: TeamMemberWithStats) => (
|
||||||
<View key={member.id} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
|
<View key={member.id} className="bg-white rounded-lg p-4 mb-3 shadow-sm" onClick={() => getNextUser(member)}>
|
||||||
<View className="flex items-center mb-3">
|
<View className="flex items-center mb-3">
|
||||||
<Avatar
|
<Avatar
|
||||||
size="40"
|
size="40"
|
||||||
src={member.avatar}
|
src={member.avatar}
|
||||||
icon={<User />}
|
icon={<User/>}
|
||||||
className="mr-3"
|
className="mr-3"
|
||||||
/>
|
/>
|
||||||
<View className="flex-1">
|
<View className="flex-1">
|
||||||
@@ -172,194 +127,65 @@ const DealerTeam: React.FC = () => {
|
|||||||
<Text className="font-semibold text-gray-800 mr-2">
|
<Text className="font-semibold text-gray-800 mr-2">
|
||||||
{member.name}
|
{member.name}
|
||||||
</Text>
|
</Text>
|
||||||
{getLevelIcon(Number(member.level))}
|
{/*{getLevelIcon(Number(member.level))}*/}
|
||||||
<Text className="text-xs text-gray-500 ml-1">
|
{/*<Text className="text-xs text-gray-500 ml-1">*/}
|
||||||
{member.level}级
|
{/* {member.level}级*/}
|
||||||
</Text>
|
{/*</Text>*/}
|
||||||
</View>
|
</View>
|
||||||
<Text className="text-xs text-gray-500">
|
<Text className="text-xs text-gray-500">
|
||||||
加入时间:{member.joinTime}
|
加入时间:{member.joinTime}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className="text-right">
|
{/*<View className="text-right">*/}
|
||||||
<Tag
|
{/* <Tag*/}
|
||||||
type={member.status === 'active' ? 'success' : 'default'}
|
{/* type={member.status === 'active' ? 'success' : 'default'}*/}
|
||||||
>
|
{/* >*/}
|
||||||
{member.status === 'active' ? '活跃' : '沉默'}
|
{/* {member.status === 'active' ? '活跃' : '沉默'}*/}
|
||||||
</Tag>
|
{/* </Tag>*/}
|
||||||
</View>
|
{/*</View>*/}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="grid grid-cols-3 gap-4 text-center">
|
<View className="grid grid-cols-3 gap-4 text-center">
|
||||||
<View>
|
<Space>
|
||||||
|
<Text className="text-xs text-gray-500">订单数</Text>
|
||||||
<Text className="text-sm font-semibold text-blue-600">
|
<Text className="text-sm font-semibold text-blue-600">
|
||||||
{member.orderCount}
|
{member.orderCount}
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="text-xs text-gray-500">订单数</Text>
|
</Space>
|
||||||
</View>
|
<Space>
|
||||||
<View>
|
<Text className="text-xs text-gray-500">贡献佣金</Text>
|
||||||
<Text className="text-sm font-semibold text-green-600">
|
<Text className="text-sm font-semibold text-green-600">
|
||||||
¥{member.commission}
|
¥{member.commission}
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="text-xs text-gray-500">贡献佣金</Text>
|
</Space>
|
||||||
</View>
|
<Space>
|
||||||
<View>
|
<Text className="text-xs text-gray-500">团队成员</Text>
|
||||||
<Text className="text-sm font-semibold text-purple-600">
|
<Text className="text-sm font-semibold text-purple-600">
|
||||||
{member.subMembers}
|
{member.subMembers}
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="text-xs text-gray-500">下级成员</Text>
|
</Space>
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|
||||||
const renderOverview = () => (
|
const renderOverview = () => (
|
||||||
<View className="p-4">
|
<View className="rounded-xl p-4">
|
||||||
{/* 团队统计卡片 */}
|
{teamMembers.slice(0, 3).map(renderMemberItem)}
|
||||||
<View className="rounded-xl p-6 mb-6 text-white relative overflow-hidden" style={{
|
|
||||||
background: 'linear-gradient(135deg, #8b5cf6 0%, #ec4899 100%)'
|
|
||||||
}}>
|
|
||||||
{/* 装饰背景 - 小程序兼容版本 */}
|
|
||||||
<View className="absolute w-32 h-32 rounded-full" style={{
|
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
|
||||||
top: '-16px',
|
|
||||||
right: '-16px'
|
|
||||||
}}></View>
|
|
||||||
<View className="absolute w-20 h-20 rounded-full" style={{
|
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
|
||||||
bottom: '-10px',
|
|
||||||
left: '-10px'
|
|
||||||
}}></View>
|
|
||||||
|
|
||||||
<View className="relative z-10">
|
|
||||||
<Text className="text-lg font-bold mb-4 text-white">团队总览</Text>
|
|
||||||
<View className="grid grid-cols-2 gap-4">
|
|
||||||
<View>
|
|
||||||
<Text className="text-2xl font-bold mb-1 text-white">{teamStats.total}</Text>
|
|
||||||
<Text className="text-sm" style={{ color: 'rgba(255, 255, 255, 0.8)' }}>团队总人数</Text>
|
|
||||||
</View>
|
|
||||||
<View>
|
|
||||||
<Text className="text-2xl font-bold mb-1 text-white">¥{teamStats.monthlyCommission}</Text>
|
|
||||||
<Text className="text-sm" style={{ color: 'rgba(255, 255, 255, 0.8)' }}>本月团队佣金</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 层级分布 */}
|
|
||||||
<View className="bg-white rounded-xl p-4 mb-4">
|
|
||||||
<Text className="font-semibold mb-4 text-gray-800">层级分布</Text>
|
|
||||||
<View className="gap-2">
|
|
||||||
<View className="flex items-center justify-between">
|
|
||||||
<View className="flex items-center">
|
|
||||||
<StarFill color="#f59e0b" size="16" className="mr-2" />
|
|
||||||
<Text className="text-sm">一级成员</Text>
|
|
||||||
</View>
|
|
||||||
<View className="flex items-center">
|
|
||||||
<Text className="text-sm font-semibold mr-2">{teamStats.firstLevel}</Text>
|
|
||||||
<Progress
|
|
||||||
percent={(teamStats.firstLevel / teamStats.total) * 100}
|
|
||||||
strokeWidth="6"
|
|
||||||
background={'#f59e0b'}
|
|
||||||
className="w-20"
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="flex items-center justify-between">
|
|
||||||
<View className="flex items-center">
|
|
||||||
<Star color="#8b5cf6" size="16" className="mr-2" />
|
|
||||||
<Text className="text-sm">二级成员</Text>
|
|
||||||
</View>
|
|
||||||
<View className="flex items-center">
|
|
||||||
<Text className="text-sm font-semibold mr-2">{teamStats.secondLevel}</Text>
|
|
||||||
<Progress
|
|
||||||
percent={(teamStats.secondLevel / teamStats.total) * 100}
|
|
||||||
strokeWidth="6"
|
|
||||||
background={'#8b5cf6'}
|
|
||||||
className="w-20"
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="flex items-center justify-between">
|
|
||||||
<View className="flex items-center">
|
|
||||||
<User color="#ec4899" size="16" className="mr-2" />
|
|
||||||
<Text className="text-sm">三级成员</Text>
|
|
||||||
</View>
|
|
||||||
<View className="flex items-center">
|
|
||||||
<Text className="text-sm font-semibold mr-2">{teamStats.thirdLevel}</Text>
|
|
||||||
<Progress
|
|
||||||
percent={(teamStats.thirdLevel / teamStats.total) * 100}
|
|
||||||
strokeWidth="6"
|
|
||||||
background={'#ec4899'}
|
|
||||||
className="w-20"
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 最新成员 */}
|
|
||||||
<View className="bg-white rounded-xl p-4">
|
|
||||||
<Text className="font-semibold mb-4 text-gray-800">最新成员</Text>
|
|
||||||
{teamMembers.slice(0, 3).map(renderMemberItem)}
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|
||||||
const renderMemberList = (level?: number) => (
|
|
||||||
<PullToRefresh
|
|
||||||
disabled={refreshing}
|
|
||||||
onRefresh={handleRefresh}
|
|
||||||
>
|
|
||||||
<View className="p-4">
|
|
||||||
{loading ? (
|
|
||||||
<View className="text-center py-8">
|
|
||||||
<Loading />
|
|
||||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
|
||||||
</View>
|
|
||||||
) : teamMembers
|
|
||||||
.filter(member => !level || member.level === level)
|
|
||||||
.length > 0 ? (
|
|
||||||
teamMembers
|
|
||||||
.filter(member => !level || member.level === level)
|
|
||||||
.map(renderMemberItem)
|
|
||||||
) : (
|
|
||||||
<Empty description={`暂无${level ? level + '级' : ''}团队成员`} />
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</PullToRefresh>
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!dealerUser) {
|
if (!dealerUser) {
|
||||||
return (
|
return (
|
||||||
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
||||||
<Loading />
|
<Loading/>
|
||||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="bg-gray-50 min-h-screen">
|
<View className="min-h-screen">
|
||||||
<Tabs value={activeTab} onChange={() => setActiveTab}>
|
{renderOverview()}
|
||||||
<Tabs.TabPane title="团队总览" value="0">
|
|
||||||
{renderOverview()}
|
|
||||||
</Tabs.TabPane>
|
|
||||||
|
|
||||||
<Tabs.TabPane title="一级成员" value="1">
|
|
||||||
{renderMemberList(1)}
|
|
||||||
</Tabs.TabPane>
|
|
||||||
|
|
||||||
<Tabs.TabPane title="二级成员" value="2">
|
|
||||||
{renderMemberList(2)}
|
|
||||||
</Tabs.TabPane>
|
|
||||||
|
|
||||||
<Tabs.TabPane title="三级成员" value="3">
|
|
||||||
{renderMemberList(3)}
|
|
||||||
</Tabs.TabPane>
|
|
||||||
</Tabs>
|
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
184
src/dealer/withdraw/__tests__/withdraw.test.tsx
Normal file
184
src/dealer/withdraw/__tests__/withdraw.test.tsx
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render, fireEvent, waitFor } from '@testing-library/react'
|
||||||
|
import DealerWithdraw from '../index'
|
||||||
|
import { useDealerUser } from '@/hooks/useDealerUser'
|
||||||
|
import * as withdrawAPI from '@/api/shop/shopDealerWithdraw'
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
jest.mock('@/hooks/useDealerUser')
|
||||||
|
jest.mock('@/api/shop/shopDealerWithdraw')
|
||||||
|
jest.mock('@tarojs/taro', () => ({
|
||||||
|
showToast: jest.fn(),
|
||||||
|
getStorageSync: jest.fn(() => 123),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const mockUseDealerUser = useDealerUser as jest.MockedFunction<typeof useDealerUser>
|
||||||
|
const mockAddShopDealerWithdraw = withdrawAPI.addShopDealerWithdraw as jest.MockedFunction<typeof withdrawAPI.addShopDealerWithdraw>
|
||||||
|
const mockPageShopDealerWithdraw = withdrawAPI.pageShopDealerWithdraw as jest.MockedFunction<typeof withdrawAPI.pageShopDealerWithdraw>
|
||||||
|
|
||||||
|
describe('DealerWithdraw', () => {
|
||||||
|
const mockDealerUser = {
|
||||||
|
userId: 123,
|
||||||
|
money: '10000.00',
|
||||||
|
realName: '测试用户',
|
||||||
|
mobile: '13800138000'
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockUseDealerUser.mockReturnValue({
|
||||||
|
dealerUser: mockDealerUser,
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
refresh: jest.fn()
|
||||||
|
})
|
||||||
|
|
||||||
|
mockPageShopDealerWithdraw.mockResolvedValue({
|
||||||
|
list: [],
|
||||||
|
count: 0
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('应该正确显示可提现余额', () => {
|
||||||
|
const { getByText } = render(<DealerWithdraw />)
|
||||||
|
expect(getByText('10000.00')).toBeInTheDocument()
|
||||||
|
expect(getByText('可提现余额')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('应该验证最低提现金额', async () => {
|
||||||
|
mockAddShopDealerWithdraw.mockResolvedValue('success')
|
||||||
|
|
||||||
|
const { getByPlaceholderText, getByText } = render(<DealerWithdraw />)
|
||||||
|
|
||||||
|
// 输入低于最低金额的数值
|
||||||
|
const amountInput = getByPlaceholderText('请输入提现金额')
|
||||||
|
fireEvent.change(amountInput, { target: { value: '50' } })
|
||||||
|
|
||||||
|
// 选择提现方式
|
||||||
|
const wechatRadio = getByText('微信钱包')
|
||||||
|
fireEvent.click(wechatRadio)
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
const submitButton = getByText('申请提现')
|
||||||
|
fireEvent.click(submitButton)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(require('@tarojs/taro').showToast).toHaveBeenCalledWith({
|
||||||
|
title: '最低提现金额为100元',
|
||||||
|
icon: 'error'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('应该验证提现金额不超过可用余额', async () => {
|
||||||
|
const { getByPlaceholderText, getByText } = render(<DealerWithdraw />)
|
||||||
|
|
||||||
|
// 输入超过可用余额的金额
|
||||||
|
const amountInput = getByPlaceholderText('请输入提现金额')
|
||||||
|
fireEvent.change(amountInput, { target: { value: '20000' } })
|
||||||
|
|
||||||
|
// 选择提现方式
|
||||||
|
const wechatRadio = getByText('微信钱包')
|
||||||
|
fireEvent.click(wechatRadio)
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
const submitButton = getByText('申请提现')
|
||||||
|
fireEvent.click(submitButton)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(require('@tarojs/taro').showToast).toHaveBeenCalledWith({
|
||||||
|
title: '提现金额超过可用余额',
|
||||||
|
icon: 'error'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('应该验证支付宝账户信息完整性', async () => {
|
||||||
|
const { getByPlaceholderText, getByText } = render(<DealerWithdraw />)
|
||||||
|
|
||||||
|
// 输入有效金额
|
||||||
|
const amountInput = getByPlaceholderText('请输入提现金额')
|
||||||
|
fireEvent.change(amountInput, { target: { value: '1000' } })
|
||||||
|
|
||||||
|
// 选择支付宝提现
|
||||||
|
const alipayRadio = getByText('支付宝')
|
||||||
|
fireEvent.click(alipayRadio)
|
||||||
|
|
||||||
|
// 只填写账号,不填写姓名
|
||||||
|
const accountInput = getByPlaceholderText('请输入支付宝账号')
|
||||||
|
fireEvent.change(accountInput, { target: { value: 'test@alipay.com' } })
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
const submitButton = getByText('申请提现')
|
||||||
|
fireEvent.click(submitButton)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(require('@tarojs/taro').showToast).toHaveBeenCalledWith({
|
||||||
|
title: '请填写完整的支付宝信息',
|
||||||
|
icon: 'error'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('应该成功提交微信提现申请', async () => {
|
||||||
|
mockAddShopDealerWithdraw.mockResolvedValue('success')
|
||||||
|
|
||||||
|
const { getByPlaceholderText, getByText } = render(<DealerWithdraw />)
|
||||||
|
|
||||||
|
// 输入有效金额
|
||||||
|
const amountInput = getByPlaceholderText('请输入提现金额')
|
||||||
|
fireEvent.change(amountInput, { target: { value: '1000' } })
|
||||||
|
|
||||||
|
// 选择微信提现
|
||||||
|
const wechatRadio = getByText('微信钱包')
|
||||||
|
fireEvent.click(wechatRadio)
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
const submitButton = getByText('申请提现')
|
||||||
|
fireEvent.click(submitButton)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockAddShopDealerWithdraw).toHaveBeenCalledWith({
|
||||||
|
userId: 123,
|
||||||
|
money: '1000',
|
||||||
|
payType: 10,
|
||||||
|
applyStatus: 10,
|
||||||
|
platform: 'MiniProgram'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(require('@tarojs/taro').showToast).toHaveBeenCalledWith({
|
||||||
|
title: '提现申请已提交',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('快捷金额按钮应该正常工作', () => {
|
||||||
|
const { getByText, getByPlaceholderText } = render(<DealerWithdraw />)
|
||||||
|
|
||||||
|
// 点击快捷金额按钮
|
||||||
|
const quickAmountButton = getByText('500')
|
||||||
|
fireEvent.click(quickAmountButton)
|
||||||
|
|
||||||
|
// 验证金额输入框的值
|
||||||
|
const amountInput = getByPlaceholderText('请输入提现金额') as HTMLInputElement
|
||||||
|
expect(amountInput.value).toBe('500')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('全部按钮应该设置为可用余额', () => {
|
||||||
|
const { getByText, getByPlaceholderText } = render(<DealerWithdraw />)
|
||||||
|
|
||||||
|
// 点击全部按钮
|
||||||
|
const allButton = getByText('全部')
|
||||||
|
fireEvent.click(allButton)
|
||||||
|
|
||||||
|
// 验证金额输入框的值
|
||||||
|
const amountInput = getByPlaceholderText('请输入提现金额') as HTMLInputElement
|
||||||
|
expect(amountInput.value).toBe('10000.00')
|
||||||
|
})
|
||||||
|
})
|
||||||
80
src/dealer/withdraw/debug.tsx
Normal file
80
src/dealer/withdraw/debug.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
import { View, Text } from '@tarojs/components'
|
||||||
|
import { Tabs, Button } from '@nutui/nutui-react-taro'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提现功能调试组件
|
||||||
|
* 用于测试 Tabs 组件的点击和切换功能
|
||||||
|
*/
|
||||||
|
const WithdrawDebug: React.FC = () => {
|
||||||
|
const [activeTab, setActiveTab] = useState<string | number>('0')
|
||||||
|
const [clickCount, setClickCount] = useState(0)
|
||||||
|
|
||||||
|
// Tab 切换处理函数
|
||||||
|
const handleTabChange = (value: string | number) => {
|
||||||
|
console.log('Tab切换:', { from: activeTab, to: value, type: typeof value })
|
||||||
|
setActiveTab(value)
|
||||||
|
setClickCount(prev => prev + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手动切换测试
|
||||||
|
const manualSwitch = (tab: string | number) => {
|
||||||
|
console.log('手动切换到:', tab)
|
||||||
|
setActiveTab(tab)
|
||||||
|
setClickCount(prev => prev + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className="bg-gray-50 min-h-screen p-4">
|
||||||
|
<View className="bg-white rounded-lg p-4 mb-4">
|
||||||
|
<Text className="text-lg font-bold mb-2">调试信息</Text>
|
||||||
|
<Text className="block mb-1">当前Tab: {String(activeTab)}</Text>
|
||||||
|
<Text className="block mb-1">切换次数: {clickCount}</Text>
|
||||||
|
<Text className="block mb-1">Tab类型: {typeof activeTab}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="bg-white rounded-lg p-4 mb-4">
|
||||||
|
<Text className="text-lg font-bold mb-2">手动切换测试</Text>
|
||||||
|
<View className="flex gap-2">
|
||||||
|
<Button size="small" onClick={() => manualSwitch('0')}>
|
||||||
|
切换到申请提现
|
||||||
|
</Button>
|
||||||
|
<Button size="small" onClick={() => manualSwitch('1')}>
|
||||||
|
切换到提现记录
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="bg-white rounded-lg">
|
||||||
|
<Tabs value={activeTab} onChange={handleTabChange}>
|
||||||
|
<Tabs.TabPane title="申请提现" value="0">
|
||||||
|
<View className="p-4">
|
||||||
|
<Text className="text-center text-gray-600">申请提现页面内容</Text>
|
||||||
|
<Text className="text-center text-sm text-gray-400 mt-2">
|
||||||
|
当前Tab值: {String(activeTab)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
|
||||||
|
<Tabs.TabPane title="提现记录" value="1">
|
||||||
|
<View className="p-4">
|
||||||
|
<Text className="text-center text-gray-600">提现记录页面内容</Text>
|
||||||
|
<Text className="text-center text-sm text-gray-400 mt-2">
|
||||||
|
当前Tab值: {String(activeTab)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
</Tabs>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="bg-white rounded-lg p-4 mt-4">
|
||||||
|
<Text className="text-lg font-bold mb-2">事件日志</Text>
|
||||||
|
<Text className="text-sm text-gray-500">
|
||||||
|
请查看控制台输出以获取详细的切换日志
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WithdrawDebug
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import React, { useState, useRef, useEffect, useCallback } from 'react'
|
import React, {useState, useRef, useEffect, useCallback} from 'react'
|
||||||
import { View, Text } from '@tarojs/components'
|
import {View, Text} from '@tarojs/components'
|
||||||
import {
|
import {
|
||||||
Cell,
|
Cell,
|
||||||
|
Space,
|
||||||
Button,
|
Button,
|
||||||
Form,
|
Form,
|
||||||
Input,
|
Input,
|
||||||
@@ -13,19 +14,19 @@ import {
|
|||||||
Loading,
|
Loading,
|
||||||
PullToRefresh
|
PullToRefresh
|
||||||
} from '@nutui/nutui-react-taro'
|
} from '@nutui/nutui-react-taro'
|
||||||
import { Wallet } from '@nutui/icons-react-taro'
|
import {Wallet} from '@nutui/icons-react-taro'
|
||||||
import { businessGradients } from '@/styles/gradients'
|
import {businessGradients} from '@/styles/gradients'
|
||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
import { useDealerUser } from '@/hooks/useDealerUser'
|
import {useDealerUser} from '@/hooks/useDealerUser'
|
||||||
import { pageShopDealerWithdraw, addShopDealerWithdraw } from '@/api/shop/shopDealerWithdraw'
|
import {pageShopDealerWithdraw, addShopDealerWithdraw} from '@/api/shop/shopDealerWithdraw'
|
||||||
import type { ShopDealerWithdraw } from '@/api/shop/shopDealerWithdraw/model'
|
import type {ShopDealerWithdraw} from '@/api/shop/shopDealerWithdraw/model'
|
||||||
|
|
||||||
interface WithdrawRecordWithDetails extends ShopDealerWithdraw {
|
interface WithdrawRecordWithDetails extends ShopDealerWithdraw {
|
||||||
accountDisplay?: string
|
accountDisplay?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const DealerWithdraw: React.FC = () => {
|
const DealerWithdraw: React.FC = () => {
|
||||||
const [activeTab, setActiveTab] = useState('0')
|
const [activeTab, setActiveTab] = useState<string | number>('0')
|
||||||
const [selectedAccount, setSelectedAccount] = useState('')
|
const [selectedAccount, setSelectedAccount] = useState('')
|
||||||
const [loading, setLoading] = useState<boolean>(false)
|
const [loading, setLoading] = useState<boolean>(false)
|
||||||
const [refreshing, setRefreshing] = useState<boolean>(false)
|
const [refreshing, setRefreshing] = useState<boolean>(false)
|
||||||
@@ -34,16 +35,28 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
const [withdrawRecords, setWithdrawRecords] = useState<WithdrawRecordWithDetails[]>([])
|
const [withdrawRecords, setWithdrawRecords] = useState<WithdrawRecordWithDetails[]>([])
|
||||||
const formRef = useRef<any>(null)
|
const formRef = useRef<any>(null)
|
||||||
|
|
||||||
const { dealerUser } = useDealerUser()
|
const {dealerUser} = useDealerUser()
|
||||||
|
|
||||||
|
// Tab 切换处理函数
|
||||||
|
const handleTabChange = (value: string | number) => {
|
||||||
|
console.log('Tab切换到:', value)
|
||||||
|
setActiveTab(value)
|
||||||
|
|
||||||
|
// 如果切换到提现记录页面,刷新数据
|
||||||
|
if (String(value) === '1') {
|
||||||
|
fetchWithdrawRecords()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 获取可提现余额
|
// 获取可提现余额
|
||||||
const fetchBalance = useCallback(async () => {
|
const fetchBalance = useCallback(async () => {
|
||||||
|
console.log(dealerUser, 'dealerUser...')
|
||||||
try {
|
try {
|
||||||
setAvailableAmount(dealerUser?.money || '0.00')
|
setAvailableAmount(dealerUser?.money || '0.00')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取余额失败:', error)
|
console.error('获取余额失败:', error)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [dealerUser])
|
||||||
|
|
||||||
// 获取提现记录
|
// 获取提现记录
|
||||||
const fetchWithdrawRecords = useCallback(async () => {
|
const fetchWithdrawRecords = useCallback(async () => {
|
||||||
@@ -104,21 +117,31 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
|
|
||||||
const getStatusText = (status?: number) => {
|
const getStatusText = (status?: number) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 40: return '已到账'
|
case 40:
|
||||||
case 20: return '审核通过'
|
return '已到账'
|
||||||
case 10: return '待审核'
|
case 20:
|
||||||
case 30: return '已驳回'
|
return '审核通过'
|
||||||
default: return '未知'
|
case 10:
|
||||||
|
return '待审核'
|
||||||
|
case 30:
|
||||||
|
return '已驳回'
|
||||||
|
default:
|
||||||
|
return '未知'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStatusColor = (status?: number) => {
|
const getStatusColor = (status?: number) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 40: return 'success'
|
case 40:
|
||||||
case 20: return 'success'
|
return 'success'
|
||||||
case 10: return 'warning'
|
case 20:
|
||||||
case 30: return 'danger'
|
return 'success'
|
||||||
default: return 'default'
|
case 10:
|
||||||
|
return 'warning'
|
||||||
|
case 30:
|
||||||
|
return 'danger'
|
||||||
|
default:
|
||||||
|
return 'default'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,9 +154,25 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!values.accountType) {
|
||||||
|
Taro.showToast({
|
||||||
|
title: '请选择提现方式',
|
||||||
|
icon: 'error'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 验证提现金额
|
// 验证提现金额
|
||||||
const amount = parseFloat(values.amount)
|
const amount = parseFloat(values.amount)
|
||||||
const available = parseFloat(availableAmount.replace(',', ''))
|
const available = parseFloat(availableAmount.replace(/,/g, ''))
|
||||||
|
|
||||||
|
if (isNaN(amount) || amount <= 0) {
|
||||||
|
Taro.showToast({
|
||||||
|
title: '请输入有效的提现金额',
|
||||||
|
icon: 'error'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (amount < 100) {
|
if (amount < 100) {
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
@@ -151,6 +190,25 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 验证账户信息
|
||||||
|
if (values.accountType === 'alipay') {
|
||||||
|
if (!values.account || !values.accountName) {
|
||||||
|
Taro.showToast({
|
||||||
|
title: '请填写完整的支付宝信息',
|
||||||
|
icon: 'error'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if (values.accountType === 'bank') {
|
||||||
|
if (!values.account || !values.accountName || !values.bankName) {
|
||||||
|
Taro.showToast({
|
||||||
|
title: '请填写完整的银行卡信息',
|
||||||
|
icon: 'error'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setSubmitting(true)
|
setSubmitting(true)
|
||||||
|
|
||||||
@@ -158,7 +216,7 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
userId: dealerUser.userId,
|
userId: dealerUser.userId,
|
||||||
money: values.amount,
|
money: values.amount,
|
||||||
payType: values.accountType === 'wechat' ? 10 :
|
payType: values.accountType === 'wechat' ? 10 :
|
||||||
values.accountType === 'alipay' ? 20 : 30,
|
values.accountType === 'alipay' ? 20 : 30,
|
||||||
applyStatus: 10, // 待审核
|
applyStatus: 10, // 待审核
|
||||||
platform: 'MiniProgram'
|
platform: 'MiniProgram'
|
||||||
}
|
}
|
||||||
@@ -204,15 +262,21 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
const quickAmounts = ['100', '300', '500', '1000']
|
const quickAmounts = ['100', '300', '500', '1000']
|
||||||
|
|
||||||
const setQuickAmount = (amount: string) => {
|
const setQuickAmount = (amount: string) => {
|
||||||
formRef.current?.setFieldsValue({ amount })
|
formRef.current?.setFieldsValue({amount})
|
||||||
}
|
}
|
||||||
|
|
||||||
const setAllAmount = () => {
|
const setAllAmount = () => {
|
||||||
formRef.current?.setFieldsValue({ amount: availableAmount.replace(',', '') })
|
formRef.current?.setFieldsValue({amount: availableAmount.replace(/,/g, '')})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化金额
|
||||||
|
const formatMoney = (money?: string) => {
|
||||||
|
if (!money) return '0.00'
|
||||||
|
return parseFloat(money).toFixed(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderWithdrawForm = () => (
|
const renderWithdrawForm = () => (
|
||||||
<View className="p-4">
|
<View>
|
||||||
{/* 余额卡片 */}
|
{/* 余额卡片 */}
|
||||||
<View className="rounded-xl p-6 mb-6 text-white relative overflow-hidden" style={{
|
<View className="rounded-xl p-6 mb-6 text-white relative overflow-hidden" style={{
|
||||||
background: businessGradients.dealer.header
|
background: businessGradients.dealer.header
|
||||||
@@ -225,14 +289,14 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
}}></View>
|
}}></View>
|
||||||
|
|
||||||
<View className="flex items-center justify-between relative z-10">
|
<View className="flex items-center justify-between relative z-10">
|
||||||
<View>
|
<View className={'flex flex-col'}>
|
||||||
|
<Text className="text-2xl font-bold text-white">{formatMoney(dealerUser?.money)}</Text>
|
||||||
<Text className="text-white text-opacity-80 text-sm mb-1">可提现余额</Text>
|
<Text className="text-white text-opacity-80 text-sm mb-1">可提现余额</Text>
|
||||||
<Text className="text-2xl font-bold text-white">¥{availableAmount}</Text>
|
|
||||||
</View>
|
</View>
|
||||||
<View className="p-3 rounded-full" style={{
|
<View className="p-3 rounded-full" style={{
|
||||||
background: 'rgba(255, 255, 255, 0.2)'
|
background: 'rgba(255, 255, 255, 0.2)'
|
||||||
}}>
|
}}>
|
||||||
<Wallet color="white" size="32" />
|
<Wallet color="white" size="32"/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View className="mt-4 pt-4 relative z-10" style={{
|
<View className="mt-4 pt-4 relative z-10" style={{
|
||||||
@@ -254,7 +318,14 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="请输入提现金额"
|
placeholder="请输入提现金额"
|
||||||
type="number"
|
type="number"
|
||||||
clearable
|
onChange={(value) => {
|
||||||
|
// 实时验证提现金额
|
||||||
|
const amount = parseFloat(value)
|
||||||
|
const available = parseFloat(availableAmount.replace(/,/g, ''))
|
||||||
|
if (!isNaN(amount) && amount > available) {
|
||||||
|
// 可以在这里添加实时提示,但不阻止输入
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
@@ -301,10 +372,10 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
{selectedAccount === 'alipay' && (
|
{selectedAccount === 'alipay' && (
|
||||||
<>
|
<>
|
||||||
<Form.Item name="account" label="支付宝账号" required>
|
<Form.Item name="account" label="支付宝账号" required>
|
||||||
<Input placeholder="请输入支付宝账号" />
|
<Input placeholder="请输入支付宝账号"/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="accountName" label="支付宝姓名" required>
|
<Form.Item name="accountName" label="支付宝姓名" required>
|
||||||
<Input placeholder="请输入支付宝实名姓名" />
|
<Input placeholder="请输入支付宝实名姓名"/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -312,13 +383,13 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
{selectedAccount === 'bank' && (
|
{selectedAccount === 'bank' && (
|
||||||
<>
|
<>
|
||||||
<Form.Item name="bankName" label="开户银行" required>
|
<Form.Item name="bankName" label="开户银行" required>
|
||||||
<Input placeholder="请输入开户银行名称" />
|
<Input placeholder="请输入开户银行名称"/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="account" label="银行卡号" required>
|
<Form.Item name="account" label="银行卡号" required>
|
||||||
<Input placeholder="请输入银行卡号" />
|
<Input placeholder="请输入银行卡号"/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="accountName" label="开户姓名" required>
|
<Form.Item name="accountName" label="开户姓名" required>
|
||||||
<Input placeholder="请输入银行卡开户姓名" />
|
<Input placeholder="请输入银行卡开户姓名"/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -347,60 +418,64 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|
||||||
const renderWithdrawRecords = () => (
|
const renderWithdrawRecords = () => {
|
||||||
<PullToRefresh
|
console.log('渲染提现记录:', {loading, recordsCount: withdrawRecords.length, dealerUserId: dealerUser?.userId})
|
||||||
disabled={refreshing}
|
|
||||||
onRefresh={handleRefresh}
|
|
||||||
>
|
|
||||||
<View className="p-4">
|
|
||||||
{loading ? (
|
|
||||||
<View className="text-center py-8">
|
|
||||||
<Loading />
|
|
||||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
|
||||||
</View>
|
|
||||||
) : 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">
|
return (
|
||||||
<Text>申请时间:{record.createTime}</Text>
|
<PullToRefresh
|
||||||
{record.auditTime && (
|
disabled={refreshing}
|
||||||
<Text className="block mt-1">
|
onRefresh={handleRefresh}
|
||||||
审核时间:{new Date(record.auditTime).toLocaleString()}
|
>
|
||||||
</Text>
|
<View>
|
||||||
)}
|
{loading ? (
|
||||||
{record.rejectReason && (
|
<View className="text-center py-8">
|
||||||
<Text className="block mt-1 text-red-500">
|
<Loading/>
|
||||||
驳回原因:{record.rejectReason}
|
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
))
|
) : withdrawRecords.length > 0 ? (
|
||||||
) : (
|
withdrawRecords.map(record => (
|
||||||
<Empty description="暂无提现记录" />
|
<View key={record.id} className="rounded-lg bg-gray-50 p-4 mb-3 shadow-sm">
|
||||||
)}
|
<View className="flex justify-between items-start mb-3">
|
||||||
</View>
|
<Space>
|
||||||
</PullToRefresh>
|
<Text className="font-semibold text-gray-800 mb-1">
|
||||||
)
|
提现金额:¥{record.money}
|
||||||
|
</Text>
|
||||||
|
<Text className="text-sm text-gray-500">
|
||||||
|
提现账户:{record.accountDisplay}
|
||||||
|
</Text>
|
||||||
|
</Space>
|
||||||
|
<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) {
|
if (!dealerUser) {
|
||||||
return (
|
return (
|
||||||
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
||||||
<Loading />
|
<Loading/>
|
||||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
@@ -408,7 +483,7 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="bg-gray-50 min-h-screen">
|
<View className="bg-gray-50 min-h-screen">
|
||||||
<Tabs value={activeTab} onChange={() => setActiveTab}>
|
<Tabs value={activeTab} onChange={handleTabChange}>
|
||||||
<Tabs.TabPane title="申请提现" value="0">
|
<Tabs.TabPane title="申请提现" value="0">
|
||||||
{renderWithdrawForm()}
|
{renderWithdrawForm()}
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
|
|||||||
@@ -4,54 +4,14 @@ import {View, Text} from '@tarojs/components';
|
|||||||
import {Space, Tabs, Button, Empty} from '@nutui/nutui-react-taro';
|
import {Space, Tabs, Button, Empty} from '@nutui/nutui-react-taro';
|
||||||
import {Phone} from '@nutui/icons-react-taro';
|
import {Phone} from '@nutui/icons-react-taro';
|
||||||
import './list.scss';
|
import './list.scss';
|
||||||
|
import {pageUsers} from "@/api/system/user";
|
||||||
|
import {ShopDealerUser} from "@/api/shop/shopDealerUser/model";
|
||||||
|
|
||||||
// 客户数据类型定义
|
|
||||||
interface Customer {
|
|
||||||
id: string;
|
|
||||||
companyName: string;
|
|
||||||
contactPerson: string;
|
|
||||||
phone: string;
|
|
||||||
address: string;
|
|
||||||
addTime: string;
|
|
||||||
status: 'pending' | 'confirmed' | 'cancelled';
|
|
||||||
}
|
|
||||||
|
|
||||||
const CustomerList = () => {
|
const CustomerList = () => {
|
||||||
const [statusBarHeight, setStatusBarHeight] = useState<number>(0);
|
|
||||||
const [activeTab, setActiveTab] = useState<string>('all');
|
const [activeTab, setActiveTab] = useState<string>('all');
|
||||||
const [customers, setCustomers] = useState<Customer[]>([]);
|
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
const [list, setList] = useState<ShopDealerUser[]>([]);
|
||||||
// 模拟客户数据
|
|
||||||
const mockCustomers: Customer[] = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
companyName: '广州雅虎信息科技公司',
|
|
||||||
contactPerson: 'XXX',
|
|
||||||
phone: '13882223433',
|
|
||||||
address: '广西南宁市良庆区五象大道401号五象新城1号楼1226室XXXXXX',
|
|
||||||
addTime: '2025-08-15 10:23:33',
|
|
||||||
status: 'pending'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
companyName: '广州雅虎信息科技公司',
|
|
||||||
contactPerson: 'XXX',
|
|
||||||
phone: '13882223433',
|
|
||||||
address: '广西南宁市良庆区五象大道401号五象新城1号楼1226室XXXXXX',
|
|
||||||
addTime: '2025-08-15 10:23:33',
|
|
||||||
status: 'confirmed'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
companyName: '广州雅虎信息科技公司',
|
|
||||||
contactPerson: 'XXX',
|
|
||||||
phone: '13882223433',
|
|
||||||
address: '广西南宁市良庆区五象大道401号五象新城1号楼1226室XXXXXX',
|
|
||||||
addTime: '2025-08-15 10:23:33',
|
|
||||||
status: 'cancelled'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const tabList = [
|
const tabList = [
|
||||||
{title: '全部', value: 'all'},
|
{title: '全部', value: 'all'},
|
||||||
@@ -62,18 +22,26 @@ const CustomerList = () => {
|
|||||||
|
|
||||||
const reload = async () => {
|
const reload = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
// 模拟API调用
|
try {
|
||||||
setTimeout(() => {
|
const res = await pageUsers({status: 0});
|
||||||
setCustomers(mockCustomers);
|
console.log(res, '客户列表');
|
||||||
|
if(res?.list){
|
||||||
|
// 为每个用户添加默认状态
|
||||||
|
const customersWithStatus: ShopDealerUser[] = res.list.map(user => ({
|
||||||
|
...user,
|
||||||
|
status: 'pending' // 默认状态为跟进中
|
||||||
|
}));
|
||||||
|
setList(customersWithStatus);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取客户列表失败:', error);
|
||||||
|
Taro.showToast({
|
||||||
|
title: '获取客户列表失败',
|
||||||
|
icon: 'error'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}, 500);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getFilteredCustomers = () => {
|
|
||||||
if (activeTab === 'all') {
|
|
||||||
return customers;
|
|
||||||
}
|
}
|
||||||
return customers.filter(customer => customer.status === activeTab);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusText = (status: string) => {
|
const getStatusText = (status: string) => {
|
||||||
@@ -108,12 +76,12 @@ const CustomerList = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAction = (customer: Customer, action: 'sign' | 'cancel' | 'detail') => {
|
const handleAction = (customer: ShopDealerUser, action: 'sign' | 'cancel' | 'detail') => {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'sign':
|
case 'sign':
|
||||||
// 跳转到签约页面
|
// 跳转到签约页面
|
||||||
Taro.navigateTo({
|
Taro.navigateTo({
|
||||||
url: `/pages/customer/sign?customerId=${customer.id}`
|
url: `/pages/customer/sign?customerId=${customer.userId}`
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'cancel':
|
case 'cancel':
|
||||||
@@ -136,7 +104,7 @@ const CustomerList = () => {
|
|||||||
case 'detail':
|
case 'detail':
|
||||||
// 跳转到客户详情页面
|
// 跳转到客户详情页面
|
||||||
Taro.navigateTo({
|
Taro.navigateTo({
|
||||||
url: `/pages/customer/detail?customerId=${customer.id}`
|
url: `/pages/customer/detail?customerId=${customer.userId}`
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -149,19 +117,7 @@ const CustomerList = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTrading = () => {
|
|
||||||
// 跳转到入市交易页面
|
|
||||||
Taro.navigateTo({
|
|
||||||
url: '/pages/customer/trading'
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Taro.getSystemInfo({
|
|
||||||
success: (res) => {
|
|
||||||
setStatusBarHeight(Number(res.statusBarHeight));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
reload().then();
|
reload().then();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -190,67 +146,67 @@ const CustomerList = () => {
|
|||||||
<View className="loading-container">
|
<View className="loading-container">
|
||||||
<Text>加载中...</Text>
|
<Text>加载中...</Text>
|
||||||
</View>
|
</View>
|
||||||
) : getFilteredCustomers().length > 0 ? (
|
) : list.length > 0 ? (
|
||||||
getFilteredCustomers().map((customer) => (
|
list.map((record) => (
|
||||||
<View key={customer.id} className="customer-item">
|
<View key={record.userId} className="customer-item">
|
||||||
<View className="customer-header">
|
<View className="customer-header">
|
||||||
<Text className="company-name">{customer.companyName}</Text>
|
<Text className="company-name">{record.realName || '未知客户'}</Text>
|
||||||
<Text
|
<Text
|
||||||
className="status-tag"
|
className="status-tag"
|
||||||
style={{color: getStatusColor(customer.status)}}
|
style={{color: getStatusColor('pending')}}
|
||||||
>
|
>
|
||||||
{getStatusText(customer.status)}
|
{getStatusText('pending')}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="customer-info">
|
<View className="customer-info">
|
||||||
<View className="info-row">
|
<View className="info-row">
|
||||||
<Text className="label">联系人:</Text>
|
<Text className="label">联系人:</Text>
|
||||||
<Text className="value">{customer.contactPerson}</Text>
|
<Text className="value">{record.realName || '未知'}</Text>
|
||||||
<Text className="label contact-label">联系电话:</Text>
|
<Text className="label contact-label">联系电话:</Text>
|
||||||
<Text className="value">{customer.phone}</Text>
|
<Text className="value">{record.mobile || '未提供'}</Text>
|
||||||
<Phone
|
<Phone
|
||||||
size={14}
|
size={14}
|
||||||
className={'text-green-500'}
|
className={'text-green-500'}
|
||||||
onClick={() => handleCall(customer.phone)}
|
onClick={() => handleCall(`${record?.mobile}`)}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="address-row">
|
<View className="address-row">
|
||||||
<Text className="label">地址:</Text>
|
<Text className="label">地址:</Text>
|
||||||
<Text className="address">{customer.address}</Text>
|
<Text className="address">{'地址未提供'}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="time-row">
|
<View className="time-row">
|
||||||
<Text className="time">添加时间:{customer.addTime}</Text>
|
<Text className="time">添加时间:{record.createTime || '未知'}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 操作按钮 */}
|
{/* 操作按钮 */}
|
||||||
<View className="action-buttons">
|
<View className="action-buttons">
|
||||||
{customer.status === 'pending' && (
|
{record.payPassword === 'pending' && (
|
||||||
<Space>
|
<Space>
|
||||||
<Button
|
<Button
|
||||||
className="action-btn sign-btn"
|
className="action-btn sign-btn"
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => handleAction(customer, 'sign')}
|
onClick={() => handleAction(record, 'sign')}
|
||||||
>
|
>
|
||||||
签约
|
签约
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className="action-btn cancel-btn"
|
className="action-btn cancel-btn"
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => handleAction(customer, 'cancel')}
|
onClick={() => handleAction(record, 'cancel')}
|
||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
)}
|
)}
|
||||||
{customer.status === 'confirmed' && (
|
{record.payPassword === 'confirmed' && (
|
||||||
<Button
|
<Button
|
||||||
className="action-btn detail-btn"
|
className="action-btn detail-btn"
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => handleAction(customer, 'detail')}
|
onClick={() => handleAction(record, 'detail')}
|
||||||
>
|
>
|
||||||
查看详情
|
查看详情
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
69
src/utils/customerStatus.ts
Normal file
69
src/utils/customerStatus.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
/**
|
||||||
|
* 客户状态管理工具函数
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 客户状态类型定义
|
||||||
|
export type CustomerStatus = 'all' | 'pending' | 'signed' | 'cancelled';
|
||||||
|
|
||||||
|
// 客户状态配置
|
||||||
|
export const CUSTOMER_STATUS_CONFIG = {
|
||||||
|
all: {
|
||||||
|
label: '全部',
|
||||||
|
color: '#666666',
|
||||||
|
tagType: 'default' as const
|
||||||
|
},
|
||||||
|
pending: {
|
||||||
|
label: '跟进中',
|
||||||
|
color: '#ff8800',
|
||||||
|
tagType: 'warning' as const
|
||||||
|
},
|
||||||
|
signed: {
|
||||||
|
label: '已签约',
|
||||||
|
color: '#52c41a',
|
||||||
|
tagType: 'success' as const
|
||||||
|
},
|
||||||
|
cancelled: {
|
||||||
|
label: '已取消',
|
||||||
|
color: '#ff4d4f',
|
||||||
|
tagType: 'danger' as const
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取状态文本
|
||||||
|
*/
|
||||||
|
export const getStatusText = (status: CustomerStatus): string => {
|
||||||
|
return CUSTOMER_STATUS_CONFIG[status]?.label || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取状态标签类型
|
||||||
|
*/
|
||||||
|
export const getStatusTagType = (status: CustomerStatus) => {
|
||||||
|
return CUSTOMER_STATUS_CONFIG[status]?.tagType || 'default';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取状态颜色
|
||||||
|
*/
|
||||||
|
export const getStatusColor = (status: CustomerStatus): string => {
|
||||||
|
return CUSTOMER_STATUS_CONFIG[status]?.color || '#666666';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有状态选项
|
||||||
|
*/
|
||||||
|
export const getStatusOptions = () => {
|
||||||
|
return Object.entries(CUSTOMER_STATUS_CONFIG).map(([value, config]) => ({
|
||||||
|
value: value as CustomerStatus,
|
||||||
|
label: config.label
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 临时函数:生成随机状态(实际项目中应该删除,从数据库获取真实状态)
|
||||||
|
*/
|
||||||
|
export const getRandomStatus = (): CustomerStatus => {
|
||||||
|
const statuses: CustomerStatus[] = ['pending', 'signed', 'cancelled'];
|
||||||
|
return statuses[Math.floor(Math.random() * statuses.length)];
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user