feat(branding): 更新应用名称及时里院子市集相关引用

- 将应用名称从"时里院子市集"更新为"通源堂健康生态平台"
- 修改了config/env.ts中的生产环境应用名称配置
- 更新了src/cms/category/index.tsx中的分享标题引用
- 调整了src/admin/components/UserCell.tsx中的导航路径从/dealer/index到/doctor/index
This commit is contained in:
2025-09-28 14:36:24 +08:00
parent a6b274d78d
commit 0a517b1247
72 changed files with 2140 additions and 4196 deletions

View File

@@ -1,4 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '医生入驻申请通道',
navigationBarTitleText: '邀请注册',
navigationBarTextStyle: 'black'
})

View File

@@ -1,96 +1,210 @@
import {useEffect, useState, useRef} from "react";
import {Loading, CellGroup, Cell, Input, Form} from '@nutui/nutui-react-taro'
import {Loading, CellGroup, Input, Form, Avatar, Button, Space} 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";
import {TenantId} from "@/config/app";
import {updateUser} from "@/api/system/user";
import {User} from "@/api/system/user/model";
import {getStoredInviteParams, handleInviteRelation} from "@/utils/invite";
import {addShopDealerUser} from "@/api/shop/shopDealerUser";
import {listUserRole, updateUserRole} from "@/api/system/userRole";
// 类型定义
interface ChooseAvatarEvent {
detail: {
avatarUrl: string;
};
}
interface InputEvent {
detail: {
value: string;
};
}
const AddUserAddress = () => {
const {user} = useUser()
const {user, loginUser} = useUser()
const [loading, setLoading] = useState<boolean>(true)
const [FormData, setFormData] = useState<ShopDealerApply>()
const [FormData, setFormData] = useState<User>()
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 () => {
const inviteParams = getStoredInviteParams()
if (inviteParams?.inviter) {
setFormData({
...user,
refereeId: Number(inviteParams.inviter),
// 清空昵称,强制用户手动输入
nickname: '',
})
} else {
// 如果没有邀请参数,也要确保昵称为空
setFormData({
...user,
nickname: '',
})
}
}
const reload = async () => {
// 判断用户是否登录
if (!user?.userId) {
return false;
const uploadAvatar = ({detail}: ChooseAvatarEvent) => {
// 先更新本地显示的头像(临时显示)
const tempFormData = {
...FormData,
avatar: `${detail.avatarUrl}`,
}
// 查询当前用户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)
setFormData(tempFormData)
Taro.uploadFile({
url: 'https://server.websoft.top/api/oss/upload',
filePath: detail.avatarUrl,
name: 'file',
header: {
'content-type': 'application/json',
TenantId
},
success: async (res) => {
const data = JSON.parse(res.data);
if (data.code === 0) {
const finalAvatarUrl = `${data.data.thumbnail}`
try {
// 使用 useUser hook 的 updateUser 方法更新头像
await updateUser({
avatar: finalAvatarUrl
})
Taro.showToast({
title: '头像上传成功',
icon: 'success',
duration: 1500
})
} catch (error) {
console.error('更新用户头像失败:', error)
}
// 无论用户信息更新是否成功都要更新本地FormData
const finalFormData = {
...tempFormData,
avatar: finalAvatarUrl
}
setFormData(finalFormData)
// 同步更新表单字段
if (formRef.current) {
formRef.current.setFieldsValue({
avatar: finalAvatarUrl
})
}
} else {
// 上传失败,恢复原来的头像
setFormData({
...FormData,
avatar: user?.avatar || ''
})
Taro.showToast({
title: '上传失败',
icon: 'error'
})
}
},
fail: (error) => {
console.error('上传头像失败:', error)
Taro.showToast({
title: '上传失败',
icon: 'error'
})
// 恢复原来的头像
setFormData({
...FormData,
avatar: user?.avatar || ''
})
}
} catch (error) {
setLoading(true)
console.error('查询申请记录失败:', error);
setIsEditMode(false);
setExistingApply(null);
}
})
}
// 提交表单
const submitSucceed = async (values: any) => {
try {
// 验证必填字段
if (!values.phone && !FormData?.phone) {
Taro.showToast({
title: '请先获取手机号',
icon: 'error'
});
return;
}
// 验证昵称:必须填写且不能是默认的微信昵称
const nickname = values.realName || FormData?.nickname || '';
if (!nickname || nickname.trim() === '') {
Taro.showToast({
title: '请填写昵称',
icon: 'error'
});
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: '请上传头像',
icon: 'error'
});
return;
}
console.log(values,FormData)
const roles = await listUserRole({userId: user?.userId})
console.log(roles, 'roles...')
// 准备提交的数据
const submitData = {
...values,
realName: values.realName || user?.nickname,
mobile: user?.phone,
refereeId: values.refereeId || FormData?.refereeId,
applyStatus: 10,
auditTime: undefined
};
await getShopDealerUser(submitData.refereeId);
await updateUser({
userId: user?.userId,
nickname: values.realName || FormData?.nickname,
phone: values.phone || FormData?.phone,
avatar: values.avatar || FormData?.avatar,
refereeId: values.refereeId || FormData?.refereeId
});
// 如果是编辑模式添加现有申请的id
if (isEditMode && existingApply?.applyId) {
submitData.applyId = existingApply.applyId;
await addShopDealerUser({
userId: user?.userId,
realName: values.realName || FormData?.nickname,
mobile: values.phone || FormData?.phone,
refereeId: values.refereeId || FormData?.refereeId
})
if (roles.length > 0) {
await updateUserRole({
...roles[0],
roleId: 1848
})
}
// 执行新增或更新操作
if (isEditMode) {
await updateShopDealerApply(submitData);
} else {
await addShopDealerApply(submitData);
}
Taro.showToast({
title: `${isEditMode ? '提交' : '提交'}成功`,
title: `注册成功`,
icon: 'success'
});
@@ -100,13 +214,130 @@ const AddUserAddress = () => {
} catch (error) {
console.error('验证邀请人失败:', error);
return Taro.showToast({
title: '邀请人ID不存在',
icon: 'error'
});
}
}
// 获取微信昵称
const getWxNickname = (nickname: string) => {
// 更新表单数据
const updatedFormData = {
...FormData,
nickname: nickname
}
setFormData(updatedFormData);
// 同步更新表单字段
if (formRef.current) {
formRef.current.setFieldsValue({
realName: nickname
})
}
}
/* 获取用户手机号 */
const handleGetPhoneNumber = ({detail}: { detail: { code?: string, encryptedData?: string, iv?: string } }) => {
const {code, encryptedData, iv} = detail
Taro.login({
success: (loginRes) => {
if (code) {
Taro.request({
url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone',
method: 'POST',
data: {
authCode: loginRes.code,
code,
encryptedData,
iv,
notVerifyPhone: true,
refereeId: 0,
sceneType: 'save_referee',
tenantId: TenantId
},
header: {
'content-type': 'application/json',
TenantId
},
success: async function (res) {
if (res.data.code == 1) {
Taro.showToast({
title: res.data.message,
icon: 'error',
duration: 2000
})
return false;
}
// 登录成功
const token = res.data.data.access_token;
const userData = res.data.data.user;
console.log(userData, 'userData...')
// 使用useUser Hook的loginUser方法更新状态
loginUser(token, userData);
if (userData.phone) {
console.log('手机号已获取', userData.phone)
const updatedFormData = {
...FormData,
phone: userData.phone,
// 不自动填充微信昵称,保持用户已输入的昵称
nickname: FormData?.nickname || '',
// 只在没有头像时才使用微信头像
avatar: FormData?.avatar || userData.avatar
}
setFormData(updatedFormData)
// 更新表单字段值
if (formRef.current) {
formRef.current.setFieldsValue({
phone: userData.phone,
// 不覆盖用户已输入的昵称
realName: FormData?.nickname || '',
avatar: FormData?.avatar || userData.avatar
})
}
Taro.showToast({
title: '手机号获取成功',
icon: 'success',
duration: 1500
})
}
// 处理邀请关系
if (userData?.userId) {
try {
const inviteSuccess = await handleInviteRelation(userData.userId)
if (inviteSuccess) {
Taro.showToast({
title: '邀请关系建立成功',
icon: 'success',
duration: 2000
})
}
} catch (error) {
console.error('处理邀请关系失败:', error)
}
}
// 显示登录成功提示
// Taro.showToast({
// title: '注册成功',
// icon: 'success',
// duration: 1500
// })
// 不需要重新启动小程序状态已经通过useUser更新
// 可以选择性地刷新当前页面数据
// await reload();
}
})
} else {
console.log('登录失败!')
}
}
})
}
// 处理固定按钮点击事件
const handleFixedButtonClick = () => {
// 触发表单提交
@@ -123,6 +354,18 @@ const AddUserAddress = () => {
})
}, [user?.userId]); // 依赖用户ID当用户变化时重新加载
// 当FormData变化时同步更新表单字段值
useEffect(() => {
if (formRef.current && FormData) {
formRef.current.setFieldsValue({
refereeId: FormData.refereeId,
phone: FormData.phone,
avatar: FormData.avatar,
realName: FormData.nickname
});
}
}, [FormData]);
if (loading) {
return <Loading className={'px-2'}></Loading>
}
@@ -139,50 +382,49 @@ const AddUserAddress = () => {
>
<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"/>
<Input placeholder="邀请人ID" disabled={true}/>
</Form.Item>
<Form.Item name="phone" label="手机号" initialValue={FormData?.phone} required>
<View className="flex items-center justify-between">
<Input
placeholder="请填写手机号"
disabled={true}
maxLength={11}
value={FormData?.phone || ''}
/>
<Button style={{color: '#ffffff'}} open-type="getPhoneNumber" onGetPhoneNumber={handleGetPhoneNumber}>
<Space>
<Button size="small"></Button>
</Space>
</Button>
</View>
</Form.Item>
{
FormData?.phone && <Form.Item name="avatar" label="头像" initialValue={FormData?.avatar} required>
<Button open-type="chooseAvatar" style={{height: '58px'}} onChooseAvatar={uploadAvatar}>
<Avatar src={FormData?.avatar || user?.avatar} size="54"/>
</Button>
</Form.Item>
}
<Form.Item name="realName" label="昵称" initialValue="" required>
<Input
type="nickname"
className="info-content__input"
placeholder="请获取微信昵称"
value={FormData?.nickname || ''}
onInput={(e: InputEvent) => getWxNickname(e.detail.value)}
/>
</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}
/>
)}
<FixedButton
icon={<Edit/>}
text={'立即注册'}
onClick={handleFixedButtonClick}
/>
</>
);

View File

@@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '添加银行卡',
navigationBarTextStyle: 'black'
})

142
src/doctor/bank/add.tsx Normal file
View File

@@ -0,0 +1,142 @@
import {useEffect, useState, useRef} from "react";
import {useRouter} from '@tarojs/taro'
import {Loading, CellGroup, Input, Form} from '@nutui/nutui-react-taro'
import Taro from '@tarojs/taro'
import {
getShopDealerBank,
listShopDealerBank,
updateShopDealerBank,
addShopDealerBank
} from "@/api/shop/shopDealerBank";
import FixedButton from "@/components/FixedButton";
import {ShopDealerBank} from "@/api/shop/shopDealerBank/model";
const AddUserAddress = () => {
const {params} = useRouter();
const [loading, setLoading] = useState<boolean>(true)
const [FormData, setFormData] = useState<ShopDealerBank>()
const formRef = useRef<any>(null)
// 判断是编辑还是新增模式
const isEditMode = !!params.id
const bankId = params.id ? Number(params.id) : undefined
const reload = async () => {
// 如果是编辑模式,加载地址数据
if (isEditMode && bankId) {
try {
const bank = await getShopDealerBank(bankId)
setFormData(bank)
} catch (error) {
console.error('加载地址失败:', error)
Taro.showToast({
title: '加载地址失败',
icon: 'error'
});
}
}
}
// 提交表单
const submitSucceed = async (values: any) => {
console.log('.>>>>>>,....')
try {
// 准备提交的数据
const submitData = {
...values,
isDefault: true // 新增或编辑的地址都设为默认地址
};
console.log('提交数据:', submitData)
// 如果是编辑模式添加id
if (isEditMode && bankId) {
submitData.id = bankId;
}
// 先处理默认地址逻辑
const defaultAddress = await listShopDealerBank({isDefault: true});
if (defaultAddress && defaultAddress.length > 0) {
// 如果当前编辑的不是默认地址,或者是新增地址,需要取消其他默认地址
if (!isEditMode || (isEditMode && defaultAddress[0].id !== bankId)) {
await updateShopDealerBank({
...defaultAddress[0],
isDefault: false
});
}
}
// 执行新增或更新操作
if (isEditMode) {
await updateShopDealerBank(submitData);
} else {
await addShopDealerBank(submitData);
}
Taro.showToast({
title: `${isEditMode ? '更新' : '保存'}成功`,
icon: 'success'
});
setTimeout(() => {
Taro.navigateBack();
}, 1000);
} catch (error) {
console.error('保存失败:', error);
Taro.showToast({
title: `${isEditMode ? '更新' : '保存'}失败`,
icon: 'error'
});
}
}
const submitFailed = (error: any) => {
console.log(error, 'err...')
}
useEffect(() => {
// 动态设置页面标题
Taro.setNavigationBarTitle({
title: isEditMode ? '编辑银行卡' : '添加银行卡'
});
reload().then(() => {
setLoading(false)
})
}, [isEditMode]);
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)}
>
<CellGroup style={{padding: '4px 0'}}>
<Form.Item name="bankName" label="开户行名称" initialValue={FormData?.bankName} required>
<Input placeholder="开户行名称" maxLength={10}/>
</Form.Item>
<Form.Item name="bankAccount" label="银行开户名" initialValue={FormData?.bankAccount} required>
<Input placeholder="银行开户名" maxLength={10}/>
</Form.Item>
<Form.Item name="bankCard" label="银行卡号" initialValue={FormData?.bankCard} required>
<Input placeholder="银行卡号" maxLength={11}/>
</Form.Item>
</CellGroup>
</Form>
{/* 底部浮动按钮 */}
<FixedButton text={isEditMode ? '更新地址' : '保存并使用'} onClick={() => formRef.current?.submit()}/>
</>
);
};
export default AddUserAddress;

