From 0b43a3bc9262cbf97b53a304bd66c003ea175a15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Wed, 3 Sep 2025 10:41:06 +0800 Subject: [PATCH] =?UTF-8?q?feat(dealer):=20=E6=B7=BB=E5=8A=A0=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E5=88=97=E8=A1=A8=E5=8A=9F=E8=83=BD=E5=B9=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E9=82=80=E8=AF=B7=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增客户列表页面,实现客户数据获取和筛选功能 - 添加客户状态管理工具函数 - 优化邀请流程,支持绑定推荐关系 - 调整提现功能,增加调试组件 - 修复申请经销商功能中的推荐人ID逻辑 --- config/env.ts | 2 +- src/api/invite/index.ts | 81 +++- src/api/shop/shopDealerUser/index.ts | 1 + src/api/system/user/index.ts | 8 +- src/app.config.ts | 3 +- src/dealer/apply/add.tsx | 42 +- src/dealer/customer/README.md | 108 +++++ src/dealer/customer/index.config.ts | 3 + src/dealer/customer/index.tsx | 184 ++++++++ src/dealer/index.tsx | 89 ++-- src/dealer/orders/index.tsx | 408 +++++------------- src/dealer/qrcode/index.tsx | 321 +++++++------- src/dealer/team/index.tsx | 270 +++--------- .../withdraw/__tests__/withdraw.test.tsx | 184 ++++++++ src/dealer/withdraw/debug.tsx | 80 ++++ src/dealer/withdraw/index.tsx | 243 +++++++---- src/pages/customer/list.tsx | 124 ++---- src/utils/customerStatus.ts | 69 +++ 18 files changed, 1286 insertions(+), 934 deletions(-) create mode 100644 src/dealer/customer/README.md create mode 100644 src/dealer/customer/index.config.ts create mode 100644 src/dealer/customer/index.tsx create mode 100644 src/dealer/withdraw/__tests__/withdraw.test.tsx create mode 100644 src/dealer/withdraw/debug.tsx create mode 100644 src/utils/customerStatus.ts diff --git a/config/env.ts b/config/env.ts index c3f95e2..de8e281 100644 --- a/config/env.ts +++ b/config/env.ts @@ -2,7 +2,7 @@ export const ENV_CONFIG = { // 开发环境 development: { - API_BASE_URL: 'http://127.0.0.1:9200/api', + API_BASE_URL: 'https://cms-api.websoft.top/api', APP_NAME: '开发环境', DEBUG: 'true', }, diff --git a/src/api/invite/index.ts b/src/api/invite/index.ts index 0e878b8..ba38f75 100644 --- a/src/api/invite/index.ts +++ b/src/api/invite/index.ts @@ -1,6 +1,6 @@ import request from '@/utils/request'; 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; } +/** + * 绑定推荐关系参数 + */ +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) { - const res = await request.post>( - SERVER_API_URL + '/invite/generate-miniprogram-code', - data - ); - if (res.code === 0) { - return res.data; + try { + const url = '/wx-login/getOrderQRCodeUnlimited/' + data.scene; + // 由于接口直接返回图片buffer,我们直接构建完整的URL + return `${BaseUrl}${url}`; + } catch (error: any) { + throw new Error(error.message || '生成小程序码失败'); } - return Promise.reject(new Error(res.message)); } /** * 生成邀请小程序码 */ -export async function generateInviteCode(inviterId: number, source: string = 'qrcode') { - const scene = `inviter=${inviterId}&source=${source}&t=${Date.now()}`; +export async function generateInviteCode(inviterId: number) { + const scene = `uid_${inviterId}`; return generateMiniProgramCode({ page: 'pages/index/index', scene: scene, - width: 430, + width: 180, checkPath: true, - envVersion: 'release' + envVersion: 'trial' }); } /** - * 建立邀请关系 + * 建立邀请关系 (旧接口,保留兼容性) */ export async function createInviteRelation(data: InviteRelationParam) { const res = await request.post>( - SERVER_API_URL + '/invite/create-relation', + '/invite/create-relation', data ); if (res.code === 0) { @@ -135,12 +148,38 @@ export async function createInviteRelation(data: InviteRelationParam) { return Promise.reject(new Error(res.message)); } +/** + * 绑定推荐关系 (新接口) + */ +export async function bindRefereeRelation(data: BindRefereeParam) { + try { + const res = await request.post>( + '/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) { const res = await request.post>( - SERVER_API_URL + '/invite/process-scene', + '/invite/process-scene', { scene, userId } ); if (res.code === 0) { @@ -154,7 +193,7 @@ export async function processInviteScene(scene: string, userId: number) { */ export async function getInviteStats(inviterId: number) { const res = await request.get>( - SERVER_API_URL + `/invite/stats/${inviterId}` + `/invite/stats/${inviterId}` ); if (res.code === 0) { return res.data; @@ -167,7 +206,7 @@ export async function getInviteStats(inviterId: number) { */ export async function pageInviteRecords(params: InviteRecordParam) { const res = await request.get>>( - SERVER_API_URL + '/invite/records/page', + '/invite/records/page', params ); if (res.code === 0) { @@ -181,7 +220,7 @@ export async function pageInviteRecords(params: InviteRecordParam) { */ export async function getMyInviteRecords(params: InviteRecordParam) { const res = await request.get>>( - SERVER_API_URL + '/invite/my-records', + '/invite/my-records', params ); if (res.code === 0) { @@ -195,7 +234,7 @@ export async function getMyInviteRecords(params: InviteRecordParam) { */ export async function validateInviteCode(scene: string) { const res = await request.post>( - SERVER_API_URL + '/invite/validate-code', + '/invite/validate-code', { scene } ); if (res.code === 0) { @@ -209,7 +248,7 @@ export async function validateInviteCode(scene: string) { */ export async function updateInviteStatus(inviteId: number, status: 'registered' | 'activated') { const res = await request.put>( - SERVER_API_URL + `/invite/update-status/${inviteId}`, + `/invite/update-status/${inviteId}`, { status } ); if (res.code === 0) { @@ -229,7 +268,7 @@ export async function getInviteRanking(params?: { limit?: number; period?: 'day' successCount: number; conversionRate: number; }>>>( - SERVER_API_URL + '/invite/ranking', + '/invite/ranking', params ); if (res.code === 0) { diff --git a/src/api/shop/shopDealerUser/index.ts b/src/api/shop/shopDealerUser/index.ts index 061c62c..0f673a1 100644 --- a/src/api/shop/shopDealerUser/index.ts +++ b/src/api/shop/shopDealerUser/index.ts @@ -6,6 +6,7 @@ import type { ShopDealerUser, ShopDealerUserParam } from './model'; * 分页查询分销商用户记录表 */ export async function pageShopDealerUser(params: ShopDealerUserParam) { + // 使用新的request方法,它会自动处理错误并返回完整的ApiResult const res = await request.get>>( '/shop/shop-dealer-user/page', params diff --git a/src/api/system/user/index.ts b/src/api/system/user/index.ts index c0f144f..7fae4ec 100644 --- a/src/api/system/user/index.ts +++ b/src/api/system/user/index.ts @@ -8,8 +8,8 @@ import {SERVER_API_URL} from "@/utils/server"; */ export async function pageUsers(params: UserParam) { const res = await request.get>>( - '/system/user/page', - {params} + SERVER_API_URL + '/system/user/page', + params ); if (res.code === 0) { return res.data; @@ -23,9 +23,7 @@ export async function pageUsers(params: UserParam) { export async function listUsers(params?: UserParam) { const res = await request.get>( '/system/user', - { - params - } + params ); if (res.code === 0 && res.data) { return res.data; diff --git a/src/app.config.ts b/src/app.config.ts index c504400..c6706aa 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -62,7 +62,8 @@ export default defineAppConfig({ "team/index", "qrcode/index", "invite-stats/index", - "info" + "info", + "customer/index", ] }, // { diff --git a/src/dealer/apply/add.tsx b/src/dealer/apply/add.tsx index 9042c90..08c2f15 100644 --- a/src/dealer/apply/add.tsx +++ b/src/dealer/apply/add.tsx @@ -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(true) - const [FormData, setFormData] = useState({}) + const [FormData, setFormData] = useState() const formRef = useRef(null) const [isEditMode, setIsEditMode] = useState(false) const [existingApply, setExistingApply] = useState(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 = () => { - - + + @@ -153,29 +157,29 @@ const AddUserAddress = () => { title={'审核状态'} extra={ - {getApplyStatusText(FormData.applyStatus)} + {getApplyStatusText(FormData?.applyStatus)} } /> - {FormData.applyStatus === 20 && ( - + {FormData?.applyStatus === 20 && ( + )} - {FormData.applyStatus === 30 && ( - + {FormData?.applyStatus === 30 && ( + )} )} {/* 底部浮动按钮 */} - {(!isEditMode || FormData.applyStatus === 10 || FormData.applyStatus === 30) && ( + {(!isEditMode || FormData?.applyStatus === 10 || FormData?.applyStatus === 30) && ( } text={isEditMode ? '保存修改' : '提交申请'} - disabled={FormData.applyStatus === 10} + disabled={FormData?.applyStatus === 10} onClick={handleFixedButtonClick} /> )} diff --git a/src/dealer/customer/README.md b/src/dealer/customer/README.md new file mode 100644 index 0000000..20ccfd6 --- /dev/null +++ b/src/dealer/customer/README.md @@ -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 # 客户状态工具函数 +``` diff --git a/src/dealer/customer/index.config.ts b/src/dealer/customer/index.config.ts new file mode 100644 index 0000000..6e26749 --- /dev/null +++ b/src/dealer/customer/index.config.ts @@ -0,0 +1,3 @@ +export default definePageConfig({ + navigationBarTitleText: '客户列表' +}) diff --git a/src/dealer/customer/index.tsx b/src/dealer/customer/index.tsx new file mode 100644 index 0000000..c6c885d --- /dev/null +++ b/src/dealer/customer/index.tsx @@ -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([]) + const [loading, setLoading] = useState(false) + const [activeTab, setActiveTab] = useState('all') + const [searchValue, setSearchValue] = useState('') + + // 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) => ( + + + + + + {customer.realName || customer.nickname || customer.username} + + {customer.customerStatus && ( + + {getStatusText(customer.customerStatus)} + + )} + + + + + {customer.phone || '未填写'} + + + + 注册时间:{customer.createTime} + + + + + ); + + // 渲染客户列表 + const renderCustomerList = () => { + const filteredList = getFilteredList(); + + if (loading) { + return ( + + + 加载中... + + ); + } + + if (filteredList.length === 0) { + return ( + + 暂无客户数据 + + ); + } + + return ( + + {filteredList.map(renderCustomerItem)} + + ); + }; + + return ( + + {/* 顶部Tabs */} + + setActiveTab(value as CustomerStatus)} + > + {tabList.map(tab => { + const counts = getStatusCounts(); + const count = counts[tab.value as keyof typeof counts] || 0; + return ( + 0 ? `(${count})` : ''}`} + value={tab.value} + /> + ); + })} + + + + {/* 客户列表 */} + {renderCustomerList()} + + ); +}; + +export default CustomerManagement; diff --git a/src/dealer/index.tsx b/src/dealer/index.tsx index 56ef5eb..06c514f 100644 --- a/src/dealer/index.tsx +++ b/src/dealer/index.tsx @@ -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 = () => { 佣金统计 - + - - ¥{formatMoney(dealerUser.money)} + + {formatMoney(dealerUser.money)} 可提现 - - ¥{formatMoney(dealerUser.freezeMoney)} + + {formatMoney(dealerUser.freezeMoney)} 冻结中 - - ¥{formatMoney(dealerUser.totalMoney)} + + {formatMoney(dealerUser.totalMoney)} 累计收益 @@ -244,45 +243,45 @@ const DealerIndex: React.FC = () => { {/* 第二行功能 */} - - navigateToPage('/dealer/invite-stats/index')}> - - - - - - + {/**/} + {/* navigateToPage('/dealer/invite-stats/index')}>*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} - {/* 预留其他功能位置 */} - - - - - - + {/* /!* 预留其他功能位置 *!/*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} - - - - - - + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} - - - - - - - + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/**/} diff --git a/src/dealer/orders/index.tsx b/src/dealer/orders/index.tsx index 1cc459b..f3b6bbf 100644 --- a/src/dealer/orders/index.tsx +++ b/src/dealer/orders/index.tsx @@ -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('0') const [loading, setLoading] = useState(false) + const [refreshing, setRefreshing] = useState(false) + const [loadingMore, setLoadingMore] = useState(false) const [orders, setOrders] = useState([]) - 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(1) + const [hasMore, setHasMore] = useState(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) => ( - - - - 订单号:{order.orderNo} - - - 客户:{order.customerName} - - {/* 显示用户在此订单中的层级 */} - - {order.userLevel === 1 && '一级分销'} - {order.userLevel === 2 && '二级分销'} - {order.userLevel === 3 && '三级分销'} - - + + + 订单号:{order.orderNo} + {getStatusText(order.isSettled, order.isInvalid)} + + + 订单金额:¥{order.orderPrice || '0.00'} + + + 我的佣金:¥{order.userCommission} + + + - - - 订单金额:¥{order.orderPrice || '0.00'} - - - 我的佣金:¥{order.userCommission} - - - 总佣金:¥{order.totalCommission} - - - + + 客户:{order.customerName} + + {order.createTime} ) - // 根据状态和层级过滤订单 - 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 ( - + 加载中... ) } return ( - - {/* 统计卡片 */} - - {/* 总体统计 */} - - - {statistics.totalOrders} - 总订单 - - - ¥{statistics.totalCommission} - 总佣金 - - - ¥{statistics.pendingCommission} - 待结算 - - - - {/* 分层统计 */} - - 分层统计 - - - {statistics.level1.orders} - 一级订单 - ¥{statistics.level1.commission} - - - {statistics.level2.orders} - 二级订单 - ¥{statistics.level2.commission} - - - {statistics.level3.orders} - 三级订单 - ¥{statistics.level3.commission} - - - - - - {/* 订单列表 */} - setActiveTab}> - - - - {loading ? ( - - - 加载中... - - ) : getFilteredOrders('0').length > 0 ? ( - getFilteredOrders('0').map(renderOrderItem) - ) : ( - - )} - - - - - + + + - {getFilteredOrders('1').length > 0 ? ( - getFilteredOrders('1').map(renderOrderItem) + {loading && orders.length === 0 ? ( + + + 加载中... + + ) : orders.length > 0 ? ( + <> + {orders.map(renderOrderItem)} + {loadingMore && ( + + + 加载更多... + + )} + {!hasMore && orders.length > 0 && ( + + 没有更多数据了 + + )} + ) : ( - + )} - - - - - {getFilteredOrders('2').length > 0 ? ( - getFilteredOrders('2').map(renderOrderItem) - ) : ( - - )} - - - - - - {getFilteredOrders('3').length > 0 ? ( - getFilteredOrders('3').map(renderOrderItem) - ) : ( - - )} - - - - - - {getFilteredOrders('4').length > 0 ? ( - getFilteredOrders('4').map(renderOrderItem) - ) : ( - - )} - - - - - - {getFilteredOrders('5').length > 0 ? ( - getFilteredOrders('5').map(renderOrderItem) - ) : ( - - )} - - - - - - {getFilteredOrders('6').length > 0 ? ( - getFilteredOrders('6').map(renderOrderItem) - ) : ( - - )} - - - + + ) } diff --git a/src/dealer/qrcode/index.tsx b/src/dealer/qrcode/index.tsx index ee9fd44..882931b 100644 --- a/src/dealer/qrcode/index.tsx +++ b/src/dealer/qrcode/index.tsx @@ -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('') const [loading, setLoading] = useState(false) - const [inviteStats, setInviteStats] = useState(null) - const [statsLoading, setStatsLoading] = useState(false) - const { dealerUser } = useDealerUser() + // const [inviteStats, setInviteStats] = useState(null) + // const [statsLoading, setStatsLoading] = useState(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 ( - + 加载中... ) @@ -179,7 +184,7 @@ const DealerQrcode: React.FC = () => { right: '-16px' }}> - + 我的邀请小程序码 分享小程序码邀请好友,获得丰厚佣金奖励 @@ -193,7 +198,7 @@ const DealerQrcode: React.FC = () => { {loading ? ( - + 生成中... ) : 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(); + } + } + }); + }} + /> ) : ( @@ -219,58 +238,64 @@ const DealerQrcode: React.FC = () => { )} - + 扫码加入我的团队 - - + + 好友扫描小程序码即可直接进入小程序并建立邀请关系 - + + + {/* 操作按钮 */} - - - - - - + + + + + + + + + + {/* 推广说明 */} - + 推广说明 - 好友通过您的二维码或链接注册成为您的下级分销商 + 好友通过您的二维码或链接注册成为您的团队成员 @@ -289,82 +314,82 @@ const DealerQrcode: React.FC = () => { {/* 邀请统计数据 */} - - 我的邀请数据 - {statsLoading ? ( - - - 加载中... - - ) : inviteStats ? ( - - - - - {inviteStats.totalInvites || 0} - - 总邀请数 - - - - {inviteStats.successfulRegistrations || 0} - - 成功注册 - - + {/**/} + {/* 我的邀请数据*/} + {/* {statsLoading ? (*/} + {/* */} + {/* */} + {/* 加载中...*/} + {/* */} + {/* ) : inviteStats ? (*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* {inviteStats.totalInvites || 0}*/} + {/* */} + {/* 总邀请数*/} + {/* */} + {/* */} + {/* */} + {/* {inviteStats.successfulRegistrations || 0}*/} + {/* */} + {/* 成功注册*/} + {/* */} + {/* */} - - - - {inviteStats.conversionRate ? `${(inviteStats.conversionRate * 100).toFixed(1)}%` : '0%'} - - 转化率 - - - - {inviteStats.todayInvites || 0} - - 今日邀请 - - + {/* */} + {/* */} + {/* */} + {/* {inviteStats.conversionRate ? `${(inviteStats.conversionRate * 100).toFixed(1)}%` : '0%'}*/} + {/* */} + {/* 转化率*/} + {/* */} + {/* */} + {/* */} + {/* {inviteStats.todayInvites || 0}*/} + {/* */} + {/* 今日邀请*/} + {/* */} + {/* */} - {/* 邀请来源统计 */} - {inviteStats.sourceStats && inviteStats.sourceStats.length > 0 && ( - - 邀请来源分布 - - {inviteStats.sourceStats.map((source, index) => ( - - - - {source.source} - - - {source.count} - - {source.conversionRate ? `${(source.conversionRate * 100).toFixed(1)}%` : '0%'} - - - - ))} - - - )} - - ) : ( - - 暂无邀请数据 - - - )} - + {/* /!* 邀请来源统计 *!/*/} + {/* {inviteStats.sourceStats && inviteStats.sourceStats.length > 0 && (*/} + {/* */} + {/* 邀请来源分布*/} + {/* */} + {/* {inviteStats.sourceStats.map((source, index) => (*/} + {/* */} + {/* */} + {/* */} + {/* {source.source}*/} + {/* */} + {/* */} + {/* {source.count}*/} + {/* */} + {/* {source.conversionRate ? `${(source.conversionRate * 100).toFixed(1)}%` : '0%'}*/} + {/* */} + {/* */} + {/* */} + {/* ))}*/} + {/* */} + {/* */} + {/* )}*/} + {/* */} + {/* ) : (*/} + {/* */} + {/* 暂无邀请数据*/} + {/* */} + {/* 刷新数据*/} + {/* */} + {/* */} + {/* )}*/} + {/**/} ) diff --git a/src/dealer/team/index.tsx b/src/dealer/team/index.tsx index 845516d..90cb9ac 100644 --- a/src/dealer/team/index.tsx +++ b/src/dealer/team/index.tsx @@ -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(false) - const [refreshing, setRefreshing] = useState(false) const [teamMembers, setTeamMembers] = useState([]) - 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() // 获取团队数据 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 - case 2: return - case 3: return - default: return - } - } - const renderMemberItem = (member: TeamMemberWithStats) => ( - + getNextUser(member)}> } + icon={} className="mr-3" /> @@ -172,194 +127,65 @@ const DealerTeam: React.FC = () => { {member.name} - {getLevelIcon(Number(member.level))} - - {member.level}级 - + {/*{getLevelIcon(Number(member.level))}*/} + {/**/} + {/* {member.level}级*/} + {/**/} 加入时间:{member.joinTime} - - - {member.status === 'active' ? '活跃' : '沉默'} - - + {/**/} + {/* */} + {/* {member.status === 'active' ? '活跃' : '沉默'}*/} + {/* */} + {/**/} - + + 订单数 {member.orderCount} - 订单数 - - + + + 贡献佣金 ¥{member.commission} - 贡献佣金 - - + + + 团队成员 {member.subMembers} - 下级成员 - + ) const renderOverview = () => ( - - {/* 团队统计卡片 */} - - {/* 装饰背景 - 小程序兼容版本 */} - - - - - 团队总览 - - - {teamStats.total} - 团队总人数 - - - ¥{teamStats.monthlyCommission} - 本月团队佣金 - - - - - - {/* 层级分布 */} - - 层级分布 - - - - - 一级成员 - - - {teamStats.firstLevel} - - - - - - - - 二级成员 - - - {teamStats.secondLevel} - - - - - - - - 三级成员 - - - {teamStats.thirdLevel} - - - - - - - {/* 最新成员 */} - - 最新成员 - {teamMembers.slice(0, 3).map(renderMemberItem)} - + + {teamMembers.slice(0, 3).map(renderMemberItem)} ) - const renderMemberList = (level?: number) => ( - - - {loading ? ( - - - 加载中... - - ) : teamMembers - .filter(member => !level || member.level === level) - .length > 0 ? ( - teamMembers - .filter(member => !level || member.level === level) - .map(renderMemberItem) - ) : ( - - )} - - - ) - if (!dealerUser) { return ( - + 加载中... ) } return ( - - setActiveTab}> - - {renderOverview()} - - - - {renderMemberList(1)} - - - - {renderMemberList(2)} - - - - {renderMemberList(3)} - - + + {renderOverview()} ) } diff --git a/src/dealer/withdraw/__tests__/withdraw.test.tsx b/src/dealer/withdraw/__tests__/withdraw.test.tsx new file mode 100644 index 0000000..c3aeab9 --- /dev/null +++ b/src/dealer/withdraw/__tests__/withdraw.test.tsx @@ -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 +const mockAddShopDealerWithdraw = withdrawAPI.addShopDealerWithdraw as jest.MockedFunction +const mockPageShopDealerWithdraw = withdrawAPI.pageShopDealerWithdraw as jest.MockedFunction + +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() + expect(getByText('10000.00')).toBeInTheDocument() + expect(getByText('可提现余额')).toBeInTheDocument() + }) + + test('应该验证最低提现金额', async () => { + mockAddShopDealerWithdraw.mockResolvedValue('success') + + const { getByPlaceholderText, getByText } = render() + + // 输入低于最低金额的数值 + 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() + + // 输入超过可用余额的金额 + 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() + + // 输入有效金额 + 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() + + // 输入有效金额 + 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() + + // 点击快捷金额按钮 + const quickAmountButton = getByText('500') + fireEvent.click(quickAmountButton) + + // 验证金额输入框的值 + const amountInput = getByPlaceholderText('请输入提现金额') as HTMLInputElement + expect(amountInput.value).toBe('500') + }) + + test('全部按钮应该设置为可用余额', () => { + const { getByText, getByPlaceholderText } = render() + + // 点击全部按钮 + const allButton = getByText('全部') + fireEvent.click(allButton) + + // 验证金额输入框的值 + const amountInput = getByPlaceholderText('请输入提现金额') as HTMLInputElement + expect(amountInput.value).toBe('10000.00') + }) +}) diff --git a/src/dealer/withdraw/debug.tsx b/src/dealer/withdraw/debug.tsx new file mode 100644 index 0000000..167d7ba --- /dev/null +++ b/src/dealer/withdraw/debug.tsx @@ -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('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 ( + + + 调试信息 + 当前Tab: {String(activeTab)} + 切换次数: {clickCount} + Tab类型: {typeof activeTab} + + + + 手动切换测试 + + + + + + + + + + + 申请提现页面内容 + + 当前Tab值: {String(activeTab)} + + + + + + + 提现记录页面内容 + + 当前Tab值: {String(activeTab)} + + + + + + + + 事件日志 + + 请查看控制台输出以获取详细的切换日志 + + + + ) +} + +export default WithdrawDebug diff --git a/src/dealer/withdraw/index.tsx b/src/dealer/withdraw/index.tsx index 9ef56f5..04a3cef 100644 --- a/src/dealer/withdraw/index.tsx +++ b/src/dealer/withdraw/index.tsx @@ -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('0') const [selectedAccount, setSelectedAccount] = useState('') const [loading, setLoading] = useState(false) const [refreshing, setRefreshing] = useState(false) @@ -34,16 +35,28 @@ const DealerWithdraw: React.FC = () => { const [withdrawRecords, setWithdrawRecords] = useState([]) const formRef = useRef(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 = () => ( - + {/* 余额卡片 */} { }}> - + + {formatMoney(dealerUser?.money)} 可提现余额 - ¥{availableAmount} - + { { + // 实时验证提现金额 + const amount = parseFloat(value) + const available = parseFloat(availableAmount.replace(/,/g, '')) + if (!isNaN(amount) && amount > available) { + // 可以在这里添加实时提示,但不阻止输入 + } + }} /> @@ -301,10 +372,10 @@ const DealerWithdraw: React.FC = () => { {selectedAccount === 'alipay' && ( <> - + - + )} @@ -312,13 +383,13 @@ const DealerWithdraw: React.FC = () => { {selectedAccount === 'bank' && ( <> - + - + - + )} @@ -347,60 +418,64 @@ const DealerWithdraw: React.FC = () => { ) - const renderWithdrawRecords = () => ( - - - {loading ? ( - - - 加载中... - - ) : withdrawRecords.length > 0 ? ( - withdrawRecords.map(record => ( - - - - - 提现金额:¥{record.money} - - - 提现账户:{record.accountDisplay} - - - - {getStatusText(record.applyStatus)} - - + const renderWithdrawRecords = () => { + console.log('渲染提现记录:', {loading, recordsCount: withdrawRecords.length, dealerUserId: dealerUser?.userId}) - - 申请时间:{record.createTime} - {record.auditTime && ( - - 审核时间:{new Date(record.auditTime).toLocaleString()} - - )} - {record.rejectReason && ( - - 驳回原因:{record.rejectReason} - - )} - + return ( + + + {loading ? ( + + + 加载中... - )) - ) : ( - - )} - - - ) + ) : withdrawRecords.length > 0 ? ( + withdrawRecords.map(record => ( + + + + + 提现金额:¥{record.money} + + + 提现账户:{record.accountDisplay} + + + + {getStatusText(record.applyStatus)} + + + + + 申请时间:{record.createTime} + {record.auditTime && ( + + 审核时间:{new Date(record.auditTime).toLocaleString()} + + )} + {record.rejectReason && ( + + 驳回原因:{record.rejectReason} + + )} + + + )) + ) : ( + + )} + + + ) + } if (!dealerUser) { return ( - + 加载中... ) @@ -408,7 +483,7 @@ const DealerWithdraw: React.FC = () => { return ( - setActiveTab}> + {renderWithdrawForm()} diff --git a/src/pages/customer/list.tsx b/src/pages/customer/list.tsx index bfe09dd..68a1059 100644 --- a/src/pages/customer/list.tsx +++ b/src/pages/customer/list.tsx @@ -4,54 +4,14 @@ import {View, Text} from '@tarojs/components'; import {Space, Tabs, Button, Empty} from '@nutui/nutui-react-taro'; import {Phone} from '@nutui/icons-react-taro'; 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 [statusBarHeight, setStatusBarHeight] = useState(0); const [activeTab, setActiveTab] = useState('all'); - const [customers, setCustomers] = useState([]); const [loading, setLoading] = useState(false); - - // 模拟客户数据 - 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 [list, setList] = useState([]); const tabList = [ {title: '全部', value: 'all'}, @@ -62,18 +22,26 @@ const CustomerList = () => { const reload = async () => { setLoading(true); - // 模拟API调用 - setTimeout(() => { - setCustomers(mockCustomers); + try { + const res = await pageUsers({status: 0}); + console.log(res, '客户列表'); + if(res?.list){ + // 为每个用户添加默认状态 + const customersWithStatus: ShopDealerUser[] = res.list.map(user => ({ + ...user, + status: 'pending' // 默认状态为跟进中 + })); + setList(customersWithStatus); + } + } catch (error) { + console.error('获取客户列表失败:', error); + Taro.showToast({ + title: '获取客户列表失败', + icon: 'error' + }); + } finally { setLoading(false); - }, 500); - }; - - const getFilteredCustomers = () => { - if (activeTab === 'all') { - return customers; } - return customers.filter(customer => customer.status === activeTab); }; 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) { case 'sign': // 跳转到签约页面 Taro.navigateTo({ - url: `/pages/customer/sign?customerId=${customer.id}` + url: `/pages/customer/sign?customerId=${customer.userId}` }); break; case 'cancel': @@ -136,7 +104,7 @@ const CustomerList = () => { case 'detail': // 跳转到客户详情页面 Taro.navigateTo({ - url: `/pages/customer/detail?customerId=${customer.id}` + url: `/pages/customer/detail?customerId=${customer.userId}` }); break; } @@ -149,19 +117,7 @@ const CustomerList = () => { }); }; - const handleTrading = () => { - // 跳转到入市交易页面 - Taro.navigateTo({ - url: '/pages/customer/trading' - }); - }; - useEffect(() => { - Taro.getSystemInfo({ - success: (res) => { - setStatusBarHeight(Number(res.statusBarHeight)); - }, - }); reload().then(); }, []); @@ -190,67 +146,67 @@ const CustomerList = () => { 加载中... - ) : getFilteredCustomers().length > 0 ? ( - getFilteredCustomers().map((customer) => ( - + ) : list.length > 0 ? ( + list.map((record) => ( + - {customer.companyName} + {record.realName || '未知客户'} - {getStatusText(customer.status)} + {getStatusText('pending')} 联系人: - {customer.contactPerson} + {record.realName || '未知'} 联系电话: - {customer.phone} + {record.mobile || '未提供'} handleCall(customer.phone)} + onClick={() => handleCall(`${record?.mobile}`)} /> 地址: - {customer.address} + {'地址未提供'} - 添加时间:{customer.addTime} + 添加时间:{record.createTime || '未知'} {/* 操作按钮 */} - {customer.status === 'pending' && ( + {record.payPassword === 'pending' && ( )} - {customer.status === 'confirmed' && ( + {record.payPassword === 'confirmed' && ( diff --git a/src/utils/customerStatus.ts b/src/utils/customerStatus.ts new file mode 100644 index 0000000..b35399c --- /dev/null +++ b/src/utils/customerStatus.ts @@ -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)]; +};