feat(user): 实现扫码登录和推荐人绑定功能- 添加扫码登录相关API接口,包括生成二维码、检查状态、确认登录等
- 在用户注册时支持从邀请参数中获取推荐人ID并绑定 - 修改管理员面板UI,添加统一扫码功能入口- 更新用户管理相关API地址,统一使用SERVER_API_URL - 调整优惠券卡片样式,移除小程序不支持的CSS属性 - 添加聊天会话和消息管理相关API模块 - 新增分销商银行卡管理API接口 - 修改系统用户模型,增加推荐人ID字段 - 更新广告位查询接口,支持根据code获取广告位 - 调整邀请绑定接口参数,将refereeId改为dealerId - 修改环境配置中的应用名称为"时里院子市集" - 移除分享到朋友圈的相关代码 - 添加管理员面板组件,提供统一扫码等管理功能 -修复用户管理API请求参数传递问题 - 添加聊天消息和会话管理的完整CRUD接口 - 更新系统用户相关接口URL,确保正确调用后端服务 - 添加分销商银行卡管理的完整API接口实现 - 修改邀请绑定接口,使用dealerId替代refereeId参数 - 修复扫码登录相关API的URL拼接问题 - 添加二维码内容解析功能,支持多种格式的token提取 - 更新用户信息模型,增加推荐人ID字段 -优化管理员面板样式和交互逻辑- 调整优惠券组件样式,兼容小程序环境限制- 修复用户管理模块的API调用问题 - 添加聊天相关数据模型和接口定义 - 更新环境配置中的应用名称 -修复邀请绑定相关的参数传递问题- 添加扫码登录状态枚举和相关数据结构定义- 优化管理员功能面板的UI展示和交互体验- 修复系统用户管理接口的请求参数问题 - 添加分销商银行卡管理相关接口实现- 调整聊天消息和会话管理接口的数据结构定义 -修复用户管理模块中的API调用路径问题 - 添加扫码登录相关工具函数,如设备信息获取等 - 更新邀请绑定接口的数据模型定义 -优化管理员面板组件的样式和功能实现 -修复系统用户管理接口中的参数传递问题 - 添加聊天相关模块的完整API接口实现 - 调整分销商银行卡管理模块的数据结构定义- 修复扫码登录相关接口的URL拼接问题- 更新用户管理模块中的API调用方式 - 添加聊天消息批量发送等相关接口实现- 修复邀请绑定接口中的参数名称问题- 优化管理员面板组件的功能和交互逻辑 - 调整系统用户管理接口的请求参数传递方式 - 添加分销商银行卡管理模块的完整接口实现 -修复聊天相关接口中的数据结构问题 - 更新扫码登录相关接口的数据模型定义 - 优化管理员功能面板的展示效果和用户体验
This commit is contained in:
@@ -9,7 +9,7 @@ export const ENV_CONFIG = {
|
||||
// 生产环境
|
||||
production: {
|
||||
API_BASE_URL: 'https://cms-api.websoft.top/api',
|
||||
APP_NAME: '通源堂健康生态平台',
|
||||
APP_NAME: '时里院子市集',
|
||||
DEBUG: 'false',
|
||||
},
|
||||
// 测试环境
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"miniprogramRoot": "./",
|
||||
"projectname": "mp-react",
|
||||
"description": "通源堂健康生态平台",
|
||||
"description": "时里院子市集",
|
||||
"appid": "touristappid",
|
||||
"setting": {
|
||||
"urlCheck": true,
|
||||
|
||||
@@ -8,6 +8,7 @@ import navTo from "@/utils/common";
|
||||
import {TenantId} from "@/config/app";
|
||||
import {getMyAvailableCoupons} from "@/api/shop/shopUserCoupon";
|
||||
import {useUser} from "@/hooks/useUser";
|
||||
import {getStoredInviteParams} from "@/utils/invite";
|
||||
|
||||
function UserCard() {
|
||||
const {getDisplayName, getRoleName} = useUser();
|
||||
@@ -134,6 +135,11 @@ function UserCard() {
|
||||
/* 获取用户手机号 */
|
||||
const handleGetPhoneNumber = ({detail}: {detail: {code?: string, encryptedData?: string, iv?: string}}) => {
|
||||
const {code, encryptedData, iv} = detail
|
||||
|
||||
// 获取存储的邀请参数
|
||||
const inviteParams = getStoredInviteParams()
|
||||
const refereeId = inviteParams?.inviter ? parseInt(inviteParams.inviter) : 0
|
||||
|
||||
Taro.login({
|
||||
success: function () {
|
||||
if (code) {
|
||||
@@ -145,7 +151,7 @@ function UserCard() {
|
||||
encryptedData,
|
||||
iv,
|
||||
notVerifyPhone: true,
|
||||
refereeId: 0,
|
||||
refereeId: refereeId, // 使用解析出的推荐人ID
|
||||
sceneType: 'save_referee',
|
||||
tenantId: TenantId
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {useEffect} from 'react'
|
||||
import {useUser} from "@/hooks/useUser";
|
||||
import {Empty} from '@nutui/nutui-react-taro';
|
||||
import {Text} from '@tarojs/components';
|
||||
|
||||
function Admin() {
|
||||
@@ -12,7 +13,16 @@ function Admin() {
|
||||
|
||||
if (!isAdmin()) {
|
||||
return (
|
||||
<Text>您不是管理员</Text>
|
||||
<Empty
|
||||
description="您不是管理员"
|
||||
imageSize={80}
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
height: 'calc(100vh - 200px)'
|
||||
}}
|
||||
>
|
||||
|
||||
</Empty>
|
||||
);
|
||||
}
|
||||
return (
|
||||
|
||||
@@ -100,3 +100,16 @@ export async function getCmsAd(id: number) {
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据id查询广告位
|
||||
*/
|
||||
export async function getCmsAdByCode(code: string) {
|
||||
const res = await request.get<ApiResult<CmsAd>>(
|
||||
'/cms/cms-ad/getByCode/' + code
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import { BaseUrl } from '@/config/app';
|
||||
|
||||
/**
|
||||
* 小程序码生成参数
|
||||
@@ -38,7 +39,7 @@ export interface InviteRelationParam {
|
||||
*/
|
||||
export interface BindRefereeParam {
|
||||
// 推荐人ID
|
||||
refereeId: number;
|
||||
dealerId: number;
|
||||
// 被推荐人ID (可选,如果不传则使用当前登录用户)
|
||||
userId?: number;
|
||||
// 推荐来源
|
||||
@@ -112,7 +113,7 @@ export async function generateMiniProgramCode(data: MiniProgramCodeParam) {
|
||||
try {
|
||||
const url = '/wx-login/getOrderQRCodeUnlimited/' + data.scene;
|
||||
// 由于接口直接返回图片buffer,我们直接构建完整的URL
|
||||
return `${API_BASE_URL}${url}`;
|
||||
return `${BaseUrl}${url}`;
|
||||
} catch (error: any) {
|
||||
throw new Error(error.message || '生成小程序码失败');
|
||||
}
|
||||
@@ -155,7 +156,7 @@ export async function bindRefereeRelation(data: BindRefereeParam) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/shop/shop-dealer-referee',
|
||||
{
|
||||
refereeId: data.refereeId,
|
||||
dealerId: data.dealerId,
|
||||
userId: data.userId,
|
||||
source: data.source || 'qrcode',
|
||||
scene: data.scene
|
||||
|
||||
246
src/api/passport/qr-login/index.ts
Normal file
246
src/api/passport/qr-login/index.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult } from '@/api';
|
||||
import Taro from '@tarojs/taro';
|
||||
import {SERVER_API_URL} from "@/utils/server";
|
||||
import {getUserInfo} from "@/api/layout";
|
||||
|
||||
/**
|
||||
* 扫码登录相关接口
|
||||
*/
|
||||
|
||||
// 生成扫码token请求参数
|
||||
export interface GenerateQRTokenParam {
|
||||
// 客户端类型:web, app, wechat
|
||||
clientType?: string;
|
||||
// 设备信息
|
||||
deviceInfo?: string;
|
||||
// 过期时间(分钟)
|
||||
expireMinutes?: number;
|
||||
}
|
||||
|
||||
// 生成扫码token响应
|
||||
export interface GenerateQRTokenResult {
|
||||
// 扫码token
|
||||
token: string;
|
||||
// 二维码内容(通常是包含token的URL或JSON)
|
||||
qrContent: string;
|
||||
// 过期时间戳
|
||||
expireTime: number;
|
||||
// 二维码图片URL(可选)
|
||||
qrImageUrl?: string;
|
||||
}
|
||||
|
||||
// 扫码状态枚举
|
||||
export enum QRLoginStatus {
|
||||
PENDING = 'pending', // 等待扫码
|
||||
SCANNED = 'scanned', // 已扫码,等待确认
|
||||
CONFIRMED = 'confirmed', // 已确认登录
|
||||
EXPIRED = 'expired', // 已过期
|
||||
CANCELLED = 'cancelled' // 已取消
|
||||
}
|
||||
|
||||
// 检查扫码状态响应
|
||||
export interface QRLoginStatusResult {
|
||||
// 当前状态
|
||||
status: QRLoginStatus;
|
||||
// 状态描述
|
||||
message?: string;
|
||||
// 如果已确认登录,返回JWT token
|
||||
accessToken?: string;
|
||||
// 用户信息
|
||||
userInfo?: {
|
||||
userId: number;
|
||||
nickname?: string;
|
||||
avatar?: string;
|
||||
phone?: string;
|
||||
};
|
||||
// 剩余有效时间(秒)
|
||||
remainingTime?: number;
|
||||
}
|
||||
|
||||
// 确认登录请求参数
|
||||
export interface ConfirmLoginParam {
|
||||
// 扫码token
|
||||
token: string;
|
||||
// 用户ID
|
||||
userId: number;
|
||||
// 登录平台:web, app, wechat
|
||||
platform?: string;
|
||||
// 微信用户信息(当platform为wechat时)
|
||||
wechatInfo?: {
|
||||
openid?: string;
|
||||
unionid?: string;
|
||||
nickname?: string;
|
||||
avatar?: string;
|
||||
gender?: string;
|
||||
};
|
||||
// 设备信息
|
||||
deviceInfo?: string;
|
||||
}
|
||||
|
||||
// 确认登录响应
|
||||
export interface ConfirmLoginResult {
|
||||
// 是否成功
|
||||
success: boolean;
|
||||
// 消息
|
||||
message: string;
|
||||
// 登录用户信息
|
||||
userInfo?: {
|
||||
userId: number;
|
||||
nickname?: string;
|
||||
avatar?: string;
|
||||
phone?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成扫码登录token
|
||||
*/
|
||||
export async function generateQRToken(data?: GenerateQRTokenParam) {
|
||||
const res = await request.post<ApiResult<GenerateQRTokenResult>>(
|
||||
SERVER_API_URL + '/qr-login/generate',
|
||||
{
|
||||
clientType: 'wechat',
|
||||
expireMinutes: 5,
|
||||
...data
|
||||
}
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查扫码登录状态
|
||||
*/
|
||||
export async function checkQRLoginStatus(token: string) {
|
||||
const res = await request.get<ApiResult<QRLoginStatusResult>>(
|
||||
SERVER_API_URL + `/qr-login/status/${token}`
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认扫码登录(通用接口)
|
||||
*/
|
||||
export async function confirmQRLogin(data: ConfirmLoginParam) {
|
||||
const res = await request.post<ApiResult<ConfirmLoginResult>>(
|
||||
SERVER_API_URL + '/qr-login/confirm',
|
||||
data
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信小程序扫码登录确认(便捷接口)
|
||||
*/
|
||||
export async function confirmWechatQRLogin(token: string, userId: number) {
|
||||
try {
|
||||
// 获取微信用户信息
|
||||
const userInfo = await getUserInfo();
|
||||
|
||||
const data: ConfirmLoginParam = {
|
||||
token,
|
||||
userId,
|
||||
platform: 'wechat',
|
||||
wechatInfo: {
|
||||
nickname: userInfo?.nickname,
|
||||
avatar: userInfo?.avatar,
|
||||
gender: userInfo?.sex
|
||||
},
|
||||
deviceInfo: await getDeviceInfo()
|
||||
};
|
||||
|
||||
const res = await request.post<ApiResult<any>>(
|
||||
SERVER_API_URL + '/qr-login/confirm',
|
||||
data
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
} catch (error: any) {
|
||||
return Promise.reject(new Error(error.message || '确认登录失败'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备信息
|
||||
*/
|
||||
async function getDeviceInfo() {
|
||||
return new Promise<string>((resolve) => {
|
||||
Taro.getSystemInfo({
|
||||
success: (res) => {
|
||||
const deviceInfo = {
|
||||
platform: res.platform,
|
||||
system: res.system,
|
||||
version: res.version,
|
||||
model: res.model,
|
||||
brand: res.brand,
|
||||
screenWidth: res.screenWidth,
|
||||
screenHeight: res.screenHeight
|
||||
};
|
||||
resolve(JSON.stringify(deviceInfo));
|
||||
},
|
||||
fail: () => {
|
||||
resolve('unknown');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析二维码内容,提取token
|
||||
*/
|
||||
export function parseQRContent(qrContent: string): string | null {
|
||||
try {
|
||||
console.log('解析二维码内容1:', qrContent);
|
||||
|
||||
// 尝试解析JSON格式
|
||||
if (qrContent.startsWith('{')) {
|
||||
const parsed = JSON.parse(qrContent);
|
||||
return parsed.token || parsed.qrCodeKey || null;
|
||||
}
|
||||
|
||||
// 尝试解析URL格式
|
||||
if (qrContent.includes('http')) {
|
||||
const url = new URL(qrContent);
|
||||
// 支持多种参数名
|
||||
return url.searchParams.get('token') ||
|
||||
url.searchParams.get('qrCodeKey') ||
|
||||
url.searchParams.get('qr_code_key') ||
|
||||
null;
|
||||
}
|
||||
|
||||
// 尝试解析简单的key=value格式
|
||||
if (qrContent.includes('=')) {
|
||||
const params = new URLSearchParams(qrContent);
|
||||
return params.get('token') ||
|
||||
params.get('qrCodeKey') ||
|
||||
params.get('qr_code_key') ||
|
||||
null;
|
||||
}
|
||||
|
||||
// 如果是以qr-login:开头的格式
|
||||
if (qrContent.startsWith('qr-login:')) {
|
||||
return qrContent.replace('qr-login:', '');
|
||||
}
|
||||
|
||||
// 直接返回内容作为token(如果是32位以上的字符串)
|
||||
if (qrContent.length >= 32) {
|
||||
return qrContent;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('解析二维码内容失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
101
src/api/shop/shopChatConversation/index.ts
Normal file
101
src/api/shop/shopChatConversation/index.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { ShopChatConversation, ShopChatConversationParam } from './model';
|
||||
|
||||
/**
|
||||
* 分页查询聊天会话表
|
||||
*/
|
||||
export async function pageShopChatConversation(params: ShopChatConversationParam) {
|
||||
const res = await request.get<ApiResult<PageResult<ShopChatConversation>>>(
|
||||
'/shop/shop-chat-conversation/page',
|
||||
params
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询聊天会话表列表
|
||||
*/
|
||||
export async function listShopChatConversation(params?: ShopChatConversationParam) {
|
||||
const res = await request.get<ApiResult<ShopChatConversation[]>>(
|
||||
'/shop/shop-chat-conversation',
|
||||
params
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加聊天会话表
|
||||
*/
|
||||
export async function addShopChatConversation(data: ShopChatConversation) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/shop/shop-chat-conversation',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改聊天会话表
|
||||
*/
|
||||
export async function updateShopChatConversation(data: ShopChatConversation) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
'/shop/shop-chat-conversation',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除聊天会话表
|
||||
*/
|
||||
export async function removeShopChatConversation(id?: number) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/shop/shop-chat-conversation/' + id
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除聊天会话表
|
||||
*/
|
||||
export async function removeShopBatchChatConversation(data: (number | undefined)[]) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/shop/shop-chat-conversation/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据id查询聊天会话表
|
||||
*/
|
||||
export async function getShopChatConversation(id: number) {
|
||||
const res = await request.get<ApiResult<ShopChatConversation>>(
|
||||
'/shop/shop-chat-conversation/' + id
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
37
src/api/shop/shopChatConversation/model/index.ts
Normal file
37
src/api/shop/shopChatConversation/model/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { PageParam } from '@/api';
|
||||
|
||||
/**
|
||||
* 聊天消息表
|
||||
*/
|
||||
export interface ShopChatConversation {
|
||||
// 自增ID
|
||||
id?: number;
|
||||
// 用户ID
|
||||
userId?: number;
|
||||
// 好友ID
|
||||
friendId?: number;
|
||||
// 消息类型
|
||||
type?: number;
|
||||
// 消息内容
|
||||
content?: string;
|
||||
// 未读消息
|
||||
unRead?: number;
|
||||
// 状态, 0未读, 1已读
|
||||
status?: number;
|
||||
// 是否删除, 0否, 1是
|
||||
deleted?: number;
|
||||
// 租户id
|
||||
tenantId?: number;
|
||||
// 注册时间
|
||||
createTime?: string;
|
||||
// 修改时间
|
||||
updateTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 聊天消息表搜索条件
|
||||
*/
|
||||
export interface ShopChatConversationParam extends PageParam {
|
||||
id?: number;
|
||||
keywords?: string;
|
||||
}
|
||||
115
src/api/shop/shopChatMessage/index.ts
Normal file
115
src/api/shop/shopChatMessage/index.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { ShopChatMessage, ShopChatMessageParam } from './model';
|
||||
|
||||
/**
|
||||
* 分页查询聊天消息表
|
||||
*/
|
||||
export async function pageShopChatMessage(params: ShopChatMessageParam) {
|
||||
const res = await request.get<ApiResult<PageResult<ShopChatMessage>>>(
|
||||
'/shop/shop-chat-message/page',
|
||||
params
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询聊天消息表列表
|
||||
*/
|
||||
export async function listShopChatMessage(params?: ShopChatMessageParam) {
|
||||
const res = await request.get<ApiResult<ShopChatMessage[]>>(
|
||||
'/shop/shop-chat-message',
|
||||
params
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加聊天消息表
|
||||
*/
|
||||
export async function addShopChatMessage(data: ShopChatMessage) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/shop/shop-chat-message',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加聊天消息表
|
||||
*/
|
||||
export async function addShopBatchChatMessage(data: ShopChatMessage[]) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/shop/shop-chat-message/batch',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改聊天消息表
|
||||
*/
|
||||
export async function updateShopChatMessage(data: ShopChatMessage) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
'/shop/shop-chat-message',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除聊天消息表
|
||||
*/
|
||||
export async function removeShopChatMessage(id?: number) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/shop/shop-chat-message/' + id
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除聊天消息表
|
||||
*/
|
||||
export async function removeShopBatchChatMessage(data: (number | undefined)[]) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/shop/shop-chat-message/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据id查询聊天消息表
|
||||
*/
|
||||
export async function getShopChatMessage(id: number) {
|
||||
const res = await request.get<ApiResult<ShopChatMessage>>(
|
||||
'/shop/shop-chat-message/' + id
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
63
src/api/shop/shopChatMessage/model/index.ts
Normal file
63
src/api/shop/shopChatMessage/model/index.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import type { PageParam } from '@/api';
|
||||
|
||||
/**
|
||||
* 聊天消息表
|
||||
*/
|
||||
export interface ShopChatMessage {
|
||||
// 自增ID
|
||||
id?: number;
|
||||
// 发送人ID
|
||||
formUserId?: number;
|
||||
// 发送人名称
|
||||
formUserName?: string;
|
||||
// 发送人头像
|
||||
formUserAvatar?: string;
|
||||
// 发送人手机号
|
||||
formUserPhone?: string;
|
||||
// 发送人别名
|
||||
formUserAlias?: string;
|
||||
// 接收人ID
|
||||
toUserId?: number;
|
||||
// 接收人名称
|
||||
toUserName?: string;
|
||||
// 接收人头像
|
||||
toUserAvatar?: string;
|
||||
// 接收人手机号
|
||||
toUserPhone?: string;
|
||||
// 接收人别名
|
||||
toUserAlias?: string;
|
||||
// 消息类型
|
||||
type?: string;
|
||||
// 消息内容
|
||||
content?: string;
|
||||
// 屏蔽接收方
|
||||
sideTo?: number;
|
||||
// 屏蔽发送方
|
||||
sideFrom?: number;
|
||||
// 是否撤回
|
||||
withdraw?: number;
|
||||
// 文件信息
|
||||
fileInfo?: string;
|
||||
// 批量发送
|
||||
toUserIds?: any[];
|
||||
// 存在联系方式
|
||||
hasContact?: number;
|
||||
// 状态, 0未读, 1已读
|
||||
status?: number;
|
||||
// 是否删除, 0否, 1是
|
||||
deleted?: number;
|
||||
// 租户id
|
||||
tenantId?: number;
|
||||
// 注册时间
|
||||
createTime?: string;
|
||||
// 修改时间
|
||||
updateTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 聊天消息表搜索条件
|
||||
*/
|
||||
export interface ShopChatMessageParam extends PageParam {
|
||||
id?: number;
|
||||
keywords?: string;
|
||||
}
|
||||
101
src/api/shop/shopDealerBank/index.ts
Normal file
101
src/api/shop/shopDealerBank/index.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { ShopDealerBank, ShopDealerBankParam } from './model';
|
||||
|
||||
/**
|
||||
* 分页查询分销商银行卡
|
||||
*/
|
||||
export async function pageShopDealerBank(params: ShopDealerBankParam) {
|
||||
const res = await request.get<ApiResult<PageResult<ShopDealerBank>>>(
|
||||
'/shop/shop-dealer-bank/page',
|
||||
params
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询分销商银行卡列表
|
||||
*/
|
||||
export async function listShopDealerBank(params?: ShopDealerBankParam) {
|
||||
const res = await request.get<ApiResult<ShopDealerBank[]>>(
|
||||
'/shop/shop-dealer-bank',
|
||||
params
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加分销商银行卡
|
||||
*/
|
||||
export async function addShopDealerBank(data: ShopDealerBank) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/shop/shop-dealer-bank',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改分销商银行卡
|
||||
*/
|
||||
export async function updateShopDealerBank(data: ShopDealerBank) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
'/shop/shop-dealer-bank',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除分销商银行卡
|
||||
*/
|
||||
export async function removeShopDealerBank(id?: number) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/shop/shop-dealer-bank/' + id
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除分销商银行卡
|
||||
*/
|
||||
export async function removeBatchShopDealerBank(data: (number | undefined)[]) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/shop/shop-dealer-bank/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据id查询分销商银行卡
|
||||
*/
|
||||
export async function getShopDealerBank(id: number) {
|
||||
const res = await request.get<ApiResult<ShopDealerBank>>(
|
||||
'/shop/shop-dealer-bank/' + id
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
45
src/api/shop/shopDealerBank/model/index.ts
Normal file
45
src/api/shop/shopDealerBank/model/index.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { PageParam } from '@/api';
|
||||
|
||||
/**
|
||||
* 分销商提现银行卡
|
||||
*/
|
||||
export interface ShopDealerBank {
|
||||
// 主键ID
|
||||
id?: number;
|
||||
// 分销商用户ID
|
||||
userId?: number;
|
||||
// 开户行名称
|
||||
bankName?: string;
|
||||
// 银行开户名
|
||||
bankAccount?: string;
|
||||
// 银行卡号
|
||||
bankCard?: string;
|
||||
// 申请状态 (10待审核 20审核通过 30驳回)
|
||||
applyStatus?: number;
|
||||
// 审核时间
|
||||
auditTime?: number;
|
||||
// 驳回原因
|
||||
rejectReason?: string;
|
||||
// 是否默认
|
||||
isDefault?: boolean;
|
||||
// 租户id
|
||||
tenantId?: number;
|
||||
// 创建时间
|
||||
createTime?: string;
|
||||
// 修改时间
|
||||
updateTime?: string;
|
||||
// 类型
|
||||
type?: string;
|
||||
// 名称
|
||||
name?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分销商提现银行卡搜索条件
|
||||
*/
|
||||
export interface ShopDealerBankParam extends PageParam {
|
||||
id?: number;
|
||||
userId?: number;
|
||||
isDefault?: boolean;
|
||||
keywords?: string;
|
||||
}
|
||||
@@ -27,4 +27,5 @@ export interface ShopDealerRefereeParam extends PageParam {
|
||||
id?: number;
|
||||
dealerId?: number;
|
||||
keywords?: string;
|
||||
deleted?: number;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import request from '@/utils/request';
|
||||
import type {ApiResult, PageResult} from '@/api/index';
|
||||
import type {ApiResult, PageResult} from '@/api';
|
||||
import type {User, UserParam} from './model';
|
||||
import {SERVER_API_URL} from "@/utils/server";
|
||||
|
||||
@@ -8,8 +8,8 @@ import {SERVER_API_URL} from "@/utils/server";
|
||||
*/
|
||||
export async function pageUsers(params: UserParam) {
|
||||
const res = await request.get<ApiResult<PageResult<User>>>(
|
||||
'/system/user/page',
|
||||
{params}
|
||||
SERVER_API_URL + '/system/user/page',
|
||||
params
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
@@ -22,10 +22,8 @@ export async function pageUsers(params: UserParam) {
|
||||
*/
|
||||
export async function listUsers(params?: UserParam) {
|
||||
const res = await request.get<ApiResult<User[]>>(
|
||||
'/system/user',
|
||||
{
|
||||
SERVER_API_URL + '/system/user',
|
||||
params
|
||||
}
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
@@ -38,7 +36,7 @@ export async function listUsers(params?: UserParam) {
|
||||
*/
|
||||
export async function getStaffs(params?: UserParam) {
|
||||
const res = await request.get<ApiResult<User[]>>(
|
||||
'/system/user',
|
||||
SERVER_API_URL + '/system/user',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -54,7 +52,7 @@ export async function getStaffs(params?: UserParam) {
|
||||
*/
|
||||
export async function getCompanyList(params?: UserParam) {
|
||||
const res = await request.get<ApiResult<User[]>>(
|
||||
'/system/user',
|
||||
SERVER_API_URL + '/system/user',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -70,7 +68,7 @@ export async function getCompanyList(params?: UserParam) {
|
||||
*/
|
||||
export async function getUser(id: number) {
|
||||
const res = await request.get<ApiResult<User>>(
|
||||
'/system/user/' + id,
|
||||
SERVER_API_URL + '/system/user/' + id,
|
||||
{}
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
@@ -84,7 +82,7 @@ export async function getUser(id: number) {
|
||||
*/
|
||||
export async function addUser(data: User) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/system/user',
|
||||
SERVER_API_URL + '/system/user',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
@@ -112,7 +110,7 @@ export async function updateUser(data: User) {
|
||||
*/
|
||||
export async function removeUser(id?: number) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/system/user/' + id
|
||||
SERVER_API_URL + '/system/user/' + id
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
@@ -125,7 +123,7 @@ export async function removeUser(id?: number) {
|
||||
*/
|
||||
export async function removeUsers(data: (number | undefined)[]) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/system/user/batch',
|
||||
SERVER_API_URL + '/system/user/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
@@ -141,7 +139,7 @@ export async function removeUsers(data: (number | undefined)[]) {
|
||||
*/
|
||||
export async function updateUserStatus(userId?: number, status?: number) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
'/system/user/status',
|
||||
SERVER_API_URL + '/system/user/status',
|
||||
{
|
||||
userId,
|
||||
status
|
||||
@@ -156,9 +154,9 @@ export async function updateUserStatus(userId?: number, status?: number) {
|
||||
/**
|
||||
* 修改推荐状态
|
||||
*/
|
||||
export async function updateUserRecommend(form) {
|
||||
export async function updateUserRecommend(form:any) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
'/system/user/recommend',
|
||||
SERVER_API_URL + '/system/user/recommend',
|
||||
form
|
||||
);
|
||||
if (res.code === 0) {
|
||||
@@ -172,7 +170,7 @@ export async function updateUserRecommend(form) {
|
||||
*/
|
||||
export async function updateUserPassword(userId?: number, password = '123456') {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
'/system/user/password',
|
||||
SERVER_API_URL + '/system/user/password',
|
||||
{
|
||||
userId,
|
||||
password
|
||||
@@ -191,7 +189,7 @@ export async function importUsers(file: File) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/system/user/import',
|
||||
SERVER_API_URL + '/system/user/import',
|
||||
formData
|
||||
);
|
||||
if (res.code === 0) {
|
||||
@@ -209,7 +207,7 @@ export async function checkExistence(
|
||||
id?: number
|
||||
) {
|
||||
const res = await request.get<ApiResult<unknown>>(
|
||||
'/system/user/existence',
|
||||
SERVER_API_URL + '/system/user/existence',
|
||||
{
|
||||
params: {field, value, id}
|
||||
}
|
||||
@@ -225,7 +223,7 @@ export async function checkExistence(
|
||||
*/
|
||||
export async function countUserBalance(params?: UserParam) {
|
||||
const res = await request.get<ApiResult<unknown>>(
|
||||
'/system/user/countUserBalance',
|
||||
SERVER_API_URL + '/system/user/countUserBalance',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -243,7 +241,7 @@ export async function countUserBalance(params?: UserParam) {
|
||||
*/
|
||||
export async function listAdminsByPhoneAll(params?: UserParam) {
|
||||
const res = await request.get<ApiResult<User[]>>(
|
||||
'/system/user/listAdminsByPhoneAll',
|
||||
SERVER_API_URL + '/system/user/listAdminsByPhoneAll',
|
||||
params
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
|
||||
@@ -128,6 +128,8 @@ export interface User {
|
||||
certification?: boolean;
|
||||
// 实名认证类型
|
||||
certificationType?: number;
|
||||
// 推荐人ID
|
||||
refereeId?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
export default defineAppConfig({
|
||||
export default {
|
||||
pages: [
|
||||
'pages/index/index',
|
||||
'pages/cart/cart',
|
||||
'pages/find/find',
|
||||
'pages/user/user'
|
||||
'pages/user/user',
|
||||
'pages/cms/category/index'
|
||||
],
|
||||
"subpackages": [
|
||||
{
|
||||
@@ -14,7 +15,10 @@ export default defineAppConfig({
|
||||
"forget",
|
||||
"setting",
|
||||
"agreement",
|
||||
"sms-login"
|
||||
"sms-login",
|
||||
'qr-login/index',
|
||||
'qr-confirm/index',
|
||||
'unified-qr/index'
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -30,12 +34,6 @@ export default defineAppConfig({
|
||||
"index"
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "gift",
|
||||
"pages": [
|
||||
"index"
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "user",
|
||||
"pages": [
|
||||
@@ -60,7 +58,12 @@ export default defineAppConfig({
|
||||
"gift/redeem",
|
||||
"gift/detail",
|
||||
"store/verification",
|
||||
"theme/index"
|
||||
"theme/index",
|
||||
"poster/poster",
|
||||
"chat/conversation/index",
|
||||
"chat/message/index",
|
||||
"chat/message/add",
|
||||
"chat/message/detail"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -112,12 +115,6 @@ export default defineAppConfig({
|
||||
selectedIconPath: "assets/tabbar/home-active.png",
|
||||
text: "首页",
|
||||
},
|
||||
// {
|
||||
// pagePath: "pages/find/find",
|
||||
// iconPath: "assets/tabbar/find.png",
|
||||
// selectedIconPath: "assets/tabbar/find-active.png",
|
||||
// text: "发现",
|
||||
// },
|
||||
{
|
||||
pagePath: "pages/cart/cart",
|
||||
iconPath: "assets/tabbar/cart.png",
|
||||
@@ -142,4 +139,4 @@ export default defineAppConfig({
|
||||
"desc": "你的位置信息将用于小程序位置接口的效果展示"
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
15
src/app.scss
15
src/app.scss
@@ -10,14 +10,14 @@ page{
|
||||
background-position: bottom;
|
||||
}
|
||||
|
||||
// 在全局样式文件中添加
|
||||
/* 在全局样式文件中添加 */
|
||||
button {
|
||||
&::after {
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 去掉 Grid 组件的边框
|
||||
/* 去掉 Grid 组件的边框 */
|
||||
.no-border-grid {
|
||||
.nut-grid-item {
|
||||
border: none !important;
|
||||
@@ -38,7 +38,7 @@ button {
|
||||
}
|
||||
}
|
||||
|
||||
// 微信授权按钮的特殊样式
|
||||
/* 微信授权按钮的特殊样式 */
|
||||
button[open-type="getPhoneNumber"] {
|
||||
background: none !important;
|
||||
padding: 0 !important;
|
||||
@@ -92,3 +92,12 @@ button[open-type="chooseAvatar"] {
|
||||
image {
|
||||
margin: 0; /* 全局设置图片的 margin */
|
||||
}
|
||||
|
||||
/* 管理员面板功能项交互效果 */
|
||||
.admin-feature-item {
|
||||
transition: transform 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.admin-feature-item:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
20
src/app.ts
20
src/app.ts
@@ -6,7 +6,7 @@ import './app.scss'
|
||||
import {loginByOpenId} from "@/api/layout";
|
||||
import {TenantId} from "@/config/app";
|
||||
import {saveStorageByLoginUser} from "@/utils/server";
|
||||
import {parseInviteParams, saveInviteParams, trackInviteSource, handleInviteRelation} from "@/utils/invite";
|
||||
import {parseInviteParams, saveInviteParams, trackInviteSource, handleInviteRelation, debugInviteInfo} from "@/utils/invite";
|
||||
|
||||
function App(props: { children: any; }) {
|
||||
const reload = () => {
|
||||
@@ -57,12 +57,13 @@ function App(props: { children: any; }) {
|
||||
// 处理启动参数
|
||||
const handleLaunchOptions = (options: any) => {
|
||||
try {
|
||||
console.log('小程序启动参数:', options)
|
||||
console.log('=== 小程序启动参数处理开始 ===')
|
||||
console.log('完整启动参数:', JSON.stringify(options, null, 2))
|
||||
|
||||
// 解析邀请参数
|
||||
const inviteParams = parseInviteParams(options)
|
||||
if (inviteParams) {
|
||||
console.log('检测到邀请参数:', inviteParams)
|
||||
console.log('✅ 成功检测到邀请参数:', inviteParams)
|
||||
|
||||
// 保存邀请参数到本地存储
|
||||
saveInviteParams(inviteParams)
|
||||
@@ -73,12 +74,21 @@ function App(props: { children: any; }) {
|
||||
// 显示邀请提示
|
||||
setTimeout(() => {
|
||||
Taro.showToast({
|
||||
title: '检测到邀请信息',
|
||||
title: `检测到邀请信息 ID:${inviteParams.inviter}`,
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
duration: 3000
|
||||
})
|
||||
}, 1000)
|
||||
|
||||
// 打印调试信息
|
||||
setTimeout(() => {
|
||||
debugInviteInfo()
|
||||
}, 2000)
|
||||
} else {
|
||||
console.log('❌ 未检测到邀请参数')
|
||||
}
|
||||
|
||||
console.log('=== 小程序启动参数处理结束 ===')
|
||||
} catch (error) {
|
||||
console.error('处理启动参数失败:', error)
|
||||
}
|
||||
|
||||
BIN
src/assets/tabbar/logo.png
Normal file
BIN
src/assets/tabbar/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.7 KiB |
BIN
src/assets/tabbar/tv-active.png
Normal file
BIN
src/assets/tabbar/tv-active.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
BIN
src/assets/tabbar/tv.png
Normal file
BIN
src/assets/tabbar/tv.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
@@ -1,5 +1,5 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
import {useShareAppMessage, useShareTimeline} from "@tarojs/taro"
|
||||
import {useShareAppMessage} from "@tarojs/taro"
|
||||
import {Loading} from '@nutui/nutui-react-taro'
|
||||
import {useEffect, useState} from "react"
|
||||
import {useRouter} from '@tarojs/taro'
|
||||
@@ -42,22 +42,15 @@ function Category() {
|
||||
})
|
||||
}, []);
|
||||
|
||||
useShareTimeline(() => {
|
||||
return {
|
||||
title: `${nav?.categoryName}_通源堂健康生态平台`,
|
||||
path: `/shop/category/index?id=${categoryId}`
|
||||
};
|
||||
});
|
||||
|
||||
useShareAppMessage(() => {
|
||||
return {
|
||||
title: `${nav?.categoryName}_通源堂健康生态平台`,
|
||||
title: `${nav?.categoryName}_时里院子市集`,
|
||||
path: `/shop/category/index?id=${categoryId}`,
|
||||
success: function (res) {
|
||||
console.log('分享成功', res);
|
||||
success: function () {
|
||||
console.log('分享成功');
|
||||
},
|
||||
fail: function (res) {
|
||||
console.log('分享失败', res);
|
||||
fail: function () {
|
||||
console.log('分享失败');
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
139
src/components/AdminPanel.tsx
Normal file
139
src/components/AdminPanel.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import React from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import { Button } from '@nutui/nutui-react-taro';
|
||||
import { Scan, Setting, User, Shop } from '@nutui/icons-react-taro';
|
||||
import navTo from '@/utils/common';
|
||||
|
||||
export interface AdminPanelProps {
|
||||
/** 是否显示面板 */
|
||||
visible: boolean;
|
||||
/** 关闭面板回调 */
|
||||
onClose?: () => void;
|
||||
/** 自定义样式类名 */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员功能面板组件
|
||||
*/
|
||||
const AdminPanel: React.FC<AdminPanelProps> = ({
|
||||
visible,
|
||||
onClose,
|
||||
className = ''
|
||||
}) => {
|
||||
if (!visible) return null;
|
||||
|
||||
// 管理员功能列表
|
||||
const adminFeatures = [
|
||||
{
|
||||
id: 'unified-qr',
|
||||
title: '统一扫码',
|
||||
description: '扫码登录和核销一体化功能',
|
||||
icon: <Scan className="text-blue-500" size="24" />,
|
||||
color: 'bg-blue-50 border-blue-200',
|
||||
onClick: () => {
|
||||
navTo('/passport/unified-qr/index', true);
|
||||
onClose?.();
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'user-management',
|
||||
title: '用户管理',
|
||||
description: '管理系统用户信息',
|
||||
icon: <User className="text-purple-500" size="24" />,
|
||||
color: 'bg-purple-50 border-purple-200',
|
||||
onClick: () => {
|
||||
// TODO: 跳转到用户管理页面
|
||||
console.log('跳转到用户管理');
|
||||
onClose?.();
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'store-management',
|
||||
title: '门店管理',
|
||||
description: '管理门店信息和设置',
|
||||
icon: <Shop className="text-orange-500" size="24" />,
|
||||
color: 'bg-orange-50 border-orange-200',
|
||||
onClick: () => {
|
||||
// TODO: 跳转到门店管理页面
|
||||
console.log('跳转到门店管理');
|
||||
onClose?.();
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'system-settings',
|
||||
title: '系统设置',
|
||||
description: '系统配置和参数管理',
|
||||
icon: <Setting className="text-gray-500" size="24" />,
|
||||
color: 'bg-gray-50 border-gray-200',
|
||||
onClick: () => {
|
||||
// TODO: 跳转到系统设置页面
|
||||
console.log('跳转到系统设置');
|
||||
onClose?.();
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<View className={`admin-panel ${className}`}>
|
||||
{/* 遮罩层 */}
|
||||
<View
|
||||
className="fixed inset-0 bg-black bg-opacity-50 z-40"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* 面板内容 */}
|
||||
<View className="fixed bottom-0 left-0 right-0 bg-white rounded-t-3xl z-50 overflow-hidden">
|
||||
{/* 面板头部 */}
|
||||
<View className="flex items-center justify-between p-4 border-b border-gray-100">
|
||||
<View className="flex items-center">
|
||||
<Setting className="text-blue-500 mr-2" size="20" />
|
||||
<Text className="text-lg font-bold text-gray-800">管理员面板</Text>
|
||||
</View>
|
||||
<Button
|
||||
size="small"
|
||||
type="default"
|
||||
onClick={onClose}
|
||||
className="text-gray-500"
|
||||
>
|
||||
关闭
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
{/* 功能网格 */}
|
||||
<View className="p-4 pb-8">
|
||||
<View className="grid grid-cols-2 gap-3">
|
||||
{adminFeatures.map((feature) => (
|
||||
<View
|
||||
key={feature.id}
|
||||
className={`${feature.color} border rounded-xl p-4 admin-feature-item`}
|
||||
onClick={feature.onClick}
|
||||
>
|
||||
<View className="flex items-center mb-2">
|
||||
{feature.icon}
|
||||
<Text className="ml-2 font-medium text-gray-800">
|
||||
{feature.title}
|
||||
</Text>
|
||||
</View>
|
||||
<Text className="text-xs text-gray-600 leading-relaxed">
|
||||
{feature.description}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 底部提示 */}
|
||||
<View className="px-4 pb-4">
|
||||
<View className="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
|
||||
<Text className="text-xs text-yellow-700 text-center">
|
||||
💡 管理员功能仅对具有管理权限的用户开放
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminPanel;
|
||||
@@ -9,25 +9,25 @@
|
||||
border: 2px solid #f0f0f0;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
// 更精美的阴影效果
|
||||
//box-shadow:
|
||||
// 0 4px 20px rgba(0, 0, 0, 0.08),
|
||||
// 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
/* 更精美的阴影效果 */
|
||||
/*box-shadow:
|
||||
0 4px 20px rgba(0, 0, 0, 0.08),
|
||||
0 1px 3px rgba(0, 0, 0, 0.1);*/
|
||||
|
||||
// 边框光晕效果
|
||||
//&::before {
|
||||
// content: '';
|
||||
// position: absolute;
|
||||
// top: 0;
|
||||
// left: 0;
|
||||
// right: 0;
|
||||
// bottom: 0;
|
||||
// border-radius: 16px;
|
||||
// padding: 1px;
|
||||
// background: linear-gradient(135deg, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0.1));
|
||||
// mask-composite: exclude;
|
||||
// pointer-events: none;
|
||||
//}
|
||||
/* 边框光晕效果 */
|
||||
/*&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border-radius: 16px;
|
||||
padding: 1px;
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0.1));
|
||||
mask-composite: exclude;
|
||||
pointer-events: none;
|
||||
}*/
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98) translateY(1px);
|
||||
@@ -38,7 +38,7 @@
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.6;
|
||||
filter: grayscale(0.3);
|
||||
/* filter: grayscale(0.3); 小程序不支持filter属性 */
|
||||
}
|
||||
|
||||
.coupon-left {
|
||||
@@ -52,7 +52,7 @@
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
// 添加光泽效果
|
||||
/* 添加光泽效果 */
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
@@ -199,11 +199,7 @@
|
||||
line-height: 1.3;
|
||||
letter-spacing: -0.5px;
|
||||
|
||||
// 添加文字渐变效果
|
||||
background: linear-gradient(135deg, #1a202c 0%, #2d3748 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
/* 文字渐变效果在小程序中不支持,使用纯色替代 */
|
||||
}
|
||||
|
||||
.coupon-validity {
|
||||
@@ -212,7 +208,7 @@
|
||||
line-height: 1.2;
|
||||
font-weight: 500;
|
||||
|
||||
// 添加图标前缀
|
||||
/* 添加图标前缀 */
|
||||
&::before {
|
||||
content: '⏰';
|
||||
margin-right: 6px;
|
||||
@@ -239,7 +235,7 @@
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
// 添加按钮光泽效果
|
||||
/* 添加按钮光泽效果 */
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
@@ -309,7 +305,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
backdrop-filter: blur(2px);
|
||||
/* backdrop-filter: blur(2px); 小程序不支持backdrop-filter属性 */
|
||||
|
||||
.status-badge {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
@@ -324,7 +320,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 动画效果
|
||||
/* 动画效果 */
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
transform: translateX(-100%) translateY(-100%) rotate(45deg);
|
||||
@@ -334,7 +330,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式优化
|
||||
/* 响应式优化 */
|
||||
@media (max-width: 768px) {
|
||||
.coupon-card {
|
||||
height: 150px;
|
||||
|
||||
@@ -2,7 +2,7 @@ import {NavBar} from '@nutui/nutui-react-taro'
|
||||
import {ArrowLeft} from '@nutui/icons-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
function Header(props) {
|
||||
function Header(props: any) {
|
||||
return (
|
||||
<>
|
||||
<NavBar
|
||||
|
||||
89
src/components/QRLoginButton.tsx
Normal file
89
src/components/QRLoginButton.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import React from 'react';
|
||||
import { Button } from '@nutui/nutui-react-taro';
|
||||
import {View} from '@tarojs/components'
|
||||
import { Scan } from '@nutui/icons-react-taro';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { useQRLogin } from '@/hooks/useQRLogin';
|
||||
|
||||
export interface QRLoginButtonProps {
|
||||
/** 按钮类型 */
|
||||
type?: 'primary' | 'success' | 'warning' | 'danger' | 'default';
|
||||
/** 按钮大小 */
|
||||
size?: 'large' | 'normal' | 'small';
|
||||
/** 按钮文本 */
|
||||
text?: string;
|
||||
/** 是否显示图标 */
|
||||
showIcon?: boolean;
|
||||
/** 自定义样式类名 */
|
||||
className?: string;
|
||||
/** 点击成功回调 */
|
||||
onSuccess?: (result: any) => void;
|
||||
/** 点击失败回调 */
|
||||
onError?: (error: string) => void;
|
||||
/** 是否使用页面模式(跳转到专门页面) */
|
||||
usePageMode?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫码登录按钮组件
|
||||
*/
|
||||
const QRLoginButton: React.FC<QRLoginButtonProps> = ({
|
||||
type = 'default',
|
||||
size = 'small',
|
||||
text = '扫码登录',
|
||||
showIcon = true,
|
||||
onSuccess,
|
||||
onError,
|
||||
usePageMode = false
|
||||
}) => {
|
||||
const { startScan, isLoading, canScan } = useQRLogin();
|
||||
|
||||
// 处理点击事件
|
||||
const handleClick = async () => {
|
||||
console.log('处理点击事件handleClick', usePageMode)
|
||||
if (usePageMode) {
|
||||
// 跳转到专门的扫码登录页面
|
||||
if (canScan()) {
|
||||
Taro.navigateTo({
|
||||
url: '/passport/qr-login/index'
|
||||
});
|
||||
} else {
|
||||
Taro.showToast({
|
||||
title: '请先登录小程序',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 直接执行扫码登录
|
||||
try {
|
||||
await startScan();
|
||||
// 成功回调会在Hook内部处理
|
||||
} catch (error: any) {
|
||||
onError?.(error.message || '扫码登录失败');
|
||||
}
|
||||
};
|
||||
|
||||
console.log(onSuccess,'onSuccess')
|
||||
const disabled = !canScan() || isLoading;
|
||||
|
||||
return (
|
||||
<Button
|
||||
type={type}
|
||||
size={size}
|
||||
loading={isLoading}
|
||||
disabled={disabled}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<View className="flex items-center justify-center">
|
||||
{showIcon && !isLoading && (
|
||||
<Scan className="mr-1" />
|
||||
)}
|
||||
{isLoading ? '扫码中...' : (disabled && !canScan() ? '请先登录' : text)}
|
||||
</View>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default QRLoginButton;
|
||||
182
src/components/QRLoginScanner.tsx
Normal file
182
src/components/QRLoginScanner.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
import React from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import { Button, Loading } from '@nutui/nutui-react-taro';
|
||||
import { Scan, Success, Failure } from '@nutui/icons-react-taro';
|
||||
import { useQRLogin, ScanLoginState } from '@/hooks/useQRLogin';
|
||||
|
||||
export interface QRLoginScannerProps {
|
||||
/** 扫码成功回调 */
|
||||
onSuccess?: (result: any) => void;
|
||||
/** 扫码失败回调 */
|
||||
onError?: (error: string) => void;
|
||||
/** 自定义样式类名 */
|
||||
className?: string;
|
||||
/** 按钮文本 */
|
||||
buttonText?: string;
|
||||
/** 是否显示状态信息 */
|
||||
showStatus?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫码登录组件
|
||||
*/
|
||||
const QRLoginScanner: React.FC<QRLoginScannerProps> = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
className = '',
|
||||
buttonText = '扫码登录',
|
||||
showStatus = true
|
||||
}) => {
|
||||
const {
|
||||
state,
|
||||
error,
|
||||
result,
|
||||
isLoading,
|
||||
startScan,
|
||||
cancel,
|
||||
reset,
|
||||
canScan
|
||||
} = useQRLogin();
|
||||
|
||||
// 处理扫码成功
|
||||
React.useEffect(() => {
|
||||
if (state === ScanLoginState.SUCCESS && result) {
|
||||
onSuccess?.(result);
|
||||
}
|
||||
}, [state, result, onSuccess]);
|
||||
|
||||
// 处理扫码失败
|
||||
React.useEffect(() => {
|
||||
if (state === ScanLoginState.ERROR && error) {
|
||||
onError?.(error);
|
||||
}
|
||||
}, [state, error, onError]);
|
||||
|
||||
// 获取状态显示内容
|
||||
const getStatusContent = () => {
|
||||
switch (state) {
|
||||
case ScanLoginState.SCANNING:
|
||||
return (
|
||||
<View className="flex items-center justify-center text-blue-500">
|
||||
<Loading className="mr-2" />
|
||||
<Text>请扫描登录二维码...</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
case ScanLoginState.CONFIRMING:
|
||||
return (
|
||||
<View className="flex items-center justify-center text-orange-500">
|
||||
<Loading className="mr-2" />
|
||||
<Text>正在确认登录...</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
case ScanLoginState.SUCCESS:
|
||||
return (
|
||||
<View className="flex items-center justify-center text-green-500">
|
||||
<Success className="mr-2" />
|
||||
<Text>登录确认成功!</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
case ScanLoginState.ERROR:
|
||||
return (
|
||||
<View className="flex items-center justify-center text-red-500">
|
||||
<Failure className="mr-2" />
|
||||
<Text>{error || '扫码登录失败'}</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取按钮状态
|
||||
const getButtonProps = () => {
|
||||
const disabled = !canScan() || isLoading;
|
||||
|
||||
switch (state) {
|
||||
case ScanLoginState.SCANNING:
|
||||
case ScanLoginState.CONFIRMING:
|
||||
return {
|
||||
loading: true,
|
||||
disabled: true,
|
||||
text: state === ScanLoginState.SCANNING ? '扫码中...' : '确认中...',
|
||||
onClick: cancel
|
||||
};
|
||||
|
||||
case ScanLoginState.SUCCESS:
|
||||
return {
|
||||
loading: false,
|
||||
disabled: false,
|
||||
text: '重新扫码',
|
||||
onClick: reset
|
||||
};
|
||||
|
||||
case ScanLoginState.ERROR:
|
||||
return {
|
||||
loading: false,
|
||||
disabled: false,
|
||||
text: '重试',
|
||||
onClick: startScan
|
||||
};
|
||||
|
||||
default:
|
||||
return {
|
||||
loading: false,
|
||||
disabled,
|
||||
text: disabled ? '请先登录' : buttonText,
|
||||
onClick: startScan
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const buttonProps = getButtonProps();
|
||||
|
||||
return (
|
||||
<View className={`qr-login-scanner ${className}`}>
|
||||
{/* 扫码按钮 */}
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
loading={buttonProps.loading}
|
||||
disabled={buttonProps.disabled}
|
||||
onClick={buttonProps.onClick}
|
||||
className="w-full"
|
||||
>
|
||||
{!buttonProps.loading && (
|
||||
<Scan className="mr-2" />
|
||||
)}
|
||||
{buttonProps.text}
|
||||
</Button>
|
||||
|
||||
{/* 状态显示 */}
|
||||
{showStatus && (
|
||||
<View className="mt-4 text-center">
|
||||
{getStatusContent()}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 成功结果显示 */}
|
||||
{state === ScanLoginState.SUCCESS && result && (
|
||||
<View className="mt-4 p-4 bg-green-50 rounded-lg">
|
||||
<Text className="text-sm text-green-700">
|
||||
已为用户 {result.userInfo?.nickname || result.userInfo?.userId} 确认登录
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 使用说明 */}
|
||||
{state === ScanLoginState.IDLE && (
|
||||
<View className="mt-4 text-center">
|
||||
<Text className="text-xs text-gray-500">
|
||||
扫描网页端显示的登录二维码即可快速登录
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default QRLoginScanner;
|
||||
272
src/components/QRScanModal.tsx
Normal file
272
src/components/QRScanModal.tsx
Normal file
@@ -0,0 +1,272 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import { Button, Popup, Loading } from '@nutui/nutui-react-taro';
|
||||
import { Scan, Close, Success, Failure } from '@nutui/icons-react-taro';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { parseQRContent, confirmQRLogin } from '@/api/passport/qr-login';
|
||||
import { useUser } from '@/hooks/useUser';
|
||||
|
||||
export interface QRScanModalProps {
|
||||
/** 是否显示弹窗 */
|
||||
visible: boolean;
|
||||
/** 关闭弹窗回调 */
|
||||
onClose: () => void;
|
||||
/** 扫码成功回调 */
|
||||
onSuccess?: (result: any) => void;
|
||||
/** 扫码失败回调 */
|
||||
onError?: (error: string) => void;
|
||||
/** 弹窗标题 */
|
||||
title?: string;
|
||||
/** 描述文本 */
|
||||
description?: string;
|
||||
/** 是否自动确认登录 */
|
||||
autoConfirm?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 二维码扫描弹窗组件(用于扫码登录)
|
||||
*/
|
||||
const QRScanModal: React.FC<QRScanModalProps> = ({
|
||||
visible,
|
||||
onClose,
|
||||
onSuccess,
|
||||
onError,
|
||||
title = '扫描登录二维码',
|
||||
description = '扫描网页端显示的登录二维码',
|
||||
autoConfirm = true
|
||||
}) => {
|
||||
const { user } = useUser();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [status, setStatus] = useState<'idle' | 'scanning' | 'confirming' | 'success' | 'error'>('idle');
|
||||
const [errorMsg, setErrorMsg] = useState('');
|
||||
|
||||
// 开始扫码
|
||||
const handleScan = async () => {
|
||||
if (!user?.userId) {
|
||||
onError?.('请先登录小程序');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setStatus('scanning');
|
||||
setErrorMsg('');
|
||||
|
||||
// 扫码
|
||||
const scanResult = await new Promise<string>((resolve, reject) => {
|
||||
Taro.scanCode({
|
||||
onlyFromCamera: true,
|
||||
scanType: ['qrCode'],
|
||||
success: (res) => {
|
||||
if (res.result) {
|
||||
resolve(res.result);
|
||||
} else {
|
||||
reject(new Error('扫码结果为空'));
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(new Error(err.errMsg || '扫码失败'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 解析二维码内容
|
||||
const token = parseQRContent(scanResult);
|
||||
if (!token) {
|
||||
throw new Error('无效的登录二维码');
|
||||
}
|
||||
|
||||
if (autoConfirm) {
|
||||
// 自动确认登录
|
||||
setStatus('confirming');
|
||||
const result = await confirmQRLogin({
|
||||
token,
|
||||
userId: user.userId,
|
||||
platform: 'wechat',
|
||||
wechatInfo: {
|
||||
nickname: user.nickname,
|
||||
avatar: user.avatar
|
||||
}
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
setStatus('success');
|
||||
onSuccess?.(result);
|
||||
|
||||
// 显示成功提示
|
||||
Taro.showToast({
|
||||
title: '登录确认成功',
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
// 延迟关闭
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
setStatus('idle');
|
||||
}, 1500);
|
||||
} else {
|
||||
throw new Error(result.message || '登录确认失败');
|
||||
}
|
||||
} else {
|
||||
// 只返回扫码结果
|
||||
onSuccess?.(scanResult);
|
||||
onClose();
|
||||
setStatus('idle');
|
||||
}
|
||||
} catch (error: any) {
|
||||
setStatus('error');
|
||||
const errorMessage = error.message || '操作失败';
|
||||
setErrorMsg(errorMessage);
|
||||
onError?.(errorMessage);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 重试
|
||||
const handleRetry = () => {
|
||||
setStatus('idle');
|
||||
setErrorMsg('');
|
||||
handleScan();
|
||||
};
|
||||
|
||||
// 关闭弹窗
|
||||
const handleClose = () => {
|
||||
setStatus('idle');
|
||||
setErrorMsg('');
|
||||
setLoading(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
// 获取状态显示内容
|
||||
const getStatusContent = () => {
|
||||
switch (status) {
|
||||
case 'scanning':
|
||||
return {
|
||||
icon: <Loading className="text-blue-500" />,
|
||||
title: '正在扫码...',
|
||||
description: '请将二维码对准摄像头'
|
||||
};
|
||||
|
||||
case 'confirming':
|
||||
return {
|
||||
icon: <Loading className="text-orange-500" />,
|
||||
title: '正在确认登录...',
|
||||
description: '请稍候,正在为您确认登录'
|
||||
};
|
||||
|
||||
case 'success':
|
||||
return {
|
||||
icon: <Success size="32" className="text-green-500" />,
|
||||
title: '登录确认成功',
|
||||
description: '网页端将自动完成登录'
|
||||
};
|
||||
|
||||
case 'error':
|
||||
return {
|
||||
icon: <Failure size="32" className="text-red-500" />,
|
||||
title: '操作失败',
|
||||
description: errorMsg || '请重试'
|
||||
};
|
||||
|
||||
default:
|
||||
return {
|
||||
icon: <Scan size="32" className="text-blue-500" />,
|
||||
title,
|
||||
description
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const statusContent = getStatusContent();
|
||||
|
||||
return (
|
||||
<Popup
|
||||
visible={visible}
|
||||
position="center"
|
||||
closeable={false}
|
||||
onClose={handleClose}
|
||||
style={{ width: '85%', borderRadius: '12px' }}
|
||||
>
|
||||
<View className="p-6 text-center relative">
|
||||
{/* 关闭按钮 */}
|
||||
{status !== 'scanning' && status !== 'confirming' && (
|
||||
<View className="absolute top-4 right-4">
|
||||
<Button
|
||||
size="small"
|
||||
type="default"
|
||||
onClick={handleClose}
|
||||
className="w-8 h-8 p-0"
|
||||
>
|
||||
<Close size="16" />
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 图标 */}
|
||||
<View className="w-16 h-16 mx-auto bg-gray-100 rounded-full flex items-center justify-center mb-4">
|
||||
{statusContent.icon}
|
||||
</View>
|
||||
|
||||
{/* 标题 */}
|
||||
<Text className="text-lg font-bold text-gray-800 mb-2 block">
|
||||
{statusContent.title}
|
||||
</Text>
|
||||
|
||||
{/* 描述 */}
|
||||
<Text className="text-gray-600 mb-6 block">
|
||||
{statusContent.description}
|
||||
</Text>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
{status === 'idle' && (
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
onClick={handleScan}
|
||||
className="w-full"
|
||||
disabled={!user?.userId}
|
||||
>
|
||||
<Scan className="mr-2" />
|
||||
{user?.userId ? '开始扫码' : '请先登录'}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{status === 'error' && (
|
||||
<View className="space-y-2">
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
onClick={handleRetry}
|
||||
className="w-full"
|
||||
>
|
||||
重试
|
||||
</Button>
|
||||
<Button
|
||||
type="default"
|
||||
size="normal"
|
||||
onClick={handleClose}
|
||||
className="w-full"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{(status === 'scanning' || status === 'confirming') && (
|
||||
<Button
|
||||
type="default"
|
||||
size="large"
|
||||
onClick={handleClose}
|
||||
className="w-full"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
)}
|
||||
</View>
|
||||
{loading}
|
||||
</Popup>
|
||||
);
|
||||
};
|
||||
|
||||
export default QRScanModal;
|
||||
126
src/components/UnifiedQRButton.tsx
Normal file
126
src/components/UnifiedQRButton.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import React from 'react';
|
||||
import { Button } from '@nutui/nutui-react-taro';
|
||||
import { View } from '@tarojs/components';
|
||||
import { Scan } from '@nutui/icons-react-taro';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { useUnifiedQRScan, ScanType, type UnifiedScanResult } from '@/hooks/useUnifiedQRScan';
|
||||
|
||||
export interface UnifiedQRButtonProps {
|
||||
/** 按钮类型 */
|
||||
type?: 'primary' | 'success' | 'warning' | 'danger' | 'default';
|
||||
/** 按钮大小 */
|
||||
size?: 'large' | 'normal' | 'small';
|
||||
/** 按钮文本 */
|
||||
text?: string;
|
||||
/** 是否显示图标 */
|
||||
showIcon?: boolean;
|
||||
/** 自定义样式类名 */
|
||||
className?: string;
|
||||
/** 扫码成功回调 */
|
||||
onSuccess?: (result: UnifiedScanResult) => void;
|
||||
/** 扫码失败回调 */
|
||||
onError?: (error: string) => void;
|
||||
/** 是否使用页面模式(跳转到专门页面) */
|
||||
usePageMode?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一扫码按钮组件
|
||||
* 支持登录和核销两种类型的二维码扫描
|
||||
*/
|
||||
const UnifiedQRButton: React.FC<UnifiedQRButtonProps> = ({
|
||||
type = 'default',
|
||||
size = 'small',
|
||||
text = '扫码',
|
||||
showIcon = true,
|
||||
onSuccess,
|
||||
onError,
|
||||
usePageMode = false
|
||||
}) => {
|
||||
const { startScan, isLoading, canScan, state, result } = useUnifiedQRScan();
|
||||
console.log(result,'useUnifiedQRScan>>result')
|
||||
// 处理点击事件
|
||||
const handleClick = async () => {
|
||||
if (usePageMode) {
|
||||
// 跳转到专门的统一扫码页面
|
||||
if (canScan()) {
|
||||
Taro.navigateTo({
|
||||
url: '/passport/unified-qr/index'
|
||||
});
|
||||
} else {
|
||||
Taro.showToast({
|
||||
title: '请先登录小程序',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 直接执行扫码
|
||||
try {
|
||||
const scanResult = await startScan();
|
||||
if (scanResult) {
|
||||
onSuccess?.(scanResult);
|
||||
|
||||
// 根据扫码类型给出不同的后续提示
|
||||
if (scanResult.type === ScanType.VERIFICATION) {
|
||||
// 核销成功后可以继续扫码
|
||||
setTimeout(() => {
|
||||
Taro.showModal({
|
||||
title: '核销成功',
|
||||
content: '是否继续扫码核销其他礼品卡?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
handleClick(); // 递归调用继续扫码
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
onError?.(error.message || '扫码失败');
|
||||
}
|
||||
};
|
||||
|
||||
const disabled = !canScan() || isLoading;
|
||||
|
||||
// 根据当前状态动态显示文本
|
||||
const getButtonText = () => {
|
||||
if (isLoading) {
|
||||
switch (state) {
|
||||
case 'scanning':
|
||||
return '扫码中...';
|
||||
case 'processing':
|
||||
return '处理中...';
|
||||
default:
|
||||
return '扫码中...';
|
||||
}
|
||||
}
|
||||
|
||||
if (disabled && !canScan()) {
|
||||
return '请先登录';
|
||||
}
|
||||
|
||||
return text;
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
type={type}
|
||||
size={size}
|
||||
loading={isLoading}
|
||||
disabled={disabled}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<View className="flex items-center justify-center">
|
||||
{showIcon && !isLoading && (
|
||||
<Scan className="mr-1" />
|
||||
)}
|
||||
{getButtonText()}
|
||||
</View>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default UnifiedQRButton;
|
||||
@@ -1,4 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '医生入驻申请通道',
|
||||
navigationBarTitleText: '邀请注册',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
|
||||
@@ -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 () => {
|
||||
// 判断用户是否登录
|
||||
if (!user?.userId) {
|
||||
return false;
|
||||
}
|
||||
// 查询当前用户ID是否已有申请记录
|
||||
try {
|
||||
const res = await pageShopDealerApply({userId: user?.userId});
|
||||
if (res && res.count > 0) {
|
||||
setIsEditMode(true);
|
||||
setExistingApply(res.list[0]);
|
||||
// 如果有记录,填充表单数据
|
||||
setFormData(res.list[0]);
|
||||
setLoading(false)
|
||||
const inviteParams = getStoredInviteParams()
|
||||
if (inviteParams?.inviter) {
|
||||
setFormData({
|
||||
...user,
|
||||
refereeId: Number(inviteParams.inviter),
|
||||
// 清空昵称,强制用户手动输入
|
||||
nickname: '',
|
||||
})
|
||||
} else {
|
||||
setIsEditMode(false);
|
||||
setExistingApply(null);
|
||||
setLoading(false)
|
||||
// 如果没有邀请参数,也要确保昵称为空
|
||||
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) {
|
||||
setLoading(true)
|
||||
console.error('查询申请记录失败:', error);
|
||||
setIsEditMode(false);
|
||||
setExistingApply(null);
|
||||
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...')
|
||||
|
||||
// 准备提交的数据
|
||||
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}
|
||||
text={'立即注册'}
|
||||
onClick={handleFixedButtonClick}
|
||||
/>
|
||||
)}
|
||||
|
||||
</>
|
||||
);
|
||||
|
||||
4
src/dealer/bank/add.config.ts
Normal file
4
src/dealer/bank/add.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '添加银行卡',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
142
src/dealer/bank/add.tsx
Normal file
142
src/dealer/bank/add.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import {useEffect, useState, useRef} from "react";
|
||||
import {useRouter} from '@tarojs/taro'
|
||||
import {Loading, CellGroup, Input, Form} from '@nutui/nutui-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {
|
||||
getShopDealerBank,
|
||||
listShopDealerBank,
|
||||
updateShopDealerBank,
|
||||
addShopDealerBank
|
||||
} from "@/api/shop/shopDealerBank";
|
||||
import FixedButton from "@/components/FixedButton";
|
||||
import {ShopDealerBank} from "@/api/shop/shopDealerBank/model";
|
||||
|
||||
const AddUserAddress = () => {
|
||||
const {params} = useRouter();
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [FormData, setFormData] = useState<ShopDealerBank>()
|
||||
const formRef = useRef<any>(null)
|
||||
|
||||
// 判断是编辑还是新增模式
|
||||
const isEditMode = !!params.id
|
||||
const bankId = params.id ? Number(params.id) : undefined
|
||||
|
||||
const reload = async () => {
|
||||
// 如果是编辑模式,加载地址数据
|
||||
if (isEditMode && bankId) {
|
||||
try {
|
||||
const bank = await getShopDealerBank(bankId)
|
||||
setFormData(bank)
|
||||
} catch (error) {
|
||||
console.error('加载地址失败:', error)
|
||||
Taro.showToast({
|
||||
title: '加载地址失败',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const submitSucceed = async (values: any) => {
|
||||
console.log('.>>>>>>,....')
|
||||
try {
|
||||
// 准备提交的数据
|
||||
const submitData = {
|
||||
...values,
|
||||
isDefault: true // 新增或编辑的地址都设为默认地址
|
||||
};
|
||||
|
||||
console.log('提交数据:', submitData)
|
||||
|
||||
// 如果是编辑模式,添加id
|
||||
if (isEditMode && bankId) {
|
||||
submitData.id = bankId;
|
||||
}
|
||||
|
||||
// 先处理默认地址逻辑
|
||||
const defaultAddress = await listShopDealerBank({isDefault: true});
|
||||
if (defaultAddress && defaultAddress.length > 0) {
|
||||
// 如果当前编辑的不是默认地址,或者是新增地址,需要取消其他默认地址
|
||||
if (!isEditMode || (isEditMode && defaultAddress[0].id !== bankId)) {
|
||||
await updateShopDealerBank({
|
||||
...defaultAddress[0],
|
||||
isDefault: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 执行新增或更新操作
|
||||
if (isEditMode) {
|
||||
await updateShopDealerBank(submitData);
|
||||
} else {
|
||||
await addShopDealerBank(submitData);
|
||||
}
|
||||
|
||||
Taro.showToast({
|
||||
title: `${isEditMode ? '更新' : '保存'}成功`,
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
Taro.navigateBack();
|
||||
}, 1000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error);
|
||||
Taro.showToast({
|
||||
title: `${isEditMode ? '更新' : '保存'}失败`,
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const submitFailed = (error: any) => {
|
||||
console.log(error, 'err...')
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// 动态设置页面标题
|
||||
Taro.setNavigationBarTitle({
|
||||
title: isEditMode ? '编辑银行卡' : '添加银行卡'
|
||||
});
|
||||
|
||||
reload().then(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}, [isEditMode]);
|
||||
|
||||
if (loading) {
|
||||
return <Loading className={'px-2'}>加载中</Loading>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form
|
||||
ref={formRef}
|
||||
divider
|
||||
initialValues={FormData}
|
||||
labelPosition="left"
|
||||
onFinish={(values) => submitSucceed(values)}
|
||||
onFinishFailed={(errors) => submitFailed(errors)}
|
||||
>
|
||||
<CellGroup style={{padding: '4px 0'}}>
|
||||
<Form.Item name="bankName" label="开户行名称" initialValue={FormData?.bankName} required>
|
||||
<Input placeholder="开户行名称" maxLength={10}/>
|
||||
</Form.Item>
|
||||
<Form.Item name="bankAccount" label="银行开户名" initialValue={FormData?.bankAccount} required>
|
||||
<Input placeholder="银行开户名" maxLength={10}/>
|
||||
</Form.Item>
|
||||
<Form.Item name="bankCard" label="银行卡号" initialValue={FormData?.bankCard} required>
|
||||
<Input placeholder="银行卡号" maxLength={11}/>
|
||||
</Form.Item>
|
||||
</CellGroup>
|
||||
</Form>
|
||||
|
||||
{/* 底部浮动按钮 */}
|
||||
<FixedButton text={isEditMode ? '更新地址' : '保存并使用'} onClick={() => formRef.current?.submit()}/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddUserAddress;
|
||||
4
src/dealer/bank/index.config.ts
Normal file
4
src/dealer/bank/index.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '银行卡管理',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
134
src/dealer/bank/index.tsx
Normal file
134
src/dealer/bank/index.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import {useState} from "react";
|
||||
import Taro, {useDidShow} from '@tarojs/taro'
|
||||
import {Button, Cell, Space, Empty, ConfigProvider} from '@nutui/nutui-react-taro'
|
||||
import {CheckNormal, Checked} from '@nutui/icons-react-taro'
|
||||
import {View} from '@tarojs/components'
|
||||
import {ShopDealerBank} from "@/api/shop/shopDealerBank/model";
|
||||
import {listShopDealerBank, removeShopDealerBank, updateShopDealerBank} from "@/api/shop/shopDealerBank";
|
||||
import FixedButton from "@/components/FixedButton";
|
||||
|
||||
const DealerBank = () => {
|
||||
const [list, setList] = useState<ShopDealerBank[]>([])
|
||||
const [bank, setAddress] = useState<ShopDealerBank>()
|
||||
|
||||
const reload = () => {
|
||||
listShopDealerBank({})
|
||||
.then(data => {
|
||||
setList(data || [])
|
||||
// 默认地址
|
||||
setAddress(data.find(item => item.isDefault))
|
||||
})
|
||||
.catch(() => {
|
||||
Taro.showToast({
|
||||
title: '获取地址失败',
|
||||
icon: 'error'
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
const onDefault = async (item: ShopDealerBank) => {
|
||||
if (bank) {
|
||||
await updateShopDealerBank({
|
||||
...bank,
|
||||
isDefault: false
|
||||
})
|
||||
}
|
||||
await updateShopDealerBank({
|
||||
id: item.id,
|
||||
isDefault: true
|
||||
})
|
||||
Taro.showToast({
|
||||
title: '设置成功',
|
||||
icon: 'success'
|
||||
});
|
||||
reload();
|
||||
}
|
||||
|
||||
const onDel = async (id?: number) => {
|
||||
await removeShopDealerBank(id)
|
||||
Taro.showToast({
|
||||
title: '删除成功',
|
||||
icon: 'success'
|
||||
});
|
||||
reload();
|
||||
}
|
||||
|
||||
const selectAddress = async (item: ShopDealerBank) => {
|
||||
if (bank) {
|
||||
await updateShopDealerBank({
|
||||
...bank,
|
||||
isDefault: false
|
||||
})
|
||||
}
|
||||
await updateShopDealerBank({
|
||||
id: item.id,
|
||||
isDefault: true
|
||||
})
|
||||
setTimeout(() => {
|
||||
Taro.navigateBack()
|
||||
}, 500)
|
||||
}
|
||||
|
||||
useDidShow(() => {
|
||||
reload()
|
||||
});
|
||||
|
||||
if (list.length == 0) {
|
||||
return (
|
||||
<ConfigProvider>
|
||||
<div className={'h-full flex flex-col justify-center items-center'} style={{
|
||||
height: 'calc(100vh - 300px)',
|
||||
}}>
|
||||
<Empty
|
||||
style={{
|
||||
backgroundColor: 'transparent'
|
||||
}}
|
||||
description="您还没有地址哦"
|
||||
/>
|
||||
<Space>
|
||||
<Button onClick={() => Taro.navigateTo({url: '/dealer/bank/add'})}>新增地址</Button>
|
||||
<Button type="success" fill="dashed"
|
||||
onClick={() => Taro.navigateTo({url: '/dealer/bank/wxAddress'})}>获取微信地址</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</ConfigProvider>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className={'p-3'}>
|
||||
{list.map((item, _) => (
|
||||
<Cell.Group>
|
||||
<Cell className={'flex flex-col gap-1'} extra={item.bankAccount} onClick={() => selectAddress(item)}>
|
||||
<View>
|
||||
<View className={'font-medium text-sm'}>{item.bankName}</View>
|
||||
</View>
|
||||
<View className={'text-xs'}>
|
||||
{item.bankCard} {item.bankAccount}
|
||||
</View>
|
||||
</Cell>
|
||||
<Cell
|
||||
align="center"
|
||||
title={
|
||||
<View className={'flex items-center gap-1'} onClick={() => onDefault(item)}>
|
||||
{item.isDefault ? <Checked className={'text-green-600'} size={16}/> : <CheckNormal size={16}/>}
|
||||
<View className={'text-gray-400'}>选择</View>
|
||||
</View>
|
||||
}
|
||||
extra={
|
||||
<>
|
||||
<View className={'text-gray-400'} onClick={() => onDel(item.id)}>
|
||||
删除
|
||||
</View>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Cell.Group>
|
||||
))}
|
||||
{/* 底部浮动按钮 */}
|
||||
<FixedButton text={'新增银行卡'} onClick={() => Taro.navigateTo({url: '/dealer/bank/add'})} />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default DealerBank;
|
||||
108
src/dealer/customer/README.md
Normal file
108
src/dealer/customer/README.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# 客户管理页面
|
||||
|
||||
## 功能概述
|
||||
|
||||
这是一个完整的客户管理页面,支持客户数据的展示、筛选和搜索功能。
|
||||
|
||||
## 主要功能
|
||||
|
||||
### 1. 数据源
|
||||
- 使用 `pageUsers` API 从 User 表读取客户数据
|
||||
- 支持按状态筛选用户(status: 0 表示正常状态)
|
||||
|
||||
### 2. 状态管理
|
||||
客户状态包括:
|
||||
- **全部** - 显示所有客户
|
||||
- **跟进中** - 正在跟进的潜在客户
|
||||
- **已签约** - 已经签约的客户
|
||||
- **已取消** - 已取消合作的客户
|
||||
|
||||
### 3. 顶部Tabs筛选
|
||||
- 支持按客户状态筛选
|
||||
- 显示每个状态的客户数量统计
|
||||
- 实时更新统计数据
|
||||
|
||||
### 4. 搜索功能
|
||||
支持多字段搜索:
|
||||
- 客户姓名(realName)
|
||||
- 昵称(nickname)
|
||||
- 用户名(username)
|
||||
- 手机号(phone)
|
||||
- 用户ID(userId)
|
||||
|
||||
### 5. 客户信息展示
|
||||
每个客户卡片显示:
|
||||
- 客户姓名和状态标签
|
||||
- 手机号码
|
||||
- 注册时间
|
||||
- 用户ID、余额、积分等统计信息
|
||||
|
||||
## 技术实现
|
||||
|
||||
### 组件结构
|
||||
```
|
||||
CustomerManagement
|
||||
├── 搜索栏 (SearchBar)
|
||||
├── 状态筛选Tabs
|
||||
└── 客户列表
|
||||
└── 客户卡片项
|
||||
```
|
||||
|
||||
### 主要状态
|
||||
- `list`: 客户数据列表
|
||||
- `loading`: 加载状态
|
||||
- `activeTab`: 当前选中的状态Tab
|
||||
- `searchValue`: 搜索关键词
|
||||
|
||||
### 工具函数
|
||||
使用 `@/utils/customerStatus` 工具函数管理客户状态:
|
||||
- `getStatusText()`: 获取状态文本
|
||||
- `getStatusTagType()`: 获取状态标签类型
|
||||
- `getStatusOptions()`: 获取状态选项列表
|
||||
|
||||
## 使用的组件
|
||||
|
||||
### NutUI 组件
|
||||
- `Tabs` / `TabPane`: 状态筛选标签页
|
||||
- `SearchBar`: 搜索输入框
|
||||
- `Tag`: 状态标签
|
||||
- `Loading`: 加载指示器
|
||||
- `Space`: 间距布局
|
||||
|
||||
### 图标
|
||||
- `Phone`: 手机号图标
|
||||
- `User`: 用户图标
|
||||
|
||||
## 数据流
|
||||
|
||||
1. 页面初始化时调用 `fetchCustomerData()` 获取用户数据
|
||||
2. 为每个用户添加客户状态(目前使用随机状态,实际项目中应从数据库获取)
|
||||
3. 根据当前Tab和搜索条件筛选数据
|
||||
4. 渲染客户列表
|
||||
|
||||
## 注意事项
|
||||
|
||||
### 临时实现
|
||||
- 当前使用 `getRandomStatus()` 生成随机客户状态
|
||||
- 实际项目中应该:
|
||||
1. 在数据库中添加客户状态字段
|
||||
2. 修改后端API返回真实的客户状态
|
||||
3. 删除随机状态生成函数
|
||||
|
||||
### 扩展建议
|
||||
1. 添加客户详情页面
|
||||
2. 支持客户状态的修改操作
|
||||
3. 添加客户添加/编辑功能
|
||||
4. 支持批量操作
|
||||
5. 添加导出功能
|
||||
6. 支持更多筛选条件(注册时间、地区等)
|
||||
|
||||
## 文件结构
|
||||
```
|
||||
src/dealer/customer/
|
||||
├── index.tsx # 主页面组件
|
||||
└── README.md # 说明文档
|
||||
|
||||
src/utils/
|
||||
└── customerStatus.ts # 客户状态工具函数
|
||||
```
|
||||
4
src/dealer/customer/add.config.ts
Normal file
4
src/dealer/customer/add.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '客户报备',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
400
src/dealer/customer/add.tsx
Normal file
400
src/dealer/customer/add.tsx
Normal file
@@ -0,0 +1,400 @@
|
||||
import {useEffect, useState, useRef} from "react";
|
||||
import {Loading, CellGroup, Cell, Input, Form, Calendar} from '@nutui/nutui-react-taro'
|
||||
import {Edit, Calendar as CalendarIcon} from '@nutui/icons-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {useRouter} from '@tarojs/taro'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import FixedButton from "@/components/FixedButton";
|
||||
import {useUser} from "@/hooks/useUser";
|
||||
import {ShopDealerApply} from "@/api/shop/shopDealerApply/model";
|
||||
import {
|
||||
addShopDealerApply, getShopDealerApply, pageShopDealerApply,
|
||||
updateShopDealerApply
|
||||
} from "@/api/shop/shopDealerApply";
|
||||
import {
|
||||
formatDateForDatabase,
|
||||
extractDateForCalendar, formatDateForDisplay
|
||||
} from "@/utils/dateUtils";
|
||||
|
||||
const AddShopDealerApply = () => {
|
||||
const {user} = useUser()
|
||||
const {params} = useRouter();
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [FormData, setFormData] = useState<ShopDealerApply>()
|
||||
const formRef = useRef<any>(null)
|
||||
const [isEditMode, setIsEditMode] = useState<boolean>(false)
|
||||
const [existingApply, setExistingApply] = useState<ShopDealerApply | null>(null)
|
||||
|
||||
// 日期选择器状态
|
||||
const [showApplyTimePicker, setShowApplyTimePicker] = useState<boolean>(false)
|
||||
const [showContractTimePicker, setShowContractTimePicker] = useState<boolean>(false)
|
||||
const [applyTime, setApplyTime] = useState<string>('')
|
||||
const [contractTime, setContractTime] = useState<string>('')
|
||||
|
||||
// 获取审核状态文字
|
||||
const getApplyStatusText = (status?: number) => {
|
||||
switch (status) {
|
||||
case 10:
|
||||
return '待审核'
|
||||
case 20:
|
||||
return '已签约'
|
||||
case 30:
|
||||
return '已取消'
|
||||
default:
|
||||
return '未知状态'
|
||||
}
|
||||
}
|
||||
|
||||
console.log(getApplyStatusText)
|
||||
|
||||
// 处理签约时间选择
|
||||
const handleApplyTimeConfirm = (param: string) => {
|
||||
const selectedDate = param[3] // 选中的日期字符串 (YYYY-M-D)
|
||||
const formattedDate = formatDateForDatabase(selectedDate) // 转换为数据库格式
|
||||
setApplyTime(selectedDate) // 保存原始格式用于显示
|
||||
setShowApplyTimePicker(false)
|
||||
|
||||
// 更新表单数据(使用数据库格式)
|
||||
if (formRef.current) {
|
||||
formRef.current.setFieldsValue({
|
||||
applyTime: formattedDate
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 处理合同日期选择
|
||||
const handleContractTimeConfirm = (param: string) => {
|
||||
const selectedDate = param[3] // 选中的日期字符串 (YYYY-M-D)
|
||||
const formattedDate = formatDateForDatabase(selectedDate) // 转换为数据库格式
|
||||
setContractTime(selectedDate) // 保存原始格式用于显示
|
||||
setShowContractTimePicker(false)
|
||||
|
||||
// 更新表单数据(使用数据库格式)
|
||||
if (formRef.current) {
|
||||
formRef.current.setFieldsValue({
|
||||
contractTime: formattedDate
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const reload = async () => {
|
||||
if (!params.id) {
|
||||
return false;
|
||||
}
|
||||
// 查询当前用户ID是否已有申请记录
|
||||
try {
|
||||
const dealerApply = await getShopDealerApply(Number(params.id));
|
||||
if (dealerApply) {
|
||||
setFormData(dealerApply)
|
||||
setIsEditMode(true);
|
||||
setExistingApply(dealerApply)
|
||||
|
||||
// 初始化日期数据(从数据库格式转换为Calendar组件格式)
|
||||
if (dealerApply.applyTime) {
|
||||
setApplyTime(extractDateForCalendar(dealerApply.applyTime))
|
||||
}
|
||||
if (dealerApply.contractTime) {
|
||||
setContractTime(extractDateForCalendar(dealerApply.contractTime))
|
||||
}
|
||||
|
||||
Taro.setNavigationBarTitle({title: '签约'})
|
||||
}
|
||||
} catch (error) {
|
||||
setLoading(true)
|
||||
console.error('查询申请记录失败:', error);
|
||||
setIsEditMode(false);
|
||||
setExistingApply(null);
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
// 计算保护期过期时间(7天后)
|
||||
const calculateExpirationTime = (): string => {
|
||||
const now = new Date();
|
||||
const expirationDate = new Date(now);
|
||||
expirationDate.setDate(now.getDate() + 7); // 7天后
|
||||
|
||||
// 格式化为数据库需要的格式:YYYY-MM-DD HH:mm:ss
|
||||
const year = expirationDate.getFullYear();
|
||||
const month = String(expirationDate.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(expirationDate.getDate()).padStart(2, '0');
|
||||
const hours = String(expirationDate.getHours()).padStart(2, '0');
|
||||
const minutes = String(expirationDate.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(expirationDate.getSeconds()).padStart(2, '0');
|
||||
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
};
|
||||
|
||||
const submitSucceed = async (values: any) => {
|
||||
try {
|
||||
// 验证必填字段
|
||||
if (!values.mobile || values.mobile.trim() === '') {
|
||||
Taro.showToast({
|
||||
title: '请填写联系方式',
|
||||
icon: 'error'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证手机号格式
|
||||
const phoneRegex = /^1[3-9]\d{9}$/;
|
||||
if (!phoneRegex.test(values.mobile)) {
|
||||
Taro.showToast({
|
||||
title: '请填写正确的手机号',
|
||||
icon: 'error'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查客户是否已存在
|
||||
const res = await pageShopDealerApply({dealerName: values.dealerName, type: 4, applyStatus: 10});
|
||||
|
||||
if (res && res.count > 0) {
|
||||
const existingCustomer = res.list[0];
|
||||
|
||||
// 检查是否在7天保护期内
|
||||
if (!isEditMode && existingCustomer.applyTime) {
|
||||
// 将申请时间字符串转换为时间戳进行比较
|
||||
const applyTimeStamp = new Date(existingCustomer.applyTime).getTime();
|
||||
const currentTimeStamp = new Date().getTime();
|
||||
const sevenDaysInMs = 7 * 24 * 60 * 60 * 1000; // 7天的毫秒数
|
||||
|
||||
// 如果在7天保护期内,不允许重复添加
|
||||
if (currentTimeStamp - applyTimeStamp < sevenDaysInMs) {
|
||||
const remainingDays = Math.ceil((sevenDaysInMs - (currentTimeStamp - applyTimeStamp)) / (24 * 60 * 60 * 1000));
|
||||
|
||||
Taro.showToast({
|
||||
title: `该客户还在保护期,还需等待${remainingDays}天后才能重新添加`,
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
});
|
||||
return false;
|
||||
} else {
|
||||
// 超过7天保护期,可以重新添加,显示确认对话框
|
||||
const modalResult = await new Promise<boolean>((resolve) => {
|
||||
Taro.showModal({
|
||||
title: '提示',
|
||||
content: '该客户已超过7天保护期,是否重新添加跟进?',
|
||||
showCancel: true,
|
||||
cancelText: '取消',
|
||||
confirmText: '确定',
|
||||
success: (modalRes) => {
|
||||
resolve(modalRes.confirm);
|
||||
},
|
||||
fail: () => {
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (!modalResult) {
|
||||
return false; // 用户取消,不继续执行
|
||||
}
|
||||
// 用户确认后继续执行添加逻辑
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 计算过期时间
|
||||
const expirationTime = isEditMode ? existingApply?.expirationTime : calculateExpirationTime();
|
||||
|
||||
// 准备提交的数据
|
||||
const submitData = {
|
||||
...values,
|
||||
type: 4,
|
||||
realName: values.realName || user?.nickname,
|
||||
mobile: values.mobile,
|
||||
refereeId: 33534,
|
||||
applyStatus: isEditMode ? 20 : 10,
|
||||
auditTime: undefined,
|
||||
// 设置保护期过期时间(7天后)
|
||||
expirationTime: expirationTime,
|
||||
// 确保日期数据正确提交(使用数据库格式)
|
||||
applyTime: values.applyTime || (applyTime ? formatDateForDatabase(applyTime) : ''),
|
||||
contractTime: values.contractTime || (contractTime ? formatDateForDatabase(contractTime) : '')
|
||||
};
|
||||
|
||||
// 调试信息
|
||||
console.log('=== 提交数据调试 ===');
|
||||
console.log('是否编辑模式:', isEditMode);
|
||||
console.log('计算的过期时间:', expirationTime);
|
||||
console.log('提交的数据:', submitData);
|
||||
console.log('==================');
|
||||
|
||||
// 如果是编辑模式,添加现有申请的id
|
||||
if (isEditMode && existingApply?.applyId) {
|
||||
submitData.applyId = existingApply.applyId;
|
||||
}
|
||||
|
||||
// 执行新增或更新操作
|
||||
if (isEditMode) {
|
||||
await updateShopDealerApply(submitData);
|
||||
} else {
|
||||
await addShopDealerApply(submitData);
|
||||
}
|
||||
|
||||
Taro.showToast({
|
||||
title: `${isEditMode ? '更新' : '提交'}成功`,
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
Taro.navigateBack();
|
||||
}, 1000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error);
|
||||
Taro.showToast({
|
||||
title: '提交失败,请重试',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 处理固定按钮点击事件
|
||||
const handleFixedButtonClick = () => {
|
||||
// 触发表单提交
|
||||
formRef.current?.submit();
|
||||
};
|
||||
|
||||
const submitFailed = (error: any) => {
|
||||
console.log(error, 'err...')
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reload().then(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}, []); // 依赖用户ID,当用户变化时重新加载
|
||||
|
||||
if (loading) {
|
||||
return <Loading className={'px-2'}>加载中</Loading>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form
|
||||
ref={formRef}
|
||||
divider
|
||||
initialValues={FormData}
|
||||
labelPosition="left"
|
||||
onFinish={(values) => submitSucceed(values)}
|
||||
onFinishFailed={(errors) => submitFailed(errors)}
|
||||
>
|
||||
<View className={'bg-gray-100 h-3'}></View>
|
||||
<CellGroup style={{padding: '4px 0'}}>
|
||||
<Form.Item name="dealerName" label="公司名称" initialValue={FormData?.dealerName} required>
|
||||
<Input placeholder="公司名称" maxLength={10} disabled={isEditMode}/>
|
||||
</Form.Item>
|
||||
<Form.Item name="realName" label="联系人" initialValue={FormData?.realName} required>
|
||||
<Input placeholder="请输入联系人" disabled={isEditMode}/>
|
||||
</Form.Item>
|
||||
<Form.Item name="mobile" label="联系方式" initialValue={FormData?.mobile} required>
|
||||
<Input placeholder="请输入手机号" disabled={isEditMode} maxLength={11}/>
|
||||
</Form.Item>
|
||||
<Form.Item name="address" label="公司地址" initialValue={FormData?.address} required>
|
||||
<Input placeholder="请输入详细地址" disabled={isEditMode}/>
|
||||
</Form.Item>
|
||||
<Form.Item name="dealerCode" label="户号" initialValue={FormData?.dealerCode} required>
|
||||
<Input placeholder="请填写户号" disabled={isEditMode}/>
|
||||
</Form.Item>
|
||||
<Form.Item name="comments" label="跟进情况" initialValue={FormData?.comments}>
|
||||
<Input placeholder="请填写跟进情况" disabled={isEditMode}/>
|
||||
</Form.Item>
|
||||
{isEditMode && (
|
||||
<>
|
||||
<Form.Item name="money" label="签约价格" initialValue={FormData?.money} required>
|
||||
<Input placeholder="(元/兆瓦时)" disabled={false}/>
|
||||
</Form.Item>
|
||||
<Form.Item name="applyTime" label="签约时间" initialValue={FormData?.applyTime}>
|
||||
<View
|
||||
className="flex items-center justify-between py-2"
|
||||
onClick={() => setShowApplyTimePicker(true)}
|
||||
>
|
||||
<View className="flex items-center">
|
||||
<CalendarIcon size={16} color="#999" className="mr-2"/>
|
||||
<Text style={{color: applyTime ? '#333' : '#999'}}>
|
||||
{applyTime ? formatDateForDisplay(applyTime) : '请选择签约时间'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Form.Item>
|
||||
<Form.Item name="contractTime" label="合同日期" initialValue={FormData?.contractTime}>
|
||||
<View
|
||||
className="flex items-center justify-between py-2"
|
||||
onClick={() => setShowContractTimePicker(true)}
|
||||
>
|
||||
<View className="flex items-center">
|
||||
<CalendarIcon size={16} color="#999" className="mr-2"/>
|
||||
<Text style={{color: contractTime ? '#333' : '#999'}}>
|
||||
{contractTime ? formatDateForDisplay(contractTime) : '请选择合同生效起止时间'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Form.Item>
|
||||
{/*<Form.Item name="refereeId" label="邀请人ID" initialValue={FormData?.refereeId} required>*/}
|
||||
{/* <Input placeholder="邀请人ID"/>*/}
|
||||
{/*</Form.Item>*/}
|
||||
</>
|
||||
)}
|
||||
<Form.Item name="userId" label="报备人" initialValue={FormData?.userId} required>
|
||||
选择
|
||||
</Form.Item>
|
||||
</CellGroup>
|
||||
</Form>
|
||||
|
||||
{/* 签约时间选择器 */}
|
||||
<Calendar
|
||||
visible={showApplyTimePicker}
|
||||
defaultValue={applyTime}
|
||||
onClose={() => setShowApplyTimePicker(false)}
|
||||
onConfirm={handleApplyTimeConfirm}
|
||||
/>
|
||||
|
||||
{/* 合同日期选择器 */}
|
||||
<Calendar
|
||||
visible={showContractTimePicker}
|
||||
defaultValue={contractTime}
|
||||
onClose={() => setShowContractTimePicker(false)}
|
||||
onConfirm={handleContractTimeConfirm}
|
||||
/>
|
||||
|
||||
{/* 审核状态显示(仅在编辑模式下显示) */}
|
||||
{isEditMode && (
|
||||
<CellGroup>
|
||||
{/*<Cell*/}
|
||||
{/* title={'审核状态'}*/}
|
||||
{/* extra={*/}
|
||||
{/* <span style={{*/}
|
||||
{/* color: FormData?.applyStatus === 20 ? '#52c41a' :*/}
|
||||
{/* FormData?.applyStatus === 30 ? '#ff4d4f' : '#faad14'*/}
|
||||
{/* }}>*/}
|
||||
{/* {getApplyStatusText(FormData?.applyStatus)}*/}
|
||||
{/* </span>*/}
|
||||
{/* }*/}
|
||||
{/*/>*/}
|
||||
{FormData?.applyStatus === 20 && (
|
||||
<Cell title={'签约时间'} extra={FormData?.auditTime || '无'}/>
|
||||
)}
|
||||
{FormData?.applyStatus === 30 && (
|
||||
<Cell title={'驳回原因'} extra={FormData?.rejectReason || '无'}/>
|
||||
)}
|
||||
</CellGroup>
|
||||
)}
|
||||
|
||||
|
||||
{/* 底部浮动按钮 */}
|
||||
{(!isEditMode || FormData?.applyStatus === 10) && (
|
||||
<FixedButton
|
||||
icon={<Edit/>}
|
||||
text={'立即提交'}
|
||||
onClick={handleFixedButtonClick}
|
||||
/>
|
||||
)}
|
||||
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddShopDealerApply;
|
||||
3
src/dealer/customer/index.config.ts
Normal file
3
src/dealer/customer/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '客户列表'
|
||||
})
|
||||
548
src/dealer/customer/index.tsx
Normal file
548
src/dealer/customer/index.tsx
Normal file
@@ -0,0 +1,548 @@
|
||||
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 {Phone, AngleDoubleLeft} from '@nutui/icons-react-taro'
|
||||
import type {ShopDealerApply, ShopDealerApply as UserType} from "@/api/shop/shopDealerApply/model";
|
||||
import {
|
||||
CustomerStatus,
|
||||
getStatusText,
|
||||
getStatusTagType,
|
||||
getStatusOptions,
|
||||
mapApplyStatusToCustomerStatus,
|
||||
mapCustomerStatusToApplyStatus
|
||||
} from '@/utils/customerStatus';
|
||||
import FixedButton from "@/components/FixedButton";
|
||||
import navTo from "@/utils/common";
|
||||
import {pageShopDealerApply, removeShopDealerApply, updateShopDealerApply} from "@/api/shop/shopDealerApply";
|
||||
|
||||
// 扩展User类型,添加客户状态和保护天数
|
||||
interface CustomerUser extends UserType {
|
||||
customerStatus?: CustomerStatus;
|
||||
protectDays?: number; // 剩余保护天数
|
||||
}
|
||||
|
||||
const CustomerIndex = () => {
|
||||
const [list, setList] = useState<CustomerUser[]>([])
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [activeTab, setActiveTab] = useState<CustomerStatus>('all')
|
||||
const [searchValue, _] = useState<string>('')
|
||||
const [page, setPage] = useState(1)
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
|
||||
// Tab配置
|
||||
const tabList = getStatusOptions();
|
||||
|
||||
// 复制手机号
|
||||
const copyPhone = (phone: string) => {
|
||||
Taro.setClipboardData({
|
||||
data: phone,
|
||||
success: () => {
|
||||
Taro.showToast({
|
||||
title: '手机号已复制',
|
||||
icon: 'success',
|
||||
duration: 1500
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 一键拨打
|
||||
const makePhoneCall = (phone: string) => {
|
||||
Taro.makePhoneCall({
|
||||
phoneNumber: phone,
|
||||
fail: () => {
|
||||
Taro.showToast({
|
||||
title: '拨打取消',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 编辑跟进情况
|
||||
const editComments = (customer: CustomerUser) => {
|
||||
Taro.showModal({
|
||||
title: '编辑跟进情况',
|
||||
// @ts-ignore
|
||||
editable: true,
|
||||
placeholderText: '请输入跟进情况',
|
||||
content: customer.comments || '',
|
||||
success: async (res) => {
|
||||
// @ts-ignore
|
||||
if (res.confirm && res.content !== undefined) {
|
||||
try {
|
||||
// 更新跟进情况
|
||||
await updateShopDealerApply({
|
||||
...customer,
|
||||
// @ts-ignore
|
||||
comments: res.content.trim()
|
||||
});
|
||||
|
||||
Taro.showToast({
|
||||
title: '更新成功',
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
// 刷新列表
|
||||
setList([]);
|
||||
setPage(1);
|
||||
setHasMore(true);
|
||||
fetchCustomerData(activeTab, true);
|
||||
} catch (error) {
|
||||
console.error('更新跟进情况失败:', error);
|
||||
Taro.showToast({
|
||||
title: '更新失败,请重试',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 计算剩余保护天数(基于过期时间)
|
||||
const calculateProtectDays = (expirationTime?: string, applyTime?: string): number => {
|
||||
try {
|
||||
// 优先使用过期时间字段
|
||||
if (expirationTime) {
|
||||
const expDate = new Date(expirationTime.replace(' ', 'T'));
|
||||
const now = new Date();
|
||||
|
||||
// 计算剩余毫秒数
|
||||
const remainingMs = expDate.getTime() - now.getTime();
|
||||
|
||||
// 转换为天数,向上取整
|
||||
const remainingDays = Math.ceil(remainingMs / (1000 * 60 * 60 * 24));
|
||||
|
||||
console.log('=== 基于过期时间计算 ===');
|
||||
console.log('过期时间:', expirationTime);
|
||||
console.log('当前时间:', now.toLocaleString());
|
||||
console.log('剩余天数:', remainingDays);
|
||||
console.log('======================');
|
||||
|
||||
return Math.max(0, remainingDays);
|
||||
}
|
||||
|
||||
// 如果没有过期时间,回退到基于申请时间计算
|
||||
if (!applyTime) return 0;
|
||||
|
||||
const protectionPeriod = 7; // 保护期7天
|
||||
|
||||
// 解析申请时间
|
||||
let applyDate: Date;
|
||||
if (applyTime.includes('T')) {
|
||||
applyDate = new Date(applyTime);
|
||||
} else {
|
||||
applyDate = new Date(applyTime.replace(' ', 'T'));
|
||||
}
|
||||
|
||||
// 获取当前时间
|
||||
const now = new Date();
|
||||
|
||||
// 只比较日期部分,忽略时间
|
||||
const applyDateOnly = new Date(applyDate.getFullYear(), applyDate.getMonth(), applyDate.getDate());
|
||||
const currentDateOnly = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
|
||||
// 计算已经过去的天数
|
||||
const timeDiff = currentDateOnly.getTime() - applyDateOnly.getTime();
|
||||
const daysPassed = Math.floor(timeDiff / (1000 * 60 * 60 * 24));
|
||||
|
||||
// 计算剩余保护天数
|
||||
const remainingDays = protectionPeriod - daysPassed;
|
||||
|
||||
console.log('=== 基于申请时间计算 ===');
|
||||
console.log('申请时间:', applyTime);
|
||||
console.log('已过去天数:', daysPassed);
|
||||
console.log('剩余保护天数:', remainingDays);
|
||||
console.log('======================');
|
||||
|
||||
return Math.max(0, remainingDays);
|
||||
} catch (error) {
|
||||
console.error('日期计算错误:', error);
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 获取客户数据
|
||||
const fetchCustomerData = useCallback(async (statusFilter?: CustomerStatus, resetPage = false, targetPage?: number) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const currentPage = resetPage ? 1 : (targetPage || page);
|
||||
|
||||
// 构建API参数,根据状态筛选
|
||||
const params: any = {
|
||||
type: 4,
|
||||
page: currentPage
|
||||
};
|
||||
const applyStatus = mapCustomerStatusToApplyStatus(statusFilter || activeTab);
|
||||
if (applyStatus !== undefined) {
|
||||
params.applyStatus = applyStatus;
|
||||
}
|
||||
|
||||
const res = await pageShopDealerApply(params);
|
||||
|
||||
if (res?.list && res.list.length > 0) {
|
||||
// 正确映射状态并计算保护天数
|
||||
const mappedList = res.list.map(customer => ({
|
||||
...customer,
|
||||
customerStatus: mapApplyStatusToCustomerStatus(customer.applyStatus || 10),
|
||||
protectDays: calculateProtectDays(customer.expirationTime, customer.applyTime || customer.createTime || '')
|
||||
}));
|
||||
|
||||
// 如果是重置页面或第一页,直接设置新数据;否则追加数据
|
||||
if (resetPage || currentPage === 1) {
|
||||
setList(mappedList);
|
||||
} else {
|
||||
setList(prevList => prevList.concat(mappedList));
|
||||
}
|
||||
|
||||
// 正确判断是否还有更多数据
|
||||
const hasMoreData = res.list.length >= 10; // 假设每页10条数据
|
||||
setHasMore(hasMoreData);
|
||||
} else {
|
||||
if (resetPage || currentPage === 1) {
|
||||
setList([]);
|
||||
}
|
||||
setHasMore(false);
|
||||
}
|
||||
|
||||
setPage(currentPage);
|
||||
} catch (error) {
|
||||
console.error('获取客户数据失败:', error);
|
||||
Taro.showToast({
|
||||
title: '加载失败,请重试',
|
||||
icon: 'none'
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [activeTab, page]);
|
||||
|
||||
const reloadMore = async () => {
|
||||
if (loading || !hasMore) return; // 防止重复加载
|
||||
const nextPage = page + 1;
|
||||
await fetchCustomerData(activeTab, false, nextPage);
|
||||
}
|
||||
|
||||
|
||||
// 根据搜索条件筛选数据(状态筛选已在API层面处理)
|
||||
const getFilteredList = () => {
|
||||
let filteredList = list;
|
||||
|
||||
// 按搜索关键词筛选
|
||||
if (searchValue.trim()) {
|
||||
const keyword = searchValue.trim().toLowerCase();
|
||||
filteredList = filteredList.filter(customer =>
|
||||
(customer.realName && customer.realName.toLowerCase().includes(keyword)) ||
|
||||
(customer.dealerName && customer.dealerName.toLowerCase().includes(keyword)) ||
|
||||
(customer.dealerCode && customer.dealerCode.toLowerCase().includes(keyword)) ||
|
||||
(customer.mobile && customer.mobile.includes(keyword)) ||
|
||||
(customer.userId && customer.userId.toString().includes(keyword))
|
||||
);
|
||||
}
|
||||
|
||||
return filteredList;
|
||||
};
|
||||
|
||||
// 获取各状态的统计数量
|
||||
const [statusCounts, setStatusCounts] = useState({
|
||||
all: 0,
|
||||
pending: 0,
|
||||
signed: 0,
|
||||
cancelled: 0
|
||||
});
|
||||
|
||||
// 获取所有状态的统计数量
|
||||
const fetchStatusCounts = useCallback(async () => {
|
||||
try {
|
||||
// 并行获取各状态的数量
|
||||
const [allRes, pendingRes, signedRes, cancelledRes] = await Promise.all([
|
||||
pageShopDealerApply({type: 4}), // 全部
|
||||
pageShopDealerApply({applyStatus: 10, type: 4}), // 跟进中
|
||||
pageShopDealerApply({applyStatus: 20, type: 4}), // 已签约
|
||||
pageShopDealerApply({applyStatus: 30, type: 4}) // 已取消
|
||||
]);
|
||||
|
||||
setStatusCounts({
|
||||
all: allRes?.count || 0,
|
||||
pending: pendingRes?.count || 0,
|
||||
signed: signedRes?.count || 0,
|
||||
cancelled: cancelledRes?.count || 0
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取状态统计失败:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getStatusCounts = () => statusCounts;
|
||||
|
||||
// 取消操作
|
||||
const handleCancel = (customer: ShopDealerApply) => {
|
||||
updateShopDealerApply({
|
||||
...customer,
|
||||
applyStatus: 30
|
||||
}).then(() => {
|
||||
Taro.showToast({
|
||||
title: '取消成功',
|
||||
icon: 'success'
|
||||
});
|
||||
// 重新加载当前tab的数据
|
||||
setList([]);
|
||||
setPage(1);
|
||||
setHasMore(true);
|
||||
fetchCustomerData(activeTab, true).then();
|
||||
fetchStatusCounts().then();
|
||||
})
|
||||
};
|
||||
|
||||
// 删除
|
||||
const handleDelete = (customer: ShopDealerApply) => {
|
||||
removeShopDealerApply(customer.applyId).then(() => {
|
||||
Taro.showToast({
|
||||
title: '删除成功',
|
||||
icon: 'success'
|
||||
});
|
||||
// 刷新当前tab的数据
|
||||
setList([]);
|
||||
setPage(1);
|
||||
setHasMore(true);
|
||||
fetchCustomerData(activeTab, true).then();
|
||||
fetchStatusCounts().then();
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化数据
|
||||
useEffect(() => {
|
||||
fetchCustomerData(activeTab, true).then();
|
||||
fetchStatusCounts().then();
|
||||
}, []);
|
||||
|
||||
// 当activeTab变化时重新获取数据
|
||||
useEffect(() => {
|
||||
setList([]); // 清空列表
|
||||
setPage(1); // 重置页码
|
||||
setHasMore(true); // 重置加载状态
|
||||
fetchCustomerData(activeTab, true);
|
||||
}, [activeTab]);
|
||||
|
||||
// 监听页面显示,当从其他页面返回时刷新数据
|
||||
useDidShow(() => {
|
||||
// 刷新当前tab的数据和统计信息
|
||||
setList([]);
|
||||
setPage(1);
|
||||
setHasMore(true);
|
||||
fetchCustomerData(activeTab, true);
|
||||
fetchStatusCounts();
|
||||
});
|
||||
|
||||
// 渲染客户项
|
||||
const renderCustomerItem = (customer: CustomerUser) => (
|
||||
<View key={customer.userId} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
|
||||
<View className="flex items-center mb-3">
|
||||
<View className="flex-1">
|
||||
<View className="flex items-center justify-between mb-1">
|
||||
<Text className="font-semibold text-gray-800 mr-2">
|
||||
{customer.dealerName}
|
||||
</Text>
|
||||
{customer.customerStatus && (
|
||||
<Tag type={getStatusTagType(customer.customerStatus)}>
|
||||
{getStatusText(customer.customerStatus)}
|
||||
</Tag>
|
||||
)}
|
||||
</View>
|
||||
<View className="flex items-center mb-1">
|
||||
<Space direction="vertical">
|
||||
<Text className="text-xs text-gray-500">联系人:{customer.realName}</Text>
|
||||
<View className="flex items-center">
|
||||
<Text className="text-xs text-gray-500" onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
makePhoneCall(customer.mobile || '');
|
||||
}}>联系电话:{customer.mobile}</Text>
|
||||
<View className="flex items-center ml-2">
|
||||
<Phone
|
||||
size={12}
|
||||
className="text-green-500 mr-2"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
makePhoneCall(customer.mobile || '');
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
className="text-xs text-blue-500 cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
copyPhone(customer.mobile || '');
|
||||
}}
|
||||
>
|
||||
复制
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Text className="text-xs text-gray-500">
|
||||
添加时间:{customer.createTime}
|
||||
</Text>
|
||||
</Space>
|
||||
</View>
|
||||
|
||||
{/* 保护天数显示 */}
|
||||
{customer.applyStatus === 10 && (
|
||||
<View className="flex items-center my-1">
|
||||
<Text className="text-xs text-gray-500 mr-2">保护期:</Text>
|
||||
{customer.protectDays && customer.protectDays > 0 ? (
|
||||
<Text className={`text-xs px-2 py-1 rounded ${
|
||||
customer.protectDays <= 2
|
||||
? 'bg-red-100 text-red-600'
|
||||
: customer.protectDays <= 4
|
||||
? 'bg-orange-100 text-orange-600'
|
||||
: 'bg-green-100 text-green-600'
|
||||
}`}>
|
||||
剩余{customer.protectDays}天
|
||||
</Text>
|
||||
) : (
|
||||
<Text className="text-xs px-2 py-1 rounded bg-gray-100 text-gray-500">
|
||||
已过期
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View className={'flex items-center gap-2'}>
|
||||
<Text className="text-xs text-gray-500">报备人:{customer?.nickName}</Text>
|
||||
<AngleDoubleLeft size={12} className={'text-blue-500'} />
|
||||
<Text className={'text-xs text-gray-500'}>{customer?.refereeName}</Text>
|
||||
</View>
|
||||
|
||||
{/* 显示 comments 字段 */}
|
||||
<Space className="flex items-center">
|
||||
<Text className="text-xs text-gray-500">跟进情况:{customer.comments || '暂无'}</Text>
|
||||
<Text
|
||||
className="text-xs text-blue-500 cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
editComments(customer);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</Text>
|
||||
</Space>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 跟进中状态显示操作按钮 */}
|
||||
{(customer.applyStatus === 10 && customer.userId == Taro.getStorageSync('UserId')) && (
|
||||
<Space className="flex justify-end">
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => navTo(`/dealer/customer/add?id=${customer.applyId}`, true)}
|
||||
style={{marginRight: '8px', backgroundColor: '#52c41a', color: 'white'}}
|
||||
>
|
||||
签约
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => handleCancel(customer)}
|
||||
style={{backgroundColor: '#ff4d4f', color: 'white'}}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
</Space>
|
||||
)}
|
||||
{(customer.applyStatus === 30 && customer.userId == Taro.getStorageSync('UserId')) && (
|
||||
<Space className="flex justify-end">
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => handleDelete(customer)}
|
||||
style={{backgroundColor: '#ff4d4f', color: 'white'}}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Space>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
|
||||
// 渲染客户列表
|
||||
const renderCustomerList = () => {
|
||||
const filteredList = getFilteredList();
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
>
|
||||
{loading && filteredList.length === 0 ? (
|
||||
<View className="flex items-center justify-center py-8">
|
||||
<Loading/>
|
||||
<Text className="text-gray-500 mt-2 ml-2">加载中...</Text>
|
||||
</View>
|
||||
) : (
|
||||
filteredList.map(renderCustomerItem)
|
||||
)}
|
||||
</InfiniteLoading>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="min-h-screen bg-gray-50">
|
||||
|
||||
{/* 顶部Tabs */}
|
||||
<View className="bg-white">
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={(value) => setActiveTab(value as CustomerStatus)}
|
||||
>
|
||||
{tabList.map(tab => {
|
||||
const counts = getStatusCounts();
|
||||
const count = counts[tab.value as keyof typeof counts] || 0;
|
||||
return (
|
||||
<TabPane
|
||||
key={tab.value}
|
||||
title={`${tab.label}${count > 0 ? `(${count})` : ''}`}
|
||||
value={tab.value}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
</View>
|
||||
|
||||
{/* 客户列表 */}
|
||||
{renderCustomerList()}
|
||||
|
||||
<FixedButton text={'客户报备'} onClick={() => Taro.navigateTo({url: '/dealer/customer/add'})}/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomerIndex;
|
||||
3
src/dealer/customer/trading.config.ts
Normal file
3
src/dealer/customer/trading.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '入市查询'
|
||||
})
|
||||
207
src/dealer/customer/trading.tsx
Normal file
207
src/dealer/customer/trading.tsx
Normal file
@@ -0,0 +1,207 @@
|
||||
import {useState, useCallback} from 'react'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {Loading, InfiniteLoading, Empty, Space, SearchBar} from '@nutui/nutui-react-taro'
|
||||
import type {ShopDealerApply as UserType} from "@/api/shop/shopDealerApply/model";
|
||||
import {
|
||||
CustomerStatus,
|
||||
mapApplyStatusToCustomerStatus,
|
||||
} from '@/utils/customerStatus';
|
||||
import {pageShopDealerApply} from "@/api/shop/shopDealerApply";
|
||||
|
||||
// 扩展User类型,添加客户状态
|
||||
interface CustomerUser extends UserType {
|
||||
customerStatus?: CustomerStatus;
|
||||
}
|
||||
|
||||
const CustomerTrading = () => {
|
||||
const [list, setList] = useState<CustomerUser[]>([])
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [searchValue, setSearchValue] = useState<string>('')
|
||||
const [page, setPage] = useState(1)
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
|
||||
// 获取客户数据
|
||||
const fetchCustomerData = useCallback(async (resetPage = false, targetPage?: number, searchKeyword?: string) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const currentPage = resetPage ? 1 : (targetPage || page);
|
||||
|
||||
// 构建API参数,根据状态筛选
|
||||
const params: any = {
|
||||
type: 3,
|
||||
page: currentPage
|
||||
};
|
||||
|
||||
// 添加搜索关键词
|
||||
if (searchKeyword && searchKeyword.trim()) {
|
||||
params.keywords = searchKeyword.trim();
|
||||
}
|
||||
|
||||
const res = await pageShopDealerApply(params);
|
||||
|
||||
if (res?.list && res.list.length > 0) {
|
||||
// 正确映射状态
|
||||
const mappedList = res.list.map(customer => ({
|
||||
...customer,
|
||||
customerStatus: mapApplyStatusToCustomerStatus(customer.applyStatus || 10)
|
||||
}));
|
||||
|
||||
// 如果是重置页面或第一页,直接设置新数据;否则追加数据
|
||||
if (resetPage || currentPage === 1) {
|
||||
setList(mappedList);
|
||||
} else {
|
||||
setList(prevList => prevList.concat(mappedList));
|
||||
}
|
||||
|
||||
// 正确判断是否还有更多数据
|
||||
const hasMoreData = res.list.length >= 10; // 假设每页10条数据
|
||||
setHasMore(hasMoreData);
|
||||
} else {
|
||||
if (resetPage || currentPage === 1) {
|
||||
setList([]);
|
||||
}
|
||||
setHasMore(false);
|
||||
}
|
||||
|
||||
setPage(currentPage);
|
||||
} catch (error) {
|
||||
console.error('获取客户数据失败:', error);
|
||||
Taro.showToast({
|
||||
title: '加载失败,请重试',
|
||||
icon: 'none'
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [page]);
|
||||
|
||||
const reloadMore = async () => {
|
||||
if (loading || !hasMore) return; // 防止重复加载
|
||||
const nextPage = page + 1;
|
||||
await fetchCustomerData(false, nextPage, searchValue);
|
||||
}
|
||||
|
||||
|
||||
// 获取列表数据(现在使用服务端搜索,不需要客户端过滤)
|
||||
const getFilteredList = () => {
|
||||
return list;
|
||||
};
|
||||
|
||||
// 搜索处理函数
|
||||
const handleSearch = (keyword: string) => {
|
||||
if(keyword.length < 4){
|
||||
Taro.showToast({
|
||||
title: '请输入至少4个字符',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
setSearchValue(keyword);
|
||||
setList([]);
|
||||
setPage(1);
|
||||
setHasMore(true);
|
||||
fetchCustomerData(true, 1, keyword);
|
||||
};
|
||||
|
||||
// 清空搜索
|
||||
const handleClearSearch = () => {
|
||||
setSearchValue('');
|
||||
setList([]);
|
||||
setPage(1);
|
||||
setHasMore(true);
|
||||
fetchCustomerData(true, 1, '');
|
||||
};
|
||||
|
||||
// 渲染客户项
|
||||
const renderCustomerItem = (customer: CustomerUser) => (
|
||||
<View key={customer.userId} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
|
||||
<View className="flex items-center">
|
||||
<View className="flex-1">
|
||||
<View className="flex items-center justify-between mb-1">
|
||||
<Text className="font-semibold text-gray-800 mr-2">
|
||||
{customer.dealerName}
|
||||
</Text>
|
||||
</View>
|
||||
<Space direction={'vertical'}>
|
||||
{/*<Text className="text-xs text-gray-500">统一代码:{customer.dealerCode}</Text>*/}
|
||||
<Text className="text-xs text-gray-500">
|
||||
更新时间:{customer.createTime}
|
||||
</Text>
|
||||
</Space>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
// 渲染客户列表
|
||||
const renderCustomerList = () => {
|
||||
const filteredList = getFilteredList();
|
||||
|
||||
return (
|
||||
<View className="p-4" style={{
|
||||
height: '90vh',
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden'
|
||||
}}>
|
||||
<InfiniteLoading
|
||||
target="scroll"
|
||||
hasMore={hasMore}
|
||||
onLoadMore={reloadMore}
|
||||
onScroll={() => {
|
||||
// 滚动事件处理
|
||||
}}
|
||||
onScrollToUpper={() => {
|
||||
// 滚动到顶部事件处理
|
||||
}}
|
||||
loadingText={
|
||||
<>
|
||||
加载中...
|
||||
</>
|
||||
}
|
||||
loadMoreText={
|
||||
filteredList.length === 0 ? (
|
||||
<Empty
|
||||
style={{backgroundColor: 'transparent'}}
|
||||
description={loading ? "加载中..." : "暂无客户数据"}
|
||||
/>
|
||||
) : (
|
||||
<View className={'h-12 flex items-center justify-center'}>
|
||||
<Text className="text-gray-500 text-sm">没有更多了</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
>
|
||||
{loading && filteredList.length === 0 ? (
|
||||
<View className="flex items-center justify-center py-8">
|
||||
<Loading/>
|
||||
<Text className="text-gray-500 mt-2 ml-2">加载中...</Text>
|
||||
</View>
|
||||
) : (
|
||||
filteredList.map(renderCustomerItem)
|
||||
)}
|
||||
</InfiniteLoading>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="min-h-screen bg-gray-50">
|
||||
{/* 搜索栏 */}
|
||||
<View className="bg-white shadow-sm">
|
||||
<SearchBar
|
||||
placeholder="请输入搜索关键词"
|
||||
value={searchValue}
|
||||
onSearch={(value) => handleSearch(value)}
|
||||
onClear={() => handleClearSearch()}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 客户列表 */}
|
||||
{renderCustomerList()}
|
||||
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomerTrading;
|
||||
@@ -1,3 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '医生版'
|
||||
navigationBarTitleText: '分销中心'
|
||||
})
|
||||
|
||||
@@ -131,28 +131,28 @@ const DealerIndex: React.FC = () => {
|
||||
<View className="mb-4">
|
||||
<Text className="font-semibold text-gray-800">佣金统计</Text>
|
||||
</View>
|
||||
<View className="grid grid-cols-3 gap-4">
|
||||
<View className="grid grid-cols-3 gap-3">
|
||||
<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 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-2xl font-bold mb-1 text-white">
|
||||
¥{formatMoney(dealerUser.freezeMoney)}
|
||||
<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-2xl font-bold mb-1 text-white">
|
||||
¥{formatMoney(dealerUser.totalMoney)}
|
||||
<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>
|
||||
|
||||
@@ -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 {
|
||||
if (isRefresh) {
|
||||
setRefreshing(true)
|
||||
} else if (page === 1) {
|
||||
setLoading(true)
|
||||
} else {
|
||||
setLoadingMore(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 result = await pageShopDealerOrder({
|
||||
page,
|
||||
limit: 10
|
||||
})
|
||||
])
|
||||
|
||||
const allOrders: OrderWithDetails[] = []
|
||||
const stats = {
|
||||
totalOrders: 0,
|
||||
totalCommission: '0.00',
|
||||
pendingCommission: '0.00',
|
||||
level1: { orders: 0, commission: '0.00' },
|
||||
level2: { orders: 0, commission: '0.00' },
|
||||
level3: { orders: 0, commission: '0.00' }
|
||||
}
|
||||
|
||||
// 处理一级分销订单
|
||||
if (level1Result?.list) {
|
||||
const level1Orders = level1Result.list.map(order => ({
|
||||
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])
|
||||
}
|
||||
|
||||
// 处理二级分销订单
|
||||
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)
|
||||
setHasMore(newOrders.length === 10)
|
||||
setCurrentPage(page)
|
||||
}
|
||||
|
||||
// 处理三级分销订单
|
||||
if (level3Result?.list) {
|
||||
const level3Orders = level3Result.list.map(order => ({
|
||||
...order,
|
||||
orderNo: `DD${order.orderId}`,
|
||||
customerName: `用户${order.userId}`,
|
||||
userLevel: 3 as const,
|
||||
userCommission: order.thirdMoney || '0.00',
|
||||
totalCommission: (
|
||||
parseFloat(order.firstMoney || '0') +
|
||||
parseFloat(order.secondMoney || '0') +
|
||||
parseFloat(order.thirdMoney || '0')
|
||||
).toFixed(2)
|
||||
}))
|
||||
|
||||
allOrders.push(...level3Orders)
|
||||
stats.level3.orders = level3Orders.length
|
||||
stats.level3.commission = level3Orders.reduce((sum, order) =>
|
||||
sum + parseFloat(order.userCommission || '0'), 0
|
||||
).toFixed(2)
|
||||
}
|
||||
|
||||
// 去重(同一个订单可能在多个层级中出现)
|
||||
const uniqueOrders = allOrders.filter((order, index, self) =>
|
||||
index === self.findIndex(o => o.orderId === order.orderId)
|
||||
)
|
||||
|
||||
// 计算总统计
|
||||
stats.totalOrders = uniqueOrders.length
|
||||
stats.totalCommission = (
|
||||
parseFloat(stats.level1.commission) +
|
||||
parseFloat(stats.level2.commission) +
|
||||
parseFloat(stats.level3.commission)
|
||||
).toFixed(2)
|
||||
stats.pendingCommission = allOrders
|
||||
.filter(order => order.isSettled === 0)
|
||||
.reduce((sum, order) => sum + parseFloat(order.userCommission || '0'), 0)
|
||||
.toFixed(2)
|
||||
|
||||
setOrders(uniqueOrders)
|
||||
setStatistics(stats)
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取分销订单失败:', error)
|
||||
Taro.showToast({
|
||||
@@ -164,18 +66,27 @@ const DealerOrders: React.FC = () => {
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setRefreshing(false)
|
||||
setLoadingMore(false)
|
||||
}
|
||||
}, [dealerUser?.userId])
|
||||
|
||||
// 刷新数据
|
||||
// 下拉刷新
|
||||
const handleRefresh = async () => {
|
||||
await fetchOrders()
|
||||
await fetchOrders(1, true)
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
const handleLoadMore = async () => {
|
||||
if (!loadingMore && hasMore) {
|
||||
await fetchOrders(currentPage + 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化加载数据
|
||||
useEffect(() => {
|
||||
if (dealerUser?.userId) {
|
||||
fetchOrders().then()
|
||||
fetchOrders(1)
|
||||
}
|
||||
}, [fetchOrders])
|
||||
|
||||
@@ -193,198 +104,87 @@ const DealerOrders: React.FC = () => {
|
||||
|
||||
const renderOrderItem = (order: OrderWithDetails) => (
|
||||
<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">
|
||||
<View className="flex justify-between items-start mb-1">
|
||||
<Text className="font-semibold text-gray-800">
|
||||
订单号:{order.orderNo}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-500">
|
||||
客户:{order.customerName}
|
||||
</Text>
|
||||
{/* 显示用户在此订单中的层级 */}
|
||||
<Text className="text-xs text-blue-500">
|
||||
{order.userLevel === 1 && '一级分销'}
|
||||
{order.userLevel === 2 && '二级分销'}
|
||||
{order.userLevel === 3 && '三级分销'}
|
||||
</Text>
|
||||
</View>
|
||||
<Tag type={getStatusColor(order.isSettled, order.isInvalid)}>
|
||||
{getStatusText(order.isSettled, order.isInvalid)}
|
||||
</Tag>
|
||||
</View>
|
||||
|
||||
<View className="flex justify-between items-center">
|
||||
<View>
|
||||
<Text className="text-sm text-gray-600">
|
||||
<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>
|
||||
<Text className="text-xs text-gray-400">
|
||||
总佣金:¥{order.totalCommission}
|
||||
</Text>
|
||||
</View>
|
||||
<Text className="text-xs text-gray-400">
|
||||
|
||||
<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>
|
||||
)
|
||||
|
||||
// 根据状态和层级过滤订单
|
||||
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 />
|
||||
<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">
|
||||
<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 ? (
|
||||
{loading && orders.length === 0 ? (
|
||||
<View className="text-center py-8">
|
||||
<Loading />
|
||||
<Loading/>
|
||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||
</View>
|
||||
) : getFilteredOrders('0').length > 0 ? (
|
||||
getFilteredOrders('0').map(renderOrderItem)
|
||||
) : 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="暂无分销订单"/>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</PullToRefresh>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane title="一级分销" value="1">
|
||||
<View className="p-4">
|
||||
{getFilteredOrders('1').length > 0 ? (
|
||||
getFilteredOrders('1').map(renderOrderItem)
|
||||
) : (
|
||||
<Empty description="暂无一级分销订单" />
|
||||
)}
|
||||
</View>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane title="二级分销" value="2">
|
||||
<View className="p-4">
|
||||
{getFilteredOrders('2').length > 0 ? (
|
||||
getFilteredOrders('2').map(renderOrderItem)
|
||||
) : (
|
||||
<Empty description="暂无二级分销订单" />
|
||||
)}
|
||||
</View>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane title="三级分销" value="3">
|
||||
<View className="p-4">
|
||||
{getFilteredOrders('3').length > 0 ? (
|
||||
getFilteredOrders('3').map(renderOrderItem)
|
||||
) : (
|
||||
<Empty description="暂无三级分销订单" />
|
||||
)}
|
||||
</View>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane title="待结算" value="4">
|
||||
<View className="p-4">
|
||||
{getFilteredOrders('4').length > 0 ? (
|
||||
getFilteredOrders('4').map(renderOrderItem)
|
||||
) : (
|
||||
<Empty description="暂无待结算订单" />
|
||||
)}
|
||||
</View>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane title="已结算" value="5">
|
||||
<View className="p-4">
|
||||
{getFilteredOrders('5').length > 0 ? (
|
||||
getFilteredOrders('5').map(renderOrderItem)
|
||||
) : (
|
||||
<Empty description="暂无已结算订单" />
|
||||
)}
|
||||
</View>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane title="已失效" value="6">
|
||||
<View className="p-4">
|
||||
{getFilteredOrders('6').length > 0 ? (
|
||||
getFilteredOrders('6').map(renderOrderItem)
|
||||
) : (
|
||||
<Empty description="暂无失效订单" />
|
||||
)}
|
||||
</View>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
{/* 推广说明 */}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '我的团队'
|
||||
navigationBarTitleText: '邀请推广'
|
||||
})
|
||||
|
||||
@@ -1,77 +1,79 @@
|
||||
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 fetchTeamData = useCallback(async () => {
|
||||
if (!dealerUser?.userId) return
|
||||
// 异步加载成员统计数据
|
||||
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 {
|
||||
setLoading(true)
|
||||
|
||||
// 获取团队成员关系
|
||||
const refereeResult = await listShopDealerReferee({
|
||||
dealerId: dealerUser.userId
|
||||
})
|
||||
|
||||
if (refereeResult) {
|
||||
// 处理团队成员数据
|
||||
const processedMembers: TeamMemberWithStats[] = refereeResult.map(member => ({
|
||||
...member,
|
||||
name: `用户${member.userId}`,
|
||||
avatar: '',
|
||||
orderCount: 0,
|
||||
commission: '0.00',
|
||||
status: 'active' as const,
|
||||
subMembers: 0,
|
||||
joinTime: member.createTime
|
||||
}))
|
||||
|
||||
// 并行获取每个成员的订单统计
|
||||
const memberStats = await Promise.all(
|
||||
processedMembers.map(async (member) => {
|
||||
try {
|
||||
const orderResult = await pageShopDealerOrder({
|
||||
// 并行获取订单统计和下级成员数量
|
||||
const [orderResult, subMembersResult] = await Promise.all([
|
||||
pageShopDealerOrder({
|
||||
page: 1,
|
||||
limit: 100,
|
||||
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
|
||||
const orderCount = orders.length
|
||||
const commission = orders.reduce((sum, order) => {
|
||||
orderCount = orders.length
|
||||
commission = orders.reduce((sum, order) => {
|
||||
const levelCommission = member.level === 1 ? order.firstMoney :
|
||||
member.level === 2 ? order.secondMoney :
|
||||
order.thirdMoney
|
||||
@@ -84,36 +86,80 @@ const DealerTeam: React.FC = () => {
|
||||
const hasRecentOrder = orders.some(order =>
|
||||
new Date(order.createTime || '') > thirtyDaysAgo
|
||||
)
|
||||
status = hasRecentOrder ? 'active' : 'inactive'
|
||||
}
|
||||
|
||||
return {
|
||||
...member,
|
||||
orderCount,
|
||||
commission,
|
||||
status: hasRecentOrder ? 'active' as const : 'inactive' as const
|
||||
status,
|
||||
subMembers: subMembersResult?.length || 0
|
||||
}
|
||||
}
|
||||
return member
|
||||
} catch (error) {
|
||||
console.error(`获取成员${member.userId}订单失败:`, error)
|
||||
return member
|
||||
console.error(`获取成员${member.userId}数据失败:`, error)
|
||||
return {
|
||||
...member,
|
||||
orderCount: 0,
|
||||
commission: '0.00',
|
||||
status: 'inactive' as const,
|
||||
subMembers: 0
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
setTeamMembers(memberStats)
|
||||
// 更新这一批成员的数据
|
||||
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
|
||||
})
|
||||
|
||||
// 计算统计数据
|
||||
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)
|
||||
// 添加小延迟,避免请求过于密集
|
||||
if (i + batchSize < members.length) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setTeamStats(stats)
|
||||
// 获取团队数据
|
||||
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)
|
||||
@@ -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">
|
||||
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}
|
||||
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}级
|
||||
<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 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>
|
||||
<Space>
|
||||
<Text className="text-xs text-gray-500">订单数</Text>
|
||||
</View>
|
||||
<View>
|
||||
<Text className="text-sm font-semibold text-green-600">
|
||||
¥{member.commission}
|
||||
<Text className="text-sm font-semibold text-blue-600">
|
||||
{isStatsLoading ? '-' : member.orderCount}
|
||||
</Text>
|
||||
</Space>
|
||||
<Space>
|
||||
<Text className="text-xs text-gray-500">贡献佣金</Text>
|
||||
</View>
|
||||
<View>
|
||||
<Text className="text-sm font-semibold text-purple-600">
|
||||
{member.subMembers}
|
||||
<Text className="text-sm font-semibold text-green-600">
|
||||
{isStatsLoading ? '-' : `¥${member.commission}`}
|
||||
</Text>
|
||||
</Space>
|
||||
<Space>
|
||||
<Text className="text-xs text-gray-500">团队成员</Text>
|
||||
<Text className={`text-sm font-semibold ${
|
||||
canClick ? 'text-purple-600' : 'text-gray-400'
|
||||
}`}>
|
||||
{isStatsLoading ? '-' : (member.subMembers || 0)}
|
||||
</Text>
|
||||
</Space>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
|
||||
const renderOverview = () => (
|
||||
<View className="p-4">
|
||||
{/* 团队统计卡片 */}
|
||||
<View className="rounded-xl p-6 mb-6 text-white relative overflow-hidden" style={{
|
||||
background: 'linear-gradient(135deg, #8b5cf6 0%, #ec4899 100%)'
|
||||
}}>
|
||||
{/* 装饰背景 - 小程序兼容版本 */}
|
||||
<View className="absolute w-32 h-32 rounded-full" style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
top: '-16px',
|
||||
right: '-16px'
|
||||
}}></View>
|
||||
<View className="absolute w-20 h-20 rounded-full" style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||
bottom: '-10px',
|
||||
left: '-10px'
|
||||
}}></View>
|
||||
|
||||
<View className="relative z-10">
|
||||
<Text className="text-lg font-bold mb-4 text-white">团队总览</Text>
|
||||
<View className="grid grid-cols-2 gap-4">
|
||||
<View>
|
||||
<Text className="text-2xl font-bold mb-1 text-white">{teamStats.total}</Text>
|
||||
<Text className="text-sm" style={{ color: 'rgba(255, 255, 255, 0.8)' }}>团队总人数</Text>
|
||||
</View>
|
||||
<View>
|
||||
<Text className="text-2xl font-bold mb-1 text-white">¥{teamStats.monthlyCommission}</Text>
|
||||
<Text className="text-sm" style={{ color: 'rgba(255, 255, 255, 0.8)' }}>本月团队佣金</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 层级分布 */}
|
||||
<View className="bg-white rounded-xl p-4 mb-4">
|
||||
<Text className="font-semibold mb-4 text-gray-800">层级分布</Text>
|
||||
<View className="gap-2">
|
||||
<View className="flex items-center justify-between">
|
||||
<View className="flex items-center">
|
||||
<StarFill color="#f59e0b" size="16" className="mr-2" />
|
||||
<Text className="text-sm">一级成员</Text>
|
||||
</View>
|
||||
<View className="flex items-center">
|
||||
<Text className="text-sm font-semibold mr-2">{teamStats.firstLevel}</Text>
|
||||
<Progress
|
||||
percent={(teamStats.firstLevel / teamStats.total) * 100}
|
||||
strokeWidth="6"
|
||||
background={'#f59e0b'}
|
||||
className="w-20"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="flex items-center justify-between">
|
||||
<View className="flex items-center">
|
||||
<Star color="#8b5cf6" size="16" className="mr-2" />
|
||||
<Text className="text-sm">二级成员</Text>
|
||||
</View>
|
||||
<View className="flex items-center">
|
||||
<Text className="text-sm font-semibold mr-2">{teamStats.secondLevel}</Text>
|
||||
<Progress
|
||||
percent={(teamStats.secondLevel / teamStats.total) * 100}
|
||||
strokeWidth="6"
|
||||
background={'#8b5cf6'}
|
||||
className="w-20"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="flex items-center justify-between">
|
||||
<View className="flex items-center">
|
||||
<User color="#ec4899" size="16" className="mr-2" />
|
||||
<Text className="text-sm">三级成员</Text>
|
||||
</View>
|
||||
<View className="flex items-center">
|
||||
<Text className="text-sm font-semibold mr-2">{teamStats.thirdLevel}</Text>
|
||||
<Progress
|
||||
percent={(teamStats.thirdLevel / teamStats.total) * 100}
|
||||
strokeWidth="6"
|
||||
background={'#ec4899'}
|
||||
className="w-20"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 最新成员 */}
|
||||
<View className="bg-white rounded-xl p-4">
|
||||
<Text className="font-semibold mb-4 text-gray-800">最新成员</Text>
|
||||
{teamMembers.slice(0, 3).map(renderMemberItem)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
|
||||
const renderMemberList = (level?: number) => (
|
||||
<PullToRefresh
|
||||
disabled={refreshing}
|
||||
onRefresh={handleRefresh}
|
||||
>
|
||||
<View className="p-4">
|
||||
{loading ? (
|
||||
<View className="text-center py-8">
|
||||
<Loading />
|
||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||
</View>
|
||||
) : teamMembers
|
||||
.filter(member => !level || member.level === level)
|
||||
.length > 0 ? (
|
||||
teamMembers
|
||||
.filter(member => !level || member.level === level)
|
||||
.map(renderMemberItem)
|
||||
) : (
|
||||
<Empty description={`暂无${level ? level + '级' : ''}团队成员`} />
|
||||
)}
|
||||
</View>
|
||||
</PullToRefresh>
|
||||
)
|
||||
|
||||
if (!dealerUser) {
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
||||
<Loading />
|
||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen">
|
||||
<Tabs value={activeTab} onChange={() => setActiveTab}>
|
||||
<Tabs.TabPane title="团队总览" value="0">
|
||||
{renderOverview()}
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane title="一级成员" value="1">
|
||||
{renderMemberList(1)}
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane title="二级成员" value="2">
|
||||
{renderMemberList(2)}
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane title="三级成员" value="3">
|
||||
{renderMemberList(3)}
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
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
|
||||
export default DealerTeam;
|
||||
|
||||
3
src/dealer/wechat/index.config.ts
Normal file
3
src/dealer/wechat/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '微信客服'
|
||||
})
|
||||
176
src/dealer/wechat/index.scss
Normal file
176
src/dealer/wechat/index.scss
Normal file
@@ -0,0 +1,176 @@
|
||||
.wechat-service-page {
|
||||
min-height: 100vh;
|
||||
|
||||
.service-tabs {
|
||||
background-color: #fff;
|
||||
|
||||
.nut-tabs__titles {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.nut-tabs__content {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.qr-container {
|
||||
padding: 20px;
|
||||
min-height: calc(100vh - 100px);
|
||||
|
||||
.qr-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
|
||||
.qr-title {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.qr-description {
|
||||
display: block;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.qr-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 30px;
|
||||
|
||||
.qr-code-wrapper {
|
||||
background-color: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 30px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
|
||||
.qr-code-image {
|
||||
width: 360px;
|
||||
height: 360px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.wechat-id {
|
||||
display: block;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.qr-tips {
|
||||
background-color: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.tip-title {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.tip-item {
|
||||
display: block;
|
||||
color: #666;
|
||||
line-height: 1.8;
|
||||
margin-bottom: 8px;
|
||||
padding-left: 10px;
|
||||
position: relative;
|
||||
|
||||
&:before {
|
||||
content: '•';
|
||||
color: #07c160;
|
||||
font-weight: bold;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式适配
|
||||
@media (max-width: 375px) {
|
||||
.wechat-service-page {
|
||||
.qr-container {
|
||||
padding: 15px;
|
||||
|
||||
.qr-content {
|
||||
.qr-code-wrapper {
|
||||
padding: 20px;
|
||||
|
||||
.qr-code-image {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 深色模式适配
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.wechat-service-page {
|
||||
background-color: #1a1a1a;
|
||||
|
||||
.service-tabs {
|
||||
.nut-tabs__titles {
|
||||
background-color: #2a2a2a;
|
||||
border-bottom-color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.qr-container {
|
||||
background-color: #1a1a1a;
|
||||
|
||||
.qr-header {
|
||||
.qr-title {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.qr-description {
|
||||
color: #ccc;
|
||||
}
|
||||
}
|
||||
|
||||
.qr-content {
|
||||
.qr-code-wrapper {
|
||||
background-color: #2a2a2a;
|
||||
|
||||
.qr-code-image {
|
||||
border-color: #444;
|
||||
}
|
||||
|
||||
.wechat-id {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.qr-tips {
|
||||
background-color: #2a2a2a;
|
||||
|
||||
.tip-title {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.tip-item {
|
||||
color: #ccc;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
121
src/dealer/wechat/index.tsx
Normal file
121
src/dealer/wechat/index.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import {useEffect, useState} from 'react'
|
||||
import {View, Text, Image} from '@tarojs/components'
|
||||
import {Tabs} from '@nutui/nutui-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import './index.scss'
|
||||
import {listCmsWebsiteField} from "@/api/cms/cmsWebsiteField";
|
||||
import {CmsWebsiteField} from "@/api/cms/cmsWebsiteField/model";
|
||||
|
||||
const WechatService = () => {
|
||||
const [activeTab, setActiveTab] = useState('0')
|
||||
const [codes, setCodes] = useState<CmsWebsiteField[]>([])
|
||||
|
||||
// 长按保存二维码到相册
|
||||
const saveQRCodeToAlbum = (imageUrl: string) => {
|
||||
// 首先下载图片到本地
|
||||
Taro.downloadFile({
|
||||
url: imageUrl,
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
// 保存图片到相册
|
||||
Taro.saveImageToPhotosAlbum({
|
||||
filePath: res.tempFilePath,
|
||||
success: () => {
|
||||
Taro.showToast({
|
||||
title: '保存成功',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
},
|
||||
fail: (error) => {
|
||||
console.error('保存失败:', error)
|
||||
if (error.errMsg.includes('auth deny')) {
|
||||
Taro.showModal({
|
||||
title: '提示',
|
||||
content: '需要您授权保存图片到相册',
|
||||
showCancel: true,
|
||||
cancelText: '取消',
|
||||
confirmText: '去设置',
|
||||
success: (modalRes) => {
|
||||
if (modalRes.confirm) {
|
||||
Taro.openSetting()
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Taro.showToast({
|
||||
title: '保存失败',
|
||||
icon: 'error',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Taro.showToast({
|
||||
title: '图片下载失败',
|
||||
icon: 'error',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
Taro.showToast({
|
||||
title: '图片下载失败',
|
||||
icon: 'error',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const renderQRCode = (data: typeof codes[0]) => (
|
||||
<View className="qr-container">
|
||||
<View className="qr-content">
|
||||
<View className="qr-code-wrapper">
|
||||
<Image
|
||||
src={`${data.value}`}
|
||||
className="qr-code-image"
|
||||
mode="aspectFit"
|
||||
onLongPress={() => saveQRCodeToAlbum(`${data.value}`)}
|
||||
/>
|
||||
{data.style && <Text className="wechat-id">联系电话:{data.style}</Text>}
|
||||
</View>
|
||||
|
||||
<View className="qr-tips">
|
||||
<Text className="tip-title">使用说明:</Text>
|
||||
<Text className="tip-item">1. 长按二维码保存到相册</Text>
|
||||
<Text className="tip-item">2. 打开微信扫一扫</Text>
|
||||
<Text className="tip-item">3. 选择相册中的二维码图片</Text>
|
||||
<Text className="tip-item">4. 添加好友并发送验证消息</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
listCmsWebsiteField({name: 'kefu'}).then(data => {
|
||||
if (data) {
|
||||
setCodes(data)
|
||||
}
|
||||
})
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View className="wechat-service-page">
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={(value) => setActiveTab(`${value}`)}
|
||||
className="service-tabs"
|
||||
>
|
||||
{codes.map((item) => (
|
||||
<Tabs.TabPane key={item.id} title={item.comments} value={item.id}>
|
||||
{renderQRCode(item)}
|
||||
</Tabs.TabPane>
|
||||
))}
|
||||
</Tabs>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default WechatService
|
||||
184
src/dealer/withdraw/__tests__/withdraw.test.tsx
Normal file
184
src/dealer/withdraw/__tests__/withdraw.test.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
import React from 'react'
|
||||
import { render, fireEvent, waitFor } from '@testing-library/react'
|
||||
import DealerWithdraw from '../index'
|
||||
import { useDealerUser } from '@/hooks/useDealerUser'
|
||||
import * as withdrawAPI from '@/api/shop/shopDealerWithdraw'
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('@/hooks/useDealerUser')
|
||||
jest.mock('@/api/shop/shopDealerWithdraw')
|
||||
jest.mock('@tarojs/taro', () => ({
|
||||
showToast: jest.fn(),
|
||||
getStorageSync: jest.fn(() => 123),
|
||||
}))
|
||||
|
||||
const mockUseDealerUser = useDealerUser as jest.MockedFunction<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')
|
||||
})
|
||||
})
|
||||
80
src/dealer/withdraw/debug.tsx
Normal file
80
src/dealer/withdraw/debug.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import React, { useState } from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Tabs, Button } from '@nutui/nutui-react-taro'
|
||||
|
||||
/**
|
||||
* 提现功能调试组件
|
||||
* 用于测试 Tabs 组件的点击和切换功能
|
||||
*/
|
||||
const WithdrawDebug: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState<string | number>('0')
|
||||
const [clickCount, setClickCount] = useState(0)
|
||||
|
||||
// Tab 切换处理函数
|
||||
const handleTabChange = (value: string | number) => {
|
||||
console.log('Tab切换:', { from: activeTab, to: value, type: typeof value })
|
||||
setActiveTab(value)
|
||||
setClickCount(prev => prev + 1)
|
||||
}
|
||||
|
||||
// 手动切换测试
|
||||
const manualSwitch = (tab: string | number) => {
|
||||
console.log('手动切换到:', tab)
|
||||
setActiveTab(tab)
|
||||
setClickCount(prev => prev + 1)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen p-4">
|
||||
<View className="bg-white rounded-lg p-4 mb-4">
|
||||
<Text className="text-lg font-bold mb-2">调试信息</Text>
|
||||
<Text className="block mb-1">当前Tab: {String(activeTab)}</Text>
|
||||
<Text className="block mb-1">切换次数: {clickCount}</Text>
|
||||
<Text className="block mb-1">Tab类型: {typeof activeTab}</Text>
|
||||
</View>
|
||||
|
||||
<View className="bg-white rounded-lg p-4 mb-4">
|
||||
<Text className="text-lg font-bold mb-2">手动切换测试</Text>
|
||||
<View className="flex gap-2">
|
||||
<Button size="small" onClick={() => manualSwitch('0')}>
|
||||
切换到申请提现
|
||||
</Button>
|
||||
<Button size="small" onClick={() => manualSwitch('1')}>
|
||||
切换到提现记录
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="bg-white rounded-lg">
|
||||
<Tabs value={activeTab} onChange={handleTabChange}>
|
||||
<Tabs.TabPane title="申请提现" value="0">
|
||||
<View className="p-4">
|
||||
<Text className="text-center text-gray-600">申请提现页面内容</Text>
|
||||
<Text className="text-center text-sm text-gray-400 mt-2">
|
||||
当前Tab值: {String(activeTab)}
|
||||
</Text>
|
||||
</View>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane title="提现记录" value="1">
|
||||
<View className="p-4">
|
||||
<Text className="text-center text-gray-600">提现记录页面内容</Text>
|
||||
<Text className="text-center text-sm text-gray-400 mt-2">
|
||||
当前Tab值: {String(activeTab)}
|
||||
</Text>
|
||||
</View>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</View>
|
||||
|
||||
<View className="bg-white rounded-lg p-4 mt-4">
|
||||
<Text className="text-lg font-bold mb-2">事件日志</Text>
|
||||
<Text className="text-sm text-gray-500">
|
||||
请查看控制台输出以获取详细的切换日志
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default WithdrawDebug
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useState, useRef, useEffect, useCallback } from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import React, {useState, useRef, useEffect, useCallback} from 'react'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import {
|
||||
Cell,
|
||||
Space,
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
@@ -13,19 +14,19 @@ import {
|
||||
Loading,
|
||||
PullToRefresh
|
||||
} from '@nutui/nutui-react-taro'
|
||||
import { Wallet } from '@nutui/icons-react-taro'
|
||||
import { businessGradients } from '@/styles/gradients'
|
||||
import {Wallet} from '@nutui/icons-react-taro'
|
||||
import {businessGradients} from '@/styles/gradients'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useDealerUser } from '@/hooks/useDealerUser'
|
||||
import { pageShopDealerWithdraw, addShopDealerWithdraw } from '@/api/shop/shopDealerWithdraw'
|
||||
import type { ShopDealerWithdraw } from '@/api/shop/shopDealerWithdraw/model'
|
||||
import {useDealerUser} from '@/hooks/useDealerUser'
|
||||
import {pageShopDealerWithdraw, addShopDealerWithdraw} from '@/api/shop/shopDealerWithdraw'
|
||||
import type {ShopDealerWithdraw} from '@/api/shop/shopDealerWithdraw/model'
|
||||
|
||||
interface WithdrawRecordWithDetails extends ShopDealerWithdraw {
|
||||
accountDisplay?: string
|
||||
}
|
||||
|
||||
const DealerWithdraw: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState('0')
|
||||
const [activeTab, setActiveTab] = useState<string | number>('0')
|
||||
const [selectedAccount, setSelectedAccount] = useState('')
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [refreshing, setRefreshing] = useState<boolean>(false)
|
||||
@@ -34,16 +35,28 @@ const DealerWithdraw: React.FC = () => {
|
||||
const [withdrawRecords, setWithdrawRecords] = useState<WithdrawRecordWithDetails[]>([])
|
||||
const formRef = useRef<any>(null)
|
||||
|
||||
const { dealerUser } = useDealerUser()
|
||||
const {dealerUser} = useDealerUser()
|
||||
|
||||
// Tab 切换处理函数
|
||||
const handleTabChange = (value: string | number) => {
|
||||
console.log('Tab切换到:', value)
|
||||
setActiveTab(value)
|
||||
|
||||
// 如果切换到提现记录页面,刷新数据
|
||||
if (String(value) === '1') {
|
||||
fetchWithdrawRecords()
|
||||
}
|
||||
}
|
||||
|
||||
// 获取可提现余额
|
||||
const fetchBalance = useCallback(async () => {
|
||||
console.log(dealerUser, 'dealerUser...')
|
||||
try {
|
||||
setAvailableAmount(dealerUser?.money || '0.00')
|
||||
} catch (error) {
|
||||
console.error('获取余额失败:', error)
|
||||
}
|
||||
}, [])
|
||||
}, [dealerUser])
|
||||
|
||||
// 获取提现记录
|
||||
const fetchWithdrawRecords = useCallback(async () => {
|
||||
@@ -104,21 +117,31 @@ const DealerWithdraw: React.FC = () => {
|
||||
|
||||
const getStatusText = (status?: number) => {
|
||||
switch (status) {
|
||||
case 40: return '已到账'
|
||||
case 20: return '审核通过'
|
||||
case 10: return '待审核'
|
||||
case 30: return '已驳回'
|
||||
default: return '未知'
|
||||
case 40:
|
||||
return '已到账'
|
||||
case 20:
|
||||
return '审核通过'
|
||||
case 10:
|
||||
return '待审核'
|
||||
case 30:
|
||||
return '已驳回'
|
||||
default:
|
||||
return '未知'
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusColor = (status?: number) => {
|
||||
switch (status) {
|
||||
case 40: return 'success'
|
||||
case 20: return 'success'
|
||||
case 10: return 'warning'
|
||||
case 30: return 'danger'
|
||||
default: return 'default'
|
||||
case 40:
|
||||
return 'success'
|
||||
case 20:
|
||||
return 'success'
|
||||
case 10:
|
||||
return 'warning'
|
||||
case 30:
|
||||
return 'danger'
|
||||
default:
|
||||
return 'default'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,9 +154,25 @@ const DealerWithdraw: React.FC = () => {
|
||||
return
|
||||
}
|
||||
|
||||
if (!values.accountType) {
|
||||
Taro.showToast({
|
||||
title: '请选择提现方式',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证提现金额
|
||||
const amount = parseFloat(values.amount)
|
||||
const available = parseFloat(availableAmount.replace(',', ''))
|
||||
const available = parseFloat(availableAmount.replace(/,/g, ''))
|
||||
|
||||
if (isNaN(amount) || amount <= 0) {
|
||||
Taro.showToast({
|
||||
title: '请输入有效的提现金额',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (amount < 100) {
|
||||
Taro.showToast({
|
||||
@@ -151,6 +190,25 @@ const DealerWithdraw: React.FC = () => {
|
||||
return
|
||||
}
|
||||
|
||||
// 验证账户信息
|
||||
if (values.accountType === 'alipay') {
|
||||
if (!values.account || !values.accountName) {
|
||||
Taro.showToast({
|
||||
title: '请填写完整的支付宝信息',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
} else if (values.accountType === 'bank') {
|
||||
if (!values.account || !values.accountName || !values.bankName) {
|
||||
Taro.showToast({
|
||||
title: '请填写完整的银行卡信息',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
setSubmitting(true)
|
||||
|
||||
@@ -204,15 +262,21 @@ const DealerWithdraw: React.FC = () => {
|
||||
const quickAmounts = ['100', '300', '500', '1000']
|
||||
|
||||
const setQuickAmount = (amount: string) => {
|
||||
formRef.current?.setFieldsValue({ amount })
|
||||
formRef.current?.setFieldsValue({amount})
|
||||
}
|
||||
|
||||
const setAllAmount = () => {
|
||||
formRef.current?.setFieldsValue({ amount: availableAmount.replace(',', '') })
|
||||
formRef.current?.setFieldsValue({amount: availableAmount.replace(/,/g, '')})
|
||||
}
|
||||
|
||||
// 格式化金额
|
||||
const formatMoney = (money?: string) => {
|
||||
if (!money) return '0.00'
|
||||
return parseFloat(money).toFixed(2)
|
||||
}
|
||||
|
||||
const renderWithdrawForm = () => (
|
||||
<View className="p-4">
|
||||
<View>
|
||||
{/* 余额卡片 */}
|
||||
<View className="rounded-xl p-6 mb-6 text-white relative overflow-hidden" style={{
|
||||
background: businessGradients.dealer.header
|
||||
@@ -225,14 +289,14 @@ 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={{
|
||||
@@ -254,7 +318,14 @@ const DealerWithdraw: React.FC = () => {
|
||||
<Input
|
||||
placeholder="请输入提现金额"
|
||||
type="number"
|
||||
clearable
|
||||
onChange={(value) => {
|
||||
// 实时验证提现金额
|
||||
const amount = parseFloat(value)
|
||||
const available = parseFloat(availableAmount.replace(/,/g, ''))
|
||||
if (!isNaN(amount) && amount > available) {
|
||||
// 可以在这里添加实时提示,但不阻止输入
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -301,10 +372,10 @@ const DealerWithdraw: React.FC = () => {
|
||||
{selectedAccount === 'alipay' && (
|
||||
<>
|
||||
<Form.Item name="account" label="支付宝账号" required>
|
||||
<Input placeholder="请输入支付宝账号" />
|
||||
<Input placeholder="请输入支付宝账号"/>
|
||||
</Form.Item>
|
||||
<Form.Item name="accountName" label="支付宝姓名" required>
|
||||
<Input placeholder="请输入支付宝实名姓名" />
|
||||
<Input placeholder="请输入支付宝实名姓名"/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
@@ -312,13 +383,13 @@ const DealerWithdraw: React.FC = () => {
|
||||
{selectedAccount === 'bank' && (
|
||||
<>
|
||||
<Form.Item name="bankName" label="开户银行" required>
|
||||
<Input placeholder="请输入开户银行名称" />
|
||||
<Input placeholder="请输入开户银行名称"/>
|
||||
</Form.Item>
|
||||
<Form.Item name="account" label="银行卡号" required>
|
||||
<Input placeholder="请输入银行卡号" />
|
||||
<Input placeholder="请输入银行卡号"/>
|
||||
</Form.Item>
|
||||
<Form.Item name="accountName" label="开户姓名" required>
|
||||
<Input placeholder="请输入银行卡开户姓名" />
|
||||
<Input placeholder="请输入银行卡开户姓名"/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
@@ -347,29 +418,32 @@ const DealerWithdraw: React.FC = () => {
|
||||
</View>
|
||||
)
|
||||
|
||||
const renderWithdrawRecords = () => (
|
||||
const renderWithdrawRecords = () => {
|
||||
console.log('渲染提现记录:', {loading, recordsCount: withdrawRecords.length, dealerUserId: dealerUser?.userId})
|
||||
|
||||
return (
|
||||
<PullToRefresh
|
||||
disabled={refreshing}
|
||||
onRefresh={handleRefresh}
|
||||
>
|
||||
<View className="p-4">
|
||||
<View>
|
||||
{loading ? (
|
||||
<View className="text-center py-8">
|
||||
<Loading />
|
||||
<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 key={record.id} className="rounded-lg bg-gray-50 p-4 mb-3 shadow-sm">
|
||||
<View className="flex justify-between items-start mb-3">
|
||||
<View>
|
||||
<Space>
|
||||
<Text className="font-semibold text-gray-800 mb-1">
|
||||
提现金额:¥{record.money}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-500">
|
||||
提现账户:{record.accountDisplay}
|
||||
</Text>
|
||||
</View>
|
||||
</Space>
|
||||
<Tag type={getStatusColor(record.applyStatus)}>
|
||||
{getStatusText(record.applyStatus)}
|
||||
</Tag>
|
||||
@@ -391,16 +465,17 @@ const DealerWithdraw: React.FC = () => {
|
||||
</View>
|
||||
))
|
||||
) : (
|
||||
<Empty description="暂无提现记录" />
|
||||
<Empty description="暂无提现记录"/>
|
||||
)}
|
||||
</View>
|
||||
</PullToRefresh>
|
||||
)
|
||||
}
|
||||
|
||||
if (!dealerUser) {
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
||||
<Loading />
|
||||
<Loading/>
|
||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||
</View>
|
||||
)
|
||||
@@ -408,7 +483,7 @@ const DealerWithdraw: React.FC = () => {
|
||||
|
||||
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>
|
||||
|
||||
4
src/doctor/apply/add.config.ts
Normal file
4
src/doctor/apply/add.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '医生入驻申请通道',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
191
src/doctor/apply/add.tsx
Normal file
191
src/doctor/apply/add.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
import {useEffect, useState, useRef} from "react";
|
||||
import {Loading, CellGroup, Cell, Input, Form} from '@nutui/nutui-react-taro'
|
||||
import {Edit} from '@nutui/icons-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {View} from '@tarojs/components'
|
||||
import FixedButton from "@/components/FixedButton";
|
||||
import {useUser} from "@/hooks/useUser";
|
||||
import {ShopDealerApply} from "@/api/shop/shopDealerApply/model";
|
||||
import {
|
||||
addShopDealerApply,
|
||||
pageShopDealerApply,
|
||||
updateShopDealerApply
|
||||
} from "@/api/shop/shopDealerApply";
|
||||
import {getShopDealerUser} from "@/api/shop/shopDealerUser";
|
||||
|
||||
const AddUserAddress = () => {
|
||||
const {user} = useUser()
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [FormData, setFormData] = useState<ShopDealerApply>()
|
||||
const formRef = useRef<any>(null)
|
||||
const [isEditMode, setIsEditMode] = useState<boolean>(false)
|
||||
const [existingApply, setExistingApply] = useState<ShopDealerApply | null>(null)
|
||||
|
||||
// 获取审核状态文字
|
||||
const getApplyStatusText = (status?: number) => {
|
||||
switch (status) {
|
||||
case 10:
|
||||
return '待审核'
|
||||
case 20:
|
||||
return '审核通过'
|
||||
case 30:
|
||||
return '驳回'
|
||||
default:
|
||||
return '未知状态'
|
||||
}
|
||||
}
|
||||
|
||||
const reload = async () => {
|
||||
// 判断用户是否登录
|
||||
if (!user?.userId) {
|
||||
return false;
|
||||
}
|
||||
// 查询当前用户ID是否已有申请记录
|
||||
try {
|
||||
const res = await pageShopDealerApply({userId: user?.userId});
|
||||
if (res && res.count > 0) {
|
||||
setIsEditMode(true);
|
||||
setExistingApply(res.list[0]);
|
||||
// 如果有记录,填充表单数据
|
||||
setFormData(res.list[0]);
|
||||
setLoading(false)
|
||||
} else {
|
||||
setIsEditMode(false);
|
||||
setExistingApply(null);
|
||||
setLoading(false)
|
||||
}
|
||||
} catch (error) {
|
||||
setLoading(true)
|
||||
console.error('查询申请记录失败:', error);
|
||||
setIsEditMode(false);
|
||||
setExistingApply(null);
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const submitSucceed = async (values: any) => {
|
||||
try {
|
||||
|
||||
// 准备提交的数据
|
||||
const submitData = {
|
||||
...values,
|
||||
realName: values.realName || user?.nickname,
|
||||
mobile: user?.phone,
|
||||
refereeId: values.refereeId || FormData?.refereeId,
|
||||
applyStatus: 10,
|
||||
auditTime: undefined
|
||||
};
|
||||
await getShopDealerUser(submitData.refereeId);
|
||||
|
||||
// 如果是编辑模式,添加现有申请的id
|
||||
if (isEditMode && existingApply?.applyId) {
|
||||
submitData.applyId = existingApply.applyId;
|
||||
}
|
||||
|
||||
// 执行新增或更新操作
|
||||
if (isEditMode) {
|
||||
await updateShopDealerApply(submitData);
|
||||
} else {
|
||||
await addShopDealerApply(submitData);
|
||||
}
|
||||
|
||||
Taro.showToast({
|
||||
title: `${isEditMode ? '提交' : '提交'}成功`,
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
Taro.navigateBack();
|
||||
}, 1000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('验证邀请人失败:', error);
|
||||
return Taro.showToast({
|
||||
title: '邀请人ID不存在',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 处理固定按钮点击事件
|
||||
const handleFixedButtonClick = () => {
|
||||
// 触发表单提交
|
||||
formRef.current?.submit();
|
||||
};
|
||||
|
||||
const submitFailed = (error: any) => {
|
||||
console.log(error, 'err...')
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reload().then(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}, [user?.userId]); // 依赖用户ID,当用户变化时重新加载
|
||||
|
||||
if (loading) {
|
||||
return <Loading className={'px-2'}>加载中</Loading>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form
|
||||
ref={formRef}
|
||||
divider
|
||||
initialValues={FormData}
|
||||
labelPosition="left"
|
||||
onFinish={(values) => submitSucceed(values)}
|
||||
onFinishFailed={(errors) => submitFailed(errors)}
|
||||
>
|
||||
<View className={'bg-gray-100 h-3'}></View>
|
||||
<CellGroup style={{padding: '4px 0'}}>
|
||||
<Form.Item name="realName" label="名称" initialValue={user?.nickname} required>
|
||||
<Input placeholder="经销商名称" maxLength={10}/>
|
||||
</Form.Item>
|
||||
<Form.Item name="mobile" label="手机号" initialValue={user?.mobile} required>
|
||||
<Input placeholder="请输入手机号" disabled={true} maxLength={11}/>
|
||||
</Form.Item>
|
||||
<Form.Item name="refereeId" label="邀请人ID" initialValue={FormData?.refereeId} required>
|
||||
<Input placeholder="邀请人ID"/>
|
||||
</Form.Item>
|
||||
</CellGroup>
|
||||
</Form>
|
||||
{/* 审核状态显示(仅在编辑模式下显示) */}
|
||||
{isEditMode && (
|
||||
<CellGroup>
|
||||
<Cell
|
||||
title={'审核状态'}
|
||||
extra={
|
||||
<span style={{
|
||||
color: FormData?.applyStatus === 20 ? '#52c41a' :
|
||||
FormData?.applyStatus === 30 ? '#ff4d4f' : '#faad14'
|
||||
}}>
|
||||
{getApplyStatusText(FormData?.applyStatus)}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
{FormData?.applyStatus === 20 && (
|
||||
<Cell title={'审核时间'} extra={FormData?.auditTime || '无'}/>
|
||||
)}
|
||||
{FormData?.applyStatus === 30 && (
|
||||
<Cell title={'驳回原因'} extra={FormData?.rejectReason || '无'}/>
|
||||
)}
|
||||
</CellGroup>
|
||||
)}
|
||||
|
||||
|
||||
{/* 底部浮动按钮 */}
|
||||
{(!isEditMode || FormData?.applyStatus === 10 || FormData?.applyStatus === 30) && (
|
||||
<FixedButton
|
||||
icon={<Edit/>}
|
||||
text={isEditMode ? '保存修改' : '提交申请'}
|
||||
disabled={FormData?.applyStatus === 10}
|
||||
onClick={handleFixedButtonClick}
|
||||
/>
|
||||
)}
|
||||
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddUserAddress;
|
||||
3
src/doctor/index.config.ts
Normal file
3
src/doctor/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '医生版'
|
||||
})
|
||||
0
src/doctor/index.scss
Normal file
0
src/doctor/index.scss
Normal file
295
src/doctor/index.tsx
Normal file
295
src/doctor/index.tsx
Normal file
@@ -0,0 +1,295 @@
|
||||
import React from 'react'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import {ConfigProvider, Button, Grid, Avatar} from '@nutui/nutui-react-taro'
|
||||
import {
|
||||
User,
|
||||
Shopping,
|
||||
Dongdong,
|
||||
ArrowRight,
|
||||
Purse,
|
||||
People
|
||||
} from '@nutui/icons-react-taro'
|
||||
import {useDealerUser} from '@/hooks/useDealerUser'
|
||||
import { useThemeStyles } from '@/hooks/useTheme'
|
||||
import {businessGradients, cardGradients, gradientUtils} from '@/styles/gradients'
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
const DealerIndex: React.FC = () => {
|
||||
const {
|
||||
dealerUser,
|
||||
error,
|
||||
refresh,
|
||||
} = useDealerUser()
|
||||
|
||||
// 使用主题样式
|
||||
const themeStyles = useThemeStyles()
|
||||
|
||||
// 导航到各个功能页面
|
||||
const navigateToPage = (url: string) => {
|
||||
Taro.navigateTo({url})
|
||||
}
|
||||
|
||||
// 格式化金额
|
||||
const formatMoney = (money?: string) => {
|
||||
if (!money) return '0.00'
|
||||
return parseFloat(money).toFixed(2)
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time?: string) => {
|
||||
if (!time) return '-'
|
||||
return new Date(time).toLocaleDateString()
|
||||
}
|
||||
|
||||
// 获取用户主题
|
||||
const userTheme = gradientUtils.getThemeByUserId(dealerUser?.userId)
|
||||
|
||||
// 获取渐变背景
|
||||
const getGradientBackground = (themeColor?: string) => {
|
||||
if (themeColor) {
|
||||
const darkerColor = gradientUtils.adjustColorBrightness(themeColor, -30)
|
||||
return gradientUtils.createGradient(themeColor, darkerColor)
|
||||
}
|
||||
return userTheme.background
|
||||
}
|
||||
|
||||
console.log(getGradientBackground(),'getGradientBackground()')
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<View className="p-4">
|
||||
<View className="bg-red-50 border border-red-200 rounded-lg p-4 mb-4">
|
||||
<Text className="text-red-600">{error}</Text>
|
||||
</View>
|
||||
<Button type="primary" onClick={refresh}>
|
||||
重试
|
||||
</Button>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="bg-gray-100 min-h-screen">
|
||||
<View>
|
||||
{/*头部信息*/}
|
||||
{dealerUser && (
|
||||
<View className="px-4 py-6 relative overflow-hidden" style={themeStyles.primaryBackground}>
|
||||
{/* 装饰性背景元素 - 小程序兼容版本 */}
|
||||
<View className="absolute w-32 h-32 rounded-full" style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
top: '-16px',
|
||||
right: '-16px'
|
||||
}}></View>
|
||||
<View className="absolute w-24 h-24 rounded-full" style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.08)',
|
||||
bottom: '-12px',
|
||||
left: '-12px'
|
||||
}}></View>
|
||||
<View className="absolute w-16 h-16 rounded-full" style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||
top: '60px',
|
||||
left: '120px'
|
||||
}}></View>
|
||||
<View className="flex items-center justify-between relative z-10 mb-4">
|
||||
<Avatar
|
||||
size="50"
|
||||
src={dealerUser?.qrcode}
|
||||
icon={<User/>}
|
||||
className="mr-4"
|
||||
style={{
|
||||
border: '2px solid rgba(255, 255, 255, 0.3)'
|
||||
}}
|
||||
/>
|
||||
<View className="flex-1 flex-col">
|
||||
<View className="text-white text-lg font-bold mb-1" style={{
|
||||
}}>
|
||||
{dealerUser?.realName || '分销商'}
|
||||
</View>
|
||||
<View className="text-sm" style={{
|
||||
color: 'rgba(255, 255, 255, 0.8)'
|
||||
}}>
|
||||
ID: {dealerUser.userId} | 推荐人: {dealerUser.refereeId || '无'}
|
||||
</View>
|
||||
</View>
|
||||
<View className="text-right hidden">
|
||||
<Text className="text-xs" style={{
|
||||
color: 'rgba(255, 255, 255, 0.9)'
|
||||
}}>加入时间</Text>
|
||||
<Text className="text-xs" style={{
|
||||
color: 'rgba(255, 255, 255, 0.7)'
|
||||
}}>
|
||||
{formatTime(dealerUser.createTime)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 佣金统计卡片 */}
|
||||
{dealerUser && (
|
||||
<View className="mx-4 -mt-6 rounded-xl p-4 relative z-10" style={cardGradients.elevated}>
|
||||
<View className="mb-4">
|
||||
<Text className="font-semibold text-gray-800">佣金统计</Text>
|
||||
</View>
|
||||
<View className="grid grid-cols-3 gap-4">
|
||||
<View className="text-center p-3 rounded-lg" style={{
|
||||
background: businessGradients.money.available
|
||||
}}>
|
||||
<Text className="text-2xl font-bold mb-1 text-white">
|
||||
¥{formatMoney(dealerUser.money)}
|
||||
</Text>
|
||||
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>可提现</Text>
|
||||
</View>
|
||||
<View className="text-center p-3 rounded-lg" style={{
|
||||
background: businessGradients.money.frozen
|
||||
}}>
|
||||
<Text className="text-2xl font-bold mb-1 text-white">
|
||||
¥{formatMoney(dealerUser.freezeMoney)}
|
||||
</Text>
|
||||
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>冻结中</Text>
|
||||
</View>
|
||||
<View className="text-center p-3 rounded-lg" style={{
|
||||
background: businessGradients.money.total
|
||||
}}>
|
||||
<Text className="text-2xl font-bold mb-1 text-white">
|
||||
¥{formatMoney(dealerUser.totalMoney)}
|
||||
</Text>
|
||||
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>累计收益</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 团队统计 */}
|
||||
{dealerUser && (
|
||||
<View className="bg-white mx-4 mt-4 rounded-xl p-4 hidden">
|
||||
<View className="flex items-center justify-between mb-4">
|
||||
<Text className="font-semibold text-gray-800">我的邀请</Text>
|
||||
<View
|
||||
className="text-gray-400 text-sm flex items-center"
|
||||
onClick={() => navigateToPage('/dealer/team/index')}
|
||||
>
|
||||
<Text>查看详情</Text>
|
||||
<ArrowRight size="12"/>
|
||||
</View>
|
||||
</View>
|
||||
<View className="grid grid-cols-3 gap-4">
|
||||
<View className="text-center grid">
|
||||
<Text className="text-xl font-bold text-purple-500 mb-1">
|
||||
{dealerUser.firstNum || 0}
|
||||
</Text>
|
||||
<Text className="text-xs text-gray-500">一级成员</Text>
|
||||
</View>
|
||||
<View className="text-center grid">
|
||||
<Text className="text-xl font-bold text-indigo-500 mb-1">
|
||||
{dealerUser.secondNum || 0}
|
||||
</Text>
|
||||
<Text className="text-xs text-gray-500">二级成员</Text>
|
||||
</View>
|
||||
<View className="text-center grid">
|
||||
<Text className="text-xl font-bold text-pink-500 mb-1">
|
||||
{dealerUser.thirdNum || 0}
|
||||
</Text>
|
||||
<Text className="text-xs text-gray-500">三级成员</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 功能导航 */}
|
||||
<View className="bg-white mx-4 mt-4 rounded-xl p-4">
|
||||
<View className="font-semibold mb-4 text-gray-800">分销工具</View>
|
||||
<ConfigProvider>
|
||||
<Grid
|
||||
columns={4}
|
||||
className="no-border-grid"
|
||||
style={{
|
||||
'--nutui-grid-border-color': 'transparent',
|
||||
'--nutui-grid-item-border-width': '0px',
|
||||
border: 'none'
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
<Grid.Item text="分销订单" onClick={() => navigateToPage('/dealer/orders/index')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Shopping color="#3b82f6" size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text={'提现申请'} onClick={() => navigateToPage('/dealer/withdraw/index')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-green-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Purse color="#10b981" size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text={'我的邀请'} onClick={() => navigateToPage('/dealer/team/index')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-purple-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<People color="#8b5cf6" size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text={'我的邀请码'} onClick={() => navigateToPage('/dealer/qrcode/index')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-orange-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Dongdong color="#f59e0b" size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
</Grid>
|
||||
|
||||
{/* 第二行功能 */}
|
||||
{/*<Grid*/}
|
||||
{/* columns={4}*/}
|
||||
{/* className="no-border-grid mt-4"*/}
|
||||
{/* style={{*/}
|
||||
{/* '--nutui-grid-border-color': 'transparent',*/}
|
||||
{/* '--nutui-grid-item-border-width': '0px',*/}
|
||||
{/* border: 'none'*/}
|
||||
{/* } as React.CSSProperties}*/}
|
||||
{/*>*/}
|
||||
{/* <Grid.Item text={'邀请统计'} onClick={() => navigateToPage('/dealer/invite-stats/index')}>*/}
|
||||
{/* <View className="text-center">*/}
|
||||
{/* <View className="w-12 h-12 bg-indigo-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
|
||||
{/* <Presentation color="#6366f1" size="20"/>*/}
|
||||
{/* </View>*/}
|
||||
{/* </View>*/}
|
||||
{/* </Grid.Item>*/}
|
||||
|
||||
{/* /!* 预留其他功能位置 *!/*/}
|
||||
{/* <Grid.Item text={''}>*/}
|
||||
{/* <View className="text-center">*/}
|
||||
{/* <View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
|
||||
{/* </View>*/}
|
||||
{/* </View>*/}
|
||||
{/* </Grid.Item>*/}
|
||||
|
||||
{/* <Grid.Item text={''}>*/}
|
||||
{/* <View className="text-center">*/}
|
||||
{/* <View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
|
||||
{/* </View>*/}
|
||||
{/* </View>*/}
|
||||
{/* </Grid.Item>*/}
|
||||
|
||||
{/* <Grid.Item text={''}>*/}
|
||||
{/* <View className="text-center">*/}
|
||||
{/* <View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
|
||||
{/* </View>*/}
|
||||
{/* </View>*/}
|
||||
{/* </Grid.Item>*/}
|
||||
{/*</Grid>*/}
|
||||
</ConfigProvider>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 底部安全区域 */}
|
||||
<View className="h-20"></View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default DealerIndex
|
||||
157
src/doctor/info.tsx
Normal file
157
src/doctor/info.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
import React from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Button, Cell, CellGroup, Tag } from '@nutui/nutui-react-taro'
|
||||
import { useDealerUser } from '@/hooks/useDealerUser'
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
const DealerInfo: React.FC = () => {
|
||||
const {
|
||||
dealerUser,
|
||||
loading,
|
||||
error,
|
||||
refresh,
|
||||
} = useDealerUser()
|
||||
|
||||
// 跳转到申请页面
|
||||
const navigateToApply = () => {
|
||||
Taro.navigateTo({
|
||||
url: '/pages/dealer/apply/add'
|
||||
})
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<View className="p-4">
|
||||
<View className="bg-red-50 border border-red-200 rounded-lg p-4 mb-4">
|
||||
<Text className="text-red-600">{error}</Text>
|
||||
</View>
|
||||
<Button type="primary" onClick={refresh}>
|
||||
重试
|
||||
</Button>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen">
|
||||
{/* 页面标题 */}
|
||||
<View className="bg-white px-4 py-3 border-b border-gray-100">
|
||||
<Text className="text-lg font-bold text-center">
|
||||
经销商信息
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{!dealerUser ? (
|
||||
// 非经销商状态
|
||||
<View className="bg-white mx-4 mt-4 rounded-lg p-6">
|
||||
<View className="text-center py-8">
|
||||
<Text className="text-gray-500 mb-4">您还不是经销商</Text>
|
||||
<Text className="text-sm text-gray-400 mb-6">
|
||||
成为经销商后可享受专属价格和佣金收益
|
||||
</Text>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
onClick={navigateToApply}
|
||||
>
|
||||
申请成为经销商
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
// 经销商信息展示
|
||||
<View>
|
||||
{/* 状态卡片 */}
|
||||
<View className="bg-white mx-4 mt-4 rounded-lg p-4">
|
||||
<View className="flex items-center justify-between mb-4">
|
||||
<Text className="text-lg font-semibold">经销商状态</Text>
|
||||
<Tag>
|
||||
{dealerUser.realName}
|
||||
</Tag>
|
||||
</View>
|
||||
|
||||
{/* 基本信息 */}
|
||||
<CellGroup>
|
||||
<Cell
|
||||
title="经销商ID"
|
||||
extra={dealerUser.userId || '-'}
|
||||
/>
|
||||
<Cell
|
||||
title="refereeId"
|
||||
extra={dealerUser.refereeId || '-'}
|
||||
/>
|
||||
<Cell
|
||||
title="成为经销商时间"
|
||||
extra={
|
||||
dealerUser.money
|
||||
}
|
||||
/>
|
||||
|
||||
</CellGroup>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<View className="mt-6 gap-2">
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
loading={loading}
|
||||
>
|
||||
检查状态
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 经销商权益 */}
|
||||
<View className="bg-white mx-4 mt-4 rounded-lg p-4">
|
||||
<Text className="font-semibold mb-3">经销商权益</Text>
|
||||
<View className="gap-2">
|
||||
<Text className="text-sm text-gray-600">
|
||||
• 享受经销商专属价格
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-600">
|
||||
• 获得推广佣金收益
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-600">
|
||||
• 优先获得新品信息
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-600">
|
||||
• 专属客服支持
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 佣金统计 */}
|
||||
<View className="bg-white mx-4 mt-4 rounded-lg p-4">
|
||||
<Text className="font-semibold mb-3">佣金统计</Text>
|
||||
<View className="grid grid-cols-3 gap-4">
|
||||
<View className="text-center">
|
||||
<Text className="text-lg font-bold text-blue-600">0</Text>
|
||||
<Text className="text-sm text-gray-500">今日佣金</Text>
|
||||
</View>
|
||||
<View className="text-center">
|
||||
<Text className="text-lg font-bold text-green-600">0</Text>
|
||||
<Text className="text-sm text-gray-500">本月佣金</Text>
|
||||
</View>
|
||||
<View className="text-center">
|
||||
<Text className="text-lg font-bold text-orange-600">0</Text>
|
||||
<Text className="text-sm text-gray-500">累计佣金</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 刷新按钮 */}
|
||||
<View className="text-center py-4">
|
||||
<Text
|
||||
className="text-blue-500 text-sm"
|
||||
onClick={refresh}
|
||||
>
|
||||
点击刷新数据
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default DealerInfo
|
||||
7
src/doctor/invite-stats/index.config.ts
Normal file
7
src/doctor/invite-stats/index.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '邀请统计',
|
||||
navigationBarBackgroundColor: '#ffffff',
|
||||
navigationBarTextStyle: 'black',
|
||||
backgroundColor: '#f5f5f5',
|
||||
enablePullDownRefresh: true
|
||||
})
|
||||
336
src/doctor/invite-stats/index.tsx
Normal file
336
src/doctor/invite-stats/index.tsx
Normal file
@@ -0,0 +1,336 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import {
|
||||
Empty,
|
||||
Tabs,
|
||||
Loading,
|
||||
PullToRefresh,
|
||||
Card,
|
||||
} from '@nutui/nutui-react-taro'
|
||||
import {
|
||||
User,
|
||||
ArrowUp,
|
||||
Calendar,
|
||||
Share,
|
||||
Target,
|
||||
Gift
|
||||
} from '@nutui/icons-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useDealerUser } from '@/hooks/useDealerUser'
|
||||
import {
|
||||
getInviteStats,
|
||||
getMyInviteRecords,
|
||||
getInviteRanking
|
||||
} from '@/api/invite'
|
||||
import type {
|
||||
InviteStats,
|
||||
InviteRecord
|
||||
} from '@/api/invite'
|
||||
import { businessGradients } from '@/styles/gradients'
|
||||
import {InviteRanking} from "@/api/invite/model";
|
||||
|
||||
const InviteStatsPage: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState<string>('stats')
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [inviteStats, setInviteStats] = useState<InviteStats | null>(null)
|
||||
const [inviteRecords, setInviteRecords] = useState<InviteRecord[]>([])
|
||||
const [ranking, setRanking] = useState<InviteRanking[]>([])
|
||||
const [dateRange, setDateRange] = useState<string>('month')
|
||||
const { dealerUser } = useDealerUser()
|
||||
|
||||
// 获取邀请统计数据
|
||||
const fetchInviteStats = useCallback(async () => {
|
||||
if (!dealerUser?.userId) return
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
const stats = await getInviteStats(dealerUser.userId)
|
||||
stats && setInviteStats(stats)
|
||||
} catch (error) {
|
||||
console.error('获取邀请统计失败:', error)
|
||||
Taro.showToast({
|
||||
title: '获取统计数据失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [dealerUser?.userId])
|
||||
|
||||
// 获取邀请记录
|
||||
const fetchInviteRecords = useCallback(async () => {
|
||||
if (!dealerUser?.userId) return
|
||||
|
||||
try {
|
||||
const result = await getMyInviteRecords({
|
||||
page: 1,
|
||||
limit: 50,
|
||||
inviterId: dealerUser.userId
|
||||
})
|
||||
setInviteRecords(result?.list || [])
|
||||
} catch (error) {
|
||||
console.error('获取邀请记录失败:', error)
|
||||
}
|
||||
}, [dealerUser?.userId])
|
||||
|
||||
// 获取邀请排行榜
|
||||
const fetchRanking = useCallback(async () => {
|
||||
try {
|
||||
const result = await getInviteRanking({
|
||||
limit: 20,
|
||||
period: dateRange as 'day' | 'week' | 'month'
|
||||
})
|
||||
setRanking(result || [])
|
||||
} catch (error) {
|
||||
console.error('获取排行榜失败:', error)
|
||||
}
|
||||
}, [dateRange])
|
||||
|
||||
// 刷新数据
|
||||
const handleRefresh = async () => {
|
||||
await Promise.all([
|
||||
fetchInviteStats(),
|
||||
fetchInviteRecords(),
|
||||
fetchRanking()
|
||||
])
|
||||
}
|
||||
|
||||
// 初始化数据
|
||||
useEffect(() => {
|
||||
if (dealerUser?.userId) {
|
||||
fetchInviteStats().then()
|
||||
fetchInviteRecords().then()
|
||||
fetchRanking().then()
|
||||
}
|
||||
}, [fetchInviteStats, fetchInviteRecords, fetchRanking])
|
||||
|
||||
// 获取状态显示文本
|
||||
const getStatusText = (status: string) => {
|
||||
const statusMap: Record<string, string> = {
|
||||
'pending': '待注册',
|
||||
'registered': '已注册',
|
||||
'activated': '已激活'
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
'pending': 'text-orange-500',
|
||||
'registered': 'text-blue-500',
|
||||
'activated': 'text-green-500'
|
||||
}
|
||||
return colorMap[status] || 'text-gray-500'
|
||||
}
|
||||
|
||||
// 渲染统计概览
|
||||
const renderStatsOverview = () => (
|
||||
<View className="px-4 space-y-4">
|
||||
{/* 核心数据卡片 */}
|
||||
<Card className="bg-white rounded-2xl shadow-sm">
|
||||
<View className="p-4">
|
||||
<Text className="text-lg font-semibold text-gray-800 mb-4">邀请概览</Text>
|
||||
{loading ? (
|
||||
<View className="flex items-center justify-center py-8">
|
||||
<Loading />
|
||||
</View>
|
||||
) : inviteStats ? (
|
||||
<View className="grid grid-cols-2 gap-4">
|
||||
<View className="text-center p-4 bg-blue-50 rounded-xl">
|
||||
<ArrowUp size="24" className="text-blue-500 mx-auto mb-2" />
|
||||
<Text className="text-2xl font-bold text-blue-600">
|
||||
{inviteStats.totalInvites || 0}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-600">总邀请数</Text>
|
||||
</View>
|
||||
|
||||
<View className="text-center p-4 bg-green-50 rounded-xl">
|
||||
<User size="24" className="text-green-500 mx-auto mb-2" />
|
||||
<Text className="text-2xl font-bold text-green-600">
|
||||
{inviteStats.successfulRegistrations || 0}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-600">成功注册</Text>
|
||||
</View>
|
||||
|
||||
<View className="text-center p-4 bg-purple-50 rounded-xl">
|
||||
<Target size="24" className="text-purple-500 mx-auto mb-2" />
|
||||
<Text className="text-2xl font-bold text-purple-600">
|
||||
{inviteStats.conversionRate ? `${(inviteStats.conversionRate * 100).toFixed(1)}%` : '0%'}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-600">转化率</Text>
|
||||
</View>
|
||||
|
||||
<View className="text-center p-4 bg-orange-50 rounded-xl">
|
||||
<Calendar size="24" className="text-orange-500 mx-auto mb-2" />
|
||||
<Text className="text-2xl font-bold text-orange-600">
|
||||
{inviteStats.todayInvites || 0}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-600">今日邀请</Text>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<View className="text-center py-8">
|
||||
<Text className="text-gray-500">暂无统计数据</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</Card>
|
||||
|
||||
{/* 邀请来源分析 */}
|
||||
{inviteStats?.sourceStats && inviteStats.sourceStats.length > 0 && (
|
||||
<Card className="bg-white rounded-2xl shadow-sm">
|
||||
<View className="p-4">
|
||||
<Text className="text-lg font-semibold text-gray-800 mb-4">邀请来源分析</Text>
|
||||
<View className="space-y-3">
|
||||
{inviteStats.sourceStats.map((source, index) => (
|
||||
<View key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||
<View className="flex items-center">
|
||||
<Share size="16" className="text-blue-500 mr-2" />
|
||||
<Text className="font-medium text-gray-800">{source.source}</Text>
|
||||
</View>
|
||||
<View className="text-right">
|
||||
<Text className="text-lg font-bold text-gray-800">{source.count}</Text>
|
||||
<Text className="text-sm text-gray-500">
|
||||
转化率 {source.conversionRate ? `${(source.conversionRate * 100).toFixed(1)}%` : '0%'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</Card>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
|
||||
// 渲染邀请记录
|
||||
const renderInviteRecords = () => (
|
||||
<View className="px-4">
|
||||
{inviteRecords.length > 0 ? (
|
||||
<View className="space-y-3">
|
||||
{inviteRecords.map((record, index) => (
|
||||
<Card key={record.id || index} className="bg-white rounded-xl shadow-sm">
|
||||
<View className="p-4">
|
||||
<View className="flex items-center justify-between mb-2">
|
||||
<Text className="font-medium text-gray-800">
|
||||
{record.inviteeName || `用户${record.inviteeId}`}
|
||||
</Text>
|
||||
<Text className={`text-sm font-medium ${getStatusColor(record.status || 'pending')}`}>
|
||||
{getStatusText(record.status || 'pending')}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className="flex items-center justify-between text-sm text-gray-500">
|
||||
<Text>来源: {record.source || '未知'}</Text>
|
||||
<Text>{record.inviteTime ? new Date(record.inviteTime).toLocaleDateString() : ''}</Text>
|
||||
</View>
|
||||
|
||||
{record.registerTime && (
|
||||
<Text className="text-xs text-green-600 mt-1">
|
||||
注册时间: {new Date(record.registerTime).toLocaleString()}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</Card>
|
||||
))}
|
||||
</View>
|
||||
) : (
|
||||
<Empty description="暂无邀请记录" />
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
|
||||
// 渲染排行榜
|
||||
const renderRanking = () => (
|
||||
<View className="px-4">
|
||||
<View className="mb-4">
|
||||
<Tabs value={dateRange} onChange={() => setDateRange}>
|
||||
<Tabs.TabPane title="日榜" value="day" />
|
||||
<Tabs.TabPane title="周榜" value="week" />
|
||||
<Tabs.TabPane title="月榜" value="month" />
|
||||
</Tabs>
|
||||
</View>
|
||||
|
||||
{ranking.length > 0 ? (
|
||||
<View className="space-y-3">
|
||||
{ranking.map((item, index) => (
|
||||
<Card key={item.inviterId} className="bg-white rounded-xl shadow-sm">
|
||||
<View className="p-4 flex items-center">
|
||||
<View className="flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 mr-3">
|
||||
{index < 3 ? (
|
||||
<Gift size="16" className={index === 0 ? 'text-yellow-500' : index === 1 ? 'text-gray-400' : 'text-orange-400'} />
|
||||
) : (
|
||||
<Text className="text-sm font-bold text-gray-600">{index + 1}</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View className="flex-1">
|
||||
<Text className="font-medium text-gray-800">{item.inviterName}</Text>
|
||||
<Text className="text-sm text-gray-500">
|
||||
邀请 {item.inviteCount} 人 · 转化率 {item.conversionRate ? `${(item.conversionRate * 100).toFixed(1)}%` : '0%'}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Text className="text-lg font-bold text-blue-600">{item.successCount}</Text>
|
||||
</View>
|
||||
</Card>
|
||||
))}
|
||||
</View>
|
||||
) : (
|
||||
<Empty description="暂无排行数据" />
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
|
||||
if (!dealerUser) {
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
||||
<Loading />
|
||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen">
|
||||
{/* 头部 */}
|
||||
<View className="rounded-b-3xl p-6 mb-6 text-white relative overflow-hidden" style={{
|
||||
background: businessGradients.dealer.header
|
||||
}}>
|
||||
<View className="absolute w-32 h-32 rounded-full" style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
top: '-16px',
|
||||
right: '-16px'
|
||||
}}></View>
|
||||
|
||||
<View className="relative z-10">
|
||||
<Text className="text-2xl font-bold mb-2 text-white">邀请统计</Text>
|
||||
<Text className="text-white text-opacity-80">
|
||||
查看您的邀请效果和推广数据
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 标签页 */}
|
||||
<View className="px-4 mb-4">
|
||||
<Tabs value={activeTab} onChange={() => setActiveTab}>
|
||||
<Tabs.TabPane title="统计概览" value="stats" />
|
||||
<Tabs.TabPane title="邀请记录" value="records" />
|
||||
<Tabs.TabPane title="排行榜" value="ranking" />
|
||||
</Tabs>
|
||||
</View>
|
||||
|
||||
{/* 内容区域 */}
|
||||
<PullToRefresh onRefresh={handleRefresh}>
|
||||
<View className="pb-6">
|
||||
{activeTab === 'stats' && renderStatsOverview()}
|
||||
{activeTab === 'records' && renderInviteRecords()}
|
||||
{activeTab === 'ranking' && renderRanking()}
|
||||
</View>
|
||||
</PullToRefresh>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default InviteStatsPage
|
||||
3
src/doctor/orders/index.config.ts
Normal file
3
src/doctor/orders/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '分销订单'
|
||||
})
|
||||
392
src/doctor/orders/index.tsx
Normal file
392
src/doctor/orders/index.tsx
Normal file
@@ -0,0 +1,392 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Empty, Tabs, Tag, PullToRefresh, Loading } from '@nutui/nutui-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { pageShopDealerOrder } from '@/api/shop/shopDealerOrder'
|
||||
import { useDealerUser } from '@/hooks/useDealerUser'
|
||||
import type { ShopDealerOrder } from '@/api/shop/shopDealerOrder/model'
|
||||
|
||||
interface OrderWithDetails extends ShopDealerOrder {
|
||||
orderNo?: string
|
||||
customerName?: string
|
||||
totalCommission?: string
|
||||
// 当前用户在此订单中的层级和佣金
|
||||
userLevel?: 1 | 2 | 3
|
||||
userCommission?: string
|
||||
}
|
||||
|
||||
const DealerOrders: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState<string>('0')
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [orders, setOrders] = useState<OrderWithDetails[]>([])
|
||||
const [statistics, setStatistics] = useState({
|
||||
totalOrders: 0,
|
||||
totalCommission: '0.00',
|
||||
pendingCommission: '0.00',
|
||||
// 分层统计
|
||||
level1: { orders: 0, commission: '0.00' },
|
||||
level2: { orders: 0, commission: '0.00' },
|
||||
level3: { orders: 0, commission: '0.00' }
|
||||
})
|
||||
|
||||
const { dealerUser } = useDealerUser()
|
||||
|
||||
// 获取订单数据 - 查询当前用户作为各层级分销商的所有订单
|
||||
const fetchOrders = useCallback(async () => {
|
||||
if (!dealerUser?.userId) return
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
|
||||
// 并行查询三个层级的订单
|
||||
const [level1Result, level2Result, level3Result] = await Promise.all([
|
||||
// 一级分销商订单
|
||||
pageShopDealerOrder({
|
||||
page: 1,
|
||||
limit: 100,
|
||||
firstUserId: dealerUser.userId
|
||||
}),
|
||||
// 二级分销商订单
|
||||
pageShopDealerOrder({
|
||||
page: 1,
|
||||
limit: 100,
|
||||
secondUserId: dealerUser.userId
|
||||
}),
|
||||
// 三级分销商订单
|
||||
pageShopDealerOrder({
|
||||
page: 1,
|
||||
limit: 100,
|
||||
thirdUserId: dealerUser.userId
|
||||
})
|
||||
])
|
||||
|
||||
const allOrders: OrderWithDetails[] = []
|
||||
const stats = {
|
||||
totalOrders: 0,
|
||||
totalCommission: '0.00',
|
||||
pendingCommission: '0.00',
|
||||
level1: { orders: 0, commission: '0.00' },
|
||||
level2: { orders: 0, commission: '0.00' },
|
||||
level3: { orders: 0, commission: '0.00' }
|
||||
}
|
||||
|
||||
// 处理一级分销订单
|
||||
if (level1Result?.list) {
|
||||
const level1Orders = level1Result.list.map(order => ({
|
||||
...order,
|
||||
orderNo: `DD${order.orderId}`,
|
||||
customerName: `用户${order.userId}`,
|
||||
userLevel: 1 as const,
|
||||
userCommission: order.firstMoney || '0.00',
|
||||
totalCommission: (
|
||||
parseFloat(order.firstMoney || '0') +
|
||||
parseFloat(order.secondMoney || '0') +
|
||||
parseFloat(order.thirdMoney || '0')
|
||||
).toFixed(2)
|
||||
}))
|
||||
|
||||
allOrders.push(...level1Orders)
|
||||
stats.level1.orders = level1Orders.length
|
||||
stats.level1.commission = level1Orders.reduce((sum, order) =>
|
||||
sum + parseFloat(order.userCommission || '0'), 0
|
||||
).toFixed(2)
|
||||
}
|
||||
|
||||
// 处理二级分销订单
|
||||
if (level2Result?.list) {
|
||||
const level2Orders = level2Result.list.map(order => ({
|
||||
...order,
|
||||
orderNo: `DD${order.orderId}`,
|
||||
customerName: `用户${order.userId}`,
|
||||
userLevel: 2 as const,
|
||||
userCommission: order.secondMoney || '0.00',
|
||||
totalCommission: (
|
||||
parseFloat(order.firstMoney || '0') +
|
||||
parseFloat(order.secondMoney || '0') +
|
||||
parseFloat(order.thirdMoney || '0')
|
||||
).toFixed(2)
|
||||
}))
|
||||
|
||||
allOrders.push(...level2Orders)
|
||||
stats.level2.orders = level2Orders.length
|
||||
stats.level2.commission = level2Orders.reduce((sum, order) =>
|
||||
sum + parseFloat(order.userCommission || '0'), 0
|
||||
).toFixed(2)
|
||||
}
|
||||
|
||||
// 处理三级分销订单
|
||||
if (level3Result?.list) {
|
||||
const level3Orders = level3Result.list.map(order => ({
|
||||
...order,
|
||||
orderNo: `DD${order.orderId}`,
|
||||
customerName: `用户${order.userId}`,
|
||||
userLevel: 3 as const,
|
||||
userCommission: order.thirdMoney || '0.00',
|
||||
totalCommission: (
|
||||
parseFloat(order.firstMoney || '0') +
|
||||
parseFloat(order.secondMoney || '0') +
|
||||
parseFloat(order.thirdMoney || '0')
|
||||
).toFixed(2)
|
||||
}))
|
||||
|
||||
allOrders.push(...level3Orders)
|
||||
stats.level3.orders = level3Orders.length
|
||||
stats.level3.commission = level3Orders.reduce((sum, order) =>
|
||||
sum + parseFloat(order.userCommission || '0'), 0
|
||||
).toFixed(2)
|
||||
}
|
||||
|
||||
// 去重(同一个订单可能在多个层级中出现)
|
||||
const uniqueOrders = allOrders.filter((order, index, self) =>
|
||||
index === self.findIndex(o => o.orderId === order.orderId)
|
||||
)
|
||||
|
||||
// 计算总统计
|
||||
stats.totalOrders = uniqueOrders.length
|
||||
stats.totalCommission = (
|
||||
parseFloat(stats.level1.commission) +
|
||||
parseFloat(stats.level2.commission) +
|
||||
parseFloat(stats.level3.commission)
|
||||
).toFixed(2)
|
||||
stats.pendingCommission = allOrders
|
||||
.filter(order => order.isSettled === 0)
|
||||
.reduce((sum, order) => sum + parseFloat(order.userCommission || '0'), 0)
|
||||
.toFixed(2)
|
||||
|
||||
setOrders(uniqueOrders)
|
||||
setStatistics(stats)
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取分销订单失败:', error)
|
||||
Taro.showToast({
|
||||
title: '获取订单失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [dealerUser?.userId])
|
||||
|
||||
// 刷新数据
|
||||
const handleRefresh = async () => {
|
||||
await fetchOrders()
|
||||
}
|
||||
|
||||
// 初始化加载数据
|
||||
useEffect(() => {
|
||||
if (dealerUser?.userId) {
|
||||
fetchOrders().then()
|
||||
}
|
||||
}, [fetchOrders])
|
||||
|
||||
const getStatusText = (isSettled?: number, isInvalid?: number) => {
|
||||
if (isInvalid === 1) return '已失效'
|
||||
if (isSettled === 1) return '已结算'
|
||||
return '待结算'
|
||||
}
|
||||
|
||||
const getStatusColor = (isSettled?: number, isInvalid?: number) => {
|
||||
if (isInvalid === 1) return 'danger'
|
||||
if (isSettled === 1) return 'success'
|
||||
return 'warning'
|
||||
}
|
||||
|
||||
const renderOrderItem = (order: OrderWithDetails) => (
|
||||
<View key={order.id} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
|
||||
<View className="flex justify-between items-start mb-3">
|
||||
<View>
|
||||
<Text className="font-semibold text-gray-800 mb-1">
|
||||
订单号:{order.orderNo}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-500">
|
||||
客户:{order.customerName}
|
||||
</Text>
|
||||
{/* 显示用户在此订单中的层级 */}
|
||||
<Text className="text-xs text-blue-500">
|
||||
{order.userLevel === 1 && '一级分销'}
|
||||
{order.userLevel === 2 && '二级分销'}
|
||||
{order.userLevel === 3 && '三级分销'}
|
||||
</Text>
|
||||
</View>
|
||||
<Tag type={getStatusColor(order.isSettled, order.isInvalid)}>
|
||||
{getStatusText(order.isSettled, order.isInvalid)}
|
||||
</Tag>
|
||||
</View>
|
||||
|
||||
<View className="flex justify-between items-center">
|
||||
<View>
|
||||
<Text className="text-sm text-gray-600">
|
||||
订单金额:¥{order.orderPrice || '0.00'}
|
||||
</Text>
|
||||
<Text className="text-sm text-orange-500 font-semibold">
|
||||
我的佣金:¥{order.userCommission}
|
||||
</Text>
|
||||
<Text className="text-xs text-gray-400">
|
||||
总佣金:¥{order.totalCommission}
|
||||
</Text>
|
||||
</View>
|
||||
<Text className="text-xs text-gray-400">
|
||||
{order.createTime}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
|
||||
// 根据状态和层级过滤订单
|
||||
const getFilteredOrders = (filter: string) => {
|
||||
switch (filter) {
|
||||
case '1': // 一级分销
|
||||
return orders.filter(order => order.userLevel === 1)
|
||||
case '2': // 二级分销
|
||||
return orders.filter(order => order.userLevel === 2)
|
||||
case '3': // 三级分销
|
||||
return orders.filter(order => order.userLevel === 3)
|
||||
case '4': // 待结算
|
||||
return orders.filter(order => order.isSettled === 0 && order.isInvalid === 0)
|
||||
case '5': // 已结算
|
||||
return orders.filter(order => order.isSettled === 1)
|
||||
case '6': // 已失效
|
||||
return orders.filter(order => order.isInvalid === 1)
|
||||
default: // 全部
|
||||
return orders
|
||||
}
|
||||
}
|
||||
|
||||
if (!dealerUser) {
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
||||
<Loading />
|
||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen">
|
||||
{/* 统计卡片 */}
|
||||
<View className="bg-white p-4 mb-4">
|
||||
{/* 总体统计 */}
|
||||
<View className="grid grid-cols-3 gap-4 mb-4">
|
||||
<View className="text-center">
|
||||
<Text className="text-lg font-bold text-blue-500">{statistics.totalOrders}</Text>
|
||||
<Text className="text-xs text-gray-500">总订单</Text>
|
||||
</View>
|
||||
<View className="text-center">
|
||||
<Text className="text-lg font-bold text-green-500">¥{statistics.totalCommission}</Text>
|
||||
<Text className="text-xs text-gray-500">总佣金</Text>
|
||||
</View>
|
||||
<View className="text-center">
|
||||
<Text className="text-lg font-bold text-orange-500">¥{statistics.pendingCommission}</Text>
|
||||
<Text className="text-xs text-gray-500">待结算</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 分层统计 */}
|
||||
<View className="border-t pt-3">
|
||||
<Text className="text-sm text-gray-600 mb-2">分层统计</Text>
|
||||
<View className="grid grid-cols-3 gap-2">
|
||||
<View className="text-center bg-gray-50 rounded p-2">
|
||||
<Text className="text-sm font-semibold text-red-500">{statistics.level1.orders}</Text>
|
||||
<Text className="text-xs text-gray-500">一级订单</Text>
|
||||
<Text className="text-xs text-red-500">¥{statistics.level1.commission}</Text>
|
||||
</View>
|
||||
<View className="text-center bg-gray-50 rounded p-2">
|
||||
<Text className="text-sm font-semibold text-blue-500">{statistics.level2.orders}</Text>
|
||||
<Text className="text-xs text-gray-500">二级订单</Text>
|
||||
<Text className="text-xs text-blue-500">¥{statistics.level2.commission}</Text>
|
||||
</View>
|
||||
<View className="text-center bg-gray-50 rounded p-2">
|
||||
<Text className="text-sm font-semibold text-purple-500">{statistics.level3.orders}</Text>
|
||||
<Text className="text-xs text-gray-500">三级订单</Text>
|
||||
<Text className="text-xs text-purple-500">¥{statistics.level3.commission}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 订单列表 */}
|
||||
<Tabs value={activeTab} onChange={() => setActiveTab}>
|
||||
<Tabs.TabPane title="全部" value="0">
|
||||
<PullToRefresh
|
||||
onRefresh={handleRefresh}
|
||||
>
|
||||
<View className="p-4">
|
||||
{loading ? (
|
||||
<View className="text-center py-8">
|
||||
<Loading />
|
||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||
</View>
|
||||
) : getFilteredOrders('0').length > 0 ? (
|
||||
getFilteredOrders('0').map(renderOrderItem)
|
||||
) : (
|
||||
<Empty description="暂无分销订单" />
|
||||
)}
|
||||
</View>
|
||||
</PullToRefresh>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane title="一级分销" value="1">
|
||||
<View className="p-4">
|
||||
{getFilteredOrders('1').length > 0 ? (
|
||||
getFilteredOrders('1').map(renderOrderItem)
|
||||
) : (
|
||||
<Empty description="暂无一级分销订单" />
|
||||
)}
|
||||
</View>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane title="二级分销" value="2">
|
||||
<View className="p-4">
|
||||
{getFilteredOrders('2').length > 0 ? (
|
||||
getFilteredOrders('2').map(renderOrderItem)
|
||||
) : (
|
||||
<Empty description="暂无二级分销订单" />
|
||||
)}
|
||||
</View>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane title="三级分销" value="3">
|
||||
<View className="p-4">
|
||||
{getFilteredOrders('3').length > 0 ? (
|
||||
getFilteredOrders('3').map(renderOrderItem)
|
||||
) : (
|
||||
<Empty description="暂无三级分销订单" />
|
||||
)}
|
||||
</View>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane title="待结算" value="4">
|
||||
<View className="p-4">
|
||||
{getFilteredOrders('4').length > 0 ? (
|
||||
getFilteredOrders('4').map(renderOrderItem)
|
||||
) : (
|
||||
<Empty description="暂无待结算订单" />
|
||||
)}
|
||||
</View>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane title="已结算" value="5">
|
||||
<View className="p-4">
|
||||
{getFilteredOrders('5').length > 0 ? (
|
||||
getFilteredOrders('5').map(renderOrderItem)
|
||||
) : (
|
||||
<Empty description="暂无已结算订单" />
|
||||
)}
|
||||
</View>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane title="已失效" value="6">
|
||||
<View className="p-4">
|
||||
{getFilteredOrders('6').length > 0 ? (
|
||||
getFilteredOrders('6').map(renderOrderItem)
|
||||
) : (
|
||||
<Empty description="暂无失效订单" />
|
||||
)}
|
||||
</View>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default DealerOrders
|
||||
3
src/doctor/qrcode/index.config.ts
Normal file
3
src/doctor/qrcode/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '推广二维码'
|
||||
})
|
||||
398
src/doctor/qrcode/index.tsx
Normal file
398
src/doctor/qrcode/index.tsx
Normal file
@@ -0,0 +1,398 @@
|
||||
import React, {useState, useEffect} from 'react'
|
||||
import {View, Text, Image} from '@tarojs/components'
|
||||
import {Button, Loading} from '@nutui/nutui-react-taro'
|
||||
import {Share, Download, Copy, QrCode} from '@nutui/icons-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {useDealerUser} from '@/hooks/useDealerUser'
|
||||
import {generateInviteCode} from '@/api/invite'
|
||||
// import type {InviteStats} from '@/api/invite'
|
||||
import {businessGradients} from '@/styles/gradients'
|
||||
|
||||
const DealerQrcode: React.FC = () => {
|
||||
const [miniProgramCodeUrl, setMiniProgramCodeUrl] = useState<string>('')
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
// const [inviteStats, setInviteStats] = useState<InviteStats | null>(null)
|
||||
// const [statsLoading, setStatsLoading] = useState<boolean>(false)
|
||||
const {dealerUser} = useDealerUser()
|
||||
|
||||
// 生成小程序码
|
||||
const generateMiniProgramCode = async () => {
|
||||
if (!dealerUser?.userId) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
|
||||
// 生成邀请小程序码
|
||||
const codeUrl = await generateInviteCode(dealerUser.userId)
|
||||
|
||||
if (codeUrl) {
|
||||
setMiniProgramCodeUrl(codeUrl)
|
||||
} else {
|
||||
throw new Error('返回的小程序码URL为空')
|
||||
}
|
||||
} catch (error: any) {
|
||||
Taro.showToast({
|
||||
title: error.message || '生成小程序码失败',
|
||||
icon: 'error'
|
||||
})
|
||||
// 清空之前的二维码
|
||||
setMiniProgramCodeUrl('')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取邀请统计数据
|
||||
// const fetchInviteStats = async () => {
|
||||
// if (!dealerUser?.userId) return
|
||||
//
|
||||
// try {
|
||||
// setStatsLoading(true)
|
||||
// const stats = await getInviteStats(dealerUser.userId)
|
||||
// stats && setInviteStats(stats)
|
||||
// } catch (error) {
|
||||
// // 静默处理错误,不影响用户体验
|
||||
// } finally {
|
||||
// setStatsLoading(false)
|
||||
// }
|
||||
// }
|
||||
|
||||
// 初始化生成小程序码和获取统计数据
|
||||
useEffect(() => {
|
||||
if (dealerUser?.userId) {
|
||||
generateMiniProgramCode()
|
||||
// fetchInviteStats()
|
||||
}
|
||||
}, [dealerUser?.userId])
|
||||
|
||||
// 保存小程序码到相册
|
||||
const saveMiniProgramCode = async () => {
|
||||
if (!miniProgramCodeUrl) {
|
||||
Taro.showToast({
|
||||
title: '小程序码未生成',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 先下载图片到本地
|
||||
const res = await Taro.downloadFile({
|
||||
url: miniProgramCodeUrl
|
||||
})
|
||||
|
||||
if (res.statusCode === 200) {
|
||||
// 保存到相册
|
||||
await Taro.saveImageToPhotosAlbum({
|
||||
filePath: res.tempFilePath
|
||||
})
|
||||
|
||||
Taro.showToast({
|
||||
title: '保存成功',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.errMsg?.includes('auth deny')) {
|
||||
Taro.showModal({
|
||||
title: '提示',
|
||||
content: '需要您授权保存图片到相册',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
Taro.openSetting()
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Taro.showToast({
|
||||
title: '保存失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 复制邀请信息
|
||||
const copyInviteInfo = () => {
|
||||
if (!dealerUser?.userId) {
|
||||
Taro.showToast({
|
||||
title: '用户信息未加载',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const inviteText = `🎉 邀请您加入我的团队!
|
||||
|
||||
扫描小程序码或搜索"通源堂健康生态平台"小程序,即可享受优质商品和服务!
|
||||
|
||||
💰 成为我的团队成员,一起赚取丰厚佣金
|
||||
🎁 新用户专享优惠等你来拿
|
||||
|
||||
邀请码:${dealerUser.userId}
|
||||
快来加入我们吧!`
|
||||
|
||||
Taro.setClipboardData({
|
||||
data: inviteText,
|
||||
success: () => {
|
||||
Taro.showToast({
|
||||
title: '邀请信息已复制',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 分享小程序码
|
||||
const shareMiniProgramCode = () => {
|
||||
if (!dealerUser?.userId) {
|
||||
Taro.showToast({
|
||||
title: '用户信息未加载',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 小程序分享
|
||||
Taro.showShareMenu({
|
||||
withShareTicket: true,
|
||||
showShareItems: ['shareAppMessage', 'shareTimeline']
|
||||
})
|
||||
}
|
||||
|
||||
if (!dealerUser) {
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
||||
<Loading/>
|
||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen">
|
||||
{/* 头部卡片 */}
|
||||
<View className="rounded-b-3xl p-6 mb-6 text-white relative overflow-hidden" style={{
|
||||
background: businessGradients.dealer.header
|
||||
}}>
|
||||
{/* 装饰背景 */}
|
||||
<View className="absolute w-32 h-32 rounded-full" style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
top: '-16px',
|
||||
right: '-16px'
|
||||
}}></View>
|
||||
|
||||
<View className="relative z-10 flex flex-col">
|
||||
<Text className="text-2xl font-bold mb-2 text-white">我的邀请小程序码</Text>
|
||||
<Text className="text-white text-opacity-80">
|
||||
分享小程序码邀请好友,获得丰厚佣金奖励
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="px-4">
|
||||
{/* 小程序码展示区 */}
|
||||
<View className="bg-white rounded-2xl p-6 mb-6 shadow-sm">
|
||||
<View className="text-center">
|
||||
{loading ? (
|
||||
<View className="w-48 h-48 mx-auto mb-4 flex items-center justify-center bg-gray-50 rounded-xl">
|
||||
<Loading/>
|
||||
<Text className="text-gray-500 mt-2">生成中...</Text>
|
||||
</View>
|
||||
) : miniProgramCodeUrl ? (
|
||||
<View className="w-48 h-48 mx-auto mb-4 bg-white rounded-xl shadow-sm p-4">
|
||||
<Image
|
||||
src={miniProgramCodeUrl}
|
||||
className="w-full h-full"
|
||||
mode="aspectFit"
|
||||
onError={() => {
|
||||
Taro.showModal({
|
||||
title: '二维码加载失败',
|
||||
content: '请检查网络连接或联系管理员',
|
||||
showCancel: true,
|
||||
confirmText: '重新生成',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
generateMiniProgramCode();
|
||||
}
|
||||
}
|
||||
});
|
||||
}}
|
||||
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<View className="w-48 h-48 mx-auto mb-4 flex items-center justify-center bg-gray-50 rounded-xl">
|
||||
<QrCode size="48" className="text-gray-400 mb-2"/>
|
||||
<Text className="text-gray-500">小程序码生成失败</Text>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
className="mt-2"
|
||||
onClick={generateMiniProgramCode}
|
||||
>
|
||||
重新生成
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View className="text-lg font-semibold text-gray-800 mb-2">
|
||||
扫码加入我的团队
|
||||
</View>
|
||||
<View className="text-sm text-gray-500 mb-4">
|
||||
好友扫描小程序码即可直接进入小程序并建立邀请关系
|
||||
</View>
|
||||
|
||||
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<View className={'gap-2'}>
|
||||
<View className={'my-2'}>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
icon={<Download/>}
|
||||
onClick={saveMiniProgramCode}
|
||||
disabled={!miniProgramCodeUrl || loading}
|
||||
>
|
||||
保存小程序码到相册
|
||||
</Button>
|
||||
</View>
|
||||
<View className={'my-2 bg-white'}>
|
||||
<Button
|
||||
size="large"
|
||||
block
|
||||
icon={<Copy/>}
|
||||
onClick={copyInviteInfo}
|
||||
disabled={!dealerUser?.userId || loading}
|
||||
>
|
||||
复制邀请信息
|
||||
</Button>
|
||||
</View>
|
||||
<View className={'my-2 bg-white'}>
|
||||
<Button
|
||||
size="large"
|
||||
block
|
||||
fill="outline"
|
||||
icon={<Share/>}
|
||||
onClick={shareMiniProgramCode}
|
||||
disabled={!dealerUser?.userId || loading}
|
||||
>
|
||||
分享给好友
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 推广说明 */}
|
||||
<View className="bg-white rounded-2xl p-4 mt-6 hidden">
|
||||
<Text className="font-semibold text-gray-800 mb-3">推广说明</Text>
|
||||
<View className="space-y-2">
|
||||
<View className="flex items-start">
|
||||
<View className="w-2 h-2 bg-blue-500 rounded-full mt-2 mr-3 flex-shrink-0"></View>
|
||||
<Text className="text-sm text-gray-600">
|
||||
好友通过您的二维码或链接注册成为您的团队成员
|
||||
</Text>
|
||||
</View>
|
||||
<View className="flex items-start">
|
||||
<View className="w-2 h-2 bg-green-500 rounded-full mt-2 mr-3 flex-shrink-0"></View>
|
||||
<Text className="text-sm text-gray-600">
|
||||
好友购买商品时,您可获得相应层级的分销佣金
|
||||
</Text>
|
||||
</View>
|
||||
<View className="flex items-start">
|
||||
<View className="w-2 h-2 bg-purple-500 rounded-full mt-2 mr-3 flex-shrink-0"></View>
|
||||
<Text className="text-sm text-gray-600">
|
||||
支持三级分销,团队越大收益越多
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 邀请统计数据 */}
|
||||
{/*<View className="bg-white rounded-2xl p-4 mt-4 mb-6">*/}
|
||||
{/* <Text className="font-semibold text-gray-800 mb-3">我的邀请数据</Text>*/}
|
||||
{/* {statsLoading ? (*/}
|
||||
{/* <View className="flex items-center justify-center py-8">*/}
|
||||
{/* <Loading/>*/}
|
||||
{/* <Text className="text-gray-500 mt-2">加载中...</Text>*/}
|
||||
{/* </View>*/}
|
||||
{/* ) : inviteStats ? (*/}
|
||||
{/* <View className="space-y-4">*/}
|
||||
{/* <View className="grid grid-cols-2 gap-4">*/}
|
||||
{/* <View className="text-center">*/}
|
||||
{/* <Text className="text-2xl font-bold text-blue-500">*/}
|
||||
{/* {inviteStats.totalInvites || 0}*/}
|
||||
{/* </Text>*/}
|
||||
{/* <Text className="text-sm text-gray-500">总邀请数</Text>*/}
|
||||
{/* </View>*/}
|
||||
{/* <View className="text-center">*/}
|
||||
{/* <Text className="text-2xl font-bold text-green-500">*/}
|
||||
{/* {inviteStats.successfulRegistrations || 0}*/}
|
||||
{/* </Text>*/}
|
||||
{/* <Text className="text-sm text-gray-500">成功注册</Text>*/}
|
||||
{/* </View>*/}
|
||||
{/* </View>*/}
|
||||
|
||||
{/* <View className="grid grid-cols-2 gap-4">*/}
|
||||
{/* <View className="text-center">*/}
|
||||
{/* <Text className="text-2xl font-bold text-purple-500">*/}
|
||||
{/* {inviteStats.conversionRate ? `${(inviteStats.conversionRate * 100).toFixed(1)}%` : '0%'}*/}
|
||||
{/* </Text>*/}
|
||||
{/* <Text className="text-sm text-gray-500">转化率</Text>*/}
|
||||
{/* </View>*/}
|
||||
{/* <View className="text-center">*/}
|
||||
{/* <Text className="text-2xl font-bold text-orange-500">*/}
|
||||
{/* {inviteStats.todayInvites || 0}*/}
|
||||
{/* </Text>*/}
|
||||
{/* <Text className="text-sm text-gray-500">今日邀请</Text>*/}
|
||||
{/* </View>*/}
|
||||
{/* </View>*/}
|
||||
|
||||
{/* /!* 邀请来源统计 *!/*/}
|
||||
{/* {inviteStats.sourceStats && inviteStats.sourceStats.length > 0 && (*/}
|
||||
{/* <View className="mt-4">*/}
|
||||
{/* <Text className="text-sm font-medium text-gray-700 mb-2">邀请来源分布</Text>*/}
|
||||
{/* <View className="space-y-2">*/}
|
||||
{/* {inviteStats.sourceStats.map((source, index) => (*/}
|
||||
{/* <View key={index} className="flex items-center justify-between py-2 px-3 bg-gray-50 rounded-lg">*/}
|
||||
{/* <View className="flex items-center">*/}
|
||||
{/* <View className="w-3 h-3 rounded-full bg-blue-500 mr-2"></View>*/}
|
||||
{/* <Text className="text-sm text-gray-700">{source.source}</Text>*/}
|
||||
{/* </View>*/}
|
||||
{/* <View className="text-right">*/}
|
||||
{/* <Text className="text-sm font-medium text-gray-800">{source.count}</Text>*/}
|
||||
{/* <Text className="text-xs text-gray-500">*/}
|
||||
{/* {source.conversionRate ? `${(source.conversionRate * 100).toFixed(1)}%` : '0%'}*/}
|
||||
{/* </Text>*/}
|
||||
{/* </View>*/}
|
||||
{/* </View>*/}
|
||||
{/* ))}*/}
|
||||
{/* </View>*/}
|
||||
{/* </View>*/}
|
||||
{/* )}*/}
|
||||
{/* </View>*/}
|
||||
{/* ) : (*/}
|
||||
{/* <View className="text-center py-8">*/}
|
||||
{/* <View className="text-gray-500">暂无邀请数据</View>*/}
|
||||
{/* <Button*/}
|
||||
{/* size="small"*/}
|
||||
{/* type="primary"*/}
|
||||
{/* className="mt-2"*/}
|
||||
{/* onClick={fetchInviteStats}*/}
|
||||
{/* >*/}
|
||||
{/* 刷新数据*/}
|
||||
{/* </Button>*/}
|
||||
{/* </View>*/}
|
||||
{/* )}*/}
|
||||
{/*</View>*/}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default DealerQrcode
|
||||
3
src/doctor/team/index.config.ts
Normal file
3
src/doctor/team/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '我的团队'
|
||||
})
|
||||
367
src/doctor/team/index.tsx
Normal file
367
src/doctor/team/index.tsx
Normal file
@@ -0,0 +1,367 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Empty, Tabs, Avatar, Tag, Progress, Loading, PullToRefresh } from '@nutui/nutui-react-taro'
|
||||
import { User, Star, StarFill } from '@nutui/icons-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useDealerUser } from '@/hooks/useDealerUser'
|
||||
import { listShopDealerReferee } from '@/api/shop/shopDealerReferee'
|
||||
import { pageShopDealerOrder } from '@/api/shop/shopDealerOrder'
|
||||
import type { ShopDealerReferee } from '@/api/shop/shopDealerReferee/model'
|
||||
|
||||
interface TeamMemberWithStats extends ShopDealerReferee {
|
||||
name?: string
|
||||
avatar?: string
|
||||
orderCount?: number
|
||||
commission?: string
|
||||
status?: 'active' | 'inactive'
|
||||
subMembers?: number
|
||||
joinTime?: string
|
||||
}
|
||||
|
||||
const DealerTeam: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState('0')
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [refreshing, setRefreshing] = useState<boolean>(false)
|
||||
const [teamMembers, setTeamMembers] = useState<TeamMemberWithStats[]>([])
|
||||
const [teamStats, setTeamStats] = useState({
|
||||
total: 0,
|
||||
firstLevel: 0,
|
||||
secondLevel: 0,
|
||||
thirdLevel: 0,
|
||||
monthlyCommission: '0.00'
|
||||
})
|
||||
|
||||
const { dealerUser } = useDealerUser()
|
||||
|
||||
// 获取团队数据
|
||||
const fetchTeamData = useCallback(async () => {
|
||||
if (!dealerUser?.userId) return
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
|
||||
// 获取团队成员关系
|
||||
const refereeResult = await listShopDealerReferee({
|
||||
dealerId: dealerUser.userId
|
||||
})
|
||||
|
||||
if (refereeResult) {
|
||||
// 处理团队成员数据
|
||||
const processedMembers: TeamMemberWithStats[] = refereeResult.map(member => ({
|
||||
...member,
|
||||
name: `用户${member.userId}`,
|
||||
avatar: '',
|
||||
orderCount: 0,
|
||||
commission: '0.00',
|
||||
status: 'active' as const,
|
||||
subMembers: 0,
|
||||
joinTime: member.createTime
|
||||
}))
|
||||
|
||||
// 并行获取每个成员的订单统计
|
||||
const memberStats = await Promise.all(
|
||||
processedMembers.map(async (member) => {
|
||||
try {
|
||||
const orderResult = await pageShopDealerOrder({
|
||||
page: 1,
|
||||
limit: 100,
|
||||
userId: member.userId
|
||||
})
|
||||
|
||||
if (orderResult?.list) {
|
||||
const orders = orderResult.list
|
||||
const orderCount = orders.length
|
||||
const commission = orders.reduce((sum, order) => {
|
||||
const levelCommission = member.level === 1 ? order.firstMoney :
|
||||
member.level === 2 ? order.secondMoney :
|
||||
order.thirdMoney
|
||||
return sum + parseFloat(levelCommission || '0')
|
||||
}, 0).toFixed(2)
|
||||
|
||||
// 判断活跃状态(30天内有订单为活跃)
|
||||
const thirtyDaysAgo = new Date()
|
||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30)
|
||||
const hasRecentOrder = orders.some(order =>
|
||||
new Date(order.createTime || '') > thirtyDaysAgo
|
||||
)
|
||||
|
||||
return {
|
||||
...member,
|
||||
orderCount,
|
||||
commission,
|
||||
status: hasRecentOrder ? 'active' as const : 'inactive' as const
|
||||
}
|
||||
}
|
||||
return member
|
||||
} catch (error) {
|
||||
console.error(`获取成员${member.userId}订单失败:`, error)
|
||||
return member
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
setTeamMembers(memberStats)
|
||||
|
||||
// 计算统计数据
|
||||
const stats = {
|
||||
total: memberStats.length,
|
||||
firstLevel: memberStats.filter(m => m.level === 1).length,
|
||||
secondLevel: memberStats.filter(m => m.level === 2).length,
|
||||
thirdLevel: memberStats.filter(m => m.level === 3).length,
|
||||
monthlyCommission: memberStats.reduce((sum, member) =>
|
||||
sum + parseFloat(member.commission || '0'), 0
|
||||
).toFixed(2)
|
||||
}
|
||||
|
||||
setTeamStats(stats)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取团队数据失败:', error)
|
||||
Taro.showToast({
|
||||
title: '获取团队数据失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [dealerUser?.userId])
|
||||
|
||||
// 刷新数据
|
||||
const handleRefresh = async () => {
|
||||
setRefreshing(true)
|
||||
await fetchTeamData()
|
||||
setRefreshing(false)
|
||||
}
|
||||
|
||||
// 初始化加载数据
|
||||
useEffect(() => {
|
||||
if (dealerUser?.userId) {
|
||||
fetchTeamData().then()
|
||||
}
|
||||
}, [fetchTeamData])
|
||||
|
||||
const getLevelColor = (level: number) => {
|
||||
switch (level) {
|
||||
case 1: return '#f59e0b'
|
||||
case 2: return '#8b5cf6'
|
||||
case 3: return '#ec4899'
|
||||
default: return '#6b7280'
|
||||
}
|
||||
}
|
||||
|
||||
const getLevelIcon = (level: number) => {
|
||||
switch (level) {
|
||||
case 1: return <StarFill color={getLevelColor(level)} size="16" />
|
||||
case 2: return <Star color={getLevelColor(level)} size="16" />
|
||||
case 3: return <User color={getLevelColor(level)} size="16" />
|
||||
default: return <User color={getLevelColor(level)} size="16" />
|
||||
}
|
||||
}
|
||||
|
||||
const renderMemberItem = (member: TeamMemberWithStats) => (
|
||||
<View key={member.id} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
|
||||
<View className="flex items-center mb-3">
|
||||
<Avatar
|
||||
size="40"
|
||||
src={member.avatar}
|
||||
icon={<User />}
|
||||
className="mr-3"
|
||||
/>
|
||||
<View className="flex-1">
|
||||
<View className="flex items-center mb-1">
|
||||
<Text className="font-semibold text-gray-800 mr-2">
|
||||
{member.name}
|
||||
</Text>
|
||||
{getLevelIcon(Number(member.level))}
|
||||
<Text className="text-xs text-gray-500 ml-1">
|
||||
{member.level}级
|
||||
</Text>
|
||||
</View>
|
||||
<Text className="text-xs text-gray-500">
|
||||
加入时间:{member.joinTime}
|
||||
</Text>
|
||||
</View>
|
||||
<View className="text-right">
|
||||
<Tag
|
||||
type={member.status === 'active' ? 'success' : 'default'}
|
||||
>
|
||||
{member.status === 'active' ? '活跃' : '沉默'}
|
||||
</Tag>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="grid grid-cols-3 gap-4 text-center">
|
||||
<View>
|
||||
<Text className="text-sm font-semibold text-blue-600">
|
||||
{member.orderCount}
|
||||
</Text>
|
||||
<Text className="text-xs text-gray-500">订单数</Text>
|
||||
</View>
|
||||
<View>
|
||||
<Text className="text-sm font-semibold text-green-600">
|
||||
¥{member.commission}
|
||||
</Text>
|
||||
<Text className="text-xs text-gray-500">贡献佣金</Text>
|
||||
</View>
|
||||
<View>
|
||||
<Text className="text-sm font-semibold text-purple-600">
|
||||
{member.subMembers}
|
||||
</Text>
|
||||
<Text className="text-xs text-gray-500">团队成员</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
|
||||
const renderOverview = () => (
|
||||
<View className="p-4">
|
||||
{/* 团队统计卡片 */}
|
||||
<View className="rounded-xl p-6 mb-6 text-white relative overflow-hidden" style={{
|
||||
background: 'linear-gradient(135deg, #8b5cf6 0%, #ec4899 100%)'
|
||||
}}>
|
||||
{/* 装饰背景 - 小程序兼容版本 */}
|
||||
<View className="absolute w-32 h-32 rounded-full" style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
top: '-16px',
|
||||
right: '-16px'
|
||||
}}></View>
|
||||
<View className="absolute w-20 h-20 rounded-full" style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||
bottom: '-10px',
|
||||
left: '-10px'
|
||||
}}></View>
|
||||
|
||||
<View className="relative z-10">
|
||||
<Text className="text-lg font-bold mb-4 text-white">团队总览</Text>
|
||||
<View className="grid grid-cols-2 gap-4">
|
||||
<View>
|
||||
<Text className="text-2xl font-bold mb-1 text-white">{teamStats.total}</Text>
|
||||
<Text className="text-sm" style={{ color: 'rgba(255, 255, 255, 0.8)' }}>团队总人数</Text>
|
||||
</View>
|
||||
<View>
|
||||
<Text className="text-2xl font-bold mb-1 text-white">¥{teamStats.monthlyCommission}</Text>
|
||||
<Text className="text-sm" style={{ color: 'rgba(255, 255, 255, 0.8)' }}>本月团队佣金</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 层级分布 */}
|
||||
<View className="bg-white rounded-xl p-4 mb-4">
|
||||
<Text className="font-semibold mb-4 text-gray-800">层级分布</Text>
|
||||
<View className="gap-2">
|
||||
<View className="flex items-center justify-between">
|
||||
<View className="flex items-center">
|
||||
<StarFill color="#f59e0b" size="16" className="mr-2" />
|
||||
<Text className="text-sm">一级成员</Text>
|
||||
</View>
|
||||
<View className="flex items-center">
|
||||
<Text className="text-sm font-semibold mr-2">{teamStats.firstLevel}</Text>
|
||||
<Progress
|
||||
percent={(teamStats.firstLevel / teamStats.total) * 100}
|
||||
strokeWidth="6"
|
||||
background={'#f59e0b'}
|
||||
className="w-20"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="flex items-center justify-between">
|
||||
<View className="flex items-center">
|
||||
<Star color="#8b5cf6" size="16" className="mr-2" />
|
||||
<Text className="text-sm">二级成员</Text>
|
||||
</View>
|
||||
<View className="flex items-center">
|
||||
<Text className="text-sm font-semibold mr-2">{teamStats.secondLevel}</Text>
|
||||
<Progress
|
||||
percent={(teamStats.secondLevel / teamStats.total) * 100}
|
||||
strokeWidth="6"
|
||||
background={'#8b5cf6'}
|
||||
className="w-20"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="flex items-center justify-between">
|
||||
<View className="flex items-center">
|
||||
<User color="#ec4899" size="16" className="mr-2" />
|
||||
<Text className="text-sm">三级成员</Text>
|
||||
</View>
|
||||
<View className="flex items-center">
|
||||
<Text className="text-sm font-semibold mr-2">{teamStats.thirdLevel}</Text>
|
||||
<Progress
|
||||
percent={(teamStats.thirdLevel / teamStats.total) * 100}
|
||||
strokeWidth="6"
|
||||
background={'#ec4899'}
|
||||
className="w-20"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 最新成员 */}
|
||||
<View className="bg-white rounded-xl p-4">
|
||||
<Text className="font-semibold mb-4 text-gray-800">最新成员</Text>
|
||||
{teamMembers.slice(0, 3).map(renderMemberItem)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
|
||||
const renderMemberList = (level?: number) => (
|
||||
<PullToRefresh
|
||||
disabled={refreshing}
|
||||
onRefresh={handleRefresh}
|
||||
>
|
||||
<View className="p-4">
|
||||
{loading ? (
|
||||
<View className="text-center py-8">
|
||||
<Loading />
|
||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||
</View>
|
||||
) : teamMembers
|
||||
.filter(member => !level || member.level === level)
|
||||
.length > 0 ? (
|
||||
teamMembers
|
||||
.filter(member => !level || member.level === level)
|
||||
.map(renderMemberItem)
|
||||
) : (
|
||||
<Empty description={`暂无${level ? level + '级' : ''}团队成员`} />
|
||||
)}
|
||||
</View>
|
||||
</PullToRefresh>
|
||||
)
|
||||
|
||||
if (!dealerUser) {
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
||||
<Loading />
|
||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen">
|
||||
<Tabs value={activeTab} onChange={() => setActiveTab}>
|
||||
<Tabs.TabPane title="团队总览" value="0">
|
||||
{renderOverview()}
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane title="一级成员" value="1">
|
||||
{renderMemberList(1)}
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane title="二级成员" value="2">
|
||||
{renderMemberList(2)}
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane title="三级成员" value="3">
|
||||
{renderMemberList(3)}
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default DealerTeam
|
||||
3
src/doctor/withdraw/index.config.ts
Normal file
3
src/doctor/withdraw/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '提现申请'
|
||||
})
|
||||
424
src/doctor/withdraw/index.tsx
Normal file
424
src/doctor/withdraw/index.tsx
Normal file
@@ -0,0 +1,424 @@
|
||||
import React, { useState, useRef, useEffect, useCallback } from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import {
|
||||
Cell,
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
CellGroup,
|
||||
Radio,
|
||||
Tabs,
|
||||
Tag,
|
||||
Empty,
|
||||
Loading,
|
||||
PullToRefresh
|
||||
} from '@nutui/nutui-react-taro'
|
||||
import { Wallet } from '@nutui/icons-react-taro'
|
||||
import { businessGradients } from '@/styles/gradients'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useDealerUser } from '@/hooks/useDealerUser'
|
||||
import { pageShopDealerWithdraw, addShopDealerWithdraw } from '@/api/shop/shopDealerWithdraw'
|
||||
import type { ShopDealerWithdraw } from '@/api/shop/shopDealerWithdraw/model'
|
||||
|
||||
interface WithdrawRecordWithDetails extends ShopDealerWithdraw {
|
||||
accountDisplay?: string
|
||||
}
|
||||
|
||||
const DealerWithdraw: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState('0')
|
||||
const [selectedAccount, setSelectedAccount] = useState('')
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [refreshing, setRefreshing] = useState<boolean>(false)
|
||||
const [submitting, setSubmitting] = useState<boolean>(false)
|
||||
const [availableAmount, setAvailableAmount] = useState<string>('0.00')
|
||||
const [withdrawRecords, setWithdrawRecords] = useState<WithdrawRecordWithDetails[]>([])
|
||||
const formRef = useRef<any>(null)
|
||||
|
||||
const { dealerUser } = useDealerUser()
|
||||
|
||||
// 获取可提现余额
|
||||
const fetchBalance = useCallback(async () => {
|
||||
try {
|
||||
setAvailableAmount(dealerUser?.money || '0.00')
|
||||
} catch (error) {
|
||||
console.error('获取余额失败:', error)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 获取提现记录
|
||||
const fetchWithdrawRecords = useCallback(async () => {
|
||||
if (!dealerUser?.userId) return
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
const result = await pageShopDealerWithdraw({
|
||||
page: 1,
|
||||
limit: 100,
|
||||
userId: dealerUser.userId
|
||||
})
|
||||
|
||||
if (result?.list) {
|
||||
const processedRecords = result.list.map(record => ({
|
||||
...record,
|
||||
accountDisplay: getAccountDisplay(record)
|
||||
}))
|
||||
setWithdrawRecords(processedRecords)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取提现记录失败:', error)
|
||||
Taro.showToast({
|
||||
title: '获取提现记录失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [dealerUser?.userId])
|
||||
|
||||
// 格式化账户显示
|
||||
const getAccountDisplay = (record: ShopDealerWithdraw) => {
|
||||
if (record.payType === 10) {
|
||||
return '微信钱包'
|
||||
} else if (record.payType === 20 && record.alipayAccount) {
|
||||
return `支付宝(${record.alipayAccount.slice(-4)})`
|
||||
} else if (record.payType === 30 && record.bankCard) {
|
||||
return `${record.bankName || '银行卡'}(尾号${record.bankCard.slice(-4)})`
|
||||
}
|
||||
return '未知账户'
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
const handleRefresh = async () => {
|
||||
setRefreshing(true)
|
||||
await Promise.all([fetchBalance(), fetchWithdrawRecords()])
|
||||
setRefreshing(false)
|
||||
}
|
||||
|
||||
// 初始化加载数据
|
||||
useEffect(() => {
|
||||
if (dealerUser?.userId) {
|
||||
fetchBalance().then()
|
||||
fetchWithdrawRecords().then()
|
||||
}
|
||||
}, [fetchBalance, fetchWithdrawRecords])
|
||||
|
||||
const getStatusText = (status?: number) => {
|
||||
switch (status) {
|
||||
case 40: return '已到账'
|
||||
case 20: return '审核通过'
|
||||
case 10: return '待审核'
|
||||
case 30: return '已驳回'
|
||||
default: return '未知'
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusColor = (status?: number) => {
|
||||
switch (status) {
|
||||
case 40: return 'success'
|
||||
case 20: return 'success'
|
||||
case 10: return 'warning'
|
||||
case 30: return 'danger'
|
||||
default: return 'default'
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async (values: any) => {
|
||||
if (!dealerUser?.userId) {
|
||||
Taro.showToast({
|
||||
title: '用户信息获取失败',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证提现金额
|
||||
const amount = parseFloat(values.amount)
|
||||
const available = parseFloat(availableAmount.replace(',', ''))
|
||||
|
||||
if (amount < 100) {
|
||||
Taro.showToast({
|
||||
title: '最低提现金额为100元',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (amount > available) {
|
||||
Taro.showToast({
|
||||
title: '提现金额超过可用余额',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setSubmitting(true)
|
||||
|
||||
const withdrawData: ShopDealerWithdraw = {
|
||||
userId: dealerUser.userId,
|
||||
money: values.amount,
|
||||
payType: values.accountType === 'wechat' ? 10 :
|
||||
values.accountType === 'alipay' ? 20 : 30,
|
||||
applyStatus: 10, // 待审核
|
||||
platform: 'MiniProgram'
|
||||
}
|
||||
|
||||
// 根据提现方式设置账户信息
|
||||
if (values.accountType === 'alipay') {
|
||||
withdrawData.alipayAccount = values.account
|
||||
withdrawData.alipayName = values.accountName
|
||||
} else if (values.accountType === 'bank') {
|
||||
withdrawData.bankCard = values.account
|
||||
withdrawData.bankAccount = values.accountName
|
||||
withdrawData.bankName = values.bankName || '银行卡'
|
||||
}
|
||||
|
||||
await addShopDealerWithdraw(withdrawData)
|
||||
|
||||
Taro.showToast({
|
||||
title: '提现申请已提交',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 重置表单
|
||||
formRef.current?.resetFields()
|
||||
setSelectedAccount('')
|
||||
|
||||
// 刷新数据
|
||||
await handleRefresh()
|
||||
|
||||
// 切换到提现记录页面
|
||||
setActiveTab('1')
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('提现申请失败:', error)
|
||||
Taro.showToast({
|
||||
title: error.message || '提现申请失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const quickAmounts = ['100', '300', '500', '1000']
|
||||
|
||||
const setQuickAmount = (amount: string) => {
|
||||
formRef.current?.setFieldsValue({ amount })
|
||||
}
|
||||
|
||||
const setAllAmount = () => {
|
||||
formRef.current?.setFieldsValue({ amount: availableAmount.replace(',', '') })
|
||||
}
|
||||
|
||||
const renderWithdrawForm = () => (
|
||||
<View className="p-4">
|
||||
{/* 余额卡片 */}
|
||||
<View className="rounded-xl p-6 mb-6 text-white relative overflow-hidden" style={{
|
||||
background: businessGradients.dealer.header
|
||||
}}>
|
||||
{/* 装饰背景 - 小程序兼容版本 */}
|
||||
<View className="absolute top-0 right-0 w-24 h-24 rounded-full" style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
right: '-12px',
|
||||
top: '-12px'
|
||||
}}></View>
|
||||
|
||||
<View className="flex items-center justify-between relative z-10">
|
||||
<View>
|
||||
<Text className="text-white text-opacity-80 text-sm mb-1">可提现余额</Text>
|
||||
<Text className="text-2xl font-bold text-white">¥{availableAmount}</Text>
|
||||
</View>
|
||||
<View className="p-3 rounded-full" style={{
|
||||
background: 'rgba(255, 255, 255, 0.2)'
|
||||
}}>
|
||||
<Wallet color="white" size="32" />
|
||||
</View>
|
||||
</View>
|
||||
<View className="mt-4 pt-4 relative z-10" style={{
|
||||
borderTop: '1px solid rgba(255, 255, 255, 0.3)'
|
||||
}}>
|
||||
<Text className="text-white text-opacity-80 text-xs">
|
||||
最低提现金额:¥100 | 手续费:免费
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Form
|
||||
ref={formRef}
|
||||
onFinish={handleSubmit}
|
||||
labelPosition="top"
|
||||
>
|
||||
<CellGroup>
|
||||
<Form.Item name="amount" label="提现金额" required>
|
||||
<Input
|
||||
placeholder="请输入提现金额"
|
||||
type="number"
|
||||
clearable
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 快捷金额 */}
|
||||
<View className="px-4 py-2">
|
||||
<Text className="text-sm text-gray-600 mb-2">快捷金额</Text>
|
||||
<View className="flex flex-wrap gap-2">
|
||||
{quickAmounts.map(amount => (
|
||||
<Button
|
||||
key={amount}
|
||||
size="small"
|
||||
fill="outline"
|
||||
onClick={() => setQuickAmount(amount)}
|
||||
>
|
||||
{amount}
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
size="small"
|
||||
fill="outline"
|
||||
onClick={setAllAmount}
|
||||
>
|
||||
全部
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Form.Item name="accountType" label="提现方式" required>
|
||||
<Radio.Group value={selectedAccount} onChange={() => setSelectedAccount}>
|
||||
<Cell.Group>
|
||||
<Cell>
|
||||
<Radio value="wechat">微信钱包</Radio>
|
||||
</Cell>
|
||||
<Cell>
|
||||
<Radio value="alipay">支付宝</Radio>
|
||||
</Cell>
|
||||
<Cell>
|
||||
<Radio value="bank">银行卡</Radio>
|
||||
</Cell>
|
||||
</Cell.Group>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
{selectedAccount === 'alipay' && (
|
||||
<>
|
||||
<Form.Item name="account" label="支付宝账号" required>
|
||||
<Input placeholder="请输入支付宝账号" />
|
||||
</Form.Item>
|
||||
<Form.Item name="accountName" label="支付宝姓名" required>
|
||||
<Input placeholder="请输入支付宝实名姓名" />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedAccount === 'bank' && (
|
||||
<>
|
||||
<Form.Item name="bankName" label="开户银行" required>
|
||||
<Input placeholder="请输入开户银行名称" />
|
||||
</Form.Item>
|
||||
<Form.Item name="account" label="银行卡号" required>
|
||||
<Input placeholder="请输入银行卡号" />
|
||||
</Form.Item>
|
||||
<Form.Item name="accountName" label="开户姓名" required>
|
||||
<Input placeholder="请输入银行卡开户姓名" />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedAccount === 'wechat' && (
|
||||
<View className="px-4 py-2">
|
||||
<Text className="text-sm text-gray-500">
|
||||
微信钱包提现将直接转入您的微信零钱
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</CellGroup>
|
||||
|
||||
<View className="mt-6 px-4">
|
||||
<Button
|
||||
block
|
||||
type="primary"
|
||||
nativeType="submit"
|
||||
loading={submitting}
|
||||
disabled={submitting || !selectedAccount}
|
||||
>
|
||||
{submitting ? '提交中...' : '申请提现'}
|
||||
</Button>
|
||||
</View>
|
||||
</Form>
|
||||
</View>
|
||||
)
|
||||
|
||||
const renderWithdrawRecords = () => (
|
||||
<PullToRefresh
|
||||
disabled={refreshing}
|
||||
onRefresh={handleRefresh}
|
||||
>
|
||||
<View className="p-4">
|
||||
{loading ? (
|
||||
<View className="text-center py-8">
|
||||
<Loading />
|
||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||
</View>
|
||||
) : withdrawRecords.length > 0 ? (
|
||||
withdrawRecords.map(record => (
|
||||
<View key={record.id} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
|
||||
<View className="flex justify-between items-start mb-3">
|
||||
<View>
|
||||
<Text className="font-semibold text-gray-800 mb-1">
|
||||
提现金额:¥{record.money}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-500">
|
||||
提现账户:{record.accountDisplay}
|
||||
</Text>
|
||||
</View>
|
||||
<Tag type={getStatusColor(record.applyStatus)}>
|
||||
{getStatusText(record.applyStatus)}
|
||||
</Tag>
|
||||
</View>
|
||||
|
||||
<View className="text-xs text-gray-400">
|
||||
<Text>申请时间:{record.createTime}</Text>
|
||||
{record.auditTime && (
|
||||
<Text className="block mt-1">
|
||||
审核时间:{new Date(record.auditTime).toLocaleString()}
|
||||
</Text>
|
||||
)}
|
||||
{record.rejectReason && (
|
||||
<Text className="block mt-1 text-red-500">
|
||||
驳回原因:{record.rejectReason}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
))
|
||||
) : (
|
||||
<Empty description="暂无提现记录" />
|
||||
)}
|
||||
</View>
|
||||
</PullToRefresh>
|
||||
)
|
||||
|
||||
if (!dealerUser) {
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
||||
<Loading />
|
||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen">
|
||||
<Tabs value={activeTab} onChange={() => setActiveTab}>
|
||||
<Tabs.TabPane title="申请提现" value="0">
|
||||
{renderWithdrawForm()}
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane title="提现记录" value="1">
|
||||
{renderWithdrawRecords()}
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default DealerWithdraw
|
||||
66
src/hooks/useAdminMode.ts
Normal file
66
src/hooks/useAdminMode.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import Taro from '@tarojs/taro';
|
||||
|
||||
/**
|
||||
* 管理员模式Hook
|
||||
* 用于管理管理员用户的模式切换(普通用户模式 vs 管理员模式)
|
||||
*/
|
||||
export function useAdminMode() {
|
||||
const [isAdminMode, setIsAdminMode] = useState<boolean>(false);
|
||||
|
||||
// 从本地存储加载管理员模式状态
|
||||
useEffect(() => {
|
||||
try {
|
||||
const savedMode = Taro.getStorageSync('admin_mode');
|
||||
if (savedMode !== undefined) {
|
||||
setIsAdminMode(savedMode === 'true' || savedMode === true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load admin mode from storage:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 切换管理员模式
|
||||
const toggleAdminMode = useCallback(() => {
|
||||
const newMode = !isAdminMode;
|
||||
setIsAdminMode(newMode);
|
||||
|
||||
try {
|
||||
// 保存到本地存储
|
||||
Taro.setStorageSync('admin_mode', newMode);
|
||||
|
||||
// 显示切换提示
|
||||
Taro.showToast({
|
||||
title: newMode ? '已切换到管理员模式' : '已切换到普通用户模式',
|
||||
icon: 'success',
|
||||
duration: 1500
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to save admin mode to storage:', error);
|
||||
}
|
||||
}, [isAdminMode]);
|
||||
|
||||
// 设置管理员模式
|
||||
const setAdminMode = useCallback((mode: boolean) => {
|
||||
if (mode !== isAdminMode) {
|
||||
setIsAdminMode(mode);
|
||||
try {
|
||||
Taro.setStorageSync('admin_mode', mode);
|
||||
} catch (error) {
|
||||
console.error('Failed to save admin mode to storage:', error);
|
||||
}
|
||||
}
|
||||
}, [isAdminMode]);
|
||||
|
||||
// 重置为普通用户模式
|
||||
const resetToUserMode = useCallback(() => {
|
||||
setAdminMode(false);
|
||||
}, [setAdminMode]);
|
||||
|
||||
return {
|
||||
isAdminMode,
|
||||
toggleAdminMode,
|
||||
setAdminMode,
|
||||
resetToUserMode
|
||||
};
|
||||
}
|
||||
@@ -28,6 +28,9 @@ export const useOrderStats = () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
if(!Taro.getStorageSync('UserId')){
|
||||
return false;
|
||||
}
|
||||
// TODO 读取订单数量
|
||||
const pending = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 0})
|
||||
const paid = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 1})
|
||||
|
||||
228
src/hooks/useQRLogin.ts
Normal file
228
src/hooks/useQRLogin.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import Taro from '@tarojs/taro';
|
||||
import {
|
||||
confirmWechatQRLogin,
|
||||
parseQRContent,
|
||||
type ConfirmLoginResult
|
||||
} from '@/api/passport/qr-login';
|
||||
|
||||
/**
|
||||
* 扫码登录状态
|
||||
*/
|
||||
export enum ScanLoginState {
|
||||
IDLE = 'idle', // 空闲状态
|
||||
SCANNING = 'scanning', // 正在扫码
|
||||
CONFIRMING = 'confirming', // 正在确认登录
|
||||
SUCCESS = 'success', // 登录成功
|
||||
ERROR = 'error' // 登录失败
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫码登录Hook
|
||||
*/
|
||||
export function useQRLogin() {
|
||||
const [state, setState] = useState<ScanLoginState>(ScanLoginState.IDLE);
|
||||
const [error, setError] = useState<string>('');
|
||||
const [result, setResult] = useState<ConfirmLoginResult | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// 用于取消操作的引用
|
||||
const cancelRef = useRef<boolean>(false);
|
||||
|
||||
/**
|
||||
* 重置状态
|
||||
*/
|
||||
const reset = useCallback(() => {
|
||||
setState(ScanLoginState.IDLE);
|
||||
setError('');
|
||||
setResult(null);
|
||||
setIsLoading(false);
|
||||
cancelRef.current = false;
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 开始扫码登录
|
||||
*/
|
||||
const startScan = useCallback(async () => {
|
||||
try {
|
||||
reset();
|
||||
setState(ScanLoginState.SCANNING);
|
||||
|
||||
// 检查用户是否已登录
|
||||
const userId = Taro.getStorageSync('UserId');
|
||||
if (!userId) {
|
||||
throw new Error('请先登录小程序');
|
||||
}
|
||||
|
||||
// 调用扫码API
|
||||
const scanResult = await new Promise<string>((resolve, reject) => {
|
||||
Taro.scanCode({
|
||||
onlyFromCamera: true,
|
||||
scanType: ['qrCode'],
|
||||
success: (res) => {
|
||||
if (res.result) {
|
||||
resolve(res.result);
|
||||
} else {
|
||||
reject(new Error('扫码结果为空'));
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(new Error(err.errMsg || '扫码失败'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 检查是否被取消
|
||||
if (cancelRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 解析二维码内容
|
||||
const token = parseQRContent(scanResult);
|
||||
console.log('解析二维码内容2:',token)
|
||||
if (!token) {
|
||||
throw new Error('无效的登录二维码');
|
||||
}
|
||||
|
||||
// 确认登录
|
||||
setState(ScanLoginState.CONFIRMING);
|
||||
setIsLoading(true);
|
||||
|
||||
const confirmResult = await confirmWechatQRLogin(token, parseInt(userId));
|
||||
console.log(confirmResult,'confirmResult>>>>')
|
||||
if (cancelRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (confirmResult.success) {
|
||||
setState(ScanLoginState.SUCCESS);
|
||||
setResult(confirmResult);
|
||||
|
||||
// 显示成功提示
|
||||
Taro.showToast({
|
||||
title: '登录确认成功',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
} else {
|
||||
throw new Error(confirmResult.message || '登录确认失败');
|
||||
}
|
||||
|
||||
} catch (err: any) {
|
||||
if (!cancelRef.current) {
|
||||
setState(ScanLoginState.ERROR);
|
||||
const errorMessage = err.message || '扫码登录失败';
|
||||
setError(errorMessage);
|
||||
|
||||
// 显示错误提示
|
||||
Taro.showToast({
|
||||
title: errorMessage,
|
||||
icon: 'error',
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [reset]);
|
||||
|
||||
/**
|
||||
* 取消扫码登录
|
||||
*/
|
||||
const cancel = useCallback(() => {
|
||||
cancelRef.current = true;
|
||||
reset();
|
||||
}, [reset]);
|
||||
|
||||
/**
|
||||
* 处理扫码结果(用于已有扫码结果的情况)
|
||||
*/
|
||||
const handleScanResult = useCallback(async (qrContent: string) => {
|
||||
try {
|
||||
reset();
|
||||
setState(ScanLoginState.CONFIRMING);
|
||||
setIsLoading(true);
|
||||
|
||||
// 检查用户是否已登录
|
||||
const userId = Taro.getStorageSync('UserId');
|
||||
if (!userId) {
|
||||
throw new Error('请先登录小程序');
|
||||
}
|
||||
|
||||
// 解析二维码内容
|
||||
const token = parseQRContent(qrContent);
|
||||
if (!token) {
|
||||
throw new Error('无效的登录二维码');
|
||||
}
|
||||
|
||||
// 确认登录
|
||||
const confirmResult = await confirmWechatQRLogin(token, parseInt(userId));
|
||||
|
||||
if (confirmResult.success) {
|
||||
setState(ScanLoginState.SUCCESS);
|
||||
setResult(confirmResult);
|
||||
|
||||
// 显示成功提示
|
||||
Taro.showToast({
|
||||
title: '登录确认成功',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
} else {
|
||||
throw new Error(confirmResult.message || '登录确认失败');
|
||||
}
|
||||
|
||||
} catch (err: any) {
|
||||
setState(ScanLoginState.ERROR);
|
||||
const errorMessage = err.message || '登录确认失败';
|
||||
setError(errorMessage);
|
||||
|
||||
// 显示错误提示
|
||||
Taro.showToast({
|
||||
title: errorMessage,
|
||||
icon: 'error',
|
||||
duration: 3000
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [reset]);
|
||||
|
||||
/**
|
||||
* 检查是否可以进行扫码登录
|
||||
*/
|
||||
const canScan = useCallback(() => {
|
||||
const userId = Taro.getStorageSync('UserId');
|
||||
const accessToken = Taro.getStorageSync('access_token');
|
||||
return !!(userId && accessToken);
|
||||
}, []);
|
||||
|
||||
// 组件卸载时取消操作
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
cancelRef.current = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
// 状态
|
||||
state,
|
||||
error,
|
||||
result,
|
||||
isLoading,
|
||||
|
||||
// 方法
|
||||
startScan,
|
||||
cancel,
|
||||
reset,
|
||||
handleScanResult,
|
||||
canScan,
|
||||
|
||||
// 便捷状态判断
|
||||
isIdle: state === ScanLoginState.IDLE,
|
||||
isScanning: state === ScanLoginState.SCANNING,
|
||||
isConfirming: state === ScanLoginState.CONFIRMING,
|
||||
isSuccess: state === ScanLoginState.SUCCESS,
|
||||
isError: state === ScanLoginState.ERROR
|
||||
};
|
||||
}
|
||||
331
src/hooks/useUnifiedQRScan.ts
Normal file
331
src/hooks/useUnifiedQRScan.ts
Normal file
@@ -0,0 +1,331 @@
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import Taro from '@tarojs/taro';
|
||||
import {
|
||||
confirmWechatQRLogin,
|
||||
parseQRContent
|
||||
} from '@/api/passport/qr-login';
|
||||
import { getShopGiftByCode, updateShopGift, decryptQrData } from "@/api/shop/shopGift";
|
||||
import { useUser } from "@/hooks/useUser";
|
||||
import { isValidJSON } from "@/utils/jsonUtils";
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
/**
|
||||
* 统一扫码状态
|
||||
*/
|
||||
export enum UnifiedScanState {
|
||||
IDLE = 'idle', // 空闲状态
|
||||
SCANNING = 'scanning', // 正在扫码
|
||||
PROCESSING = 'processing', // 正在处理
|
||||
SUCCESS = 'success', // 处理成功
|
||||
ERROR = 'error' // 处理失败
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫码类型
|
||||
*/
|
||||
export enum ScanType {
|
||||
LOGIN = 'login', // 登录二维码
|
||||
VERIFICATION = 'verification', // 核销二维码
|
||||
UNKNOWN = 'unknown' // 未知类型
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一扫码结果
|
||||
*/
|
||||
export interface UnifiedScanResult {
|
||||
type: ScanType;
|
||||
data: any;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一扫码Hook
|
||||
* 可以处理登录和核销两种类型的二维码
|
||||
*/
|
||||
export function useUnifiedQRScan() {
|
||||
const { isAdmin } = useUser();
|
||||
const [state, setState] = useState<UnifiedScanState>(UnifiedScanState.IDLE);
|
||||
const [error, setError] = useState<string>('');
|
||||
const [result, setResult] = useState<UnifiedScanResult | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [scanType, setScanType] = useState<ScanType>(ScanType.UNKNOWN);
|
||||
|
||||
// 用于取消操作的引用
|
||||
const cancelRef = useRef<boolean>(false);
|
||||
|
||||
/**
|
||||
* 重置状态
|
||||
*/
|
||||
const reset = useCallback(() => {
|
||||
setState(UnifiedScanState.IDLE);
|
||||
setError('');
|
||||
setResult(null);
|
||||
setIsLoading(false);
|
||||
setScanType(ScanType.UNKNOWN);
|
||||
cancelRef.current = false;
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 检测二维码类型
|
||||
*/
|
||||
const detectScanType = useCallback((scanResult: string): ScanType => {
|
||||
try {
|
||||
// 1. 检查是否为JSON格式(核销二维码)
|
||||
if (isValidJSON(scanResult)) {
|
||||
const json = JSON.parse(scanResult);
|
||||
if (json.businessType === 'gift' && json.token && json.data) {
|
||||
return ScanType.VERIFICATION;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 检查是否为登录二维码
|
||||
const loginToken = parseQRContent(scanResult);
|
||||
if (loginToken) {
|
||||
return ScanType.LOGIN;
|
||||
}
|
||||
|
||||
// 3. 检查是否为纯文本核销码(6位数字)
|
||||
if (/^\d{6}$/.test(scanResult.trim())) {
|
||||
return ScanType.VERIFICATION;
|
||||
}
|
||||
|
||||
return ScanType.UNKNOWN;
|
||||
} catch (error) {
|
||||
console.error('检测二维码类型失败:', error);
|
||||
return ScanType.UNKNOWN;
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 处理登录二维码
|
||||
*/
|
||||
const handleLoginQR = useCallback(async (scanResult: string): Promise<UnifiedScanResult> => {
|
||||
const userId = Taro.getStorageSync('UserId');
|
||||
if (!userId) {
|
||||
throw new Error('请先登录小程序');
|
||||
}
|
||||
|
||||
const token = parseQRContent(scanResult);
|
||||
if (!token) {
|
||||
throw new Error('无效的登录二维码');
|
||||
}
|
||||
|
||||
const confirmResult = await confirmWechatQRLogin(token, parseInt(userId));
|
||||
if (confirmResult.status === 'confirmed') {
|
||||
return {
|
||||
type: ScanType.LOGIN,
|
||||
data: confirmResult,
|
||||
message: '登录成功'
|
||||
};
|
||||
} else {
|
||||
throw new Error(confirmResult.message || '登录确认失败');
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 处理核销二维码
|
||||
*/
|
||||
const handleVerificationQR = useCallback(async (scanResult: string): Promise<UnifiedScanResult> => {
|
||||
if (!isAdmin()) {
|
||||
throw new Error('您没有核销权限');
|
||||
}
|
||||
|
||||
let code = '';
|
||||
|
||||
// 判断是否为加密的JSON格式
|
||||
if (isValidJSON(scanResult)) {
|
||||
const json = JSON.parse(scanResult);
|
||||
if (json.businessType === 'gift' && json.token && json.data) {
|
||||
// 解密获取核销码
|
||||
const decryptedData = await decryptQrData({
|
||||
token: json.token,
|
||||
encryptedData: json.data
|
||||
});
|
||||
|
||||
if (decryptedData) {
|
||||
code = decryptedData.toString();
|
||||
} else {
|
||||
throw new Error('解密失败');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 直接使用扫码结果作为核销码
|
||||
code = scanResult.trim();
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
throw new Error('无法获取有效的核销码');
|
||||
}
|
||||
|
||||
// 验证核销码
|
||||
const gift = await getShopGiftByCode(code);
|
||||
|
||||
if (!gift) {
|
||||
throw new Error('核销码无效');
|
||||
}
|
||||
|
||||
if (gift.status === 1) {
|
||||
throw new Error('此礼品码已使用');
|
||||
}
|
||||
|
||||
if (gift.status === 2) {
|
||||
throw new Error('此礼品码已失效');
|
||||
}
|
||||
|
||||
if (gift.userId === 0) {
|
||||
throw new Error('此礼品码未认领');
|
||||
}
|
||||
|
||||
// 执行核销
|
||||
await updateShopGift({
|
||||
...gift,
|
||||
status: 1,
|
||||
operatorUserId: Number(Taro.getStorageSync('UserId')) || 0,
|
||||
takeTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
verificationTime: dayjs().format('YYYY-MM-DD HH:mm:ss')
|
||||
});
|
||||
|
||||
return {
|
||||
type: ScanType.VERIFICATION,
|
||||
data: gift,
|
||||
message: '核销成功'
|
||||
};
|
||||
}, [isAdmin]);
|
||||
|
||||
/**
|
||||
* 开始扫码
|
||||
*/
|
||||
const startScan = useCallback(async (): Promise<UnifiedScanResult | null> => {
|
||||
try {
|
||||
reset();
|
||||
setState(UnifiedScanState.SCANNING);
|
||||
|
||||
// 调用扫码API
|
||||
const scanResult = await new Promise<string>((resolve, reject) => {
|
||||
Taro.scanCode({
|
||||
onlyFromCamera: true,
|
||||
scanType: ['qrCode'],
|
||||
success: (res) => {
|
||||
if (res.result) {
|
||||
resolve(res.result);
|
||||
} else {
|
||||
reject(new Error('扫码结果为空'));
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(new Error(err.errMsg || '扫码失败'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 检查是否被取消
|
||||
if (cancelRef.current) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检测二维码类型
|
||||
const type = detectScanType(scanResult);
|
||||
setScanType(type);
|
||||
|
||||
if (type === ScanType.UNKNOWN) {
|
||||
throw new Error('不支持的二维码类型');
|
||||
}
|
||||
|
||||
// 开始处理
|
||||
setState(UnifiedScanState.PROCESSING);
|
||||
setIsLoading(true);
|
||||
|
||||
let result: UnifiedScanResult;
|
||||
|
||||
switch (type) {
|
||||
case ScanType.LOGIN:
|
||||
result = await handleLoginQR(scanResult);
|
||||
break;
|
||||
case ScanType.VERIFICATION:
|
||||
result = await handleVerificationQR(scanResult);
|
||||
break;
|
||||
default:
|
||||
throw new Error('未知的扫码类型');
|
||||
}
|
||||
|
||||
if (cancelRef.current) {
|
||||
return null;
|
||||
}
|
||||
|
||||
setState(UnifiedScanState.SUCCESS);
|
||||
setResult(result);
|
||||
|
||||
// 显示成功提示
|
||||
Taro.showToast({
|
||||
title: result.message,
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
|
||||
return result;
|
||||
|
||||
} catch (err: any) {
|
||||
if (!cancelRef.current) {
|
||||
setState(UnifiedScanState.ERROR);
|
||||
const errorMessage = err.message || '处理失败';
|
||||
setError(errorMessage);
|
||||
|
||||
// 显示错误提示
|
||||
Taro.showToast({
|
||||
title: errorMessage,
|
||||
icon: 'error',
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
return null;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [reset, detectScanType, handleLoginQR, handleVerificationQR]);
|
||||
|
||||
/**
|
||||
* 取消扫码
|
||||
*/
|
||||
const cancel = useCallback(() => {
|
||||
cancelRef.current = true;
|
||||
reset();
|
||||
}, [reset]);
|
||||
|
||||
/**
|
||||
* 检查是否可以进行扫码
|
||||
*/
|
||||
const canScan = useCallback(() => {
|
||||
const userId = Taro.getStorageSync('UserId');
|
||||
const accessToken = Taro.getStorageSync('access_token');
|
||||
return !!(userId && accessToken);
|
||||
}, []);
|
||||
|
||||
// 组件卸载时取消操作
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
cancelRef.current = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
// 状态
|
||||
state,
|
||||
error,
|
||||
result,
|
||||
isLoading,
|
||||
scanType,
|
||||
|
||||
// 方法
|
||||
startScan,
|
||||
cancel,
|
||||
reset,
|
||||
canScan,
|
||||
|
||||
// 便捷状态判断
|
||||
isIdle: state === UnifiedScanState.IDLE,
|
||||
isScanning: state === UnifiedScanState.SCANNING,
|
||||
isProcessing: state === UnifiedScanState.PROCESSING,
|
||||
isSuccess: state === UnifiedScanState.SUCCESS,
|
||||
isError: state === UnifiedScanState.ERROR
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { User } from '@/api/system/user/model';
|
||||
import { getUserInfo, updateUserInfo } from '@/api/layout';
|
||||
import { getUserInfo, updateUserInfo, loginByOpenId } from '@/api/layout';
|
||||
import { TenantId } from '@/config/app';
|
||||
import {getStoredInviteParams, handleInviteRelation} from '@/utils/invite';
|
||||
|
||||
// 用户Hook
|
||||
export const useUser = () => {
|
||||
@@ -9,8 +11,62 @@ export const useUser = () => {
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// 自动登录(通过OpenID)
|
||||
const autoLoginByOpenId = async () => {
|
||||
try {
|
||||
const res = await new Promise<any>((resolve, reject) => {
|
||||
Taro.login({
|
||||
success: (loginRes) => {
|
||||
loginByOpenId({
|
||||
code: loginRes.code,
|
||||
tenantId: TenantId
|
||||
}).then(async (data) => {
|
||||
if (data) {
|
||||
// 保存登录信息
|
||||
saveUserToStorage(data.access_token, data.user);
|
||||
setUser(data.user);
|
||||
setIsLoggedIn(true);
|
||||
|
||||
// 处理邀请关系
|
||||
if (data.user?.userId) {
|
||||
try {
|
||||
const inviteSuccess = await handleInviteRelation(data.user.userId);
|
||||
if (inviteSuccess) {
|
||||
console.log('自动登录时邀请关系建立成功');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('自动登录时处理邀请关系失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
resolve(data.user);
|
||||
} else {
|
||||
reject(new Error('自动登录失败'));
|
||||
}
|
||||
}).catch(_ => {
|
||||
// 首次注册,跳转到邀请注册页面
|
||||
const pages = Taro.getCurrentPages();
|
||||
const currentPage = pages[pages.length - 1];
|
||||
const inviteParams = getStoredInviteParams()
|
||||
if (currentPage?.route !== 'dealer/apply/add' && inviteParams?.inviter) {
|
||||
return Taro.navigateTo({
|
||||
url: '/dealer/apply/add'
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
return res;
|
||||
} catch (error) {
|
||||
console.error('自动登录失败:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// 从本地存储加载用户数据
|
||||
const loadUserFromStorage = () => {
|
||||
const loadUserFromStorage = async () => {
|
||||
try {
|
||||
const token = Taro.getStorageSync('access_token');
|
||||
const userData = Taro.getStorageSync('User');
|
||||
@@ -26,9 +82,14 @@ export const useUser = () => {
|
||||
setIsLoggedIn(true);
|
||||
setUser({ userId, tenantId } as User);
|
||||
} else {
|
||||
// 没有本地登录信息,尝试自动登录
|
||||
console.log('没有本地登录信息,尝试自动登录...');
|
||||
const autoLoginResult = await autoLoginByOpenId();
|
||||
if (!autoLoginResult) {
|
||||
setUser(null);
|
||||
setIsLoggedIn(false);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载用户数据失败:', error);
|
||||
setUser(null);
|
||||
@@ -43,9 +104,24 @@ export const useUser = () => {
|
||||
try {
|
||||
Taro.setStorageSync('access_token', token);
|
||||
Taro.setStorageSync('User', userInfo);
|
||||
|
||||
// 确保关键字段不为空时才保存,避免覆盖现有数据
|
||||
if (userInfo.userId) {
|
||||
Taro.setStorageSync('UserId', userInfo.userId);
|
||||
}
|
||||
if (userInfo.tenantId) {
|
||||
Taro.setStorageSync('TenantId', userInfo.tenantId);
|
||||
}
|
||||
if (userInfo.phone) {
|
||||
Taro.setStorageSync('Phone', userInfo.phone);
|
||||
}
|
||||
// 保存头像和昵称信息
|
||||
if (userInfo.avatar) {
|
||||
Taro.setStorageSync('Avatar', userInfo.avatar);
|
||||
}
|
||||
if (userInfo.nickname) {
|
||||
Taro.setStorageSync('Nickname', userInfo.nickname);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存用户数据失败:', error);
|
||||
}
|
||||
@@ -114,9 +190,16 @@ export const useUser = () => {
|
||||
}
|
||||
|
||||
try {
|
||||
const updatedUser = { ...user, ...userData };
|
||||
// 先获取最新的用户信息,确保我们有完整的数据
|
||||
const latestUserInfo = await getUserInfo();
|
||||
|
||||
// 合并最新的用户信息和要更新的数据
|
||||
const updatedUser = { ...latestUserInfo, ...userData };
|
||||
|
||||
// 调用API更新用户信息
|
||||
await updateUserInfo(updatedUser);
|
||||
|
||||
// 更新本地状态
|
||||
setUser(updatedUser);
|
||||
|
||||
// 更新本地存储
|
||||
@@ -216,7 +299,10 @@ export const useUser = () => {
|
||||
|
||||
// 初始化时加载用户数据
|
||||
useEffect(() => {
|
||||
loadUserFromStorage();
|
||||
loadUserFromStorage().catch(error => {
|
||||
console.error('初始化用户数据失败:', error);
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
@@ -231,6 +317,7 @@ export const useUser = () => {
|
||||
fetchUserInfo,
|
||||
updateUser,
|
||||
loadUserFromStorage,
|
||||
autoLoginByOpenId,
|
||||
|
||||
// 工具方法
|
||||
hasPermission,
|
||||
|
||||
@@ -39,6 +39,10 @@ export const useUserData = (): UseUserDataReturn => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
if(!Taro.getStorageSync('UserId')){
|
||||
return;
|
||||
}
|
||||
|
||||
// 并发请求所有数据
|
||||
const [userDataRes, couponsRes, giftCardsRes] = await Promise.all([
|
||||
getUserInfo(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import Taro, {useShareAppMessage, useShareTimeline, useDidShow} from '@tarojs/taro';
|
||||
import Taro, {useShareAppMessage, useDidShow} from '@tarojs/taro';
|
||||
import {
|
||||
NavBar,
|
||||
Checkbox,
|
||||
@@ -39,15 +39,9 @@ function Cart() {
|
||||
nutuiInputnumberButtonBorderRadius: '4px',
|
||||
}
|
||||
|
||||
useShareTimeline(() => {
|
||||
return {
|
||||
title: '购物车 - 网宿小店'
|
||||
};
|
||||
});
|
||||
|
||||
useShareAppMessage(() => {
|
||||
return {
|
||||
title: '购物车 - 网宿小店',
|
||||
title: '购物车 - 时里院子市集',
|
||||
success: function () {
|
||||
console.log('分享成功');
|
||||
},
|
||||
|
||||
25
src/pages/cms/category/components/ArticleList.tsx
Normal file
25
src/pages/cms/category/components/ArticleList.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import {Image, Cell} from '@nutui/nutui-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
const ArticleList = (props: any) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={'px-3'}>
|
||||
{props.data.map((item, index) => {
|
||||
return (
|
||||
<Cell
|
||||
title={item.title}
|
||||
extra={
|
||||
<Image src={item.image} mode={'aspectFit'} lazyLoad={false} width={100} height="100"/>
|
||||
}
|
||||
key={index}
|
||||
onClick={() => Taro.navigateTo({url: '/cms/detail/index?id=' + item.articleId})}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default ArticleList
|
||||
59
src/pages/cms/category/components/ArticleTabs.tsx
Normal file
59
src/pages/cms/category/components/ArticleTabs.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import {Tabs, Loading} from '@nutui/nutui-react-taro'
|
||||
import {pageCmsArticle} from "@/api/cms/cmsArticle";
|
||||
import {CmsArticle} from "@/api/cms/cmsArticle/model";
|
||||
import ArticleList from "./ArticleList";
|
||||
|
||||
const ArticleTabs = (props: any) => {
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [tab1value, setTab1value] = useState<string | number>('0')
|
||||
const [list, setList] = useState<CmsArticle[]>([])
|
||||
|
||||
const reload = async (value) => {
|
||||
const {data} = props
|
||||
pageCmsArticle({
|
||||
categoryId: data[value].navigationId,
|
||||
page: 1,
|
||||
status: 0,
|
||||
limit: 10
|
||||
}).then((res) => {
|
||||
res && setList(res?.list || [])
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err)
|
||||
})
|
||||
.finally(() => {
|
||||
setTab1value(value)
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reload(0).then()
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Loading className={'px-2'}>加载中</Loading>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tabs
|
||||
value={tab1value}
|
||||
onChange={(value) => {
|
||||
reload(value).then()
|
||||
}}
|
||||
>
|
||||
{props.data?.map((item, index) => {
|
||||
return (
|
||||
<Tabs.TabPane title={item.categoryName} key={index}/>
|
||||
)
|
||||
})}
|
||||
</Tabs>
|
||||
<ArticleList data={list}/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default ArticleTabs
|
||||
31
src/pages/cms/category/components/Banner.tsx
Normal file
31
src/pages/cms/category/components/Banner.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Swiper } from '@nutui/nutui-react-taro'
|
||||
import {CmsAd} from "@/api/cms/cmsAd/model";
|
||||
import {Image} from '@nutui/nutui-react-taro'
|
||||
import {getCmsAd} from "@/api/cms/cmsAd";
|
||||
|
||||
const MyPage = () => {
|
||||
const [item, setItem] = useState<CmsAd>()
|
||||
const reload = () => {
|
||||
getCmsAd(439).then(data => {
|
||||
setItem(data)
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reload()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Swiper defaultValue={0} height={item?.height} indicator style={{ height: item?.height + 'px', display: 'none' }}>
|
||||
{item?.imageList?.map((item) => (
|
||||
<Swiper.Item key={item}>
|
||||
<Image width="100%" height="100%" src={item.url} mode={'scaleToFill'} lazyLoad={false} style={{ height: item.height + 'px' }} />
|
||||
</Swiper.Item>
|
||||
))}
|
||||
</Swiper>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default MyPage
|
||||
4
src/pages/cms/category/index.config.ts
Normal file
4
src/pages/cms/category/index.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '文章列表',
|
||||
navigationBarTextStyle: 'black'
|
||||
})
|
||||
0
src/pages/cms/category/index.scss
Normal file
0
src/pages/cms/category/index.scss
Normal file
71
src/pages/cms/category/index.tsx
Normal file
71
src/pages/cms/category/index.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
import {useShareAppMessage} from "@tarojs/taro"
|
||||
import {Loading} from '@nutui/nutui-react-taro'
|
||||
import {useEffect, useState} from "react"
|
||||
import {useRouter} from '@tarojs/taro'
|
||||
import {getCmsNavigation, listCmsNavigation} from "@/api/cms/cmsNavigation";
|
||||
import {CmsNavigation} from "@/api/cms/cmsNavigation/model";
|
||||
import {pageCmsArticle} from "@/api/cms/cmsArticle";
|
||||
import {CmsArticle} from "@/api/cms/cmsArticle/model";
|
||||
import ArticleList from './components/ArticleList'
|
||||
import ArticleTabs from "./components/ArticleTabs";
|
||||
import './index.scss'
|
||||
|
||||
function Category() {
|
||||
const {params} = useRouter();
|
||||
const [categoryId, setCategoryId] = useState<number>(0)
|
||||
const [category, setCategory] = useState<CmsNavigation[]>([])
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [nav, setNav] = useState<CmsNavigation>()
|
||||
const [list, setList] = useState<CmsArticle[]>([])
|
||||
|
||||
const reload = async () => {
|
||||
// 1.加载远程数据
|
||||
const id = Number(params.id || 4328)
|
||||
const nav = await getCmsNavigation(id)
|
||||
const categoryList = await listCmsNavigation({parentId: id})
|
||||
const shopGoods = await pageCmsArticle({categoryId: id})
|
||||
|
||||
// 2.赋值
|
||||
setCategoryId(id)
|
||||
setNav(nav)
|
||||
setList(shopGoods?.list || [])
|
||||
setCategory(categoryList)
|
||||
Taro.setNavigationBarTitle({
|
||||
title: `${nav?.categoryName}`
|
||||
})
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
reload().then(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}, []);
|
||||
|
||||
useShareAppMessage(() => {
|
||||
return {
|
||||
title: `${nav?.categoryName}_时里院子市集`,
|
||||
path: `/shop/category/index?id=${categoryId}`,
|
||||
success: function () {
|
||||
console.log('分享成功');
|
||||
},
|
||||
fail: function () {
|
||||
console.log('分享失败');
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Loading className={'px-2'}>加载中</Loading>
|
||||
)
|
||||
}
|
||||
|
||||
if(category.length > 0){
|
||||
return <ArticleTabs data={category}/>
|
||||
}
|
||||
|
||||
return <ArticleList data={list}/>
|
||||
}
|
||||
|
||||
export default Category
|
||||
3
src/pages/cms/detail/index.config.ts
Normal file
3
src/pages/cms/detail/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '文章详情'
|
||||
})
|
||||
0
src/pages/cms/detail/index.scss
Normal file
0
src/pages/cms/detail/index.scss
Normal file
53
src/pages/cms/detail/index.tsx
Normal file
53
src/pages/cms/detail/index.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
import {useEffect, useState} from 'react'
|
||||
import {useRouter} from '@tarojs/taro'
|
||||
import {Loading} from '@nutui/nutui-react-taro'
|
||||
import {View, RichText} from '@tarojs/components'
|
||||
import {wxParse} from "@/utils/common";
|
||||
import {getCmsArticle} from "@/api/cms/cmsArticle";
|
||||
import {CmsArticle} from "@/api/cms/cmsArticle/model"
|
||||
import Line from "@/components/Gap";
|
||||
import './index.scss'
|
||||
|
||||
function Detail() {
|
||||
const {params} = useRouter();
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
// 文章详情
|
||||
const [item, setItem] = useState<CmsArticle>()
|
||||
const reload = async () => {
|
||||
const item = await getCmsArticle(Number(params.id))
|
||||
|
||||
if (item) {
|
||||
item.content = wxParse(item.content)
|
||||
setItem(item)
|
||||
Taro.setNavigationBarTitle({
|
||||
title: `${item?.categoryName}`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reload().then(() => {
|
||||
setLoading(false)
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Loading className={'px-2'}>加载中</Loading>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'bg-white'}>
|
||||
<div className={'p-4 font-bold text-lg'}>{item?.title}</div>
|
||||
<div className={'text-gray-400 text-sm px-4 '}>{item?.createTime}</div>
|
||||
<View className={'content p-4'}>
|
||||
<RichText nodes={item?.content}/>
|
||||
</View>
|
||||
<Line height={44}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Detail
|
||||
@@ -1,31 +1,123 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Swiper } from '@nutui/nutui-react-taro'
|
||||
import {useEffect, useState} from 'react'
|
||||
import {View} from '@tarojs/components'
|
||||
import {Swiper} from '@nutui/nutui-react-taro'
|
||||
import {CmsAd} from "@/api/cms/cmsAd/model";
|
||||
import {Image} from '@nutui/nutui-react-taro'
|
||||
import {getCmsAd} from "@/api/cms/cmsAd";
|
||||
import {getCmsAdByCode} from "@/api/cms/cmsAd";
|
||||
import navTo from "@/utils/common";
|
||||
import {ShopGoods} from "@/api/shop/shopGoods/model";
|
||||
import {listShopGoods} from "@/api/shop/shopGoods";
|
||||
|
||||
|
||||
const MyPage = () => {
|
||||
const [item, setItem] = useState<CmsAd>()
|
||||
const reload = () => {
|
||||
getCmsAd(439).then(data => {
|
||||
setItem(data)
|
||||
const [carouselData, setCarouselData] = useState<CmsAd>()
|
||||
// const [hotToday, setHotToday] = useState<CmsAd>()
|
||||
// const [groupBuy, setGroupBuy] = useState<CmsAd>()
|
||||
const [hotGoods, setHotGoods] = useState<ShopGoods[]>([])
|
||||
|
||||
// 加载数据
|
||||
const loadData = () => {
|
||||
// 轮播图
|
||||
getCmsAdByCode('flash').then(data => {
|
||||
setCarouselData(data)
|
||||
})
|
||||
// 今日热卖素材(上层图片)
|
||||
// getCmsAd(444).then(data => {
|
||||
// setHotToday(data)
|
||||
// })
|
||||
// 社区拼团素材(下层图片)
|
||||
// getCmsAd(445).then(data => {
|
||||
// setGroupBuy(data)
|
||||
// })
|
||||
// 今日热卖
|
||||
listShopGoods({categoryId: 4424, limit: 2}).then(data => {
|
||||
setHotGoods(data)
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reload()
|
||||
loadData()
|
||||
}, [])
|
||||
|
||||
// 轮播图高度,默认200px
|
||||
const carouselHeight = carouselData?.height || 200;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Swiper defaultValue={0} height={item?.height} indicator style={{ height: item?.height + 'px', display: 'none' }}>
|
||||
{item?.imageList?.map((item) => (
|
||||
<Swiper.Item key={item}>
|
||||
<Image width="100%" height="100%" src={item.url} mode={'scaleToFill'} lazyLoad={false} style={{ height: item.height + 'px' }} />
|
||||
<View className="flex p-2 justify-between" style={{height: `${carouselHeight}px`}}>
|
||||
{/* 左侧轮播图区域 */}
|
||||
<View style={{width: '50%', height: '100%'}}>
|
||||
<Swiper
|
||||
defaultValue={0}
|
||||
height={carouselHeight}
|
||||
indicator
|
||||
style={{height: `${carouselHeight}px`}}
|
||||
>
|
||||
{carouselData?.imageList?.map((img, index) => (
|
||||
<Swiper.Item key={index}>
|
||||
<Image
|
||||
width="100%"
|
||||
height="100%"
|
||||
src={img.url}
|
||||
mode={'scaleToFill'}
|
||||
onClick={() => navTo(`${img.path}`)}
|
||||
lazyLoad={false}
|
||||
style={{height: `${carouselHeight}px`, borderRadius: '4px'}}
|
||||
/>
|
||||
</Swiper.Item>
|
||||
))}
|
||||
</Swiper>
|
||||
</>
|
||||
</View>
|
||||
|
||||
{/* 右侧上下图片区域 - 从API获取数据 */}
|
||||
<View className="flex flex-col" style={{width: '50%', height: '100%'}}>
|
||||
{/* 上层图片 - 使用今日热卖素材 */}
|
||||
<View className={'ml-2 bg-white rounded-lg'}>
|
||||
<View className={'px-3 my-2 font-bold text-sm'}>今日热卖</View>
|
||||
<View className={'px-3 flex'} style={{
|
||||
height: '110px'
|
||||
}}>
|
||||
{
|
||||
hotGoods.map(item => (
|
||||
<View className={'item flex flex-col mr-4'}>
|
||||
<Image
|
||||
width={70}
|
||||
height={70}
|
||||
src={item.image}
|
||||
mode={'scaleToFill'}
|
||||
lazyLoad={false}
|
||||
style={{
|
||||
borderRadius: '4px'
|
||||
}}
|
||||
onClick={() => navTo('/shop/category/index?id=4424')}
|
||||
/>
|
||||
<View className={'text-xs py-2'}>到手价¥{item.price}</View>
|
||||
</View>
|
||||
))
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 下层图片 - 使用社区拼团素材 */}
|
||||
<View className={'ml-2 bg-white rounded-lg mt-3'}>
|
||||
<View className={'px-3 my-2 font-bold text-sm'}>走进社区</View>
|
||||
<View className={'rounded-lg px-3 pb-3'}>
|
||||
<Image
|
||||
width={'100%'}
|
||||
height={100}
|
||||
src={'https://oss.wsdns.cn/20250919/941c99899e694a7798cab3bb28f1f238.png?x-oss-process=image/resize,m_fixed,w_750/quality,Q_90'}
|
||||
mode={'scaleToFill'}
|
||||
lazyLoad={false}
|
||||
style={{
|
||||
borderRadius: '4px'
|
||||
}}
|
||||
onClick={() => navTo('cms/detail/index?id=10109')}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default MyPage
|
||||
|
||||
|
||||
@@ -1,21 +1,56 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import {Image} from '@nutui/nutui-react-taro'
|
||||
import {Image, Swiper, SwiperItem, Empty} from '@nutui/nutui-react-taro'
|
||||
import {Share} from '@nutui/icons-react-taro'
|
||||
import {View, Text} from '@tarojs/components';
|
||||
import Taro, {useShareAppMessage, useShareTimeline} from "@tarojs/taro";
|
||||
import Taro from "@tarojs/taro";
|
||||
import {Tabs} from '@nutui/nutui-react-taro'
|
||||
import {ShopGoods} from "@/api/shop/shopGoods/model";
|
||||
import {pageShopGoods} from "@/api/shop/shopGoods";
|
||||
import './BestSellers.scss'
|
||||
|
||||
|
||||
const BestSellers = () => {
|
||||
const [tab1value, setTab1value] = useState<string | number>('0')
|
||||
const [list, setList] = useState<ShopGoods[]>([])
|
||||
const [goods, setGoods] = useState<ShopGoods>()
|
||||
const [goods, setGoods] = useState<ShopGoods | null>(null)
|
||||
// 轮播图固定高度,可根据需求调整
|
||||
const SWIPER_HEIGHT = 180;
|
||||
|
||||
const reload = () => {
|
||||
pageShopGoods({}).then(res => {
|
||||
setList(res?.list || []);
|
||||
})
|
||||
const processGoodsItem = (item: ShopGoods) => {
|
||||
const pics: string[] = [];
|
||||
// 添加主图
|
||||
if (item.image) {
|
||||
pics.push(item.image);
|
||||
}
|
||||
// 处理附加图片
|
||||
if (item.files) {
|
||||
try {
|
||||
// 解析文件字符串为对象
|
||||
const files = typeof item.files === "string"
|
||||
? JSON.parse(item.files)
|
||||
: item.files;
|
||||
|
||||
// 收集所有图片URL
|
||||
Object.values(files).forEach(file => {
|
||||
if (file?.url) {
|
||||
pics.push(file.url);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('解析文件失败:', error);
|
||||
}
|
||||
}
|
||||
// 返回新对象,避免直接修改原对象
|
||||
return {...item, pics};
|
||||
};
|
||||
|
||||
// 处理商品列表
|
||||
const goods = (res?.list || []).map(processGoodsItem);
|
||||
setList(goods);
|
||||
}).catch(err => {
|
||||
console.error('获取商品列表失败:', err);
|
||||
});
|
||||
}
|
||||
|
||||
// 处理分享点击
|
||||
@@ -24,14 +59,12 @@ const BestSellers = () => {
|
||||
|
||||
// 显示分享选项菜单
|
||||
Taro.showActionSheet({
|
||||
itemList: ['分享给好友', '分享到朋友圈'],
|
||||
itemList: ['分享给好友'],
|
||||
success: (res) => {
|
||||
if (res.tapIndex === 0) {
|
||||
// 分享给好友 - 触发转发
|
||||
Taro.showShareMenu({
|
||||
withShareTicket: true,
|
||||
success: () => {
|
||||
// 提示用户点击右上角分享
|
||||
Taro.showToast({
|
||||
title: '请点击右上角分享给好友',
|
||||
icon: 'none',
|
||||
@@ -39,13 +72,6 @@ const BestSellers = () => {
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (res.tapIndex === 1) {
|
||||
// 分享到朋友圈
|
||||
Taro.showToast({
|
||||
title: '请点击右上角分享到朋友圈',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
@@ -55,53 +81,84 @@ const BestSellers = () => {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reload()
|
||||
}, [])
|
||||
reload();
|
||||
}, []);
|
||||
|
||||
// 分享给好友
|
||||
useShareAppMessage(() => {
|
||||
// 配置分享内容
|
||||
Taro.useShareAppMessage(() => {
|
||||
if (goods) {
|
||||
return {
|
||||
title: goods?.name || '精选商品',
|
||||
path: `/shop/goodsDetail/index?id=${goods?.goodsId}`,
|
||||
imageUrl: goods?.image, // 分享图片
|
||||
success: function (res: any) {
|
||||
console.log('分享成功', res);
|
||||
Taro.showToast({
|
||||
title: '分享成功',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
},
|
||||
fail: function (res: any) {
|
||||
console.log('分享失败', res);
|
||||
Taro.showToast({
|
||||
title: '分享失败',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
}
|
||||
title: goods.name,
|
||||
path: `/shop/goodsDetail/index?id=${goods.goodsId}`,
|
||||
imageUrl: goods.image || ''
|
||||
};
|
||||
});
|
||||
|
||||
// 分享到朋友圈
|
||||
useShareTimeline(() => {
|
||||
}
|
||||
return {
|
||||
title: `${goods?.name || '精选商品'} - 网宿小店`,
|
||||
path: `/shop/goodsDetail/index?id=${goods?.goodsId}`,
|
||||
imageUrl: goods?.image
|
||||
title: '热销商品',
|
||||
path: '/pages/index/index'
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<View className={'py-3'}>
|
||||
<View className={'flex flex-col justify-between items-center rounded-lg px-2'}>
|
||||
{list?.map((item, index) => {
|
||||
return (
|
||||
<View key={index} className={'flex flex-col rounded-lg bg-white shadow-sm w-full mb-5'}>
|
||||
<Image src={item.image} mode={'aspectFit'} lazyLoad={false}
|
||||
radius="10px 10px 0 0" height="180"
|
||||
onClick={() => Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}/>
|
||||
<Tabs
|
||||
value={tab1value}
|
||||
className={'w-full'}
|
||||
onChange={(value) => {
|
||||
setTab1value(value)
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: '#fff',
|
||||
}}
|
||||
activeType="smile"
|
||||
>
|
||||
<Tabs.TabPane title="今日主推">
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane title="即将到期">
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane title="活动预告">
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
|
||||
{tab1value == '0' && list?.map((item) => (
|
||||
<View
|
||||
key={item.goodsId || item.id} // 使用商品唯一ID作为key
|
||||
className={'flex flex-col rounded-lg bg-white shadow-sm w-full mb-5'}
|
||||
>
|
||||
{/* 轮播图组件 */}
|
||||
{item.pics && item.pics.length > 0 ? (
|
||||
<Swiper
|
||||
defaultValue={0}
|
||||
height={SWIPER_HEIGHT}
|
||||
indicator
|
||||
className="swiper-container"
|
||||
autoPlay
|
||||
interval={3000}
|
||||
>
|
||||
{item.pics.map((pic, picIndex) => (
|
||||
<SwiperItem key={picIndex}>
|
||||
<Image
|
||||
radius="12px 12px 0 0"
|
||||
height={SWIPER_HEIGHT}
|
||||
src={pic}
|
||||
mode={'aspectFill'} // 使用aspectFill保持比例并填充容器
|
||||
lazyLoad
|
||||
onClick={() => Taro.navigateTo({
|
||||
url: `/shop/goodsDetail/index?id=${item.goodsId}`
|
||||
})}
|
||||
className="swiper-image"
|
||||
/>
|
||||
</SwiperItem>
|
||||
))}
|
||||
</Swiper>
|
||||
) : (
|
||||
// 没有图片时显示占位图
|
||||
<View className="no-image-placeholder" style={{height: `${SWIPER_HEIGHT}px`}}>
|
||||
<Text className="placeholder-text">暂无图片</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View className={'flex flex-col p-2 rounded-lg'}>
|
||||
<View>
|
||||
<View className={'car-no text-sm'}>{item.name}</View>
|
||||
@@ -115,7 +172,7 @@ const BestSellers = () => {
|
||||
<Text className={'font-bold text-2xl'}>{item.price}</Text>
|
||||
</View>
|
||||
<View className={'buy-btn'}>
|
||||
<View className={'cart-icon flex items-center'}>
|
||||
<View className={'cart-icon flex items-center hidden'}>
|
||||
<View
|
||||
className={'flex flex-col justify-center items-center text-white px-3 gap-1 text-nowrap whitespace-nowrap cursor-pointer'}
|
||||
onClick={() => handleShare(item)}
|
||||
@@ -123,19 +180,36 @@ const BestSellers = () => {
|
||||
<Share size={20}/>
|
||||
</View>
|
||||
</View>
|
||||
<Text className={'text-white pl-4 pr-5'}
|
||||
onClick={() => Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}>购买
|
||||
<Text
|
||||
className={'text-white pl-5 pr-5'}
|
||||
onClick={() => Taro.navigateTo({
|
||||
url: `/shop/goodsDetail/index?id=${item.goodsId}`
|
||||
})}
|
||||
>
|
||||
购买
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
})}
|
||||
))}
|
||||
|
||||
{
|
||||
tab1value == '1' && <Empty description="暂无相关商品" style={{
|
||||
background: 'transparent',
|
||||
}}/>
|
||||
}
|
||||
|
||||
{
|
||||
tab1value == '2' && <Empty description="暂无相关商品" style={{
|
||||
background: 'transparent',
|
||||
}}/>
|
||||
}
|
||||
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default BestSellers
|
||||
|
||||
@@ -8,7 +8,7 @@ import {TenantId} from "@/config/app";
|
||||
import {getOrganization} from "@/api/system/organization";
|
||||
import {myUserVerify} from "@/api/system/userVerify";
|
||||
import { useShopInfo } from '@/hooks/useShopInfo';
|
||||
import {handleInviteRelation} from "@/utils/invite";
|
||||
import {handleInviteRelation, getStoredInviteParams} from "@/utils/invite";
|
||||
import {View,Text} from '@tarojs/components'
|
||||
import MySearch from "./MySearch";
|
||||
import './Header.scss';
|
||||
@@ -88,6 +88,22 @@ const Header = (props: any) => {
|
||||
/* 获取用户手机号 */
|
||||
const handleGetPhoneNumber = ({detail}: {detail: {code?: string, encryptedData?: string, iv?: string}}) => {
|
||||
const {code, encryptedData, iv} = detail
|
||||
|
||||
// 防重复登录检查
|
||||
const loginKey = 'login_in_progress'
|
||||
const loginInProgress = Taro.getStorageSync(loginKey)
|
||||
|
||||
if (loginInProgress && Date.now() - loginInProgress < 5000) { // 5秒内防重
|
||||
return
|
||||
}
|
||||
|
||||
// 标记登录开始
|
||||
Taro.setStorageSync(loginKey, Date.now())
|
||||
|
||||
// 获取存储的邀请参数
|
||||
const inviteParams = getStoredInviteParams()
|
||||
const refereeId = inviteParams?.inviter ? parseInt(inviteParams.inviter) : 0
|
||||
|
||||
Taro.login({
|
||||
success: function () {
|
||||
if (code) {
|
||||
@@ -99,7 +115,7 @@ const Header = (props: any) => {
|
||||
encryptedData,
|
||||
iv,
|
||||
notVerifyPhone: true,
|
||||
refereeId: 0,
|
||||
refereeId: refereeId, // 使用解析出的推荐人ID
|
||||
sceneType: 'save_referee',
|
||||
tenantId: TenantId
|
||||
},
|
||||
@@ -108,6 +124,9 @@ const Header = (props: any) => {
|
||||
TenantId
|
||||
},
|
||||
success: async function (res) {
|
||||
// 清除登录防重标记
|
||||
Taro.removeStorageSync('login_in_progress')
|
||||
|
||||
if (res.data.code == 1) {
|
||||
Taro.showToast({
|
||||
title: res.data.message,
|
||||
@@ -124,14 +143,7 @@ const Header = (props: any) => {
|
||||
// 处理邀请关系
|
||||
if (res.data.data.user?.userId) {
|
||||
try {
|
||||
const inviteSuccess = await handleInviteRelation(res.data.data.user.userId)
|
||||
if (inviteSuccess) {
|
||||
Taro.showToast({
|
||||
title: '邀请关系建立成功',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
await handleInviteRelation(res.data.data.user.userId)
|
||||
} catch (error) {
|
||||
console.error('处理邀请关系失败:', error)
|
||||
}
|
||||
@@ -141,6 +153,10 @@ const Header = (props: any) => {
|
||||
Taro.reLaunch({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
},
|
||||
fail: function() {
|
||||
// 清除登录防重标记
|
||||
Taro.removeStorageSync('login_in_progress')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
@@ -157,9 +173,10 @@ const Header = (props: any) => {
|
||||
return (
|
||||
<>
|
||||
<View className={'fixed top-0 header-bg'} style={{
|
||||
height: !props.stickyStatus ? '180px' : '148px',
|
||||
height: !props.stickyStatus ? '180px' : 'auto',
|
||||
paddingBottom: '12px'
|
||||
}}>
|
||||
<MySearch/>
|
||||
<MySearch statusBarHeight={statusBarHeight} />
|
||||
{/*{!props.stickyStatus && <MySearch done={reload}/>}*/}
|
||||
</View>
|
||||
<NavBar
|
||||
@@ -190,6 +207,7 @@ const Header = (props: any) => {
|
||||
<TriangleDown className={'text-white'} size={9}/>
|
||||
</View>
|
||||
)}>
|
||||
{/*<QRLoginButton />*/}
|
||||
</NavBar>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ import {Input, Radio, Button} from '@nutui/nutui-react-taro'
|
||||
import {TenantId} from "@/config/app";
|
||||
import './login.scss';
|
||||
import {saveStorageByLoginUser} from "@/utils/server";
|
||||
import {handleInviteRelation} from "@/utils/invite";
|
||||
import {handleInviteRelation, getStoredInviteParams} from "@/utils/invite";
|
||||
|
||||
// 微信获取手机号回调参数类型
|
||||
interface GetPhoneNumberDetail {
|
||||
@@ -40,6 +40,11 @@ const Login = (props: LoginProps) => {
|
||||
/* 获取用户手机号 */
|
||||
const handleGetPhoneNumber = ({detail}: GetPhoneNumberEvent) => {
|
||||
const {code, encryptedData, iv} = detail
|
||||
|
||||
// 获取存储的邀请参数
|
||||
const inviteParams = getStoredInviteParams()
|
||||
const refereeId = inviteParams?.inviter ? parseInt(inviteParams.inviter) : 0
|
||||
|
||||
Taro.login({
|
||||
success: function () {
|
||||
if (code) {
|
||||
@@ -51,7 +56,7 @@ const Login = (props: LoginProps) => {
|
||||
encryptedData,
|
||||
iv,
|
||||
notVerifyPhone: true,
|
||||
refereeId: 0,
|
||||
refereeId: refereeId, // 使用解析出的推荐人ID
|
||||
sceneType: 'save_referee',
|
||||
tenantId: TenantId
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@ import {useState} from "react";
|
||||
import Taro from '@tarojs/taro';
|
||||
import { goTo } from '@/utils/navigation';
|
||||
|
||||
function MySearch() {
|
||||
function MySearch(props: any) {
|
||||
const [keywords, setKeywords] = useState<string>('')
|
||||
|
||||
const onKeywords = (keywords: string) => {
|
||||
@@ -39,7 +39,7 @@ function MySearch() {
|
||||
background: '#ffffff',
|
||||
padding: '0 5px',
|
||||
borderRadius: '20px',
|
||||
marginTop: '100px',
|
||||
marginTop: `${props.statusBarHeight + 50}px`,
|
||||
}}
|
||||
>
|
||||
<Search size={18} className={'ml-2 text-gray-400'}/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Header from './Header';
|
||||
import BestSellers from './BestSellers';
|
||||
import Taro from '@tarojs/taro';
|
||||
import {useShareAppMessage, useShareTimeline} from "@tarojs/taro"
|
||||
import {useShareAppMessage} from "@tarojs/taro"
|
||||
import {useEffect, useState} from "react";
|
||||
import {getShopInfo} from "@/api/layout";
|
||||
import {Sticky} from '@nutui/nutui-react-taro'
|
||||
@@ -16,22 +16,28 @@ function Home() {
|
||||
// 吸顶状态
|
||||
const [stickyStatus, setStickyStatus] = useState<boolean>(false)
|
||||
|
||||
useShareTimeline(() => {
|
||||
return {
|
||||
title: '网宿小店 - 网宿软件',
|
||||
path: `/pages/index/index`
|
||||
};
|
||||
});
|
||||
|
||||
useShareAppMessage(() => {
|
||||
// 获取当前用户ID,用于生成邀请链接
|
||||
const userId = Taro.getStorageSync('UserId');
|
||||
|
||||
return {
|
||||
title: '网宿小店 - 网宿软件',
|
||||
path: `/pages/index/index`,
|
||||
path: userId ? `/pages/index/index?inviter=${userId}&source=share&t=${Date.now()}` : `/pages/index/index`,
|
||||
success: function () {
|
||||
console.log('分享成功');
|
||||
console.log('首页分享成功');
|
||||
Taro.showToast({
|
||||
title: '分享成功',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
},
|
||||
fail: function () {
|
||||
console.log('分享失败');
|
||||
console.log('首页分享失败');
|
||||
Taro.showToast({
|
||||
title: '分享失败',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -89,18 +95,32 @@ function Home() {
|
||||
|
||||
})
|
||||
|
||||
// 检查是否有待处理的邀请关系
|
||||
// 检查是否有待处理的邀请关系 - 异步处理,不阻塞页面加载
|
||||
if (hasPendingInvite()) {
|
||||
console.log('检测到待处理的邀请关系')
|
||||
// 延迟处理,确保用户信息已加载
|
||||
// 延迟处理,确保用户信息已加载,并设置超时保护
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const success = await checkAndHandleInviteRelation()
|
||||
// 设置超时保护,避免长时间等待
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('邀请关系处理超时')), 8000)
|
||||
);
|
||||
|
||||
const invitePromise = checkAndHandleInviteRelation();
|
||||
|
||||
const success = await Promise.race([invitePromise, timeoutPromise]);
|
||||
if (success) {
|
||||
console.log('首页邀请关系处理成功')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('首页邀请关系处理失败:', error)
|
||||
// 邀请关系处理失败不应该影响页面正常显示
|
||||
// 可以选择清除邀请参数,避免重复尝试
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
if (errorMessage?.includes('超时')) {
|
||||
console.log('邀请关系处理超时,清除邀请参数')
|
||||
// 可以选择清除邀请参数或稍后重试
|
||||
}
|
||||
}
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
3
src/pages/menu/menu.config.ts
Normal file
3
src/pages/menu/menu.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '菜单'
|
||||
})
|
||||
81
src/pages/menu/menu.tsx
Normal file
81
src/pages/menu/menu.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import Taro from '@tarojs/taro';
|
||||
import {pageCmsArticle} from "@/api/cms/cmsArticle";
|
||||
import {CmsArticle} from "@/api/cms/cmsArticle/model";
|
||||
import {NavBar, Cell} from '@nutui/nutui-react-taro';
|
||||
import {Text} from '@tarojs/components';
|
||||
import {ArrowRight, ImageRectangle, Coupon, Follow} from '@nutui/icons-react-taro'
|
||||
import navTo from "@/utils/common";
|
||||
|
||||
/**
|
||||
* 文章终极列表
|
||||
* @constructor
|
||||
*/
|
||||
const Menu = () => {
|
||||
const [statusBarHeight, setStatusBarHeight] = useState<number>()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [list, setList] = useState<CmsArticle[]>()
|
||||
|
||||
const reload = async () => {
|
||||
setLoading(true)
|
||||
const article = await pageCmsArticle({categoryId: 4289, status: 0})
|
||||
if (article) {
|
||||
setList(article?.list)
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
Taro.getSystemInfo({
|
||||
success: (res) => {
|
||||
setStatusBarHeight(res.statusBarHeight)
|
||||
},
|
||||
})
|
||||
reload().then(() => {
|
||||
console.log('初始化完成')
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading && (<div>暂无数据</div>)}
|
||||
<NavBar
|
||||
fixed={false}
|
||||
style={{marginTop: `${statusBarHeight}px`, backgroundColor: 'transparent'}}
|
||||
onBackClick={() => {
|
||||
}}
|
||||
>
|
||||
<span>发现</span>
|
||||
</NavBar>
|
||||
{list && (
|
||||
<>
|
||||
<Cell title={
|
||||
<div style={{display: 'inline-flex', alignItems: 'center'}}>
|
||||
<ImageRectangle size={18}/>
|
||||
<Text className={'pl-3'} style={{fontSize: '16px'}}>好物推荐</Text>
|
||||
</div>
|
||||
} extra={<ArrowRight color="#cccccc" size={18}/>}/>
|
||||
<Cell
|
||||
title={
|
||||
<div style={{display: 'inline-flex', alignItems: 'center'}}>
|
||||
<Coupon size={18}/>
|
||||
<span className={'pl-3'} style={{fontSize: '16px'}}>权益中心</span>
|
||||
</div>
|
||||
}
|
||||
extra={<ArrowRight color="#cccccc" size={18}/>}
|
||||
onClick={() => {
|
||||
navTo('/shop/shopArticle/index', true)
|
||||
}}
|
||||
/>
|
||||
<Cell title={
|
||||
<div style={{display: 'inline-flex', alignItems: 'center'}}>
|
||||
<Follow size={18}/>
|
||||
<span className={'pl-3'} style={{fontSize: '16px'}}>我的收藏</span>
|
||||
</div>
|
||||
} extra={<ArrowRight color="#cccccc" size={18}/>}/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default Menu
|
||||
@@ -6,7 +6,7 @@ import {useUser} from '@/hooks/useUser'
|
||||
import {useEffect} from "react";
|
||||
import {useDealerUser} from "@/hooks/useDealerUser";
|
||||
|
||||
const UserCell = () => {
|
||||
const IsDealer = () => {
|
||||
const {isSuperAdmin} = useUser();
|
||||
const {dealerUser} = useDealerUser()
|
||||
|
||||
@@ -20,7 +20,7 @@ const UserCell = () => {
|
||||
if (isSuperAdmin()) {
|
||||
return (
|
||||
<>
|
||||
<View className={'px-4'}>
|
||||
<View className={'px-4'} style={{ marginTop: '8px', position: 'relative', zIndex: 25 }}>
|
||||
<Cell
|
||||
className="nutui-cell-clickable"
|
||||
style={{
|
||||
@@ -46,7 +46,7 @@ const UserCell = () => {
|
||||
if (dealerUser) {
|
||||
return (
|
||||
<>
|
||||
<View className={'px-4'}>
|
||||
<View className={'px-4'} style={{ marginTop: '8px', position: 'relative', zIndex: 25 }}>
|
||||
<Cell
|
||||
className="nutui-cell-clickable"
|
||||
style={{
|
||||
@@ -56,7 +56,7 @@ const UserCell = () => {
|
||||
<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'}>分销中心</Text>
|
||||
{/*<Text className={'text-white opacity-80 pl-3'}>门店核销</Text>*/}
|
||||
</View>
|
||||
}
|
||||
@@ -73,7 +73,7 @@ const UserCell = () => {
|
||||
*/
|
||||
return (
|
||||
<>
|
||||
<View className={'px-4'}>
|
||||
<View className={'px-4'} style={{ marginTop: '8px', position: 'relative', zIndex: 25 }}>
|
||||
<Cell
|
||||
className="nutui-cell-clickable"
|
||||
style={{
|
||||
@@ -82,19 +82,15 @@ const UserCell = () => {
|
||||
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>
|
||||
<Text style={{fontSize: '16px'}} className={'pl-3 text-orange-100 font-medium'}>开通VIP</Text>
|
||||
<Text className={'text-white opacity-80 pl-3'}>享优惠</Text>
|
||||
</View>
|
||||
}
|
||||
extra={
|
||||
<>
|
||||
<Text className={'text-white opacity-80 px-3'}>需医师资格证</Text>
|
||||
<ArrowRight color="#cccccc" size={18}/>
|
||||
</>
|
||||
}
|
||||
extra={<ArrowRight color="#cccccc" size={18}/>}
|
||||
onClick={() => navTo('/dealer/apply/add', true)}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default UserCell
|
||||
export default IsDealer
|
||||
|
||||
@@ -1,28 +1,21 @@
|
||||
import {Button} from '@nutui/nutui-react-taro'
|
||||
import {Avatar, Tag} from '@nutui/nutui-react-taro'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import {Scan} from '@nutui/icons-react-taro';
|
||||
import {Avatar, Tag, Space, Button} from '@nutui/nutui-react-taro'
|
||||
import {View, Text, Image} from '@tarojs/components'
|
||||
import {getUserInfo, getWxOpenId} from '@/api/layout';
|
||||
import Taro from '@tarojs/taro';
|
||||
import {useEffect, useState} from "react";
|
||||
import {useEffect, useState, forwardRef, useImperativeHandle} from "react";
|
||||
import {User} from "@/api/system/user/model";
|
||||
import navTo from "@/utils/common";
|
||||
import {TenantId} from "@/config/app";
|
||||
import {getMyAvailableCoupons} from "@/api/shop/shopUserCoupon";
|
||||
import {useUser} from "@/hooks/useUser";
|
||||
import {useUserData} from "@/hooks/useUserData";
|
||||
import {getStoredInviteParams} from "@/utils/invite";
|
||||
import UnifiedQRButton from "@/components/UnifiedQRButton";
|
||||
|
||||
function UserCard() {
|
||||
const {
|
||||
isAdmin
|
||||
} = useUser();
|
||||
const { data, refresh } = useUserData()
|
||||
const UserCard = forwardRef<any, any>((_, ref) => {
|
||||
const {data, refresh} = useUserData()
|
||||
const {getDisplayName, getRoleName} = useUser();
|
||||
const [IsLogin, setIsLogin] = useState<boolean>(false)
|
||||
const [userInfo, setUserInfo] = useState<User>()
|
||||
const [couponCount, setCouponCount] = useState(0)
|
||||
const [pointsCount, setPointsCount] = useState(0)
|
||||
const [giftCount, setGiftCount] = useState(0)
|
||||
|
||||
// 下拉刷新
|
||||
const handleRefresh = async () => {
|
||||
@@ -33,6 +26,11 @@ function UserCard() {
|
||||
})
|
||||
}
|
||||
|
||||
// 暴露方法给父组件
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleRefresh
|
||||
}))
|
||||
|
||||
useEffect(() => {
|
||||
// Taro.getSetting:获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限。
|
||||
Taro.getSetting({
|
||||
@@ -50,33 +48,6 @@ function UserCard() {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const loadUserStats = (userId: number) => {
|
||||
// 加载优惠券数量
|
||||
getMyAvailableCoupons()
|
||||
.then((coupons: any) => {
|
||||
setCouponCount(coupons?.length || 0)
|
||||
})
|
||||
.catch((error: any) => {
|
||||
console.error('Coupon count error:', error)
|
||||
})
|
||||
|
||||
// 加载积分数量
|
||||
console.log(userId)
|
||||
setPointsCount(0)
|
||||
// getUserPointsStats(userId)
|
||||
// .then((res: any) => {
|
||||
// setPointsCount(res.currentPoints || 0)
|
||||
// })
|
||||
// .catch((error: any) => {
|
||||
// console.error('Points stats error:', error)
|
||||
// })
|
||||
// 加载礼品劵数量
|
||||
setGiftCount(0)
|
||||
// pageUserGiftLog({userId, page: 1, limit: 1}).then(res => {
|
||||
// setGiftCount(res.count || 0)
|
||||
// })
|
||||
}
|
||||
|
||||
const reload = () => {
|
||||
Taro.getUserInfo({
|
||||
success: (res) => {
|
||||
@@ -92,11 +63,6 @@ function UserCard() {
|
||||
setIsLogin(true);
|
||||
Taro.setStorageSync('UserId', data.userId)
|
||||
|
||||
// 加载用户统计数据
|
||||
if (data.userId) {
|
||||
loadUserStats(data.userId)
|
||||
}
|
||||
|
||||
// 获取openId
|
||||
if (!data.openid) {
|
||||
Taro.login({
|
||||
@@ -149,8 +115,13 @@ function UserCard() {
|
||||
};
|
||||
|
||||
/* 获取用户手机号 */
|
||||
const handleGetPhoneNumber = ({detail}: {detail: {code?: string, encryptedData?: string, iv?: string}}) => {
|
||||
const handleGetPhoneNumber = ({detail}: { detail: { code?: string, encryptedData?: string, iv?: string } }) => {
|
||||
const {code, encryptedData, iv} = detail
|
||||
|
||||
// 获取存储的邀请参数
|
||||
const inviteParams = getStoredInviteParams()
|
||||
const refereeId = inviteParams?.inviter ? parseInt(inviteParams.inviter) : 0
|
||||
|
||||
Taro.login({
|
||||
success: function () {
|
||||
if (code) {
|
||||
@@ -162,7 +133,7 @@ function UserCard() {
|
||||
encryptedData,
|
||||
iv,
|
||||
notVerifyPhone: true,
|
||||
refereeId: 0,
|
||||
refereeId: refereeId, // 使用解析出的推荐人ID
|
||||
sceneType: 'save_referee',
|
||||
tenantId: TenantId
|
||||
},
|
||||
@@ -194,26 +165,30 @@ function UserCard() {
|
||||
}
|
||||
|
||||
return (
|
||||
<View className={'header-bg pt-20'}>
|
||||
<View className={'pt-20'}>
|
||||
<View className={'p-4'}>
|
||||
{/* 使用相对定位容器,让个人资料图片可以绝对定位在右上角 */}
|
||||
<View className="relative z-20">
|
||||
<View
|
||||
className={'user-card w-full flex flex-col justify-around rounded-xl shadow-sm'}
|
||||
className={'user-card w-full flex flex-col justify-around rounded-xl'}
|
||||
style={{
|
||||
background: 'linear-gradient(to bottom, #ffffff, #ffffff)', // 这种情况建议使用类名来控制样式(引入外联样式)
|
||||
// width: '720rpx',
|
||||
// margin: '10px auto 0px auto',
|
||||
background: 'linear-gradient(to bottom, #ffffff, #ffffff)',
|
||||
height: '170px',
|
||||
// borderRadius: '22px 22px 0 0',
|
||||
}}
|
||||
>
|
||||
<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} shape="round"/>
|
||||
<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} 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>
|
||||
)
|
||||
}
|
||||
@@ -230,11 +205,30 @@ function UserCard() {
|
||||
) : ''}
|
||||
</View>
|
||||
</View>
|
||||
{isAdmin() && <Scan onClick={() => navTo('/user/store/verification', true)} />}
|
||||
<View className={'mr-4 text-sm px-3 py-1 text-black border-gray-400 border-solid border-2 rounded-3xl'}
|
||||
onClick={() => navTo('/user/profile/profile', true)}>
|
||||
{'个人资料'}
|
||||
</View>
|
||||
<Space style={{
|
||||
marginTop: '30px',
|
||||
marginRight: '10px'
|
||||
}}>
|
||||
{/*统一扫码入口 - 支持登录和核销*/}
|
||||
<UnifiedQRButton
|
||||
text="扫码"
|
||||
size="small"
|
||||
onSuccess={(result) => {
|
||||
console.log('统一扫码成功:', result);
|
||||
// 根据扫码类型给出不同的提示
|
||||
if (result.type === 'verification') {
|
||||
// 核销成功,可以显示更多信息或跳转到详情页
|
||||
Taro.showModal({
|
||||
title: '核销成功',
|
||||
content: `已成功核销的品类:${result.data.goodsName || '礼品卡'},面值¥${result.data.faceValue}`
|
||||
});
|
||||
}
|
||||
}}
|
||||
onError={(error) => {
|
||||
console.error('统一扫码失败:', error);
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
</View>
|
||||
<View className={'flex justify-around mt-1'}>
|
||||
<View className={'item flex justify-center flex-col items-center'}
|
||||
@@ -258,10 +252,27 @@ function UserCard() {
|
||||
</View>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export default UserCard;
|
||||
|
||||
@@ -26,7 +26,7 @@ const UserCell = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<View className={'px-4'}>
|
||||
<View className={'px-4'} style={{ marginTop: '8px', position: 'relative', zIndex: 20 }}>
|
||||
|
||||
<Cell.Group divider={true} description={
|
||||
<View style={{display: 'inline-flex', alignItems: 'center'}}>
|
||||
|
||||
145
src/pages/user/components/UserGrid.tsx
Normal file
145
src/pages/user/components/UserGrid.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
import {Grid, ConfigProvider} from '@nutui/nutui-react-taro'
|
||||
import navTo from "@/utils/common";
|
||||
import Taro from '@tarojs/taro'
|
||||
import {View, Button} from '@tarojs/components'
|
||||
import {
|
||||
ShieldCheck,
|
||||
Location,
|
||||
Tips,
|
||||
Ask,
|
||||
// Dongdong,
|
||||
People,
|
||||
// AfterSaleService,
|
||||
Logout,
|
||||
ShoppingAdd,
|
||||
Service
|
||||
} from '@nutui/icons-react-taro'
|
||||
import {useUser} from "@/hooks/useUser";
|
||||
|
||||
const UserCell = () => {
|
||||
const {logoutUser} = useUser();
|
||||
|
||||
const onLogout = () => {
|
||||
Taro.showModal({
|
||||
title: '提示',
|
||||
content: '确定要退出登录吗?',
|
||||
success: function (res) {
|
||||
if (res.confirm) {
|
||||
// 使用 useUser hook 的 logoutUser 方法
|
||||
logoutUser();
|
||||
Taro.reLaunch({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<View className="bg-white mx-4 mt-4 rounded-xl">
|
||||
<View className="font-semibold text-gray-800 pt-4 pl-4">我的服务</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={() => navTo('/user/poster/poster', true)}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<ShoppingAdd color="#3b82f6" size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
{/* 修改联系我们为微信客服 */}
|
||||
<Grid.Item text="联系我们">
|
||||
<Button
|
||||
open-type="contact"
|
||||
className="w-full h-full flex flex-col items-center justify-center p-0 bg-transparent"
|
||||
hover-class="none"
|
||||
style={{border: 'none'}}
|
||||
>
|
||||
<View className="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Service color="#67C23A" size="20"/>
|
||||
</View>
|
||||
</Button>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text="收货地址" onClick={() => navTo('/user/address/index', true)}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-emerald-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Location color="#3b82f6" size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text={'实名认证'} onClick={() => navTo('/user/userVerify/index', true)}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-green-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<ShieldCheck color="#10b981" size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text={'推广邀请'} onClick={() => navTo('/dealer/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"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
{/*<Grid.Item text={'我的邀请码'} onClick={() => navTo('/dealer/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>*/}
|
||||
{/* </View>*/}
|
||||
{/*</Grid.Item>*/}
|
||||
|
||||
{/*<Grid.Item text={'管理中心'} onClick={() => navTo('/admin/index', true)}>*/}
|
||||
{/* <View className="text-center">*/}
|
||||
{/* <View className="w-12 h-12 bg-red-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
|
||||
{/* <AfterSaleService className={'text-red-500'} size="20"/>*/}
|
||||
{/* </View>*/}
|
||||
{/* </View>*/}
|
||||
{/*</Grid.Item>*/}
|
||||
|
||||
<Grid.Item text={'常见问题'} onClick={() => navTo('/user/help/index')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-cyan-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Ask className={'text-cyan-500'} size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text={'关于我们'} onClick={() => navTo('/user/about/index')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-amber-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Tips className={'text-amber-500'} size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text={'安全退出'} onClick={onLogout}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-pink-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Logout className={'text-pink-500'} size="20"/>
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
</Grid>
|
||||
</ConfigProvider>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default UserCell
|
||||
|
||||
@@ -14,7 +14,7 @@ function UserOrder() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<View className={'px-4 pb-2'}>
|
||||
<View className={'px-4 pb-2 z-30 relative'} style={{ marginTop: '8px' }}>
|
||||
<View
|
||||
className={'user-card w-full flex flex-col justify-around rounded-xl shadow-sm'}
|
||||
style={{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user