View File

@@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '银行卡管理',
navigationBarTextStyle: 'black'
})

134
src/doctor/bank/index.tsx Normal file
View File

@@ -0,0 +1,134 @@
import {useState} from "react";
import Taro, {useDidShow} from '@tarojs/taro'
import {Button, Cell, Space, Empty, ConfigProvider} from '@nutui/nutui-react-taro'
import {CheckNormal, Checked} from '@nutui/icons-react-taro'
import {View} from '@tarojs/components'
import {ShopDealerBank} from "@/api/shop/shopDealerBank/model";
import {listShopDealerBank, removeShopDealerBank, updateShopDealerBank} from "@/api/shop/shopDealerBank";
import FixedButton from "@/components/FixedButton";
const DealerBank = () => {
const [list, setList] = useState<ShopDealerBank[]>([])
const [bank, setAddress] = useState<ShopDealerBank>()
const reload = () => {
listShopDealerBank({})
.then(data => {
setList(data || [])
// 默认地址
setAddress(data.find(item => item.isDefault))
})
.catch(() => {
Taro.showToast({
title: '获取地址失败',
icon: 'error'
});
})
}
const onDefault = async (item: ShopDealerBank) => {
if (bank) {
await updateShopDealerBank({
...bank,
isDefault: false
})
}
await updateShopDealerBank({
id: item.id,
isDefault: true
})
Taro.showToast({
title: '设置成功',
icon: 'success'
});
reload();
}
const onDel = async (id?: number) => {
await removeShopDealerBank(id)
Taro.showToast({
title: '删除成功',
icon: 'success'
});
reload();
}
const selectAddress = async (item: ShopDealerBank) => {
if (bank) {
await updateShopDealerBank({
...bank,
isDefault: false
})
}
await updateShopDealerBank({
id: item.id,
isDefault: true
})
setTimeout(() => {
Taro.navigateBack()
}, 500)
}
useDidShow(() => {
reload()
});
if (list.length == 0) {
return (
<ConfigProvider>
<div className={'h-full flex flex-col justify-center items-center'} style={{
height: 'calc(100vh - 300px)',
}}>
<Empty
style={{
backgroundColor: 'transparent'
}}
description="您还没有地址哦"
/>
<Space>
<Button onClick={() => Taro.navigateTo({url: '/doctor/bank/add'})}></Button>
<Button type="success" fill="dashed"
onClick={() => Taro.navigateTo({url: '/doctor/bank/wxAddress'})}></Button>
</Space>
</div>
</ConfigProvider>
)
}
return (
<View className={'p-3'}>
{list.map((item, _) => (
<Cell.Group>
<Cell className={'flex flex-col gap-1'} extra={item.bankAccount} onClick={() => selectAddress(item)}>
<View>
<View className={'font-medium text-sm'}>{item.bankName}</View>
</View>
<View className={'text-xs'}>
{item.bankCard} {item.bankAccount}
</View>
</Cell>
<Cell
align="center"
title={
<View className={'flex items-center gap-1'} onClick={() => onDefault(item)}>
{item.isDefault ? <Checked className={'text-green-600'} size={16}/> : <CheckNormal size={16}/>}
<View className={'text-gray-400'}></View>
</View>
}
extra={
<>
<View className={'text-gray-400'} onClick={() => onDel(item.id)}>
</View>
</>
}
/>
</Cell.Group>
))}
{/* 底部浮动按钮 */}
<FixedButton text={'新增银行卡'} onClick={() => Taro.navigateTo({url: '/doctor/bank/add'})} />
</View>
);
};
export default DealerBank;

View File

@@ -0,0 +1,108 @@
# 客户管理页面
## 功能概述
这是一个完整的客户管理页面,支持客户数据的展示、筛选和搜索功能。
## 主要功能
### 1. 数据源
- 使用 `pageUsers` API 从 User 表读取客户数据
- 支持按状态筛选用户status: 0 表示正常状态)
### 2. 状态管理
客户状态包括:
- **全部** - 显示所有客户
- **跟进中** - 正在跟进的潜在客户
- **已签约** - 已经签约的客户
- **已取消** - 已取消合作的客户
### 3. 顶部Tabs筛选
- 支持按客户状态筛选
- 显示每个状态的客户数量统计
- 实时更新统计数据
### 4. 搜索功能
支持多字段搜索:
- 客户姓名realName
- 昵称nickname
- 用户名username
- 手机号phone
- 用户IDuserId
### 5. 客户信息展示
每个客户卡片显示:
- 客户姓名和状态标签
- 手机号码
- 注册时间
- 用户ID、余额、积分等统计信息
## 技术实现
### 组件结构
```
CustomerManagement
├── 搜索栏 (SearchBar)
├── 状态筛选Tabs
└── 客户列表
└── 客户卡片项
```
### 主要状态
- `list`: 客户数据列表
- `loading`: 加载状态
- `activeTab`: 当前选中的状态Tab
- `searchValue`: 搜索关键词
### 工具函数
使用 `@/utils/customerStatus` 工具函数管理客户状态:
- `getStatusText()`: 获取状态文本
- `getStatusTagType()`: 获取状态标签类型
- `getStatusOptions()`: 获取状态选项列表
## 使用的组件
### NutUI 组件
- `Tabs` / `TabPane`: 状态筛选标签页
- `SearchBar`: 搜索输入框
- `Tag`: 状态标签
- `Loading`: 加载指示器
- `Space`: 间距布局
### 图标
- `Phone`: 手机号图标
- `User`: 用户图标
## 数据流
1. 页面初始化时调用 `fetchCustomerData()` 获取用户数据
2. 为每个用户添加客户状态(目前使用随机状态,实际项目中应从数据库获取)
3. 根据当前Tab和搜索条件筛选数据
4. 渲染客户列表
## 注意事项
### 临时实现
- 当前使用 `getRandomStatus()` 生成随机客户状态
- 实际项目中应该:
1. 在数据库中添加客户状态字段
2. 修改后端API返回真实的客户状态
3. 删除随机状态生成函数
### 扩展建议
1. 添加客户详情页面
2. 支持客户状态的修改操作
3. 添加客户添加/编辑功能
4. 支持批量操作
5. 添加导出功能
6. 支持更多筛选条件(注册时间、地区等)
## 文件结构
```
src/doctor/customer/
├── index.tsx # 主页面组件
└── README.md # 说明文档
src/utils/
└── customerStatus.ts # 客户状态工具函数
```

View File

@@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '患者报备',
navigationBarTextStyle: 'black'
})

400
src/doctor/customer/add.tsx Normal file
View File

@@ -0,0 +1,400 @@
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 Taro from '@tarojs/taro'
import {useRouter} from '@tarojs/taro'
import {View, Text} from '@tarojs/components'
import FixedButton from "@/components/FixedButton";
import {useUser} from "@/hooks/useUser";
import {ShopDealerApply} from "@/api/shop/shopDealerApply/model";
import {
addShopDealerApply, getShopDealerApply, pageShopDealerApply,
updateShopDealerApply
} from "@/api/shop/shopDealerApply";
import {
formatDateForDatabase,
extractDateForCalendar, formatDateForDisplay
} from "@/utils/dateUtils";
const AddShopDealerApply = () => {
const {user} = useUser()
const {params} = useRouter();
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 [showApplyTimePicker, setShowApplyTimePicker] = useState<boolean>(false)
const [showContractTimePicker, setShowContractTimePicker] = useState<boolean>(false)
const [applyTime, setApplyTime] = useState<string>('')
const [contractTime, setContractTime] = useState<string>('')
// 获取审核状态文字
const getApplyStatusText = (status?: number) => {
switch (status) {
case 10:
return '待审核'
case 20:
return '已签约'
case 30:
return '已取消'
default:
return '未知状态'
}
}
console.log(getApplyStatusText)
// 处理签约时间选择
const handleApplyTimeConfirm = (param: string) => {
const selectedDate = param[3] // 选中的日期字符串 (YYYY-M-D)
const formattedDate = formatDateForDatabase(selectedDate) // 转换为数据库格式
setApplyTime(selectedDate) // 保存原始格式用于显示
setShowApplyTimePicker(false)
// 更新表单数据(使用数据库格式)
if (formRef.current) {
formRef.current.setFieldsValue({
applyTime: formattedDate
})
}
}
// 处理合同日期选择
const handleContractTimeConfirm = (param: string) => {
const selectedDate = param[3] // 选中的日期字符串 (YYYY-M-D)
const formattedDate = formatDateForDatabase(selectedDate) // 转换为数据库格式
setContractTime(selectedDate) // 保存原始格式用于显示
setShowContractTimePicker(false)
// 更新表单数据(使用数据库格式)
if (formRef.current) {
formRef.current.setFieldsValue({
contractTime: formattedDate
})
}
}
const reload = async () => {
if (!params.id) {
return false;
}
// 查询当前用户ID是否已有申请记录
try {
const dealerApply = await getShopDealerApply(Number(params.id));
if (dealerApply) {
setFormData(dealerApply)
setIsEditMode(true);
setExistingApply(dealerApply)
// 初始化日期数据从数据库格式转换为Calendar组件格式
if (dealerApply.applyTime) {
setApplyTime(extractDateForCalendar(dealerApply.applyTime))
}
if (dealerApply.contractTime) {
setContractTime(extractDateForCalendar(dealerApply.contractTime))
}
Taro.setNavigationBarTitle({title: '签约'})
}
} catch (error) {
setLoading(true)
console.error('查询申请记录失败:', error);
setIsEditMode(false);
setExistingApply(null);
}
}
// 提交表单
// 计算保护期过期时间7天后
const calculateExpirationTime = (): string => {
const now = new Date();
const expirationDate = new Date(now);
expirationDate.setDate(now.getDate() + 7); // 7天后
// 格式化为数据库需要的格式YYYY-MM-DD HH:mm:ss
const year = expirationDate.getFullYear();
const month = String(expirationDate.getMonth() + 1).padStart(2, '0');
const day = String(expirationDate.getDate()).padStart(2, '0');
const hours = String(expirationDate.getHours()).padStart(2, '0');
const minutes = String(expirationDate.getMinutes()).padStart(2, '0');
const seconds = String(expirationDate.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
};
const submitSucceed = async (values: any) => {
try {
// 验证必填字段
if (!values.mobile || values.mobile.trim() === '') {
Taro.showToast({
title: '请填写联系方式',
icon: 'error'
});
return;
}
// 验证手机号格式
const phoneRegex = /^1[3-9]\d{9}$/;
if (!phoneRegex.test(values.mobile)) {
Taro.showToast({
title: '请填写正确的手机号',
icon: 'error'
});
return;
}
// 检查客户是否已存在
const res = await pageShopDealerApply({dealerName: values.dealerName, type: 4, applyStatus: 10});
if (res && res.count > 0) {
const existingCustomer = res.list[0];
// 检查是否在7天保护期内
if (!isEditMode && existingCustomer.applyTime) {
// 将申请时间字符串转换为时间戳进行比较
const applyTimeStamp = new Date(existingCustomer.applyTime).getTime();
const currentTimeStamp = new Date().getTime();
const sevenDaysInMs = 7 * 24 * 60 * 60 * 1000; // 7天的毫秒数
// 如果在7天保护期内不允许重复添加
if (currentTimeStamp - applyTimeStamp < sevenDaysInMs) {
const remainingDays = Math.ceil((sevenDaysInMs - (currentTimeStamp - applyTimeStamp)) / (24 * 60 * 60 * 1000));
Taro.showToast({
title: `该客户还在保护期,还需等待${remainingDays}天后才能重新添加`,
icon: 'none',
duration: 3000
});
return false;
} else {
// 超过7天保护期可以重新添加显示确认对话框
const modalResult = await new Promise<boolean>((resolve) => {
Taro.showModal({
title: '提示',
content: '该客户已超过7天保护期是否重新添加跟进',
showCancel: true,
cancelText: '取消',
confirmText: '确定',
success: (modalRes) => {
resolve(modalRes.confirm);
},
fail: () => {
resolve(false);
}
});
});
if (!modalResult) {
return false; // 用户取消,不继续执行
}
// 用户确认后继续执行添加逻辑
}
}
}
// 计算过期时间
const expirationTime = isEditMode ? existingApply?.expirationTime : calculateExpirationTime();
// 准备提交的数据
const submitData = {
...values,
type: 4,
realName: values.realName || user?.nickname,
mobile: values.mobile,
refereeId: 33534,
applyStatus: isEditMode ? 20 : 10,
auditTime: undefined,
// 设置保护期过期时间7天后
expirationTime: expirationTime,
// 确保日期数据正确提交(使用数据库格式)
applyTime: values.applyTime || (applyTime ? formatDateForDatabase(applyTime) : ''),
contractTime: values.contractTime || (contractTime ? formatDateForDatabase(contractTime) : '')
};
// 调试信息
console.log('=== 提交数据调试 ===');
console.log('是否编辑模式:', isEditMode);
console.log('计算的过期时间:', expirationTime);
console.log('提交的数据:', submitData);
console.log('==================');
// 如果是编辑模式添加现有申请的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);
Taro.showToast({
title: '提交失败,请重试',
icon: 'error'
});
}
}
// 处理固定按钮点击事件
const handleFixedButtonClick = () => {
// 触发表单提交
formRef.current?.submit();
};
const submitFailed = (error: any) => {
console.log(error, 'err...')
}
useEffect(() => {
reload().then(() => {
setLoading(false)
})
}, []); // 依赖用户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="dealerName" label="公司名称" initialValue={FormData?.dealerName} required>
<Input placeholder="公司名称" maxLength={10} disabled={isEditMode}/>
</Form.Item>
<Form.Item name="realName" label="联系人" initialValue={FormData?.realName} required>
<Input placeholder="请输入联系人" disabled={isEditMode}/>
</Form.Item>
<Form.Item name="mobile" label="联系方式" initialValue={FormData?.mobile} required>
<Input placeholder="请输入手机号" disabled={isEditMode} maxLength={11}/>
</Form.Item>
<Form.Item name="address" label="公司地址" initialValue={FormData?.address} required>
<Input placeholder="请输入详细地址" disabled={isEditMode}/>
</Form.Item>
<Form.Item name="dealerCode" label="户号" initialValue={FormData?.dealerCode} required>
<Input placeholder="请填写户号" disabled={isEditMode}/>
</Form.Item>
<Form.Item name="comments" label="跟进情况" initialValue={FormData?.comments}>
<Input placeholder="请填写跟进情况" disabled={isEditMode}/>
</Form.Item>
{isEditMode && (
<>
<Form.Item name="money" label="签约价格" initialValue={FormData?.money} required>
<Input placeholder="(元/兆瓦时)" disabled={false}/>
</Form.Item>
<Form.Item name="applyTime" label="签约时间" initialValue={FormData?.applyTime}>
<View
className="flex items-center justify-between py-2"
onClick={() => setShowApplyTimePicker(true)}
>
<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}>
<View
className="flex items-center justify-between py-2"
onClick={() => setShowContractTimePicker(true)}
>
<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>*/}
</>
)}
<Form.Item name="userId" label="报备人" initialValue={FormData?.userId} required>
</Form.Item>
</CellGroup>
</Form>
{/* 签约时间选择器 */}
<Calendar
visible={showApplyTimePicker}
defaultValue={applyTime}
onClose={() => setShowApplyTimePicker(false)}
onConfirm={handleApplyTimeConfirm}
/>
{/* 合同日期选择器 */}
<Calendar
visible={showContractTimePicker}
defaultValue={contractTime}
onClose={() => setShowContractTimePicker(false)}
onConfirm={handleContractTimeConfirm}
/>
{/* 审核状态显示(仅在编辑模式下显示) */}
{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) && (
<FixedButton
icon={<Edit/>}
text={'立即提交'}
onClick={handleFixedButtonClick}
/>
)}
</>
);
};
export default AddShopDealerApply;

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '患者管理'
})

