feat(dealer): 优化业务员申请和团队管理功能

-强制用户手动输入昵称,清空默认微信昵称
- 添加昵称验证逻辑,禁止使用默认昵称
- 优化团队数据加载和展示逻辑
- 添加保存二维码到相册功能
- 调整提现金额门槛为100元
This commit is contained in:
2025-09-12 13:09:41 +08:00
parent b2d79ab052
commit 86516a8334
8 changed files with 359 additions and 192 deletions

View File

@@ -37,6 +37,14 @@ const AddUserAddress = () => {
setFormData({
...user,
refereeId: Number(inviteParams.inviter),
// 清空昵称,强制用户手动输入
nickname: '',
})
} else {
// 如果没有邀请参数,也要确保昵称为空
setFormData({
...user,
nickname: '',
})
}
}
@@ -130,7 +138,9 @@ const AddUserAddress = () => {
return;
}
if (!values.realName && !FormData?.nickname) {
// 验证昵称:必须填写且不能是默认的微信昵称
const nickname = values.realName || FormData?.nickname || '';
if (!nickname || nickname.trim() === '') {
Taro.showToast({
title: '请填写昵称',
icon: 'error'
@@ -138,6 +148,25 @@ const AddUserAddress = () => {
return;
}
// 检查是否为默认的微信昵称(常见的默认昵称)
const defaultNicknames = ['微信用户', 'WeChat User', '微信昵称'];
if (defaultNicknames.includes(nickname.trim())) {
Taro.showToast({
title: '请填写真实昵称,不能使用默认昵称',
icon: 'error'
});
return;
}
// 验证昵称长度
if (nickname.trim().length < 2) {
Taro.showToast({
title: '昵称至少需要2个字符',
icon: 'error'
});
return;
}
if (!values.avatar && !FormData?.avatar) {
Taro.showToast({
title: '请上传头像',
@@ -145,6 +174,7 @@ const AddUserAddress = () => {
});
return;
}
console.log(values,FormData)
const roles = await listUserRole({userId: user?.userId})
console.log(roles, 'roles...')
@@ -247,8 +277,11 @@ const AddUserAddress = () => {
console.log('手机号已获取', userData.phone)
const updatedFormData = {
...FormData,
...userData,
phone: userData.phone
phone: userData.phone,
// 不自动填充微信昵称,保持用户已输入的昵称
nickname: FormData?.nickname || '',
// 只在没有头像时才使用微信头像
avatar: FormData?.avatar || userData.avatar
}
setFormData(updatedFormData)
@@ -256,8 +289,9 @@ const AddUserAddress = () => {
if (formRef.current) {
formRef.current.setFieldsValue({
phone: userData.phone,
realName: userData.nickname || FormData?.nickname,
avatar: userData.avatar || FormData?.avatar
// 不覆盖用户已输入的昵称
realName: FormData?.nickname || '',
avatar: FormData?.avatar || userData.avatar
})
}
@@ -373,11 +407,11 @@ const AddUserAddress = () => {
</Button>
</Form.Item>
}
<Form.Item name="realName" label="昵称" initialValue={FormData?.nickname} required>
<Form.Item name="realName" label="昵称" initialValue="" required>
<Input
type="nickname"
className="info-content__input"
placeholder="请输入昵称"
placeholder="请获取微信昵称"
value={FormData?.nickname || ''}
onInput={(e: InputEvent) => getWxNickname(e.detail.value)}
/>

View File

@@ -1,20 +1,19 @@
import {useEffect, useState, useRef} from "react";
import {Loading, CellGroup, Cell, Input, Form, Calendar} from '@nutui/nutui-react-taro'
import {Edit, Calendar as CalendarIcon} from '@nutui/icons-react-taro'
import {Edit} from '@nutui/icons-react-taro'
import Taro from '@tarojs/taro'
import {useRouter} from '@tarojs/taro'
import {View,Text} from '@tarojs/components'
import {View} from '@tarojs/components'
import FixedButton from "@/components/FixedButton";
import {useUser} from "@/hooks/useUser";
import {ShopDealerApply} from "@/api/shop/shopDealerApply/model";
import {
addShopDealerApply, getShopDealerApply,
addShopDealerApply, getShopDealerApply, pageShopDealerApply,
updateShopDealerApply
} from "@/api/shop/shopDealerApply";
import {
formatDateForDatabase,
extractDateForCalendar,
formatDateForDisplay
extractDateForCalendar
} from "@/utils/dateUtils";
const AddShopDealerApply = () => {
@@ -47,7 +46,6 @@ const AddShopDealerApply = () => {
}
// 处理签约时间选择
const handleApplyTimeConfirm = (param: string) => {
const selectedDate = param[3] // 选中的日期字符串 (YYYY-M-D)
@@ -79,13 +77,13 @@ const AddShopDealerApply = () => {
}
const reload = async () => {
if(!params.id){
if (!params.id) {
return false;
}
// 查询当前用户ID是否已有申请记录
try {
const dealerApply = await getShopDealerApply(Number(params.id));
if(dealerApply){
if (dealerApply) {
setFormData(dealerApply)
setIsEditMode(true);
setExistingApply(dealerApply)
@@ -109,49 +107,60 @@ const AddShopDealerApply = () => {
}
// 提交表单
const submitSucceed = async (values: any) => {
try {
// 准备提交的数据
const submitData = {
...values,
realName: values.realName || user?.nickname,
mobile: user?.phone,
refereeId: 33534,
applyStatus: 10,
auditTime: undefined,
// 确保日期数据正确提交(使用数据库格式)
applyTime: values.applyTime || (applyTime ? formatDateForDatabase(applyTime) : ''),
contractTime: values.contractTime || (contractTime ? formatDateForDatabase(contractTime) : '')
};
const submitSucceed = (values: any) => {
// 如果是编辑模式添加现有申请的id
if (isEditMode && existingApply?.applyId) {
submitData.applyId = existingApply.applyId;
pageShopDealerApply({dealerName: values.dealerName, type: 4}).then((res) => {
if (res && res?.count > 0) {
Taro.showToast({
title: '该客户已存在',
icon: 'error'
});
return false;
}
try {
// 准备提交的数据
const submitData = {
...values,
type: 4,
realName: values.realName || user?.nickname,
mobile: user?.phone,
refereeId: 33534,
applyStatus: 10,
auditTime: undefined,
// 确保日期数据正确提交(使用数据库格式)
applyTime: values.applyTime || (applyTime ? formatDateForDatabase(applyTime) : ''),
contractTime: values.contractTime || (contractTime ? formatDateForDatabase(contractTime) : '')
};
// 执行新增或更新操作
if (isEditMode) {
await updateShopDealerApply(submitData);
} else {
await addShopDealerApply(submitData);
// 如果是编辑模式添加现有申请的id
if (isEditMode && existingApply?.applyId) {
submitData.applyId = existingApply.applyId;
}
// 执行新增或更新操作
if (isEditMode) {
updateShopDealerApply(submitData);
} else {
addShopDealerApply(submitData);
}
Taro.showToast({
title: `${isEditMode ? '提交' : '提交'}成功`,
icon: 'success'
});
setTimeout(() => {
Taro.navigateBack();
}, 1000);
} catch (error) {
console.error('验证邀请人失败:', error);
return Taro.showToast({
title: '邀请人ID不存在',
icon: 'error'
});
}
Taro.showToast({
title: `${isEditMode ? '提交' : '提交'}成功`,
icon: 'success'
});
setTimeout(() => {
Taro.navigateBack();
}, 1000);
} catch (error) {
console.error('验证邀请人失败:', error);
return Taro.showToast({
title: '邀请人ID不存在',
icon: 'error'
});
}
})
}
// 处理固定按钮点击事件
@@ -201,43 +210,43 @@ const AddShopDealerApply = () => {
<Form.Item name="dealerCode" label="户号" initialValue={FormData?.dealerCode} required>
<Input placeholder="请填写户号" disabled={isEditMode}/>
</Form.Item>
<Form.Item name="money" label="签约价格" initialValue={FormData?.money} required>
<Input placeholder="(元/兆瓦时)" disabled={false}/>
</Form.Item>
<Form.Item name="applyTime" label="签约时间" initialValue={FormData?.applyTime} required>
<View
className="flex items-center justify-between py-2"
onClick={() => !isEditMode && setShowApplyTimePicker(true)}
style={{
cursor: isEditMode ? 'not-allowed' : 'pointer',
opacity: isEditMode ? 0.6 : 1
}}
>
<View className="flex items-center">
<CalendarIcon size={16} color="#999" className="mr-2" />
<Text style={{ color: applyTime ? '#333' : '#999' }}>
{applyTime ? formatDateForDisplay(applyTime) : '请选择签约时间'}
</Text>
</View>
</View>
</Form.Item>
<Form.Item name="contractTime" label="合同日期" initialValue={FormData?.contractTime} required>
<View
className="flex items-center justify-between py-2"
onClick={() => !isEditMode && setShowContractTimePicker(true)}
style={{
cursor: isEditMode ? 'not-allowed' : 'pointer',
opacity: isEditMode ? 0.6 : 1
}}
>
<View className="flex items-center">
<CalendarIcon size={16} color="#999" className="mr-2" />
<Text style={{ color: contractTime ? '#333' : '#999' }}>
{contractTime ? formatDateForDisplay(contractTime) : '请选择合同生效起止时间'}
</Text>
</View>
</View>
</Form.Item>
{/*<Form.Item name="money" label="签约价格" initialValue={FormData?.money} required>*/}
{/* <Input placeholder="(元/兆瓦时)" disabled={false}/>*/}
{/*</Form.Item>*/}
{/*<Form.Item name="applyTime" label="签约时间" initialValue={FormData?.applyTime} required>*/}
{/* <View*/}
{/* className="flex items-center justify-between py-2"*/}
{/* onClick={() => !isEditMode && setShowApplyTimePicker(true)}*/}
{/* style={{*/}
{/* cursor: isEditMode ? 'not-allowed' : 'pointer',*/}
{/* opacity: isEditMode ? 0.6 : 1*/}
{/* }}*/}
{/* >*/}
{/* <View className="flex items-center">*/}
{/* <CalendarIcon size={16} color="#999" className="mr-2" />*/}
{/* <Text style={{ color: applyTime ? '#333' : '#999' }}>*/}
{/* {applyTime ? formatDateForDisplay(applyTime) : '请选择签约时间'}*/}
{/* </Text>*/}
{/* </View>*/}
{/* </View>*/}
{/*</Form.Item>*/}
{/*<Form.Item name="contractTime" label="合同日期" initialValue={FormData?.contractTime} required>*/}
{/* <View*/}
{/* className="flex items-center justify-between py-2"*/}
{/* onClick={() => !isEditMode && setShowContractTimePicker(true)}*/}
{/* style={{*/}
{/* cursor: isEditMode ? 'not-allowed' : 'pointer',*/}
{/* opacity: isEditMode ? 0.6 : 1*/}
{/* }}*/}
{/* >*/}
{/* <View className="flex items-center">*/}
{/* <CalendarIcon size={16} color="#999" className="mr-2" />*/}
{/* <Text style={{ color: contractTime ? '#333' : '#999' }}>*/}
{/* {contractTime ? formatDateForDisplay(contractTime) : '请选择合同生效起止时间'}*/}
{/* </Text>*/}
{/* </View>*/}
{/* </View>*/}
{/*</Form.Item>*/}
{/*<Form.Item name="refereeId" label="邀请人ID" initialValue={FormData?.refereeId} required>*/}
{/* <Input placeholder="邀请人ID"/>*/}
{/*</Form.Item>*/}

View File

@@ -41,7 +41,7 @@ const CustomerIndex = () => {
// 构建API参数根据状态筛选
const params: any = {
type: 0,
type: 4,
page: currentPage
};
const applyStatus = mapCustomerStatusToApplyStatus(statusFilter || activeTab);
@@ -126,10 +126,10 @@ const CustomerIndex = () => {
try {
// 并行获取各状态的数量
const [allRes, pendingRes, signedRes, cancelledRes] = await Promise.all([
pageShopDealerApply({type: 0}), // 全部
pageShopDealerApply({applyStatus: 10, type: 0}), // 跟进中
pageShopDealerApply({applyStatus: 20, type: 0}), // 已签约
pageShopDealerApply({applyStatus: 30, type: 0}) // 已取消
pageShopDealerApply({type: 4}), // 全部
pageShopDealerApply({applyStatus: 10, type: 4}), // 跟进中
pageShopDealerApply({applyStatus: 20, type: 4}), // 已签约
pageShopDealerApply({applyStatus: 30, type: 4}) // 已取消
]);
setStatusCounts({

View File

@@ -1,6 +1,7 @@
import React, {useState, useEffect, useCallback} from 'react'
import {View, Text} from '@tarojs/components'
import {Space,Empty, Avatar, Button} from '@nutui/nutui-react-taro'
import {Phone} from '@nutui/icons-react-taro'
import {Space, Empty, Avatar, Button} from '@nutui/nutui-react-taro'
import Taro from '@tarojs/taro'
import {useDealerUser} from '@/hooks/useDealerUser'
import {listShopDealerReferee} from '@/api/shop/shopDealerReferee'
@@ -41,6 +42,90 @@ const DealerTeam: React.FC = () => {
// 当前查看的用户名称
const [currentDealerName, setCurrentDealerName] = useState<string>('')
// 异步加载成员统计数据
const loadMemberStats = async (members: TeamMemberWithStats[]) => {
// 分批处理,避免过多并发请求
const batchSize = 3
for (let i = 0; i < members.length; i += batchSize) {
const batch = members.slice(i, i + batchSize)
const batchStats = await Promise.all(
batch.map(async (member) => {
try {
// 并行获取订单统计和下级成员数量
const [orderResult, subMembersResult] = await Promise.all([
pageShopDealerOrder({
page: 1,
userId: member.userId
}),
listShopDealerReferee({
dealerId: member.userId,
deleted: 0
})
])
let orderCount = 0
let commission = '0.00'
let status: 'active' | 'inactive' = 'inactive'
if (orderResult?.list) {
const orders = orderResult.list
orderCount = orders.length
commission = orders.reduce((sum, order) => {
const levelCommission = member.level === 1 ? order.firstMoney :
member.level === 2 ? order.secondMoney :
order.thirdMoney
return sum + parseFloat(levelCommission || '0')
}, 0).toFixed(2)
// 判断活跃状态30天内有订单为活跃
const thirtyDaysAgo = new Date()
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30)
const hasRecentOrder = orders.some(order =>
new Date(order.createTime || '') > thirtyDaysAgo
)
status = hasRecentOrder ? 'active' : 'inactive'
}
return {
...member,
orderCount,
commission,
status,
subMembers: subMembersResult?.length || 0
}
} catch (error) {
console.error(`获取成员${member.userId}数据失败:`, error)
return {
...member,
orderCount: 0,
commission: '0.00',
status: 'inactive' as const,
subMembers: 0
}
}
})
)
// 更新这一批成员的数据
setTeamMembers(prevMembers => {
const updatedMembers = [...prevMembers]
batchStats.forEach(updatedMember => {
const index = updatedMembers.findIndex(m => m.userId === updatedMember.userId)
if (index !== -1) {
updatedMembers[index] = updatedMember
}
})
return updatedMembers
})
// 添加小延迟,避免请求过于密集
if (i + batchSize < members.length) {
await new Promise(resolve => setTimeout(resolve, 100))
}
}
}
// 获取团队数据
const fetchTeamData = useCallback(async () => {
if (!dealerUser?.userId && !dealerId) return
@@ -54,6 +139,7 @@ const DealerTeam: React.FC = () => {
})
if (refereeResult) {
console.log('团队成员原始数据:', refereeResult)
// 处理团队成员数据
const processedMembers: TeamMemberWithStats[] = refereeResult.map(member => ({
...member,
@@ -65,60 +151,12 @@ const DealerTeam: React.FC = () => {
joinTime: member.createTime
}))
// 并行获取每个成员的订单统计和下级成员数量
const memberStats = await Promise.all(
processedMembers.map(async (member) => {
try {
// 获取订单统计
const orderResult = await pageShopDealerOrder({
page: 1,
userId: member.userId
})
// 先显示基础数据,然后异步加载详细统计
setTeamMembers(processedMembers)
setLoading(false)
// 获取下级成员数量
const subMembersResult = await listShopDealerReferee({
dealerId: member.userId,
deleted: 0
})
let orderCount = 0
let commission = '0.00'
let status: 'active' | 'inactive' = 'inactive'
if (orderResult?.list) {
const orders = orderResult.list
orderCount = orders.length
commission = orders.reduce((sum, order) => {
const levelCommission = member.level === 1 ? order.firstMoney :
member.level === 2 ? order.secondMoney :
order.thirdMoney
return sum + parseFloat(levelCommission || '0')
}, 0).toFixed(2)
// 判断活跃状态30天内有订单为活跃
const thirtyDaysAgo = new Date()
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30)
const hasRecentOrder = orders.some(order =>
new Date(order.createTime || '') > thirtyDaysAgo
)
status = hasRecentOrder ? 'active' : 'inactive'
}
return {
...member,
orderCount,
commission,
status,
subMembers: subMembersResult?.length || 0
}
} catch (error) {
console.error(`获取成员${member.userId}数据失败:`, error)
return member
}
})
)
setTeamMembers(memberStats)
// 异步加载每个成员的详细统计数据
loadMemberStats(processedMembers)
}
} catch (error) {
@@ -198,6 +236,10 @@ const DealerTeam: React.FC = () => {
const renderMemberItem = (member: TeamMemberWithStats) => {
// 判断是否可以点击:有下级成员且未达到层级限制
const canClick = member.subMembers && member.subMembers > 0 && levelStack.length < 1
// 判断是否显示手机号只有本级levelStack.length === 0才显示
const showPhone = levelStack.length === 0
// 判断数据是否还在加载中初始值都是0或'0.00'
const isStatsLoading = member.orderCount === 0 && member.commission === '0.00' && member.subMembers === 0
return (
<View
@@ -207,47 +249,56 @@ const DealerTeam: React.FC = () => {
}`}
onClick={() => getNextUser(member)}
>
<View className="flex items-center mb-3">
<Avatar
size="40"
src={member.avatar}
className="mr-3"
/>
<View className="flex-1">
<View className="flex items-center mb-1">
<Text className="font-semibold text-gray-800 mr-2">
{member.nickname}
<View className="flex items-center mb-3">
<Avatar
size="40"
src={member.avatar}
className="mr-3"
/>
<View className="flex-1">
<View className="flex items-center justify-between mb-1">
<View className="flex items-center">
<Text className="font-semibold text-gray-800 mr-2">
{member.nickname}
</Text>
</View>
{/* 显示手机号(仅本级可见) */}
{showPhone && member.phone && (
<Text className="text-sm text-gray-500">
{member.phone}
<Phone size={12} className="ml-1 text-green-500"/>
</Text>
)}
</View>
<Text className="text-xs text-gray-500">
{member.joinTime}
</Text>
</View>
<Text className="text-xs text-gray-500">
{member.joinTime}
</Text>
</View>
<View className="grid grid-cols-3 gap-4 text-center">
<Space>
<Text className="text-xs text-gray-500"></Text>
<Text className="text-sm font-semibold text-blue-600">
{isStatsLoading ? '-' : member.orderCount}
</Text>
</Space>
<Space>
<Text className="text-xs text-gray-500"></Text>
<Text className="text-sm font-semibold text-green-600">
{isStatsLoading ? '-' : `¥${member.commission}`}
</Text>
</Space>
<Space>
<Text className="text-xs text-gray-500"></Text>
<Text className={`text-sm font-semibold ${
canClick ? 'text-purple-600' : 'text-gray-400'
}`}>
{isStatsLoading ? '-' : (member.subMembers || 0)}
</Text>
</Space>
</View>
</View>
<View className="grid grid-cols-3 gap-4 text-center">
<Space>
<Text className="text-xs text-gray-500"></Text>
<Text className="text-sm font-semibold text-blue-600">
{member.orderCount}
</Text>
</Space>
<Space>
<Text className="text-xs text-gray-500"></Text>
<Text className="text-sm font-semibold text-green-600">
¥{member.commission}
</Text>
</Space>
<Space>
<Text className="text-xs text-gray-500"></Text>
<Text className={`text-sm font-semibold ${
canClick ? 'text-purple-600' : 'text-gray-400'
}`}>
{member.subMembers || 0}
</Text>
</Space>
</View>
</View>
)
}
@@ -287,7 +338,7 @@ const DealerTeam: React.FC = () => {
<Space className="flex items-center justify-center">
<Empty description="您还不是业务人员" style={{
backgroundColor: 'transparent'
}} actions={[{ text: '立即申请', onClick: () => navTo(`/dealer/apply/add`,true)}]}
}} actions={[{text: '立即申请', onClick: () => navTo(`/dealer/apply/add`, true)}]}
/>
</Space>
)

View File

@@ -1,6 +1,7 @@
import {useEffect, useState} from 'react'
import {View, Text, Image} from '@tarojs/components'
import {Tabs} from '@nutui/nutui-react-taro'
import Taro from '@tarojs/taro'
import './index.scss'
import {listCmsWebsiteField} from "@/api/cms/cmsWebsiteField";
import {CmsWebsiteField} from "@/api/cms/cmsWebsiteField/model";
@@ -9,15 +10,74 @@ const WechatService = () => {
const [activeTab, setActiveTab] = useState('0')
const [codes, setCodes] = useState<CmsWebsiteField[]>([])
// 长按保存二维码到相册
const saveQRCodeToAlbum = (imageUrl: string) => {
// 首先下载图片到本地
Taro.downloadFile({
url: imageUrl,
success: (res) => {
if (res.statusCode === 200) {
// 保存图片到相册
Taro.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: () => {
Taro.showToast({
title: '保存成功',
icon: 'success',
duration: 2000
})
},
fail: (error) => {
console.error('保存失败:', error)
if (error.errMsg.includes('auth deny')) {
Taro.showModal({
title: '提示',
content: '需要您授权保存图片到相册',
showCancel: true,
cancelText: '取消',
confirmText: '去设置',
success: (modalRes) => {
if (modalRes.confirm) {
Taro.openSetting()
}
}
})
} else {
Taro.showToast({
title: '保存失败',
icon: 'error',
duration: 2000
})
}
}
})
} else {
Taro.showToast({
title: '图片下载失败',
icon: 'error',
duration: 2000
})
}
},
fail: () => {
Taro.showToast({
title: '图片下载失败',
icon: 'error',
duration: 2000
})
}
})
}
const renderQRCode = (data: typeof codes[0]) => (
<View className="qr-container">
<View className="qr-content">
<View className="qr-code-wrapper">
<Image
src={`${data.value}`}
className="qr-code-image"
mode="aspectFit"
onLongPress={() => saveQRCodeToAlbum(`${data.value}`)}
/>
{data.style && <Text className="wechat-id">{data.style}</Text>}
</View>

View File

@@ -204,9 +204,9 @@ const DealerWithdraw: React.FC = () => {
return
}
if (amount < 10) {
if (amount < 100) {
Taro.showToast({
title: '最低提现金额为10元',
title: '最低提现金额为100元',
icon: 'error'
})
return
@@ -316,7 +316,7 @@ const DealerWithdraw: React.FC = () => {
borderTop: '1px solid rgba(255, 255, 255, 0.3)'
}}>
<Text className="text-white text-opacity-80 text-xs">
¥10
¥100
</Text>
</View>
</View>