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: {
|
production: {
|
||||||
API_BASE_URL: 'https://cms-api.websoft.top/api',
|
API_BASE_URL: 'https://cms-api.websoft.top/api',
|
||||||
APP_NAME: '通源堂健康生态平台',
|
APP_NAME: '时里院子市集',
|
||||||
DEBUG: 'false',
|
DEBUG: 'false',
|
||||||
},
|
},
|
||||||
// 测试环境
|
// 测试环境
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"miniprogramRoot": "./",
|
"miniprogramRoot": "./",
|
||||||
"projectname": "mp-react",
|
"projectname": "mp-react",
|
||||||
"description": "通源堂健康生态平台",
|
"description": "时里院子市集",
|
||||||
"appid": "touristappid",
|
"appid": "touristappid",
|
||||||
"setting": {
|
"setting": {
|
||||||
"urlCheck": true,
|
"urlCheck": true,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import navTo from "@/utils/common";
|
|||||||
import {TenantId} from "@/config/app";
|
import {TenantId} from "@/config/app";
|
||||||
import {getMyAvailableCoupons} from "@/api/shop/shopUserCoupon";
|
import {getMyAvailableCoupons} from "@/api/shop/shopUserCoupon";
|
||||||
import {useUser} from "@/hooks/useUser";
|
import {useUser} from "@/hooks/useUser";
|
||||||
|
import {getStoredInviteParams} from "@/utils/invite";
|
||||||
|
|
||||||
function UserCard() {
|
function UserCard() {
|
||||||
const {getDisplayName, getRoleName} = useUser();
|
const {getDisplayName, getRoleName} = useUser();
|
||||||
@@ -134,6 +135,11 @@ 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 {code, encryptedData, iv} = detail
|
||||||
|
|
||||||
|
// 获取存储的邀请参数
|
||||||
|
const inviteParams = getStoredInviteParams()
|
||||||
|
const refereeId = inviteParams?.inviter ? parseInt(inviteParams.inviter) : 0
|
||||||
|
|
||||||
Taro.login({
|
Taro.login({
|
||||||
success: function () {
|
success: function () {
|
||||||
if (code) {
|
if (code) {
|
||||||
@@ -145,7 +151,7 @@ function UserCard() {
|
|||||||
encryptedData,
|
encryptedData,
|
||||||
iv,
|
iv,
|
||||||
notVerifyPhone: true,
|
notVerifyPhone: true,
|
||||||
refereeId: 0,
|
refereeId: refereeId, // 使用解析出的推荐人ID
|
||||||
sceneType: 'save_referee',
|
sceneType: 'save_referee',
|
||||||
tenantId: TenantId
|
tenantId: TenantId
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {useEffect} from 'react'
|
import {useEffect} from 'react'
|
||||||
import {useUser} from "@/hooks/useUser";
|
import {useUser} from "@/hooks/useUser";
|
||||||
|
import {Empty} from '@nutui/nutui-react-taro';
|
||||||
import {Text} from '@tarojs/components';
|
import {Text} from '@tarojs/components';
|
||||||
|
|
||||||
function Admin() {
|
function Admin() {
|
||||||
@@ -12,7 +13,16 @@ function Admin() {
|
|||||||
|
|
||||||
if (!isAdmin()) {
|
if (!isAdmin()) {
|
||||||
return (
|
return (
|
||||||
<Text>您不是管理员</Text>
|
<Empty
|
||||||
|
description="您不是管理员"
|
||||||
|
imageSize={80}
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
height: 'calc(100vh - 200px)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
|
||||||
|
</Empty>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -100,3 +100,16 @@ export async function getCmsAd(id: number) {
|
|||||||
}
|
}
|
||||||
return Promise.reject(new Error(res.message));
|
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 request from '@/utils/request';
|
||||||
import type { ApiResult, PageResult } from '@/api';
|
import type { ApiResult, PageResult } from '@/api';
|
||||||
|
import { BaseUrl } from '@/config/app';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 小程序码生成参数
|
* 小程序码生成参数
|
||||||
@@ -38,7 +39,7 @@ export interface InviteRelationParam {
|
|||||||
*/
|
*/
|
||||||
export interface BindRefereeParam {
|
export interface BindRefereeParam {
|
||||||
// 推荐人ID
|
// 推荐人ID
|
||||||
refereeId: number;
|
dealerId: number;
|
||||||
// 被推荐人ID (可选,如果不传则使用当前登录用户)
|
// 被推荐人ID (可选,如果不传则使用当前登录用户)
|
||||||
userId?: number;
|
userId?: number;
|
||||||
// 推荐来源
|
// 推荐来源
|
||||||
@@ -112,7 +113,7 @@ export async function generateMiniProgramCode(data: MiniProgramCodeParam) {
|
|||||||
try {
|
try {
|
||||||
const url = '/wx-login/getOrderQRCodeUnlimited/' + data.scene;
|
const url = '/wx-login/getOrderQRCodeUnlimited/' + data.scene;
|
||||||
// 由于接口直接返回图片buffer,我们直接构建完整的URL
|
// 由于接口直接返回图片buffer,我们直接构建完整的URL
|
||||||
return `${API_BASE_URL}${url}`;
|
return `${BaseUrl}${url}`;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
throw new Error(error.message || '生成小程序码失败');
|
throw new Error(error.message || '生成小程序码失败');
|
||||||
}
|
}
|
||||||
@@ -155,7 +156,7 @@ export async function bindRefereeRelation(data: BindRefereeParam) {
|
|||||||
const res = await request.post<ApiResult<unknown>>(
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
'/shop/shop-dealer-referee',
|
'/shop/shop-dealer-referee',
|
||||||
{
|
{
|
||||||
refereeId: data.refereeId,
|
dealerId: data.dealerId,
|
||||||
userId: data.userId,
|
userId: data.userId,
|
||||||
source: data.source || 'qrcode',
|
source: data.source || 'qrcode',
|
||||||
scene: data.scene
|
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;
|
id?: number;
|
||||||
dealerId?: number;
|
dealerId?: number;
|
||||||
keywords?: string;
|
keywords?: string;
|
||||||
|
deleted?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import request from '@/utils/request';
|
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 type {User, UserParam} from './model';
|
||||||
import {SERVER_API_URL} from "@/utils/server";
|
import {SERVER_API_URL} from "@/utils/server";
|
||||||
|
|
||||||
@@ -8,8 +8,8 @@ import {SERVER_API_URL} from "@/utils/server";
|
|||||||
*/
|
*/
|
||||||
export async function pageUsers(params: UserParam) {
|
export async function pageUsers(params: UserParam) {
|
||||||
const res = await request.get<ApiResult<PageResult<User>>>(
|
const res = await request.get<ApiResult<PageResult<User>>>(
|
||||||
'/system/user/page',
|
SERVER_API_URL + '/system/user/page',
|
||||||
{params}
|
params
|
||||||
);
|
);
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
return res.data;
|
return res.data;
|
||||||
@@ -22,10 +22,8 @@ export async function pageUsers(params: UserParam) {
|
|||||||
*/
|
*/
|
||||||
export async function listUsers(params?: UserParam) {
|
export async function listUsers(params?: UserParam) {
|
||||||
const res = await request.get<ApiResult<User[]>>(
|
const res = await request.get<ApiResult<User[]>>(
|
||||||
'/system/user',
|
SERVER_API_URL + '/system/user',
|
||||||
{
|
|
||||||
params
|
params
|
||||||
}
|
|
||||||
);
|
);
|
||||||
if (res.code === 0 && res.data) {
|
if (res.code === 0 && res.data) {
|
||||||
return res.data;
|
return res.data;
|
||||||
@@ -38,7 +36,7 @@ export async function listUsers(params?: UserParam) {
|
|||||||
*/
|
*/
|
||||||
export async function getStaffs(params?: UserParam) {
|
export async function getStaffs(params?: UserParam) {
|
||||||
const res = await request.get<ApiResult<User[]>>(
|
const res = await request.get<ApiResult<User[]>>(
|
||||||
'/system/user',
|
SERVER_API_URL + '/system/user',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -54,7 +52,7 @@ export async function getStaffs(params?: UserParam) {
|
|||||||
*/
|
*/
|
||||||
export async function getCompanyList(params?: UserParam) {
|
export async function getCompanyList(params?: UserParam) {
|
||||||
const res = await request.get<ApiResult<User[]>>(
|
const res = await request.get<ApiResult<User[]>>(
|
||||||
'/system/user',
|
SERVER_API_URL + '/system/user',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -70,7 +68,7 @@ export async function getCompanyList(params?: UserParam) {
|
|||||||
*/
|
*/
|
||||||
export async function getUser(id: number) {
|
export async function getUser(id: number) {
|
||||||
const res = await request.get<ApiResult<User>>(
|
const res = await request.get<ApiResult<User>>(
|
||||||
'/system/user/' + id,
|
SERVER_API_URL + '/system/user/' + id,
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
if (res.code === 0 && res.data) {
|
if (res.code === 0 && res.data) {
|
||||||
@@ -84,7 +82,7 @@ export async function getUser(id: number) {
|
|||||||
*/
|
*/
|
||||||
export async function addUser(data: User) {
|
export async function addUser(data: User) {
|
||||||
const res = await request.post<ApiResult<unknown>>(
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
'/system/user',
|
SERVER_API_URL + '/system/user',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
@@ -112,7 +110,7 @@ export async function updateUser(data: User) {
|
|||||||
*/
|
*/
|
||||||
export async function removeUser(id?: number) {
|
export async function removeUser(id?: number) {
|
||||||
const res = await request.del<ApiResult<unknown>>(
|
const res = await request.del<ApiResult<unknown>>(
|
||||||
'/system/user/' + id
|
SERVER_API_URL + '/system/user/' + id
|
||||||
);
|
);
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
return res.message;
|
return res.message;
|
||||||
@@ -125,7 +123,7 @@ export async function removeUser(id?: number) {
|
|||||||
*/
|
*/
|
||||||
export async function removeUsers(data: (number | undefined)[]) {
|
export async function removeUsers(data: (number | undefined)[]) {
|
||||||
const res = await request.del<ApiResult<unknown>>(
|
const res = await request.del<ApiResult<unknown>>(
|
||||||
'/system/user/batch',
|
SERVER_API_URL + '/system/user/batch',
|
||||||
{
|
{
|
||||||
data
|
data
|
||||||
}
|
}
|
||||||
@@ -141,7 +139,7 @@ export async function removeUsers(data: (number | undefined)[]) {
|
|||||||
*/
|
*/
|
||||||
export async function updateUserStatus(userId?: number, status?: number) {
|
export async function updateUserStatus(userId?: number, status?: number) {
|
||||||
const res = await request.put<ApiResult<unknown>>(
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
'/system/user/status',
|
SERVER_API_URL + '/system/user/status',
|
||||||
{
|
{
|
||||||
userId,
|
userId,
|
||||||
status
|
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>>(
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
'/system/user/recommend',
|
SERVER_API_URL + '/system/user/recommend',
|
||||||
form
|
form
|
||||||
);
|
);
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
@@ -172,7 +170,7 @@ export async function updateUserRecommend(form) {
|
|||||||
*/
|
*/
|
||||||
export async function updateUserPassword(userId?: number, password = '123456') {
|
export async function updateUserPassword(userId?: number, password = '123456') {
|
||||||
const res = await request.put<ApiResult<unknown>>(
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
'/system/user/password',
|
SERVER_API_URL + '/system/user/password',
|
||||||
{
|
{
|
||||||
userId,
|
userId,
|
||||||
password
|
password
|
||||||
@@ -191,7 +189,7 @@ export async function importUsers(file: File) {
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
const res = await request.post<ApiResult<unknown>>(
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
'/system/user/import',
|
SERVER_API_URL + '/system/user/import',
|
||||||
formData
|
formData
|
||||||
);
|
);
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
@@ -209,7 +207,7 @@ export async function checkExistence(
|
|||||||
id?: number
|
id?: number
|
||||||
) {
|
) {
|
||||||
const res = await request.get<ApiResult<unknown>>(
|
const res = await request.get<ApiResult<unknown>>(
|
||||||
'/system/user/existence',
|
SERVER_API_URL + '/system/user/existence',
|
||||||
{
|
{
|
||||||
params: {field, value, id}
|
params: {field, value, id}
|
||||||
}
|
}
|
||||||
@@ -225,7 +223,7 @@ export async function checkExistence(
|
|||||||
*/
|
*/
|
||||||
export async function countUserBalance(params?: UserParam) {
|
export async function countUserBalance(params?: UserParam) {
|
||||||
const res = await request.get<ApiResult<unknown>>(
|
const res = await request.get<ApiResult<unknown>>(
|
||||||
'/system/user/countUserBalance',
|
SERVER_API_URL + '/system/user/countUserBalance',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -243,7 +241,7 @@ export async function countUserBalance(params?: UserParam) {
|
|||||||
*/
|
*/
|
||||||
export async function listAdminsByPhoneAll(params?: UserParam) {
|
export async function listAdminsByPhoneAll(params?: UserParam) {
|
||||||
const res = await request.get<ApiResult<User[]>>(
|
const res = await request.get<ApiResult<User[]>>(
|
||||||
'/system/user/listAdminsByPhoneAll',
|
SERVER_API_URL + '/system/user/listAdminsByPhoneAll',
|
||||||
params
|
params
|
||||||
);
|
);
|
||||||
if (res.code === 0 && res.data) {
|
if (res.code === 0 && res.data) {
|
||||||
|
|||||||
@@ -128,6 +128,8 @@ export interface User {
|
|||||||
certification?: boolean;
|
certification?: boolean;
|
||||||
// 实名认证类型
|
// 实名认证类型
|
||||||
certificationType?: number;
|
certificationType?: number;
|
||||||
|
// 推荐人ID
|
||||||
|
refereeId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
export default defineAppConfig({
|
export default {
|
||||||
pages: [
|
pages: [
|
||||||
'pages/index/index',
|
'pages/index/index',
|
||||||
'pages/cart/cart',
|
'pages/cart/cart',
|
||||||
'pages/find/find',
|
'pages/find/find',
|
||||||
'pages/user/user'
|
'pages/user/user',
|
||||||
|
'pages/cms/category/index'
|
||||||
],
|
],
|
||||||
"subpackages": [
|
"subpackages": [
|
||||||
{
|
{
|
||||||
@@ -14,7 +15,10 @@ export default defineAppConfig({
|
|||||||
"forget",
|
"forget",
|
||||||
"setting",
|
"setting",
|
||||||
"agreement",
|
"agreement",
|
||||||
"sms-login"
|
"sms-login",
|
||||||
|
'qr-login/index',
|
||||||
|
'qr-confirm/index',
|
||||||
|
'unified-qr/index'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -30,12 +34,6 @@ export default defineAppConfig({
|
|||||||
"index"
|
"index"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"root": "gift",
|
|
||||||
"pages": [
|
|
||||||
"index"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"root": "user",
|
"root": "user",
|
||||||
"pages": [
|
"pages": [
|
||||||
@@ -60,7 +58,12 @@ export default defineAppConfig({
|
|||||||
"gift/redeem",
|
"gift/redeem",
|
||||||
"gift/detail",
|
"gift/detail",
|
||||||
"store/verification",
|
"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",
|
selectedIconPath: "assets/tabbar/home-active.png",
|
||||||
text: "首页",
|
text: "首页",
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// pagePath: "pages/find/find",
|
|
||||||
// iconPath: "assets/tabbar/find.png",
|
|
||||||
// selectedIconPath: "assets/tabbar/find-active.png",
|
|
||||||
// text: "发现",
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
pagePath: "pages/cart/cart",
|
pagePath: "pages/cart/cart",
|
||||||
iconPath: "assets/tabbar/cart.png",
|
iconPath: "assets/tabbar/cart.png",
|
||||||
@@ -142,4 +139,4 @@ export default defineAppConfig({
|
|||||||
"desc": "你的位置信息将用于小程序位置接口的效果展示"
|
"desc": "你的位置信息将用于小程序位置接口的效果展示"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|||||||
15
src/app.scss
15
src/app.scss
@@ -10,14 +10,14 @@ page{
|
|||||||
background-position: bottom;
|
background-position: bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 在全局样式文件中添加
|
/* 在全局样式文件中添加 */
|
||||||
button {
|
button {
|
||||||
&::after {
|
&::after {
|
||||||
border: none !important;
|
border: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 去掉 Grid 组件的边框
|
/* 去掉 Grid 组件的边框 */
|
||||||
.no-border-grid {
|
.no-border-grid {
|
||||||
.nut-grid-item {
|
.nut-grid-item {
|
||||||
border: none !important;
|
border: none !important;
|
||||||
@@ -38,7 +38,7 @@ button {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 微信授权按钮的特殊样式
|
/* 微信授权按钮的特殊样式 */
|
||||||
button[open-type="getPhoneNumber"] {
|
button[open-type="getPhoneNumber"] {
|
||||||
background: none !important;
|
background: none !important;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
@@ -92,3 +92,12 @@ button[open-type="chooseAvatar"] {
|
|||||||
image {
|
image {
|
||||||
margin: 0; /* 全局设置图片的 margin */
|
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 {loginByOpenId} from "@/api/layout";
|
||||||
import {TenantId} from "@/config/app";
|
import {TenantId} from "@/config/app";
|
||||||
import {saveStorageByLoginUser} from "@/utils/server";
|
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; }) {
|
function App(props: { children: any; }) {
|
||||||
const reload = () => {
|
const reload = () => {
|
||||||
@@ -57,12 +57,13 @@ function App(props: { children: any; }) {
|
|||||||
// 处理启动参数
|
// 处理启动参数
|
||||||
const handleLaunchOptions = (options: any) => {
|
const handleLaunchOptions = (options: any) => {
|
||||||
try {
|
try {
|
||||||
console.log('小程序启动参数:', options)
|
console.log('=== 小程序启动参数处理开始 ===')
|
||||||
|
console.log('完整启动参数:', JSON.stringify(options, null, 2))
|
||||||
|
|
||||||
// 解析邀请参数
|
// 解析邀请参数
|
||||||
const inviteParams = parseInviteParams(options)
|
const inviteParams = parseInviteParams(options)
|
||||||
if (inviteParams) {
|
if (inviteParams) {
|
||||||
console.log('检测到邀请参数:', inviteParams)
|
console.log('✅ 成功检测到邀请参数:', inviteParams)
|
||||||
|
|
||||||
// 保存邀请参数到本地存储
|
// 保存邀请参数到本地存储
|
||||||
saveInviteParams(inviteParams)
|
saveInviteParams(inviteParams)
|
||||||
@@ -73,12 +74,21 @@ function App(props: { children: any; }) {
|
|||||||
// 显示邀请提示
|
// 显示邀请提示
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
title: '检测到邀请信息',
|
title: `检测到邀请信息 ID:${inviteParams.inviter}`,
|
||||||
icon: 'success',
|
icon: 'success',
|
||||||
duration: 2000
|
duration: 3000
|
||||||
})
|
})
|
||||||
}, 1000)
|
}, 1000)
|
||||||
|
|
||||||
|
// 打印调试信息
|
||||||
|
setTimeout(() => {
|
||||||
|
debugInviteInfo()
|
||||||
|
}, 2000)
|
||||||
|
} else {
|
||||||
|
console.log('❌ 未检测到邀请参数')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('=== 小程序启动参数处理结束 ===')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('处理启动参数失败:', 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 Taro from '@tarojs/taro'
|
||||||
import {useShareAppMessage, useShareTimeline} from "@tarojs/taro"
|
import {useShareAppMessage} from "@tarojs/taro"
|
||||||
import {Loading} from '@nutui/nutui-react-taro'
|
import {Loading} from '@nutui/nutui-react-taro'
|
||||||
import {useEffect, useState} from "react"
|
import {useEffect, useState} from "react"
|
||||||
import {useRouter} from '@tarojs/taro'
|
import {useRouter} from '@tarojs/taro'
|
||||||
@@ -42,22 +42,15 @@ function Category() {
|
|||||||
})
|
})
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useShareTimeline(() => {
|
|
||||||
return {
|
|
||||||
title: `${nav?.categoryName}_通源堂健康生态平台`,
|
|
||||||
path: `/shop/category/index?id=${categoryId}`
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
useShareAppMessage(() => {
|
useShareAppMessage(() => {
|
||||||
return {
|
return {
|
||||||
title: `${nav?.categoryName}_通源堂健康生态平台`,
|
title: `${nav?.categoryName}_时里院子市集`,
|
||||||
path: `/shop/category/index?id=${categoryId}`,
|
path: `/shop/category/index?id=${categoryId}`,
|
||||||
success: function (res) {
|
success: function () {
|
||||||
console.log('分享成功', res);
|
console.log('分享成功');
|
||||||
},
|
},
|
||||||
fail: function (res) {
|
fail: function () {
|
||||||
console.log('分享失败', res);
|
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;
|
border: 2px solid #f0f0f0;
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
// 更精美的阴影效果
|
/* 更精美的阴影效果 */
|
||||||
//box-shadow:
|
/*box-shadow:
|
||||||
// 0 4px 20px rgba(0, 0, 0, 0.08),
|
0 4px 20px rgba(0, 0, 0, 0.08),
|
||||||
// 0 1px 3px rgba(0, 0, 0, 0.1);
|
0 1px 3px rgba(0, 0, 0, 0.1);*/
|
||||||
|
|
||||||
// 边框光晕效果
|
/* 边框光晕效果 */
|
||||||
//&::before {
|
/*&::before {
|
||||||
// content: '';
|
content: '';
|
||||||
// position: absolute;
|
position: absolute;
|
||||||
// top: 0;
|
top: 0;
|
||||||
// left: 0;
|
left: 0;
|
||||||
// right: 0;
|
right: 0;
|
||||||
// bottom: 0;
|
bottom: 0;
|
||||||
// border-radius: 16px;
|
border-radius: 16px;
|
||||||
// padding: 1px;
|
padding: 1px;
|
||||||
// background: linear-gradient(135deg, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0.1));
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0.1));
|
||||||
// mask-composite: exclude;
|
mask-composite: exclude;
|
||||||
// pointer-events: none;
|
pointer-events: none;
|
||||||
//}
|
}*/
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
transform: scale(0.98) translateY(1px);
|
transform: scale(0.98) translateY(1px);
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
filter: grayscale(0.3);
|
/* filter: grayscale(0.3); 小程序不支持filter属性 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.coupon-left {
|
.coupon-left {
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
// 添加光泽效果
|
/* 添加光泽效果 */
|
||||||
&::after {
|
&::after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -199,11 +199,7 @@
|
|||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
letter-spacing: -0.5px;
|
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 {
|
.coupon-validity {
|
||||||
@@ -212,7 +208,7 @@
|
|||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
||||||
// 添加图标前缀
|
/* 添加图标前缀 */
|
||||||
&::before {
|
&::before {
|
||||||
content: '⏰';
|
content: '⏰';
|
||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
@@ -239,7 +235,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
// 添加按钮光泽效果
|
/* 添加按钮光泽效果 */
|
||||||
&::before {
|
&::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -309,7 +305,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
backdrop-filter: blur(2px);
|
/* backdrop-filter: blur(2px); 小程序不支持backdrop-filter属性 */
|
||||||
|
|
||||||
.status-badge {
|
.status-badge {
|
||||||
background: rgba(0, 0, 0, 0.7);
|
background: rgba(0, 0, 0, 0.7);
|
||||||
@@ -324,7 +320,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 动画效果
|
/* 动画效果 */
|
||||||
@keyframes shimmer {
|
@keyframes shimmer {
|
||||||
0% {
|
0% {
|
||||||
transform: translateX(-100%) translateY(-100%) rotate(45deg);
|
transform: translateX(-100%) translateY(-100%) rotate(45deg);
|
||||||
@@ -334,7 +330,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 响应式优化
|
/* 响应式优化 */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.coupon-card {
|
.coupon-card {
|
||||||
height: 150px;
|
height: 150px;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import {NavBar} from '@nutui/nutui-react-taro'
|
|||||||
import {ArrowLeft} from '@nutui/icons-react-taro'
|
import {ArrowLeft} from '@nutui/icons-react-taro'
|
||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
|
|
||||||
function Header(props) {
|
function Header(props: any) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NavBar
|
<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({
|
export default definePageConfig({
|
||||||
navigationBarTitleText: '医生入驻申请通道',
|
navigationBarTitleText: '邀请注册',
|
||||||
navigationBarTextStyle: 'black'
|
navigationBarTextStyle: 'black'
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,96 +1,210 @@
|
|||||||
import {useEffect, useState, useRef} from "react";
|
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 {Edit} from '@nutui/icons-react-taro'
|
||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
import {View} from '@tarojs/components'
|
import {View} from '@tarojs/components'
|
||||||
import FixedButton from "@/components/FixedButton";
|
import FixedButton from "@/components/FixedButton";
|
||||||
import {useUser} from "@/hooks/useUser";
|
import {useUser} from "@/hooks/useUser";
|
||||||
import {ShopDealerApply} from "@/api/shop/shopDealerApply/model";
|
import {TenantId} from "@/config/app";
|
||||||
import {
|
import {updateUser} from "@/api/system/user";
|
||||||
addShopDealerApply,
|
import {User} from "@/api/system/user/model";
|
||||||
pageShopDealerApply,
|
import {getStoredInviteParams, handleInviteRelation} from "@/utils/invite";
|
||||||
updateShopDealerApply
|
import {addShopDealerUser} from "@/api/shop/shopDealerUser";
|
||||||
} from "@/api/shop/shopDealerApply";
|
import {listUserRole, updateUserRole} from "@/api/system/userRole";
|
||||||
import {getShopDealerUser} from "@/api/shop/shopDealerUser";
|
|
||||||
|
// 类型定义
|
||||||
|
interface ChooseAvatarEvent {
|
||||||
|
detail: {
|
||||||
|
avatarUrl: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InputEvent {
|
||||||
|
detail: {
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const AddUserAddress = () => {
|
const AddUserAddress = () => {
|
||||||
const {user} = useUser()
|
const {user, loginUser} = useUser()
|
||||||
const [loading, setLoading] = useState<boolean>(true)
|
const [loading, setLoading] = useState<boolean>(true)
|
||||||
const [FormData, setFormData] = useState<ShopDealerApply>()
|
const [FormData, setFormData] = useState<User>()
|
||||||
const formRef = useRef<any>(null)
|
const formRef = useRef<any>(null)
|
||||||
const [isEditMode, setIsEditMode] = useState<boolean>(false)
|
|
||||||
const [existingApply, setExistingApply] = useState<ShopDealerApply | null>(null)
|
|
||||||
|
|
||||||
// 获取审核状态文字
|
|
||||||
const getApplyStatusText = (status?: number) => {
|
|
||||||
switch (status) {
|
|
||||||
case 10:
|
|
||||||
return '待审核'
|
|
||||||
case 20:
|
|
||||||
return '审核通过'
|
|
||||||
case 30:
|
|
||||||
return '驳回'
|
|
||||||
default:
|
|
||||||
return '未知状态'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const reload = async () => {
|
const reload = async () => {
|
||||||
// 判断用户是否登录
|
const inviteParams = getStoredInviteParams()
|
||||||
if (!user?.userId) {
|
if (inviteParams?.inviter) {
|
||||||
return false;
|
setFormData({
|
||||||
}
|
...user,
|
||||||
// 查询当前用户ID是否已有申请记录
|
refereeId: Number(inviteParams.inviter),
|
||||||
try {
|
// 清空昵称,强制用户手动输入
|
||||||
const res = await pageShopDealerApply({userId: user?.userId});
|
nickname: '',
|
||||||
if (res && res.count > 0) {
|
})
|
||||||
setIsEditMode(true);
|
|
||||||
setExistingApply(res.list[0]);
|
|
||||||
// 如果有记录,填充表单数据
|
|
||||||
setFormData(res.list[0]);
|
|
||||||
setLoading(false)
|
|
||||||
} else {
|
} else {
|
||||||
setIsEditMode(false);
|
// 如果没有邀请参数,也要确保昵称为空
|
||||||
setExistingApply(null);
|
setFormData({
|
||||||
setLoading(false)
|
...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) {
|
} catch (error) {
|
||||||
setLoading(true)
|
console.error('更新用户头像失败:', error)
|
||||||
console.error('查询申请记录失败:', error);
|
|
||||||
setIsEditMode(false);
|
|
||||||
setExistingApply(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 无论用户信息更新是否成功,都要更新本地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) => {
|
const submitSucceed = async (values: any) => {
|
||||||
try {
|
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 = {
|
await updateUser({
|
||||||
...values,
|
userId: user?.userId,
|
||||||
realName: values.realName || user?.nickname,
|
nickname: values.realName || FormData?.nickname,
|
||||||
mobile: user?.phone,
|
phone: values.phone || FormData?.phone,
|
||||||
refereeId: values.refereeId || FormData?.refereeId,
|
avatar: values.avatar || FormData?.avatar,
|
||||||
applyStatus: 10,
|
refereeId: values.refereeId || FormData?.refereeId
|
||||||
auditTime: undefined
|
});
|
||||||
};
|
|
||||||
await getShopDealerUser(submitData.refereeId);
|
|
||||||
|
|
||||||
// 如果是编辑模式,添加现有申请的id
|
await addShopDealerUser({
|
||||||
if (isEditMode && existingApply?.applyId) {
|
userId: user?.userId,
|
||||||
submitData.applyId = existingApply.applyId;
|
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({
|
Taro.showToast({
|
||||||
title: `${isEditMode ? '提交' : '提交'}成功`,
|
title: `注册成功`,
|
||||||
icon: 'success'
|
icon: 'success'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -100,13 +214,130 @@ const AddUserAddress = () => {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('验证邀请人失败:', 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 = () => {
|
const handleFixedButtonClick = () => {
|
||||||
// 触发表单提交
|
// 触发表单提交
|
||||||
@@ -123,6 +354,18 @@ const AddUserAddress = () => {
|
|||||||
})
|
})
|
||||||
}, [user?.userId]); // 依赖用户ID,当用户变化时重新加载
|
}, [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) {
|
if (loading) {
|
||||||
return <Loading className={'px-2'}>加载中</Loading>
|
return <Loading className={'px-2'}>加载中</Loading>
|
||||||
}
|
}
|
||||||
@@ -139,50 +382,49 @@ const AddUserAddress = () => {
|
|||||||
>
|
>
|
||||||
<View className={'bg-gray-100 h-3'}></View>
|
<View className={'bg-gray-100 h-3'}></View>
|
||||||
<CellGroup style={{padding: '4px 0'}}>
|
<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>
|
<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>
|
</Form.Item>
|
||||||
</CellGroup>
|
</CellGroup>
|
||||||
</Form>
|
</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
|
<FixedButton
|
||||||
icon={<Edit/>}
|
icon={<Edit/>}
|
||||||
text={isEditMode ? '保存修改' : '提交申请'}
|
text={'立即注册'}
|
||||||
disabled={FormData?.applyStatus === 10}
|
|
||||||
onClick={handleFixedButtonClick}
|
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({
|
export default definePageConfig({
|
||||||
navigationBarTitleText: '医生版'
|
navigationBarTitleText: '分销中心'
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -131,28 +131,28 @@ const DealerIndex: React.FC = () => {
|
|||||||
<View className="mb-4">
|
<View className="mb-4">
|
||||||
<Text className="font-semibold text-gray-800">佣金统计</Text>
|
<Text className="font-semibold text-gray-800">佣金统计</Text>
|
||||||
</View>
|
</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={{
|
<View className="text-center p-3 rounded-lg" style={{
|
||||||
background: businessGradients.money.available
|
background: businessGradients.money.available
|
||||||
}}>
|
}}>
|
||||||
<Text className="text-2xl font-bold mb-1 text-white">
|
<Text className="text-lg font-bold mb-1 text-white">
|
||||||
¥{formatMoney(dealerUser.money)}
|
{formatMoney(dealerUser.money)}
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>可提现</Text>
|
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>可提现</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className="text-center p-3 rounded-lg" style={{
|
<View className="text-center p-3 rounded-lg" style={{
|
||||||
background: businessGradients.money.frozen
|
background: businessGradients.money.frozen
|
||||||
}}>
|
}}>
|
||||||
<Text className="text-2xl font-bold mb-1 text-white">
|
<Text className="text-lg font-bold mb-1 text-white">
|
||||||
¥{formatMoney(dealerUser.freezeMoney)}
|
{formatMoney(dealerUser.freezeMoney)}
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>冻结中</Text>
|
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>冻结中</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className="text-center p-3 rounded-lg" style={{
|
<View className="text-center p-3 rounded-lg" style={{
|
||||||
background: businessGradients.money.total
|
background: businessGradients.money.total
|
||||||
}}>
|
}}>
|
||||||
<Text className="text-2xl font-bold mb-1 text-white">
|
<Text className="text-lg font-bold mb-1 text-white">
|
||||||
¥{formatMoney(dealerUser.totalMoney)}
|
{formatMoney(dealerUser.totalMoney)}
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>累计收益</Text>
|
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}>累计收益</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,161 +1,63 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react'
|
import React, {useState, useEffect, useCallback} from 'react'
|
||||||
import { View, Text } from '@tarojs/components'
|
import {View, Text, ScrollView} from '@tarojs/components'
|
||||||
import { Empty, Tabs, Tag, PullToRefresh, Loading } from '@nutui/nutui-react-taro'
|
import {Empty, Tag, PullToRefresh, Loading} from '@nutui/nutui-react-taro'
|
||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
import { pageShopDealerOrder } from '@/api/shop/shopDealerOrder'
|
import {pageShopDealerOrder} from '@/api/shop/shopDealerOrder'
|
||||||
import { useDealerUser } from '@/hooks/useDealerUser'
|
import {useDealerUser} from '@/hooks/useDealerUser'
|
||||||
import type { ShopDealerOrder } from '@/api/shop/shopDealerOrder/model'
|
import type {ShopDealerOrder} from '@/api/shop/shopDealerOrder/model'
|
||||||
|
|
||||||
interface OrderWithDetails extends ShopDealerOrder {
|
interface OrderWithDetails extends ShopDealerOrder {
|
||||||
orderNo?: string
|
orderNo?: string
|
||||||
customerName?: string
|
customerName?: string
|
||||||
totalCommission?: string
|
|
||||||
// 当前用户在此订单中的层级和佣金
|
|
||||||
userLevel?: 1 | 2 | 3
|
|
||||||
userCommission?: string
|
userCommission?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const DealerOrders: React.FC = () => {
|
const DealerOrders: React.FC = () => {
|
||||||
const [activeTab, setActiveTab] = useState<string>('0')
|
|
||||||
const [loading, setLoading] = useState<boolean>(false)
|
const [loading, setLoading] = useState<boolean>(false)
|
||||||
|
const [refreshing, setRefreshing] = useState<boolean>(false)
|
||||||
|
const [loadingMore, setLoadingMore] = useState<boolean>(false)
|
||||||
const [orders, setOrders] = useState<OrderWithDetails[]>([])
|
const [orders, setOrders] = useState<OrderWithDetails[]>([])
|
||||||
const [statistics, setStatistics] = useState({
|
const [currentPage, setCurrentPage] = useState<number>(1)
|
||||||
totalOrders: 0,
|
const [hasMore, setHasMore] = useState<boolean>(true)
|
||||||
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 {dealerUser} = useDealerUser()
|
||||||
|
|
||||||
// 获取订单数据 - 查询当前用户作为各层级分销商的所有订单
|
// 获取订单数据
|
||||||
const fetchOrders = useCallback(async () => {
|
const fetchOrders = useCallback(async (page: number = 1, isRefresh: boolean = false) => {
|
||||||
if (!dealerUser?.userId) return
|
if (!dealerUser?.userId) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (isRefresh) {
|
||||||
|
setRefreshing(true)
|
||||||
|
} else if (page === 1) {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
} else {
|
||||||
|
setLoadingMore(true)
|
||||||
|
}
|
||||||
|
|
||||||
// 并行查询三个层级的订单
|
const result = await pageShopDealerOrder({
|
||||||
const [level1Result, level2Result, level3Result] = await Promise.all([
|
page,
|
||||||
// 一级分销商订单
|
limit: 10
|
||||||
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[] = []
|
if (result?.list) {
|
||||||
const stats = {
|
const newOrders = result.list.map(order => ({
|
||||||
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,
|
...order,
|
||||||
orderNo: `DD${order.orderId}`,
|
orderNo: `${order.orderId}`,
|
||||||
customerName: `用户${order.userId}`,
|
customerName: `用户${order.userId}`,
|
||||||
userLevel: 1 as const,
|
userCommission: order.firstMoney || '0.00'
|
||||||
userCommission: order.firstMoney || '0.00',
|
|
||||||
totalCommission: (
|
|
||||||
parseFloat(order.firstMoney || '0') +
|
|
||||||
parseFloat(order.secondMoney || '0') +
|
|
||||||
parseFloat(order.thirdMoney || '0')
|
|
||||||
).toFixed(2)
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
allOrders.push(...level1Orders)
|
if (page === 1) {
|
||||||
stats.level1.orders = level1Orders.length
|
setOrders(newOrders)
|
||||||
stats.level1.commission = level1Orders.reduce((sum, order) =>
|
} else {
|
||||||
sum + parseFloat(order.userCommission || '0'), 0
|
setOrders(prev => [...prev, ...newOrders])
|
||||||
).toFixed(2)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理二级分销订单
|
setHasMore(newOrders.length === 10)
|
||||||
if (level2Result?.list) {
|
setCurrentPage(page)
|
||||||
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) {
|
} catch (error) {
|
||||||
console.error('获取分销订单失败:', error)
|
console.error('获取分销订单失败:', error)
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
@@ -164,18 +66,27 @@ const DealerOrders: React.FC = () => {
|
|||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
setRefreshing(false)
|
||||||
|
setLoadingMore(false)
|
||||||
}
|
}
|
||||||
}, [dealerUser?.userId])
|
}, [dealerUser?.userId])
|
||||||
|
|
||||||
// 刷新数据
|
// 下拉刷新
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
await fetchOrders()
|
await fetchOrders(1, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载更多
|
||||||
|
const handleLoadMore = async () => {
|
||||||
|
if (!loadingMore && hasMore) {
|
||||||
|
await fetchOrders(currentPage + 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化加载数据
|
// 初始化加载数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dealerUser?.userId) {
|
if (dealerUser?.userId) {
|
||||||
fetchOrders().then()
|
fetchOrders(1)
|
||||||
}
|
}
|
||||||
}, [fetchOrders])
|
}, [fetchOrders])
|
||||||
|
|
||||||
@@ -193,198 +104,87 @@ const DealerOrders: React.FC = () => {
|
|||||||
|
|
||||||
const renderOrderItem = (order: OrderWithDetails) => (
|
const renderOrderItem = (order: OrderWithDetails) => (
|
||||||
<View key={order.id} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
|
<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 className="flex justify-between items-start mb-1">
|
||||||
<View>
|
<Text className="font-semibold text-gray-800">
|
||||||
<Text className="font-semibold text-gray-800 mb-1">
|
|
||||||
订单号:{order.orderNo}
|
订单号:{order.orderNo}
|
||||||
</Text>
|
</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)}>
|
<Tag type={getStatusColor(order.isSettled, order.isInvalid)}>
|
||||||
{getStatusText(order.isSettled, order.isInvalid)}
|
{getStatusText(order.isSettled, order.isInvalid)}
|
||||||
</Tag>
|
</Tag>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="flex justify-between items-center">
|
<View className="flex justify-between items-center mb-1">
|
||||||
<View>
|
<Text className="text-sm text-gray-400">
|
||||||
<Text className="text-sm text-gray-600">
|
|
||||||
订单金额:¥{order.orderPrice || '0.00'}
|
订单金额:¥{order.orderPrice || '0.00'}
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="text-sm text-orange-500 font-semibold">
|
<Text className="text-sm text-orange-500 font-semibold">
|
||||||
我的佣金:¥{order.userCommission}
|
我的佣金:¥{order.userCommission}
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="text-xs text-gray-400">
|
|
||||||
总佣金:¥{order.totalCommission}
|
|
||||||
</Text>
|
|
||||||
</View>
|
</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}
|
{order.createTime}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</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) {
|
if (!dealerUser) {
|
||||||
return (
|
return (
|
||||||
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
||||||
<Loading />
|
<Loading/>
|
||||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="bg-gray-50 min-h-screen">
|
<View className="min-h-screen bg-gray-50">
|
||||||
{/* 统计卡片 */}
|
|
||||||
<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
|
<PullToRefresh
|
||||||
onRefresh={handleRefresh}
|
onRefresh={handleRefresh}
|
||||||
|
disabled={refreshing}
|
||||||
|
pullingText="下拉刷新"
|
||||||
|
canReleaseText="释放刷新"
|
||||||
|
refreshingText="刷新中..."
|
||||||
|
completeText="刷新完成"
|
||||||
|
>
|
||||||
|
<ScrollView
|
||||||
|
scrollY
|
||||||
|
className="h-screen"
|
||||||
|
onScrollToLower={handleLoadMore}
|
||||||
|
lowerThreshold={50}
|
||||||
>
|
>
|
||||||
<View className="p-4">
|
<View className="p-4">
|
||||||
{loading ? (
|
{loading && orders.length === 0 ? (
|
||||||
<View className="text-center py-8">
|
<View className="text-center py-8">
|
||||||
<Loading />
|
<Loading/>
|
||||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||||
</View>
|
</View>
|
||||||
) : getFilteredOrders('0').length > 0 ? (
|
) : orders.length > 0 ? (
|
||||||
getFilteredOrders('0').map(renderOrderItem)
|
<>
|
||||||
|
{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>
|
</View>
|
||||||
|
</ScrollView>
|
||||||
</PullToRefresh>
|
</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>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, {useState, useEffect} from 'react'
|
import React, {useState, useEffect} from 'react'
|
||||||
import {View, Text, Image} from '@tarojs/components'
|
import {View, Text, Image} from '@tarojs/components'
|
||||||
import {Button, Loading} from '@nutui/nutui-react-taro'
|
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 Taro from '@tarojs/taro'
|
||||||
import {useDealerUser} from '@/hooks/useDealerUser'
|
import {useDealerUser} from '@/hooks/useDealerUser'
|
||||||
import {generateInviteCode} from '@/api/invite'
|
import {generateInviteCode} from '@/api/invite'
|
||||||
@@ -115,52 +115,52 @@ const DealerQrcode: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 复制邀请信息
|
// 复制邀请信息
|
||||||
const copyInviteInfo = () => {
|
// const copyInviteInfo = () => {
|
||||||
if (!dealerUser?.userId) {
|
// if (!dealerUser?.userId) {
|
||||||
Taro.showToast({
|
// Taro.showToast({
|
||||||
title: '用户信息未加载',
|
// title: '用户信息未加载',
|
||||||
icon: 'error'
|
// icon: 'error'
|
||||||
})
|
// })
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
const inviteText = `🎉 邀请您加入我的团队!
|
// const inviteText = `🎉 邀请您加入我的团队!
|
||||||
|
//
|
||||||
扫描小程序码或搜索"通源堂健康生态平台"小程序,即可享受优质商品和服务!
|
// 扫描小程序码或搜索"时里院子市集"小程序,即可享受优质商品和服务!
|
||||||
|
//
|
||||||
💰 成为我的团队成员,一起赚取丰厚佣金
|
// 💰 成为我的团队成员,一起赚取丰厚佣金
|
||||||
🎁 新用户专享优惠等你来拿
|
// 🎁 新用户专享优惠等你来拿
|
||||||
|
//
|
||||||
邀请码:${dealerUser.userId}
|
// 邀请码:${dealerUser.userId}
|
||||||
快来加入我们吧!`
|
// 快来加入我们吧!`
|
||||||
|
//
|
||||||
Taro.setClipboardData({
|
// Taro.setClipboardData({
|
||||||
data: inviteText,
|
// data: inviteText,
|
||||||
success: () => {
|
// success: () => {
|
||||||
Taro.showToast({
|
// Taro.showToast({
|
||||||
title: '邀请信息已复制',
|
// title: '邀请信息已复制',
|
||||||
icon: 'success'
|
// icon: 'success'
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
|
|
||||||
// 分享小程序码
|
// 分享小程序码
|
||||||
const shareMiniProgramCode = () => {
|
// const shareMiniProgramCode = () => {
|
||||||
if (!dealerUser?.userId) {
|
// if (!dealerUser?.userId) {
|
||||||
Taro.showToast({
|
// Taro.showToast({
|
||||||
title: '用户信息未加载',
|
// title: '用户信息未加载',
|
||||||
icon: 'error'
|
// icon: 'error'
|
||||||
})
|
// })
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// 小程序分享
|
// // 小程序分享
|
||||||
Taro.showShareMenu({
|
// Taro.showShareMenu({
|
||||||
withShareTicket: true,
|
// withShareTicket: true,
|
||||||
showShareItems: ['shareAppMessage', 'shareTimeline']
|
// showShareItems: ['shareAppMessage']
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (!dealerUser) {
|
if (!dealerUser) {
|
||||||
return (
|
return (
|
||||||
@@ -263,29 +263,29 @@ const DealerQrcode: React.FC = () => {
|
|||||||
保存小程序码到相册
|
保存小程序码到相册
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
<View className={'my-2 bg-white'}>
|
{/*<View className={'my-2 bg-white'}>*/}
|
||||||
<Button
|
{/* <Button*/}
|
||||||
size="large"
|
{/* size="large"*/}
|
||||||
block
|
{/* block*/}
|
||||||
icon={<Copy/>}
|
{/* icon={<Copy/>}*/}
|
||||||
onClick={copyInviteInfo}
|
{/* onClick={copyInviteInfo}*/}
|
||||||
disabled={!dealerUser?.userId || loading}
|
{/* disabled={!dealerUser?.userId || loading}*/}
|
||||||
>
|
{/* >*/}
|
||||||
复制邀请信息
|
{/* 复制邀请信息*/}
|
||||||
</Button>
|
{/* </Button>*/}
|
||||||
</View>
|
{/*</View>*/}
|
||||||
<View className={'my-2 bg-white'}>
|
{/*<View className={'my-2 bg-white'}>*/}
|
||||||
<Button
|
{/* <Button*/}
|
||||||
size="large"
|
{/* size="large"*/}
|
||||||
block
|
{/* block*/}
|
||||||
fill="outline"
|
{/* fill="outline"*/}
|
||||||
icon={<Share/>}
|
{/* icon={<Share/>}*/}
|
||||||
onClick={shareMiniProgramCode}
|
{/* onClick={shareMiniProgramCode}*/}
|
||||||
disabled={!dealerUser?.userId || loading}
|
{/* disabled={!dealerUser?.userId || loading}*/}
|
||||||
>
|
{/* >*/}
|
||||||
分享给好友
|
{/* 分享给好友*/}
|
||||||
</Button>
|
{/* </Button>*/}
|
||||||
</View>
|
{/*</View>*/}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 推广说明 */}
|
{/* 推广说明 */}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export default definePageConfig({
|
export default definePageConfig({
|
||||||
navigationBarTitleText: '我的团队'
|
navigationBarTitleText: '邀请推广'
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,77 +1,79 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react'
|
import React, {useState, useEffect, useCallback} from 'react'
|
||||||
import { View, Text } from '@tarojs/components'
|
import {View, Text} from '@tarojs/components'
|
||||||
import { Empty, Tabs, Avatar, Tag, Progress, Loading, PullToRefresh } from '@nutui/nutui-react-taro'
|
import {Phone, Edit, Message} from '@nutui/icons-react-taro'
|
||||||
import { User, Star, StarFill } from '@nutui/icons-react-taro'
|
import {Space, Empty, Avatar, Button} from '@nutui/nutui-react-taro'
|
||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
import { useDealerUser } from '@/hooks/useDealerUser'
|
import {useDealerUser} from '@/hooks/useDealerUser'
|
||||||
import { listShopDealerReferee } from '@/api/shop/shopDealerReferee'
|
import {listShopDealerReferee} from '@/api/shop/shopDealerReferee'
|
||||||
import { pageShopDealerOrder } from '@/api/shop/shopDealerOrder'
|
import {pageShopDealerOrder} from '@/api/shop/shopDealerOrder'
|
||||||
import type { ShopDealerReferee } from '@/api/shop/shopDealerReferee/model'
|
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 {
|
interface TeamMemberWithStats extends ShopDealerReferee {
|
||||||
name?: string
|
name?: string
|
||||||
avatar?: string
|
avatar?: string
|
||||||
|
nickname?: string;
|
||||||
|
alias?: string;
|
||||||
|
phone?: string;
|
||||||
orderCount?: number
|
orderCount?: number
|
||||||
commission?: string
|
commission?: string
|
||||||
status?: 'active' | 'inactive'
|
status?: 'active' | 'inactive'
|
||||||
subMembers?: number
|
subMembers?: number
|
||||||
joinTime?: string
|
joinTime?: string
|
||||||
|
dealerAvatar?: string;
|
||||||
|
dealerName?: string;
|
||||||
|
dealerPhone?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 层级信息接口
|
||||||
|
interface LevelInfo {
|
||||||
|
dealerId: number
|
||||||
|
dealerName?: string
|
||||||
|
level: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const DealerTeam: React.FC = () => {
|
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 [teamMembers, setTeamMembers] = useState<TeamMemberWithStats[]>([])
|
||||||
const [teamStats, setTeamStats] = useState({
|
const {dealerUser} = useDealerUser()
|
||||||
total: 0,
|
const [dealerId, setDealerId] = useState<number>()
|
||||||
firstLevel: 0,
|
// 层级栈,用于支持返回上一层
|
||||||
secondLevel: 0,
|
const [levelStack, setLevelStack] = useState<LevelInfo[]>([])
|
||||||
thirdLevel: 0,
|
const [loading, setLoading] = useState(false)
|
||||||
monthlyCommission: '0.00'
|
// 当前查看的用户名称
|
||||||
})
|
const [currentDealerName, setCurrentDealerName] = useState<string>('')
|
||||||
|
|
||||||
const { dealerUser } = useDealerUser()
|
// 异步加载成员统计数据
|
||||||
|
const loadMemberStats = async (members: TeamMemberWithStats[]) => {
|
||||||
// 获取团队数据
|
// 分批处理,避免过多并发请求
|
||||||
const fetchTeamData = useCallback(async () => {
|
const batchSize = 3
|
||||||
if (!dealerUser?.userId) return
|
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 {
|
try {
|
||||||
setLoading(true)
|
// 并行获取订单统计和下级成员数量
|
||||||
|
const [orderResult, subMembersResult] = await Promise.all([
|
||||||
// 获取团队成员关系
|
pageShopDealerOrder({
|
||||||
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,
|
page: 1,
|
||||||
limit: 100,
|
|
||||||
userId: member.userId
|
userId: member.userId
|
||||||
|
}),
|
||||||
|
listShopDealerReferee({
|
||||||
|
dealerId: member.userId,
|
||||||
|
deleted: 0
|
||||||
})
|
})
|
||||||
|
])
|
||||||
|
|
||||||
|
let orderCount = 0
|
||||||
|
let commission = '0.00'
|
||||||
|
let status: 'active' | 'inactive' = 'inactive'
|
||||||
|
|
||||||
if (orderResult?.list) {
|
if (orderResult?.list) {
|
||||||
const orders = orderResult.list
|
const orders = orderResult.list
|
||||||
const orderCount = orders.length
|
orderCount = orders.length
|
||||||
const commission = orders.reduce((sum, order) => {
|
commission = orders.reduce((sum, order) => {
|
||||||
const levelCommission = member.level === 1 ? order.firstMoney :
|
const levelCommission = member.level === 1 ? order.firstMoney :
|
||||||
member.level === 2 ? order.secondMoney :
|
member.level === 2 ? order.secondMoney :
|
||||||
order.thirdMoney
|
order.thirdMoney
|
||||||
@@ -84,36 +86,80 @@ const DealerTeam: React.FC = () => {
|
|||||||
const hasRecentOrder = orders.some(order =>
|
const hasRecentOrder = orders.some(order =>
|
||||||
new Date(order.createTime || '') > thirtyDaysAgo
|
new Date(order.createTime || '') > thirtyDaysAgo
|
||||||
)
|
)
|
||||||
|
status = hasRecentOrder ? 'active' : 'inactive'
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...member,
|
...member,
|
||||||
orderCount,
|
orderCount,
|
||||||
commission,
|
commission,
|
||||||
status: hasRecentOrder ? 'active' as const : 'inactive' as const
|
status,
|
||||||
|
subMembers: subMembersResult?.length || 0
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return member
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`获取成员${member.userId}订单失败:`, error)
|
console.error(`获取成员${member.userId}数据失败:`, error)
|
||||||
return member
|
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 = {
|
if (i + batchSize < members.length) {
|
||||||
total: memberStats.length,
|
await new Promise(resolve => setTimeout(resolve, 100))
|
||||||
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)
|
// 获取团队数据
|
||||||
|
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) {
|
} catch (error) {
|
||||||
console.error('获取团队数据失败:', error)
|
console.error('获取团队数据失败:', error)
|
||||||
@@ -124,244 +170,270 @@ const DealerTeam: React.FC = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}, [dealerUser?.userId])
|
}, [dealerUser?.userId, dealerId])
|
||||||
|
|
||||||
// 刷新数据
|
// 查看下级成员
|
||||||
const handleRefresh = async () => {
|
const getNextUser = (item: TeamMemberWithStats) => {
|
||||||
setRefreshing(true)
|
// 检查层级限制:最多只能查看2层(levelStack.length >= 1 表示已经是第2层了)
|
||||||
await fetchTeamData()
|
if (levelStack.length >= 1) {
|
||||||
setRefreshing(false)
|
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(() => {
|
useEffect(() => {
|
||||||
if (dealerUser?.userId) {
|
if (dealerUser?.userId || dealerId) {
|
||||||
fetchTeamData().then()
|
fetchTeamData().then()
|
||||||
}
|
}
|
||||||
}, [fetchTeamData])
|
}, [fetchTeamData])
|
||||||
|
|
||||||
const getLevelColor = (level: number) => {
|
// 初始化当前用户名称
|
||||||
switch (level) {
|
useEffect(() => {
|
||||||
case 1: return '#f59e0b'
|
if (!dealerId && dealerUser?.realName && !currentDealerName) {
|
||||||
case 2: return '#8b5cf6'
|
setCurrentDealerName(dealerUser.realName)
|
||||||
case 3: return '#ec4899'
|
|
||||||
default: return '#6b7280'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}, [dealerUser, dealerId, currentDealerName])
|
||||||
|
|
||||||
const getLevelIcon = (level: number) => {
|
const renderMemberItem = (member: TeamMemberWithStats, index: number) => {
|
||||||
switch (level) {
|
// 判断是否可以点击:有下级成员且未达到层级限制
|
||||||
case 1: return <StarFill color={getLevelColor(level)} size="16" />
|
const canClick = member.subMembers && member.subMembers > 0 && levelStack.length < 1
|
||||||
case 2: return <Star color={getLevelColor(level)} size="16" />
|
// 判断是否显示手机号:只有本级(levelStack.length === 0)才显示
|
||||||
case 3: return <User color={getLevelColor(level)} size="16" />
|
const showPhone = levelStack.length === 0
|
||||||
default: return <User color={getLevelColor(level)} size="16" />
|
// 判断数据是否还在加载中(初始值都是0或'0.00')
|
||||||
}
|
const isStatsLoading = member.orderCount === 0 && member.commission === '0.00' && member.subMembers === 0
|
||||||
}
|
|
||||||
|
|
||||||
const renderMemberItem = (member: TeamMemberWithStats) => (
|
return (
|
||||||
<View key={member.id} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
|
<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">
|
<View className="flex items-center mb-3">
|
||||||
<Avatar
|
<Avatar
|
||||||
size="40"
|
size="40"
|
||||||
src={member.avatar}
|
src={member.avatar}
|
||||||
icon={<User />}
|
|
||||||
className="mr-3"
|
className="mr-3"
|
||||||
/>
|
/>
|
||||||
<View className="flex-1">
|
<View className="flex-1">
|
||||||
<View className="flex items-center mb-1">
|
<View className="flex items-center justify-between mb-1">
|
||||||
<Text className="font-semibold text-gray-800 mr-2">
|
<View className="flex items-center">
|
||||||
{member.name}
|
<Space>
|
||||||
</Text>
|
{member.alias ? <Text className="font-semibold text-blue-700 mr-2">{member.alias}</Text> :
|
||||||
{getLevelIcon(Number(member.level))}
|
<Text className="font-semibold text-gray-800 mr-2">{member.nickname}</Text>}
|
||||||
<Text className="text-xs text-gray-500 ml-1">
|
{/*别名备注*/}
|
||||||
{member.level}级
|
<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>
|
</Text>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
<Text className="text-xs text-gray-500">
|
<Text className="text-xs text-gray-500">
|
||||||
加入时间:{member.joinTime}
|
加入时间:{member.joinTime}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className="text-right">
|
|
||||||
<Tag
|
|
||||||
type={member.status === 'active' ? 'success' : 'default'}
|
|
||||||
>
|
|
||||||
{member.status === 'active' ? '活跃' : '沉默'}
|
|
||||||
</Tag>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="grid grid-cols-3 gap-4 text-center">
|
<View className="grid grid-cols-3 gap-4 text-center">
|
||||||
<View>
|
<Space>
|
||||||
<Text className="text-sm font-semibold text-blue-600">
|
|
||||||
{member.orderCount}
|
|
||||||
</Text>
|
|
||||||
<Text className="text-xs text-gray-500">订单数</Text>
|
<Text className="text-xs text-gray-500">订单数</Text>
|
||||||
</View>
|
<Text className="text-sm font-semibold text-blue-600">
|
||||||
<View>
|
{isStatsLoading ? '-' : member.orderCount}
|
||||||
<Text className="text-sm font-semibold text-green-600">
|
|
||||||
¥{member.commission}
|
|
||||||
</Text>
|
</Text>
|
||||||
|
</Space>
|
||||||
|
<Space>
|
||||||
<Text className="text-xs text-gray-500">贡献佣金</Text>
|
<Text className="text-xs text-gray-500">贡献佣金</Text>
|
||||||
</View>
|
<Text className="text-sm font-semibold text-green-600">
|
||||||
<View>
|
{isStatsLoading ? '-' : `¥${member.commission}`}
|
||||||
<Text className="text-sm font-semibold text-purple-600">
|
|
||||||
{member.subMembers}
|
|
||||||
</Text>
|
</Text>
|
||||||
|
</Space>
|
||||||
|
<Space>
|
||||||
<Text className="text-xs text-gray-500">团队成员</Text>
|
<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>
|
</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 (
|
const renderOverview = () => (
|
||||||
<View className="bg-gray-50 min-h-screen">
|
<View className="rounded-xl p-4">
|
||||||
<Tabs value={activeTab} onChange={() => setActiveTab}>
|
<View
|
||||||
<Tabs.TabPane title="团队总览" value="0">
|
className={'bg-white rounded-lg py-2 px-4 mb-3 shadow-sm text-right text-sm font-medium flex justify-between items-center'}>
|
||||||
{renderOverview()}
|
<Text className="text-lg font-semibold">我的团队成员</Text>
|
||||||
</Tabs.TabPane>
|
<Text className={'text-gray-500 '}>成员数:{teamMembers.length}</Text>
|
||||||
|
|
||||||
<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>
|
</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 React, {useState, useRef, useEffect, useCallback} from 'react'
|
||||||
import { View, Text } from '@tarojs/components'
|
import {View, Text} from '@tarojs/components'
|
||||||
import {
|
import {
|
||||||
Cell,
|
Cell,
|
||||||
|
Space,
|
||||||
Button,
|
Button,
|
||||||
Form,
|
Form,
|
||||||
Input,
|
Input,
|
||||||
@@ -13,19 +14,19 @@ import {
|
|||||||
Loading,
|
Loading,
|
||||||
PullToRefresh
|
PullToRefresh
|
||||||
} from '@nutui/nutui-react-taro'
|
} from '@nutui/nutui-react-taro'
|
||||||
import { Wallet } from '@nutui/icons-react-taro'
|
import {Wallet} from '@nutui/icons-react-taro'
|
||||||
import { businessGradients } from '@/styles/gradients'
|
import {businessGradients} from '@/styles/gradients'
|
||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
import { useDealerUser } from '@/hooks/useDealerUser'
|
import {useDealerUser} from '@/hooks/useDealerUser'
|
||||||
import { pageShopDealerWithdraw, addShopDealerWithdraw } from '@/api/shop/shopDealerWithdraw'
|
import {pageShopDealerWithdraw, addShopDealerWithdraw} from '@/api/shop/shopDealerWithdraw'
|
||||||
import type { ShopDealerWithdraw } from '@/api/shop/shopDealerWithdraw/model'
|
import type {ShopDealerWithdraw} from '@/api/shop/shopDealerWithdraw/model'
|
||||||
|
|
||||||
interface WithdrawRecordWithDetails extends ShopDealerWithdraw {
|
interface WithdrawRecordWithDetails extends ShopDealerWithdraw {
|
||||||
accountDisplay?: string
|
accountDisplay?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const DealerWithdraw: React.FC = () => {
|
const DealerWithdraw: React.FC = () => {
|
||||||
const [activeTab, setActiveTab] = useState('0')
|
const [activeTab, setActiveTab] = useState<string | number>('0')
|
||||||
const [selectedAccount, setSelectedAccount] = useState('')
|
const [selectedAccount, setSelectedAccount] = useState('')
|
||||||
const [loading, setLoading] = useState<boolean>(false)
|
const [loading, setLoading] = useState<boolean>(false)
|
||||||
const [refreshing, setRefreshing] = useState<boolean>(false)
|
const [refreshing, setRefreshing] = useState<boolean>(false)
|
||||||
@@ -34,16 +35,28 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
const [withdrawRecords, setWithdrawRecords] = useState<WithdrawRecordWithDetails[]>([])
|
const [withdrawRecords, setWithdrawRecords] = useState<WithdrawRecordWithDetails[]>([])
|
||||||
const formRef = useRef<any>(null)
|
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 () => {
|
const fetchBalance = useCallback(async () => {
|
||||||
|
console.log(dealerUser, 'dealerUser...')
|
||||||
try {
|
try {
|
||||||
setAvailableAmount(dealerUser?.money || '0.00')
|
setAvailableAmount(dealerUser?.money || '0.00')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取余额失败:', error)
|
console.error('获取余额失败:', error)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [dealerUser])
|
||||||
|
|
||||||
// 获取提现记录
|
// 获取提现记录
|
||||||
const fetchWithdrawRecords = useCallback(async () => {
|
const fetchWithdrawRecords = useCallback(async () => {
|
||||||
@@ -104,21 +117,31 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
|
|
||||||
const getStatusText = (status?: number) => {
|
const getStatusText = (status?: number) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 40: return '已到账'
|
case 40:
|
||||||
case 20: return '审核通过'
|
return '已到账'
|
||||||
case 10: return '待审核'
|
case 20:
|
||||||
case 30: return '已驳回'
|
return '审核通过'
|
||||||
default: return '未知'
|
case 10:
|
||||||
|
return '待审核'
|
||||||
|
case 30:
|
||||||
|
return '已驳回'
|
||||||
|
default:
|
||||||
|
return '未知'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStatusColor = (status?: number) => {
|
const getStatusColor = (status?: number) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 40: return 'success'
|
case 40:
|
||||||
case 20: return 'success'
|
return 'success'
|
||||||
case 10: return 'warning'
|
case 20:
|
||||||
case 30: return 'danger'
|
return 'success'
|
||||||
default: return 'default'
|
case 10:
|
||||||
|
return 'warning'
|
||||||
|
case 30:
|
||||||
|
return 'danger'
|
||||||
|
default:
|
||||||
|
return 'default'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,9 +154,25 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!values.accountType) {
|
||||||
|
Taro.showToast({
|
||||||
|
title: '请选择提现方式',
|
||||||
|
icon: 'error'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 验证提现金额
|
// 验证提现金额
|
||||||
const amount = parseFloat(values.amount)
|
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) {
|
if (amount < 100) {
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
@@ -151,6 +190,25 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
return
|
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 {
|
try {
|
||||||
setSubmitting(true)
|
setSubmitting(true)
|
||||||
|
|
||||||
@@ -204,15 +262,21 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
const quickAmounts = ['100', '300', '500', '1000']
|
const quickAmounts = ['100', '300', '500', '1000']
|
||||||
|
|
||||||
const setQuickAmount = (amount: string) => {
|
const setQuickAmount = (amount: string) => {
|
||||||
formRef.current?.setFieldsValue({ amount })
|
formRef.current?.setFieldsValue({amount})
|
||||||
}
|
}
|
||||||
|
|
||||||
const setAllAmount = () => {
|
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 = () => (
|
const renderWithdrawForm = () => (
|
||||||
<View className="p-4">
|
<View>
|
||||||
{/* 余额卡片 */}
|
{/* 余额卡片 */}
|
||||||
<View className="rounded-xl p-6 mb-6 text-white relative overflow-hidden" style={{
|
<View className="rounded-xl p-6 mb-6 text-white relative overflow-hidden" style={{
|
||||||
background: businessGradients.dealer.header
|
background: businessGradients.dealer.header
|
||||||
@@ -225,14 +289,14 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
}}></View>
|
}}></View>
|
||||||
|
|
||||||
<View className="flex items-center justify-between relative z-10">
|
<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-white text-opacity-80 text-sm mb-1">可提现余额</Text>
|
||||||
<Text className="text-2xl font-bold text-white">¥{availableAmount}</Text>
|
|
||||||
</View>
|
</View>
|
||||||
<View className="p-3 rounded-full" style={{
|
<View className="p-3 rounded-full" style={{
|
||||||
background: 'rgba(255, 255, 255, 0.2)'
|
background: 'rgba(255, 255, 255, 0.2)'
|
||||||
}}>
|
}}>
|
||||||
<Wallet color="white" size="32" />
|
<Wallet color="white" size="32"/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View className="mt-4 pt-4 relative z-10" style={{
|
<View className="mt-4 pt-4 relative z-10" style={{
|
||||||
@@ -254,7 +318,14 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="请输入提现金额"
|
placeholder="请输入提现金额"
|
||||||
type="number"
|
type="number"
|
||||||
clearable
|
onChange={(value) => {
|
||||||
|
// 实时验证提现金额
|
||||||
|
const amount = parseFloat(value)
|
||||||
|
const available = parseFloat(availableAmount.replace(/,/g, ''))
|
||||||
|
if (!isNaN(amount) && amount > available) {
|
||||||
|
// 可以在这里添加实时提示,但不阻止输入
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
@@ -301,10 +372,10 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
{selectedAccount === 'alipay' && (
|
{selectedAccount === 'alipay' && (
|
||||||
<>
|
<>
|
||||||
<Form.Item name="account" label="支付宝账号" required>
|
<Form.Item name="account" label="支付宝账号" required>
|
||||||
<Input placeholder="请输入支付宝账号" />
|
<Input placeholder="请输入支付宝账号"/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="accountName" label="支付宝姓名" required>
|
<Form.Item name="accountName" label="支付宝姓名" required>
|
||||||
<Input placeholder="请输入支付宝实名姓名" />
|
<Input placeholder="请输入支付宝实名姓名"/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -312,13 +383,13 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
{selectedAccount === 'bank' && (
|
{selectedAccount === 'bank' && (
|
||||||
<>
|
<>
|
||||||
<Form.Item name="bankName" label="开户银行" required>
|
<Form.Item name="bankName" label="开户银行" required>
|
||||||
<Input placeholder="请输入开户银行名称" />
|
<Input placeholder="请输入开户银行名称"/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="account" label="银行卡号" required>
|
<Form.Item name="account" label="银行卡号" required>
|
||||||
<Input placeholder="请输入银行卡号" />
|
<Input placeholder="请输入银行卡号"/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="accountName" label="开户姓名" required>
|
<Form.Item name="accountName" label="开户姓名" required>
|
||||||
<Input placeholder="请输入银行卡开户姓名" />
|
<Input placeholder="请输入银行卡开户姓名"/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -347,29 +418,32 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|
||||||
const renderWithdrawRecords = () => (
|
const renderWithdrawRecords = () => {
|
||||||
|
console.log('渲染提现记录:', {loading, recordsCount: withdrawRecords.length, dealerUserId: dealerUser?.userId})
|
||||||
|
|
||||||
|
return (
|
||||||
<PullToRefresh
|
<PullToRefresh
|
||||||
disabled={refreshing}
|
disabled={refreshing}
|
||||||
onRefresh={handleRefresh}
|
onRefresh={handleRefresh}
|
||||||
>
|
>
|
||||||
<View className="p-4">
|
<View>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<View className="text-center py-8">
|
<View className="text-center py-8">
|
||||||
<Loading />
|
<Loading/>
|
||||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||||
</View>
|
</View>
|
||||||
) : withdrawRecords.length > 0 ? (
|
) : withdrawRecords.length > 0 ? (
|
||||||
withdrawRecords.map(record => (
|
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 className="flex justify-between items-start mb-3">
|
||||||
<View>
|
<Space>
|
||||||
<Text className="font-semibold text-gray-800 mb-1">
|
<Text className="font-semibold text-gray-800 mb-1">
|
||||||
提现金额:¥{record.money}
|
提现金额:¥{record.money}
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="text-sm text-gray-500">
|
<Text className="text-sm text-gray-500">
|
||||||
提现账户:{record.accountDisplay}
|
提现账户:{record.accountDisplay}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</Space>
|
||||||
<Tag type={getStatusColor(record.applyStatus)}>
|
<Tag type={getStatusColor(record.applyStatus)}>
|
||||||
{getStatusText(record.applyStatus)}
|
{getStatusText(record.applyStatus)}
|
||||||
</Tag>
|
</Tag>
|
||||||
@@ -391,16 +465,17 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<Empty description="暂无提现记录" />
|
<Empty description="暂无提现记录"/>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</PullToRefresh>
|
</PullToRefresh>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (!dealerUser) {
|
if (!dealerUser) {
|
||||||
return (
|
return (
|
||||||
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
||||||
<Loading />
|
<Loading/>
|
||||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
@@ -408,7 +483,7 @@ const DealerWithdraw: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="bg-gray-50 min-h-screen">
|
<View className="bg-gray-50 min-h-screen">
|
||||||
<Tabs value={activeTab} onChange={() => setActiveTab}>
|
<Tabs value={activeTab} onChange={handleTabChange}>
|
||||||
<Tabs.TabPane title="申请提现" value="0">
|
<Tabs.TabPane title="申请提现" value="0">
|
||||||
{renderWithdrawForm()}
|
{renderWithdrawForm()}
|
||||||
</Tabs.TabPane>
|
</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);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
|
if(!Taro.getStorageSync('UserId')){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
// TODO 读取订单数量
|
// TODO 读取订单数量
|
||||||
const pending = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 0})
|
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})
|
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 { useState, useEffect } from 'react';
|
||||||
import Taro from '@tarojs/taro';
|
import Taro from '@tarojs/taro';
|
||||||
import { User } from '@/api/system/user/model';
|
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
|
// 用户Hook
|
||||||
export const useUser = () => {
|
export const useUser = () => {
|
||||||
@@ -9,8 +11,62 @@ export const useUser = () => {
|
|||||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||||
const [loading, setLoading] = useState(true);
|
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 {
|
try {
|
||||||
const token = Taro.getStorageSync('access_token');
|
const token = Taro.getStorageSync('access_token');
|
||||||
const userData = Taro.getStorageSync('User');
|
const userData = Taro.getStorageSync('User');
|
||||||
@@ -26,9 +82,14 @@ export const useUser = () => {
|
|||||||
setIsLoggedIn(true);
|
setIsLoggedIn(true);
|
||||||
setUser({ userId, tenantId } as User);
|
setUser({ userId, tenantId } as User);
|
||||||
} else {
|
} else {
|
||||||
|
// 没有本地登录信息,尝试自动登录
|
||||||
|
console.log('没有本地登录信息,尝试自动登录...');
|
||||||
|
const autoLoginResult = await autoLoginByOpenId();
|
||||||
|
if (!autoLoginResult) {
|
||||||
setUser(null);
|
setUser(null);
|
||||||
setIsLoggedIn(false);
|
setIsLoggedIn(false);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载用户数据失败:', error);
|
console.error('加载用户数据失败:', error);
|
||||||
setUser(null);
|
setUser(null);
|
||||||
@@ -43,9 +104,24 @@ export const useUser = () => {
|
|||||||
try {
|
try {
|
||||||
Taro.setStorageSync('access_token', token);
|
Taro.setStorageSync('access_token', token);
|
||||||
Taro.setStorageSync('User', userInfo);
|
Taro.setStorageSync('User', userInfo);
|
||||||
|
|
||||||
|
// 确保关键字段不为空时才保存,避免覆盖现有数据
|
||||||
|
if (userInfo.userId) {
|
||||||
Taro.setStorageSync('UserId', userInfo.userId);
|
Taro.setStorageSync('UserId', userInfo.userId);
|
||||||
|
}
|
||||||
|
if (userInfo.tenantId) {
|
||||||
Taro.setStorageSync('TenantId', userInfo.tenantId);
|
Taro.setStorageSync('TenantId', userInfo.tenantId);
|
||||||
|
}
|
||||||
|
if (userInfo.phone) {
|
||||||
Taro.setStorageSync('Phone', 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) {
|
} catch (error) {
|
||||||
console.error('保存用户数据失败:', error);
|
console.error('保存用户数据失败:', error);
|
||||||
}
|
}
|
||||||
@@ -114,9 +190,16 @@ export const useUser = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const updatedUser = { ...user, ...userData };
|
// 先获取最新的用户信息,确保我们有完整的数据
|
||||||
|
const latestUserInfo = await getUserInfo();
|
||||||
|
|
||||||
|
// 合并最新的用户信息和要更新的数据
|
||||||
|
const updatedUser = { ...latestUserInfo, ...userData };
|
||||||
|
|
||||||
|
// 调用API更新用户信息
|
||||||
await updateUserInfo(updatedUser);
|
await updateUserInfo(updatedUser);
|
||||||
|
|
||||||
|
// 更新本地状态
|
||||||
setUser(updatedUser);
|
setUser(updatedUser);
|
||||||
|
|
||||||
// 更新本地存储
|
// 更新本地存储
|
||||||
@@ -216,7 +299,10 @@ export const useUser = () => {
|
|||||||
|
|
||||||
// 初始化时加载用户数据
|
// 初始化时加载用户数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadUserFromStorage();
|
loadUserFromStorage().catch(error => {
|
||||||
|
console.error('初始化用户数据失败:', error);
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -231,6 +317,7 @@ export const useUser = () => {
|
|||||||
fetchUserInfo,
|
fetchUserInfo,
|
||||||
updateUser,
|
updateUser,
|
||||||
loadUserFromStorage,
|
loadUserFromStorage,
|
||||||
|
autoLoginByOpenId,
|
||||||
|
|
||||||
// 工具方法
|
// 工具方法
|
||||||
hasPermission,
|
hasPermission,
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ export const useUserData = (): UseUserDataReturn => {
|
|||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
|
|
||||||
|
if(!Taro.getStorageSync('UserId')){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 并发请求所有数据
|
// 并发请求所有数据
|
||||||
const [userDataRes, couponsRes, giftCardsRes] = await Promise.all([
|
const [userDataRes, couponsRes, giftCardsRes] = await Promise.all([
|
||||||
getUserInfo(),
|
getUserInfo(),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import Taro, {useShareAppMessage, useShareTimeline, useDidShow} from '@tarojs/taro';
|
import Taro, {useShareAppMessage, useDidShow} from '@tarojs/taro';
|
||||||
import {
|
import {
|
||||||
NavBar,
|
NavBar,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
@@ -39,15 +39,9 @@ function Cart() {
|
|||||||
nutuiInputnumberButtonBorderRadius: '4px',
|
nutuiInputnumberButtonBorderRadius: '4px',
|
||||||
}
|
}
|
||||||
|
|
||||||
useShareTimeline(() => {
|
|
||||||
return {
|
|
||||||
title: '购物车 - 网宿小店'
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
useShareAppMessage(() => {
|
useShareAppMessage(() => {
|
||||||
return {
|
return {
|
||||||
title: '购物车 - 网宿小店',
|
title: '购物车 - 时里院子市集',
|
||||||
success: function () {
|
success: function () {
|
||||||
console.log('分享成功');
|
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 {useEffect, useState} from 'react'
|
||||||
import { Swiper } from '@nutui/nutui-react-taro'
|
import {View} from '@tarojs/components'
|
||||||
|
import {Swiper} from '@nutui/nutui-react-taro'
|
||||||
import {CmsAd} from "@/api/cms/cmsAd/model";
|
import {CmsAd} from "@/api/cms/cmsAd/model";
|
||||||
import {Image} from '@nutui/nutui-react-taro'
|
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 MyPage = () => {
|
||||||
const [item, setItem] = useState<CmsAd>()
|
const [carouselData, setCarouselData] = useState<CmsAd>()
|
||||||
const reload = () => {
|
// const [hotToday, setHotToday] = useState<CmsAd>()
|
||||||
getCmsAd(439).then(data => {
|
// const [groupBuy, setGroupBuy] = useState<CmsAd>()
|
||||||
setItem(data)
|
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(() => {
|
useEffect(() => {
|
||||||
reload()
|
loadData()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// 轮播图高度,默认200px
|
||||||
|
const carouselHeight = carouselData?.height || 200;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<View className="flex p-2 justify-between" style={{height: `${carouselHeight}px`}}>
|
||||||
<Swiper defaultValue={0} height={item?.height} indicator style={{ height: item?.height + 'px', display: 'none' }}>
|
{/* 左侧轮播图区域 */}
|
||||||
{item?.imageList?.map((item) => (
|
<View style={{width: '50%', height: '100%'}}>
|
||||||
<Swiper.Item key={item}>
|
<Swiper
|
||||||
<Image width="100%" height="100%" src={item.url} mode={'scaleToFill'} lazyLoad={false} style={{ height: item.height + 'px' }} />
|
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.Item>
|
||||||
))}
|
))}
|
||||||
</Swiper>
|
</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
|
export default MyPage
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,56 @@
|
|||||||
import {useEffect, useState} from "react";
|
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 {Share} from '@nutui/icons-react-taro'
|
||||||
import {View, Text} from '@tarojs/components';
|
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 {ShopGoods} from "@/api/shop/shopGoods/model";
|
||||||
import {pageShopGoods} from "@/api/shop/shopGoods";
|
import {pageShopGoods} from "@/api/shop/shopGoods";
|
||||||
import './BestSellers.scss'
|
|
||||||
|
|
||||||
|
|
||||||
const BestSellers = () => {
|
const BestSellers = () => {
|
||||||
|
const [tab1value, setTab1value] = useState<string | number>('0')
|
||||||
const [list, setList] = useState<ShopGoods[]>([])
|
const [list, setList] = useState<ShopGoods[]>([])
|
||||||
const [goods, setGoods] = useState<ShopGoods>()
|
const [goods, setGoods] = useState<ShopGoods | null>(null)
|
||||||
|
// 轮播图固定高度,可根据需求调整
|
||||||
|
const SWIPER_HEIGHT = 180;
|
||||||
|
|
||||||
const reload = () => {
|
const reload = () => {
|
||||||
pageShopGoods({}).then(res => {
|
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({
|
Taro.showActionSheet({
|
||||||
itemList: ['分享给好友', '分享到朋友圈'],
|
itemList: ['分享给好友'],
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
if (res.tapIndex === 0) {
|
if (res.tapIndex === 0) {
|
||||||
// 分享给好友 - 触发转发
|
|
||||||
Taro.showShareMenu({
|
Taro.showShareMenu({
|
||||||
withShareTicket: true,
|
withShareTicket: true,
|
||||||
success: () => {
|
success: () => {
|
||||||
// 提示用户点击右上角分享
|
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
title: '请点击右上角分享给好友',
|
title: '请点击右上角分享给好友',
|
||||||
icon: 'none',
|
icon: 'none',
|
||||||
@@ -39,13 +72,6 @@ const BestSellers = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (res.tapIndex === 1) {
|
|
||||||
// 分享到朋友圈
|
|
||||||
Taro.showToast({
|
|
||||||
title: '请点击右上角分享到朋友圈',
|
|
||||||
icon: 'none',
|
|
||||||
duration: 2000
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fail: (err) => {
|
fail: (err) => {
|
||||||
@@ -55,53 +81,84 @@ const BestSellers = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reload()
|
reload();
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
// 分享给好友
|
// 配置分享内容
|
||||||
useShareAppMessage(() => {
|
Taro.useShareAppMessage(() => {
|
||||||
|
if (goods) {
|
||||||
return {
|
return {
|
||||||
title: goods?.name || '精选商品',
|
title: goods.name,
|
||||||
path: `/shop/goodsDetail/index?id=${goods?.goodsId}`,
|
path: `/shop/goodsDetail/index?id=${goods.goodsId}`,
|
||||||
imageUrl: goods?.image, // 分享图片
|
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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
});
|
}
|
||||||
|
|
||||||
// 分享到朋友圈
|
|
||||||
useShareTimeline(() => {
|
|
||||||
return {
|
return {
|
||||||
title: `${goods?.name || '精选商品'} - 网宿小店`,
|
title: '热销商品',
|
||||||
path: `/shop/goodsDetail/index?id=${goods?.goodsId}`,
|
path: '/pages/index/index'
|
||||||
imageUrl: goods?.image
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<View className={'py-3'}>
|
<View className={'py-3'}>
|
||||||
<View className={'flex flex-col justify-between items-center rounded-lg px-2'}>
|
<View className={'flex flex-col justify-between items-center rounded-lg px-2'}>
|
||||||
{list?.map((item, index) => {
|
<Tabs
|
||||||
return (
|
value={tab1value}
|
||||||
<View key={index} className={'flex flex-col rounded-lg bg-white shadow-sm w-full mb-5'}>
|
className={'w-full'}
|
||||||
<Image src={item.image} mode={'aspectFit'} lazyLoad={false}
|
onChange={(value) => {
|
||||||
radius="10px 10px 0 0" height="180"
|
setTab1value(value)
|
||||||
onClick={() => Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}/>
|
}}
|
||||||
|
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 className={'flex flex-col p-2 rounded-lg'}>
|
||||||
<View>
|
<View>
|
||||||
<View className={'car-no text-sm'}>{item.name}</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>
|
<Text className={'font-bold text-2xl'}>{item.price}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className={'buy-btn'}>
|
<View className={'buy-btn'}>
|
||||||
<View className={'cart-icon flex items-center'}>
|
<View className={'cart-icon flex items-center hidden'}>
|
||||||
<View
|
<View
|
||||||
className={'flex flex-col justify-center items-center text-white px-3 gap-1 text-nowrap whitespace-nowrap cursor-pointer'}
|
className={'flex flex-col justify-center items-center text-white px-3 gap-1 text-nowrap whitespace-nowrap cursor-pointer'}
|
||||||
onClick={() => handleShare(item)}
|
onClick={() => handleShare(item)}
|
||||||
@@ -123,19 +180,36 @@ const BestSellers = () => {
|
|||||||
<Share size={20}/>
|
<Share size={20}/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<Text className={'text-white pl-4 pr-5'}
|
<Text
|
||||||
onClick={() => Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}>购买
|
className={'text-white pl-5 pr-5'}
|
||||||
|
onClick={() => Taro.navigateTo({
|
||||||
|
url: `/shop/goodsDetail/index?id=${item.goodsId}`
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
购买
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)
|
))}
|
||||||
})}
|
|
||||||
|
{
|
||||||
|
tab1value == '1' && <Empty description="暂无相关商品" style={{
|
||||||
|
background: 'transparent',
|
||||||
|
}}/>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
tab1value == '2' && <Empty description="暂无相关商品" style={{
|
||||||
|
background: 'transparent',
|
||||||
|
}}/>
|
||||||
|
}
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default BestSellers
|
export default BestSellers
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {TenantId} from "@/config/app";
|
|||||||
import {getOrganization} from "@/api/system/organization";
|
import {getOrganization} from "@/api/system/organization";
|
||||||
import {myUserVerify} from "@/api/system/userVerify";
|
import {myUserVerify} from "@/api/system/userVerify";
|
||||||
import { useShopInfo } from '@/hooks/useShopInfo';
|
import { useShopInfo } from '@/hooks/useShopInfo';
|
||||||
import {handleInviteRelation} from "@/utils/invite";
|
import {handleInviteRelation, getStoredInviteParams} from "@/utils/invite";
|
||||||
import {View,Text} from '@tarojs/components'
|
import {View,Text} from '@tarojs/components'
|
||||||
import MySearch from "./MySearch";
|
import MySearch from "./MySearch";
|
||||||
import './Header.scss';
|
import './Header.scss';
|
||||||
@@ -88,6 +88,22 @@ const Header = (props: any) => {
|
|||||||
/* 获取用户手机号 */
|
/* 获取用户手机号 */
|
||||||
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 {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({
|
Taro.login({
|
||||||
success: function () {
|
success: function () {
|
||||||
if (code) {
|
if (code) {
|
||||||
@@ -99,7 +115,7 @@ const Header = (props: any) => {
|
|||||||
encryptedData,
|
encryptedData,
|
||||||
iv,
|
iv,
|
||||||
notVerifyPhone: true,
|
notVerifyPhone: true,
|
||||||
refereeId: 0,
|
refereeId: refereeId, // 使用解析出的推荐人ID
|
||||||
sceneType: 'save_referee',
|
sceneType: 'save_referee',
|
||||||
tenantId: TenantId
|
tenantId: TenantId
|
||||||
},
|
},
|
||||||
@@ -108,6 +124,9 @@ const Header = (props: any) => {
|
|||||||
TenantId
|
TenantId
|
||||||
},
|
},
|
||||||
success: async function (res) {
|
success: async function (res) {
|
||||||
|
// 清除登录防重标记
|
||||||
|
Taro.removeStorageSync('login_in_progress')
|
||||||
|
|
||||||
if (res.data.code == 1) {
|
if (res.data.code == 1) {
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
title: res.data.message,
|
title: res.data.message,
|
||||||
@@ -124,14 +143,7 @@ const Header = (props: any) => {
|
|||||||
// 处理邀请关系
|
// 处理邀请关系
|
||||||
if (res.data.data.user?.userId) {
|
if (res.data.data.user?.userId) {
|
||||||
try {
|
try {
|
||||||
const inviteSuccess = await handleInviteRelation(res.data.data.user.userId)
|
await handleInviteRelation(res.data.data.user.userId)
|
||||||
if (inviteSuccess) {
|
|
||||||
Taro.showToast({
|
|
||||||
title: '邀请关系建立成功',
|
|
||||||
icon: 'success',
|
|
||||||
duration: 2000
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('处理邀请关系失败:', error)
|
console.error('处理邀请关系失败:', error)
|
||||||
}
|
}
|
||||||
@@ -141,6 +153,10 @@ const Header = (props: any) => {
|
|||||||
Taro.reLaunch({
|
Taro.reLaunch({
|
||||||
url: '/pages/index/index'
|
url: '/pages/index/index'
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
fail: function() {
|
||||||
|
// 清除登录防重标记
|
||||||
|
Taro.removeStorageSync('login_in_progress')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@@ -157,9 +173,10 @@ const Header = (props: any) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<View className={'fixed top-0 header-bg'} style={{
|
<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}/>}*/}
|
{/*{!props.stickyStatus && <MySearch done={reload}/>}*/}
|
||||||
</View>
|
</View>
|
||||||
<NavBar
|
<NavBar
|
||||||
@@ -190,6 +207,7 @@ const Header = (props: any) => {
|
|||||||
<TriangleDown className={'text-white'} size={9}/>
|
<TriangleDown className={'text-white'} size={9}/>
|
||||||
</View>
|
</View>
|
||||||
)}>
|
)}>
|
||||||
|
{/*<QRLoginButton />*/}
|
||||||
</NavBar>
|
</NavBar>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {Input, Radio, Button} from '@nutui/nutui-react-taro'
|
|||||||
import {TenantId} from "@/config/app";
|
import {TenantId} from "@/config/app";
|
||||||
import './login.scss';
|
import './login.scss';
|
||||||
import {saveStorageByLoginUser} from "@/utils/server";
|
import {saveStorageByLoginUser} from "@/utils/server";
|
||||||
import {handleInviteRelation} from "@/utils/invite";
|
import {handleInviteRelation, getStoredInviteParams} from "@/utils/invite";
|
||||||
|
|
||||||
// 微信获取手机号回调参数类型
|
// 微信获取手机号回调参数类型
|
||||||
interface GetPhoneNumberDetail {
|
interface GetPhoneNumberDetail {
|
||||||
@@ -40,6 +40,11 @@ const Login = (props: LoginProps) => {
|
|||||||
/* 获取用户手机号 */
|
/* 获取用户手机号 */
|
||||||
const handleGetPhoneNumber = ({detail}: GetPhoneNumberEvent) => {
|
const handleGetPhoneNumber = ({detail}: GetPhoneNumberEvent) => {
|
||||||
const {code, encryptedData, iv} = detail
|
const {code, encryptedData, iv} = detail
|
||||||
|
|
||||||
|
// 获取存储的邀请参数
|
||||||
|
const inviteParams = getStoredInviteParams()
|
||||||
|
const refereeId = inviteParams?.inviter ? parseInt(inviteParams.inviter) : 0
|
||||||
|
|
||||||
Taro.login({
|
Taro.login({
|
||||||
success: function () {
|
success: function () {
|
||||||
if (code) {
|
if (code) {
|
||||||
@@ -51,7 +56,7 @@ const Login = (props: LoginProps) => {
|
|||||||
encryptedData,
|
encryptedData,
|
||||||
iv,
|
iv,
|
||||||
notVerifyPhone: true,
|
notVerifyPhone: true,
|
||||||
refereeId: 0,
|
refereeId: refereeId, // 使用解析出的推荐人ID
|
||||||
sceneType: 'save_referee',
|
sceneType: 'save_referee',
|
||||||
tenantId: TenantId
|
tenantId: TenantId
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {useState} from "react";
|
|||||||
import Taro from '@tarojs/taro';
|
import Taro from '@tarojs/taro';
|
||||||
import { goTo } from '@/utils/navigation';
|
import { goTo } from '@/utils/navigation';
|
||||||
|
|
||||||
function MySearch() {
|
function MySearch(props: any) {
|
||||||
const [keywords, setKeywords] = useState<string>('')
|
const [keywords, setKeywords] = useState<string>('')
|
||||||
|
|
||||||
const onKeywords = (keywords: string) => {
|
const onKeywords = (keywords: string) => {
|
||||||
@@ -39,7 +39,7 @@ function MySearch() {
|
|||||||
background: '#ffffff',
|
background: '#ffffff',
|
||||||
padding: '0 5px',
|
padding: '0 5px',
|
||||||
borderRadius: '20px',
|
borderRadius: '20px',
|
||||||
marginTop: '100px',
|
marginTop: `${props.statusBarHeight + 50}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Search size={18} className={'ml-2 text-gray-400'}/>
|
<Search size={18} className={'ml-2 text-gray-400'}/>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Header from './Header';
|
import Header from './Header';
|
||||||
import BestSellers from './BestSellers';
|
import BestSellers from './BestSellers';
|
||||||
import Taro from '@tarojs/taro';
|
import Taro from '@tarojs/taro';
|
||||||
import {useShareAppMessage, useShareTimeline} from "@tarojs/taro"
|
import {useShareAppMessage} from "@tarojs/taro"
|
||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import {getShopInfo} from "@/api/layout";
|
import {getShopInfo} from "@/api/layout";
|
||||||
import {Sticky} from '@nutui/nutui-react-taro'
|
import {Sticky} from '@nutui/nutui-react-taro'
|
||||||
@@ -16,22 +16,28 @@ function Home() {
|
|||||||
// 吸顶状态
|
// 吸顶状态
|
||||||
const [stickyStatus, setStickyStatus] = useState<boolean>(false)
|
const [stickyStatus, setStickyStatus] = useState<boolean>(false)
|
||||||
|
|
||||||
useShareTimeline(() => {
|
|
||||||
return {
|
|
||||||
title: '网宿小店 - 网宿软件',
|
|
||||||
path: `/pages/index/index`
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
useShareAppMessage(() => {
|
useShareAppMessage(() => {
|
||||||
|
// 获取当前用户ID,用于生成邀请链接
|
||||||
|
const userId = Taro.getStorageSync('UserId');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: '网宿小店 - 网宿软件',
|
title: '网宿小店 - 网宿软件',
|
||||||
path: `/pages/index/index`,
|
path: userId ? `/pages/index/index?inviter=${userId}&source=share&t=${Date.now()}` : `/pages/index/index`,
|
||||||
success: function () {
|
success: function () {
|
||||||
console.log('分享成功');
|
console.log('首页分享成功');
|
||||||
|
Taro.showToast({
|
||||||
|
title: '分享成功',
|
||||||
|
icon: 'success',
|
||||||
|
duration: 2000
|
||||||
|
});
|
||||||
},
|
},
|
||||||
fail: function () {
|
fail: function () {
|
||||||
console.log('分享失败');
|
console.log('首页分享失败');
|
||||||
|
Taro.showToast({
|
||||||
|
title: '分享失败',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2000
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -89,18 +95,32 @@ function Home() {
|
|||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 检查是否有待处理的邀请关系
|
// 检查是否有待处理的邀请关系 - 异步处理,不阻塞页面加载
|
||||||
if (hasPendingInvite()) {
|
if (hasPendingInvite()) {
|
||||||
console.log('检测到待处理的邀请关系')
|
console.log('检测到待处理的邀请关系')
|
||||||
// 延迟处理,确保用户信息已加载
|
// 延迟处理,确保用户信息已加载,并设置超时保护
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
try {
|
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) {
|
if (success) {
|
||||||
console.log('首页邀请关系处理成功')
|
console.log('首页邀请关系处理成功')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('首页邀请关系处理失败:', error)
|
console.error('首页邀请关系处理失败:', error)
|
||||||
|
// 邀请关系处理失败不应该影响页面正常显示
|
||||||
|
// 可以选择清除邀请参数,避免重复尝试
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
|
if (errorMessage?.includes('超时')) {
|
||||||
|
console.log('邀请关系处理超时,清除邀请参数')
|
||||||
|
// 可以选择清除邀请参数或稍后重试
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, 2000)
|
}, 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 {useEffect} from "react";
|
||||||
import {useDealerUser} from "@/hooks/useDealerUser";
|
import {useDealerUser} from "@/hooks/useDealerUser";
|
||||||
|
|
||||||
const UserCell = () => {
|
const IsDealer = () => {
|
||||||
const {isSuperAdmin} = useUser();
|
const {isSuperAdmin} = useUser();
|
||||||
const {dealerUser} = useDealerUser()
|
const {dealerUser} = useDealerUser()
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ const UserCell = () => {
|
|||||||
if (isSuperAdmin()) {
|
if (isSuperAdmin()) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<View className={'px-4'}>
|
<View className={'px-4'} style={{ marginTop: '8px', position: 'relative', zIndex: 25 }}>
|
||||||
<Cell
|
<Cell
|
||||||
className="nutui-cell-clickable"
|
className="nutui-cell-clickable"
|
||||||
style={{
|
style={{
|
||||||
@@ -46,7 +46,7 @@ const UserCell = () => {
|
|||||||
if (dealerUser) {
|
if (dealerUser) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<View className={'px-4'}>
|
<View className={'px-4'} style={{ marginTop: '8px', position: 'relative', zIndex: 25 }}>
|
||||||
<Cell
|
<Cell
|
||||||
className="nutui-cell-clickable"
|
className="nutui-cell-clickable"
|
||||||
style={{
|
style={{
|
||||||
@@ -56,7 +56,7 @@ const UserCell = () => {
|
|||||||
<View style={{display: 'inline-flex', alignItems: 'center'}}>
|
<View style={{display: 'inline-flex', alignItems: 'center'}}>
|
||||||
<Reward className={'text-orange-100 '} size={16}/>
|
<Reward className={'text-orange-100 '} size={16}/>
|
||||||
<Text style={{fontSize: '16px'}}
|
<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>*/}
|
{/*<Text className={'text-white opacity-80 pl-3'}>门店核销</Text>*/}
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
@@ -73,7 +73,7 @@ const UserCell = () => {
|
|||||||
*/
|
*/
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<View className={'px-4'}>
|
<View className={'px-4'} style={{ marginTop: '8px', position: 'relative', zIndex: 25 }}>
|
||||||
<Cell
|
<Cell
|
||||||
className="nutui-cell-clickable"
|
className="nutui-cell-clickable"
|
||||||
style={{
|
style={{
|
||||||
@@ -82,19 +82,15 @@ const UserCell = () => {
|
|||||||
title={
|
title={
|
||||||
<View style={{display: 'inline-flex', alignItems: 'center'}}>
|
<View style={{display: 'inline-flex', alignItems: 'center'}}>
|
||||||
<Reward className={'text-orange-100 '} size={16}/>
|
<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>
|
</View>
|
||||||
}
|
}
|
||||||
extra={
|
extra={<ArrowRight color="#cccccc" size={18}/>}
|
||||||
<>
|
|
||||||
<Text className={'text-white opacity-80 px-3'}>需医师资格证</Text>
|
|
||||||
<ArrowRight color="#cccccc" size={18}/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
onClick={() => navTo('/dealer/apply/add', true)}
|
onClick={() => navTo('/dealer/apply/add', true)}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
export default UserCell
|
export default IsDealer
|
||||||
|
|||||||
@@ -1,28 +1,21 @@
|
|||||||
import {Button} from '@nutui/nutui-react-taro'
|
import {Avatar, Tag, Space, Button} from '@nutui/nutui-react-taro'
|
||||||
import {Avatar, Tag} from '@nutui/nutui-react-taro'
|
import {View, Text, Image} from '@tarojs/components'
|
||||||
import {View, Text} from '@tarojs/components'
|
|
||||||
import {Scan} from '@nutui/icons-react-taro';
|
|
||||||
import {getUserInfo, getWxOpenId} from '@/api/layout';
|
import {getUserInfo, getWxOpenId} from '@/api/layout';
|
||||||
import Taro from '@tarojs/taro';
|
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 {User} from "@/api/system/user/model";
|
||||||
import navTo from "@/utils/common";
|
import navTo from "@/utils/common";
|
||||||
import {TenantId} from "@/config/app";
|
import {TenantId} from "@/config/app";
|
||||||
import {getMyAvailableCoupons} from "@/api/shop/shopUserCoupon";
|
|
||||||
import {useUser} from "@/hooks/useUser";
|
import {useUser} from "@/hooks/useUser";
|
||||||
import {useUserData} from "@/hooks/useUserData";
|
import {useUserData} from "@/hooks/useUserData";
|
||||||
|
import {getStoredInviteParams} from "@/utils/invite";
|
||||||
|
import UnifiedQRButton from "@/components/UnifiedQRButton";
|
||||||
|
|
||||||
function UserCard() {
|
const UserCard = forwardRef<any, any>((_, ref) => {
|
||||||
const {
|
const {data, refresh} = useUserData()
|
||||||
isAdmin
|
|
||||||
} = useUser();
|
|
||||||
const { data, refresh } = useUserData()
|
|
||||||
const {getDisplayName, getRoleName} = useUser();
|
const {getDisplayName, getRoleName} = useUser();
|
||||||
const [IsLogin, setIsLogin] = useState<boolean>(false)
|
const [IsLogin, setIsLogin] = useState<boolean>(false)
|
||||||
const [userInfo, setUserInfo] = useState<User>()
|
const [userInfo, setUserInfo] = useState<User>()
|
||||||
const [couponCount, setCouponCount] = useState(0)
|
|
||||||
const [pointsCount, setPointsCount] = useState(0)
|
|
||||||
const [giftCount, setGiftCount] = useState(0)
|
|
||||||
|
|
||||||
// 下拉刷新
|
// 下拉刷新
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
@@ -33,6 +26,11 @@ function UserCard() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 暴露方法给父组件
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
handleRefresh
|
||||||
|
}))
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Taro.getSetting:获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限。
|
// Taro.getSetting:获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限。
|
||||||
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 = () => {
|
const reload = () => {
|
||||||
Taro.getUserInfo({
|
Taro.getUserInfo({
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
@@ -92,11 +63,6 @@ function UserCard() {
|
|||||||
setIsLogin(true);
|
setIsLogin(true);
|
||||||
Taro.setStorageSync('UserId', data.userId)
|
Taro.setStorageSync('UserId', data.userId)
|
||||||
|
|
||||||
// 加载用户统计数据
|
|
||||||
if (data.userId) {
|
|
||||||
loadUserStats(data.userId)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取openId
|
// 获取openId
|
||||||
if (!data.openid) {
|
if (!data.openid) {
|
||||||
Taro.login({
|
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 {code, encryptedData, iv} = detail
|
||||||
|
|
||||||
|
// 获取存储的邀请参数
|
||||||
|
const inviteParams = getStoredInviteParams()
|
||||||
|
const refereeId = inviteParams?.inviter ? parseInt(inviteParams.inviter) : 0
|
||||||
|
|
||||||
Taro.login({
|
Taro.login({
|
||||||
success: function () {
|
success: function () {
|
||||||
if (code) {
|
if (code) {
|
||||||
@@ -162,7 +133,7 @@ function UserCard() {
|
|||||||
encryptedData,
|
encryptedData,
|
||||||
iv,
|
iv,
|
||||||
notVerifyPhone: true,
|
notVerifyPhone: true,
|
||||||
refereeId: 0,
|
refereeId: refereeId, // 使用解析出的推荐人ID
|
||||||
sceneType: 'save_referee',
|
sceneType: 'save_referee',
|
||||||
tenantId: TenantId
|
tenantId: TenantId
|
||||||
},
|
},
|
||||||
@@ -194,26 +165,30 @@ function UserCard() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className={'header-bg pt-20'}>
|
<View className={'pt-20'}>
|
||||||
<View className={'p-4'}>
|
<View className={'p-4'}>
|
||||||
|
{/* 使用相对定位容器,让个人资料图片可以绝对定位在右上角 */}
|
||||||
|
<View className="relative z-20">
|
||||||
<View
|
<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={{
|
style={{
|
||||||
background: 'linear-gradient(to bottom, #ffffff, #ffffff)', // 这种情况建议使用类名来控制样式(引入外联样式)
|
background: 'linear-gradient(to bottom, #ffffff, #ffffff)',
|
||||||
// width: '720rpx',
|
|
||||||
// margin: '10px auto 0px auto',
|
|
||||||
height: '170px',
|
height: '170px',
|
||||||
// borderRadius: '22px 22px 0 0',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View className={'user-card-header flex w-full justify-between items-center pt-4'}>
|
<View className={'user-card-header flex w-full justify-between items-center pt-4'}>
|
||||||
<View className={'flex items-center mx-4'}>
|
<View className={'flex items-center mx-4'}>
|
||||||
{
|
{
|
||||||
IsLogin ? (
|
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}>
|
<Button className={'text-black'} open-type="getPhoneNumber"
|
||||||
<Avatar size="large" src={userInfo?.avatar} shape="round"/>
|
onGetPhoneNumber={handleGetPhoneNumber}>
|
||||||
|
<Avatar size="large"
|
||||||
|
src={userInfo?.avatar || 'https://oss.wsdns.cn/20250623/62f830b85edb4a7293b8948c25e6f987.jpeg'}
|
||||||
|
shape="round"/>
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -230,11 +205,30 @@ function UserCard() {
|
|||||||
) : ''}
|
) : ''}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{isAdmin() && <Scan onClick={() => navTo('/user/store/verification', true)} />}
|
<Space style={{
|
||||||
<View className={'mr-4 text-sm px-3 py-1 text-black border-gray-400 border-solid border-2 rounded-3xl'}
|
marginTop: '30px',
|
||||||
onClick={() => navTo('/user/profile/profile', true)}>
|
marginRight: '10px'
|
||||||
{'个人资料'}
|
}}>
|
||||||
</View>
|
{/*统一扫码入口 - 支持登录和核销*/}
|
||||||
|
<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>
|
||||||
<View className={'flex justify-around mt-1'}>
|
<View className={'flex justify-around mt-1'}>
|
||||||
<View className={'item flex justify-center flex-col items-center'}
|
<View className={'item flex justify-center flex-col items-center'}
|
||||||
@@ -258,10 +252,27 @@ function UserCard() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</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;
|
export default UserCard;
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const UserCell = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<View className={'px-4'}>
|
<View className={'px-4'} style={{ marginTop: '8px', position: 'relative', zIndex: 20 }}>
|
||||||
|
|
||||||
<Cell.Group divider={true} description={
|
<Cell.Group divider={true} description={
|
||||||
<View style={{display: 'inline-flex', alignItems: 'center'}}>
|
<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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<View className={'px-4 pb-2'}>
|
<View className={'px-4 pb-2 z-30 relative'} style={{ marginTop: '8px' }}>
|
||||||
<View
|
<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 shadow-sm'}
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user