View File

@@ -0,0 +1,583 @@
import {useState, useEffect, useCallback} from 'react'
import {View, Text} from '@tarojs/components'
import Taro, {useDidShow} from '@tarojs/taro'
import {Loading, InfiniteLoading, Empty, Space, Tabs, TabPane, Tag, Button, SearchBar} from '@nutui/nutui-react-taro'
import {Phone, AngleDoubleLeft} from '@nutui/icons-react-taro'
import type {ShopDealerApply, ShopDealerApply as UserType} from "@/api/shop/shopDealerApply/model";
import {
CustomerStatus,
getStatusText,
getStatusTagType,
getStatusOptions,
mapApplyStatusToCustomerStatus,
mapCustomerStatusToApplyStatus
} from '@/utils/customerStatus';
import FixedButton from "@/components/FixedButton";
import navTo from "@/utils/common";
import {pageShopDealerApply, removeShopDealerApply, updateShopDealerApply} from "@/api/shop/shopDealerApply";
// 扩展User类型添加客户状态和保护天数
interface CustomerUser extends UserType {
customerStatus?: CustomerStatus;
protectDays?: number; // 剩余保护天数
}
const CustomerIndex = () => {
const [list, setList] = useState<CustomerUser[]>([])
const [loading, setLoading] = useState<boolean>(false)
const [activeTab, setActiveTab] = useState<CustomerStatus>('all')
const [searchValue, setSearchValue] = useState<string>('')
const [displaySearchValue, setDisplaySearchValue] = useState<string>('')
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
// Tab配置
const tabList = getStatusOptions();
// 复制手机号
const copyPhone = (phone: string) => {
Taro.setClipboardData({
data: phone,
success: () => {
Taro.showToast({
title: '手机号已复制',
icon: 'success',
duration: 1500
});
}
});
};
// 一键拨打
const makePhoneCall = (phone: string) => {
Taro.makePhoneCall({
phoneNumber: phone,
fail: () => {
Taro.showToast({
title: '拨打取消',
icon: 'error'
});
}
});
};
// 编辑跟进情况
const editComments = (customer: CustomerUser) => {
Taro.showModal({
title: '编辑跟进情况',
// @ts-ignore
editable: true,
placeholderText: '请输入跟进情况',
content: customer.comments || '',
success: async (res) => {
// @ts-ignore
if (res.confirm && res.content !== undefined) {
try {
// 更新跟进情况
await updateShopDealerApply({
...customer,
// @ts-ignore
comments: res.content.trim()
});
Taro.showToast({
title: '更新成功',
icon: 'success'
});
// 刷新列表
setList([]);
setPage(1);
setHasMore(true);
fetchCustomerData(activeTab, true);
} catch (error) {
console.error('更新跟进情况失败:', error);
Taro.showToast({
title: '更新失败,请重试',
icon: 'error'
});
}
}
}
});
};
// 计算剩余保护天数(基于过期时间)
const calculateProtectDays = (expirationTime?: string, applyTime?: string): number => {
try {
// 优先使用过期时间字段
if (expirationTime) {
const expDate = new Date(expirationTime.replace(' ', 'T'));
const now = new Date();
// 计算剩余毫秒数
const remainingMs = expDate.getTime() - now.getTime();
// 转换为天数,向上取整
const remainingDays = Math.ceil(remainingMs / (1000 * 60 * 60 * 24));
console.log('=== 基于过期时间计算 ===');
console.log('过期时间:', expirationTime);
console.log('当前时间:', now.toLocaleString());
console.log('剩余天数:', remainingDays);
console.log('======================');
return Math.max(0, remainingDays);
}
// 如果没有过期时间,回退到基于申请时间计算
if (!applyTime) return 0;
const protectionPeriod = 7; // 保护期7天
// 解析申请时间
let applyDate: Date;
if (applyTime.includes('T')) {
applyDate = new Date(applyTime);
} else {
applyDate = new Date(applyTime.replace(' ', 'T'));
}
// 获取当前时间
const now = new Date();
// 只比较日期部分,忽略时间
const applyDateOnly = new Date(applyDate.getFullYear(), applyDate.getMonth(), applyDate.getDate());
const currentDateOnly = new Date(now.getFullYear(), now.getMonth(), now.getDate());
// 计算已经过去的天数
const timeDiff = currentDateOnly.getTime() - applyDateOnly.getTime();
const daysPassed = Math.floor(timeDiff / (1000 * 60 * 60 * 24));
// 计算剩余保护天数
const remainingDays = protectionPeriod - daysPassed;
console.log('=== 基于申请时间计算 ===');
console.log('申请时间:', applyTime);
console.log('已过去天数:', daysPassed);
console.log('剩余保护天数:', remainingDays);
console.log('======================');
return Math.max(0, remainingDays);
} catch (error) {
console.error('日期计算错误:', error);
return 0;
}
};
// 获取客户数据
const fetchCustomerData = useCallback(async (statusFilter?: CustomerStatus, resetPage = false, targetPage?: number) => {
setLoading(true);
try {
const currentPage = resetPage ? 1 : (targetPage || page);
// 构建API参数根据状态筛选
const params: any = {
type: 4,
page: currentPage
};
const applyStatus = mapCustomerStatusToApplyStatus(statusFilter || activeTab);
if (applyStatus !== undefined) {
params.applyStatus = applyStatus;
}
const res = await pageShopDealerApply(params);
if (res?.list && res.list.length > 0) {
// 正确映射状态并计算保护天数
const mappedList = res.list.map(customer => ({
...customer,
customerStatus: mapApplyStatusToCustomerStatus(customer.applyStatus || 10),
protectDays: calculateProtectDays(customer.expirationTime, customer.applyTime || customer.createTime || '')
}));
// 如果是重置页面或第一页,直接设置新数据;否则追加数据
if (resetPage || currentPage === 1) {
setList(mappedList);
} else {
setList(prevList => prevList.concat(mappedList));
}
// 正确判断是否还有更多数据
const hasMoreData = res.list.length >= 10; // 假设每页10条数据
setHasMore(hasMoreData);
} else {
if (resetPage || currentPage === 1) {
setList([]);
}
setHasMore(false);
}
setPage(currentPage);
} catch (error) {
console.error('获取客户数据失败:', error);
Taro.showToast({
title: '加载失败,请重试',
icon: 'none'
});
} finally {
setLoading(false);
}
}, [activeTab, page]);
const reloadMore = async () => {
if (loading || !hasMore) return; // 防止重复加载
const nextPage = page + 1;
await fetchCustomerData(activeTab, false, nextPage);
}
// 防抖搜索功能
useEffect(() => {
const timer = setTimeout(() => {
setDisplaySearchValue(searchValue);
}, 300); // 300ms 防抖
return () => clearTimeout(timer);
}, [searchValue]);
// 根据搜索条件筛选数据状态筛选已在API层面处理
const getFilteredList = () => {
let filteredList = list;
// 按搜索关键词筛选
if (displaySearchValue.trim()) {
const keyword = displaySearchValue.trim().toLowerCase();
filteredList = filteredList.filter(customer =>
(customer.realName && customer.realName.toLowerCase().includes(keyword)) ||
(customer.dealerName && customer.dealerName.toLowerCase().includes(keyword)) ||
(customer.dealerCode && customer.dealerCode.toLowerCase().includes(keyword)) ||
(customer.mobile && customer.mobile.includes(keyword)) ||
(customer.userId && customer.userId.toString().includes(keyword))
);
}
return filteredList;
};
// 获取各状态的统计数量
const [statusCounts, setStatusCounts] = useState({
all: 0,
pending: 0,
signed: 0,
cancelled: 0
});
// 获取所有状态的统计数量
const fetchStatusCounts = useCallback(async () => {
try {
// 并行获取各状态的数量
const [allRes, pendingRes, signedRes, cancelledRes] = await Promise.all([
pageShopDealerApply({type: 4}), // 全部
pageShopDealerApply({applyStatus: 10, type: 4}), // 跟进中
pageShopDealerApply({applyStatus: 20, type: 4}), // 已签约
pageShopDealerApply({applyStatus: 30, type: 4}) // 已取消
]);
setStatusCounts({
all: allRes?.count || 0,
pending: pendingRes?.count || 0,
signed: signedRes?.count || 0,
cancelled: cancelledRes?.count || 0
});
} catch (error) {
console.error('获取状态统计失败:', error);
}
}, []);
const getStatusCounts = () => statusCounts;
// 取消操作
const handleCancel = (customer: ShopDealerApply) => {
updateShopDealerApply({
...customer,
applyStatus: 30
}).then(() => {
Taro.showToast({
title: '取消成功',
icon: 'success'
});
// 重新加载当前tab的数据
setList([]);
setPage(1);
setHasMore(true);
fetchCustomerData(activeTab, true).then();
fetchStatusCounts().then();
})
};
// 删除
const handleDelete = (customer: ShopDealerApply) => {
removeShopDealerApply(customer.applyId).then(() => {
Taro.showToast({
title: '删除成功',
icon: 'success'
});
// 刷新当前tab的数据
setList([]);
setPage(1);
setHasMore(true);
fetchCustomerData(activeTab, true).then();
fetchStatusCounts().then();
})
}
// 初始化数据
useEffect(() => {
fetchCustomerData(activeTab, true).then();
fetchStatusCounts().then();
}, []);
// 当activeTab变化时重新获取数据
useEffect(() => {
setList([]); // 清空列表
setPage(1); // 重置页码
setHasMore(true); // 重置加载状态
fetchCustomerData(activeTab, true);
}, [activeTab]);
// 监听页面显示,当从其他页面返回时刷新数据
useDidShow(() => {
// 刷新当前tab的数据和统计信息
setList([]);
setPage(1);
setHasMore(true);
fetchCustomerData(activeTab, true);
fetchStatusCounts();
});
// 渲染客户项
const renderCustomerItem = (customer: CustomerUser) => (
<View key={customer.userId} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
<View className="flex items-center mb-3">
<View className="flex-1">
<View className="flex items-center justify-between mb-1">
<Text className="font-semibold text-gray-800 mr-2">
{customer.dealerName}
</Text>
{customer.customerStatus && (
<Tag type={getStatusTagType(customer.customerStatus)}>
{getStatusText(customer.customerStatus)}
</Tag>
)}
</View>
<View className="flex items-center mb-1">
<Space direction="vertical">
<Text className="text-xs text-gray-500">{customer.realName}</Text>
<View className="flex items-center">
<Text className="text-xs text-gray-500" onClick={(e) => {
e.stopPropagation();
makePhoneCall(customer.mobile || '');
}}>{customer.mobile}</Text>
<View className="flex items-center ml-2">
<Phone
size={12}
className="text-green-500 mr-2"
onClick={(e) => {
e.stopPropagation();
makePhoneCall(customer.mobile || '');
}}
/>
<Text
className="text-xs text-blue-500 cursor-pointer"
onClick={(e) => {
e.stopPropagation();
copyPhone(customer.mobile || '');
}}
>
</Text>
</View>
</View>
<Text className="text-xs text-gray-500">
{customer.createTime}
</Text>
</Space>
</View>
{/* 保护天数显示 */}
{customer.applyStatus === 10 && (
<View className="flex items-center my-1">
<Text className="text-xs text-gray-500 mr-2"></Text>
{customer.protectDays && customer.protectDays > 0 ? (
<Text className={`text-xs px-2 py-1 rounded ${
customer.protectDays <= 2
? 'bg-red-100 text-red-600'
: customer.protectDays <= 4
? 'bg-orange-100 text-orange-600'
: 'bg-green-100 text-green-600'
}`}>
{customer.protectDays}
</Text>
) : (
<Text className="text-xs px-2 py-1 rounded bg-gray-100 text-gray-500">
</Text>
)}
</View>
)}
<View className={'flex items-center gap-2'}>
<Text className="text-xs text-gray-500">{customer?.nickName}</Text>
<AngleDoubleLeft size={12} className={'text-blue-500'} />
<Text className={'text-xs text-gray-500'}>{customer?.refereeName}</Text>
</View>
{/* 显示 comments 字段 */}
<Space className="flex items-center">
<Text className="text-xs text-gray-500">{customer.comments || '暂无'}</Text>
<Text
className="text-xs text-blue-500 cursor-pointer"
onClick={(e) => {
e.stopPropagation();
editComments(customer);
}}
>
</Text>
</Space>
</View>
</View>
{/* 跟进中状态显示操作按钮 */}
{(customer.applyStatus === 10 && customer.userId == Taro.getStorageSync('UserId')) && (
<Space className="flex justify-end">
<Button
size="small"
onClick={() => navTo(`/dealer/customer/add?id=${customer.applyId}`, true)}
style={{marginRight: '8px', backgroundColor: '#52c41a', color: 'white'}}
>
</Button>
<Button
size="small"
onClick={() => handleCancel(customer)}
style={{backgroundColor: '#ff4d4f', color: 'white'}}
>
</Button>
</Space>
)}
{(customer.applyStatus === 30 && customer.userId == Taro.getStorageSync('UserId')) && (
<Space className="flex justify-end">
<Button
size="small"
onClick={() => handleDelete(customer)}
style={{backgroundColor: '#ff4d4f', color: 'white'}}
>
</Button>
</Space>
)}
</View>
);
// 渲染客户列表
const renderCustomerList = () => {
const filteredList = getFilteredList();
const isSearching = displaySearchValue.trim().length > 0;
return (
<View className="flex-1">
{/* 搜索结果统计 */}
{isSearching && (
<View className="bg-white px-4 py-2 border-b border-gray-100">
<Text className="text-sm text-gray-600">
"{displaySearchValue}" {filteredList.length}
</Text>
</View>
)}
<View className="p-4" style={{
height: isSearching ? 'calc(90vh - 40px)' : '90vh',
overflowY: 'auto',
overflowX: 'hidden'
}}>
<InfiniteLoading
target="scroll"
hasMore={hasMore}
onLoadMore={reloadMore}
onScroll={() => {
// 滚动事件处理
}}
onScrollToUpper={() => {
// 滚动到顶部事件处理
}}
loadingText={
<>
...
</>
}
loadMoreText={
filteredList.length === 0 ? (
<Empty
style={{backgroundColor: 'transparent'}}
description={loading ? "加载中..." : "暂无客户数据"}
/>
) : (
<View className={'h-3 flex items-center justify-center'}>
<Text className="text-gray-500 text-sm"></Text>
</View>
)
}
>
{loading && filteredList.length === 0 ? (
<View className="flex items-center justify-center py-8">
<Loading/>
<Text className="text-gray-500 mt-2 ml-2">...</Text>
</View>
) : (
filteredList.map(renderCustomerItem)
)}
</InfiniteLoading>
</View>
</View>
);
};
return (
<View className="min-h-screen bg-gray-50">
{/* 搜索栏 */}
<View className="bg-white py-2 border-b border-gray-100">
<SearchBar
value={searchValue}
placeholder="搜索客户名称、手机号"
onChange={(value) => setSearchValue(value)}
onClear={() => {
setSearchValue('');
setDisplaySearchValue('');
}}
clearable
/>
</View>
{/* 顶部Tabs */}
<View className="bg-white">
<Tabs
value={activeTab}
onChange={(value) => setActiveTab(value as CustomerStatus)}
>
{tabList.map(tab => {
const counts = getStatusCounts();
const count = counts[tab.value as keyof typeof counts] || 0;
return (
<TabPane
key={tab.value}
title={`${tab.label}${count > 0 ? `(${count})` : ''}`}
value={tab.value}
/>
);
})}
</Tabs>
</View>
{/* 客户列表 */}
{renderCustomerList()}
<FixedButton text={'客户报备'} onClick={() => Taro.navigateTo({url: '/dealer/customer/add'})}/>
</View>
);
};
export default CustomerIndex;

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '入市查询'
})

