feat(user): 实现扫码登录和推荐人绑定功能- 添加扫码登录相关API接口,包括生成二维码、检查状态、确认登录等
- 在用户注册时支持从邀请参数中获取推荐人ID并绑定 - 修改管理员面板UI,添加统一扫码功能入口- 更新用户管理相关API地址,统一使用SERVER_API_URL - 调整优惠券卡片样式,移除小程序不支持的CSS属性 - 添加聊天会话和消息管理相关API模块 - 新增分销商银行卡管理API接口 - 修改系统用户模型,增加推荐人ID字段 - 更新广告位查询接口,支持根据code获取广告位 - 调整邀请绑定接口参数,将refereeId改为dealerId - 修改环境配置中的应用名称为"时里院子市集" - 移除分享到朋友圈的相关代码 - 添加管理员面板组件,提供统一扫码等管理功能 -修复用户管理API请求参数传递问题 - 添加聊天消息和会话管理的完整CRUD接口 - 更新系统用户相关接口URL,确保正确调用后端服务 - 添加分销商银行卡管理的完整API接口实现 - 修改邀请绑定接口,使用dealerId替代refereeId参数 - 修复扫码登录相关API的URL拼接问题 - 添加二维码内容解析功能,支持多种格式的token提取 - 更新用户信息模型,增加推荐人ID字段 -优化管理员面板样式和交互逻辑- 调整优惠券组件样式,兼容小程序环境限制- 修复用户管理模块的API调用问题 - 添加聊天相关数据模型和接口定义 - 更新环境配置中的应用名称 -修复邀请绑定相关的参数传递问题- 添加扫码登录状态枚举和相关数据结构定义- 优化管理员功能面板的UI展示和交互体验- 修复系统用户管理接口的请求参数问题 - 添加分销商银行卡管理相关接口实现- 调整聊天消息和会话管理接口的数据结构定义 -修复用户管理模块中的API调用路径问题 - 添加扫码登录相关工具函数,如设备信息获取等 - 更新邀请绑定接口的数据模型定义 -优化管理员面板组件的样式和功能实现 -修复系统用户管理接口中的参数传递问题 - 添加聊天相关模块的完整API接口实现 - 调整分销商银行卡管理模块的数据结构定义- 修复扫码登录相关接口的URL拼接问题- 更新用户管理模块中的API调用方式 - 添加聊天消息批量发送等相关接口实现- 修复邀请绑定接口中的参数名称问题- 优化管理员面板组件的功能和交互逻辑 - 调整系统用户管理接口的请求参数传递方式 - 添加分销商银行卡管理模块的完整接口实现 -修复聊天相关接口中的数据结构问题 - 更新扫码登录相关接口的数据模型定义 - 优化管理员功能面板的展示效果和用户体验
This commit is contained in:
4
src/doctor/apply/add.config.ts
Normal file
4
src/doctor/apply/add.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '医生入驻申请通道',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
191
src/doctor/apply/add.tsx
Normal file
191
src/doctor/apply/add.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
import {useEffect, useState, useRef} from "react";
|
||||
import {Loading, CellGroup, Cell, Input, Form} from '@nutui/nutui-react-taro'
|
||||
import {Edit} from '@nutui/icons-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {View} from '@tarojs/components'
|
||||
import FixedButton from "@/components/FixedButton";
|
||||
import {useUser} from "@/hooks/useUser";
|
||||
import {ShopDealerApply} from "@/api/shop/shopDealerApply/model";
|
||||
import {
|
||||
addShopDealerApply,
|
||||
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 formRef = useRef<any>(null)
|
||||
const [isEditMode, setIsEditMode] = useState<boolean>(false)
|
||||
const [existingApply, setExistingApply] = useState<ShopDealerApply | null>(null)
|
||||
|
||||
// 获取审核状态文字
|
||||
const getApplyStatusText = (status?: number) => {
|
||||
switch (status) {
|
||||
case 10:
|
||||
return '待审核'
|
||||
case 20:
|
||||
return '审核通过'
|
||||
case 30:
|
||||
return '驳回'
|
||||
default:
|
||||
return '未知状态'
|
||||
}
|
||||
}
|
||||
|
||||
const reload = async () => {
|
||||
// 判断用户是否登录
|
||||
if (!user?.userId) {
|
||||
return false;
|
||||
}
|
||||
// 查询当前用户ID是否已有申请记录
|
||||
try {
|
||||
const res = await pageShopDealerApply({userId: user?.userId});
|
||||
if (res && res.count > 0) {
|
||||
setIsEditMode(true);
|
||||
setExistingApply(res.list[0]);
|
||||
// 如果有记录,填充表单数据
|
||||
setFormData(res.list[0]);
|
||||
setLoading(false)
|
||||
} else {
|
||||
setIsEditMode(false);
|
||||
setExistingApply(null);
|
||||
setLoading(false)
|
||||
}
|
||||
} catch (error) {
|
||||
setLoading(true)
|
||||
console.error('查询申请记录失败:', error);
|
||||
setIsEditMode(false);
|
||||
setExistingApply(null);
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const submitSucceed = async (values: any) => {
|
||||
try {
|
||||
|
||||
// 准备提交的数据
|
||||
const submitData = {
|
||||
...values,
|
||||
realName: values.realName || user?.nickname,
|
||||
mobile: user?.phone,
|
||||
refereeId: values.refereeId || FormData?.refereeId,
|
||||
applyStatus: 10,
|
||||
auditTime: undefined
|
||||
};
|
||||
await getShopDealerUser(submitData.refereeId);
|
||||
|
||||
// 如果是编辑模式,添加现有申请的id
|
||||
if (isEditMode && existingApply?.applyId) {
|
||||
submitData.applyId = existingApply.applyId;
|
||||
}
|
||||
|
||||
// 执行新增或更新操作
|
||||
if (isEditMode) {
|
||||
await updateShopDealerApply(submitData);
|
||||
} else {
|
||||
await addShopDealerApply(submitData);
|
||||
}
|
||||
|
||||
Taro.showToast({
|
||||
title: `${isEditMode ? '提交' : '提交'}成功`,
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
Taro.navigateBack();
|
||||
}, 1000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('验证邀请人失败:', error);
|
||||
return Taro.showToast({
|
||||
title: '邀请人ID不存在',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 处理固定按钮点击事件
|
||||
const handleFixedButtonClick = () => {
|
||||
// 触发表单提交
|
||||
formRef.current?.submit();
|
||||
};
|
||||
|
||||
const submitFailed = (error: any) => {
|
||||
console.log(error, 'err...')
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reload().then(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}, [user?.userId]); // 依赖用户ID,当用户变化时重新加载
|
||||
|
||||
if (loading) {
|
||||
return <Loading className={'px-2'}>加载中</Loading>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form
|
||||
ref={formRef}
|
||||
divider
|
||||
initialValues={FormData}
|
||||
labelPosition="left"
|
||||
onFinish={(values) => submitSucceed(values)}
|
||||
onFinishFailed={(errors) => submitFailed(errors)}
|
||||
>
|
||||
<View className={'bg-gray-100 h-3'}></View>
|
||||
<CellGroup style={{padding: '4px 0'}}>
|
||||
<Form.Item name="realName" label="名称" initialValue={user?.nickname} required>
|
||||
<Input placeholder="经销商名称" maxLength={10}/>
|
||||
</Form.Item>
|
||||
<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>
|
||||
</CellGroup>
|
||||
</Form>
|
||||
{/* 审核状态显示(仅在编辑模式下显示) */}
|
||||
{isEditMode && (
|
||||
<CellGroup>
|
||||
<Cell
|
||||
title={'审核状态'}
|
||||
extra={
|
||||
<span style={{
|
||||
color: FormData?.applyStatus === 20 ? '#52c41a' :
|
||||
FormData?.applyStatus === 30 ? '#ff4d4f' : '#faad14'
|
||||
}}>
|
||||
{getApplyStatusText(FormData?.applyStatus)}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
{FormData?.applyStatus === 20 && (
|
||||
<Cell title={'审核时间'} extra={FormData?.auditTime || '无'}/>
|
||||
)}
|
||||
{FormData?.applyStatus === 30 && (
|
||||
<Cell title={'驳回原因'} extra={FormData?.rejectReason || '无'}/>
|
||||
)}
|
||||
</CellGroup>
|
||||
)}
|
||||
|
||||
|
||||
{/* 底部浮动按钮 */}
|
||||
{(!isEditMode || FormData?.applyStatus === 10 || FormData?.applyStatus === 30) && (
|
||||
<FixedButton
|
||||
icon={<Edit/>}
|
||||
text={isEditMode ? '保存修改' : '提交申请'}
|
||||
disabled={FormData?.applyStatus === 10}
|
||||
onClick={handleFixedButtonClick}
|
||||
/>
|
||||
)}
|
||||
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddUserAddress;
|
||||
3
src/doctor/index.config.ts
Normal file
3
src/doctor/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '医生版'
|
||||
})
|
||||
0
src/doctor/index.scss
Normal file
0
src/doctor/index.scss
Normal file
295
src/doctor/index.tsx
Normal file
295
src/doctor/index.tsx
Normal file
@@ -0,0 +1,295 @@
|
||||
import React from 'react'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import {ConfigProvider, Button, Grid, Avatar} from '@nutui/nutui-react-taro'
|
||||
import {
|
||||
User,
|
||||
Shopping,
|
||||
Dongdong,
|
||||
ArrowRight,
|
||||
Purse,
|
||||
People
|
||||
} from '@nutui/icons-react-taro'
|
||||
import {useDealerUser} from '@/hooks/useDealerUser'
|
||||
import { useThemeStyles } from '@/hooks/useTheme'
|
||||
import {businessGradients, cardGradients, gradientUtils} from '@/styles/gradients'
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
const DealerIndex: React.FC = () => {
|
||||
const {
|
||||
dealerUser,
|
||||
error,
|
||||
refresh,
|
||||
} = useDealerUser()
|
||||
|
||||
// 使用主题样式
|
||||
const themeStyles = useThemeStyles()
|
||||
|
||||
// 导航到各个功能页面
|
||||
const navigateToPage = (url: string) => {
|
||||
Taro.navigateTo({url})
|
||||
}
|
||||
|
||||
// 格式化金额
|
||||
const formatMoney = (money?: string) => {
|
||||
if (!money) return '0.00'
|
||||
return parseFloat(money).toFixed(2)
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time?: string) => {
|
||||
if (!time) return '-'
|
||||
return new Date(time).toLocaleDateString()
|
||||
}
|
||||
|
||||
// 获取用户主题
|
||||
const userTheme = gradientUtils.getThemeByUserId(dealerUser?.userId)
|
||||
|
||||
// 获取渐变背景
|
||||
const getGradientBackground = (themeColor?: string) => {
|
||||
if (themeColor) {
|
||||
const darkerColor = gradientUtils.adjustColorBrightness(themeColor, -30)
|
||||
return gradientUtils.createGradient(themeColor, darkerColor)
|
||||
}
|
||||
return userTheme.background
|
||||
}
|
||||
|
||||
console.log(getGradientBackground(),'getGradientBackground()')
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<View className="p-4">
|
||||
<View className="bg-red-50 border border-red-200 rounded-lg p-4 mb-4">
|
||||
<Text className="text-red-600">{error}</Text>
|
||||
</View>
|
||||
<Button type="primary" onClick={refresh}>
|
||||
重试
|
||||
</Button>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="bg-gray-100 min-h-screen">
|
||||
<View>
|
||||
{/*头部信息*/}
|
||||
{dealerUser && (
|
||||
<View className="px-4 py-6 relative overflow-hidden" style={themeStyles.primaryBackground}>
|
||||
{/* 装饰性背景元素 - 小程序兼容版本 */}
|
||||
<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-24 h-24 rounded-full" style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.08)',
|
||||
bottom: '-12px',
|
||||
left: '-12px'
|
||||
}}></View>
|
||||
<View className="absolute w-16 h-16 rounded-full" style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||
top: '60px',
|
||||
left: '120px'
|
||||
}}></View>
|
||||
<View className="flex items-center justify-between relative z-10 mb-4">
|
||||
<Avatar
|
||||
size="50"
|
||||
src={dealerUser?.qrcode}
|
||||
icon={<User/>}
|
||||
className="mr-4"
|
||||
style={{
|
||||
border: '2px solid rgba(255, 255, 255, 0.3)'
|
||||
}}
|
||||
/>
|
||||
<View className="flex-1 flex-col">
|
||||
<View className="text-white text-lg font-bold mb-1" style={{
|
||||
}}>
|
||||
{dealerUser?.realName || '分销商'}
|
||||
</View>
|
||||
<View className="text-sm" style={{
|
||||
color: 'rgba(255, 255, 255, 0.8)'
|
||||
}}>
|
||||
ID: {dealerUser.userId} | 推荐人: {dealerUser.refereeId || '无'}
|
||||
</View>
|
||||
</View>
|
||||
<View className="text-right hidden">
|
||||
<Text className="text-xs" style={{
|
||||
color: 'rgba(255, 255, 255, 0.9)'
|
||||
}}>加入时间</Text>
|
||||
<Text className="text-xs" style={{
|
||||
color: 'rgba(255, 255, 255, 0.7)'
|
||||
}}>
|
||||
{formatTime(dealerUser.createTime)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 佣金统计卡片 */}
|
||||
{dealerUser && (
|
||||
<View className="mx-4 -mt-6 rounded-xl p-4 relative z-10" style={cardGradients.elevated}>
|
||||
<View className="mb-4">
|
||||
<Text className="font-semibold text-gray-800">佣金统计</Text>
|
||||
</View>
|
||||
<View className="grid grid-cols-3 gap-4">
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>累计收益</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 团队统计 */}
|
||||
{dealerUser && (
|
||||
<View className="bg-white mx-4 mt-4 rounded-xl p-4 hidden">
|
||||
<View className="flex items-center justify-between mb-4">
|
||||
<Text className="font-semibold text-gray-800">我的邀请</Text>
|
||||
<View
|
||||
className="text-gray-400 text-sm flex items-center"
|
||||
onClick={() => navigateToPage('/dealer/team/index')}
|
||||
>
|
||||
<Text>查看详情</Text>
|
||||
<ArrowRight size="12"/>
|
||||
</View>
|
||||
</View>
|
||||
<View className="grid grid-cols-3 gap-4">
|
||||
<View className="text-center grid">
|
||||
<Text className="text-xl font-bold text-purple-500 mb-1">
|
||||
{dealerUser.firstNum || 0}
|
||||
</Text>
|
||||
<Text className="text-xs text-gray-500">一级成员</Text>
|
||||
</View>
|
||||
<View className="text-center grid">
|
||||
<Text className="text-xl font-bold text-indigo-500 mb-1">
|
||||
{dealerUser.secondNum || 0}
|
||||
</Text>
|
||||
<Text className="text-xs text-gray-500">二级成员</Text>
|
||||
</View>
|
||||
<View className="text-center grid">
|
||||
<Text className="text-xl font-bold text-pink-500 mb-1">
|
||||
{dealerUser.thirdNum || 0}
|
||||
</Text>
|
||||
<Text className="text-xs text-gray-500">三级成员</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 功能导航 */}
|
||||
<View className="bg-white mx-4 mt-4 rounded-xl p-4">
|
||||
<View className="font-semibold mb-4 text-gray-800">分销工具</View>
|
||||
<ConfigProvider>
|
||||
<Grid
|
||||
columns={4}
|
||||
className="no-border-grid"
|
||||
style={{
|
||||
'--nutui-grid-border-color': 'transparent',
|
||||
'--nutui-grid-item-border-width': '0px',
|
||||
border: 'none'
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
<Grid.Item text="分销订单" onClick={() => navigateToPage('/dealer/orders/index')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Shopping color="#3b82f6" size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text={'提现申请'} onClick={() => navigateToPage('/dealer/withdraw/index')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-green-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Purse color="#10b981" size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text={'我的邀请'} onClick={() => navigateToPage('/dealer/team/index')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-purple-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<People color="#8b5cf6" size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text={'我的邀请码'} onClick={() => navigateToPage('/dealer/qrcode/index')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-orange-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Dongdong color="#f59e0b" size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
</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.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>*/}
|
||||
</ConfigProvider>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 底部安全区域 */}
|
||||
<View className="h-20"></View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default DealerIndex
|
||||
157
src/doctor/info.tsx
Normal file
157
src/doctor/info.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
import React from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Button, Cell, CellGroup, Tag } from '@nutui/nutui-react-taro'
|
||||
import { useDealerUser } from '@/hooks/useDealerUser'
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
const DealerInfo: React.FC = () => {
|
||||
const {
|
||||
dealerUser,
|
||||
loading,
|
||||
error,
|
||||
refresh,
|
||||
} = useDealerUser()
|
||||
|
||||
// 跳转到申请页面
|
||||
const navigateToApply = () => {
|
||||
Taro.navigateTo({
|
||||
url: '/pages/dealer/apply/add'
|
||||
})
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<View className="p-4">
|
||||
<View className="bg-red-50 border border-red-200 rounded-lg p-4 mb-4">
|
||||
<Text className="text-red-600">{error}</Text>
|
||||
</View>
|
||||
<Button type="primary" onClick={refresh}>
|
||||
重试
|
||||
</Button>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen">
|
||||
{/* 页面标题 */}
|
||||
<View className="bg-white px-4 py-3 border-b border-gray-100">
|
||||
<Text className="text-lg font-bold text-center">
|
||||
经销商信息
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{!dealerUser ? (
|
||||
// 非经销商状态
|
||||
<View className="bg-white mx-4 mt-4 rounded-lg p-6">
|
||||
<View className="text-center py-8">
|
||||
<Text className="text-gray-500 mb-4">您还不是经销商</Text>
|
||||
<Text className="text-sm text-gray-400 mb-6">
|
||||
成为经销商后可享受专属价格和佣金收益
|
||||
</Text>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
onClick={navigateToApply}
|
||||
>
|
||||
申请成为经销商
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
// 经销商信息展示
|
||||
<View>
|
||||
{/* 状态卡片 */}
|
||||
<View className="bg-white mx-4 mt-4 rounded-lg p-4">
|
||||
<View className="flex items-center justify-between mb-4">
|
||||
<Text className="text-lg font-semibold">经销商状态</Text>
|
||||
<Tag>
|
||||
{dealerUser.realName}
|
||||
</Tag>
|
||||
</View>
|
||||
|
||||
{/* 基本信息 */}
|
||||
<CellGroup>
|
||||
<Cell
|
||||
title="经销商ID"
|
||||
extra={dealerUser.userId || '-'}
|
||||
/>
|
||||
<Cell
|
||||
title="refereeId"
|
||||
extra={dealerUser.refereeId || '-'}
|
||||
/>
|
||||
<Cell
|
||||
title="成为经销商时间"
|
||||
extra={
|
||||
dealerUser.money
|
||||
}
|
||||
/>
|
||||
|
||||
</CellGroup>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<View className="mt-6 gap-2">
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
loading={loading}
|
||||
>
|
||||
检查状态
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 经销商权益 */}
|
||||
<View className="bg-white mx-4 mt-4 rounded-lg p-4">
|
||||
<Text className="font-semibold mb-3">经销商权益</Text>
|
||||
<View className="gap-2">
|
||||
<Text className="text-sm text-gray-600">
|
||||
• 享受经销商专属价格
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-600">
|
||||
• 获得推广佣金收益
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-600">
|
||||
• 优先获得新品信息
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-600">
|
||||
• 专属客服支持
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 佣金统计 */}
|
||||
<View className="bg-white mx-4 mt-4 rounded-lg p-4">
|
||||
<Text className="font-semibold mb-3">佣金统计</Text>
|
||||
<View className="grid grid-cols-3 gap-4">
|
||||
<View className="text-center">
|
||||
<Text className="text-lg font-bold text-blue-600">0</Text>
|
||||
<Text className="text-sm text-gray-500">今日佣金</Text>
|
||||
</View>
|
||||
<View className="text-center">
|
||||
<Text className="text-lg font-bold text-green-600">0</Text>
|
||||
<Text className="text-sm text-gray-500">本月佣金</Text>
|
||||
</View>
|
||||
<View className="text-center">
|
||||
<Text className="text-lg font-bold text-orange-600">0</Text>
|
||||
<Text className="text-sm text-gray-500">累计佣金</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 刷新按钮 */}
|
||||
<View className="text-center py-4">
|
||||
<Text
|
||||
className="text-blue-500 text-sm"
|
||||
onClick={refresh}
|
||||
>
|
||||
点击刷新数据
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default DealerInfo
|
||||
7
src/doctor/invite-stats/index.config.ts
Normal file
7
src/doctor/invite-stats/index.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '邀请统计',
|
||||
navigationBarBackgroundColor: '#ffffff',
|
||||
navigationBarTextStyle: 'black',
|
||||
backgroundColor: '#f5f5f5',
|
||||
enablePullDownRefresh: true
|
||||
})
|
||||
336
src/doctor/invite-stats/index.tsx
Normal file
336
src/doctor/invite-stats/index.tsx
Normal file
@@ -0,0 +1,336 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import {
|
||||
Empty,
|
||||
Tabs,
|
||||
Loading,
|
||||
PullToRefresh,
|
||||
Card,
|
||||
} from '@nutui/nutui-react-taro'
|
||||
import {
|
||||
User,
|
||||
ArrowUp,
|
||||
Calendar,
|
||||
Share,
|
||||
Target,
|
||||
Gift
|
||||
} from '@nutui/icons-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useDealerUser } from '@/hooks/useDealerUser'
|
||||
import {
|
||||
getInviteStats,
|
||||
getMyInviteRecords,
|
||||
getInviteRanking
|
||||
} from '@/api/invite'
|
||||
import type {
|
||||
InviteStats,
|
||||
InviteRecord
|
||||
} from '@/api/invite'
|
||||
import { businessGradients } from '@/styles/gradients'
|
||||
import {InviteRanking} from "@/api/invite/model";
|
||||
|
||||
const InviteStatsPage: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState<string>('stats')
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [inviteStats, setInviteStats] = useState<InviteStats | null>(null)
|
||||
const [inviteRecords, setInviteRecords] = useState<InviteRecord[]>([])
|
||||
const [ranking, setRanking] = useState<InviteRanking[]>([])
|
||||
const [dateRange, setDateRange] = useState<string>('month')
|
||||
const { dealerUser } = useDealerUser()
|
||||
|
||||
// 获取邀请统计数据
|
||||
const fetchInviteStats = useCallback(async () => {
|
||||
if (!dealerUser?.userId) return
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
const stats = await getInviteStats(dealerUser.userId)
|
||||
stats && setInviteStats(stats)
|
||||
} catch (error) {
|
||||
console.error('获取邀请统计失败:', error)
|
||||
Taro.showToast({
|
||||
title: '获取统计数据失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [dealerUser?.userId])
|
||||
|
||||
// 获取邀请记录
|
||||
const fetchInviteRecords = useCallback(async () => {
|
||||
if (!dealerUser?.userId) return
|
||||
|
||||
try {
|
||||
const result = await getMyInviteRecords({
|
||||
page: 1,
|
||||
limit: 50,
|
||||
inviterId: dealerUser.userId
|
||||
})
|
||||
setInviteRecords(result?.list || [])
|
||||
} catch (error) {
|
||||
console.error('获取邀请记录失败:', error)
|
||||
}
|
||||
}, [dealerUser?.userId])
|
||||
|
||||
// 获取邀请排行榜
|
||||
const fetchRanking = useCallback(async () => {
|
||||
try {
|
||||
const result = await getInviteRanking({
|
||||
limit: 20,
|
||||
period: dateRange as 'day' | 'week' | 'month'
|
||||
})
|
||||
setRanking(result || [])
|
||||
} catch (error) {
|
||||
console.error('获取排行榜失败:', error)
|
||||
}
|
||||
}, [dateRange])
|
||||
|
||||
// 刷新数据
|
||||
const handleRefresh = async () => {
|
||||
await Promise.all([
|
||||
fetchInviteStats(),
|
||||
fetchInviteRecords(),
|
||||
fetchRanking()
|
||||
])
|
||||
}
|
||||
|
||||
// 初始化数据
|
||||
useEffect(() => {
|
||||
if (dealerUser?.userId) {
|
||||
fetchInviteStats().then()
|
||||
fetchInviteRecords().then()
|
||||
fetchRanking().then()
|
||||
}
|
||||
}, [fetchInviteStats, fetchInviteRecords, fetchRanking])
|
||||
|
||||
// 获取状态显示文本
|
||||
const getStatusText = (status: string) => {
|
||||
const statusMap: Record<string, string> = {
|
||||
'pending': '待注册',
|
||||
'registered': '已注册',
|
||||
'activated': '已激活'
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
'pending': 'text-orange-500',
|
||||
'registered': 'text-blue-500',
|
||||
'activated': 'text-green-500'
|
||||
}
|
||||
return colorMap[status] || 'text-gray-500'
|
||||
}
|
||||
|
||||
// 渲染统计概览
|
||||
const renderStatsOverview = () => (
|
||||
<View className="px-4 space-y-4">
|
||||
{/* 核心数据卡片 */}
|
||||
<Card className="bg-white rounded-2xl shadow-sm">
|
||||
<View className="p-4">
|
||||
<Text className="text-lg font-semibold text-gray-800 mb-4">邀请概览</Text>
|
||||
{loading ? (
|
||||
<View className="flex items-center justify-center py-8">
|
||||
<Loading />
|
||||
</View>
|
||||
) : inviteStats ? (
|
||||
<View className="grid grid-cols-2 gap-4">
|
||||
<View className="text-center p-4 bg-blue-50 rounded-xl">
|
||||
<ArrowUp size="24" className="text-blue-500 mx-auto mb-2" />
|
||||
<Text className="text-2xl font-bold text-blue-600">
|
||||
{inviteStats.totalInvites || 0}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-600">总邀请数</Text>
|
||||
</View>
|
||||
|
||||
<View className="text-center p-4 bg-green-50 rounded-xl">
|
||||
<User size="24" className="text-green-500 mx-auto mb-2" />
|
||||
<Text className="text-2xl font-bold text-green-600">
|
||||
{inviteStats.successfulRegistrations || 0}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-600">成功注册</Text>
|
||||
</View>
|
||||
|
||||
<View className="text-center p-4 bg-purple-50 rounded-xl">
|
||||
<Target size="24" className="text-purple-500 mx-auto mb-2" />
|
||||
<Text className="text-2xl font-bold text-purple-600">
|
||||
{inviteStats.conversionRate ? `${(inviteStats.conversionRate * 100).toFixed(1)}%` : '0%'}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-600">转化率</Text>
|
||||
</View>
|
||||
|
||||
<View className="text-center p-4 bg-orange-50 rounded-xl">
|
||||
<Calendar size="24" className="text-orange-500 mx-auto mb-2" />
|
||||
<Text className="text-2xl font-bold text-orange-600">
|
||||
{inviteStats.todayInvites || 0}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-600">今日邀请</Text>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<View className="text-center py-8">
|
||||
<Text className="text-gray-500">暂无统计数据</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</Card>
|
||||
|
||||
{/* 邀请来源分析 */}
|
||||
{inviteStats?.sourceStats && inviteStats.sourceStats.length > 0 && (
|
||||
<Card className="bg-white rounded-2xl shadow-sm">
|
||||
<View className="p-4">
|
||||
<Text className="text-lg font-semibold text-gray-800 mb-4">邀请来源分析</Text>
|
||||
<View className="space-y-3">
|
||||
{inviteStats.sourceStats.map((source, index) => (
|
||||
<View key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||
<View className="flex items-center">
|
||||
<Share size="16" className="text-blue-500 mr-2" />
|
||||
<Text className="font-medium text-gray-800">{source.source}</Text>
|
||||
</View>
|
||||
<View className="text-right">
|
||||
<Text className="text-lg font-bold text-gray-800">{source.count}</Text>
|
||||
<Text className="text-sm text-gray-500">
|
||||
转化率 {source.conversionRate ? `${(source.conversionRate * 100).toFixed(1)}%` : '0%'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</Card>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
|
||||
// 渲染邀请记录
|
||||
const renderInviteRecords = () => (
|
||||
<View className="px-4">
|
||||
{inviteRecords.length > 0 ? (
|
||||
<View className="space-y-3">
|
||||
{inviteRecords.map((record, index) => (
|
||||
<Card key={record.id || index} className="bg-white rounded-xl shadow-sm">
|
||||
<View className="p-4">
|
||||
<View className="flex items-center justify-between mb-2">
|
||||
<Text className="font-medium text-gray-800">
|
||||
{record.inviteeName || `用户${record.inviteeId}`}
|
||||
</Text>
|
||||
<Text className={`text-sm font-medium ${getStatusColor(record.status || 'pending')}`}>
|
||||
{getStatusText(record.status || 'pending')}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className="flex items-center justify-between text-sm text-gray-500">
|
||||
<Text>来源: {record.source || '未知'}</Text>
|
||||
<Text>{record.inviteTime ? new Date(record.inviteTime).toLocaleDateString() : ''}</Text>
|
||||
</View>
|
||||
|
||||
{record.registerTime && (
|
||||
<Text className="text-xs text-green-600 mt-1">
|
||||
注册时间: {new Date(record.registerTime).toLocaleString()}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</Card>
|
||||
))}
|
||||
</View>
|
||||
) : (
|
||||
<Empty description="暂无邀请记录" />
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
|
||||
// 渲染排行榜
|
||||
const renderRanking = () => (
|
||||
<View className="px-4">
|
||||
<View className="mb-4">
|
||||
<Tabs value={dateRange} onChange={() => setDateRange}>
|
||||
<Tabs.TabPane title="日榜" value="day" />
|
||||
<Tabs.TabPane title="周榜" value="week" />
|
||||
<Tabs.TabPane title="月榜" value="month" />
|
||||
</Tabs>
|
||||
</View>
|
||||
|
||||
{ranking.length > 0 ? (
|
||||
<View className="space-y-3">
|
||||
{ranking.map((item, index) => (
|
||||
<Card key={item.inviterId} className="bg-white rounded-xl shadow-sm">
|
||||
<View className="p-4 flex items-center">
|
||||
<View className="flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 mr-3">
|
||||
{index < 3 ? (
|
||||
<Gift size="16" className={index === 0 ? 'text-yellow-500' : index === 1 ? 'text-gray-400' : 'text-orange-400'} />
|
||||
) : (
|
||||
<Text className="text-sm font-bold text-gray-600">{index + 1}</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View className="flex-1">
|
||||
<Text className="font-medium text-gray-800">{item.inviterName}</Text>
|
||||
<Text className="text-sm text-gray-500">
|
||||
邀请 {item.inviteCount} 人 · 转化率 {item.conversionRate ? `${(item.conversionRate * 100).toFixed(1)}%` : '0%'}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Text className="text-lg font-bold text-blue-600">{item.successCount}</Text>
|
||||
</View>
|
||||
</Card>
|
||||
))}
|
||||
</View>
|
||||
) : (
|
||||
<Empty description="暂无排行数据" />
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
|
||||
if (!dealerUser) {
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
||||
<Loading />
|
||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen">
|
||||
{/* 头部 */}
|
||||
<View className="rounded-b-3xl p-6 mb-6 text-white relative overflow-hidden" style={{
|
||||
background: businessGradients.dealer.header
|
||||
}}>
|
||||
<View className="absolute w-32 h-32 rounded-full" style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
top: '-16px',
|
||||
right: '-16px'
|
||||
}}></View>
|
||||
|
||||
<View className="relative z-10">
|
||||
<Text className="text-2xl font-bold mb-2 text-white">邀请统计</Text>
|
||||
<Text className="text-white text-opacity-80">
|
||||
查看您的邀请效果和推广数据
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 标签页 */}
|
||||
<View className="px-4 mb-4">
|
||||
<Tabs value={activeTab} onChange={() => setActiveTab}>
|
||||
<Tabs.TabPane title="统计概览" value="stats" />
|
||||
<Tabs.TabPane title="邀请记录" value="records" />
|
||||
<Tabs.TabPane title="排行榜" value="ranking" />
|
||||
</Tabs>
|
||||
</View>
|
||||
|
||||
{/* 内容区域 */}
|
||||
<PullToRefresh onRefresh={handleRefresh}>
|
||||
<View className="pb-6">
|
||||
{activeTab === 'stats' && renderStatsOverview()}
|
||||
{activeTab === 'records' && renderInviteRecords()}
|
||||
{activeTab === 'ranking' && renderRanking()}
|
||||
</View>
|
||||
</PullToRefresh>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default InviteStatsPage
|
||||
3
src/doctor/orders/index.config.ts
Normal file
3
src/doctor/orders/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '分销订单'
|
||||
})
|
||||
392
src/doctor/orders/index.tsx
Normal file
392
src/doctor/orders/index.tsx
Normal file
@@ -0,0 +1,392 @@
|
||||
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 Taro from '@tarojs/taro'
|
||||
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 [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 { dealerUser } = useDealerUser()
|
||||
|
||||
// 获取订单数据 - 查询当前用户作为各层级分销商的所有订单
|
||||
const fetchOrders = useCallback(async () => {
|
||||
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 (level1Result?.list) {
|
||||
const level1Orders = level1Result.list.map(order => ({
|
||||
...order,
|
||||
orderNo: `DD${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)
|
||||
}))
|
||||
|
||||
allOrders.push(...level1Orders)
|
||||
stats.level1.orders = level1Orders.length
|
||||
stats.level1.commission = level1Orders.reduce((sum, order) =>
|
||||
sum + parseFloat(order.userCommission || '0'), 0
|
||||
).toFixed(2)
|
||||
}
|
||||
|
||||
// 处理二级分销订单
|
||||
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({
|
||||
title: '获取订单失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [dealerUser?.userId])
|
||||
|
||||
// 刷新数据
|
||||
const handleRefresh = async () => {
|
||||
await fetchOrders()
|
||||
}
|
||||
|
||||
// 初始化加载数据
|
||||
useEffect(() => {
|
||||
if (dealerUser?.userId) {
|
||||
fetchOrders().then()
|
||||
}
|
||||
}, [fetchOrders])
|
||||
|
||||
const getStatusText = (isSettled?: number, isInvalid?: number) => {
|
||||
if (isInvalid === 1) return '已失效'
|
||||
if (isSettled === 1) return '已结算'
|
||||
return '待结算'
|
||||
}
|
||||
|
||||
const getStatusColor = (isSettled?: number, isInvalid?: number) => {
|
||||
if (isInvalid === 1) return 'danger'
|
||||
if (isSettled === 1) return 'success'
|
||||
return 'warning'
|
||||
}
|
||||
|
||||
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>
|
||||
<Tag type={getStatusColor(order.isSettled, order.isInvalid)}>
|
||||
{getStatusText(order.isSettled, order.isInvalid)}
|
||||
</Tag>
|
||||
</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">
|
||||
{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 />
|
||||
<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="p-4">
|
||||
{getFilteredOrders('1').length > 0 ? (
|
||||
getFilteredOrders('1').map(renderOrderItem)
|
||||
) : (
|
||||
<Empty description="暂无一级分销订单" />
|
||||
)}
|
||||
</View>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane title="二级分销" value="2">
|
||||
<View className="p-4">
|
||||
{getFilteredOrders('2').length > 0 ? (
|
||||
getFilteredOrders('2').map(renderOrderItem)
|
||||
) : (
|
||||
<Empty description="暂无二级分销订单" />
|
||||
)}
|
||||
</View>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane title="三级分销" value="3">
|
||||
<View className="p-4">
|
||||
{getFilteredOrders('3').length > 0 ? (
|
||||
getFilteredOrders('3').map(renderOrderItem)
|
||||
) : (
|
||||
<Empty description="暂无三级分销订单" />
|
||||
)}
|
||||
</View>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane title="待结算" value="4">
|
||||
<View className="p-4">
|
||||
{getFilteredOrders('4').length > 0 ? (
|
||||
getFilteredOrders('4').map(renderOrderItem)
|
||||
) : (
|
||||
<Empty description="暂无待结算订单" />
|
||||
)}
|
||||
</View>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane title="已结算" value="5">
|
||||
<View className="p-4">
|
||||
{getFilteredOrders('5').length > 0 ? (
|
||||
getFilteredOrders('5').map(renderOrderItem)
|
||||
) : (
|
||||
<Empty description="暂无已结算订单" />
|
||||
)}
|
||||
</View>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane title="已失效" value="6">
|
||||
<View className="p-4">
|
||||
{getFilteredOrders('6').length > 0 ? (
|
||||
getFilteredOrders('6').map(renderOrderItem)
|
||||
) : (
|
||||
<Empty description="暂无失效订单" />
|
||||
)}
|
||||
</View>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default DealerOrders
|
||||
3
src/doctor/qrcode/index.config.ts
Normal file
3
src/doctor/qrcode/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '推广二维码'
|
||||
})
|
||||
398
src/doctor/qrcode/index.tsx
Normal file
398
src/doctor/qrcode/index.tsx
Normal file
@@ -0,0 +1,398 @@
|
||||
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} 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 generateMiniProgramCode = async () => {
|
||||
if (!dealerUser?.userId) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
|
||||
// 生成邀请小程序码
|
||||
const codeUrl = await generateInviteCode(dealerUser.userId)
|
||||
|
||||
if (codeUrl) {
|
||||
setMiniProgramCodeUrl(codeUrl)
|
||||
} else {
|
||||
throw new Error('返回的小程序码URL为空')
|
||||
}
|
||||
} catch (error: any) {
|
||||
Taro.showToast({
|
||||
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) {
|
||||
// // 静默处理错误,不影响用户体验
|
||||
// } finally {
|
||||
// setStatsLoading(false)
|
||||
// }
|
||||
// }
|
||||
|
||||
// 初始化生成小程序码和获取统计数据
|
||||
useEffect(() => {
|
||||
if (dealerUser?.userId) {
|
||||
generateMiniProgramCode()
|
||||
// fetchInviteStats()
|
||||
}
|
||||
}, [dealerUser?.userId])
|
||||
|
||||
// 保存小程序码到相册
|
||||
const saveMiniProgramCode = async () => {
|
||||
if (!miniProgramCodeUrl) {
|
||||
Taro.showToast({
|
||||
title: '小程序码未生成',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 先下载图片到本地
|
||||
const res = await Taro.downloadFile({
|
||||
url: miniProgramCodeUrl
|
||||
})
|
||||
|
||||
if (res.statusCode === 200) {
|
||||
// 保存到相册
|
||||
await Taro.saveImageToPhotosAlbum({
|
||||
filePath: res.tempFilePath
|
||||
})
|
||||
|
||||
Taro.showToast({
|
||||
title: '保存成功',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.errMsg?.includes('auth deny')) {
|
||||
Taro.showModal({
|
||||
title: '提示',
|
||||
content: '需要您授权保存图片到相册',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
Taro.openSetting()
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Taro.showToast({
|
||||
title: '保存失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 复制邀请信息
|
||||
const copyInviteInfo = () => {
|
||||
if (!dealerUser?.userId) {
|
||||
Taro.showToast({
|
||||
title: '用户信息未加载',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const inviteText = `🎉 邀请您加入我的团队!
|
||||
|
||||
扫描小程序码或搜索"通源堂健康生态平台"小程序,即可享受优质商品和服务!
|
||||
|
||||
💰 成为我的团队成员,一起赚取丰厚佣金
|
||||
🎁 新用户专享优惠等你来拿
|
||||
|
||||
邀请码:${dealerUser.userId}
|
||||
快来加入我们吧!`
|
||||
|
||||
Taro.setClipboardData({
|
||||
data: inviteText,
|
||||
success: () => {
|
||||
Taro.showToast({
|
||||
title: '邀请信息已复制',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 分享小程序码
|
||||
const shareMiniProgramCode = () => {
|
||||
if (!dealerUser?.userId) {
|
||||
Taro.showToast({
|
||||
title: '用户信息未加载',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 小程序分享
|
||||
Taro.showShareMenu({
|
||||
withShareTicket: true,
|
||||
showShareItems: ['shareAppMessage', 'shareTimeline']
|
||||
})
|
||||
}
|
||||
|
||||
if (!dealerUser) {
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
||||
<Loading/>
|
||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen">
|
||||
{/* 头部卡片 */}
|
||||
<View className="rounded-b-3xl p-6 mb-6 text-white relative overflow-hidden" style={{
|
||||
background: businessGradients.dealer.header
|
||||
}}>
|
||||
{/* 装饰背景 */}
|
||||
<View className="absolute w-32 h-32 rounded-full" style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
top: '-16px',
|
||||
right: '-16px'
|
||||
}}></View>
|
||||
|
||||
<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">
|
||||
分享小程序码邀请好友,获得丰厚佣金奖励
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="px-4">
|
||||
{/* 小程序码展示区 */}
|
||||
<View className="bg-white rounded-2xl p-6 mb-6 shadow-sm">
|
||||
<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/>
|
||||
<Text className="text-gray-500 mt-2">生成中...</Text>
|
||||
</View>
|
||||
) : miniProgramCodeUrl ? (
|
||||
<View className="w-48 h-48 mx-auto mb-4 bg-white rounded-xl shadow-sm p-4">
|
||||
<Image
|
||||
src={miniProgramCodeUrl}
|
||||
className="w-full h-full"
|
||||
mode="aspectFit"
|
||||
onError={() => {
|
||||
Taro.showModal({
|
||||
title: '二维码加载失败',
|
||||
content: '请检查网络连接或联系管理员',
|
||||
showCancel: true,
|
||||
confirmText: '重新生成',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
generateMiniProgramCode();
|
||||
}
|
||||
}
|
||||
});
|
||||
}}
|
||||
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<View className="w-48 h-48 mx-auto mb-4 flex items-center justify-center bg-gray-50 rounded-xl">
|
||||
<QrCode size="48" className="text-gray-400 mb-2"/>
|
||||
<Text className="text-gray-500">小程序码生成失败</Text>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
className="mt-2"
|
||||
onClick={generateMiniProgramCode}
|
||||
>
|
||||
重新生成
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View className="text-lg font-semibold text-gray-800 mb-2">
|
||||
扫码加入我的团队
|
||||
</View>
|
||||
<View className="text-sm text-gray-500 mb-4">
|
||||
好友扫描小程序码即可直接进入小程序并建立邀请关系
|
||||
</View>
|
||||
|
||||
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<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 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">
|
||||
<View className="w-2 h-2 bg-green-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">
|
||||
<View className="w-2 h-2 bg-purple-500 rounded-full mt-2 mr-3 flex-shrink-0"></View>
|
||||
<Text className="text-sm text-gray-600">
|
||||
支持三级分销,团队越大收益越多
|
||||
</Text>
|
||||
</View>
|
||||
</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>*/}
|
||||
|
||||
{/* /!* 邀请来源统计 *!/*/}
|
||||
{/* {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>
|
||||
)
|
||||
}
|
||||
|
||||
export default DealerQrcode
|
||||
3
src/doctor/team/index.config.ts
Normal file
3
src/doctor/team/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '我的团队'
|
||||
})
|
||||
367
src/doctor/team/index.tsx
Normal file
367
src/doctor/team/index.tsx
Normal file
@@ -0,0 +1,367 @@
|
||||
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 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'
|
||||
|
||||
interface TeamMemberWithStats extends ShopDealerReferee {
|
||||
name?: string
|
||||
avatar?: string
|
||||
orderCount?: number
|
||||
commission?: string
|
||||
status?: 'active' | 'inactive'
|
||||
subMembers?: number
|
||||
joinTime?: string
|
||||
}
|
||||
|
||||
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 fetchTeamData = useCallback(async () => {
|
||||
if (!dealerUser?.userId) return
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
|
||||
// 获取团队成员关系
|
||||
const refereeResult = await listShopDealerReferee({
|
||||
dealerId: dealerUser.userId
|
||||
})
|
||||
|
||||
if (refereeResult) {
|
||||
// 处理团队成员数据
|
||||
const processedMembers: TeamMemberWithStats[] = refereeResult.map(member => ({
|
||||
...member,
|
||||
name: `用户${member.userId}`,
|
||||
avatar: '',
|
||||
orderCount: 0,
|
||||
commission: '0.00',
|
||||
status: 'active' as const,
|
||||
subMembers: 0,
|
||||
joinTime: member.createTime
|
||||
}))
|
||||
|
||||
// 并行获取每个成员的订单统计
|
||||
const memberStats = await Promise.all(
|
||||
processedMembers.map(async (member) => {
|
||||
try {
|
||||
const orderResult = await pageShopDealerOrder({
|
||||
page: 1,
|
||||
limit: 100,
|
||||
userId: member.userId
|
||||
})
|
||||
|
||||
if (orderResult?.list) {
|
||||
const orders = orderResult.list
|
||||
const orderCount = orders.length
|
||||
const 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
|
||||
)
|
||||
|
||||
return {
|
||||
...member,
|
||||
orderCount,
|
||||
commission,
|
||||
status: hasRecentOrder ? 'active' as const : 'inactive' as const
|
||||
}
|
||||
}
|
||||
return member
|
||||
} catch (error) {
|
||||
console.error(`获取成员${member.userId}订单失败:`, error)
|
||||
return member
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
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)
|
||||
Taro.showToast({
|
||||
title: '获取团队数据失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [dealerUser?.userId])
|
||||
|
||||
// 刷新数据
|
||||
const handleRefresh = async () => {
|
||||
setRefreshing(true)
|
||||
await fetchTeamData()
|
||||
setRefreshing(false)
|
||||
}
|
||||
|
||||
// 初始化加载数据
|
||||
useEffect(() => {
|
||||
if (dealerUser?.userId) {
|
||||
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 className="flex items-center mb-3">
|
||||
<Avatar
|
||||
size="40"
|
||||
src={member.avatar}
|
||||
icon={<User />}
|
||||
className="mr-3"
|
||||
/>
|
||||
<View className="flex-1">
|
||||
<View className="flex items-center mb-1">
|
||||
<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>
|
||||
</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>
|
||||
|
||||
<View className="grid grid-cols-3 gap-4 text-center">
|
||||
<View>
|
||||
<Text className="text-sm font-semibold text-blue-600">
|
||||
{member.orderCount}
|
||||
</Text>
|
||||
<Text className="text-xs text-gray-500">订单数</Text>
|
||||
</View>
|
||||
<View>
|
||||
<Text className="text-sm font-semibold text-green-600">
|
||||
¥{member.commission}
|
||||
</Text>
|
||||
<Text className="text-xs text-gray-500">贡献佣金</Text>
|
||||
</View>
|
||||
<View>
|
||||
<Text className="text-sm font-semibold text-purple-600">
|
||||
{member.subMembers}
|
||||
</Text>
|
||||
<Text className="text-xs text-gray-500">团队成员</Text>
|
||||
</View>
|
||||
</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>
|
||||
)
|
||||
|
||||
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 />
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
export default DealerTeam
|
||||
3
src/doctor/withdraw/index.config.ts
Normal file
3
src/doctor/withdraw/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '提现申请'
|
||||
})
|
||||
424
src/doctor/withdraw/index.tsx
Normal file
424
src/doctor/withdraw/index.tsx
Normal file
@@ -0,0 +1,424 @@
|
||||
import React, { useState, useRef, useEffect, useCallback } from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import {
|
||||
Cell,
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
CellGroup,
|
||||
Radio,
|
||||
Tabs,
|
||||
Tag,
|
||||
Empty,
|
||||
Loading,
|
||||
PullToRefresh
|
||||
} from '@nutui/nutui-react-taro'
|
||||
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'
|
||||
|
||||
interface WithdrawRecordWithDetails extends ShopDealerWithdraw {
|
||||
accountDisplay?: string
|
||||
}
|
||||
|
||||
const DealerWithdraw: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState('0')
|
||||
const [selectedAccount, setSelectedAccount] = useState('')
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [refreshing, setRefreshing] = useState<boolean>(false)
|
||||
const [submitting, setSubmitting] = useState<boolean>(false)
|
||||
const [availableAmount, setAvailableAmount] = useState<string>('0.00')
|
||||
const [withdrawRecords, setWithdrawRecords] = useState<WithdrawRecordWithDetails[]>([])
|
||||
const formRef = useRef<any>(null)
|
||||
|
||||
const { dealerUser } = useDealerUser()
|
||||
|
||||
// 获取可提现余额
|
||||
const fetchBalance = useCallback(async () => {
|
||||
try {
|
||||
setAvailableAmount(dealerUser?.money || '0.00')
|
||||
} catch (error) {
|
||||
console.error('获取余额失败:', error)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 获取提现记录
|
||||
const fetchWithdrawRecords = useCallback(async () => {
|
||||
if (!dealerUser?.userId) return
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
const result = await pageShopDealerWithdraw({
|
||||
page: 1,
|
||||
limit: 100,
|
||||
userId: dealerUser.userId
|
||||
})
|
||||
|
||||
if (result?.list) {
|
||||
const processedRecords = result.list.map(record => ({
|
||||
...record,
|
||||
accountDisplay: getAccountDisplay(record)
|
||||
}))
|
||||
setWithdrawRecords(processedRecords)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取提现记录失败:', error)
|
||||
Taro.showToast({
|
||||
title: '获取提现记录失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [dealerUser?.userId])
|
||||
|
||||
// 格式化账户显示
|
||||
const getAccountDisplay = (record: ShopDealerWithdraw) => {
|
||||
if (record.payType === 10) {
|
||||
return '微信钱包'
|
||||
} else if (record.payType === 20 && record.alipayAccount) {
|
||||
return `支付宝(${record.alipayAccount.slice(-4)})`
|
||||
} else if (record.payType === 30 && record.bankCard) {
|
||||
return `${record.bankName || '银行卡'}(尾号${record.bankCard.slice(-4)})`
|
||||
}
|
||||
return '未知账户'
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
const handleRefresh = async () => {
|
||||
setRefreshing(true)
|
||||
await Promise.all([fetchBalance(), fetchWithdrawRecords()])
|
||||
setRefreshing(false)
|
||||
}
|
||||
|
||||
// 初始化加载数据
|
||||
useEffect(() => {
|
||||
if (dealerUser?.userId) {
|
||||
fetchBalance().then()
|
||||
fetchWithdrawRecords().then()
|
||||
}
|
||||
}, [fetchBalance, fetchWithdrawRecords])
|
||||
|
||||
const getStatusText = (status?: number) => {
|
||||
switch (status) {
|
||||
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'
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async (values: any) => {
|
||||
if (!dealerUser?.userId) {
|
||||
Taro.showToast({
|
||||
title: '用户信息获取失败',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证提现金额
|
||||
const amount = parseFloat(values.amount)
|
||||
const available = parseFloat(availableAmount.replace(',', ''))
|
||||
|
||||
if (amount < 100) {
|
||||
Taro.showToast({
|
||||
title: '最低提现金额为100元',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (amount > available) {
|
||||
Taro.showToast({
|
||||
title: '提现金额超过可用余额',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setSubmitting(true)
|
||||
|
||||
const withdrawData: ShopDealerWithdraw = {
|
||||
userId: dealerUser.userId,
|
||||
money: values.amount,
|
||||
payType: values.accountType === 'wechat' ? 10 :
|
||||
values.accountType === 'alipay' ? 20 : 30,
|
||||
applyStatus: 10, // 待审核
|
||||
platform: 'MiniProgram'
|
||||
}
|
||||
|
||||
// 根据提现方式设置账户信息
|
||||
if (values.accountType === 'alipay') {
|
||||
withdrawData.alipayAccount = values.account
|
||||
withdrawData.alipayName = values.accountName
|
||||
} else if (values.accountType === 'bank') {
|
||||
withdrawData.bankCard = values.account
|
||||
withdrawData.bankAccount = values.accountName
|
||||
withdrawData.bankName = values.bankName || '银行卡'
|
||||
}
|
||||
|
||||
await addShopDealerWithdraw(withdrawData)
|
||||
|
||||
Taro.showToast({
|
||||
title: '提现申请已提交',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 重置表单
|
||||
formRef.current?.resetFields()
|
||||
setSelectedAccount('')
|
||||
|
||||
// 刷新数据
|
||||
await handleRefresh()
|
||||
|
||||
// 切换到提现记录页面
|
||||
setActiveTab('1')
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('提现申请失败:', error)
|
||||
Taro.showToast({
|
||||
title: error.message || '提现申请失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const quickAmounts = ['100', '300', '500', '1000']
|
||||
|
||||
const setQuickAmount = (amount: string) => {
|
||||
formRef.current?.setFieldsValue({ amount })
|
||||
}
|
||||
|
||||
const setAllAmount = () => {
|
||||
formRef.current?.setFieldsValue({ amount: availableAmount.replace(',', '') })
|
||||
}
|
||||
|
||||
const renderWithdrawForm = () => (
|
||||
<View className="p-4">
|
||||
{/* 余额卡片 */}
|
||||
<View className="rounded-xl p-6 mb-6 text-white relative overflow-hidden" style={{
|
||||
background: businessGradients.dealer.header
|
||||
}}>
|
||||
{/* 装饰背景 - 小程序兼容版本 */}
|
||||
<View className="absolute top-0 right-0 w-24 h-24 rounded-full" style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
right: '-12px',
|
||||
top: '-12px'
|
||||
}}></View>
|
||||
|
||||
<View className="flex items-center justify-between relative z-10">
|
||||
<View>
|
||||
<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" />
|
||||
</View>
|
||||
</View>
|
||||
<View className="mt-4 pt-4 relative z-10" style={{
|
||||
borderTop: '1px solid rgba(255, 255, 255, 0.3)'
|
||||
}}>
|
||||
<Text className="text-white text-opacity-80 text-xs">
|
||||
最低提现金额:¥100 | 手续费:免费
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Form
|
||||
ref={formRef}
|
||||
onFinish={handleSubmit}
|
||||
labelPosition="top"
|
||||
>
|
||||
<CellGroup>
|
||||
<Form.Item name="amount" label="提现金额" required>
|
||||
<Input
|
||||
placeholder="请输入提现金额"
|
||||
type="number"
|
||||
clearable
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 快捷金额 */}
|
||||
<View className="px-4 py-2">
|
||||
<Text className="text-sm text-gray-600 mb-2">快捷金额</Text>
|
||||
<View className="flex flex-wrap gap-2">
|
||||
{quickAmounts.map(amount => (
|
||||
<Button
|
||||
key={amount}
|
||||
size="small"
|
||||
fill="outline"
|
||||
onClick={() => setQuickAmount(amount)}
|
||||
>
|
||||
{amount}
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
size="small"
|
||||
fill="outline"
|
||||
onClick={setAllAmount}
|
||||
>
|
||||
全部
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Form.Item name="accountType" label="提现方式" required>
|
||||
<Radio.Group value={selectedAccount} onChange={() => setSelectedAccount}>
|
||||
<Cell.Group>
|
||||
<Cell>
|
||||
<Radio value="wechat">微信钱包</Radio>
|
||||
</Cell>
|
||||
<Cell>
|
||||
<Radio value="alipay">支付宝</Radio>
|
||||
</Cell>
|
||||
<Cell>
|
||||
<Radio value="bank">银行卡</Radio>
|
||||
</Cell>
|
||||
</Cell.Group>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
{selectedAccount === 'alipay' && (
|
||||
<>
|
||||
<Form.Item name="account" label="支付宝账号" required>
|
||||
<Input placeholder="请输入支付宝账号" />
|
||||
</Form.Item>
|
||||
<Form.Item name="accountName" label="支付宝姓名" required>
|
||||
<Input placeholder="请输入支付宝实名姓名" />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedAccount === 'bank' && (
|
||||
<>
|
||||
<Form.Item name="bankName" label="开户银行" required>
|
||||
<Input placeholder="请输入开户银行名称" />
|
||||
</Form.Item>
|
||||
<Form.Item name="account" label="银行卡号" required>
|
||||
<Input placeholder="请输入银行卡号" />
|
||||
</Form.Item>
|
||||
<Form.Item name="accountName" label="开户姓名" required>
|
||||
<Input placeholder="请输入银行卡开户姓名" />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedAccount === 'wechat' && (
|
||||
<View className="px-4 py-2">
|
||||
<Text className="text-sm text-gray-500">
|
||||
微信钱包提现将直接转入您的微信零钱
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</CellGroup>
|
||||
|
||||
<View className="mt-6 px-4">
|
||||
<Button
|
||||
block
|
||||
type="primary"
|
||||
nativeType="submit"
|
||||
loading={submitting}
|
||||
disabled={submitting || !selectedAccount}
|
||||
>
|
||||
{submitting ? '提交中...' : '申请提现'}
|
||||
</Button>
|
||||
</View>
|
||||
</Form>
|
||||
</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>
|
||||
|
||||
<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 />
|
||||
<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">
|
||||
{renderWithdrawForm()}
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane title="提现记录" value="1">
|
||||
{renderWithdrawRecords()}
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default DealerWithdraw
|
||||
Reference in New Issue
Block a user