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

@@ -9,7 +9,7 @@ export const ENV_CONFIG = {
// 生产环境
production: {
API_BASE_URL: 'https://cms-api.websoft.top/api',
APP_NAME: '时里院子市集',
APP_NAME: '通源堂健康生态平台',
DEBUG: 'false',
},
// 测试环境

View File

@@ -1,7 +1,7 @@
{
"miniprogramRoot": "./",
"projectname": "mp-react",
"description": "时里院子市集",
"projectname": "template-10559",
"description": "通源堂健康生态平台",
"appid": "touristappid",
"setting": {
"urlCheck": true,

View File

@@ -36,7 +36,7 @@ const UserCell = () => {
backgroundImage: 'linear-gradient(to right bottom, #54a799, #177b73)',
}}
title={
<View style={{display: 'inline-flex', alignItems: 'center'}} onClick={() => navTo('/dealer/index', true)}>
<View style={{display: 'inline-flex', alignItems: 'center'}} onClick={() => navTo('/doctor/index', true)}>
<Reward className={'text-orange-100 '} size={16}/>
<Text style={{fontSize: '16px'}} className={'pl-3 text-orange-100 font-medium'}></Text>
<Text className={'text-white opacity-80 pl-3'}></Text>

View File

@@ -102,7 +102,7 @@ export async function getCmsAd(id: number) {
}
/**
* 根据id查询广告位
* 根据code查询广告位
*/
export async function getCmsAdByCode(code: string) {
const res = await request.get<ApiResult<CmsAd>>(

View File

@@ -1,4 +1,4 @@
import type { PageParam } from '@/api/index';
import type { PageParam } from '@/api';
/**
* 广告位

View File

@@ -1,5 +1,5 @@
import request from '@/utils/request';
import type {ApiResult, PageResult} from '@/api/index';
import type {ApiResult, PageResult} from '@/api';
import type {CmsArticle, CmsArticleParam} from './model';
/**
@@ -204,3 +204,15 @@ export async function getByIds(params?: CmsArticleParam) {
return Promise.reject(new Error(res.message));
}
/**
* 根据code查询文章
*/
export async function getCmsArticleByCode(code: string) {
const res = await request.get<ApiResult<CmsArticle>>(
'/cms/cms-article/getByCode/' + code
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -1,4 +1,4 @@
import type { PageParam } from '@/api/index';
import type { PageParam } from '@/api';
/**
* 应用参数
@@ -48,10 +48,13 @@ export interface Config {
loginBgImg?: string;
address?: string;
tel?: string;
theme?: string;
workDay?: string;
kefu2?: string;
kefu1?: string;
email?: string;
loginTitle?: string;
sysLogo?: string;
vipText?: string;
vipComments?: string;
}

218
src/api/cmsArticle/index.ts Normal file
View File

@@ -0,0 +1,218 @@
import request from '@/utils/request';
import type {ApiResult, PageResult} from '@/api';
import type {CmsArticle, CmsArticleParam} from './model';
/**
* 分页查询文章
*/
export async function pageCmsArticle(params: CmsArticleParam) {
const res = await request.get<ApiResult<PageResult<CmsArticle>>>(
'/cms/cms-article/page',
params
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 查询文章列表
*/
export async function listCmsArticle(params?: CmsArticleParam) {
const res = await request.get<ApiResult<CmsArticle[]>>(
'/cms/cms-article',
params
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 添加文章
*/
export async function addCmsArticle(data: CmsArticle) {
const res = await request.post<ApiResult<unknown>>(
'/cms/cms-article',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 修改文章
*/
export async function updateCmsArticle(data: CmsArticle) {
const res = await request.put<ApiResult<unknown>>(
'/cms/cms-article',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 删除文章
*/
export async function removeCmsArticle(id?: number) {
const res = await request.del<ApiResult<unknown>>(
'/cms/cms-article/' + id
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 批量删除文章
*/
export async function removeBatchCmsArticle(data: (number | undefined)[]) {
const res = await request.del<ApiResult<unknown>>(
'/cms/cms-article/batch',
{
data
}
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 根据id查询文章
*/
export async function getCmsArticle(id: number) {
const res = await request.get<ApiResult<CmsArticle>>(
'/cms/cms-article/' + id
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
export async function getCount(params?: CmsArticleParam) {
const res = await request.get<ApiResult<unknown>>('/cms/cms-article/data', {
params
});
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 上一篇
* @param params
*/
export async function getPrevious(params?: CmsArticleParam) {
const res = await request.get<ApiResult<CmsArticle>>(
'/cms/cms-article/getPrevious/' + params?.articleId,
{
params
}
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 下一篇
* @param params
*/
export async function getNext(params?: CmsArticleParam) {
const res = await request.get<ApiResult<CmsArticle>>(
'/cms/cms-article/getNext/' + params?.articleId,
{
params
}
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 验证文章密码
* @param params
*/
export async function checkArticlePassword(params?: CmsArticleParam) {
const res = await request.get<ApiResult<unknown>>(
'/cms/cms-article/checkArticlePassword',
{
params
}
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
export async function findTags(params?: CmsArticleParam) {
const res = await request.get<ApiResult<CmsArticle[]>>(
'/cms/cms-article/findTags',
{
params
}
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
export async function pageTags(params?: CmsArticleParam) {
const res = await request.get<ApiResult<CmsArticle[]>>(
'/cms/cms-article/pageTags',
{
params
}
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 按IDS查询文章
* @param params
*/
export async function getByIds(params?: CmsArticleParam) {
const res = await request.get<ApiResult<CmsArticle[]>>(
'/cms/cms-article/getByIds',
{
params
}
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 根据code查询文章
*/
export async function getByCode(code: string) {
const res = await request.get<ApiResult<CmsArticle>>(
'/cms/cms-article/getByCode/' + code
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -0,0 +1,132 @@
import type { PageParam } from '@/api/index';
/**
* 文章
*/
export interface CmsArticle {
// 文章ID
articleId?: number;
// 文章标题
title?: string;
// 文章类型 0常规 1视频
type?: number;
// 文章模型
model?: string;
// 文章详情页模板
detail?: string;
// banner图片
banner?: string;
// 列表显示方式(10小图展示 20大图展示)
showType?: number;
// 话题
topic?: string;
// 标签
tags?: any;
// 栏目ID
categoryId?: number;
// 栏目名称
categoryName?: string;
// 封面图
image?: string;
// 价格
price?: number;
startTime?: any;
endTime?: any;
// 缩列图
thumbnail?: string;
// 来源
source?: string;
// 产品概述
overview?: string;
// 虚拟阅读量(仅用作展示)
virtualViews?: number;
// 实际阅读量
actualViews?: number;
// 购买人数
bmUsers?: number;
// 浏览权限(0公开 1会员 2密码)
permission?: number;
// 访问密码
password?: string;
// 确认密码
password2?: string;
// 发布来源客户端 (APP、H5、小程序等)
platform?: string;
// 文章附件
files?: string;
// 视频地址
video?: string;
// 接受的文件类型
accept?: string;
// 经度
longitude?: string;
// 纬度
latitude?: string;
// 所在省份
province?: string;
// 所在城市
city?: string;
// 所在辖区
region?: string;
// 街道地址
address?: string;
// 点赞数
likes?: number;
// pdf地址
pdfUrl?: string;
// 评论数
commentNumbers?: number;
// 提醒谁看
toUsers?: string;
// 文章内容
content?: string;
// 是否推荐
recommend?: number;
// 用户ID
userId?: number;
// 排序(数字越小越靠前)
sortNumber?: number;
// 备注
comments?: string;
// 状态, 0已发布, 1待审核 2已驳回 3违规内容
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
// 父级id
parentId?: number;
nickname?: string;
username?: string;
author?: string;
shopId?: number;
tenantName?: string;
logo?: string;
fileList?: any;
// 编辑器类型
editor?: number;
}
/**
* 文章搜索条件
*/
export interface CmsArticleParam extends PageParam {
articleId?: number;
articleIds?: string;
categoryId?: number;
parentId?: number;
status?: number;
// 是否推荐
recommend?: number;
keywords?: string;
// 验证密码
password?: string;
password2?: string;
tags?: string;
detail?: string;
sceneType?: string;
}

View File

@@ -111,6 +111,7 @@ export interface InviteRecordParam {
*/
export async function generateMiniProgramCode(data: MiniProgramCodeParam) {
try {
// return 'http://127.0.0.1:9200/api/wx-login/getOrderQRCodeUnlimited/' + data.scene;
const url = '/wx-login/getOrderQRCodeUnlimited/' + data.scene;
// 由于接口直接返回图片buffer我们直接构建完整的URL
return `${BaseUrl}${url}`;

View File

@@ -1,4 +1,4 @@
import type { PageParam } from '@/api/index';
import type { PageParam } from '@/api';
/**
* 分销商申请记录表
@@ -10,6 +10,14 @@ export interface ShopDealerApply {
userId?: number;
// 姓名
realName?: string;
// 分销商名称
dealerName?: string;
// 分销商编号
dealerCode?: string;
// 详细地址
address?: string;
// 金额
money?: number;
// 手机号
mobile?: string;
// 推荐人用户ID
@@ -17,7 +25,9 @@ export interface ShopDealerApply {
// 申请方式(10需后台审核 20无需审核)
applyType?: number;
// 申请时间
applyTime?: number;
applyTime?: string;
// 签单时间
contractTime?: string;
// 审核状态 (10待审核 20审核通过 30驳回)
applyStatus?: number;
// 审核时间
@@ -30,6 +40,14 @@ export interface ShopDealerApply {
createTime?: string;
// 修改时间
updateTime?: string;
// 过期时间
expirationTime?: string;
// 备注
comments?: string;
// 昵称
nickName?: string;
// 推荐人名称
refereeName?: string;
}
/**
@@ -37,7 +55,10 @@ export interface ShopDealerApply {
*/
export interface ShopDealerApplyParam extends PageParam {
applyId?: number;
type?: number;
dealerName?: string;
mobile?: string;
userId?: number;
keywords?: string;
applyStatus?: number; // 申请状态筛选 (10待审核 20审核通过 30驳回)
}

View File

@@ -73,16 +73,20 @@ export default {
]
},
{
"root": "dealer",
"root": "doctor",
"pages": [
"index",
"apply/add",
"withdraw/index",
"orders/index",
"orders/add",
"team/index",
"qrcode/index",
"invite-stats/index",
"info"
"info",
"customer/index",
"customer/add",
"customer/trading",
]
},
{

View File

@@ -7,6 +7,7 @@ import {loginByOpenId} from "@/api/layout";
import {TenantId} from "@/config/app";
import {saveStorageByLoginUser} from "@/utils/server";
import {parseInviteParams, saveInviteParams, trackInviteSource, handleInviteRelation, debugInviteInfo} from "@/utils/invite";
import {configWebsiteField} from "@/api/cms/cmsWebsiteField";
function App(props: { children: any; }) {
const reload = () => {
@@ -52,6 +53,7 @@ function App(props: { children: any; }) {
// 处理小程序启动参数中的邀请信息
const options = Taro.getLaunchOptionsSync()
handleLaunchOptions(options)
handleTheme()
})
// 处理启动参数
@@ -94,6 +96,15 @@ function App(props: { children: any; }) {
}
}
const handleTheme = () => {
configWebsiteField().then(data => {
// 设置主题
if(data.theme && !Taro.getStorageSync('user_theme')){
Taro.setStorageSync('user_theme', data.theme)
}
})
}
// 对应 onHide
useDidHide(() => {
})

View File

@@ -44,7 +44,7 @@ function Category() {
useShareAppMessage(() => {
return {
title: `${nav?.categoryName}_时里院子市集`,
title: `${nav?.categoryName}_通源堂健康生态平台`,
path: `/shop/category/index?id=${categoryId}`,
success: function () {
console.log('分享成功');

View File

@@ -29,7 +29,7 @@ export interface UnifiedQRButtonProps {
* 支持登录和核销两种类型的二维码扫描
*/
const UnifiedQRButton: React.FC<UnifiedQRButtonProps> = ({
type = 'default',
type = 'danger',
size = 'small',
text = '扫码',
showIcon = true,

View File

@@ -1,433 +0,0 @@
import {useEffect, useState, useRef} from "react";
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 {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, loginUser} = useUser()
const [loading, setLoading] = useState<boolean>(true)
const [FormData, setFormData] = useState<User>()
const formRef = useRef<any>(null)
const reload = async () => {
const inviteParams = getStoredInviteParams()
if (inviteParams?.inviter) {
setFormData({
...user,
refereeId: Number(inviteParams.inviter),
// 清空昵称,强制用户手动输入
nickname: '',
})
} else {
// 如果没有邀请参数,也要确保昵称为空
setFormData({
...user,
nickname: '',
})
}
}
const uploadAvatar = ({detail}: ChooseAvatarEvent) => {
// 先更新本地显示的头像(临时显示)
const tempFormData = {
...FormData,
avatar: `${detail.avatarUrl}`,
}
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 || ''
})
}
})
}
// 提交表单
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...')
// 准备提交的数据
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
});
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
})
}
Taro.showToast({
title: `注册成功`,
icon: 'success'
});
setTimeout(() => {
Taro.navigateBack();
}, 1000);
} catch (error) {
console.error('验证邀请人失败:', 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 = () => {
// 触发表单提交
formRef.current?.submit();
};
const submitFailed = (error: any) => {
console.log(error, 'err...')
}
useEffect(() => {
reload().then(() => {
setLoading(false)
})
}, [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>
}
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="refereeId" label="邀请人ID" initialValue={FormData?.refereeId} required>
<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>
{/* 底部浮动按钮 */}
<FixedButton
icon={<Edit/>}
text={'立即注册'}
onClick={handleFixedButtonClick}
/>
</>
);
};
export default AddUserAddress;

View File

@@ -1,3 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '客户列表'
})

View File

@@ -1,3 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '分销中心'
})

View File

View File

@@ -1,295 +0,0 @@
import React from 'react'
import {View, Text} from '@tarojs/components'
import {ConfigProvider, Button, Grid, Avatar} from '@nutui/nutui-react-taro'
import {
User,
Shopping,
Dongdong,
ArrowRight,
Purse,
People
} from '@nutui/icons-react-taro'
import {useDealerUser} from '@/hooks/useDealerUser'
import { useThemeStyles } from '@/hooks/useTheme'
import {businessGradients, cardGradients, gradientUtils} from '@/styles/gradients'
import Taro from '@tarojs/taro'
const DealerIndex: React.FC = () => {
const {
dealerUser,
error,
refresh,
} = useDealerUser()
// 使用主题样式
const themeStyles = useThemeStyles()
// 导航到各个功能页面
const navigateToPage = (url: string) => {
Taro.navigateTo({url})
}
// 格式化金额
const formatMoney = (money?: string) => {
if (!money) return '0.00'
return parseFloat(money).toFixed(2)
}
// 格式化时间
const formatTime = (time?: string) => {
if (!time) return '-'
return new Date(time).toLocaleDateString()
}
// 获取用户主题
const userTheme = gradientUtils.getThemeByUserId(dealerUser?.userId)
// 获取渐变背景
const getGradientBackground = (themeColor?: string) => {
if (themeColor) {
const darkerColor = gradientUtils.adjustColorBrightness(themeColor, -30)
return gradientUtils.createGradient(themeColor, darkerColor)
}
return userTheme.background
}
console.log(getGradientBackground(),'getGradientBackground()')
if (error) {
return (
<View className="p-4">
<View className="bg-red-50 border border-red-200 rounded-lg p-4 mb-4">
<Text className="text-red-600">{error}</Text>
</View>
<Button type="primary" onClick={refresh}>
</Button>
</View>
)
}
return (
<View className="bg-gray-100 min-h-screen">
<View>
{/*头部信息*/}
{dealerUser && (
<View className="px-4 py-6 relative overflow-hidden" style={themeStyles.primaryBackground}>
{/* 装饰性背景元素 - 小程序兼容版本 */}
<View className="absolute w-32 h-32 rounded-full" style={{
backgroundColor: 'rgba(255, 255, 255, 0.1)',
top: '-16px',
right: '-16px'
}}></View>
<View className="absolute w-24 h-24 rounded-full" style={{
backgroundColor: 'rgba(255, 255, 255, 0.08)',
bottom: '-12px',
left: '-12px'
}}></View>
<View className="absolute w-16 h-16 rounded-full" style={{
backgroundColor: 'rgba(255, 255, 255, 0.05)',
top: '60px',
left: '120px'
}}></View>
<View className="flex items-center justify-between relative z-10 mb-4">
<Avatar
size="50"
src={dealerUser?.qrcode}
icon={<User/>}
className="mr-4"
style={{
border: '2px solid rgba(255, 255, 255, 0.3)'
}}
/>
<View className="flex-1 flex-col">
<View className="text-white text-lg font-bold mb-1" style={{
}}>
{dealerUser?.realName || '分销商'}
</View>
<View className="text-sm" style={{
color: 'rgba(255, 255, 255, 0.8)'
}}>
ID: {dealerUser.userId} | : {dealerUser.refereeId || '无'}
</View>
</View>
<View className="text-right hidden">
<Text className="text-xs" style={{
color: 'rgba(255, 255, 255, 0.9)'
}}></Text>
<Text className="text-xs" style={{
color: 'rgba(255, 255, 255, 0.7)'
}}>
{formatTime(dealerUser.createTime)}
</Text>
</View>
</View>
</View>
)}
{/* 佣金统计卡片 */}
{dealerUser && (
<View className="mx-4 -mt-6 rounded-xl p-4 relative z-10" style={cardGradients.elevated}>
<View className="mb-4">
<Text className="font-semibold text-gray-800"></Text>
</View>
<View className="grid grid-cols-3 gap-3">
<View className="text-center p-3 rounded-lg" style={{
background: businessGradients.money.available
}}>
<Text className="text-lg font-bold mb-1 text-white">
{formatMoney(dealerUser.money)}
</Text>
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}></Text>
</View>
<View className="text-center p-3 rounded-lg" style={{
background: businessGradients.money.frozen
}}>
<Text className="text-lg font-bold mb-1 text-white">
{formatMoney(dealerUser.freezeMoney)}
</Text>
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}></Text>
</View>
<View className="text-center p-3 rounded-lg" style={{
background: businessGradients.money.total
}}>
<Text className="text-lg font-bold mb-1 text-white">
{formatMoney(dealerUser.totalMoney)}
</Text>
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}></Text>
</View>
</View>
</View>
)}
{/* 团队统计 */}
{dealerUser && (
<View className="bg-white mx-4 mt-4 rounded-xl p-4 hidden">
<View className="flex items-center justify-between mb-4">
<Text className="font-semibold text-gray-800"></Text>
<View
className="text-gray-400 text-sm flex items-center"
onClick={() => navigateToPage('/dealer/team/index')}
>
<Text></Text>
<ArrowRight size="12"/>
</View>
</View>
<View className="grid grid-cols-3 gap-4">
<View className="text-center grid">
<Text className="text-xl font-bold text-purple-500 mb-1">
{dealerUser.firstNum || 0}
</Text>
<Text className="text-xs text-gray-500"></Text>
</View>
<View className="text-center grid">
<Text className="text-xl font-bold text-indigo-500 mb-1">
{dealerUser.secondNum || 0}
</Text>
<Text className="text-xs text-gray-500"></Text>
</View>
<View className="text-center grid">
<Text className="text-xl font-bold text-pink-500 mb-1">
{dealerUser.thirdNum || 0}
</Text>
<Text className="text-xs text-gray-500"></Text>
</View>
</View>
</View>
)}
{/* 功能导航 */}
<View className="bg-white mx-4 mt-4 rounded-xl p-4">
<View className="font-semibold mb-4 text-gray-800"></View>
<ConfigProvider>
<Grid
columns={4}
className="no-border-grid"
style={{
'--nutui-grid-border-color': 'transparent',
'--nutui-grid-item-border-width': '0px',
border: 'none'
} as React.CSSProperties}
>
<Grid.Item text="分销订单" onClick={() => navigateToPage('/dealer/orders/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Shopping color="#3b82f6" size="20"/>
</View>
</View>
</Grid.Item>
<Grid.Item text={'提现申请'} onClick={() => navigateToPage('/dealer/withdraw/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-green-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Purse color="#10b981" size="20"/>
</View>
</View>
</Grid.Item>
<Grid.Item text={'我的邀请'} onClick={() => navigateToPage('/dealer/team/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-purple-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<People color="#8b5cf6" size="20"/>
</View>
</View>
</Grid.Item>
<Grid.Item text={'我的邀请码'} onClick={() => navigateToPage('/dealer/qrcode/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-orange-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Dongdong color="#f59e0b" size="20"/>
</View>
</View>
</Grid.Item>
</Grid>
{/* 第二行功能 */}
{/*<Grid*/}
{/* columns={4}*/}
{/* className="no-border-grid mt-4"*/}
{/* style={{*/}
{/* '--nutui-grid-border-color': 'transparent',*/}
{/* '--nutui-grid-item-border-width': '0px',*/}
{/* border: 'none'*/}
{/* } as React.CSSProperties}*/}
{/*>*/}
{/* <Grid.Item text={'邀请统计'} onClick={() => navigateToPage('/dealer/invite-stats/index')}>*/}
{/* <View className="text-center">*/}
{/* <View className="w-12 h-12 bg-indigo-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
{/* <Presentation color="#6366f1" size="20"/>*/}
{/* </View>*/}
{/* </View>*/}
{/* </Grid.Item>*/}
{/* /!* 预留其他功能位置 *!/*/}
{/* <Grid.Item text={''}>*/}
{/* <View className="text-center">*/}
{/* <View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
{/* </View>*/}
{/* </View>*/}
{/* </Grid.Item>*/}
{/* <Grid.Item text={''}>*/}
{/* <View className="text-center">*/}
{/* <View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
{/* </View>*/}
{/* </View>*/}
{/* </Grid.Item>*/}
{/* <Grid.Item text={''}>*/}
{/* <View className="text-center">*/}
{/* <View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
{/* </View>*/}
{/* </View>*/}
{/* </Grid.Item>*/}
{/*</Grid>*/}
</ConfigProvider>
</View>
</View>
{/* 底部安全区域 */}
<View className="h-20"></View>
</View>
)
}
export default DealerIndex

View File

@@ -1,157 +0,0 @@
import React from 'react'
import { View, Text } from '@tarojs/components'
import { Button, Cell, CellGroup, Tag } from '@nutui/nutui-react-taro'
import { useDealerUser } from '@/hooks/useDealerUser'
import Taro from '@tarojs/taro'
const DealerInfo: React.FC = () => {
const {
dealerUser,
loading,
error,
refresh,
} = useDealerUser()
// 跳转到申请页面
const navigateToApply = () => {
Taro.navigateTo({
url: '/pages/dealer/apply/add'
})
}
if (error) {
return (
<View className="p-4">
<View className="bg-red-50 border border-red-200 rounded-lg p-4 mb-4">
<Text className="text-red-600">{error}</Text>
</View>
<Button type="primary" onClick={refresh}>
</Button>
</View>
)
}
return (
<View className="bg-gray-50 min-h-screen">
{/* 页面标题 */}
<View className="bg-white px-4 py-3 border-b border-gray-100">
<Text className="text-lg font-bold text-center">
</Text>
</View>
{!dealerUser ? (
// 非经销商状态
<View className="bg-white mx-4 mt-4 rounded-lg p-6">
<View className="text-center py-8">
<Text className="text-gray-500 mb-4"></Text>
<Text className="text-sm text-gray-400 mb-6">
</Text>
<Button
type="primary"
size="large"
onClick={navigateToApply}
>
</Button>
</View>
</View>
) : (
// 经销商信息展示
<View>
{/* 状态卡片 */}
<View className="bg-white mx-4 mt-4 rounded-lg p-4">
<View className="flex items-center justify-between mb-4">
<Text className="text-lg font-semibold"></Text>
<Tag>
{dealerUser.realName}
</Tag>
</View>
{/* 基本信息 */}
<CellGroup>
<Cell
title="经销商ID"
extra={dealerUser.userId || '-'}
/>
<Cell
title="refereeId"
extra={dealerUser.refereeId || '-'}
/>
<Cell
title="成为经销商时间"
extra={
dealerUser.money
}
/>
</CellGroup>
{/* 操作按钮 */}
<View className="mt-6 gap-2">
<Button
type="primary"
size="large"
loading={loading}
>
</Button>
</View>
</View>
{/* 经销商权益 */}
<View className="bg-white mx-4 mt-4 rounded-lg p-4">
<Text className="font-semibold mb-3"></Text>
<View className="gap-2">
<Text className="text-sm text-gray-600">
</Text>
<Text className="text-sm text-gray-600">
广
</Text>
<Text className="text-sm text-gray-600">
</Text>
<Text className="text-sm text-gray-600">
</Text>
</View>
</View>
{/* 佣金统计 */}
<View className="bg-white mx-4 mt-4 rounded-lg p-4">
<Text className="font-semibold mb-3"></Text>
<View className="grid grid-cols-3 gap-4">
<View className="text-center">
<Text className="text-lg font-bold text-blue-600">0</Text>
<Text className="text-sm text-gray-500"></Text>
</View>
<View className="text-center">
<Text className="text-lg font-bold text-green-600">0</Text>
<Text className="text-sm text-gray-500"></Text>
</View>
<View className="text-center">
<Text className="text-lg font-bold text-orange-600">0</Text>
<Text className="text-sm text-gray-500"></Text>
</View>
</View>
</View>
</View>
)}
{/* 刷新按钮 */}
<View className="text-center py-4">
<Text
className="text-blue-500 text-sm"
onClick={refresh}
>
</Text>
</View>
</View>
)
}
export default DealerInfo

View File

@@ -1,7 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '邀请统计',
navigationBarBackgroundColor: '#ffffff',
navigationBarTextStyle: 'black',
backgroundColor: '#f5f5f5',
enablePullDownRefresh: true
})

View File

@@ -1,336 +0,0 @@
import React, { useState, useEffect, useCallback } from 'react'
import { View, Text } from '@tarojs/components'
import {
Empty,
Tabs,
Loading,
PullToRefresh,
Card,
} from '@nutui/nutui-react-taro'
import {
User,
ArrowUp,
Calendar,
Share,
Target,
Gift
} from '@nutui/icons-react-taro'
import Taro from '@tarojs/taro'
import { useDealerUser } from '@/hooks/useDealerUser'
import {
getInviteStats,
getMyInviteRecords,
getInviteRanking
} from '@/api/invite'
import type {
InviteStats,
InviteRecord
} from '@/api/invite'
import { businessGradients } from '@/styles/gradients'
import {InviteRanking} from "@/api/invite/model";
const InviteStatsPage: React.FC = () => {
const [activeTab, setActiveTab] = useState<string>('stats')
const [loading, setLoading] = useState<boolean>(false)
const [inviteStats, setInviteStats] = useState<InviteStats | null>(null)
const [inviteRecords, setInviteRecords] = useState<InviteRecord[]>([])
const [ranking, setRanking] = useState<InviteRanking[]>([])
const [dateRange, setDateRange] = useState<string>('month')
const { dealerUser } = useDealerUser()
// 获取邀请统计数据
const fetchInviteStats = useCallback(async () => {
if (!dealerUser?.userId) return
try {
setLoading(true)
const stats = await getInviteStats(dealerUser.userId)
stats && setInviteStats(stats)
} catch (error) {
console.error('获取邀请统计失败:', error)
Taro.showToast({
title: '获取统计数据失败',
icon: 'error'
})
} finally {
setLoading(false)
}
}, [dealerUser?.userId])
// 获取邀请记录
const fetchInviteRecords = useCallback(async () => {
if (!dealerUser?.userId) return
try {
const result = await getMyInviteRecords({
page: 1,
limit: 50,
inviterId: dealerUser.userId
})
setInviteRecords(result?.list || [])
} catch (error) {
console.error('获取邀请记录失败:', error)
}
}, [dealerUser?.userId])
// 获取邀请排行榜
const fetchRanking = useCallback(async () => {
try {
const result = await getInviteRanking({
limit: 20,
period: dateRange as 'day' | 'week' | 'month'
})
setRanking(result || [])
} catch (error) {
console.error('获取排行榜失败:', error)
}
}, [dateRange])
// 刷新数据
const handleRefresh = async () => {
await Promise.all([
fetchInviteStats(),
fetchInviteRecords(),
fetchRanking()
])
}
// 初始化数据
useEffect(() => {
if (dealerUser?.userId) {
fetchInviteStats().then()
fetchInviteRecords().then()
fetchRanking().then()
}
}, [fetchInviteStats, fetchInviteRecords, fetchRanking])
// 获取状态显示文本
const getStatusText = (status: string) => {
const statusMap: Record<string, string> = {
'pending': '待注册',
'registered': '已注册',
'activated': '已激活'
}
return statusMap[status] || status
}
// 获取状态颜色
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
'pending': 'text-orange-500',
'registered': 'text-blue-500',
'activated': 'text-green-500'
}
return colorMap[status] || 'text-gray-500'
}
// 渲染统计概览
const renderStatsOverview = () => (
<View className="px-4 space-y-4">
{/* 核心数据卡片 */}
<Card className="bg-white rounded-2xl shadow-sm">
<View className="p-4">
<Text className="text-lg font-semibold text-gray-800 mb-4"></Text>
{loading ? (
<View className="flex items-center justify-center py-8">
<Loading />
</View>
) : inviteStats ? (
<View className="grid grid-cols-2 gap-4">
<View className="text-center p-4 bg-blue-50 rounded-xl">
<ArrowUp size="24" className="text-blue-500 mx-auto mb-2" />
<Text className="text-2xl font-bold text-blue-600">
{inviteStats.totalInvites || 0}
</Text>
<Text className="text-sm text-gray-600"></Text>
</View>
<View className="text-center p-4 bg-green-50 rounded-xl">
<User size="24" className="text-green-500 mx-auto mb-2" />
<Text className="text-2xl font-bold text-green-600">
{inviteStats.successfulRegistrations || 0}
</Text>
<Text className="text-sm text-gray-600"></Text>
</View>
<View className="text-center p-4 bg-purple-50 rounded-xl">
<Target size="24" className="text-purple-500 mx-auto mb-2" />
<Text className="text-2xl font-bold text-purple-600">
{inviteStats.conversionRate ? `${(inviteStats.conversionRate * 100).toFixed(1)}%` : '0%'}
</Text>
<Text className="text-sm text-gray-600"></Text>
</View>
<View className="text-center p-4 bg-orange-50 rounded-xl">
<Calendar size="24" className="text-orange-500 mx-auto mb-2" />
<Text className="text-2xl font-bold text-orange-600">
{inviteStats.todayInvites || 0}
</Text>
<Text className="text-sm text-gray-600"></Text>
</View>
</View>
) : (
<View className="text-center py-8">
<Text className="text-gray-500"></Text>
</View>
)}
</View>
</Card>
{/* 邀请来源分析 */}
{inviteStats?.sourceStats && inviteStats.sourceStats.length > 0 && (
<Card className="bg-white rounded-2xl shadow-sm">
<View className="p-4">
<Text className="text-lg font-semibold text-gray-800 mb-4"></Text>
<View className="space-y-3">
{inviteStats.sourceStats.map((source, index) => (
<View key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<View className="flex items-center">
<Share size="16" className="text-blue-500 mr-2" />
<Text className="font-medium text-gray-800">{source.source}</Text>
</View>
<View className="text-right">
<Text className="text-lg font-bold text-gray-800">{source.count}</Text>
<Text className="text-sm text-gray-500">
{source.conversionRate ? `${(source.conversionRate * 100).toFixed(1)}%` : '0%'}
</Text>
</View>
</View>
))}
</View>
</View>
</Card>
)}
</View>
)
// 渲染邀请记录
const renderInviteRecords = () => (
<View className="px-4">
{inviteRecords.length > 0 ? (
<View className="space-y-3">
{inviteRecords.map((record, index) => (
<Card key={record.id || index} className="bg-white rounded-xl shadow-sm">
<View className="p-4">
<View className="flex items-center justify-between mb-2">
<Text className="font-medium text-gray-800">
{record.inviteeName || `用户${record.inviteeId}`}
</Text>
<Text className={`text-sm font-medium ${getStatusColor(record.status || 'pending')}`}>
{getStatusText(record.status || 'pending')}
</Text>
</View>
<View className="flex items-center justify-between text-sm text-gray-500">
<Text>: {record.source || '未知'}</Text>
<Text>{record.inviteTime ? new Date(record.inviteTime).toLocaleDateString() : ''}</Text>
</View>
{record.registerTime && (
<Text className="text-xs text-green-600 mt-1">
: {new Date(record.registerTime).toLocaleString()}
</Text>
)}
</View>
</Card>
))}
</View>
) : (
<Empty description="暂无邀请记录" />
)}
</View>
)
// 渲染排行榜
const renderRanking = () => (
<View className="px-4">
<View className="mb-4">
<Tabs value={dateRange} onChange={() => setDateRange}>
<Tabs.TabPane title="日榜" value="day" />
<Tabs.TabPane title="周榜" value="week" />
<Tabs.TabPane title="月榜" value="month" />
</Tabs>
</View>
{ranking.length > 0 ? (
<View className="space-y-3">
{ranking.map((item, index) => (
<Card key={item.inviterId} className="bg-white rounded-xl shadow-sm">
<View className="p-4 flex items-center">
<View className="flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 mr-3">
{index < 3 ? (
<Gift size="16" className={index === 0 ? 'text-yellow-500' : index === 1 ? 'text-gray-400' : 'text-orange-400'} />
) : (
<Text className="text-sm font-bold text-gray-600">{index + 1}</Text>
)}
</View>
<View className="flex-1">
<Text className="font-medium text-gray-800">{item.inviterName}</Text>
<Text className="text-sm text-gray-500">
{item.inviteCount} · {item.conversionRate ? `${(item.conversionRate * 100).toFixed(1)}%` : '0%'}
</Text>
</View>
<Text className="text-lg font-bold text-blue-600">{item.successCount}</Text>
</View>
</Card>
))}
</View>
) : (
<Empty description="暂无排行数据" />
)}
</View>
)
if (!dealerUser) {
return (
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
<Loading />
<Text className="text-gray-500 mt-2">...</Text>
</View>
)
}
return (
<View className="bg-gray-50 min-h-screen">
{/* 头部 */}
<View className="rounded-b-3xl p-6 mb-6 text-white relative overflow-hidden" style={{
background: businessGradients.dealer.header
}}>
<View className="absolute w-32 h-32 rounded-full" style={{
backgroundColor: 'rgba(255, 255, 255, 0.1)',
top: '-16px',
right: '-16px'
}}></View>
<View className="relative z-10">
<Text className="text-2xl font-bold mb-2 text-white"></Text>
<Text className="text-white text-opacity-80">
广
</Text>
</View>
</View>
{/* 标签页 */}
<View className="px-4 mb-4">
<Tabs value={activeTab} onChange={() => setActiveTab}>
<Tabs.TabPane title="统计概览" value="stats" />
<Tabs.TabPane title="邀请记录" value="records" />
<Tabs.TabPane title="排行榜" value="ranking" />
</Tabs>
</View>
{/* 内容区域 */}
<PullToRefresh onRefresh={handleRefresh}>
<View className="pb-6">
{activeTab === 'stats' && renderStatsOverview()}
{activeTab === 'records' && renderInviteRecords()}
{activeTab === 'ranking' && renderRanking()}
</View>
</PullToRefresh>
</View>
)
}
export default InviteStatsPage

View File

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

View File

@@ -1,192 +0,0 @@
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'
interface OrderWithDetails extends ShopDealerOrder {
orderNo?: string
customerName?: string
userCommission?: string
}
const DealerOrders: React.FC = () => {
const [loading, setLoading] = useState<boolean>(false)
const [refreshing, setRefreshing] = useState<boolean>(false)
const [loadingMore, setLoadingMore] = useState<boolean>(false)
const [orders, setOrders] = useState<OrderWithDetails[]>([])
const [currentPage, setCurrentPage] = useState<number>(1)
const [hasMore, setHasMore] = useState<boolean>(true)
const {dealerUser} = useDealerUser()
// 获取订单数据
const fetchOrders = useCallback(async (page: number = 1, isRefresh: boolean = false) => {
if (!dealerUser?.userId) return
try {
if (isRefresh) {
setRefreshing(true)
} else if (page === 1) {
setLoading(true)
} else {
setLoadingMore(true)
}
const result = await pageShopDealerOrder({
page,
limit: 10
})
if (result?.list) {
const newOrders = result.list.map(order => ({
...order,
orderNo: `${order.orderId}`,
customerName: `用户${order.userId}`,
userCommission: order.firstMoney || '0.00'
}))
if (page === 1) {
setOrders(newOrders)
} else {
setOrders(prev => [...prev, ...newOrders])
}
setHasMore(newOrders.length === 10)
setCurrentPage(page)
}
} catch (error) {
console.error('获取分销订单失败:', error)
Taro.showToast({
title: '获取订单失败',
icon: 'error'
})
} finally {
setLoading(false)
setRefreshing(false)
setLoadingMore(false)
}
}, [dealerUser?.userId])
// 下拉刷新
const handleRefresh = async () => {
await fetchOrders(1, true)
}
// 加载更多
const handleLoadMore = async () => {
if (!loadingMore && hasMore) {
await fetchOrders(currentPage + 1)
}
}
// 初始化加载数据
useEffect(() => {
if (dealerUser?.userId) {
fetchOrders(1)
}
}, [fetchOrders])
const getStatusText = (isSettled?: number, isInvalid?: number) => {
if (isInvalid === 1) return '已失效'
if (isSettled === 1) return '已结算'
return '待结算'
}
const getStatusColor = (isSettled?: number, isInvalid?: number) => {
if (isInvalid === 1) return 'danger'
if (isSettled === 1) return 'success'
return 'warning'
}
const renderOrderItem = (order: OrderWithDetails) => (
<View key={order.id} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
<View className="flex justify-between items-start mb-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">
<Text className="text-sm text-gray-400">
{order.customerName}
</Text>
<Text className="text-sm text-gray-400">
{order.createTime}
</Text>
</View>
</View>
)
if (!dealerUser) {
return (
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
<Loading/>
<Text className="text-gray-500 mt-2">...</Text>
</View>
)
}
return (
<View className="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">
{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="暂无分销订单"/>
)}
</View>
</ScrollView>
</PullToRefresh>
</View>
)
}
export default DealerOrders

View File

@@ -1,3 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '推广二维码'
})

View File

@@ -1,398 +0,0 @@
import React, {useState, useEffect} from 'react'
import {View, Text, Image} from '@tarojs/components'
import {Button, Loading} from '@nutui/nutui-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'
// import type {InviteStats} from '@/api/invite'
import {businessGradients} from '@/styles/gradients'
const DealerQrcode: React.FC = () => {
const [miniProgramCodeUrl, setMiniProgramCodeUrl] = useState<string>('')
const [loading, setLoading] = useState<boolean>(false)
// const [inviteStats, setInviteStats] = useState<InviteStats | null>(null)
// const [statsLoading, setStatsLoading] = useState<boolean>(false)
const {dealerUser} = useDealerUser()
// 生成小程序码
const generateMiniProgramCode = async () => {
if (!dealerUser?.userId) {
return
}
try {
setLoading(true)
// 生成邀请小程序码
const codeUrl = await generateInviteCode(dealerUser.userId)
if (codeUrl) {
setMiniProgramCodeUrl(codeUrl)
} else {
throw new Error('返回的小程序码URL为空')
}
} catch (error: any) {
Taro.showToast({
title: error.message || '生成小程序码失败',
icon: 'error'
})
// 清空之前的二维码
setMiniProgramCodeUrl('')
} finally {
setLoading(false)
}
}
// 获取邀请统计数据
// const fetchInviteStats = async () => {
// if (!dealerUser?.userId) return
//
// try {
// setStatsLoading(true)
// const stats = await getInviteStats(dealerUser.userId)
// stats && setInviteStats(stats)
// } catch (error) {
// // 静默处理错误,不影响用户体验
// } finally {
// setStatsLoading(false)
// }
// }
// 初始化生成小程序码和获取统计数据
useEffect(() => {
if (dealerUser?.userId) {
generateMiniProgramCode()
// fetchInviteStats()
}
}, [dealerUser?.userId])
// 保存小程序码到相册
const saveMiniProgramCode = async () => {
if (!miniProgramCodeUrl) {
Taro.showToast({
title: '小程序码未生成',
icon: 'error'
})
return
}
try {
// 先下载图片到本地
const res = await Taro.downloadFile({
url: miniProgramCodeUrl
})
if (res.statusCode === 200) {
// 保存到相册
await Taro.saveImageToPhotosAlbum({
filePath: res.tempFilePath
})
Taro.showToast({
title: '保存成功',
icon: 'success'
})
}
} catch (error: any) {
if (error.errMsg?.includes('auth deny')) {
Taro.showModal({
title: '提示',
content: '需要您授权保存图片到相册',
success: (res) => {
if (res.confirm) {
Taro.openSetting()
}
}
})
} else {
Taro.showToast({
title: '保存失败',
icon: 'error'
})
}
}
}
// 复制邀请信息
// const copyInviteInfo = () => {
// if (!dealerUser?.userId) {
// Taro.showToast({
// title: '用户信息未加载',
// icon: 'error'
// })
// return
// }
//
// const inviteText = `🎉 邀请您加入我的团队!
//
// 扫描小程序码或搜索"时里院子市集"小程序,即可享受优质商品和服务!
//
// 💰 成为我的团队成员,一起赚取丰厚佣金
// 🎁 新用户专享优惠等你来拿
//
// 邀请码:${dealerUser.userId}
// 快来加入我们吧!`
//
// Taro.setClipboardData({
// data: inviteText,
// success: () => {
// Taro.showToast({
// title: '邀请信息已复制',
// icon: 'success'
// })
// }
// })
// }
// 分享小程序码
// const shareMiniProgramCode = () => {
// if (!dealerUser?.userId) {
// Taro.showToast({
// title: '用户信息未加载',
// icon: 'error'
// })
// return
// }
//
// // 小程序分享
// Taro.showShareMenu({
// withShareTicket: true,
// showShareItems: ['shareAppMessage']
// })
// }
if (!dealerUser) {
return (
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
<Loading/>
<Text className="text-gray-500 mt-2">...</Text>
</View>
)
}
return (
<View className="bg-gray-50 min-h-screen">
{/* 头部卡片 */}
<View className="rounded-b-3xl p-6 mb-6 text-white relative overflow-hidden" style={{
background: businessGradients.dealer.header
}}>
{/* 装饰背景 */}
<View className="absolute w-32 h-32 rounded-full" style={{
backgroundColor: 'rgba(255, 255, 255, 0.1)',
top: '-16px',
right: '-16px'
}}></View>
<View className="relative z-10 flex flex-col">
<Text className="text-2xl font-bold mb-2 text-white"></Text>
<Text className="text-white text-opacity-80">
</Text>
</View>
</View>
<View className="px-4">
{/* 小程序码展示区 */}
<View className="bg-white rounded-2xl p-6 mb-6 shadow-sm">
<View className="text-center">
{loading ? (
<View className="w-48 h-48 mx-auto mb-4 flex items-center justify-center bg-gray-50 rounded-xl">
<Loading/>
<Text className="text-gray-500 mt-2">...</Text>
</View>
) : miniProgramCodeUrl ? (
<View className="w-48 h-48 mx-auto mb-4 bg-white rounded-xl shadow-sm p-4">
<Image
src={miniProgramCodeUrl}
className="w-full h-full"
mode="aspectFit"
onError={() => {
Taro.showModal({
title: '二维码加载失败',
content: '请检查网络连接或联系管理员',
showCancel: true,
confirmText: '重新生成',
success: (res) => {
if (res.confirm) {
generateMiniProgramCode();
}
}
});
}}
/>
</View>
) : (
<View className="w-48 h-48 mx-auto mb-4 flex items-center justify-center bg-gray-50 rounded-xl">
<QrCode size="48" className="text-gray-400 mb-2"/>
<Text className="text-gray-500"></Text>
<Button
size="small"
type="primary"
className="mt-2"
onClick={generateMiniProgramCode}
>
</Button>
</View>
)}
<View className="text-lg font-semibold text-gray-800 mb-2">
</View>
<View className="text-sm text-gray-500 mb-4">
</View>
</View>
</View>
{/* 操作按钮 */}
<View className={'gap-2'}>
<View className={'my-2'}>
<Button
type="primary"
size="large"
block
icon={<Download/>}
onClick={saveMiniProgramCode}
disabled={!miniProgramCodeUrl || loading}
>
</Button>
</View>
{/*<View className={'my-2 bg-white'}>*/}
{/* <Button*/}
{/* size="large"*/}
{/* block*/}
{/* icon={<Copy/>}*/}
{/* onClick={copyInviteInfo}*/}
{/* disabled={!dealerUser?.userId || loading}*/}
{/* >*/}
{/* 复制邀请信息*/}
{/* </Button>*/}
{/*</View>*/}
{/*<View className={'my-2 bg-white'}>*/}
{/* <Button*/}
{/* size="large"*/}
{/* block*/}
{/* fill="outline"*/}
{/* icon={<Share/>}*/}
{/* onClick={shareMiniProgramCode}*/}
{/* disabled={!dealerUser?.userId || loading}*/}
{/* >*/}
{/* 分享给好友*/}
{/* </Button>*/}
{/*</View>*/}
</View>
{/* 推广说明 */}
<View className="bg-white rounded-2xl p-4 mt-6 hidden">
<Text className="font-semibold text-gray-800 mb-3">广</Text>
<View className="space-y-2">
<View className="flex items-start">
<View className="w-2 h-2 bg-blue-500 rounded-full mt-2 mr-3 flex-shrink-0"></View>
<Text className="text-sm text-gray-600">
</Text>
</View>
<View className="flex items-start">
<View className="w-2 h-2 bg-green-500 rounded-full mt-2 mr-3 flex-shrink-0"></View>
<Text className="text-sm text-gray-600">
</Text>
</View>
<View className="flex items-start">
<View className="w-2 h-2 bg-purple-500 rounded-full mt-2 mr-3 flex-shrink-0"></View>
<Text className="text-sm text-gray-600">
</Text>
</View>
</View>
</View>
{/* 邀请统计数据 */}
{/*<View className="bg-white rounded-2xl p-4 mt-4 mb-6">*/}
{/* <Text className="font-semibold text-gray-800 mb-3">我的邀请数据</Text>*/}
{/* {statsLoading ? (*/}
{/* <View className="flex items-center justify-center py-8">*/}
{/* <Loading/>*/}
{/* <Text className="text-gray-500 mt-2">加载中...</Text>*/}
{/* </View>*/}
{/* ) : inviteStats ? (*/}
{/* <View className="space-y-4">*/}
{/* <View className="grid grid-cols-2 gap-4">*/}
{/* <View className="text-center">*/}
{/* <Text className="text-2xl font-bold text-blue-500">*/}
{/* {inviteStats.totalInvites || 0}*/}
{/* </Text>*/}
{/* <Text className="text-sm text-gray-500">总邀请数</Text>*/}
{/* </View>*/}
{/* <View className="text-center">*/}
{/* <Text className="text-2xl font-bold text-green-500">*/}
{/* {inviteStats.successfulRegistrations || 0}*/}
{/* </Text>*/}
{/* <Text className="text-sm text-gray-500">成功注册</Text>*/}
{/* </View>*/}
{/* </View>*/}
{/* <View className="grid grid-cols-2 gap-4">*/}
{/* <View className="text-center">*/}
{/* <Text className="text-2xl font-bold text-purple-500">*/}
{/* {inviteStats.conversionRate ? `${(inviteStats.conversionRate * 100).toFixed(1)}%` : '0%'}*/}
{/* </Text>*/}
{/* <Text className="text-sm text-gray-500">转化率</Text>*/}
{/* </View>*/}
{/* <View className="text-center">*/}
{/* <Text className="text-2xl font-bold text-orange-500">*/}
{/* {inviteStats.todayInvites || 0}*/}
{/* </Text>*/}
{/* <Text className="text-sm text-gray-500">今日邀请</Text>*/}
{/* </View>*/}
{/* </View>*/}
{/* /!* 邀请来源统计 *!/*/}
{/* {inviteStats.sourceStats && inviteStats.sourceStats.length > 0 && (*/}
{/* <View className="mt-4">*/}
{/* <Text className="text-sm font-medium text-gray-700 mb-2">邀请来源分布</Text>*/}
{/* <View className="space-y-2">*/}
{/* {inviteStats.sourceStats.map((source, index) => (*/}
{/* <View key={index} className="flex items-center justify-between py-2 px-3 bg-gray-50 rounded-lg">*/}
{/* <View className="flex items-center">*/}
{/* <View className="w-3 h-3 rounded-full bg-blue-500 mr-2"></View>*/}
{/* <Text className="text-sm text-gray-700">{source.source}</Text>*/}
{/* </View>*/}
{/* <View className="text-right">*/}
{/* <Text className="text-sm font-medium text-gray-800">{source.count}</Text>*/}
{/* <Text className="text-xs text-gray-500">*/}
{/* {source.conversionRate ? `${(source.conversionRate * 100).toFixed(1)}%` : '0%'}*/}
{/* </Text>*/}
{/* </View>*/}
{/* </View>*/}
{/* ))}*/}
{/* </View>*/}
{/* </View>*/}
{/* )}*/}
{/* </View>*/}
{/* ) : (*/}
{/* <View className="text-center py-8">*/}
{/* <View className="text-gray-500">暂无邀请数据</View>*/}
{/* <Button*/}
{/* size="small"*/}
{/* type="primary"*/}
{/* className="mt-2"*/}
{/* onClick={fetchInviteStats}*/}
{/* >*/}
{/* 刷新数据*/}
{/* </Button>*/}
{/* </View>*/}
{/* )}*/}
{/*</View>*/}
</View>
</View>
)
}
export default DealerQrcode

View File

@@ -1,3 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '邀请推广'
})

View File

@@ -1,439 +0,0 @@
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 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 [teamMembers, setTeamMembers] = useState<TeamMemberWithStats[]>([])
const {dealerUser} = useDealerUser()
const [dealerId, setDealerId] = useState<number>()
// 层级栈,用于支持返回上一层
const [levelStack, setLevelStack] = useState<LevelInfo[]>([])
const [loading, setLoading] = useState(false)
// 当前查看的用户名称
const [currentDealerName, setCurrentDealerName] = useState<string>('')
// 异步加载成员统计数据
const loadMemberStats = async (members: TeamMemberWithStats[]) => {
// 分批处理,避免过多并发请求
const batchSize = 3
for (let i = 0; i < members.length; i += batchSize) {
const batch = members.slice(i, i + batchSize)
const batchStats = await Promise.all(
batch.map(async (member) => {
try {
// 并行获取订单统计和下级成员数量
const [orderResult, subMembersResult] = await Promise.all([
pageShopDealerOrder({
page: 1,
userId: member.userId
}),
listShopDealerReferee({
dealerId: member.userId,
deleted: 0
})
])
let orderCount = 0
let commission = '0.00'
let status: 'active' | 'inactive' = 'inactive'
if (orderResult?.list) {
const orders = orderResult.list
orderCount = orders.length
commission = orders.reduce((sum, order) => {
const levelCommission = member.level === 1 ? order.firstMoney :
member.level === 2 ? order.secondMoney :
order.thirdMoney
return sum + parseFloat(levelCommission || '0')
}, 0).toFixed(2)
// 判断活跃状态30天内有订单为活跃
const thirtyDaysAgo = new Date()
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30)
const hasRecentOrder = orders.some(order =>
new Date(order.createTime || '') > thirtyDaysAgo
)
status = hasRecentOrder ? 'active' : 'inactive'
}
return {
...member,
orderCount,
commission,
status,
subMembers: subMembersResult?.length || 0
}
} catch (error) {
console.error(`获取成员${member.userId}数据失败:`, error)
return {
...member,
orderCount: 0,
commission: '0.00',
status: 'inactive' as const,
subMembers: 0
}
}
})
)
// 更新这一批成员的数据
setTeamMembers(prevMembers => {
const updatedMembers = [...prevMembers]
batchStats.forEach(updatedMember => {
const index = updatedMembers.findIndex(m => m.userId === updatedMember.userId)
if (index !== -1) {
updatedMembers[index] = updatedMember
}
})
return updatedMembers
})
// 添加小延迟,避免请求过于密集
if (i + batchSize < members.length) {
await new Promise(resolve => setTimeout(resolve, 100))
}
}
}
// 获取团队数据
const fetchTeamData = useCallback(async () => {
if (!dealerUser?.userId && !dealerId) return
try {
setLoading(true)
console.log(dealerId, 'dealerId>>>>>>>>>')
// 获取团队成员关系
const refereeResult = await listShopDealerReferee({
dealerId: dealerId ? dealerId : dealerUser?.userId
})
if (refereeResult) {
console.log('团队成员原始数据:', refereeResult)
// 处理团队成员数据
const processedMembers: TeamMemberWithStats[] = refereeResult.map(member => ({
...member,
name: `${member.userId}`,
orderCount: 0,
commission: '0.00',
status: 'active' as const,
subMembers: 0,
joinTime: member.createTime
}))
// 先显示基础数据,然后异步加载详细统计
setTeamMembers(processedMembers)
setLoading(false)
// 异步加载每个成员的详细统计数据
loadMemberStats(processedMembers)
}
} catch (error) {
console.error('获取团队数据失败:', error)
Taro.showToast({
title: '获取团队数据失败',
icon: 'error'
})
} finally {
setLoading(false)
}
}, [dealerUser?.userId, dealerId])
// 查看下级成员
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 || dealerId) {
fetchTeamData().then()
}
}, [fetchTeamData])
// 初始化当前用户名称
useEffect(() => {
if (!dealerId && dealerUser?.realName && !currentDealerName) {
setCurrentDealerName(dealerUser.realName)
}
}, [dealerUser, dealerId, currentDealerName])
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
return (
<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>
)
}
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(`/dealer/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(`/dealer/qrcode/index`, true)}/>
</>
)
}
export default DealerTeam;

View File

@@ -1,184 +0,0 @@
import React from 'react'
import { render, fireEvent, waitFor } from '@testing-library/react'
import DealerWithdraw from '../index'
import { useDealerUser } from '@/hooks/useDealerUser'
import * as withdrawAPI from '@/api/shop/shopDealerWithdraw'
// Mock dependencies
jest.mock('@/hooks/useDealerUser')
jest.mock('@/api/shop/shopDealerWithdraw')
jest.mock('@tarojs/taro', () => ({
showToast: jest.fn(),
getStorageSync: jest.fn(() => 123),
}))
const mockUseDealerUser = useDealerUser as jest.MockedFunction<typeof useDealerUser>
const mockAddShopDealerWithdraw = withdrawAPI.addShopDealerWithdraw as jest.MockedFunction<typeof withdrawAPI.addShopDealerWithdraw>
const mockPageShopDealerWithdraw = withdrawAPI.pageShopDealerWithdraw as jest.MockedFunction<typeof withdrawAPI.pageShopDealerWithdraw>
describe('DealerWithdraw', () => {
const mockDealerUser = {
userId: 123,
money: '10000.00',
realName: '测试用户',
mobile: '13800138000'
}
beforeEach(() => {
mockUseDealerUser.mockReturnValue({
dealerUser: mockDealerUser,
loading: false,
error: null,
refresh: jest.fn()
})
mockPageShopDealerWithdraw.mockResolvedValue({
list: [],
count: 0
})
})
afterEach(() => {
jest.clearAllMocks()
})
test('应该正确显示可提现余额', () => {
const { getByText } = render(<DealerWithdraw />)
expect(getByText('10000.00')).toBeInTheDocument()
expect(getByText('可提现余额')).toBeInTheDocument()
})
test('应该验证最低提现金额', async () => {
mockAddShopDealerWithdraw.mockResolvedValue('success')
const { getByPlaceholderText, getByText } = render(<DealerWithdraw />)
// 输入低于最低金额的数值
const amountInput = getByPlaceholderText('请输入提现金额')
fireEvent.change(amountInput, { target: { value: '50' } })
// 选择提现方式
const wechatRadio = getByText('微信钱包')
fireEvent.click(wechatRadio)
// 提交表单
const submitButton = getByText('申请提现')
fireEvent.click(submitButton)
await waitFor(() => {
expect(require('@tarojs/taro').showToast).toHaveBeenCalledWith({
title: '最低提现金额为100元',
icon: 'error'
})
})
})
test('应该验证提现金额不超过可用余额', async () => {
const { getByPlaceholderText, getByText } = render(<DealerWithdraw />)
// 输入超过可用余额的金额
const amountInput = getByPlaceholderText('请输入提现金额')
fireEvent.change(amountInput, { target: { value: '20000' } })
// 选择提现方式
const wechatRadio = getByText('微信钱包')
fireEvent.click(wechatRadio)
// 提交表单
const submitButton = getByText('申请提现')
fireEvent.click(submitButton)
await waitFor(() => {
expect(require('@tarojs/taro').showToast).toHaveBeenCalledWith({
title: '提现金额超过可用余额',
icon: 'error'
})
})
})
test('应该验证支付宝账户信息完整性', async () => {
const { getByPlaceholderText, getByText } = render(<DealerWithdraw />)
// 输入有效金额
const amountInput = getByPlaceholderText('请输入提现金额')
fireEvent.change(amountInput, { target: { value: '1000' } })
// 选择支付宝提现
const alipayRadio = getByText('支付宝')
fireEvent.click(alipayRadio)
// 只填写账号,不填写姓名
const accountInput = getByPlaceholderText('请输入支付宝账号')
fireEvent.change(accountInput, { target: { value: 'test@alipay.com' } })
// 提交表单
const submitButton = getByText('申请提现')
fireEvent.click(submitButton)
await waitFor(() => {
expect(require('@tarojs/taro').showToast).toHaveBeenCalledWith({
title: '请填写完整的支付宝信息',
icon: 'error'
})
})
})
test('应该成功提交微信提现申请', async () => {
mockAddShopDealerWithdraw.mockResolvedValue('success')
const { getByPlaceholderText, getByText } = render(<DealerWithdraw />)
// 输入有效金额
const amountInput = getByPlaceholderText('请输入提现金额')
fireEvent.change(amountInput, { target: { value: '1000' } })
// 选择微信提现
const wechatRadio = getByText('微信钱包')
fireEvent.click(wechatRadio)
// 提交表单
const submitButton = getByText('申请提现')
fireEvent.click(submitButton)
await waitFor(() => {
expect(mockAddShopDealerWithdraw).toHaveBeenCalledWith({
userId: 123,
money: '1000',
payType: 10,
applyStatus: 10,
platform: 'MiniProgram'
})
})
await waitFor(() => {
expect(require('@tarojs/taro').showToast).toHaveBeenCalledWith({
title: '提现申请已提交',
icon: 'success'
})
})
})
test('快捷金额按钮应该正常工作', () => {
const { getByText, getByPlaceholderText } = render(<DealerWithdraw />)
// 点击快捷金额按钮
const quickAmountButton = getByText('500')
fireEvent.click(quickAmountButton)
// 验证金额输入框的值
const amountInput = getByPlaceholderText('请输入提现金额') as HTMLInputElement
expect(amountInput.value).toBe('500')
})
test('全部按钮应该设置为可用余额', () => {
const { getByText, getByPlaceholderText } = render(<DealerWithdraw />)
// 点击全部按钮
const allButton = getByText('全部')
fireEvent.click(allButton)
// 验证金额输入框的值
const amountInput = getByPlaceholderText('请输入提现金额') as HTMLInputElement
expect(amountInput.value).toBe('10000.00')
})
})

View File

@@ -1,3 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '提现申请'
})

View File

@@ -1,499 +0,0 @@
import React, {useState, useRef, useEffect, useCallback} from 'react'
import {View, Text} from '@tarojs/components'
import {
Cell,
Space,
Button,
Form,
Input,
CellGroup,
Radio,
Tabs,
Tag,
Empty,
Loading,
PullToRefresh
} from '@nutui/nutui-react-taro'
import {Wallet} from '@nutui/icons-react-taro'
import {businessGradients} from '@/styles/gradients'
import Taro from '@tarojs/taro'
import {useDealerUser} from '@/hooks/useDealerUser'
import {pageShopDealerWithdraw, addShopDealerWithdraw} from '@/api/shop/shopDealerWithdraw'
import type {ShopDealerWithdraw} from '@/api/shop/shopDealerWithdraw/model'
interface WithdrawRecordWithDetails extends ShopDealerWithdraw {
accountDisplay?: string
}
const DealerWithdraw: React.FC = () => {
const [activeTab, setActiveTab] = useState<string | number>('0')
const [selectedAccount, setSelectedAccount] = useState('')
const [loading, setLoading] = useState<boolean>(false)
const [refreshing, setRefreshing] = useState<boolean>(false)
const [submitting, setSubmitting] = useState<boolean>(false)
const [availableAmount, setAvailableAmount] = useState<string>('0.00')
const [withdrawRecords, setWithdrawRecords] = useState<WithdrawRecordWithDetails[]>([])
const formRef = useRef<any>(null)
const {dealerUser} = useDealerUser()
// Tab 切换处理函数
const handleTabChange = (value: string | number) => {
console.log('Tab切换到:', value)
setActiveTab(value)
// 如果切换到提现记录页面,刷新数据
if (String(value) === '1') {
fetchWithdrawRecords()
}
}
// 获取可提现余额
const fetchBalance = useCallback(async () => {
console.log(dealerUser, 'dealerUser...')
try {
setAvailableAmount(dealerUser?.money || '0.00')
} catch (error) {
console.error('获取余额失败:', error)
}
}, [dealerUser])
// 获取提现记录
const fetchWithdrawRecords = useCallback(async () => {
if (!dealerUser?.userId) return
try {
setLoading(true)
const result = await pageShopDealerWithdraw({
page: 1,
limit: 100,
userId: dealerUser.userId
})
if (result?.list) {
const processedRecords = result.list.map(record => ({
...record,
accountDisplay: getAccountDisplay(record)
}))
setWithdrawRecords(processedRecords)
}
} catch (error) {
console.error('获取提现记录失败:', error)
Taro.showToast({
title: '获取提现记录失败',
icon: 'error'
})
} finally {
setLoading(false)
}
}, [dealerUser?.userId])
// 格式化账户显示
const getAccountDisplay = (record: ShopDealerWithdraw) => {
if (record.payType === 10) {
return '微信钱包'
} else if (record.payType === 20 && record.alipayAccount) {
return `支付宝(${record.alipayAccount.slice(-4)})`
} else if (record.payType === 30 && record.bankCard) {
return `${record.bankName || '银行卡'}(尾号${record.bankCard.slice(-4)})`
}
return '未知账户'
}
// 刷新数据
const handleRefresh = async () => {
setRefreshing(true)
await Promise.all([fetchBalance(), fetchWithdrawRecords()])
setRefreshing(false)
}
// 初始化加载数据
useEffect(() => {
if (dealerUser?.userId) {
fetchBalance().then()
fetchWithdrawRecords().then()
}
}, [fetchBalance, fetchWithdrawRecords])
const getStatusText = (status?: number) => {
switch (status) {
case 40:
return '已到账'
case 20:
return '审核通过'
case 10:
return '待审核'
case 30:
return '已驳回'
default:
return '未知'
}
}
const getStatusColor = (status?: number) => {
switch (status) {
case 40:
return 'success'
case 20:
return 'success'
case 10:
return 'warning'
case 30:
return 'danger'
default:
return 'default'
}
}
const handleSubmit = async (values: any) => {
if (!dealerUser?.userId) {
Taro.showToast({
title: '用户信息获取失败',
icon: 'error'
})
return
}
if (!values.accountType) {
Taro.showToast({
title: '请选择提现方式',
icon: 'error'
})
return
}
// 验证提现金额
const amount = parseFloat(values.amount)
const available = parseFloat(availableAmount.replace(/,/g, ''))
if (isNaN(amount) || amount <= 0) {
Taro.showToast({
title: '请输入有效的提现金额',
icon: 'error'
})
return
}
if (amount < 100) {
Taro.showToast({
title: '最低提现金额为100元',
icon: 'error'
})
return
}
if (amount > available) {
Taro.showToast({
title: '提现金额超过可用余额',
icon: 'error'
})
return
}
// 验证账户信息
if (values.accountType === 'alipay') {
if (!values.account || !values.accountName) {
Taro.showToast({
title: '请填写完整的支付宝信息',
icon: 'error'
})
return
}
} else if (values.accountType === 'bank') {
if (!values.account || !values.accountName || !values.bankName) {
Taro.showToast({
title: '请填写完整的银行卡信息',
icon: 'error'
})
return
}
}
try {
setSubmitting(true)
const withdrawData: ShopDealerWithdraw = {
userId: dealerUser.userId,
money: values.amount,
payType: values.accountType === 'wechat' ? 10 :
values.accountType === 'alipay' ? 20 : 30,
applyStatus: 10, // 待审核
platform: 'MiniProgram'
}
// 根据提现方式设置账户信息
if (values.accountType === 'alipay') {
withdrawData.alipayAccount = values.account
withdrawData.alipayName = values.accountName
} else if (values.accountType === 'bank') {
withdrawData.bankCard = values.account
withdrawData.bankAccount = values.accountName
withdrawData.bankName = values.bankName || '银行卡'
}
await addShopDealerWithdraw(withdrawData)
Taro.showToast({
title: '提现申请已提交',
icon: 'success'
})
// 重置表单
formRef.current?.resetFields()
setSelectedAccount('')
// 刷新数据
await handleRefresh()
// 切换到提现记录页面
setActiveTab('1')
} catch (error: any) {
console.error('提现申请失败:', error)
Taro.showToast({
title: error.message || '提现申请失败',
icon: 'error'
})
} finally {
setSubmitting(false)
}
}
const quickAmounts = ['100', '300', '500', '1000']
const setQuickAmount = (amount: string) => {
formRef.current?.setFieldsValue({amount})
}
const setAllAmount = () => {
formRef.current?.setFieldsValue({amount: availableAmount.replace(/,/g, '')})
}
// 格式化金额
const formatMoney = (money?: string) => {
if (!money) return '0.00'
return parseFloat(money).toFixed(2)
}
const renderWithdrawForm = () => (
<View>
{/* 余额卡片 */}
<View className="rounded-xl p-6 mb-6 text-white relative overflow-hidden" style={{
background: businessGradients.dealer.header
}}>
{/* 装饰背景 - 小程序兼容版本 */}
<View className="absolute top-0 right-0 w-24 h-24 rounded-full" style={{
backgroundColor: 'rgba(255, 255, 255, 0.1)',
right: '-12px',
top: '-12px'
}}></View>
<View className="flex items-center justify-between relative z-10">
<View 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>
</View>
<View className="p-3 rounded-full" style={{
background: 'rgba(255, 255, 255, 0.2)'
}}>
<Wallet color="white" size="32"/>
</View>
</View>
<View className="mt-4 pt-4 relative z-10" style={{
borderTop: '1px solid rgba(255, 255, 255, 0.3)'
}}>
<Text className="text-white text-opacity-80 text-xs">
¥100 |
</Text>
</View>
</View>
<Form
ref={formRef}
onFinish={handleSubmit}
labelPosition="top"
>
<CellGroup>
<Form.Item name="amount" label="提现金额" required>
<Input
placeholder="请输入提现金额"
type="number"
onChange={(value) => {
// 实时验证提现金额
const amount = parseFloat(value)
const available = parseFloat(availableAmount.replace(/,/g, ''))
if (!isNaN(amount) && amount > available) {
// 可以在这里添加实时提示,但不阻止输入
}
}}
/>
</Form.Item>
{/* 快捷金额 */}
<View className="px-4 py-2">
<Text className="text-sm text-gray-600 mb-2"></Text>
<View className="flex flex-wrap gap-2">
{quickAmounts.map(amount => (
<Button
key={amount}
size="small"
fill="outline"
onClick={() => setQuickAmount(amount)}
>
{amount}
</Button>
))}
<Button
size="small"
fill="outline"
onClick={setAllAmount}
>
</Button>
</View>
</View>
<Form.Item name="accountType" label="提现方式" required>
<Radio.Group value={selectedAccount} onChange={() => setSelectedAccount}>
<Cell.Group>
<Cell>
<Radio value="wechat"></Radio>
</Cell>
<Cell>
<Radio value="alipay"></Radio>
</Cell>
<Cell>
<Radio value="bank"></Radio>
</Cell>
</Cell.Group>
</Radio.Group>
</Form.Item>
{selectedAccount === 'alipay' && (
<>
<Form.Item name="account" label="支付宝账号" required>
<Input placeholder="请输入支付宝账号"/>
</Form.Item>
<Form.Item name="accountName" label="支付宝姓名" required>
<Input placeholder="请输入支付宝实名姓名"/>
</Form.Item>
</>
)}
{selectedAccount === 'bank' && (
<>
<Form.Item name="bankName" label="开户银行" required>
<Input placeholder="请输入开户银行名称"/>
</Form.Item>
<Form.Item name="account" label="银行卡号" required>
<Input placeholder="请输入银行卡号"/>
</Form.Item>
<Form.Item name="accountName" label="开户姓名" required>
<Input placeholder="请输入银行卡开户姓名"/>
</Form.Item>
</>
)}
{selectedAccount === 'wechat' && (
<View className="px-4 py-2">
<Text className="text-sm text-gray-500">
</Text>
</View>
)}
</CellGroup>
<View className="mt-6 px-4">
<Button
block
type="primary"
nativeType="submit"
loading={submitting}
disabled={submitting || !selectedAccount}
>
{submitting ? '提交中...' : '申请提现'}
</Button>
</View>
</Form>
</View>
)
const renderWithdrawRecords = () => {
console.log('渲染提现记录:', {loading, recordsCount: withdrawRecords.length, dealerUserId: dealerUser?.userId})
return (
<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>
<Text className="font-semibold text-gray-800 mb-1">
¥{record.money}
</Text>
<Text className="text-sm text-gray-500">
{record.accountDisplay}
</Text>
</Space>
<Tag type={getStatusColor(record.applyStatus)}>
{getStatusText(record.applyStatus)}
</Tag>
</View>
<View className="text-xs text-gray-400">
<Text>{record.createTime}</Text>
{record.auditTime && (
<Text className="block mt-1">
{new Date(record.auditTime).toLocaleString()}
</Text>
)}
{record.rejectReason && (
<Text className="block mt-1 text-red-500">
{record.rejectReason}
</Text>
)}
</View>
</View>
))
) : (
<Empty description="暂无提现记录"/>
)}
</View>
</PullToRefresh>
)
}
if (!dealerUser) {
return (
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
<Loading/>
<Text className="text-gray-500 mt-2">...</Text>
</View>
)
}
return (
<View className="bg-gray-50 min-h-screen">
<Tabs value={activeTab} onChange={handleTabChange}>
<Tabs.TabPane title="申请提现" value="0">
{renderWithdrawForm()}
</Tabs.TabPane>
<Tabs.TabPane title="提现记录" value="1">
{renderWithdrawRecords()}
</Tabs.TabPane>
</Tabs>
</View>
)
}
export default DealerWithdraw

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

@@ -86,9 +86,9 @@ const DealerBank = () => {
description="您还没有地址哦"
/>
<Space>
<Button onClick={() => Taro.navigateTo({url: '/dealer/bank/add'})}></Button>
<Button onClick={() => Taro.navigateTo({url: '/doctor/bank/add'})}></Button>
<Button type="success" fill="dashed"
onClick={() => Taro.navigateTo({url: '/dealer/bank/wxAddress'})}></Button>
onClick={() => Taro.navigateTo({url: '/doctor/bank/wxAddress'})}></Button>
</Space>
</div>
</ConfigProvider>
@@ -126,7 +126,7 @@ const DealerBank = () => {
</Cell.Group>
))}
{/* 底部浮动按钮 */}
<FixedButton text={'新增银行卡'} onClick={() => Taro.navigateTo({url: '/dealer/bank/add'})} />
<FixedButton text={'新增银行卡'} onClick={() => Taro.navigateTo({url: '/doctor/bank/add'})} />
</View>
);
};

View File

@@ -99,7 +99,7 @@ CustomerManagement
## 文件结构
```
src/dealer/customer/
src/doctor/customer/
├── index.tsx # 主页面组件
└── README.md # 说明文档

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
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} from '@nutui/nutui-react-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 {
@@ -26,7 +26,8 @@ const CustomerIndex = () => {
const [list, setList] = useState<CustomerUser[]>([])
const [loading, setLoading] = useState<boolean>(false)
const [activeTab, setActiveTab] = useState<CustomerStatus>('all')
const [searchValue, _] = useState<string>('')
const [searchValue, setSearchValue] = useState<string>('')
const [displaySearchValue, setDisplaySearchValue] = useState<string>('')
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
@@ -227,13 +228,22 @@ const CustomerIndex = () => {
}
// 防抖搜索功能
useEffect(() => {
const timer = setTimeout(() => {
setDisplaySearchValue(searchValue);
}, 300); // 300ms 防抖
return () => clearTimeout(timer);
}, [searchValue]);
// 根据搜索条件筛选数据状态筛选已在API层面处理
const getFilteredList = () => {
let filteredList = list;
// 按搜索关键词筛选
if (searchValue.trim()) {
const keyword = searchValue.trim().toLowerCase();
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)) ||
@@ -466,56 +476,81 @@ const CustomerIndex = () => {
// 渲染客户列表
const renderCustomerList = () => {
const filteredList = getFilteredList();
const isSearching = displaySearchValue.trim().length > 0;
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-3 flex items-center justify-center'}>
<Text className="text-gray-500 text-sm"></Text>
<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>
)
}
>
{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>
) : (
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">

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

@@ -1,4 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '客户报备',
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

@@ -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>
)
}

View File

@@ -50,7 +50,7 @@ export const useUser = () => {
const inviteParams = getStoredInviteParams()
if (currentPage?.route !== 'dealer/apply/add' && inviteParams?.inviter) {
return Taro.navigateTo({
url: '/dealer/apply/add'
url: '/doctor/apply/add'
});
}
});

View File

@@ -41,7 +41,7 @@ function Cart() {
useShareAppMessage(() => {
return {
title: '购物车 - 时里院子市集',
title: '购物车 - 通源堂健康生态平台',
success: function () {
console.log('分享成功');
},

View File

@@ -44,7 +44,7 @@ function Category() {
useShareAppMessage(() => {
return {
title: `${nav?.categoryName}_时里院子市集`,
title: `${nav?.categoryName}_通源堂健康生态平台`,
path: `/shop/category/index?id=${categoryId}`,
success: function () {
console.log('分享成功');

View File

@@ -5,8 +5,10 @@ import {ArrowRight, Reward, Setting} from '@nutui/icons-react-taro'
import {useUser} from '@/hooks/useUser'
import {useEffect} from "react";
import {useDealerUser} from "@/hooks/useDealerUser";
import {useThemeStyles} from "@/hooks/useTheme";
const IsDealer = () => {
const themeStyles = useThemeStyles();
const {isSuperAdmin} = useUser();
const {dealerUser} = useDealerUser()
@@ -23,9 +25,7 @@ const IsDealer = () => {
<View className={'px-4'} style={{ marginTop: '8px', position: 'relative', zIndex: 25 }}>
<Cell
className="nutui-cell-clickable"
style={{
backgroundImage: 'linear-gradient(to right bottom, #e53e3e, #c53030)',
}}
style={themeStyles.primaryBackground}
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Setting className={'text-white '} size={16}/>
@@ -49,19 +49,17 @@ const IsDealer = () => {
<View className={'px-4'} style={{ marginTop: '8px', position: 'relative', zIndex: 25 }}>
<Cell
className="nutui-cell-clickable"
style={{
backgroundImage: 'linear-gradient(to right bottom, #54a799, #177b73)',
}}
style={themeStyles.primaryBackground}
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Reward className={'text-orange-100 '} size={16}/>
<Text style={{fontSize: '16px'}}
className={'pl-3 text-orange-100 font-medium'}></Text>
className={'pl-3 text-orange-100 font-medium'}>VIP申请</Text>
{/*<Text className={'text-white opacity-80 pl-3'}>门店核销</Text>*/}
</View>
}
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => navTo('/dealer/index', true)}
onClick={() => navTo('/doctor/index', true)}
/>
</View>
</>
@@ -76,9 +74,7 @@ const IsDealer = () => {
<View className={'px-4'} style={{ marginTop: '8px', position: 'relative', zIndex: 25 }}>
<Cell
className="nutui-cell-clickable"
style={{
backgroundImage: 'linear-gradient(to right bottom, #54a799, #177b73)',
}}
style={themeStyles.primaryBackground}
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Reward className={'text-orange-100 '} size={16}/>
@@ -87,7 +83,7 @@ const IsDealer = () => {
</View>
}
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => navTo('/dealer/apply/add', true)}
onClick={() => navTo('/doctor/apply/add', true)}
/>
</View>
</>

View File

@@ -1,5 +1,5 @@
import {Avatar, Tag, Space, Button} from '@nutui/nutui-react-taro'
import {View, Text, Image} from '@tarojs/components'
import {Avatar, Tag, Space} from '@nutui/nutui-react-taro'
import {View, Text} from '@tarojs/components'
import {getUserInfo, getWxOpenId} from '@/api/layout';
import Taro from '@tarojs/taro';
import {useEffect, useState, forwardRef, useImperativeHandle} from "react";
@@ -10,6 +10,7 @@ import {useUser} from "@/hooks/useUser";
import {useUserData} from "@/hooks/useUserData";
import {getStoredInviteParams} from "@/utils/invite";
import UnifiedQRButton from "@/components/UnifiedQRButton";
import {useThemeStyles} from "@/hooks/useTheme";
const UserCard = forwardRef<any, any>((_, ref) => {
const {data, refresh} = useUserData()
@@ -17,6 +18,8 @@ const UserCard = forwardRef<any, any>((_, ref) => {
const [IsLogin, setIsLogin] = useState<boolean>(false)
const [userInfo, setUserInfo] = useState<User>()
const themeStyles = useThemeStyles();
// 下拉刷新
const handleRefresh = async () => {
await refresh()
@@ -95,7 +98,6 @@ const UserCard = forwardRef<any, any>((_, ref) => {
});
};
const openSetting = () => {
// Taro.openSetting调起客户端小程序设置界面返回用户设置的操作结果。设置界面只会出现小程序已经向用户请求过的权限。
Taro.openSetting({
@@ -118,6 +120,11 @@ const UserCard = forwardRef<any, any>((_, ref) => {
const handleGetPhoneNumber = ({detail}: { detail: { code?: string, encryptedData?: string, iv?: string } }) => {
const {code, encryptedData, iv} = detail
// 判断用户是否已登录
if(IsLogin){
return navTo(`/user/profile/profile`)
}
// 获取存储的邀请参数
const inviteParams = getStoredInviteParams()
const refereeId = inviteParams?.inviter ? parseInt(inviteParams.inviter) : 0
@@ -165,35 +172,19 @@ const UserCard = forwardRef<any, any>((_, ref) => {
}
return (
<View className={'pt-20'}>
<View className={'p-4'}>
<View className={'pt-14'}>
{/* 使用相对定位容器,让个人资料图片可以绝对定位在右上角 */}
<View className="relative z-20">
<View
className={'user-card w-full flex flex-col justify-around rounded-xl'}
style={{
background: 'linear-gradient(to bottom, #ffffff, #ffffff)',
height: '170px',
}}
>
<View className={'user-card-header flex w-full justify-between items-center pt-4'}>
<View className={'flex items-center mx-4'}>
{
IsLogin ? (
<Avatar size="large"
src={userInfo?.avatar || 'https://oss.wsdns.cn/20250623/62f830b85edb4a7293b8948c25e6f987.jpeg'}
shape="round"/>
) : (
<Button className={'text-black'} open-type="getPhoneNumber"
onGetPhoneNumber={handleGetPhoneNumber}>
<Avatar size="large"
src={userInfo?.avatar || 'https://oss.wsdns.cn/20250623/62f830b85edb4a7293b8948c25e6f987.jpeg'}
shape="round"/>
</Button>
)
}
<View className={'flex items-center mx-4'} onClick={handleGetPhoneNumber}>
<Avatar size="large"
src={userInfo?.avatar || ''}
shape="round"/>
<View className={'user-info flex flex-col px-2'}>
<View className={'py-1 text-black font-bold'}>{getDisplayName()}</View>
<View className={'py-1 text-white font-bold'}>{getDisplayName()}</View>
{IsLogin ? (
<View className={'grade text-xs py-1'}>
<Tag type="success" round>
@@ -211,7 +202,7 @@ const UserCard = forwardRef<any, any>((_, ref) => {
}}>
{/*统一扫码入口 - 支持登录和核销*/}
<UnifiedQRButton
text="扫"
text="扫一扫"
size="small"
onSuccess={(result) => {
console.log('统一扫码成功:', result);
@@ -230,47 +221,31 @@ const UserCard = forwardRef<any, any>((_, ref) => {
/>
</Space>
</View>
<View className={'flex justify-around mt-1'}>
<View className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/wallet/wallet', true)}>
<Text className={'text-sm text-gray-500'}></Text>
<Text className={'text-xl'}>{data?.balance || '0.00'}</Text>
</View>
<View className={'item flex justify-center flex-col items-center'}>
<Text className={'text-sm text-gray-500'}></Text>
<Text className={'text-xl'}>{data?.points || 0}</Text>
</View>
<View className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/coupon/index', true)}>
<Text className={'text-sm text-gray-500'}></Text>
<Text className={'text-xl'}>{data?.coupons || 0}</Text>
</View>
<View className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/gift/index', true)}>
<Text className={'text-sm text-gray-500'}></Text>
<Text className={'text-xl'}>{data?.giftCards || 0}</Text>
<View className={'py-2'}>
<View className={'flex justify-around mt-1'}>
<View className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/wallet/wallet', true)}>
<Text className={'text-sm'} style={themeStyles.textColor}></Text>
<Text className={'text-xl'} style={themeStyles.textColor}>{data?.balance || '0.00'}</Text>
</View>
<View className={'item flex justify-center flex-col items-center'}>
<Text className={'text-sm'} style={themeStyles.textColor}></Text>
<Text className={'text-xl'} style={themeStyles.textColor}>{data?.points || 0}</Text>
</View>
<View className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/coupon/index', true)}>
<Text className={'text-sm'} style={themeStyles.textColor}></Text>
<Text className={'text-xl'} style={themeStyles.textColor}>{data?.coupons || 0}</Text>
</View>
<View className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/gift/index', true)}>
<Text className={'text-sm'} style={themeStyles.textColor}></Text>
<Text className={'text-xl'} style={themeStyles.textColor}>{data?.giftCards || 0}</Text>
</View>
</View>
</View>
</View>
{/* 个人资料图片,定位在右上角 */}
<View
className="absolute top-0 right-0 overflow-hidden z-30"
style={{
borderRadius: "0 0.75rem 0 0"
}}
onClick={() => navTo('/user/profile/profile', true)}
>
<Image
src="https://oss.wsdns.cn/20250913/7c3de38b377344b89131aba40214f63f.png"
style={{
width: "200rpx"
}}
mode="widthFix"
/>
</View>
</View>
</View>
</View>
)
})

View File

@@ -87,7 +87,7 @@ const UserCell = () => {
</View>
</Grid.Item>
<Grid.Item text={'推广邀请'} onClick={() => navTo('/dealer/team/index', true)}>
<Grid.Item text={'推广邀请'} onClick={() => navTo('/doctor/team/index', true)}>
<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"/>
@@ -95,7 +95,7 @@ const UserCell = () => {
</View>
</Grid.Item>
{/*<Grid.Item text={'我的邀请码'} onClick={() => navTo('/dealer/qrcode/index', true)}>*/}
{/*<Grid.Item text={'我的邀请码'} onClick={() => navTo('/doctor/qrcode/index', true)}>*/}
{/* <View className="text-center">*/}
{/* <View className="w-12 h-12 bg-orange-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
{/* <Dongdong color="#f59e0b" size="20"/>*/}

View File

@@ -33,7 +33,25 @@ function User() {
onRefresh={handleRefresh}
headHeight={60}
>
<View className={'h-44 w-full fixed top-0 z-0'} style={themeStyles.primaryBackground}></View>
{/* 装饰性背景 */}
<View className={'h-64 w-full fixed top-0 z-0'} style={themeStyles.primaryBackground}>
{/* 装饰性背景元素 - 小程序兼容版本 */}
<View className="absolute w-32 h-32 rounded-full" style={{
backgroundColor: 'rgba(255, 255, 255, 0.1)',
top: '-16px',
right: '-16px'
}}></View>
<View className="absolute w-24 h-24 rounded-full" style={{
backgroundColor: 'rgba(255, 255, 255, 0.08)',
bottom: '-12px',
left: '-12px'
}}></View>
<View className="absolute w-16 h-16 rounded-full" style={{
backgroundColor: 'rgba(255, 255, 255, 0.05)',
top: '60px',
left: '120px'
}}></View>
</View>
<UserCard ref={userCardRef}/>
<UserOrder/>
<IsDealer/>

View File

@@ -42,7 +42,7 @@ function Category() {
useShareAppMessage(() => {
return {
title: `${nav?.categoryName}_时里院子市集`,
title: `${nav?.categoryName}_通源堂健康生态平台`,
path: `/shop/category/index?id=${categoryId}`,
success: function () {
console.log('分享成功');

View File

@@ -110,7 +110,7 @@ const AddMessage = () => {
) : '选择发送对象'} extra={(
<ArrowRight color="#cccccc" className={toUser ? 'mt-2' : ''} size={toUser ? 20 : 18}/>
)}
onClick={() => navTo(`/dealer/team/index`, true)}/>
onClick={() => navTo(`/doctor/team/index`, true)}/>
<Form
ref={formRef}
divider

View File

@@ -51,7 +51,7 @@ const AddMessageDetail = () => {
) : '选择发送对象'} extra={(
<ArrowRight color="#cccccc" className={item ? 'mt-2' : ''} size={item ? 20 : 18}/>
)}
onClick={() => navTo(`/dealer/team/index`, true)}/>
onClick={() => navTo(`/doctor/team/index`, true)}/>
<CellGroup>
<Cell title={'发布人'} extra={item?.formUserAlias || item?.formUserName}/>
<Cell title={'创建时间'} extra={item?.createTime}/>

103
src/utils/customerStatus.ts Normal file
View File

@@ -0,0 +1,103 @@
/**
* 客户状态管理工具函数
*/
// 客户状态类型定义
export type CustomerStatus = 'all' | 'pending' | 'signed' | 'cancelled';
// 客户状态配置
export const CUSTOMER_STATUS_CONFIG = {
all: {
label: '全部',
color: '#666666',
tagType: 'default' as const
},
pending: {
label: '跟进中',
color: '#ff8800',
tagType: 'warning' as const
},
signed: {
label: '已签约',
color: '#52c41a',
tagType: 'success' as const
},
cancelled: {
label: '已取消',
color: '#999999',
tagType: 'default' as const
}
};
/**
* 获取状态文本
*/
export const getStatusText = (status: CustomerStatus): string => {
return CUSTOMER_STATUS_CONFIG[status]?.label || '';
};
/**
* 获取状态标签类型
*/
export const getStatusTagType = (status: CustomerStatus) => {
return CUSTOMER_STATUS_CONFIG[status]?.tagType || 'default';
};
/**
* 获取状态颜色
*/
export const getStatusColor = (status: CustomerStatus): string => {
return CUSTOMER_STATUS_CONFIG[status]?.color || '#666666';
};
/**
* 获取所有状态选项
*/
export const getStatusOptions = () => {
return Object.entries(CUSTOMER_STATUS_CONFIG).map(([value, config]) => ({
value: value as CustomerStatus,
label: config.label
}));
};
/**
* 将数字状态映射为字符串状态
*/
export const mapApplyStatusToCustomerStatus = (applyStatus: number): CustomerStatus => {
switch (applyStatus) {
case 10:
return 'pending'; // 跟进中
case 20:
return 'signed'; // 已签约
case 30:
return 'cancelled'; // 已取消
default:
return 'pending'; // 默认为跟进中
}
};
/**
* 将字符串状态映射为数字状态
*/
export const mapCustomerStatusToApplyStatus = (customerStatus: CustomerStatus): number | undefined => {
switch (customerStatus) {
case 'pending':
return 10; // 跟进中
case 'signed':
return 20; // 已签约
case 'cancelled':
return 30; // 已取消
case 'all':
return undefined; // 全部,不筛选
default:
return undefined;
}
};
/**
* 临时函数:生成随机状态(实际项目中应该删除,从数据库获取真实状态)
*/
export const getRandomStatus = (): CustomerStatus => {
const statuses: CustomerStatus[] = ['pending', 'signed', 'cancelled'];
return statuses[Math.floor(Math.random() * statuses.length)];
};

126
src/utils/dateUtils.ts Normal file
View File

@@ -0,0 +1,126 @@
/**
* 日期格式化工具函数
* 用于处理各种日期格式转换
*/
/**
* 格式化日期为数据库格式 YYYY-MM-DD HH:mm:ss
* @param dateStr 输入的日期字符串,支持多种格式
* @returns 数据库格式的日期字符串
*/
export const formatDateForDatabase = (dateStr: string): string => {
if (!dateStr) return ''
let parts: string[] = []
// 处理不同的日期格式
if (dateStr.includes('/')) {
// 处理 YYYY/MM/DD 或 YYYY/M/D 格式
parts = dateStr.split('/')
} else if (dateStr.includes('-')) {
// 处理 YYYY-MM-DD 或 YYYY-M-D 格式
parts = dateStr.split('-')
} else {
return dateStr
}
if (parts.length !== 3) return dateStr
const year = parts[0]
const month = parts[1].padStart(2, '0')
const day = parts[2].padStart(2, '0')
return `${year}-${month}-${day} 00:00:00`
}
/**
* 从数据库格式提取日期部分用于Calendar组件显示
* @param dateTimeStr 数据库格式的日期时间字符串
* @returns Calendar组件需要的格式 (YYYY-M-D)
*/
export const extractDateForCalendar = (dateTimeStr: string): string => {
if (!dateTimeStr) return ''
// 处理不同的输入格式
let dateStr = ''
if (dateTimeStr.includes(' ')) {
// 从 "YYYY-MM-DD HH:mm:ss" 格式中提取日期部分
dateStr = dateTimeStr.split(' ')[0]
} else {
dateStr = dateTimeStr
}
// 转换为Calendar组件需要的格式 (YYYY-M-D)
if (dateStr.includes('-')) {
const parts = dateStr.split('-')
if (parts.length === 3) {
const year = parts[0]
const month = parseInt(parts[1]).toString() // 去掉前导0
const day = parseInt(parts[2]).toString() // 去掉前导0
return `${year}-${month}-${day}`
}
}
return dateStr
}
/**
* 格式化日期为用户友好的显示格式 YYYY-MM-DD
* @param dateStr 输入的日期字符串
* @returns 用户友好的日期格式
*/
export const formatDateForDisplay = (dateStr: string): string => {
if (!dateStr) return ''
// 如果是数据库格式,先提取日期部分
let dateOnly = dateStr
if (dateStr.includes(' ')) {
dateOnly = dateStr.split(' ')[0]
}
// 如果已经是标准格式,直接返回
if (/^\d{4}-\d{2}-\d{2}$/.test(dateOnly)) {
return dateOnly
}
// 处理其他格式
let parts: string[] = []
if (dateOnly.includes('/')) {
parts = dateOnly.split('/')
} else if (dateOnly.includes('-')) {
parts = dateOnly.split('-')
} else {
return dateStr
}
if (parts.length !== 3) return dateStr
const year = parts[0]
const month = parts[1].padStart(2, '0')
const day = parts[2].padStart(2, '0')
return `${year}-${month}-${day}`
}
/**
* 获取当前日期的字符串格式
* @param format 'database' | 'display' | 'calendar'
* @returns 格式化的当前日期
*/
export const getCurrentDate = (format: 'database' | 'display' | 'calendar' = 'display'): string => {
const now = new Date()
const year = now.getFullYear()
const month = now.getMonth() + 1
const day = now.getDate()
switch (format) {
case 'database':
return `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')} 00:00:00`
case 'display':
return `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`
case 'calendar':
return `${year}-${month}-${day}`
default:
return `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`
}
}