feat(dealer): 添加客户列表功能并优化邀请流程
- 新增客户列表页面,实现客户数据获取和筛选功能 - 添加客户状态管理工具函数 - 优化邀请流程,支持绑定推荐关系 - 调整提现功能,增加调试组件 - 修复申请经销商功能中的推荐人ID逻辑
This commit is contained in:
@@ -11,11 +11,12 @@ import {
|
||||
pageShopDealerApply,
|
||||
updateShopDealerApply
|
||||
} from "@/api/shop/shopDealerApply";
|
||||
import {getShopDealerUser} from "@/api/shop/shopDealerUser";
|
||||
|
||||
const AddUserAddress = () => {
|
||||
const {user} = useUser()
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [FormData, setFormData] = useState<ShopDealerApply>({})
|
||||
const [FormData, setFormData] = useState<ShopDealerApply>()
|
||||
const formRef = useRef<any>(null)
|
||||
const [isEditMode, setIsEditMode] = useState<boolean>(false)
|
||||
const [existingApply, setExistingApply] = useState<ShopDealerApply | null>(null)
|
||||
@@ -47,31 +48,34 @@ const AddUserAddress = () => {
|
||||
setExistingApply(res.list[0]);
|
||||
// 如果有记录,填充表单数据
|
||||
setFormData(res.list[0]);
|
||||
setLoading(false)
|
||||
} else {
|
||||
setFormData({})
|
||||
setIsEditMode(false);
|
||||
setExistingApply(null);
|
||||
setLoading(false)
|
||||
}
|
||||
} catch (error) {
|
||||
setLoading(true)
|
||||
console.error('查询申请记录失败:', error);
|
||||
setIsEditMode(false);
|
||||
setExistingApply(null);
|
||||
setFormData({})
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const submitSucceed = async (values: any) => {
|
||||
try {
|
||||
|
||||
// 准备提交的数据
|
||||
const submitData = {
|
||||
...values,
|
||||
realName: values.realName || user?.nickname,
|
||||
mobile: user?.phone,
|
||||
refereeId: values.refereeId,
|
||||
refereeId: values.refereeId || FormData?.refereeId,
|
||||
applyStatus: 10,
|
||||
auditTime: undefined
|
||||
};
|
||||
await getShopDealerUser(submitData.refereeId);
|
||||
|
||||
// 如果是编辑模式,添加现有申请的id
|
||||
if (isEditMode && existingApply?.applyId) {
|
||||
@@ -86,7 +90,7 @@ const AddUserAddress = () => {
|
||||
}
|
||||
|
||||
Taro.showToast({
|
||||
title: `${isEditMode ? '更新' : '提交'}成功`,
|
||||
title: `${isEditMode ? '提交' : '提交'}成功`,
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
@@ -95,9 +99,9 @@ const AddUserAddress = () => {
|
||||
}, 1000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error);
|
||||
Taro.showToast({
|
||||
title: `${isEditMode ? '更新' : '提交'}失败`,
|
||||
console.error('验证邀请人失败:', error);
|
||||
return Taro.showToast({
|
||||
title: '邀请人ID不存在',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
@@ -141,8 +145,8 @@ const AddUserAddress = () => {
|
||||
<Form.Item name="mobile" label="手机号" initialValue={user?.mobile} required>
|
||||
<Input placeholder="请输入手机号" disabled={true} maxLength={11}/>
|
||||
</Form.Item>
|
||||
<Form.Item name="refereeId" label="推荐人ID" initialValue={FormData?.refereeId} required>
|
||||
<Input placeholder="推荐人ID"/>
|
||||
<Form.Item name="refereeId" label="邀请人ID" initialValue={FormData?.refereeId} required>
|
||||
<Input placeholder="邀请人ID"/>
|
||||
</Form.Item>
|
||||
</CellGroup>
|
||||
</Form>
|
||||
@@ -153,29 +157,29 @@ const AddUserAddress = () => {
|
||||
title={'审核状态'}
|
||||
extra={
|
||||
<span style={{
|
||||
color: FormData.applyStatus === 20 ? '#52c41a' :
|
||||
FormData.applyStatus === 30 ? '#ff4d4f' : '#faad14'
|
||||
color: FormData?.applyStatus === 20 ? '#52c41a' :
|
||||
FormData?.applyStatus === 30 ? '#ff4d4f' : '#faad14'
|
||||
}}>
|
||||
{getApplyStatusText(FormData.applyStatus)}
|
||||
{getApplyStatusText(FormData?.applyStatus)}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
{FormData.applyStatus === 20 && (
|
||||
<Cell title={'审核时间'} extra={FormData.auditTime || '无'}/>
|
||||
{FormData?.applyStatus === 20 && (
|
||||
<Cell title={'审核时间'} extra={FormData?.auditTime || '无'}/>
|
||||
)}
|
||||
{FormData.applyStatus === 30 && (
|
||||
<Cell title={'驳回原因'} extra={FormData.rejectReason || '无'}/>
|
||||
{FormData?.applyStatus === 30 && (
|
||||
<Cell title={'驳回原因'} extra={FormData?.rejectReason || '无'}/>
|
||||
)}
|
||||
</CellGroup>
|
||||
)}
|
||||
|
||||
|
||||
{/* 底部浮动按钮 */}
|
||||
{(!isEditMode || FormData.applyStatus === 10 || FormData.applyStatus === 30) && (
|
||||
{(!isEditMode || FormData?.applyStatus === 10 || FormData?.applyStatus === 30) && (
|
||||
<FixedButton
|
||||
icon={<Edit/>}
|
||||
text={isEditMode ? '保存修改' : '提交申请'}
|
||||
disabled={FormData.applyStatus === 10}
|
||||
disabled={FormData?.applyStatus === 10}
|
||||
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,
|
||||
ArrowRight,
|
||||
Purse,
|
||||
People,
|
||||
Presentation
|
||||
People
|
||||
} from '@nutui/icons-react-taro'
|
||||
import {useDealerUser} from '@/hooks/useDealerUser'
|
||||
import { useThemeStyles } from '@/hooks/useTheme'
|
||||
@@ -132,28 +131,28 @@ const DealerIndex: React.FC = () => {
|
||||
<View className="mb-4">
|
||||
<Text className="font-semibold text-gray-800">佣金统计</Text>
|
||||
</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={{
|
||||
background: businessGradients.money.available
|
||||
}}>
|
||||
<Text className="text-2xl font-bold mb-1 text-white">
|
||||
¥{formatMoney(dealerUser.money)}
|
||||
<Text className="text-lg font-bold mb-1 text-white">
|
||||
{formatMoney(dealerUser.money)}
|
||||
</Text>
|
||||
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>可提现</Text>
|
||||
</View>
|
||||
<View className="text-center p-3 rounded-lg" style={{
|
||||
background: businessGradients.money.frozen
|
||||
}}>
|
||||
<Text className="text-2xl font-bold mb-1 text-white">
|
||||
¥{formatMoney(dealerUser.freezeMoney)}
|
||||
<Text className="text-lg font-bold mb-1 text-white">
|
||||
{formatMoney(dealerUser.freezeMoney)}
|
||||
</Text>
|
||||
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>冻结中</Text>
|
||||
</View>
|
||||
<View className="text-center p-3 rounded-lg" style={{
|
||||
background: businessGradients.money.total
|
||||
}}>
|
||||
<Text className="text-2xl font-bold mb-1 text-white">
|
||||
¥{formatMoney(dealerUser.totalMoney)}
|
||||
<Text className="text-lg font-bold mb-1 text-white">
|
||||
{formatMoney(dealerUser.totalMoney)}
|
||||
</Text>
|
||||
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>累计收益</Text>
|
||||
</View>
|
||||
@@ -244,45 +243,45 @@ const DealerIndex: React.FC = () => {
|
||||
</Grid>
|
||||
|
||||
{/* 第二行功能 */}
|
||||
<Grid
|
||||
columns={4}
|
||||
className="no-border-grid mt-4"
|
||||
style={{
|
||||
'--nutui-grid-border-color': 'transparent',
|
||||
'--nutui-grid-item-border-width': '0px',
|
||||
border: 'none'
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
<Grid.Item text={'邀请统计'} onClick={() => navigateToPage('/dealer/invite-stats/index')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-indigo-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Presentation color="#6366f1" size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
{/*<Grid*/}
|
||||
{/* columns={4}*/}
|
||||
{/* className="no-border-grid mt-4"*/}
|
||||
{/* style={{*/}
|
||||
{/* '--nutui-grid-border-color': 'transparent',*/}
|
||||
{/* '--nutui-grid-item-border-width': '0px',*/}
|
||||
{/* border: 'none'*/}
|
||||
{/* } as React.CSSProperties}*/}
|
||||
{/*>*/}
|
||||
{/* <Grid.Item text={'邀请统计'} onClick={() => navigateToPage('/dealer/invite-stats/index')}>*/}
|
||||
{/* <View className="text-center">*/}
|
||||
{/* <View className="w-12 h-12 bg-indigo-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
|
||||
{/* <Presentation color="#6366f1" size="20"/>*/}
|
||||
{/* </View>*/}
|
||||
{/* </View>*/}
|
||||
{/* </Grid.Item>*/}
|
||||
|
||||
{/* 预留其他功能位置 */}
|
||||
<Grid.Item text={''}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
{/* /!* 预留其他功能位置 *!/*/}
|
||||
{/* <Grid.Item text={''}>*/}
|
||||
{/* <View className="text-center">*/}
|
||||
{/* <View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
|
||||
{/* </View>*/}
|
||||
{/* </View>*/}
|
||||
{/* </Grid.Item>*/}
|
||||
|
||||
<Grid.Item text={''}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
{/* <Grid.Item text={''}>*/}
|
||||
{/* <View className="text-center">*/}
|
||||
{/* <View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
|
||||
{/* </View>*/}
|
||||
{/* </View>*/}
|
||||
{/* </Grid.Item>*/}
|
||||
|
||||
<Grid.Item text={''}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
</Grid>
|
||||
{/* <Grid.Item text={''}>*/}
|
||||
{/* <View className="text-center">*/}
|
||||
{/* <View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
|
||||
{/* </View>*/}
|
||||
{/* </View>*/}
|
||||
{/* </Grid.Item>*/}
|
||||
{/*</Grid>*/}
|
||||
</ConfigProvider>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -1,161 +1,63 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Empty, Tabs, Tag, PullToRefresh, Loading } from '@nutui/nutui-react-taro'
|
||||
import React, {useState, useEffect, useCallback} from 'react'
|
||||
import {View, Text, ScrollView} from '@tarojs/components'
|
||||
import {Empty, Tag, PullToRefresh, Loading} from '@nutui/nutui-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { pageShopDealerOrder } from '@/api/shop/shopDealerOrder'
|
||||
import { useDealerUser } from '@/hooks/useDealerUser'
|
||||
import type { ShopDealerOrder } from '@/api/shop/shopDealerOrder/model'
|
||||
import {pageShopDealerOrder} from '@/api/shop/shopDealerOrder'
|
||||
import {useDealerUser} from '@/hooks/useDealerUser'
|
||||
import type {ShopDealerOrder} from '@/api/shop/shopDealerOrder/model'
|
||||
|
||||
interface OrderWithDetails extends ShopDealerOrder {
|
||||
orderNo?: string
|
||||
customerName?: string
|
||||
totalCommission?: string
|
||||
// 当前用户在此订单中的层级和佣金
|
||||
userLevel?: 1 | 2 | 3
|
||||
userCommission?: string
|
||||
}
|
||||
|
||||
const DealerOrders: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState<string>('0')
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [refreshing, setRefreshing] = useState<boolean>(false)
|
||||
const [loadingMore, setLoadingMore] = useState<boolean>(false)
|
||||
const [orders, setOrders] = useState<OrderWithDetails[]>([])
|
||||
const [statistics, setStatistics] = useState({
|
||||
totalOrders: 0,
|
||||
totalCommission: '0.00',
|
||||
pendingCommission: '0.00',
|
||||
// 分层统计
|
||||
level1: { orders: 0, commission: '0.00' },
|
||||
level2: { orders: 0, commission: '0.00' },
|
||||
level3: { orders: 0, commission: '0.00' }
|
||||
})
|
||||
const [currentPage, setCurrentPage] = useState<number>(1)
|
||||
const [hasMore, setHasMore] = useState<boolean>(true)
|
||||
|
||||
const { dealerUser } = useDealerUser()
|
||||
const {dealerUser} = useDealerUser()
|
||||
|
||||
// 获取订单数据 - 查询当前用户作为各层级分销商的所有订单
|
||||
const fetchOrders = useCallback(async () => {
|
||||
// 获取订单数据
|
||||
const fetchOrders = useCallback(async (page: number = 1, isRefresh: boolean = false) => {
|
||||
if (!dealerUser?.userId) return
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
|
||||
// 并行查询三个层级的订单
|
||||
const [level1Result, level2Result, level3Result] = await Promise.all([
|
||||
// 一级分销商订单
|
||||
pageShopDealerOrder({
|
||||
page: 1,
|
||||
limit: 100,
|
||||
firstUserId: dealerUser.userId
|
||||
}),
|
||||
// 二级分销商订单
|
||||
pageShopDealerOrder({
|
||||
page: 1,
|
||||
limit: 100,
|
||||
secondUserId: dealerUser.userId
|
||||
}),
|
||||
// 三级分销商订单
|
||||
pageShopDealerOrder({
|
||||
page: 1,
|
||||
limit: 100,
|
||||
thirdUserId: dealerUser.userId
|
||||
})
|
||||
])
|
||||
|
||||
const allOrders: OrderWithDetails[] = []
|
||||
const stats = {
|
||||
totalOrders: 0,
|
||||
totalCommission: '0.00',
|
||||
pendingCommission: '0.00',
|
||||
level1: { orders: 0, commission: '0.00' },
|
||||
level2: { orders: 0, commission: '0.00' },
|
||||
level3: { orders: 0, commission: '0.00' }
|
||||
if (isRefresh) {
|
||||
setRefreshing(true)
|
||||
} else if (page === 1) {
|
||||
setLoading(true)
|
||||
} else {
|
||||
setLoadingMore(true)
|
||||
}
|
||||
|
||||
// 处理一级分销订单
|
||||
if (level1Result?.list) {
|
||||
const level1Orders = level1Result.list.map(order => ({
|
||||
const result = await pageShopDealerOrder({
|
||||
page,
|
||||
limit: 10
|
||||
})
|
||||
|
||||
if (result?.list) {
|
||||
const newOrders = result.list.map(order => ({
|
||||
...order,
|
||||
orderNo: `DD${order.orderId}`,
|
||||
orderNo: `${order.orderId}`,
|
||||
customerName: `用户${order.userId}`,
|
||||
userLevel: 1 as const,
|
||||
userCommission: order.firstMoney || '0.00',
|
||||
totalCommission: (
|
||||
parseFloat(order.firstMoney || '0') +
|
||||
parseFloat(order.secondMoney || '0') +
|
||||
parseFloat(order.thirdMoney || '0')
|
||||
).toFixed(2)
|
||||
userCommission: order.firstMoney || '0.00'
|
||||
}))
|
||||
|
||||
allOrders.push(...level1Orders)
|
||||
stats.level1.orders = level1Orders.length
|
||||
stats.level1.commission = level1Orders.reduce((sum, order) =>
|
||||
sum + parseFloat(order.userCommission || '0'), 0
|
||||
).toFixed(2)
|
||||
if (page === 1) {
|
||||
setOrders(newOrders)
|
||||
} else {
|
||||
setOrders(prev => [...prev, ...newOrders])
|
||||
}
|
||||
|
||||
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) {
|
||||
console.error('获取分销订单失败:', error)
|
||||
Taro.showToast({
|
||||
@@ -164,18 +66,27 @@ const DealerOrders: React.FC = () => {
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setRefreshing(false)
|
||||
setLoadingMore(false)
|
||||
}
|
||||
}, [dealerUser?.userId])
|
||||
|
||||
// 刷新数据
|
||||
// 下拉刷新
|
||||
const handleRefresh = async () => {
|
||||
await fetchOrders()
|
||||
await fetchOrders(1, true)
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
const handleLoadMore = async () => {
|
||||
if (!loadingMore && hasMore) {
|
||||
await fetchOrders(currentPage + 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化加载数据
|
||||
useEffect(() => {
|
||||
if (dealerUser?.userId) {
|
||||
fetchOrders().then()
|
||||
fetchOrders(1)
|
||||
}
|
||||
}, [fetchOrders])
|
||||
|
||||
@@ -193,198 +104,87 @@ const DealerOrders: React.FC = () => {
|
||||
|
||||
const renderOrderItem = (order: OrderWithDetails) => (
|
||||
<View key={order.id} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
|
||||
<View className="flex justify-between items-start mb-3">
|
||||
<View>
|
||||
<Text className="font-semibold text-gray-800 mb-1">
|
||||
订单号:{order.orderNo}
|
||||
</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>
|
||||
<View className="flex justify-between items-start mb-1">
|
||||
<Text className="font-semibold text-gray-800">
|
||||
订单号:{order.orderNo}
|
||||
</Text>
|
||||
<Tag type={getStatusColor(order.isSettled, order.isInvalid)}>
|
||||
{getStatusText(order.isSettled, order.isInvalid)}
|
||||
</Tag>
|
||||
</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>
|
||||
<Text className="text-sm text-gray-600">
|
||||
订单金额:¥{order.orderPrice || '0.00'}
|
||||
</Text>
|
||||
<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">
|
||||
<Text className="text-sm text-gray-400">
|
||||
客户:{order.customerName}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-400">
|
||||
{order.createTime}
|
||||
</Text>
|
||||
</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) {
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
||||
<Loading />
|
||||
<Loading/>
|
||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen">
|
||||
{/* 统计卡片 */}
|
||||
<View className="bg-white p-4 mb-4">
|
||||
{/* 总体统计 */}
|
||||
<View className="grid grid-cols-3 gap-4 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
|
||||
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="min-h-screen bg-gray-50">
|
||||
<PullToRefresh
|
||||
onRefresh={handleRefresh}
|
||||
disabled={refreshing}
|
||||
pullingText="下拉刷新"
|
||||
canReleaseText="释放刷新"
|
||||
refreshingText="刷新中..."
|
||||
completeText="刷新完成"
|
||||
>
|
||||
<ScrollView
|
||||
scrollY
|
||||
className="h-screen"
|
||||
onScrollToLower={handleLoadMore}
|
||||
lowerThreshold={50}
|
||||
>
|
||||
<View className="p-4">
|
||||
{getFilteredOrders('1').length > 0 ? (
|
||||
getFilteredOrders('1').map(renderOrderItem)
|
||||
{loading && orders.length === 0 ? (
|
||||
<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>
|
||||
</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>
|
||||
</ScrollView>
|
||||
</PullToRefresh>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,64 +1,69 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { View, Text, Image } from '@tarojs/components'
|
||||
import { Button, Loading } from '@nutui/nutui-react-taro'
|
||||
import { Share, Download, Copy, QrCode } from '@nutui/icons-react-taro'
|
||||
import React, {useState, useEffect} from 'react'
|
||||
import {View, Text, Image} from '@tarojs/components'
|
||||
import {Button, Loading} from '@nutui/nutui-react-taro'
|
||||
import {Share, Download, Copy, QrCode} from '@nutui/icons-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useDealerUser } from '@/hooks/useDealerUser'
|
||||
import { generateInviteCode, getInviteStats } from '@/api/invite'
|
||||
import type { InviteStats } from '@/api/invite'
|
||||
import { businessGradients } from '@/styles/gradients'
|
||||
import {useDealerUser} from '@/hooks/useDealerUser'
|
||||
import {generateInviteCode} from '@/api/invite'
|
||||
// import type {InviteStats} from '@/api/invite'
|
||||
import {businessGradients} from '@/styles/gradients'
|
||||
|
||||
const DealerQrcode: React.FC = () => {
|
||||
const [miniProgramCodeUrl, setMiniProgramCodeUrl] = useState<string>('')
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [inviteStats, setInviteStats] = useState<InviteStats | null>(null)
|
||||
const [statsLoading, setStatsLoading] = useState<boolean>(false)
|
||||
const { dealerUser } = useDealerUser()
|
||||
// const [inviteStats, setInviteStats] = useState<InviteStats | null>(null)
|
||||
// const [statsLoading, setStatsLoading] = useState<boolean>(false)
|
||||
const {dealerUser} = useDealerUser()
|
||||
|
||||
// 生成小程序码
|
||||
const generateMiniProgramCode = async () => {
|
||||
if (!dealerUser?.userId) return
|
||||
if (!dealerUser?.userId) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
|
||||
// 生成邀请小程序码
|
||||
const codeUrl = await generateInviteCode(dealerUser.userId, 'qrcode')
|
||||
const codeUrl = await generateInviteCode(dealerUser.userId)
|
||||
|
||||
if (codeUrl) {
|
||||
setMiniProgramCodeUrl(codeUrl)
|
||||
} else {
|
||||
throw new Error('返回的小程序码URL为空')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('生成小程序码失败:', error)
|
||||
} catch (error: any) {
|
||||
Taro.showToast({
|
||||
title: '生成小程序码失败',
|
||||
title: error.message || '生成小程序码失败',
|
||||
icon: 'error'
|
||||
})
|
||||
// 清空之前的二维码
|
||||
setMiniProgramCodeUrl('')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取邀请统计数据
|
||||
const fetchInviteStats = async () => {
|
||||
if (!dealerUser?.userId) return
|
||||
|
||||
try {
|
||||
setStatsLoading(true)
|
||||
const stats = await getInviteStats(dealerUser.userId)
|
||||
stats && setInviteStats(stats)
|
||||
} catch (error) {
|
||||
console.error('获取邀请统计失败:', error)
|
||||
} finally {
|
||||
setStatsLoading(false)
|
||||
}
|
||||
}
|
||||
// const fetchInviteStats = async () => {
|
||||
// if (!dealerUser?.userId) return
|
||||
//
|
||||
// try {
|
||||
// setStatsLoading(true)
|
||||
// const stats = await getInviteStats(dealerUser.userId)
|
||||
// stats && setInviteStats(stats)
|
||||
// } catch (error) {
|
||||
// // 静默处理错误,不影响用户体验
|
||||
// } finally {
|
||||
// setStatsLoading(false)
|
||||
// }
|
||||
// }
|
||||
|
||||
// 初始化生成小程序码和获取统计数据
|
||||
useEffect(() => {
|
||||
if (dealerUser?.userId) {
|
||||
generateMiniProgramCode()
|
||||
fetchInviteStats()
|
||||
// fetchInviteStats()
|
||||
}
|
||||
}, [dealerUser?.userId])
|
||||
|
||||
@@ -121,9 +126,9 @@ const DealerQrcode: React.FC = () => {
|
||||
|
||||
const inviteText = `🎉 邀请您加入我的团队!
|
||||
|
||||
扫描小程序码或搜索"网宿小店"小程序,即可享受优质商品和服务!
|
||||
扫描小程序码或搜索"时里院子市集"小程序,即可享受优质商品和服务!
|
||||
|
||||
💰 成为我的下级分销商,一起赚取丰厚佣金
|
||||
💰 成为我的团队成员,一起赚取丰厚佣金
|
||||
🎁 新用户专享优惠等你来拿
|
||||
|
||||
邀请码:${dealerUser.userId}
|
||||
@@ -153,14 +158,14 @@ const DealerQrcode: React.FC = () => {
|
||||
// 小程序分享
|
||||
Taro.showShareMenu({
|
||||
withShareTicket: true,
|
||||
showShareItems: ['shareAppMessage', 'shareTimeline']
|
||||
showShareItems: ['shareAppMessage']
|
||||
})
|
||||
}
|
||||
|
||||
if (!dealerUser) {
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
||||
<Loading />
|
||||
<Loading/>
|
||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||
</View>
|
||||
)
|
||||
@@ -179,7 +184,7 @@ const DealerQrcode: React.FC = () => {
|
||||
right: '-16px'
|
||||
}}></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-white text-opacity-80">
|
||||
分享小程序码邀请好友,获得丰厚佣金奖励
|
||||
@@ -193,7 +198,7 @@ const DealerQrcode: React.FC = () => {
|
||||
<View className="text-center">
|
||||
{loading ? (
|
||||
<View className="w-48 h-48 mx-auto mb-4 flex items-center justify-center bg-gray-50 rounded-xl">
|
||||
<Loading />
|
||||
<Loading/>
|
||||
<Text className="text-gray-500 mt-2">生成中...</Text>
|
||||
</View>
|
||||
) : miniProgramCodeUrl ? (
|
||||
@@ -202,6 +207,20 @@ const DealerQrcode: React.FC = () => {
|
||||
src={miniProgramCodeUrl}
|
||||
className="w-full h-full"
|
||||
mode="aspectFit"
|
||||
onError={() => {
|
||||
Taro.showModal({
|
||||
title: '二维码加载失败',
|
||||
content: '请检查网络连接或联系管理员',
|
||||
showCancel: true,
|
||||
confirmText: '重新生成',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
generateMiniProgramCode();
|
||||
}
|
||||
}
|
||||
});
|
||||
}}
|
||||
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
@@ -219,58 +238,64 @@ const DealerQrcode: React.FC = () => {
|
||||
</View>
|
||||
)}
|
||||
|
||||
<Text className="text-lg font-semibold text-gray-800 mb-2">
|
||||
<View className="text-lg font-semibold text-gray-800 mb-2">
|
||||
扫码加入我的团队
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-500 mb-6">
|
||||
</View>
|
||||
<View className="text-sm text-gray-500 mb-4">
|
||||
好友扫描小程序码即可直接进入小程序并建立邀请关系
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<View className="space-y-3">
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
icon={<Download />}
|
||||
onClick={saveMiniProgramCode}
|
||||
disabled={!miniProgramCodeUrl || loading}
|
||||
>
|
||||
保存小程序码到相册
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="large"
|
||||
block
|
||||
icon={<Copy />}
|
||||
onClick={copyInviteInfo}
|
||||
disabled={!dealerUser?.userId || loading}
|
||||
>
|
||||
复制邀请信息
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="large"
|
||||
block
|
||||
fill="outline"
|
||||
icon={<Share />}
|
||||
onClick={shareMiniProgramCode}
|
||||
disabled={!dealerUser?.userId || loading}
|
||||
>
|
||||
分享给好友
|
||||
</Button>
|
||||
<View className={'gap-2'}>
|
||||
<View className={'my-2'}>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
icon={<Download/>}
|
||||
onClick={saveMiniProgramCode}
|
||||
disabled={!miniProgramCodeUrl || loading}
|
||||
>
|
||||
保存小程序码到相册
|
||||
</Button>
|
||||
</View>
|
||||
<View className={'my-2 bg-white'}>
|
||||
<Button
|
||||
size="large"
|
||||
block
|
||||
icon={<Copy/>}
|
||||
onClick={copyInviteInfo}
|
||||
disabled={!dealerUser?.userId || loading}
|
||||
>
|
||||
复制邀请信息
|
||||
</Button>
|
||||
</View>
|
||||
<View className={'my-2 bg-white'}>
|
||||
<Button
|
||||
size="large"
|
||||
block
|
||||
fill="outline"
|
||||
icon={<Share/>}
|
||||
onClick={shareMiniProgramCode}
|
||||
disabled={!dealerUser?.userId || loading}
|
||||
>
|
||||
分享给好友
|
||||
</Button>
|
||||
</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>
|
||||
<View className="space-y-2">
|
||||
<View className="flex items-start">
|
||||
<View className="w-2 h-2 bg-blue-500 rounded-full mt-2 mr-3 flex-shrink-0"></View>
|
||||
<Text className="text-sm text-gray-600">
|
||||
好友通过您的二维码或链接注册成为您的下级分销商
|
||||
好友通过您的二维码或链接注册成为您的团队成员
|
||||
</Text>
|
||||
</View>
|
||||
<View className="flex items-start">
|
||||
@@ -289,82 +314,82 @@ const DealerQrcode: React.FC = () => {
|
||||
</View>
|
||||
|
||||
{/* 邀请统计数据 */}
|
||||
<View className="bg-white rounded-2xl p-4 mt-4 mb-6">
|
||||
<Text className="font-semibold text-gray-800 mb-3">我的邀请数据</Text>
|
||||
{statsLoading ? (
|
||||
<View className="flex items-center justify-center py-8">
|
||||
<Loading />
|
||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||
</View>
|
||||
) : inviteStats ? (
|
||||
<View className="space-y-4">
|
||||
<View className="grid grid-cols-2 gap-4">
|
||||
<View className="text-center">
|
||||
<Text className="text-2xl font-bold text-blue-500">
|
||||
{inviteStats.totalInvites || 0}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-500">总邀请数</Text>
|
||||
</View>
|
||||
<View className="text-center">
|
||||
<Text className="text-2xl font-bold text-green-500">
|
||||
{inviteStats.successfulRegistrations || 0}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-500">成功注册</Text>
|
||||
</View>
|
||||
</View>
|
||||
{/*<View className="bg-white rounded-2xl p-4 mt-4 mb-6">*/}
|
||||
{/* <Text className="font-semibold text-gray-800 mb-3">我的邀请数据</Text>*/}
|
||||
{/* {statsLoading ? (*/}
|
||||
{/* <View className="flex items-center justify-center py-8">*/}
|
||||
{/* <Loading/>*/}
|
||||
{/* <Text className="text-gray-500 mt-2">加载中...</Text>*/}
|
||||
{/* </View>*/}
|
||||
{/* ) : inviteStats ? (*/}
|
||||
{/* <View className="space-y-4">*/}
|
||||
{/* <View className="grid grid-cols-2 gap-4">*/}
|
||||
{/* <View className="text-center">*/}
|
||||
{/* <Text className="text-2xl font-bold text-blue-500">*/}
|
||||
{/* {inviteStats.totalInvites || 0}*/}
|
||||
{/* </Text>*/}
|
||||
{/* <Text className="text-sm text-gray-500">总邀请数</Text>*/}
|
||||
{/* </View>*/}
|
||||
{/* <View className="text-center">*/}
|
||||
{/* <Text className="text-2xl font-bold text-green-500">*/}
|
||||
{/* {inviteStats.successfulRegistrations || 0}*/}
|
||||
{/* </Text>*/}
|
||||
{/* <Text className="text-sm text-gray-500">成功注册</Text>*/}
|
||||
{/* </View>*/}
|
||||
{/* </View>*/}
|
||||
|
||||
<View className="grid grid-cols-2 gap-4">
|
||||
<View className="text-center">
|
||||
<Text className="text-2xl font-bold text-purple-500">
|
||||
{inviteStats.conversionRate ? `${(inviteStats.conversionRate * 100).toFixed(1)}%` : '0%'}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-500">转化率</Text>
|
||||
</View>
|
||||
<View className="text-center">
|
||||
<Text className="text-2xl font-bold text-orange-500">
|
||||
{inviteStats.todayInvites || 0}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-500">今日邀请</Text>
|
||||
</View>
|
||||
</View>
|
||||
{/* <View className="grid grid-cols-2 gap-4">*/}
|
||||
{/* <View className="text-center">*/}
|
||||
{/* <Text className="text-2xl font-bold text-purple-500">*/}
|
||||
{/* {inviteStats.conversionRate ? `${(inviteStats.conversionRate * 100).toFixed(1)}%` : '0%'}*/}
|
||||
{/* </Text>*/}
|
||||
{/* <Text className="text-sm text-gray-500">转化率</Text>*/}
|
||||
{/* </View>*/}
|
||||
{/* <View className="text-center">*/}
|
||||
{/* <Text className="text-2xl font-bold text-orange-500">*/}
|
||||
{/* {inviteStats.todayInvites || 0}*/}
|
||||
{/* </Text>*/}
|
||||
{/* <Text className="text-sm text-gray-500">今日邀请</Text>*/}
|
||||
{/* </View>*/}
|
||||
{/* </View>*/}
|
||||
|
||||
{/* 邀请来源统计 */}
|
||||
{inviteStats.sourceStats && inviteStats.sourceStats.length > 0 && (
|
||||
<View className="mt-4">
|
||||
<Text className="text-sm font-medium text-gray-700 mb-2">邀请来源分布</Text>
|
||||
<View className="space-y-2">
|
||||
{inviteStats.sourceStats.map((source, index) => (
|
||||
<View key={index} className="flex items-center justify-between py-2 px-3 bg-gray-50 rounded-lg">
|
||||
<View className="flex items-center">
|
||||
<View className="w-3 h-3 rounded-full bg-blue-500 mr-2"></View>
|
||||
<Text className="text-sm text-gray-700">{source.source}</Text>
|
||||
</View>
|
||||
<View className="text-right">
|
||||
<Text className="text-sm font-medium text-gray-800">{source.count}</Text>
|
||||
<Text className="text-xs text-gray-500">
|
||||
{source.conversionRate ? `${(source.conversionRate * 100).toFixed(1)}%` : '0%'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
) : (
|
||||
<View className="text-center py-8">
|
||||
<Text className="text-gray-500">暂无邀请数据</Text>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
className="mt-2"
|
||||
onClick={fetchInviteStats}
|
||||
>
|
||||
刷新数据
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
{/* /!* 邀请来源统计 *!/*/}
|
||||
{/* {inviteStats.sourceStats && inviteStats.sourceStats.length > 0 && (*/}
|
||||
{/* <View className="mt-4">*/}
|
||||
{/* <Text className="text-sm font-medium text-gray-700 mb-2">邀请来源分布</Text>*/}
|
||||
{/* <View className="space-y-2">*/}
|
||||
{/* {inviteStats.sourceStats.map((source, index) => (*/}
|
||||
{/* <View key={index} className="flex items-center justify-between py-2 px-3 bg-gray-50 rounded-lg">*/}
|
||||
{/* <View className="flex items-center">*/}
|
||||
{/* <View className="w-3 h-3 rounded-full bg-blue-500 mr-2"></View>*/}
|
||||
{/* <Text className="text-sm text-gray-700">{source.source}</Text>*/}
|
||||
{/* </View>*/}
|
||||
{/* <View className="text-right">*/}
|
||||
{/* <Text className="text-sm font-medium text-gray-800">{source.count}</Text>*/}
|
||||
{/* <Text className="text-xs text-gray-500">*/}
|
||||
{/* {source.conversionRate ? `${(source.conversionRate * 100).toFixed(1)}%` : '0%'}*/}
|
||||
{/* </Text>*/}
|
||||
{/* </View>*/}
|
||||
{/* </View>*/}
|
||||
{/* ))}*/}
|
||||
{/* </View>*/}
|
||||
{/* </View>*/}
|
||||
{/* )}*/}
|
||||
{/* </View>*/}
|
||||
{/* ) : (*/}
|
||||
{/* <View className="text-center py-8">*/}
|
||||
{/* <View className="text-gray-500">暂无邀请数据</View>*/}
|
||||
{/* <Button*/}
|
||||
{/* size="small"*/}
|
||||
{/* type="primary"*/}
|
||||
{/* className="mt-2"*/}
|
||||
{/* onClick={fetchInviteStats}*/}
|
||||
{/* >*/}
|
||||
{/* 刷新数据*/}
|
||||
{/* </Button>*/}
|
||||
{/* </View>*/}
|
||||
{/* )}*/}
|
||||
{/*</View>*/}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Empty, Tabs, Avatar, Tag, Progress, Loading, PullToRefresh } from '@nutui/nutui-react-taro'
|
||||
import { User, Star, StarFill } from '@nutui/icons-react-taro'
|
||||
import React, {useState, useEffect, useCallback} from 'react'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import {Space, Avatar, Loading} from '@nutui/nutui-react-taro'
|
||||
import {User} from '@nutui/icons-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useDealerUser } from '@/hooks/useDealerUser'
|
||||
import { listShopDealerReferee } from '@/api/shop/shopDealerReferee'
|
||||
import { pageShopDealerOrder } from '@/api/shop/shopDealerOrder'
|
||||
import type { ShopDealerReferee } from '@/api/shop/shopDealerReferee/model'
|
||||
import {useDealerUser} from '@/hooks/useDealerUser'
|
||||
import {listShopDealerReferee} from '@/api/shop/shopDealerReferee'
|
||||
import {pageShopDealerOrder} from '@/api/shop/shopDealerOrder'
|
||||
import type {ShopDealerReferee} from '@/api/shop/shopDealerReferee/model'
|
||||
|
||||
interface TeamMemberWithStats extends ShopDealerReferee {
|
||||
name?: string
|
||||
@@ -19,30 +19,19 @@ interface TeamMemberWithStats extends ShopDealerReferee {
|
||||
}
|
||||
|
||||
const DealerTeam: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState('0')
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [refreshing, setRefreshing] = useState<boolean>(false)
|
||||
const [teamMembers, setTeamMembers] = useState<TeamMemberWithStats[]>([])
|
||||
const [teamStats, setTeamStats] = useState({
|
||||
total: 0,
|
||||
firstLevel: 0,
|
||||
secondLevel: 0,
|
||||
thirdLevel: 0,
|
||||
monthlyCommission: '0.00'
|
||||
})
|
||||
|
||||
const { dealerUser } = useDealerUser()
|
||||
const {dealerUser} = useDealerUser()
|
||||
const [dealerId, setDealerId] = useState<number>()
|
||||
|
||||
// 获取团队数据
|
||||
const fetchTeamData = useCallback(async () => {
|
||||
if (!dealerUser?.userId) return
|
||||
if (!dealerUser?.userId && !dealerId) return
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
|
||||
console.log(dealerId, 'dealerId>>>>>>>>>')
|
||||
// 获取团队成员关系
|
||||
const refereeResult = await listShopDealerReferee({
|
||||
dealerId: dealerUser.userId
|
||||
dealerId: dealerId ? dealerId : dealerUser?.userId
|
||||
})
|
||||
|
||||
if (refereeResult) {
|
||||
@@ -73,8 +62,8 @@ const DealerTeam: React.FC = () => {
|
||||
const orderCount = orders.length
|
||||
const commission = orders.reduce((sum, order) => {
|
||||
const levelCommission = member.level === 1 ? order.firstMoney :
|
||||
member.level === 2 ? order.secondMoney :
|
||||
order.thirdMoney
|
||||
member.level === 2 ? order.secondMoney :
|
||||
order.thirdMoney
|
||||
return sum + parseFloat(levelCommission || '0')
|
||||
}, 0).toFixed(2)
|
||||
|
||||
@@ -102,18 +91,6 @@ const DealerTeam: React.FC = () => {
|
||||
|
||||
setTeamMembers(memberStats)
|
||||
|
||||
// 计算统计数据
|
||||
const stats = {
|
||||
total: memberStats.length,
|
||||
firstLevel: memberStats.filter(m => m.level === 1).length,
|
||||
secondLevel: memberStats.filter(m => m.level === 2).length,
|
||||
thirdLevel: memberStats.filter(m => m.level === 3).length,
|
||||
monthlyCommission: memberStats.reduce((sum, member) =>
|
||||
sum + parseFloat(member.commission || '0'), 0
|
||||
).toFixed(2)
|
||||
}
|
||||
|
||||
setTeamStats(stats)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取团队数据失败:', error)
|
||||
@@ -121,50 +98,28 @@ const DealerTeam: React.FC = () => {
|
||||
title: '获取团队数据失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [dealerUser?.userId])
|
||||
}, [dealerUser?.userId, dealerId])
|
||||
|
||||
// 刷新数据
|
||||
const handleRefresh = async () => {
|
||||
setRefreshing(true)
|
||||
await fetchTeamData()
|
||||
setRefreshing(false)
|
||||
const getNextUser = (item: TeamMemberWithStats) => {
|
||||
console.log('点击用户:', item.userId, item.name)
|
||||
setDealerId(item.userId)
|
||||
}
|
||||
|
||||
// 初始化加载数据
|
||||
// 监听数据变化,获取团队数据
|
||||
useEffect(() => {
|
||||
if (dealerUser?.userId) {
|
||||
if (dealerUser?.userId || dealerId) {
|
||||
fetchTeamData().then()
|
||||
}
|
||||
}, [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) => (
|
||||
<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">
|
||||
<Avatar
|
||||
size="40"
|
||||
src={member.avatar}
|
||||
icon={<User />}
|
||||
icon={<User/>}
|
||||
className="mr-3"
|
||||
/>
|
||||
<View className="flex-1">
|
||||
@@ -172,194 +127,65 @@ const DealerTeam: React.FC = () => {
|
||||
<Text className="font-semibold text-gray-800 mr-2">
|
||||
{member.name}
|
||||
</Text>
|
||||
{getLevelIcon(Number(member.level))}
|
||||
<Text className="text-xs text-gray-500 ml-1">
|
||||
{member.level}级
|
||||
</Text>
|
||||
{/*{getLevelIcon(Number(member.level))}*/}
|
||||
{/*<Text className="text-xs text-gray-500 ml-1">*/}
|
||||
{/* {member.level}级*/}
|
||||
{/*</Text>*/}
|
||||
</View>
|
||||
<Text className="text-xs text-gray-500">
|
||||
加入时间:{member.joinTime}
|
||||
</Text>
|
||||
</View>
|
||||
<View className="text-right">
|
||||
<Tag
|
||||
type={member.status === 'active' ? 'success' : 'default'}
|
||||
>
|
||||
{member.status === 'active' ? '活跃' : '沉默'}
|
||||
</Tag>
|
||||
</View>
|
||||
{/*<View className="text-right">*/}
|
||||
{/* <Tag*/}
|
||||
{/* type={member.status === 'active' ? 'success' : 'default'}*/}
|
||||
{/* >*/}
|
||||
{/* {member.status === 'active' ? '活跃' : '沉默'}*/}
|
||||
{/* </Tag>*/}
|
||||
{/*</View>*/}
|
||||
</View>
|
||||
|
||||
<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">
|
||||
{member.orderCount}
|
||||
</Text>
|
||||
<Text className="text-xs text-gray-500">订单数</Text>
|
||||
</View>
|
||||
<View>
|
||||
</Space>
|
||||
<Space>
|
||||
<Text className="text-xs text-gray-500">贡献佣金</Text>
|
||||
<Text className="text-sm font-semibold text-green-600">
|
||||
¥{member.commission}
|
||||
</Text>
|
||||
<Text className="text-xs text-gray-500">贡献佣金</Text>
|
||||
</View>
|
||||
<View>
|
||||
</Space>
|
||||
<Space>
|
||||
<Text className="text-xs text-gray-500">团队成员</Text>
|
||||
<Text className="text-sm font-semibold text-purple-600">
|
||||
{member.subMembers}
|
||||
</Text>
|
||||
<Text className="text-xs text-gray-500">下级成员</Text>
|
||||
</View>
|
||||
</Space>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
|
||||
const renderOverview = () => (
|
||||
<View className="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)}
|
||||
</View>
|
||||
<View className="rounded-xl p-4">
|
||||
{teamMembers.slice(0, 3).map(renderMemberItem)}
|
||||
</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) {
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
||||
<Loading />
|
||||
<Loading/>
|
||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen">
|
||||
<Tabs value={activeTab} onChange={() => setActiveTab}>
|
||||
<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 className="min-h-screen">
|
||||
{renderOverview()}
|
||||
</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 { View, Text } from '@tarojs/components'
|
||||
import React, {useState, useRef, useEffect, useCallback} from 'react'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import {
|
||||
Cell,
|
||||
Space,
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
@@ -13,19 +14,19 @@ import {
|
||||
Loading,
|
||||
PullToRefresh
|
||||
} from '@nutui/nutui-react-taro'
|
||||
import { Wallet } from '@nutui/icons-react-taro'
|
||||
import { businessGradients } from '@/styles/gradients'
|
||||
import {Wallet} from '@nutui/icons-react-taro'
|
||||
import {businessGradients} from '@/styles/gradients'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useDealerUser } from '@/hooks/useDealerUser'
|
||||
import { pageShopDealerWithdraw, addShopDealerWithdraw } from '@/api/shop/shopDealerWithdraw'
|
||||
import type { ShopDealerWithdraw } from '@/api/shop/shopDealerWithdraw/model'
|
||||
import {useDealerUser} from '@/hooks/useDealerUser'
|
||||
import {pageShopDealerWithdraw, addShopDealerWithdraw} from '@/api/shop/shopDealerWithdraw'
|
||||
import type {ShopDealerWithdraw} from '@/api/shop/shopDealerWithdraw/model'
|
||||
|
||||
interface WithdrawRecordWithDetails extends ShopDealerWithdraw {
|
||||
accountDisplay?: string
|
||||
}
|
||||
|
||||
const DealerWithdraw: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState('0')
|
||||
const [activeTab, setActiveTab] = useState<string | number>('0')
|
||||
const [selectedAccount, setSelectedAccount] = useState('')
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [refreshing, setRefreshing] = useState<boolean>(false)
|
||||
@@ -34,16 +35,28 @@ const DealerWithdraw: React.FC = () => {
|
||||
const [withdrawRecords, setWithdrawRecords] = useState<WithdrawRecordWithDetails[]>([])
|
||||
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 () => {
|
||||
console.log(dealerUser, 'dealerUser...')
|
||||
try {
|
||||
setAvailableAmount(dealerUser?.money || '0.00')
|
||||
setAvailableAmount(dealerUser?.money || '0.00')
|
||||
} catch (error) {
|
||||
console.error('获取余额失败:', error)
|
||||
}
|
||||
}, [])
|
||||
}, [dealerUser])
|
||||
|
||||
// 获取提现记录
|
||||
const fetchWithdrawRecords = useCallback(async () => {
|
||||
@@ -104,21 +117,31 @@ const DealerWithdraw: React.FC = () => {
|
||||
|
||||
const getStatusText = (status?: number) => {
|
||||
switch (status) {
|
||||
case 40: return '已到账'
|
||||
case 20: return '审核通过'
|
||||
case 10: return '待审核'
|
||||
case 30: return '已驳回'
|
||||
default: return '未知'
|
||||
case 40:
|
||||
return '已到账'
|
||||
case 20:
|
||||
return '审核通过'
|
||||
case 10:
|
||||
return '待审核'
|
||||
case 30:
|
||||
return '已驳回'
|
||||
default:
|
||||
return '未知'
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusColor = (status?: number) => {
|
||||
switch (status) {
|
||||
case 40: return 'success'
|
||||
case 20: return 'success'
|
||||
case 10: return 'warning'
|
||||
case 30: return 'danger'
|
||||
default: return 'default'
|
||||
case 40:
|
||||
return 'success'
|
||||
case 20:
|
||||
return 'success'
|
||||
case 10:
|
||||
return 'warning'
|
||||
case 30:
|
||||
return 'danger'
|
||||
default:
|
||||
return 'default'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,9 +154,25 @@ const DealerWithdraw: React.FC = () => {
|
||||
return
|
||||
}
|
||||
|
||||
if (!values.accountType) {
|
||||
Taro.showToast({
|
||||
title: '请选择提现方式',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证提现金额
|
||||
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) {
|
||||
Taro.showToast({
|
||||
@@ -151,6 +190,25 @@ const DealerWithdraw: React.FC = () => {
|
||||
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 {
|
||||
setSubmitting(true)
|
||||
|
||||
@@ -158,7 +216,7 @@ const DealerWithdraw: React.FC = () => {
|
||||
userId: dealerUser.userId,
|
||||
money: values.amount,
|
||||
payType: values.accountType === 'wechat' ? 10 :
|
||||
values.accountType === 'alipay' ? 20 : 30,
|
||||
values.accountType === 'alipay' ? 20 : 30,
|
||||
applyStatus: 10, // 待审核
|
||||
platform: 'MiniProgram'
|
||||
}
|
||||
@@ -204,15 +262,21 @@ const DealerWithdraw: React.FC = () => {
|
||||
const quickAmounts = ['100', '300', '500', '1000']
|
||||
|
||||
const setQuickAmount = (amount: string) => {
|
||||
formRef.current?.setFieldsValue({ amount })
|
||||
formRef.current?.setFieldsValue({amount})
|
||||
}
|
||||
|
||||
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 = () => (
|
||||
<View className="p-4">
|
||||
<View>
|
||||
{/* 余额卡片 */}
|
||||
<View className="rounded-xl p-6 mb-6 text-white relative overflow-hidden" style={{
|
||||
background: businessGradients.dealer.header
|
||||
@@ -225,14 +289,14 @@ const DealerWithdraw: React.FC = () => {
|
||||
}}></View>
|
||||
|
||||
<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-2xl font-bold text-white">¥{availableAmount}</Text>
|
||||
</View>
|
||||
<View className="p-3 rounded-full" style={{
|
||||
background: 'rgba(255, 255, 255, 0.2)'
|
||||
}}>
|
||||
<Wallet color="white" size="32" />
|
||||
<Wallet color="white" size="32"/>
|
||||
</View>
|
||||
</View>
|
||||
<View className="mt-4 pt-4 relative z-10" style={{
|
||||
@@ -254,7 +318,14 @@ const DealerWithdraw: React.FC = () => {
|
||||
<Input
|
||||
placeholder="请输入提现金额"
|
||||
type="number"
|
||||
clearable
|
||||
onChange={(value) => {
|
||||
// 实时验证提现金额
|
||||
const amount = parseFloat(value)
|
||||
const available = parseFloat(availableAmount.replace(/,/g, ''))
|
||||
if (!isNaN(amount) && amount > available) {
|
||||
// 可以在这里添加实时提示,但不阻止输入
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -301,10 +372,10 @@ const DealerWithdraw: React.FC = () => {
|
||||
{selectedAccount === 'alipay' && (
|
||||
<>
|
||||
<Form.Item name="account" label="支付宝账号" required>
|
||||
<Input placeholder="请输入支付宝账号" />
|
||||
<Input placeholder="请输入支付宝账号"/>
|
||||
</Form.Item>
|
||||
<Form.Item name="accountName" label="支付宝姓名" required>
|
||||
<Input placeholder="请输入支付宝实名姓名" />
|
||||
<Input placeholder="请输入支付宝实名姓名"/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
@@ -312,13 +383,13 @@ const DealerWithdraw: React.FC = () => {
|
||||
{selectedAccount === 'bank' && (
|
||||
<>
|
||||
<Form.Item name="bankName" label="开户银行" required>
|
||||
<Input placeholder="请输入开户银行名称" />
|
||||
<Input placeholder="请输入开户银行名称"/>
|
||||
</Form.Item>
|
||||
<Form.Item name="account" label="银行卡号" required>
|
||||
<Input placeholder="请输入银行卡号" />
|
||||
<Input placeholder="请输入银行卡号"/>
|
||||
</Form.Item>
|
||||
<Form.Item name="accountName" label="开户姓名" required>
|
||||
<Input placeholder="请输入银行卡开户姓名" />
|
||||
<Input placeholder="请输入银行卡开户姓名"/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
@@ -347,60 +418,64 @@ const DealerWithdraw: React.FC = () => {
|
||||
</View>
|
||||
)
|
||||
|
||||
const renderWithdrawRecords = () => (
|
||||
<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>
|
||||
) : 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>
|
||||
const renderWithdrawRecords = () => {
|
||||
console.log('渲染提现记录:', {loading, recordsCount: withdrawRecords.length, dealerUserId: dealerUser?.userId})
|
||||
|
||||
<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>
|
||||
return (
|
||||
<PullToRefresh
|
||||
disabled={refreshing}
|
||||
onRefresh={handleRefresh}
|
||||
>
|
||||
<View>
|
||||
{loading ? (
|
||||
<View className="text-center py-8">
|
||||
<Loading/>
|
||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||
</View>
|
||||
))
|
||||
) : (
|
||||
<Empty description="暂无提现记录" />
|
||||
)}
|
||||
</View>
|
||||
</PullToRefresh>
|
||||
)
|
||||
) : withdrawRecords.length > 0 ? (
|
||||
withdrawRecords.map(record => (
|
||||
<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">
|
||||
<Space>
|
||||
<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) {
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
||||
<Loading />
|
||||
<Loading/>
|
||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||
</View>
|
||||
)
|
||||
@@ -408,7 +483,7 @@ const DealerWithdraw: React.FC = () => {
|
||||
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen">
|
||||
<Tabs value={activeTab} onChange={() => setActiveTab}>
|
||||
<Tabs value={activeTab} onChange={handleTabChange}>
|
||||
<Tabs.TabPane title="申请提现" value="0">
|
||||
{renderWithdrawForm()}
|
||||
</Tabs.TabPane>
|
||||
|
||||
Reference in New Issue
Block a user