View File

@@ -0,0 +1,207 @@
import {useState, useCallback} from 'react'
import {View, Text} from '@tarojs/components'
import Taro from '@tarojs/taro'
import {Loading, InfiniteLoading, Empty, Space, SearchBar} from '@nutui/nutui-react-taro'
import type {ShopDealerApply as UserType} from "@/api/shop/shopDealerApply/model";
import {
CustomerStatus,
mapApplyStatusToCustomerStatus,
} from '@/utils/customerStatus';
import {pageShopDealerApply} from "@/api/shop/shopDealerApply";
// 扩展User类型添加客户状态
interface CustomerUser extends UserType {
customerStatus?: CustomerStatus;
}
const CustomerTrading = () => {
const [list, setList] = useState<CustomerUser[]>([])
const [loading, setLoading] = useState<boolean>(false)
const [searchValue, setSearchValue] = useState<string>('')
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
// 获取客户数据
const fetchCustomerData = useCallback(async (resetPage = false, targetPage?: number, searchKeyword?: string) => {
setLoading(true);
try {
const currentPage = resetPage ? 1 : (targetPage || page);
// 构建API参数根据状态筛选
const params: any = {
type: 3,
page: currentPage
};
// 添加搜索关键词
if (searchKeyword && searchKeyword.trim()) {
params.keywords = searchKeyword.trim();
}
const res = await pageShopDealerApply(params);
if (res?.list && res.list.length > 0) {
// 正确映射状态
const mappedList = res.list.map(customer => ({
...customer,
customerStatus: mapApplyStatusToCustomerStatus(customer.applyStatus || 10)
}));
// 如果是重置页面或第一页,直接设置新数据;否则追加数据
if (resetPage || currentPage === 1) {
setList(mappedList);
} else {
setList(prevList => prevList.concat(mappedList));
}
// 正确判断是否还有更多数据
const hasMoreData = res.list.length >= 10; // 假设每页10条数据
setHasMore(hasMoreData);
} else {
if (resetPage || currentPage === 1) {
setList([]);
}
setHasMore(false);
}
setPage(currentPage);
} catch (error) {
console.error('获取客户数据失败:', error);
Taro.showToast({
title: '加载失败,请重试',
icon: 'none'
});
} finally {
setLoading(false);
}
}, [page]);
const reloadMore = async () => {
if (loading || !hasMore) return; // 防止重复加载
const nextPage = page + 1;
await fetchCustomerData(false, nextPage, searchValue);
}
// 获取列表数据(现在使用服务端搜索,不需要客户端过滤)
const getFilteredList = () => {
return list;
};
// 搜索处理函数
const handleSearch = (keyword: string) => {
if(keyword.length < 4){
Taro.showToast({
title: '请输入至少4个字符',
icon: 'none'
});
return;
}
setSearchValue(keyword);
setList([]);
setPage(1);
setHasMore(true);
fetchCustomerData(true, 1, keyword);
};
// 清空搜索
const handleClearSearch = () => {
setSearchValue('');
setList([]);
setPage(1);
setHasMore(true);
fetchCustomerData(true, 1, '');
};
// 渲染客户项
const renderCustomerItem = (customer: CustomerUser) => (
<View key={customer.userId} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
<View className="flex items-center">
<View className="flex-1">
<View className="flex items-center justify-between mb-1">
<Text className="font-semibold text-gray-800 mr-2">
{customer.dealerName}
</Text>
</View>
<Space direction={'vertical'}>
{/*<Text className="text-xs text-gray-500">统一代码:{customer.dealerCode}</Text>*/}
<Text className="text-xs text-gray-500">
{customer.createTime}
</Text>
</Space>
</View>
</View>
</View>
);
// 渲染客户列表
const renderCustomerList = () => {
const filteredList = getFilteredList();
return (
<View className="p-4" style={{
height: '90vh',
overflowY: 'auto',
overflowX: 'hidden'
}}>
<InfiniteLoading
target="scroll"
hasMore={hasMore}
onLoadMore={reloadMore}
onScroll={() => {
// 滚动事件处理
}}
onScrollToUpper={() => {
// 滚动到顶部事件处理
}}
loadingText={
<>
...
</>
}
loadMoreText={
filteredList.length === 0 ? (
<Empty
style={{backgroundColor: 'transparent'}}
description={loading ? "加载中..." : "暂无客户数据"}
/>
) : (
<View className={'h-12 flex items-center justify-center'}>
<Text className="text-gray-500 text-sm"></Text>
</View>
)
}
>
{loading && filteredList.length === 0 ? (
<View className="flex items-center justify-center py-8">
<Loading/>
<Text className="text-gray-500 mt-2 ml-2">...</Text>
</View>
) : (
filteredList.map(renderCustomerItem)
)}
</InfiniteLoading>
</View>
);
};
return (
<View className="min-h-screen bg-gray-50">
{/* 搜索栏 */}
<View className="bg-white shadow-sm">
<SearchBar
placeholder="请输入搜索关键词"
value={searchValue}
onSearch={(value) => handleSearch(value)}
onClear={() => handleClearSearch()}
/>
</View>
{/* 客户列表 */}
{renderCustomerList()}
</View>
);
};
export default CustomerTrading;

View File

@@ -1,3 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '医生'
navigationBarTitleText: '医生'
})

View File

