feat(dealer): 添加客户列表功能并优化邀请流程

- 新增客户列表页面,实现客户数据获取和筛选功能
- 添加客户状态管理工具函数
- 优化邀请流程,支持绑定推荐关系
- 调整提现功能,增加调试组件
- 修复申请经销商功能中的推荐人ID逻辑
This commit is contained in:
2025-09-03 10:41:06 +08:00
parent 7834124095
commit 0b43a3bc92
18 changed files with 1286 additions and 934 deletions

View File

@@ -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',
}, },

View File

@@ -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) {

View File

@@ -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

View File

@@ -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;

View File

@@ -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",
] ]
}, },
// { // {

View File

@@ -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}
/> />
)} )}

View File

@@ -0,0 +1,108 @@
# 客户管理页面
## 功能概述
这是一个完整的客户管理页面,支持客户数据的展示、筛选和搜索功能。
## 主要功能
### 1. 数据源
- 使用 `pageUsers` API 从 User 表读取客户数据
- 支持按状态筛选用户status: 0 表示正常状态)
### 2. 状态管理
客户状态包括:
- **全部** - 显示所有客户
- **跟进中** - 正在跟进的潜在客户
- **已签约** - 已经签约的客户
- **已取消** - 已取消合作的客户
### 3. 顶部Tabs筛选
- 支持按客户状态筛选
- 显示每个状态的客户数量统计
- 实时更新统计数据
### 4. 搜索功能
支持多字段搜索:
- 客户姓名realName
- 昵称nickname
- 用户名username
- 手机号phone
- 用户IDuserId
### 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 # 客户状态工具函数
```

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '客户列表'
})

View 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;

View File

@@ -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>

View File

@@ -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 {
if (isRefresh) {
setRefreshing(true)
} else if (page === 1) {
setLoading(true) setLoading(true)
} else {
setLoadingMore(true)
}
// 并行查询三个层级的订单 const result = await pageShopDealerOrder({
const [level1Result, level2Result, level3Result] = await Promise.all([ page,
// 一级分销商订单 limit: 10
pageShopDealerOrder({
page: 1,
limit: 100,
firstUserId: dealerUser.userId
}),
// 二级分销商订单
pageShopDealerOrder({
page: 1,
limit: 100,
secondUserId: dealerUser.userId
}),
// 三级分销商订单
pageShopDealerOrder({
page: 1,
limit: 100,
thirdUserId: dealerUser.userId
}) })
])
const allOrders: OrderWithDetails[] = [] if (result?.list) {
const stats = { const newOrders = result.list.map(order => ({
totalOrders: 0,
totalCommission: '0.00',
pendingCommission: '0.00',
level1: { orders: 0, commission: '0.00' },
level2: { orders: 0, commission: '0.00' },
level3: { orders: 0, commission: '0.00' }
}
// 处理一级分销订单
if (level1Result?.list) {
const level1Orders = level1Result.list.map(order => ({
...order, ...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)
if (level2Result?.list) { setCurrentPage(page)
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"> <View className="flex justify-between items-center mb-1">
<View> <Text className="text-sm text-gray-400">
<Text className="text-sm text-gray-600">
¥{order.orderPrice || '0.00'} ¥{order.orderPrice || '0.00'}
</Text> </Text>
<Text className="text-sm text-orange-500 font-semibold"> <Text className="text-sm text-orange-500 font-semibold">
¥{order.userCommission} ¥{order.userCommission}
</Text> </Text>
<Text className="text-xs text-gray-400">
¥{order.totalCommission}
</Text>
</View> </View>
<Text className="text-xs text-gray-400">
<View className="flex justify-between items-center">
<Text className="text-sm text-gray-400">
{order.customerName}
</Text>
<Text className="text-sm 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">
{/* 统计卡片 */}
<View className="bg-white p-4 mb-4">
{/* 总体统计 */}
<View className="grid grid-cols-3 gap-4 mb-4">
<View className="text-center">
<Text className="text-lg font-bold text-blue-500">{statistics.totalOrders}</Text>
<Text className="text-xs text-gray-500"></Text>
</View>
<View className="text-center">
<Text className="text-lg font-bold text-green-500">¥{statistics.totalCommission}</Text>
<Text className="text-xs text-gray-500"></Text>
</View>
<View className="text-center">
<Text className="text-lg font-bold text-orange-500">¥{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 <PullToRefresh
onRefresh={handleRefresh} onRefresh={handleRefresh}
disabled={refreshing}
pullingText="下拉刷新"
canReleaseText="释放刷新"
refreshingText="刷新中..."
completeText="刷新完成"
>
<ScrollView
scrollY
className="h-screen"
onScrollToLower={handleLoadMore}
lowerThreshold={50}
> >
<View className="p-4"> <View className="p-4">
{loading ? ( {loading && orders.length === 0 ? (
<View className="text-center py-8"> <View className="text-center py-8">
<Loading /> <Loading/>
<Text className="text-gray-500 mt-2">...</Text> <Text className="text-gray-500 mt-2">...</Text>
</View> </View>
) : getFilteredOrders('0').length > 0 ? ( ) : orders.length > 0 ? (
getFilteredOrders('0').map(renderOrderItem) <>
{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>
</ScrollView>
</PullToRefresh> </PullToRefresh>
</Tabs.TabPane>
<Tabs.TabPane title="一级分销" value="1">
<View className="p-4">
{getFilteredOrders('1').length > 0 ? (
getFilteredOrders('1').map(renderOrderItem)
) : (
<Empty description="暂无一级分销订单" />
)}
</View>
</Tabs.TabPane>
<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>
) )
} }

View File

@@ -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'}>
<View className={'my-2'}>
<Button <Button
type="primary" type="primary"
size="large" size="large"
block block
icon={<Download />} icon={<Download/>}
onClick={saveMiniProgramCode} onClick={saveMiniProgramCode}
disabled={!miniProgramCodeUrl || loading} disabled={!miniProgramCodeUrl || loading}
> >
</Button> </Button>
</View>
<View className={'my-2 bg-white'}>
<Button <Button
size="large" size="large"
block block
icon={<Copy />} icon={<Copy/>}
onClick={copyInviteInfo} onClick={copyInviteInfo}
disabled={!dealerUser?.userId || loading} disabled={!dealerUser?.userId || loading}
> >
</Button> </Button>
</View>
<View className={'my-2 bg-white'}>
<Button <Button
size="large" size="large"
block block
fill="outline" fill="outline"
icon={<Share />} icon={<Share/>}
onClick={shareMiniProgramCode} onClick={shareMiniProgramCode}
disabled={!dealerUser?.userId || loading} disabled={!dealerUser?.userId || loading}
> >
</Button> </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>
) )

View File

@@ -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) {
@@ -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">
{/* 团队统计卡片 */}
<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)} {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}>
<Tabs.TabPane title="团队总览" value="0">
{renderOverview()} {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>
) )
} }

View 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')
})
})

View 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

View File

@@ -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)
@@ -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,29 +418,32 @@ const DealerWithdraw: React.FC = () => {
</View> </View>
) )
const renderWithdrawRecords = () => ( const renderWithdrawRecords = () => {
console.log('渲染提现记录:', {loading, recordsCount: withdrawRecords.length, dealerUserId: dealerUser?.userId})
return (
<PullToRefresh <PullToRefresh
disabled={refreshing} disabled={refreshing}
onRefresh={handleRefresh} onRefresh={handleRefresh}
> >
<View className="p-4"> <View>
{loading ? ( {loading ? (
<View className="text-center py-8"> <View className="text-center py-8">
<Loading /> <Loading/>
<Text className="text-gray-500 mt-2">...</Text> <Text className="text-gray-500 mt-2">...</Text>
</View> </View>
) : withdrawRecords.length > 0 ? ( ) : withdrawRecords.length > 0 ? (
withdrawRecords.map(record => ( withdrawRecords.map(record => (
<View key={record.id} className="bg-white rounded-lg p-4 mb-3 shadow-sm"> <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 className="flex justify-between items-start mb-3">
<View> <Space>
<Text className="font-semibold text-gray-800 mb-1"> <Text className="font-semibold text-gray-800 mb-1">
¥{record.money} ¥{record.money}
</Text> </Text>
<Text className="text-sm text-gray-500"> <Text className="text-sm text-gray-500">
{record.accountDisplay} {record.accountDisplay}
</Text> </Text>
</View> </Space>
<Tag type={getStatusColor(record.applyStatus)}> <Tag type={getStatusColor(record.applyStatus)}>
{getStatusText(record.applyStatus)} {getStatusText(record.applyStatus)}
</Tag> </Tag>
@@ -391,16 +465,17 @@ const DealerWithdraw: React.FC = () => {
</View> </View>
)) ))
) : ( ) : (
<Empty description="暂无提现记录" /> <Empty description="暂无提现记录"/>
)} )}
</View> </View>
</PullToRefresh> </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>

View File

@@ -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, '客户列表');
setLoading(false); if(res?.list){
}, 500); // 为每个用户添加默认状态
}; const customersWithStatus: ShopDealerUser[] = res.list.map(user => ({
...user,
const getFilteredCustomers = () => { status: 'pending' // 默认状态为跟进中
if (activeTab === 'all') { }));
return customers; setList(customersWithStatus);
}
} catch (error) {
console.error('获取客户列表失败:', error);
Taro.showToast({
title: '获取客户列表失败',
icon: 'error'
});
} finally {
setLoading(false);
} }
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>

View 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)];
};