@@ -3,15 +3,18 @@ import {View, Text} from '@tarojs/components'
import {ConfigProvider, Button, Grid, Avatar} from '@nutui/nutui-react-taro'
import {
User,
Shopping,
Dongdong,
ArrowRight,
Purse,
People
UserAdd,
Edit,
Comment,
QrCode,
Notice,
Orderlist,
Health,
PickedUp
} from '@nutui/icons-react-taro'
import {useDealerUser} from '@/hooks/useDealerUser'
import { useThemeStyles } from '@/hooks/useTheme'
import {businessGradients, cardGradients, gradientUtils} from '@/styles/gradients'
import {gradientUtils} from '@/styles/gradients'
import Taro from '@tarojs/taro'
const DealerIndex: React.FC = () => {
@@ -30,10 +33,10 @@ const DealerIndex: React.FC = () => {
}
// 格式化金额
const formatMoney = (money?: string) => {
if (!money) return '0.00'
return parseFloat(money).toFixed(2)
}
// const formatMoney = (money?: string) => {
// if (!money) return '0.00'
// return parseFloat(money).toFixed(2)
// }
// 格式化时间
const formatTime = (time?: string) => {
@@ -103,12 +106,12 @@ const DealerIndex: React.FC = () => {
<View className="flex-1 flex-col">
<View className="text-white text-lg font-bold mb-1" style={{
}}>
{dealerUser?.realName || '分销商'}
{dealerUser?.realName || '医生名称'}
</View>
<View className="text-sm" style={{
color: 'rgba(255, 255, 255, 0.8)'
}}>
ID: {dealerUser.userId} | : {dealerUser.refereeId || '无'}
: {dealerUser.userId}
</View>
</View>
<View className="text-right hidden">
@@ -125,80 +128,9 @@ const DealerIndex: React.FC = () => {
</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>
<View className="font-semibold mb-4 text-gray-800"></View>
<ConfigProvider>
<Grid
columns={4}
@@ -209,37 +141,70 @@ const DealerIndex: React.FC = () => {
border: 'none'
} as React.CSSProperties}
>
<Grid.Item text="分销订单" onClick={() => navigateToPage('/dealer/orders/index')}>
<Grid.Item text="患者管理" onClick={() => navigateToPage('/doctor/customer/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"/>
<PickedUp color="#3b82f6" size="20"/>
</View>
</View>
</Grid.Item>
<Grid.Item text={'提现申请'} onClick={() => navigateToPage('/dealer/withdraw/index')}>
<Grid.Item text={'在线开方'} onClick={() => navigateToPage('/doctor/orders/add')}>
<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"/>
<Edit color="#10b981" size="20"/>
</View>
</View>
</Grid.Item>
<Grid.Item text={'我的邀请'} onClick={() => navigateToPage('/dealer/team/index')}>
<Grid.Item text={'咨询管理'} onClick={() => navigateToPage('/doctor/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"/>
<Comment color="#8b5cf6" size="20"/>
</View>
</View>
</Grid.Item>
<Grid.Item text={'我的邀请码'} onClick={() => navigateToPage('/dealer/qrcode/index')}>
<Grid.Item text={'处方管理'} onClick={() => navigateToPage('/doctor/orders/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"/>
<Orderlist color="#f59e0b" 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">
<Notice size="20"/>
</View>
</View>
</Grid.Item>
<Grid.Item text={'我的邀请'} onClick={() => navigateToPage('/doctor/team/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<UserAdd size="20"/>
</View>
</View>
</Grid.Item>
<Grid.Item text={'我的邀请码'} onClick={() => navigateToPage('/doctor/qrcode/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<QrCode size="20"/>
</View>
</View>
</Grid.Item>
<Grid.Item text={'医生认证'} onClick={() => navigateToPage('/doctor/apply/add')}>
<View className="text-center">
<View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Health size="20"/>
</View>
</View>
</Grid.Item>
</Grid>
{/* 第二行功能 */}
@@ -252,7 +217,7 @@ const DealerIndex: React.FC = () => {
{/* border: 'none'*/}
{/* } as React.CSSProperties}*/}
{/*>*/}
{/* <Grid.Item text={'邀请统计'} onClick={() => navigateToPage('/dealer/invite-stats/index')}>*/}
{/* <Grid.Item text={'邀请统计'} onClick={() => navigateToPage('/doctor/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 File

@@ -15,7 +15,7 @@ const DealerInfo: React.FC = () => {
// 跳转到申请页面
const navigateToApply = () => {
Taro.navigateTo({
url: '/pages/dealer/apply/add'
url: '/pages/doctor/apply/add'
})
}

View File

@@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '在线开方',
navigationBarTextStyle: 'black'
})

135
src/doctor/orders/add.tsx Normal file
View File

@@ -0,0 +1,135 @@
import {useEffect, useState, useRef} from "react";
import {useRouter} from '@tarojs/taro'
import {Loading, CellGroup, Input, Form, Cell, Avatar} from '@nutui/nutui-react-taro'
import {ArrowRight} from '@nutui/icons-react-taro'
import {View, Text} from '@tarojs/components'
import Taro from '@tarojs/taro'
import FixedButton from "@/components/FixedButton";
import {addShopChatMessage} from "@/api/shop/shopChatMessage";
import {ShopChatMessage} from "@/api/shop/shopChatMessage/model";
import navTo from "@/utils/common";
import {getUser} from "@/api/system/user";
import {User} from "@/api/system/user/model";
const AddMessage = () => {
const {params} = useRouter();
const [toUser, setToUser] = useState<User>()
const [loading, setLoading] = useState<boolean>(true)
const [FormData, _] = useState<ShopChatMessage>()
const formRef = useRef<any>(null)
// 判断是编辑还是新增模式
const isEditMode = !!params.id
const toUserId = params.id ? Number(params.id) : undefined
const reload = async () => {
if(toUserId){
getUser(Number(toUserId)).then(data => {
setToUser(data)
})
}
}
// 提交表单
const submitSucceed = async (values: any) => {
try {
// 准备提交的数据
const submitData = {
...values
};
console.log('提交数据:', submitData)
// 参数校验
if(!toUser){
Taro.showToast({
title: `请选择发送对象`,
icon: 'error'
});
return false;
}
// 判断内容是否为空
if (!values.content) {
Taro.showToast({
title: `请输入内容`,
icon: 'error'
});
return false;
}
// 执行新增或更新操作
await addShopChatMessage({
toUserId: toUserId,
formUserId: Taro.getStorageSync('UserId'),
type: 'text',
content: values.content
});
Taro.showToast({
title: `发送成功`,
icon: 'success'
});
setTimeout(() => {
Taro.navigateBack();
}, 1000);
} catch (error) {
console.error('发送失败:', error);
Taro.showToast({
title: `发送失败`,
icon: 'error'
});
}
}
const submitFailed = (error: any) => {
console.log(error, 'err...')
}
useEffect(() => {
reload().then(() => {
setLoading(false)
})
}, [isEditMode]);
if (loading) {
return <Loading className={'px-2'}></Loading>
}
return (
<>
<Cell title={toUser ? (
<View className={'flex items-center'}>
<Avatar src={toUser.avatar}/>
<View className={'ml-2 flex flex-col'}>
<Text>{toUser.alias || toUser.nickname}</Text>
<Text className={'text-gray-300'}>{toUser.mobile}</Text>
</View>
</View>
) : '选择患者'} extra={(
<ArrowRight color="#cccccc" className={toUser ? 'mt-2' : ''} size={toUser ? 20 : 18}/>
)}
onClick={() => navTo(`/doctor/customer/index`, true)}/>
<Form
ref={formRef}
divider
initialValues={FormData}
labelPosition="left"
onFinish={(values) => submitSucceed(values)}
onFinishFailed={(errors) => submitFailed(errors)}
>
<CellGroup style={{padding: '4px 0'}}>
<Form.Item name="content" initialValue={FormData?.content} required>
<Input placeholder="填写消息内容" maxLength={300}/>
</Form.Item>
</CellGroup>
</Form>
{/* 底部浮动按钮 */}
<FixedButton text={isEditMode ? '立即发送' : '立即发送'} onClick={() => formRef.current?.submit()}/>
</>
);
};
export default AddMessage;

View File

@@ -1,3 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '分销订单'
navigationBarTitleText: '处方管理'
})

View File

@@ -1,161 +1,63 @@
import React, { useState, useEffect, useCallback } from 'react'
import { View, Text } from '@tarojs/components'
import { Empty, Tabs, Tag, PullToRefresh, Loading } from '@nutui/nutui-react-taro'
import React, {useState, useEffect, useCallback} from 'react'
import {View, Text, ScrollView} from '@tarojs/components'
import {Empty, Tag, PullToRefresh, Loading} from '@nutui/nutui-react-taro'
import Taro from '@tarojs/taro'
import { pageShopDealerOrder } from '@/api/shop/shopDealerOrder'
import { useDealerUser } from '@/hooks/useDealerUser'
import type { ShopDealerOrder } from '@/api/shop/shopDealerOrder/model'
import {pageShopDealerOrder} from '@/api/shop/shopDealerOrder'
import {useDealerUser} from '@/hooks/useDealerUser'
import type {ShopDealerOrder} from '@/api/shop/shopDealerOrder/model'
interface OrderWithDetails extends ShopDealerOrder {
orderNo?: string
customerName?: string
totalCommission?: string
// 当前用户在此订单中的层级和佣金
userLevel?: 1 | 2 | 3
userCommission?: string
}
const DealerOrders: React.FC = () => {
const [activeTab, setActiveTab] = useState<string>('0')
const [loading, setLoading] = useState<boolean>(false)
const [refreshing, setRefreshing] = useState<boolean>(false)
const [loadingMore, setLoadingMore] = useState<boolean>(false)
const [orders, setOrders] = useState<OrderWithDetails[]>([])
const [statistics, setStatistics] = useState({
totalOrders: 0,
totalCommission: '0.00',
pendingCommission: '0.00',
// 分层统计
level1: { orders: 0, commission: '0.00' },
level2: { orders: 0, commission: '0.00' },
level3: { orders: 0, commission: '0.00' }
})
const [currentPage, setCurrentPage] = useState<number>(1)
const [hasMore, setHasMore] = useState<boolean>(true)
const { dealerUser } = useDealerUser()
const {dealerUser} = useDealerUser()
// 获取订单数据 - 查询当前用户作为各层级分销商的所有订单
const fetchOrders = useCallback(async () => {
// 获取订单数据
const fetchOrders = useCallback(async (page: number = 1, isRefresh: boolean = false) => {
if (!dealerUser?.userId) return
try {
setLoading(true)
// 并行查询三个层级的订单
const [level1Result, level2Result, level3Result] = await Promise.all([
// 一级分销商订单
pageShopDealerOrder({
page: 1,
limit: 100,
firstUserId: dealerUser.userId
}),
// 二级分销商订单
pageShopDealerOrder({
page: 1,
limit: 100,
secondUserId: dealerUser.userId
}),
// 三级分销商订单
pageShopDealerOrder({
page: 1,
limit: 100,
thirdUserId: dealerUser.userId
})
])
const allOrders: OrderWithDetails[] = []
const stats = {
totalOrders: 0,
totalCommission: '0.00',
pendingCommission: '0.00',
level1: { orders: 0, commission: '0.00' },
level2: { orders: 0, commission: '0.00' },
level3: { orders: 0, commission: '0.00' }
if (isRefresh) {
setRefreshing(true)
} else if (page === 1) {
setLoading(true)
} else {
setLoadingMore(true)
}
// 处理一级分销订单
if (level1Result?.list) {
const level1Orders = level1Result.list.map(order => ({
const result = await pageShopDealerOrder({
page,
limit: 10
})
if (result?.list) {
const newOrders = result.list.map(order => ({
...order,
orderNo: `DD${order.orderId}`,
orderNo: `${order.orderId}`,
customerName: `用户${order.userId}`,
userLevel: 1 as const,
userCommission: order.firstMoney || '0.00',
totalCommission: (
parseFloat(order.firstMoney || '0') +
parseFloat(order.secondMoney || '0') +
parseFloat(order.thirdMoney || '0')
).toFixed(2)
userCommission: order.firstMoney || '0.00'
}))
allOrders.push(...level1Orders)
stats.level1.orders = level1Orders.length
stats.level1.commission = level1Orders.reduce((sum, order) =>
sum + parseFloat(order.userCommission || '0'), 0
).toFixed(2)
if (page === 1) {
setOrders(newOrders)
} else {
setOrders(prev => [...prev, ...newOrders])
}
setHasMore(newOrders.length === 10)
setCurrentPage(page)
}
// 处理二级分销订单
if (level2Result?.list) {
const level2Orders = level2Result.list.map(order => ({
...order,
orderNo: `DD${order.orderId}`,
customerName: `用户${order.userId}`,
userLevel: 2 as const,
userCommission: order.secondMoney || '0.00',
totalCommission: (
parseFloat(order.firstMoney || '0') +
parseFloat(order.secondMoney || '0') +
parseFloat(order.thirdMoney || '0')
).toFixed(2)
}))
allOrders.push(...level2Orders)
stats.level2.orders = level2Orders.length
stats.level2.commission = level2Orders.reduce((sum, order) =>
sum + parseFloat(order.userCommission || '0'), 0
).toFixed(2)
}
// 处理三级分销订单
if (level3Result?.list) {
const level3Orders = level3Result.list.map(order => ({
...order,
orderNo: `DD${order.orderId}`,
customerName: `用户${order.userId}`,
userLevel: 3 as const,
userCommission: order.thirdMoney || '0.00',
totalCommission: (
parseFloat(order.firstMoney || '0') +
parseFloat(order.secondMoney || '0') +
parseFloat(order.thirdMoney || '0')
).toFixed(2)
}))
allOrders.push(...level3Orders)
stats.level3.orders = level3Orders.length
stats.level3.commission = level3Orders.reduce((sum, order) =>
sum + parseFloat(order.userCommission || '0'), 0
).toFixed(2)
}
// 去重(同一个订单可能在多个层级中出现)
const uniqueOrders = allOrders.filter((order, index, self) =>
index === self.findIndex(o => o.orderId === order.orderId)
)
// 计算总统计
stats.totalOrders = uniqueOrders.length
stats.totalCommission = (
parseFloat(stats.level1.commission) +
parseFloat(stats.level2.commission) +
parseFloat(stats.level3.commission)
).toFixed(2)
stats.pendingCommission = allOrders
.filter(order => order.isSettled === 0)
.reduce((sum, order) => sum + parseFloat(order.userCommission || '0'), 0)
.toFixed(2)
setOrders(uniqueOrders)
setStatistics(stats)
} catch (error) {
console.error('获取分销订单失败:', error)
Taro.showToast({
@@ -164,18 +66,27 @@ const DealerOrders: React.FC = () => {
})
} finally {
setLoading(false)
setRefreshing(false)
setLoadingMore(false)
}
}, [dealerUser?.userId])
// 刷新数据
// 下拉刷新
const handleRefresh = async () => {
await fetchOrders()
await fetchOrders(1, true)
}
// 加载更多
const handleLoadMore = async () => {
if (!loadingMore && hasMore) {
await fetchOrders(currentPage + 1)
}
}
// 初始化加载数据
useEffect(() => {
if (dealerUser?.userId) {
fetchOrders().then()
fetchOrders(1)
}
}, [fetchOrders])
@@ -193,198 +104,80 @@ const DealerOrders: React.FC = () => {
const renderOrderItem = (order: OrderWithDetails) => (
<View key={order.id} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
<View className="flex justify-between items-start mb-3">
<View>
<Text className="font-semibold text-gray-800 mb-1">
{order.orderNo}
</Text>
<Text className="text-sm text-gray-500">
{order.customerName}
</Text>
{/* 显示用户在此订单中的层级 */}
<Text className="text-xs text-blue-500">
{order.userLevel === 1 && '一级分销'}
{order.userLevel === 2 && '二级分销'}
{order.userLevel === 3 && '三级分销'}
</Text>
</View>
<View className="flex justify-between items-start mb-1">
<Text className="font-semibold text-gray-800">
{order.orderNo}
</Text>
<Tag type={getStatusColor(order.isSettled, order.isInvalid)}>
{getStatusText(order.isSettled, order.isInvalid)}
</Tag>
</View>
<View className="flex justify-between items-center mb-1">
<Text className="text-sm text-gray-400">
¥{order.orderPrice || '0.00'}
</Text>
<Text className="text-sm text-orange-500 font-semibold">
¥{order.userCommission}
</Text>
</View>
<View className="flex justify-between items-center">
<View>
<Text className="text-sm text-gray-600">
¥{order.orderPrice || '0.00'}
</Text>
<Text className="text-sm text-orange-500 font-semibold">
¥{order.userCommission}
</Text>
<Text className="text-xs text-gray-400">
¥{order.totalCommission}
</Text>
</View>
<Text className="text-xs text-gray-400">
<Text className="text-sm text-gray-400">
{order.customerName}
</Text>
<Text className="text-sm text-gray-400">
{order.createTime}
</Text>
</View>
</View>
)
// 根据状态和层级过滤订单
const getFilteredOrders = (filter: string) => {
switch (filter) {
case '1': // 一级分销
return orders.filter(order => order.userLevel === 1)
case '2': // 二级分销
return orders.filter(order => order.userLevel === 2)
case '3': // 三级分销
return orders.filter(order => order.userLevel === 3)
case '4': // 待结算
return orders.filter(order => order.isSettled === 0 && order.isInvalid === 0)
case '5': // 已结算
return orders.filter(order => order.isSettled === 1)
case '6': // 已失效
return orders.filter(order => order.isInvalid === 1)
default: // 全部
return orders
}
}
if (!dealerUser) {
return (
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
<Loading />
<Text className="text-gray-500 mt-2">...</Text>
</View>
)
}
return (
<View className="bg-gray-50 min-h-screen">
{/* 统计卡片 */}
<View className="bg-white p-4 mb-4">
{/* 总体统计 */}
<View className="grid grid-cols-3 gap-4 mb-4">
<View className="text-center">
<Text className="text-lg font-bold text-blue-500">{statistics.totalOrders}</Text>
<Text className="text-xs text-gray-500"></Text>
</View>
<View className="text-center">
<Text className="text-lg font-bold text-green-500">¥{statistics.totalCommission}</Text>
<Text className="text-xs text-gray-500"></Text>
</View>
<View className="text-center">
<Text className="text-lg font-bold text-orange-500">¥{statistics.pendingCommission}</Text>
<Text className="text-xs text-gray-500"></Text>
</View>
</View>
{/* 分层统计 */}
<View className="border-t pt-3">
<Text className="text-sm text-gray-600 mb-2"></Text>
<View className="grid grid-cols-3 gap-2">
<View className="text-center bg-gray-50 rounded p-2">
<Text className="text-sm font-semibold text-red-500">{statistics.level1.orders}</Text>
<Text className="text-xs text-gray-500"></Text>
<Text className="text-xs text-red-500">¥{statistics.level1.commission}</Text>
</View>
<View className="text-center bg-gray-50 rounded p-2">
<Text className="text-sm font-semibold text-blue-500">{statistics.level2.orders}</Text>
<Text className="text-xs text-gray-500"></Text>
<Text className="text-xs text-blue-500">¥{statistics.level2.commission}</Text>
</View>
<View className="text-center bg-gray-50 rounded p-2">
<Text className="text-sm font-semibold text-purple-500">{statistics.level3.orders}</Text>
<Text className="text-xs text-gray-500"></Text>
<Text className="text-xs text-purple-500">¥{statistics.level3.commission}</Text>
</View>
</View>
</View>
</View>
{/* 订单列表 */}
<Tabs value={activeTab} onChange={() => setActiveTab}>
<Tabs.TabPane title="全部" value="0">
<PullToRefresh
onRefresh={handleRefresh}
>
<View className="p-4">
{loading ? (
<View className="text-center py-8">
<Loading />
<Text className="text-gray-500 mt-2">...</Text>
</View>
) : getFilteredOrders('0').length > 0 ? (
getFilteredOrders('0').map(renderOrderItem)
) : (
<Empty description="暂无分销订单" />
)}
</View>
</PullToRefresh>
</Tabs.TabPane>
<Tabs.TabPane title="一级分销" value="1">
<View className="min-h-screen bg-gray-50">
<PullToRefresh
onRefresh={handleRefresh}
disabled={refreshing}
pullingText="下拉刷新"
canReleaseText="释放刷新"
refreshingText="刷新中..."
completeText="刷新完成"
>
<ScrollView
scrollY
className="h-screen"
onScrollToLower={handleLoadMore}
lowerThreshold={50}
>
<View className="p-4">
{getFilteredOrders('1').length > 0 ? (
getFilteredOrders('1').map(renderOrderItem)
{loading && orders.length === 0 ? (
<View className="text-center py-8">
<Loading/>
<Text className="text-gray-500 mt-2">...</Text>
</View>
) : orders.length > 0 ? (
<>
{orders.map(renderOrderItem)}
{loadingMore && (
<View className="text-center py-4">
<Loading/>
<Text className="text-gray-500 mt-1 text-sm">...</Text>
</View>
)}
{!hasMore && orders.length > 0 && (
<View className="text-center py-4">
<Text className="text-gray-400 text-sm"></Text>
</View>
)}
</>
) : (
<Empty description="暂无一级分销订单" />
<Empty description="暂无处方" style={{
backgroundColor: 'transparent'
}}/>
)}
</View>
</Tabs.TabPane>
<Tabs.TabPane title="二级分销" value="2">
<View className="p-4">
{getFilteredOrders('2').length > 0 ? (
getFilteredOrders('2').map(renderOrderItem)
) : (
<Empty description="暂无二级分销订单" />
)}
</View>
</Tabs.TabPane>
<Tabs.TabPane title="三级分销" value="3">
<View className="p-4">
{getFilteredOrders('3').length > 0 ? (
getFilteredOrders('3').map(renderOrderItem)
) : (
<Empty description="暂无三级分销订单" />
)}
</View>
</Tabs.TabPane>
<Tabs.TabPane title="待结算" value="4">
<View className="p-4">
{getFilteredOrders('4').length > 0 ? (
getFilteredOrders('4').map(renderOrderItem)
) : (
<Empty description="暂无待结算订单" />
)}
</View>
</Tabs.TabPane>
<Tabs.TabPane title="已结算" value="5">
<View className="p-4">
{getFilteredOrders('5').length > 0 ? (
getFilteredOrders('5').map(renderOrderItem)
) : (
<Empty description="暂无已结算订单" />
)}
</View>
</Tabs.TabPane>
<Tabs.TabPane title="已失效" value="6">
<View className="p-4">
{getFilteredOrders('6').length > 0 ? (
getFilteredOrders('6').map(renderOrderItem)
) : (
<Empty description="暂无失效订单" />
)}
</View>
</Tabs.TabPane>
</Tabs>
</ScrollView>
</PullToRefresh>
</View>
)
}

View File

@@ -1,7 +1,7 @@
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 {Download, QrCode} from '@nutui/icons-react-taro'
import Taro from '@tarojs/taro'
import {useDealerUser} from '@/hooks/useDealerUser'
import {generateInviteCode} from '@/api/invite'
@@ -115,52 +115,52 @@ const DealerQrcode: React.FC = () => {
}
// 复制邀请信息
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 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']
})
}
// const shareMiniProgramCode = () => {
// if (!dealerUser?.userId) {
// Taro.showToast({
// title: '用户信息未加载',
// icon: 'error'
// })
// return
// }
//
// // 小程序分享
// Taro.showShareMenu({
// withShareTicket: true,
// showShareItems: ['shareAppMessage']
// })
// }
if (!dealerUser) {
return (
@@ -263,29 +263,29 @@ const DealerQrcode: React.FC = () => {
</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 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 File

@@ -1,3 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '我的团队'
navigationBarTitleText: '患者管理'
})

View File

@@ -1,56 +1,151 @@
import React, { useState, useEffect, useCallback } from 'react'
import { View, Text } from '@tarojs/components'
import { Empty, Tabs, Avatar, Tag, Progress, Loading, PullToRefresh } from '@nutui/nutui-react-taro'
import { User, Star, StarFill } from '@nutui/icons-react-taro'
import React, {useState, useEffect, useCallback} from 'react'
import {View, Text} from '@tarojs/components'
import {Phone, Edit, Message} 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'
import { pageShopDealerOrder } from '@/api/shop/shopDealerOrder'
import type { ShopDealerReferee } from '@/api/shop/shopDealerReferee/model'
import {useDealerUser} from '@/hooks/useDealerUser'
import {listShopDealerReferee} from '@/api/shop/shopDealerReferee'
import {pageShopDealerOrder} from '@/api/shop/shopDealerOrder'
import type {ShopDealerReferee} from '@/api/shop/shopDealerReferee/model'
import FixedButton from "@/components/FixedButton";
import navTo from "@/utils/common";
import {updateUser} from "@/api/system/user";
interface TeamMemberWithStats extends ShopDealerReferee {
name?: string
avatar?: string
nickname?: string;
alias?: string;
phone?: string;
orderCount?: number
commission?: string
status?: 'active' | 'inactive'
subMembers?: number
joinTime?: string
dealerAvatar?: string;
dealerName?: string;
dealerPhone?: string;
}
// 层级信息接口
interface LevelInfo {
dealerId: number
dealerName?: string
level: number
}
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 [dealerId, setDealerId] = useState<number>()
// 层级栈,用于支持返回上一层
const [levelStack, setLevelStack] = useState<LevelInfo[]>([])
const [loading, setLoading] = useState(false)
// 当前查看的用户名称
const [currentDealerName, setCurrentDealerName] = useState<string>('')
const { dealerUser } = useDealerUser()
// 异步加载成员统计数据
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) return
if (!dealerUser?.userId && !dealerId) return
try {
setLoading(true)
console.log(dealerId, 'dealerId>>>>>>>>>')
// 获取团队成员关系
const refereeResult = await listShopDealerReferee({
dealerId: dealerUser.userId
dealerId: dealerId ? dealerId : dealerUser?.userId
})
if (refereeResult) {
console.log('团队成员原始数据:', refereeResult)
// 处理团队成员数据
const processedMembers: TeamMemberWithStats[] = refereeResult.map(member => ({
...member,
name: `用户${member.userId}`,
avatar: '',
name: `${member.userId}`,
orderCount: 0,
commission: '0.00',
status: 'active' as const,
@@ -58,62 +153,13 @@ const DealerTeam: React.FC = () => {
joinTime: member.createTime
}))
// 并行获取每个成员的订单统计
const memberStats = await Promise.all(
processedMembers.map(async (member) => {
try {
const orderResult = await pageShopDealerOrder({
page: 1,
limit: 100,
userId: member.userId
})
// 先显示基础数据,然后异步加载详细统计
setTeamMembers(processedMembers)
setLoading(false)
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)
// 异步加载每个成员的详细统计数据
loadMemberStats(processedMembers)
// 判断活跃状态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)
@@ -124,244 +170,270 @@ const DealerTeam: React.FC = () => {
} finally {
setLoading(false)
}
}, [dealerUser?.userId])
}, [dealerUser?.userId, dealerId])
// 刷新数据
const handleRefresh = async () => {
setRefreshing(true)
await fetchTeamData()
setRefreshing(false)
// 查看下级成员
const getNextUser = (item: TeamMemberWithStats) => {
// 检查层级限制最多只能查看2层levelStack.length >= 1 表示已经是第2层了
if (levelStack.length >= 1) {
return
}
// 如果没有下级成员,不允许点击
if (!item.subMembers || item.subMembers === 0) {
return
}
console.log('点击用户:', item.userId, item.name)
// 将当前层级信息推入栈中
const currentLevel: LevelInfo = {
dealerId: dealerId || dealerUser?.userId || 0,
dealerName: currentDealerName || (dealerId ? '上级' : dealerUser?.realName || '我'),
level: levelStack.length
}
setLevelStack(prev => [...prev, currentLevel])
// 切换到下级
setDealerId(item.userId)
setCurrentDealerName(item.nickname || item.dealerName || `用户${item.userId}`)
}
// 初始化加载数据
// 返回上一层
const goBack = () => {
if (levelStack.length === 0) {
// 如果栈为空,返回首页或上一页
Taro.navigateBack()
return
}
// 从栈中弹出上一层信息
const prevLevel = levelStack[levelStack.length - 1]
setLevelStack(prev => prev.slice(0, -1))
if (prevLevel.dealerId === (dealerUser?.userId || 0)) {
// 返回到根层级
setDealerId(undefined)
setCurrentDealerName('')
} else {
setDealerId(prevLevel.dealerId)
setCurrentDealerName(prevLevel.dealerName || '')
}
}
// 一键拨打
const makePhoneCall = (phone: string) => {
Taro.makePhoneCall({
phoneNumber: phone,
fail: () => {
Taro.showToast({
title: '拨打取消',
icon: 'error'
});
}
});
};
// 别名备注
const editAlias = (item: any, index: number) => {
Taro.showModal({
title: '备注',
// @ts-ignore
editable: true,
placeholderText: '真实姓名',
content: item.alias || '',
success: async (res: any) => {
if (res.confirm && res.content !== undefined) {
try {
// 更新跟进情况
await updateUser({
userId: item.userId,
alias: res.content.trim()
});
teamMembers[index].alias = res.content.trim()
setTeamMembers(teamMembers)
} catch (error) {
console.error('备注失败:', error);
Taro.showToast({
title: '备注失败,请重试',
icon: 'error'
});
}
}
}
});
};
// 发送消息
const sendMessage = (item: TeamMemberWithStats) => {
return navTo(`/user/chat/message/add?id=${item.userId}`, true)
}
// 监听数据变化,获取团队数据
useEffect(() => {
if (dealerUser?.userId) {
if (dealerUser?.userId || dealerId) {
fetchTeamData().then()
}
}, [fetchTeamData])
const getLevelColor = (level: number) => {
switch (level) {
case 1: return '#f59e0b'
case 2: return '#8b5cf6'
case 3: return '#ec4899'
default: return '#6b7280'
// 初始化当前用户名称
useEffect(() => {
if (!dealerId && dealerUser?.realName && !currentDealerName) {
setCurrentDealerName(dealerUser.realName)
}
}
}, [dealerUser, dealerId, currentDealerName])
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, index: number) => {
// 判断是否可以点击:有下级成员且未达到层级限制
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
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
key={member.id}
className={`bg-white rounded-lg p-4 mb-3 shadow-sm ${
canClick ? 'cursor-pointer' : 'cursor-default opacity-75'
}`}
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 justify-between mb-1">
<View className="flex items-center">
<Space>
{member.alias ? <Text className="font-semibold text-blue-700 mr-2">{member.alias}</Text> :
<Text className="font-semibold text-gray-800 mr-2">{member.nickname}</Text>}
{/*别名备注*/}
<Edit size={16} className={'text-blue-500 mr-2'} onClick={(e) => {
e.stopPropagation()
editAlias(member, index)
}}/>
{/*发送消息*/}
<Message size={16} className={'text-orange-500 mr-2'} onClick={(e) => {
e.stopPropagation()
sendMessage(member)
}}/>
</Space>
</View>
{/* 显示手机号(仅本级可见) */}
{showPhone && member.phone && (
<Text className="text-sm text-gray-500" onClick={(e) => {
e.stopPropagation();
makePhoneCall(member.phone || '');
}}>
{member.phone}
<Phone size={12} className="ml-1 text-green-500"/>
</Text>
)}
</View>
<Text className="text-xs text-gray-500">
{member.joinTime}
</Text>
</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">
{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>
)
}
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>
const renderOverview = () => (
<View className="rounded-xl p-4">
<View
className={'bg-white rounded-lg py-2 px-4 mb-3 shadow-sm text-right text-sm font-medium flex justify-between items-center'}>
<Text className="text-lg font-semibold"></Text>
<Text className={'text-gray-500 '}>{teamMembers.length}</Text>
</View>
{teamMembers.map(renderMemberItem)}
</View>
)
// 渲染顶部导航栏
const renderHeader = () => {
if (levelStack.length === 0) return null
return (
<View className="bg-white p-4 mb-3 shadow-sm">
<View className="flex items-center justify-between">
<View className="flex items-center">
<Text className="text-lg font-semibold">
{currentDealerName}
</Text>
</View>
<Button
size="small"
type="primary"
onClick={goBack}
className="bg-blue-500"
>
</Button>
</View>
</View>
)
}
if (!dealerUser) {
return (
<Space className="flex items-center justify-center">
<Empty description="您还不是业务人员" style={{
backgroundColor: 'transparent'
}} actions={[{text: '立即申请', onClick: () => navTo(`/doctor/apply/add`, true)}]}
/>
</Space>
)
}
return (
<>
{renderHeader()}
{loading ? (
<View className="flex items-center justify-center mt-20">
<Text className="text-gray-500">...</Text>
</View>
) : teamMembers.length > 0 ? (
renderOverview()
) : (
<View className="flex items-center justify-center mt-20">
<Empty description="暂无成员" style={{
backgroundColor: 'transparent'
}}/>
</View>
)}
<FixedButton text={'立即添加'} onClick={() => navTo(`/doctor/qrcode/index`, true)}/>
</>
)
}
export default DealerTeam
export default DealerTeam;

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '微信客服'
})

View File

@@ -0,0 +1,176 @@
.wechat-service-page {
min-height: 100vh;
.service-tabs {
background-color: #fff;
.nut-tabs__titles {
background-color: #fff;
}
.nut-tabs__content {
padding: 0;
}
}
.qr-container {
padding: 20px;
min-height: calc(100vh - 100px);
.qr-header {
text-align: center;
margin-bottom: 30px;
.qr-title {
display: block;
font-weight: bold;
color: #333;
margin-bottom: 8px;
}
.qr-description {
display: block;
color: #666;
line-height: 1.5;
}
}
.qr-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 30px;
.qr-code-wrapper {
background-color: #fff;
border-radius: 12px;
padding: 30px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
text-align: center;
.qr-code-image {
width: 360px;
height: 360px;
border-radius: 8px;
margin-bottom: 15px;
}
.wechat-id {
display: block;
color: #333;
font-weight: 500;
}
}
.qr-tips {
background-color: #fff;
border-radius: 12px;
padding: 20px;
width: 100%;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
.tip-title {
display: block;
font-weight: bold;
color: #333;
margin-bottom: 15px;
}
.tip-item {
display: block;
color: #666;
line-height: 1.8;
margin-bottom: 8px;
padding-left: 10px;
position: relative;
&:before {
content: '';
color: #07c160;
font-weight: bold;
position: absolute;
left: 0;
}
&:last-child {
margin-bottom: 0;
}
}
}
}
}
}
// 响应式适配
@media (max-width: 375px) {
.wechat-service-page {
.qr-container {
padding: 15px;
.qr-content {
.qr-code-wrapper {
padding: 20px;
.qr-code-image {
width: 180px;
height: 180px;
}
}
}
}
}
}
// 深色模式适配
@media (prefers-color-scheme: dark) {
.wechat-service-page {
background-color: #1a1a1a;
.service-tabs {
.nut-tabs__titles {
background-color: #2a2a2a;
border-bottom-color: #333;
}
}
.qr-container {
background-color: #1a1a1a;
.qr-header {
.qr-title {
color: #fff;
}
.qr-description {
color: #ccc;
}
}
.qr-content {
.qr-code-wrapper {
background-color: #2a2a2a;
.qr-code-image {
border-color: #444;
}
.wechat-id {
color: #fff;
}
}
.qr-tips {
background-color: #2a2a2a;
.tip-title {
color: #fff;
}
.tip-item {
color: #ccc;
}
}
}
}
}
}

121
src/doctor/wechat/index.tsx Normal file
View File

@@ -0,0 +1,121 @@
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";
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 className="qr-tips">
<Text className="tip-title">使</Text>
<Text className="tip-item">1. </Text>
<Text className="tip-item">2. </Text>
<Text className="tip-item">3. </Text>
<Text className="tip-item">4. </Text>
</View>
</View>
</View>
)
useEffect(() => {
listCmsWebsiteField({name: 'kefu'}).then(data => {
if (data) {
setCodes(data)
}
})
}, []);
return (
<View className="wechat-service-page">
<Tabs
value={activeTab}
onChange={(value) => setActiveTab(`${value}`)}
className="service-tabs"
>
{codes.map((item) => (
<Tabs.TabPane key={item.id} title={item.comments} value={item.id}>
{renderQRCode(item)}
</Tabs.TabPane>
))}
</Tabs>
</View>
)
}
export default WechatService

View File

@@ -0,0 +1,80 @@
import React, { useState } from 'react'
import { View, Text } from '@tarojs/components'
import { Tabs, Button } from '@nutui/nutui-react-taro'
/**
* 提现功能调试组件
* 用于测试 Tabs 组件的点击和切换功能
*/
const WithdrawDebug: React.FC = () => {
const [activeTab, setActiveTab] = useState<string | number>('0')
const [clickCount, setClickCount] = useState(0)
// Tab 切换处理函数
const handleTabChange = (value: string | number) => {
console.log('Tab切换:', { from: activeTab, to: value, type: typeof value })
setActiveTab(value)
setClickCount(prev => prev + 1)
}
// 手动切换测试
const manualSwitch = (tab: string | number) => {
console.log('手动切换到:', tab)
setActiveTab(tab)
setClickCount(prev => prev + 1)
}
return (
<View className="bg-gray-50 min-h-screen p-4">
<View className="bg-white rounded-lg p-4 mb-4">
<Text className="text-lg font-bold mb-2"></Text>
<Text className="block mb-1">Tab: {String(activeTab)}</Text>
<Text className="block mb-1">: {clickCount}</Text>
<Text className="block mb-1">Tab类型: {typeof activeTab}</Text>
</View>
<View className="bg-white rounded-lg p-4 mb-4">
<Text className="text-lg font-bold mb-2"></Text>
<View className="flex gap-2">
<Button size="small" onClick={() => manualSwitch('0')}>
</Button>
<Button size="small" onClick={() => manualSwitch('1')}>
</Button>
</View>
</View>
<View className="bg-white rounded-lg">
<Tabs value={activeTab} onChange={handleTabChange}>
<Tabs.TabPane title="申请提现" value="0">
<View className="p-4">
<Text className="text-center text-gray-600"></Text>
<Text className="text-center text-sm text-gray-400 mt-2">
Tab值: {String(activeTab)}
</Text>
</View>
</Tabs.TabPane>
<Tabs.TabPane title="提现记录" value="1">
<View className="p-4">
<Text className="text-center text-gray-600"></Text>
<Text className="text-center text-sm text-gray-400 mt-2">
Tab值: {String(activeTab)}
</Text>
</View>
</Tabs.TabPane>
</Tabs>
</View>
<View className="bg-white rounded-lg p-4 mt-4">
<Text className="text-lg font-bold mb-2"></Text>
<Text className="text-sm text-gray-500">
</Text>
</View>
</View>
)
}
export default WithdrawDebug

View File

@@ -1,49 +1,67 @@
import React, { useState, useRef, useEffect, useCallback } from 'react'
import { View, Text } from '@tarojs/components'
import React, {useState, useEffect, useCallback} from 'react'
import {View, Text} from '@tarojs/components'
import {
Cell,
Space,
Button,
Form,
Input,
CellGroup,
Radio,
Tabs,
Tag,
Empty,
ActionSheet,
Loading,
PullToRefresh
} from '@nutui/nutui-react-taro'
import { Wallet } from '@nutui/icons-react-taro'
import { businessGradients } from '@/styles/gradients'
import {Wallet, ArrowRight} from '@nutui/icons-react-taro'
import {businessGradients} from '@/styles/gradients'
import Taro from '@tarojs/taro'
import { useDealerUser } from '@/hooks/useDealerUser'
import { pageShopDealerWithdraw, addShopDealerWithdraw } from '@/api/shop/shopDealerWithdraw'
import type { ShopDealerWithdraw } from '@/api/shop/shopDealerWithdraw/model'
import {useDealerUser} from '@/hooks/useDealerUser'
import {pageShopDealerWithdraw, addShopDealerWithdraw} from '@/api/shop/shopDealerWithdraw'
import type {ShopDealerWithdraw} from '@/api/shop/shopDealerWithdraw/model'
import {ShopDealerBank} from "@/api/shop/shopDealerBank/model";
import {listShopDealerBank} from "@/api/shop/shopDealerBank";
import {listCmsWebsiteField} from "@/api/cms/cmsWebsiteField";
interface WithdrawRecordWithDetails extends ShopDealerWithdraw {
accountDisplay?: string
}
const DealerWithdraw: React.FC = () => {
const [activeTab, setActiveTab] = useState('0')
const [selectedAccount, setSelectedAccount] = useState('')
const [activeTab, setActiveTab] = useState<string | number>('0')
const [loading, setLoading] = useState<boolean>(false)
const [refreshing, setRefreshing] = useState<boolean>(false)
const [submitting, setSubmitting] = useState<boolean>(false)
const [banks, setBanks] = useState<any[]>([])
const [bank, setBank] = useState<ShopDealerBank>()
const [isVisible, setIsVisible] = useState<boolean>(false)
const [availableAmount, setAvailableAmount] = useState<string>('0.00')
const [withdrawRecords, setWithdrawRecords] = useState<WithdrawRecordWithDetails[]>([])
const formRef = useRef<any>(null)
const [withdrawAmount, setWithdrawAmount] = useState<string>('')
const [withdrawValue, setWithdrawValue] = useState<string>('')
const { dealerUser } = useDealerUser()
const {dealerUser} = useDealerUser()
// Tab 切换处理函数
const handleTabChange = (value: string | number) => {
console.log('Tab切换到:', value)
setActiveTab(value)
// 如果切换到提现记录页面,刷新数据
if (String(value) === '1') {
fetchWithdrawRecords().then()
}
}
// 获取可提现余额
const fetchBalance = useCallback(async () => {
console.log(dealerUser, 'dealerUser...')
try {
setAvailableAmount(dealerUser?.money || '0.00')
setAvailableAmount(String(dealerUser?.money || '0.00'))
} catch (error) {
console.error('获取余额失败:', error)
}
}, [])
}, [dealerUser])
// 获取提现记录
const fetchWithdrawRecords = useCallback(async () => {
@@ -75,6 +93,21 @@ const DealerWithdraw: React.FC = () => {
}
}, [dealerUser?.userId])
function fetchBanks() {
listShopDealerBank({}).then(data => {
const list = data.map(d => {
d.name = d.bankName;
d.type = d.bankName;
return d;
})
setBanks(list.concat({
name: '管理银行卡',
type: 'add'
}))
setBank(data[0])
})
}
// 格式化账户显示
const getAccountDisplay = (record: ShopDealerWithdraw) => {
if (record.payType === 10) {
@@ -94,35 +127,66 @@ const DealerWithdraw: React.FC = () => {
setRefreshing(false)
}
const handleSelect = (item: ShopDealerBank) => {
if(item.type === 'add'){
return Taro.navigateTo({
url: '/doctor/bank/index'
})
}
setBank(item)
setIsVisible(false)
}
function fetchCmsField() {
listCmsWebsiteField({ name: 'WithdrawValue'}).then(res => {
if(res && res.length > 0){
const text = res[0].value;
setWithdrawValue(text || '')
}
})
}
// 初始化加载数据
useEffect(() => {
if (dealerUser?.userId) {
fetchBalance().then()
fetchWithdrawRecords().then()
fetchBanks()
fetchCmsField()
}
}, [fetchBalance, fetchWithdrawRecords])
const getStatusText = (status?: number) => {
switch (status) {
case 40: return '已到账'
case 20: return '审核通过'
case 10: return '待审核'
case 30: return '已驳回'
default: return '未知'
case 40:
return '已到账'
case 20:
return '审核通过'
case 10:
return '待审核'
case 30:
return '已驳回'
default:
return '未知'
}
}
const getStatusColor = (status?: number) => {
switch (status) {
case 40: return 'success'
case 20: return 'success'
case 10: return 'warning'
case 30: return 'danger'
default: return 'default'
case 40:
return 'success'
case 20:
return 'success'
case 10:
return 'warning'
case 30:
return 'danger'
default:
return 'default'
}
}
const handleSubmit = async (values: any) => {
const handleSubmit = async () => {
if (!dealerUser?.userId) {
Taro.showToast({
title: '用户信息获取失败',
@@ -131,9 +195,26 @@ const DealerWithdraw: React.FC = () => {
return
}
if (!bank) {
Taro.showToast({
title: '请选择提现银行卡',
icon: 'error'
})
return
}
// 验证提现金额
const amount = parseFloat(values.amount)
const available = parseFloat(availableAmount.replace(',', ''))
const amount = parseFloat(withdrawAmount)
const availableStr = String(availableAmount || '0')
const available = parseFloat(availableStr.replace(/,/g, ''))
if (isNaN(amount) || amount <= 0) {
Taro.showToast({
title: '请输入有效的提现金额',
icon: 'error'
})
return
}
if (amount < 100) {
Taro.showToast({
@@ -151,26 +232,27 @@ const DealerWithdraw: React.FC = () => {
return
}
// 验证银行卡信息
if (!bank.bankCard || !bank.bankAccount || !bank.bankName) {
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,
money: withdrawAmount,
payType: 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 || '银行卡'
platform: 'MiniProgram',
bankCard: bank.bankCard,
bankAccount: bank.bankAccount,
bankName: bank.bankName
}
await addShopDealerWithdraw(withdrawData)
@@ -181,8 +263,7 @@ const DealerWithdraw: React.FC = () => {
})
// 重置表单
formRef.current?.resetFields()
setSelectedAccount('')
setWithdrawAmount('')
// 刷新数据
await handleRefresh()
@@ -201,18 +282,26 @@ const DealerWithdraw: React.FC = () => {
}
}
const quickAmounts = ['100', '300', '500', '1000']
const setQuickAmount = (amount: string) => {
formRef.current?.setFieldsValue({ amount })
// 格式化金额
const formatMoney = (money?: string) => {
if (!money) return '0.00'
return parseFloat(money).toFixed(2)
}
const setAllAmount = () => {
formRef.current?.setFieldsValue({ amount: availableAmount.replace(',', '') })
// 计算预计到账金额
const calculateExpectedAmount = (amount: string) => {
if (!amount || isNaN(parseFloat(amount))) return '0.00'
const withdrawAmount = parseFloat(amount)
// 提现费率 16% + 3元
const feeRate = 0.16
const fixedFee = 3
const totalFee = withdrawAmount * feeRate + fixedFee
const expectedAmount = withdrawAmount - totalFee
return Math.max(0, expectedAmount).toFixed(2)
}
const renderWithdrawForm = () => (
<View className="p-4">
<View>
{/* 余额卡片 */}
<View className="rounded-xl p-6 mb-6 text-white relative overflow-hidden" style={{
background: businessGradients.dealer.header
@@ -225,190 +314,132 @@ const DealerWithdraw: React.FC = () => {
}}></View>
<View className="flex items-center justify-between relative z-10">
<View>
<View className={'flex flex-col'}>
<Text className="text-2xl font-bold text-white">{formatMoney(dealerUser?.money)}</Text>
<Text className="text-white text-opacity-80 text-sm mb-1"></Text>
<Text className="text-2xl font-bold text-white">¥{availableAmount}</Text>
</View>
<View className="p-3 rounded-full" style={{
background: 'rgba(255, 255, 255, 0.2)'
}}>
<Wallet color="white" size="32" />
<Wallet color="white" size="32"/>
</View>
</View>
<View className="mt-4 pt-4 relative z-10" style={{
borderTop: '1px solid rgba(255, 255, 255, 0.3)'
}}>
<Text className="text-white text-opacity-80 text-xs">
¥100 |
¥100
</Text>
</View>
</View>
<Form
ref={formRef}
onFinish={handleSubmit}
labelPosition="top"
>
<CellGroup>
<Form.Item name="amount" label="提现金额" required>
<CellGroup>
<Cell style={{
padding: '36px 12px'
}} title={
<View className="flex items-center justify-between">
<Text className={'text-xl'}></Text>
<Input
placeholder="请输入提现金额"
placeholder="提现金额"
type="number"
clearable
maxLength={7}
value={withdrawAmount}
onChange={(value) => setWithdrawAmount(value)}
style={{
padding: '0 10px',
fontSize: '20px'
}}
/>
</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>
<Button fill="none" size={'small'} onClick={() => setWithdrawAmount(dealerUser?.money || '0')}><Text className={'text-blue-500'}></Text></Button>
</View>
}
/>
<Cell title={'提现到'} onClick={() => setIsVisible(true)} extra={
<View className="flex items-center justify-between gap-1">
{bank ? <Text className={'text-gray-800'}>{bank.bankName}</Text> : <Text className={'text-gray-400'}></Text>}
<ArrowRight className={'text-gray-300'} size={15}/>
</View>
}/>
<Cell title={'预计到账金额'} description={'提现费率 16% +3元'} extra={
<View className="flex items-center justify-between gap-1">
<Text className={'text-orange-500 px-2 text-lg'}>¥{calculateExpectedAmount(withdrawAmount)}</Text>
</View>
}/>
<Cell title={<Text className={'text-gray-400'}>{withdrawValue}</Text>}/>
</CellGroup>
<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 className="mt-6 px-4">
<Button
block
type="primary"
nativeType="submit"
loading={submitting}
disabled={submitting || !withdrawAmount || !bank}
onClick={handleSubmit}
>
{submitting ? '提交中...' : '申请提现'}
</Button>
</View>
</View>
)
const renderWithdrawRecords = () => (
<PullToRefresh
disabled={refreshing}
onRefresh={handleRefresh}
>
<View className="p-4">
{loading ? (
<View className="text-center py-8">
<Loading />
<Text className="text-gray-500 mt-2">...</Text>
</View>
) : withdrawRecords.length > 0 ? (
withdrawRecords.map(record => (
<View key={record.id} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
<View className="flex justify-between items-start mb-3">
<View>
<Text className="font-semibold text-gray-800 mb-1">
¥{record.money}
</Text>
<Text className="text-sm text-gray-500">
{record.accountDisplay}
</Text>
</View>
<Tag type={getStatusColor(record.applyStatus)}>
{getStatusText(record.applyStatus)}
</Tag>
</View>
const renderWithdrawRecords = () => {
console.log('渲染提现记录:', {loading, recordsCount: withdrawRecords.length, dealerUserId: dealerUser?.userId})
<View className="text-xs text-gray-400">
<Text>{record.createTime}</Text>
{record.auditTime && (
<Text className="block mt-1">
{new Date(record.auditTime).toLocaleString()}
</Text>
)}
{record.rejectReason && (
<Text className="block mt-1 text-red-500">
{record.rejectReason}
</Text>
)}
</View>
</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>
<PullToRefresh
disabled={refreshing}
onRefresh={handleRefresh}
>
<View>
{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="rounded-lg bg-gray-50 p-4 mb-3 shadow-sm">
<View className="flex justify-between items-start mb-3">
<Space direction={'vertical'}>
<Text className="font-semibold text-gray-800 mb-1">
¥{record.money}
</Text>
<Text className="text-sm text-gray-500">
{record.accountDisplay}
</Text>
</Space>
<Tag type={getStatusColor(record.applyStatus)}>
{getStatusText(record.applyStatus)}
</Tag>
</View>
<View className="text-xs text-gray-400">
<Text>{record.createTime}</Text>
{record.auditTime && (
<Text className="block mt-1">
{new Date(record.auditTime).toLocaleString()}
</Text>
)}
{record.rejectReason && (
<Text className="block mt-1 text-red-500">
{record.rejectReason}
</Text>
)}
</View>
</View>
))
) : (
<Empty description="暂无提现记录"/>
)}
</View>
</PullToRefresh>
)
}
return (
<View className="bg-gray-50 min-h-screen">
<Tabs value={activeTab} onChange={() => setActiveTab}>
<Tabs value={activeTab} onChange={handleTabChange}>
<Tabs.TabPane title="申请提现" value="0">
{renderWithdrawForm()}
</Tabs.TabPane>
@@ -417,6 +448,12 @@ const DealerWithdraw: React.FC = () => {
{renderWithdrawRecords()}
</Tabs.TabPane>
</Tabs>
<ActionSheet
visible={isVisible}
options={banks}
onSelect={handleSelect}
onCancel={() => setIsVisible(false)}
/>
</View>
)
}