diff --git a/config/env.ts b/config/env.ts index edbcd70..c92883b 100644 --- a/config/env.ts +++ b/config/env.ts @@ -9,7 +9,7 @@ export const ENV_CONFIG = { // 生产环境 production: { API_BASE_URL: 'https://cms-api.websoft.top/api', - APP_NAME: '通源堂健康生态平台', + APP_NAME: '时里院子市集', DEBUG: 'false', }, // 测试环境 diff --git a/project.tt.json b/project.tt.json index 1c24092..71d3700 100644 --- a/project.tt.json +++ b/project.tt.json @@ -1,7 +1,7 @@ { "miniprogramRoot": "./", "projectname": "mp-react", - "description": "通源堂健康生态平台", + "description": "时里院子市集", "appid": "touristappid", "setting": { "urlCheck": true, diff --git a/src/admin/components/UserCard.tsx b/src/admin/components/UserCard.tsx index 132dc49..a83f124 100644 --- a/src/admin/components/UserCard.tsx +++ b/src/admin/components/UserCard.tsx @@ -8,6 +8,7 @@ import navTo from "@/utils/common"; import {TenantId} from "@/config/app"; import {getMyAvailableCoupons} from "@/api/shop/shopUserCoupon"; import {useUser} from "@/hooks/useUser"; +import {getStoredInviteParams} from "@/utils/invite"; function UserCard() { const {getDisplayName, getRoleName} = useUser(); @@ -134,6 +135,11 @@ function UserCard() { /* 获取用户手机号 */ const handleGetPhoneNumber = ({detail}: {detail: {code?: string, encryptedData?: string, iv?: string}}) => { const {code, encryptedData, iv} = detail + + // 获取存储的邀请参数 + const inviteParams = getStoredInviteParams() + const refereeId = inviteParams?.inviter ? parseInt(inviteParams.inviter) : 0 + Taro.login({ success: function () { if (code) { @@ -145,7 +151,7 @@ function UserCard() { encryptedData, iv, notVerifyPhone: true, - refereeId: 0, + refereeId: refereeId, // 使用解析出的推荐人ID sceneType: 'save_referee', tenantId: TenantId }, diff --git a/src/admin/index.tsx b/src/admin/index.tsx index 8fd127e..16f0cab 100644 --- a/src/admin/index.tsx +++ b/src/admin/index.tsx @@ -1,5 +1,6 @@ import {useEffect} from 'react' import {useUser} from "@/hooks/useUser"; +import {Empty} from '@nutui/nutui-react-taro'; import {Text} from '@tarojs/components'; function Admin() { @@ -12,7 +13,16 @@ function Admin() { if (!isAdmin()) { return ( - 您不是管理员 + + + ); } return ( diff --git a/src/api/cms/cmsAd/index.ts b/src/api/cms/cmsAd/index.ts index 9cd1dcc..a9b1ca7 100644 --- a/src/api/cms/cmsAd/index.ts +++ b/src/api/cms/cmsAd/index.ts @@ -100,3 +100,16 @@ export async function getCmsAd(id: number) { } return Promise.reject(new Error(res.message)); } + +/** + * 根据id查询广告位 + */ +export async function getCmsAdByCode(code: string) { + const res = await request.get>( + '/cms/cms-ad/getByCode/' + code + ); + if (res.code === 0 && res.data) { + return res.data; + } + return Promise.reject(new Error(res.message)); +} diff --git a/src/api/invite/index.ts b/src/api/invite/index.ts index 9f973a1..ba38f75 100644 --- a/src/api/invite/index.ts +++ b/src/api/invite/index.ts @@ -1,5 +1,6 @@ import request from '@/utils/request'; import type { ApiResult, PageResult } from '@/api'; +import { BaseUrl } from '@/config/app'; /** * 小程序码生成参数 @@ -38,7 +39,7 @@ export interface InviteRelationParam { */ export interface BindRefereeParam { // 推荐人ID - refereeId: number; + dealerId: number; // 被推荐人ID (可选,如果不传则使用当前登录用户) userId?: number; // 推荐来源 @@ -112,7 +113,7 @@ export async function generateMiniProgramCode(data: MiniProgramCodeParam) { try { const url = '/wx-login/getOrderQRCodeUnlimited/' + data.scene; // 由于接口直接返回图片buffer,我们直接构建完整的URL - return `${API_BASE_URL}${url}`; + return `${BaseUrl}${url}`; } catch (error: any) { throw new Error(error.message || '生成小程序码失败'); } @@ -155,7 +156,7 @@ export async function bindRefereeRelation(data: BindRefereeParam) { const res = await request.post>( '/shop/shop-dealer-referee', { - refereeId: data.refereeId, + dealerId: data.dealerId, userId: data.userId, source: data.source || 'qrcode', scene: data.scene diff --git a/src/api/passport/qr-login/index.ts b/src/api/passport/qr-login/index.ts new file mode 100644 index 0000000..f76c47f --- /dev/null +++ b/src/api/passport/qr-login/index.ts @@ -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>( + 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>( + 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>( + 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>( + 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((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; + } +} diff --git a/src/api/shop/shopChatConversation/index.ts b/src/api/shop/shopChatConversation/index.ts new file mode 100644 index 0000000..6a5ba5b --- /dev/null +++ b/src/api/shop/shopChatConversation/index.ts @@ -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>>( + '/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>( + '/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>( + '/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>( + '/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>( + '/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>( + '/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>( + '/shop/shop-chat-conversation/' + id + ); + if (res.code === 0 && res.data) { + return res.data; + } + return Promise.reject(new Error(res.message)); +} diff --git a/src/api/shop/shopChatConversation/model/index.ts b/src/api/shop/shopChatConversation/model/index.ts new file mode 100644 index 0000000..ea6a9b3 --- /dev/null +++ b/src/api/shop/shopChatConversation/model/index.ts @@ -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; +} diff --git a/src/api/shop/shopChatMessage/index.ts b/src/api/shop/shopChatMessage/index.ts new file mode 100644 index 0000000..4667ce7 --- /dev/null +++ b/src/api/shop/shopChatMessage/index.ts @@ -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>>( + '/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>( + '/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>( + '/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>( + '/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>( + '/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>( + '/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>( + '/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>( + '/shop/shop-chat-message/' + id + ); + if (res.code === 0 && res.data) { + return res.data; + } + return Promise.reject(new Error(res.message)); +} diff --git a/src/api/shop/shopChatMessage/model/index.ts b/src/api/shop/shopChatMessage/model/index.ts new file mode 100644 index 0000000..411d45c --- /dev/null +++ b/src/api/shop/shopChatMessage/model/index.ts @@ -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; +} diff --git a/src/api/shop/shopDealerBank/index.ts b/src/api/shop/shopDealerBank/index.ts new file mode 100644 index 0000000..eca6fea --- /dev/null +++ b/src/api/shop/shopDealerBank/index.ts @@ -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>>( + '/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>( + '/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>( + '/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>( + '/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>( + '/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>( + '/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>( + '/shop/shop-dealer-bank/' + id + ); + if (res.code === 0 && res.data) { + return res.data; + } + return Promise.reject(new Error(res.message)); +} diff --git a/src/api/shop/shopDealerBank/model/index.ts b/src/api/shop/shopDealerBank/model/index.ts new file mode 100644 index 0000000..0fbd93b --- /dev/null +++ b/src/api/shop/shopDealerBank/model/index.ts @@ -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; +} diff --git a/src/api/shop/shopDealerReferee/model/index.ts b/src/api/shop/shopDealerReferee/model/index.ts index 61c1e49..044a77d 100644 --- a/src/api/shop/shopDealerReferee/model/index.ts +++ b/src/api/shop/shopDealerReferee/model/index.ts @@ -27,4 +27,5 @@ export interface ShopDealerRefereeParam extends PageParam { id?: number; dealerId?: number; keywords?: string; + deleted?: number; } diff --git a/src/api/system/user/index.ts b/src/api/system/user/index.ts index c0f144f..0c55116 100644 --- a/src/api/system/user/index.ts +++ b/src/api/system/user/index.ts @@ -1,5 +1,5 @@ import request from '@/utils/request'; -import type {ApiResult, PageResult} from '@/api/index'; +import type {ApiResult, PageResult} from '@/api'; import type {User, UserParam} from './model'; import {SERVER_API_URL} from "@/utils/server"; @@ -8,8 +8,8 @@ import {SERVER_API_URL} from "@/utils/server"; */ export async function pageUsers(params: UserParam) { const res = await request.get>>( - '/system/user/page', - {params} + SERVER_API_URL + '/system/user/page', + params ); if (res.code === 0) { return res.data; @@ -22,10 +22,8 @@ export async function pageUsers(params: UserParam) { */ export async function listUsers(params?: UserParam) { const res = await request.get>( - '/system/user', - { - params - } + SERVER_API_URL + '/system/user', + params ); if (res.code === 0 && res.data) { return res.data; @@ -38,7 +36,7 @@ export async function listUsers(params?: UserParam) { */ export async function getStaffs(params?: UserParam) { const res = await request.get>( - '/system/user', + SERVER_API_URL + '/system/user', { params } @@ -54,7 +52,7 @@ export async function getStaffs(params?: UserParam) { */ export async function getCompanyList(params?: UserParam) { const res = await request.get>( - '/system/user', + SERVER_API_URL + '/system/user', { params } @@ -70,7 +68,7 @@ export async function getCompanyList(params?: UserParam) { */ export async function getUser(id: number) { const res = await request.get>( - '/system/user/' + id, + SERVER_API_URL + '/system/user/' + id, {} ); if (res.code === 0 && res.data) { @@ -84,7 +82,7 @@ export async function getUser(id: number) { */ export async function addUser(data: User) { const res = await request.post>( - '/system/user', + SERVER_API_URL + '/system/user', data ); if (res.code === 0) { @@ -112,7 +110,7 @@ export async function updateUser(data: User) { */ export async function removeUser(id?: number) { const res = await request.del>( - '/system/user/' + id + SERVER_API_URL + '/system/user/' + id ); if (res.code === 0) { return res.message; @@ -125,7 +123,7 @@ export async function removeUser(id?: number) { */ export async function removeUsers(data: (number | undefined)[]) { const res = await request.del>( - '/system/user/batch', + SERVER_API_URL + '/system/user/batch', { data } @@ -141,7 +139,7 @@ export async function removeUsers(data: (number | undefined)[]) { */ export async function updateUserStatus(userId?: number, status?: number) { const res = await request.put>( - '/system/user/status', + SERVER_API_URL + '/system/user/status', { userId, status @@ -156,9 +154,9 @@ export async function updateUserStatus(userId?: number, status?: number) { /** * 修改推荐状态 */ -export async function updateUserRecommend(form) { +export async function updateUserRecommend(form:any) { const res = await request.put>( - '/system/user/recommend', + SERVER_API_URL + '/system/user/recommend', form ); if (res.code === 0) { @@ -172,7 +170,7 @@ export async function updateUserRecommend(form) { */ export async function updateUserPassword(userId?: number, password = '123456') { const res = await request.put>( - '/system/user/password', + SERVER_API_URL + '/system/user/password', { userId, password @@ -191,7 +189,7 @@ export async function importUsers(file: File) { const formData = new FormData(); formData.append('file', file); const res = await request.post>( - '/system/user/import', + SERVER_API_URL + '/system/user/import', formData ); if (res.code === 0) { @@ -209,7 +207,7 @@ export async function checkExistence( id?: number ) { const res = await request.get>( - '/system/user/existence', + SERVER_API_URL + '/system/user/existence', { params: {field, value, id} } @@ -225,7 +223,7 @@ export async function checkExistence( */ export async function countUserBalance(params?: UserParam) { const res = await request.get>( - '/system/user/countUserBalance', + SERVER_API_URL + '/system/user/countUserBalance', { params } @@ -243,7 +241,7 @@ export async function countUserBalance(params?: UserParam) { */ export async function listAdminsByPhoneAll(params?: UserParam) { const res = await request.get>( - '/system/user/listAdminsByPhoneAll', + SERVER_API_URL + '/system/user/listAdminsByPhoneAll', params ); if (res.code === 0 && res.data) { diff --git a/src/api/system/user/model/index.ts b/src/api/system/user/model/index.ts index 33c8feb..ab734c4 100644 --- a/src/api/system/user/model/index.ts +++ b/src/api/system/user/model/index.ts @@ -128,6 +128,8 @@ export interface User { certification?: boolean; // 实名认证类型 certificationType?: number; + // 推荐人ID + refereeId?: number; } /** diff --git a/src/app.config.ts b/src/app.config.ts index c01f7d3..8496b4d 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -1,9 +1,10 @@ -export default defineAppConfig({ +export default { pages: [ 'pages/index/index', 'pages/cart/cart', 'pages/find/find', - 'pages/user/user' + 'pages/user/user', + 'pages/cms/category/index' ], "subpackages": [ { @@ -14,7 +15,10 @@ export default defineAppConfig({ "forget", "setting", "agreement", - "sms-login" + "sms-login", + 'qr-login/index', + 'qr-confirm/index', + 'unified-qr/index' ] }, { @@ -30,12 +34,6 @@ export default defineAppConfig({ "index" ] }, - { - "root": "gift", - "pages": [ - "index" - ] - }, { "root": "user", "pages": [ @@ -60,7 +58,12 @@ export default defineAppConfig({ "gift/redeem", "gift/detail", "store/verification", - "theme/index" + "theme/index", + "poster/poster", + "chat/conversation/index", + "chat/message/index", + "chat/message/add", + "chat/message/detail" ] }, { @@ -112,12 +115,6 @@ export default defineAppConfig({ selectedIconPath: "assets/tabbar/home-active.png", text: "首页", }, - // { - // pagePath: "pages/find/find", - // iconPath: "assets/tabbar/find.png", - // selectedIconPath: "assets/tabbar/find-active.png", - // text: "发现", - // }, { pagePath: "pages/cart/cart", iconPath: "assets/tabbar/cart.png", @@ -142,4 +139,4 @@ export default defineAppConfig({ "desc": "你的位置信息将用于小程序位置接口的效果展示" } } -}) +} diff --git a/src/app.scss b/src/app.scss index 263b035..ae872ba 100644 --- a/src/app.scss +++ b/src/app.scss @@ -10,14 +10,14 @@ page{ background-position: bottom; } -// 在全局样式文件中添加 +/* 在全局样式文件中添加 */ button { &::after { border: none !important; } } -// 去掉 Grid 组件的边框 +/* 去掉 Grid 组件的边框 */ .no-border-grid { .nut-grid-item { border: none !important; @@ -38,7 +38,7 @@ button { } } -// 微信授权按钮的特殊样式 +/* 微信授权按钮的特殊样式 */ button[open-type="getPhoneNumber"] { background: none !important; padding: 0 !important; @@ -92,3 +92,12 @@ button[open-type="chooseAvatar"] { image { margin: 0; /* 全局设置图片的 margin */ } + +/* 管理员面板功能项交互效果 */ +.admin-feature-item { + transition: transform 0.15s ease-in-out; +} + +.admin-feature-item:active { + transform: scale(0.95); +} diff --git a/src/app.ts b/src/app.ts index 418ba66..9eba816 100644 --- a/src/app.ts +++ b/src/app.ts @@ -6,7 +6,7 @@ import './app.scss' import {loginByOpenId} from "@/api/layout"; import {TenantId} from "@/config/app"; import {saveStorageByLoginUser} from "@/utils/server"; -import {parseInviteParams, saveInviteParams, trackInviteSource, handleInviteRelation} from "@/utils/invite"; +import {parseInviteParams, saveInviteParams, trackInviteSource, handleInviteRelation, debugInviteInfo} from "@/utils/invite"; function App(props: { children: any; }) { const reload = () => { @@ -57,12 +57,13 @@ function App(props: { children: any; }) { // 处理启动参数 const handleLaunchOptions = (options: any) => { try { - console.log('小程序启动参数:', options) + console.log('=== 小程序启动参数处理开始 ===') + console.log('完整启动参数:', JSON.stringify(options, null, 2)) // 解析邀请参数 const inviteParams = parseInviteParams(options) if (inviteParams) { - console.log('检测到邀请参数:', inviteParams) + console.log('✅ 成功检测到邀请参数:', inviteParams) // 保存邀请参数到本地存储 saveInviteParams(inviteParams) @@ -73,12 +74,21 @@ function App(props: { children: any; }) { // 显示邀请提示 setTimeout(() => { Taro.showToast({ - title: '检测到邀请信息', + title: `检测到邀请信息 ID:${inviteParams.inviter}`, icon: 'success', - duration: 2000 + duration: 3000 }) }, 1000) + + // 打印调试信息 + setTimeout(() => { + debugInviteInfo() + }, 2000) + } else { + console.log('❌ 未检测到邀请参数') } + + console.log('=== 小程序启动参数处理结束 ===') } catch (error) { console.error('处理启动参数失败:', error) } diff --git a/src/assets/tabbar/logo.png b/src/assets/tabbar/logo.png new file mode 100644 index 0000000..ac9655f Binary files /dev/null and b/src/assets/tabbar/logo.png differ diff --git a/src/assets/tabbar/tv-active.png b/src/assets/tabbar/tv-active.png new file mode 100644 index 0000000..4852b4f Binary files /dev/null and b/src/assets/tabbar/tv-active.png differ diff --git a/src/assets/tabbar/tv.png b/src/assets/tabbar/tv.png new file mode 100644 index 0000000..2dd6bd0 Binary files /dev/null and b/src/assets/tabbar/tv.png differ diff --git a/src/cms/category/index.tsx b/src/cms/category/index.tsx index c3f8d35..fe81623 100644 --- a/src/cms/category/index.tsx +++ b/src/cms/category/index.tsx @@ -1,5 +1,5 @@ import Taro from '@tarojs/taro' -import {useShareAppMessage, useShareTimeline} from "@tarojs/taro" +import {useShareAppMessage} from "@tarojs/taro" import {Loading} from '@nutui/nutui-react-taro' import {useEffect, useState} from "react" import {useRouter} from '@tarojs/taro' @@ -42,22 +42,15 @@ function Category() { }) }, []); - useShareTimeline(() => { - return { - title: `${nav?.categoryName}_通源堂健康生态平台`, - path: `/shop/category/index?id=${categoryId}` - }; - }); - useShareAppMessage(() => { return { - title: `${nav?.categoryName}_通源堂健康生态平台`, + title: `${nav?.categoryName}_时里院子市集`, path: `/shop/category/index?id=${categoryId}`, - success: function (res) { - console.log('分享成功', res); + success: function () { + console.log('分享成功'); }, - fail: function (res) { - console.log('分享失败', res); + fail: function () { + console.log('分享失败'); } }; }); diff --git a/src/components/AdminPanel.tsx b/src/components/AdminPanel.tsx new file mode 100644 index 0000000..8aa3d9b --- /dev/null +++ b/src/components/AdminPanel.tsx @@ -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 = ({ + visible, + onClose, + className = '' +}) => { + if (!visible) return null; + + // 管理员功能列表 + const adminFeatures = [ + { + id: 'unified-qr', + title: '统一扫码', + description: '扫码登录和核销一体化功能', + icon: , + color: 'bg-blue-50 border-blue-200', + onClick: () => { + navTo('/passport/unified-qr/index', true); + onClose?.(); + } + }, + { + id: 'user-management', + title: '用户管理', + description: '管理系统用户信息', + icon: , + color: 'bg-purple-50 border-purple-200', + onClick: () => { + // TODO: 跳转到用户管理页面 + console.log('跳转到用户管理'); + onClose?.(); + } + }, + { + id: 'store-management', + title: '门店管理', + description: '管理门店信息和设置', + icon: , + color: 'bg-orange-50 border-orange-200', + onClick: () => { + // TODO: 跳转到门店管理页面 + console.log('跳转到门店管理'); + onClose?.(); + } + }, + { + id: 'system-settings', + title: '系统设置', + description: '系统配置和参数管理', + icon: , + color: 'bg-gray-50 border-gray-200', + onClick: () => { + // TODO: 跳转到系统设置页面 + console.log('跳转到系统设置'); + onClose?.(); + } + } + ]; + + return ( + + {/* 遮罩层 */} + + + {/* 面板内容 */} + + {/* 面板头部 */} + + + + 管理员面板 + + + + + {/* 功能网格 */} + + + {adminFeatures.map((feature) => ( + + + {feature.icon} + + {feature.title} + + + + {feature.description} + + + ))} + + + + {/* 底部提示 */} + + + + 💡 管理员功能仅对具有管理权限的用户开放 + + + + + + ); +}; + +export default AdminPanel; diff --git a/src/components/CouponCard.scss b/src/components/CouponCard.scss index 7127d49..9d00ba2 100644 --- a/src/components/CouponCard.scss +++ b/src/components/CouponCard.scss @@ -9,25 +9,25 @@ border: 2px solid #f0f0f0; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - // 更精美的阴影效果 - //box-shadow: - // 0 4px 20px rgba(0, 0, 0, 0.08), - // 0 1px 3px rgba(0, 0, 0, 0.1); + /* 更精美的阴影效果 */ + /*box-shadow: + 0 4px 20px rgba(0, 0, 0, 0.08), + 0 1px 3px rgba(0, 0, 0, 0.1);*/ - // 边框光晕效果 - //&::before { - // content: ''; - // position: absolute; - // top: 0; - // left: 0; - // right: 0; - // bottom: 0; - // border-radius: 16px; - // padding: 1px; - // background: linear-gradient(135deg, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0.1)); - // mask-composite: exclude; - // pointer-events: none; - //} + /* 边框光晕效果 */ + /*&::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 16px; + padding: 1px; + background: linear-gradient(135deg, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0.1)); + mask-composite: exclude; + pointer-events: none; + }*/ &:active { transform: scale(0.98) translateY(1px); @@ -38,7 +38,7 @@ &.disabled { opacity: 0.6; - filter: grayscale(0.3); + /* filter: grayscale(0.3); 小程序不支持filter属性 */ } .coupon-left { @@ -52,7 +52,7 @@ position: relative; overflow: hidden; - // 添加光泽效果 + /* 添加光泽效果 */ &::after { content: ''; position: absolute; @@ -199,11 +199,7 @@ line-height: 1.3; letter-spacing: -0.5px; - // 添加文字渐变效果 - background: linear-gradient(135deg, #1a202c 0%, #2d3748 100%); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; + /* 文字渐变效果在小程序中不支持,使用纯色替代 */ } .coupon-validity { @@ -212,7 +208,7 @@ line-height: 1.2; font-weight: 500; - // 添加图标前缀 + /* 添加图标前缀 */ &::before { content: '⏰'; margin-right: 6px; @@ -239,7 +235,7 @@ position: relative; overflow: hidden; - // 添加按钮光泽效果 + /* 添加按钮光泽效果 */ &::before { content: ''; position: absolute; @@ -309,7 +305,7 @@ align-items: center; justify-content: center; z-index: 10; - backdrop-filter: blur(2px); + /* backdrop-filter: blur(2px); 小程序不支持backdrop-filter属性 */ .status-badge { background: rgba(0, 0, 0, 0.7); @@ -324,7 +320,7 @@ } } -// 动画效果 +/* 动画效果 */ @keyframes shimmer { 0% { transform: translateX(-100%) translateY(-100%) rotate(45deg); @@ -334,7 +330,7 @@ } } -// 响应式优化 +/* 响应式优化 */ @media (max-width: 768px) { .coupon-card { height: 150px; diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 72e11cf..72425d7 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -2,7 +2,7 @@ import {NavBar} from '@nutui/nutui-react-taro' import {ArrowLeft} from '@nutui/icons-react-taro' import Taro from '@tarojs/taro' -function Header(props) { +function Header(props: any) { return ( <> void; + /** 点击失败回调 */ + onError?: (error: string) => void; + /** 是否使用页面模式(跳转到专门页面) */ + usePageMode?: boolean; +} + +/** + * 扫码登录按钮组件 + */ +const QRLoginButton: React.FC = ({ + 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 ( + + ); +}; + +export default QRLoginButton; diff --git a/src/components/QRLoginScanner.tsx b/src/components/QRLoginScanner.tsx new file mode 100644 index 0000000..a891376 --- /dev/null +++ b/src/components/QRLoginScanner.tsx @@ -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 = ({ + 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 ( + + + 请扫描登录二维码... + + ); + + case ScanLoginState.CONFIRMING: + return ( + + + 正在确认登录... + + ); + + case ScanLoginState.SUCCESS: + return ( + + + 登录确认成功! + + ); + + case ScanLoginState.ERROR: + return ( + + + {error || '扫码登录失败'} + + ); + + 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 ( + + {/* 扫码按钮 */} + + + {/* 状态显示 */} + {showStatus && ( + + {getStatusContent()} + + )} + + {/* 成功结果显示 */} + {state === ScanLoginState.SUCCESS && result && ( + + + 已为用户 {result.userInfo?.nickname || result.userInfo?.userId} 确认登录 + + + )} + + {/* 使用说明 */} + {state === ScanLoginState.IDLE && ( + + + 扫描网页端显示的登录二维码即可快速登录 + + + )} + + ); +}; + +export default QRLoginScanner; diff --git a/src/components/QRScanModal.tsx b/src/components/QRScanModal.tsx new file mode 100644 index 0000000..efbce60 --- /dev/null +++ b/src/components/QRScanModal.tsx @@ -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 = ({ + 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((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: , + title: '正在扫码...', + description: '请将二维码对准摄像头' + }; + + case 'confirming': + return { + icon: , + title: '正在确认登录...', + description: '请稍候,正在为您确认登录' + }; + + case 'success': + return { + icon: , + title: '登录确认成功', + description: '网页端将自动完成登录' + }; + + case 'error': + return { + icon: , + title: '操作失败', + description: errorMsg || '请重试' + }; + + default: + return { + icon: , + title, + description + }; + } + }; + + const statusContent = getStatusContent(); + + return ( + + + {/* 关闭按钮 */} + {status !== 'scanning' && status !== 'confirming' && ( + + + + )} + + {/* 图标 */} + + {statusContent.icon} + + + {/* 标题 */} + + {statusContent.title} + + + {/* 描述 */} + + {statusContent.description} + + + {/* 操作按钮 */} + {status === 'idle' && ( + + )} + + {status === 'error' && ( + + + + + )} + + {(status === 'scanning' || status === 'confirming') && ( + + )} + + {loading} + + ); +}; + +export default QRScanModal; diff --git a/src/components/UnifiedQRButton.tsx b/src/components/UnifiedQRButton.tsx new file mode 100644 index 0000000..18fee99 --- /dev/null +++ b/src/components/UnifiedQRButton.tsx @@ -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 = ({ + 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 ( + + ); +}; + +export default UnifiedQRButton; diff --git a/src/dealer/apply/add.config.ts b/src/dealer/apply/add.config.ts index 3f7f9ad..ac37521 100644 --- a/src/dealer/apply/add.config.ts +++ b/src/dealer/apply/add.config.ts @@ -1,4 +1,4 @@ export default definePageConfig({ - navigationBarTitleText: '医生入驻申请通道', + navigationBarTitleText: '邀请注册', navigationBarTextStyle: 'black' }) diff --git a/src/dealer/apply/add.tsx b/src/dealer/apply/add.tsx index 08c2f15..f862bf3 100644 --- a/src/dealer/apply/add.tsx +++ b/src/dealer/apply/add.tsx @@ -1,96 +1,210 @@ import {useEffect, useState, useRef} from "react"; -import {Loading, CellGroup, Cell, Input, Form} from '@nutui/nutui-react-taro' +import {Loading, CellGroup, Input, Form, Avatar, Button, Space} from '@nutui/nutui-react-taro' import {Edit} from '@nutui/icons-react-taro' import Taro from '@tarojs/taro' import {View} from '@tarojs/components' import FixedButton from "@/components/FixedButton"; import {useUser} from "@/hooks/useUser"; -import {ShopDealerApply} from "@/api/shop/shopDealerApply/model"; -import { - addShopDealerApply, - pageShopDealerApply, - updateShopDealerApply -} from "@/api/shop/shopDealerApply"; -import {getShopDealerUser} from "@/api/shop/shopDealerUser"; +import {TenantId} from "@/config/app"; +import {updateUser} from "@/api/system/user"; +import {User} from "@/api/system/user/model"; +import {getStoredInviteParams, handleInviteRelation} from "@/utils/invite"; +import {addShopDealerUser} from "@/api/shop/shopDealerUser"; +import {listUserRole, updateUserRole} from "@/api/system/userRole"; + +// 类型定义 +interface ChooseAvatarEvent { + detail: { + avatarUrl: string; + }; +} + +interface InputEvent { + detail: { + value: string; + }; +} const AddUserAddress = () => { - const {user} = useUser() + const {user, loginUser} = useUser() const [loading, setLoading] = useState(true) - const [FormData, setFormData] = useState() + const [FormData, setFormData] = useState() const formRef = useRef(null) - const [isEditMode, setIsEditMode] = useState(false) - const [existingApply, setExistingApply] = useState(null) - // 获取审核状态文字 - const getApplyStatusText = (status?: number) => { - switch (status) { - case 10: - return '待审核' - case 20: - return '审核通过' - case 30: - return '驳回' - default: - return '未知状态' + const reload = async () => { + const inviteParams = getStoredInviteParams() + if (inviteParams?.inviter) { + setFormData({ + ...user, + refereeId: Number(inviteParams.inviter), + // 清空昵称,强制用户手动输入 + nickname: '', + }) + } else { + // 如果没有邀请参数,也要确保昵称为空 + setFormData({ + ...user, + nickname: '', + }) } } - const reload = async () => { - // 判断用户是否登录 - if (!user?.userId) { - return false; + + const uploadAvatar = ({detail}: ChooseAvatarEvent) => { + // 先更新本地显示的头像(临时显示) + const tempFormData = { + ...FormData, + avatar: `${detail.avatarUrl}`, } - // 查询当前用户ID是否已有申请记录 - try { - const res = await pageShopDealerApply({userId: user?.userId}); - if (res && res.count > 0) { - setIsEditMode(true); - setExistingApply(res.list[0]); - // 如果有记录,填充表单数据 - setFormData(res.list[0]); - setLoading(false) - } else { - setIsEditMode(false); - setExistingApply(null); - setLoading(false) + setFormData(tempFormData) + + Taro.uploadFile({ + url: 'https://server.websoft.top/api/oss/upload', + filePath: detail.avatarUrl, + name: 'file', + header: { + 'content-type': 'application/json', + TenantId + }, + success: async (res) => { + const data = JSON.parse(res.data); + if (data.code === 0) { + const finalAvatarUrl = `${data.data.thumbnail}` + + try { + // 使用 useUser hook 的 updateUser 方法更新头像 + await updateUser({ + avatar: finalAvatarUrl + }) + + Taro.showToast({ + title: '头像上传成功', + icon: 'success', + duration: 1500 + }) + } catch (error) { + console.error('更新用户头像失败:', error) + } + + // 无论用户信息更新是否成功,都要更新本地FormData + const finalFormData = { + ...tempFormData, + avatar: finalAvatarUrl + } + setFormData(finalFormData) + + // 同步更新表单字段 + if (formRef.current) { + formRef.current.setFieldsValue({ + avatar: finalAvatarUrl + }) + } + } else { + // 上传失败,恢复原来的头像 + setFormData({ + ...FormData, + avatar: user?.avatar || '' + }) + Taro.showToast({ + title: '上传失败', + icon: 'error' + }) + } + }, + fail: (error) => { + console.error('上传头像失败:', error) + Taro.showToast({ + title: '上传失败', + icon: 'error' + }) + // 恢复原来的头像 + setFormData({ + ...FormData, + avatar: user?.avatar || '' + }) } - } catch (error) { - setLoading(true) - console.error('查询申请记录失败:', error); - setIsEditMode(false); - setExistingApply(null); - } + }) } // 提交表单 const submitSucceed = async (values: any) => { try { + // 验证必填字段 + if (!values.phone && !FormData?.phone) { + Taro.showToast({ + title: '请先获取手机号', + icon: 'error' + }); + return; + } + + // 验证昵称:必须填写且不能是默认的微信昵称 + const nickname = values.realName || FormData?.nickname || ''; + if (!nickname || nickname.trim() === '') { + Taro.showToast({ + title: '请填写昵称', + icon: 'error' + }); + return; + } + + // 检查是否为默认的微信昵称(常见的默认昵称) + const defaultNicknames = ['微信用户', 'WeChat User', '微信昵称']; + if (defaultNicknames.includes(nickname.trim())) { + Taro.showToast({ + title: '请填写真实昵称,不能使用默认昵称', + icon: 'error' + }); + return; + } + + // 验证昵称长度 + if (nickname.trim().length < 2) { + Taro.showToast({ + title: '昵称至少需要2个字符', + icon: 'error' + }); + return; + } + + if (!values.avatar && !FormData?.avatar) { + Taro.showToast({ + title: '请上传头像', + icon: 'error' + }); + return; + } + console.log(values,FormData) + + const roles = await listUserRole({userId: user?.userId}) + console.log(roles, 'roles...') // 准备提交的数据 - const submitData = { - ...values, - realName: values.realName || user?.nickname, - mobile: user?.phone, - refereeId: values.refereeId || FormData?.refereeId, - applyStatus: 10, - auditTime: undefined - }; - await getShopDealerUser(submitData.refereeId); + await updateUser({ + userId: user?.userId, + nickname: values.realName || FormData?.nickname, + phone: values.phone || FormData?.phone, + avatar: values.avatar || FormData?.avatar, + refereeId: values.refereeId || FormData?.refereeId + }); - // 如果是编辑模式,添加现有申请的id - if (isEditMode && existingApply?.applyId) { - submitData.applyId = existingApply.applyId; + await addShopDealerUser({ + userId: user?.userId, + realName: values.realName || FormData?.nickname, + mobile: values.phone || FormData?.phone, + refereeId: values.refereeId || FormData?.refereeId + }) + + if (roles.length > 0) { + await updateUserRole({ + ...roles[0], + roleId: 1848 + }) } - // 执行新增或更新操作 - if (isEditMode) { - await updateShopDealerApply(submitData); - } else { - await addShopDealerApply(submitData); - } Taro.showToast({ - title: `${isEditMode ? '提交' : '提交'}成功`, + title: `注册成功`, icon: 'success' }); @@ -100,13 +214,130 @@ const AddUserAddress = () => { } catch (error) { console.error('验证邀请人失败:', error); - return Taro.showToast({ - title: '邀请人ID不存在', - icon: 'error' - }); } } + // 获取微信昵称 + const getWxNickname = (nickname: string) => { + // 更新表单数据 + const updatedFormData = { + ...FormData, + nickname: nickname + } + setFormData(updatedFormData); + + // 同步更新表单字段 + if (formRef.current) { + formRef.current.setFieldsValue({ + realName: nickname + }) + } + } + + /* 获取用户手机号 */ + const handleGetPhoneNumber = ({detail}: { detail: { code?: string, encryptedData?: string, iv?: string } }) => { + const {code, encryptedData, iv} = detail + Taro.login({ + success: (loginRes) => { + if (code) { + Taro.request({ + url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone', + method: 'POST', + data: { + authCode: loginRes.code, + code, + encryptedData, + iv, + notVerifyPhone: true, + refereeId: 0, + sceneType: 'save_referee', + tenantId: TenantId + }, + header: { + 'content-type': 'application/json', + TenantId + }, + success: async function (res) { + if (res.data.code == 1) { + Taro.showToast({ + title: res.data.message, + icon: 'error', + duration: 2000 + }) + return false; + } + // 登录成功 + const token = res.data.data.access_token; + const userData = res.data.data.user; + console.log(userData, 'userData...') + // 使用useUser Hook的loginUser方法更新状态 + loginUser(token, userData); + + if (userData.phone) { + console.log('手机号已获取', userData.phone) + const updatedFormData = { + ...FormData, + phone: userData.phone, + // 不自动填充微信昵称,保持用户已输入的昵称 + nickname: FormData?.nickname || '', + // 只在没有头像时才使用微信头像 + avatar: FormData?.avatar || userData.avatar + } + setFormData(updatedFormData) + + // 更新表单字段值 + if (formRef.current) { + formRef.current.setFieldsValue({ + phone: userData.phone, + // 不覆盖用户已输入的昵称 + realName: FormData?.nickname || '', + avatar: FormData?.avatar || userData.avatar + }) + } + + Taro.showToast({ + title: '手机号获取成功', + icon: 'success', + duration: 1500 + }) + } + + + // 处理邀请关系 + if (userData?.userId) { + try { + const inviteSuccess = await handleInviteRelation(userData.userId) + if (inviteSuccess) { + Taro.showToast({ + title: '邀请关系建立成功', + icon: 'success', + duration: 2000 + }) + } + } catch (error) { + console.error('处理邀请关系失败:', error) + } + } + + // 显示登录成功提示 + // Taro.showToast({ + // title: '注册成功', + // icon: 'success', + // duration: 1500 + // }) + + // 不需要重新启动小程序,状态已经通过useUser更新 + // 可以选择性地刷新当前页面数据 + // await reload(); + } + }) + } else { + console.log('登录失败!') + } + } + }) + } + // 处理固定按钮点击事件 const handleFixedButtonClick = () => { // 触发表单提交 @@ -123,6 +354,18 @@ const AddUserAddress = () => { }) }, [user?.userId]); // 依赖用户ID,当用户变化时重新加载 + // 当FormData变化时,同步更新表单字段值 + useEffect(() => { + if (formRef.current && FormData) { + formRef.current.setFieldsValue({ + refereeId: FormData.refereeId, + phone: FormData.phone, + avatar: FormData.avatar, + realName: FormData.nickname + }); + } + }, [FormData]); + if (loading) { return 加载中 } @@ -139,50 +382,49 @@ const AddUserAddress = () => { > - - - - - - - + + + + + + + + + + + { + FormData?.phone && + + + } + + getWxNickname(e.detail.value)} + /> - {/* 审核状态显示(仅在编辑模式下显示) */} - {isEditMode && ( - - - {getApplyStatusText(FormData?.applyStatus)} - - } - /> - {FormData?.applyStatus === 20 && ( - - )} - {FormData?.applyStatus === 30 && ( - - )} - - )} - {/* 底部浮动按钮 */} - {(!isEditMode || FormData?.applyStatus === 10 || FormData?.applyStatus === 30) && ( - } - text={isEditMode ? '保存修改' : '提交申请'} - disabled={FormData?.applyStatus === 10} - onClick={handleFixedButtonClick} - /> - )} + } + text={'立即注册'} + onClick={handleFixedButtonClick} + /> ); diff --git a/src/dealer/bank/add.config.ts b/src/dealer/bank/add.config.ts new file mode 100644 index 0000000..e07ffad --- /dev/null +++ b/src/dealer/bank/add.config.ts @@ -0,0 +1,4 @@ +export default definePageConfig({ + navigationBarTitleText: '添加银行卡', + navigationBarTextStyle: 'black' +}) diff --git a/src/dealer/bank/add.tsx b/src/dealer/bank/add.tsx new file mode 100644 index 0000000..ef11e80 --- /dev/null +++ b/src/dealer/bank/add.tsx @@ -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(true) + const [FormData, setFormData] = useState() + const formRef = useRef(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 加载中 + } + + return ( + <> +
submitSucceed(values)} + onFinishFailed={(errors) => submitFailed(errors)} + > + + + + + + + + + + + +
+ + {/* 底部浮动按钮 */} + formRef.current?.submit()}/> + + ); +}; + +export default AddUserAddress; diff --git a/src/dealer/bank/index.config.ts b/src/dealer/bank/index.config.ts new file mode 100644 index 0000000..8f92036 --- /dev/null +++ b/src/dealer/bank/index.config.ts @@ -0,0 +1,4 @@ +export default definePageConfig({ + navigationBarTitleText: '银行卡管理', + navigationBarTextStyle: 'black' +}) diff --git a/src/dealer/bank/index.tsx b/src/dealer/bank/index.tsx new file mode 100644 index 0000000..71046ff --- /dev/null +++ b/src/dealer/bank/index.tsx @@ -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([]) + const [bank, setAddress] = useState() + + 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 ( + +
+ + + + + +
+
+ ) + } + + return ( + + {list.map((item, _) => ( + + selectAddress(item)}> + + {item.bankName} + + + {item.bankCard} {item.bankAccount} + + + onDefault(item)}> + {item.isDefault ? : } + 选择 + + } + extra={ + <> + onDel(item.id)}> + 删除 + + + } + /> + + ))} + {/* 底部浮动按钮 */} + Taro.navigateTo({url: '/dealer/bank/add'})} /> +
+ ); +}; + +export default DealerBank; diff --git a/src/dealer/customer/README.md b/src/dealer/customer/README.md new file mode 100644 index 0000000..20ccfd6 --- /dev/null +++ b/src/dealer/customer/README.md @@ -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 # 客户状态工具函数 +``` diff --git a/src/dealer/customer/add.config.ts b/src/dealer/customer/add.config.ts new file mode 100644 index 0000000..fb7c4ce --- /dev/null +++ b/src/dealer/customer/add.config.ts @@ -0,0 +1,4 @@ +export default definePageConfig({ + navigationBarTitleText: '客户报备', + navigationBarTextStyle: 'black' +}) diff --git a/src/dealer/customer/add.tsx b/src/dealer/customer/add.tsx new file mode 100644 index 0000000..d91c3b0 --- /dev/null +++ b/src/dealer/customer/add.tsx @@ -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(true) + const [FormData, setFormData] = useState() + const formRef = useRef(null) + const [isEditMode, setIsEditMode] = useState(false) + const [existingApply, setExistingApply] = useState(null) + + // 日期选择器状态 + const [showApplyTimePicker, setShowApplyTimePicker] = useState(false) + const [showContractTimePicker, setShowContractTimePicker] = useState(false) + const [applyTime, setApplyTime] = useState('') + const [contractTime, setContractTime] = useState('') + + // 获取审核状态文字 + 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((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 加载中 + } + + return ( + <> +
submitSucceed(values)} + onFinishFailed={(errors) => submitFailed(errors)} + > + + + + + + + + + + + + + + + + + + + + + {isEditMode && ( + <> + + + + + setShowApplyTimePicker(true)} + > + + + + {applyTime ? formatDateForDisplay(applyTime) : '请选择签约时间'} + + + + + + setShowContractTimePicker(true)} + > + + + + {contractTime ? formatDateForDisplay(contractTime) : '请选择合同生效起止时间'} + + + + + {/**/} + {/* */} + {/**/} + + )} + +选择 + + +
+ + {/* 签约时间选择器 */} + setShowApplyTimePicker(false)} + onConfirm={handleApplyTimeConfirm} + /> + + {/* 合同日期选择器 */} + setShowContractTimePicker(false)} + onConfirm={handleContractTimeConfirm} + /> + + {/* 审核状态显示(仅在编辑模式下显示) */} + {isEditMode && ( + + {/**/} + {/* {getApplyStatusText(FormData?.applyStatus)}*/} + {/* */} + {/* }*/} + {/*/>*/} + {FormData?.applyStatus === 20 && ( + + )} + {FormData?.applyStatus === 30 && ( + + )} + + )} + + + {/* 底部浮动按钮 */} + {(!isEditMode || FormData?.applyStatus === 10) && ( + } + text={'立即提交'} + onClick={handleFixedButtonClick} + /> + )} + + + ); +}; + +export default AddShopDealerApply; diff --git a/src/dealer/customer/index.config.ts b/src/dealer/customer/index.config.ts new file mode 100644 index 0000000..6e26749 --- /dev/null +++ b/src/dealer/customer/index.config.ts @@ -0,0 +1,3 @@ +export default definePageConfig({ + navigationBarTitleText: '客户列表' +}) diff --git a/src/dealer/customer/index.tsx b/src/dealer/customer/index.tsx new file mode 100644 index 0000000..bc316ec --- /dev/null +++ b/src/dealer/customer/index.tsx @@ -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([]) + const [loading, setLoading] = useState(false) + const [activeTab, setActiveTab] = useState('all') + const [searchValue, _] = useState('') + 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) => ( + + + + + + {customer.dealerName} + + {customer.customerStatus && ( + + {getStatusText(customer.customerStatus)} + + )} + + + + 联系人:{customer.realName} + + { + e.stopPropagation(); + makePhoneCall(customer.mobile || ''); + }}>联系电话:{customer.mobile} + + { + e.stopPropagation(); + makePhoneCall(customer.mobile || ''); + }} + /> + { + e.stopPropagation(); + copyPhone(customer.mobile || ''); + }} + > + 复制 + + + + + 添加时间:{customer.createTime} + + + + + {/* 保护天数显示 */} + {customer.applyStatus === 10 && ( + + 保护期: + {customer.protectDays && customer.protectDays > 0 ? ( + + 剩余{customer.protectDays}天 + + ) : ( + + 已过期 + + )} + + )} + + + 报备人:{customer?.nickName} + + {customer?.refereeName} + + + {/* 显示 comments 字段 */} + + 跟进情况:{customer.comments || '暂无'} + { + e.stopPropagation(); + editComments(customer); + }} + > + 编辑 + + + + + + {/* 跟进中状态显示操作按钮 */} + {(customer.applyStatus === 10 && customer.userId == Taro.getStorageSync('UserId')) && ( + + + + + )} + {(customer.applyStatus === 30 && customer.userId == Taro.getStorageSync('UserId')) && ( + + + + )} + + ); + + // 渲染客户列表 + const renderCustomerList = () => { + const filteredList = getFilteredList(); + + return ( + + { + // 滚动事件处理 + }} + onScrollToUpper={() => { + // 滚动到顶部事件处理 + }} + loadingText={ + <> + 加载中... + + } + loadMoreText={ + filteredList.length === 0 ? ( + + ) : ( + + 没有更多了 + + ) + } + > + {loading && filteredList.length === 0 ? ( + + + 加载中... + + ) : ( + filteredList.map(renderCustomerItem) + )} + + + ); + }; + + return ( + + + {/* 顶部Tabs */} + + setActiveTab(value as CustomerStatus)} + > + {tabList.map(tab => { + const counts = getStatusCounts(); + const count = counts[tab.value as keyof typeof counts] || 0; + return ( + 0 ? `(${count})` : ''}`} + value={tab.value} + /> + ); + })} + + + + {/* 客户列表 */} + {renderCustomerList()} + + Taro.navigateTo({url: '/dealer/customer/add'})}/> + + ); +}; + +export default CustomerIndex; diff --git a/src/dealer/customer/trading.config.ts b/src/dealer/customer/trading.config.ts new file mode 100644 index 0000000..8773b6f --- /dev/null +++ b/src/dealer/customer/trading.config.ts @@ -0,0 +1,3 @@ +export default definePageConfig({ + navigationBarTitleText: '入市查询' +}) diff --git a/src/dealer/customer/trading.tsx b/src/dealer/customer/trading.tsx new file mode 100644 index 0000000..43d8ca4 --- /dev/null +++ b/src/dealer/customer/trading.tsx @@ -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([]) + const [loading, setLoading] = useState(false) + const [searchValue, setSearchValue] = useState('') + 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) => ( + + + + + + {customer.dealerName} + + + + {/*统一代码:{customer.dealerCode}*/} + + 更新时间:{customer.createTime} + + + + + + ); + + // 渲染客户列表 + const renderCustomerList = () => { + const filteredList = getFilteredList(); + + return ( + + { + // 滚动事件处理 + }} + onScrollToUpper={() => { + // 滚动到顶部事件处理 + }} + loadingText={ + <> + 加载中... + + } + loadMoreText={ + filteredList.length === 0 ? ( + + ) : ( + + 没有更多了 + + ) + } + > + {loading && filteredList.length === 0 ? ( + + + 加载中... + + ) : ( + filteredList.map(renderCustomerItem) + )} + + + ); + }; + + return ( + + {/* 搜索栏 */} + + handleSearch(value)} + onClear={() => handleClearSearch()} + /> + + + {/* 客户列表 */} + {renderCustomerList()} + + + ); +}; + +export default CustomerTrading; diff --git a/src/dealer/index.config.ts b/src/dealer/index.config.ts index bbd5ebc..d456dbd 100644 --- a/src/dealer/index.config.ts +++ b/src/dealer/index.config.ts @@ -1,3 +1,3 @@ export default definePageConfig({ - navigationBarTitleText: '医生版' + navigationBarTitleText: '分销中心' }) diff --git a/src/dealer/index.tsx b/src/dealer/index.tsx index da6ce5d..06c514f 100644 --- a/src/dealer/index.tsx +++ b/src/dealer/index.tsx @@ -131,28 +131,28 @@ const DealerIndex: React.FC = () => { 佣金统计 - + - - ¥{formatMoney(dealerUser.money)} + + {formatMoney(dealerUser.money)} 可提现 - - ¥{formatMoney(dealerUser.freezeMoney)} + + {formatMoney(dealerUser.freezeMoney)} 冻结中 - - ¥{formatMoney(dealerUser.totalMoney)} + + {formatMoney(dealerUser.totalMoney)} 累计收益 diff --git a/src/dealer/orders/index.tsx b/src/dealer/orders/index.tsx index 1cc459b..f3b6bbf 100644 --- a/src/dealer/orders/index.tsx +++ b/src/dealer/orders/index.tsx @@ -1,161 +1,63 @@ -import React, { useState, useEffect, useCallback } from 'react' -import { View, Text } from '@tarojs/components' -import { Empty, Tabs, Tag, PullToRefresh, Loading } from '@nutui/nutui-react-taro' +import React, {useState, useEffect, useCallback} from 'react' +import {View, Text, ScrollView} from '@tarojs/components' +import {Empty, Tag, PullToRefresh, Loading} from '@nutui/nutui-react-taro' import Taro from '@tarojs/taro' -import { pageShopDealerOrder } from '@/api/shop/shopDealerOrder' -import { useDealerUser } from '@/hooks/useDealerUser' -import type { ShopDealerOrder } from '@/api/shop/shopDealerOrder/model' +import {pageShopDealerOrder} from '@/api/shop/shopDealerOrder' +import {useDealerUser} from '@/hooks/useDealerUser' +import type {ShopDealerOrder} from '@/api/shop/shopDealerOrder/model' interface OrderWithDetails extends ShopDealerOrder { orderNo?: string customerName?: string - totalCommission?: string - // 当前用户在此订单中的层级和佣金 - userLevel?: 1 | 2 | 3 userCommission?: string } const DealerOrders: React.FC = () => { - const [activeTab, setActiveTab] = useState('0') const [loading, setLoading] = useState(false) + const [refreshing, setRefreshing] = useState(false) + const [loadingMore, setLoadingMore] = useState(false) const [orders, setOrders] = useState([]) - const [statistics, setStatistics] = useState({ - totalOrders: 0, - totalCommission: '0.00', - pendingCommission: '0.00', - // 分层统计 - level1: { orders: 0, commission: '0.00' }, - level2: { orders: 0, commission: '0.00' }, - level3: { orders: 0, commission: '0.00' } - }) + const [currentPage, setCurrentPage] = useState(1) + const [hasMore, setHasMore] = useState(true) - const { dealerUser } = useDealerUser() + const {dealerUser} = useDealerUser() - // 获取订单数据 - 查询当前用户作为各层级分销商的所有订单 - const fetchOrders = useCallback(async () => { + // 获取订单数据 + const fetchOrders = useCallback(async (page: number = 1, isRefresh: boolean = false) => { if (!dealerUser?.userId) return try { - setLoading(true) - - // 并行查询三个层级的订单 - const [level1Result, level2Result, level3Result] = await Promise.all([ - // 一级分销商订单 - pageShopDealerOrder({ - page: 1, - limit: 100, - firstUserId: dealerUser.userId - }), - // 二级分销商订单 - pageShopDealerOrder({ - page: 1, - limit: 100, - secondUserId: dealerUser.userId - }), - // 三级分销商订单 - pageShopDealerOrder({ - page: 1, - limit: 100, - thirdUserId: dealerUser.userId - }) - ]) - - const allOrders: OrderWithDetails[] = [] - const stats = { - totalOrders: 0, - totalCommission: '0.00', - pendingCommission: '0.00', - level1: { orders: 0, commission: '0.00' }, - level2: { orders: 0, commission: '0.00' }, - level3: { orders: 0, commission: '0.00' } + if (isRefresh) { + setRefreshing(true) + } else if (page === 1) { + setLoading(true) + } else { + setLoadingMore(true) } - // 处理一级分销订单 - if (level1Result?.list) { - const level1Orders = level1Result.list.map(order => ({ + const result = await pageShopDealerOrder({ + page, + limit: 10 + }) + + if (result?.list) { + const newOrders = result.list.map(order => ({ ...order, - orderNo: `DD${order.orderId}`, + orderNo: `${order.orderId}`, customerName: `用户${order.userId}`, - userLevel: 1 as const, - userCommission: order.firstMoney || '0.00', - totalCommission: ( - parseFloat(order.firstMoney || '0') + - parseFloat(order.secondMoney || '0') + - parseFloat(order.thirdMoney || '0') - ).toFixed(2) + userCommission: order.firstMoney || '0.00' })) - allOrders.push(...level1Orders) - stats.level1.orders = level1Orders.length - stats.level1.commission = level1Orders.reduce((sum, order) => - sum + parseFloat(order.userCommission || '0'), 0 - ).toFixed(2) + if (page === 1) { + setOrders(newOrders) + } else { + setOrders(prev => [...prev, ...newOrders]) + } + + setHasMore(newOrders.length === 10) + setCurrentPage(page) } - // 处理二级分销订单 - if (level2Result?.list) { - const level2Orders = level2Result.list.map(order => ({ - ...order, - orderNo: `DD${order.orderId}`, - customerName: `用户${order.userId}`, - userLevel: 2 as const, - userCommission: order.secondMoney || '0.00', - totalCommission: ( - parseFloat(order.firstMoney || '0') + - parseFloat(order.secondMoney || '0') + - parseFloat(order.thirdMoney || '0') - ).toFixed(2) - })) - - allOrders.push(...level2Orders) - stats.level2.orders = level2Orders.length - stats.level2.commission = level2Orders.reduce((sum, order) => - sum + parseFloat(order.userCommission || '0'), 0 - ).toFixed(2) - } - - // 处理三级分销订单 - if (level3Result?.list) { - const level3Orders = level3Result.list.map(order => ({ - ...order, - orderNo: `DD${order.orderId}`, - customerName: `用户${order.userId}`, - userLevel: 3 as const, - userCommission: order.thirdMoney || '0.00', - totalCommission: ( - parseFloat(order.firstMoney || '0') + - parseFloat(order.secondMoney || '0') + - parseFloat(order.thirdMoney || '0') - ).toFixed(2) - })) - - allOrders.push(...level3Orders) - stats.level3.orders = level3Orders.length - stats.level3.commission = level3Orders.reduce((sum, order) => - sum + parseFloat(order.userCommission || '0'), 0 - ).toFixed(2) - } - - // 去重(同一个订单可能在多个层级中出现) - const uniqueOrders = allOrders.filter((order, index, self) => - index === self.findIndex(o => o.orderId === order.orderId) - ) - - // 计算总统计 - stats.totalOrders = uniqueOrders.length - stats.totalCommission = ( - parseFloat(stats.level1.commission) + - parseFloat(stats.level2.commission) + - parseFloat(stats.level3.commission) - ).toFixed(2) - stats.pendingCommission = allOrders - .filter(order => order.isSettled === 0) - .reduce((sum, order) => sum + parseFloat(order.userCommission || '0'), 0) - .toFixed(2) - - setOrders(uniqueOrders) - setStatistics(stats) - } catch (error) { console.error('获取分销订单失败:', error) Taro.showToast({ @@ -164,18 +66,27 @@ const DealerOrders: React.FC = () => { }) } finally { setLoading(false) + setRefreshing(false) + setLoadingMore(false) } }, [dealerUser?.userId]) - // 刷新数据 + // 下拉刷新 const handleRefresh = async () => { - await fetchOrders() + await fetchOrders(1, true) + } + + // 加载更多 + const handleLoadMore = async () => { + if (!loadingMore && hasMore) { + await fetchOrders(currentPage + 1) + } } // 初始化加载数据 useEffect(() => { if (dealerUser?.userId) { - fetchOrders().then() + fetchOrders(1) } }, [fetchOrders]) @@ -193,198 +104,87 @@ const DealerOrders: React.FC = () => { const renderOrderItem = (order: OrderWithDetails) => ( - - - - 订单号:{order.orderNo} - - - 客户:{order.customerName} - - {/* 显示用户在此订单中的层级 */} - - {order.userLevel === 1 && '一级分销'} - {order.userLevel === 2 && '二级分销'} - {order.userLevel === 3 && '三级分销'} - - + + + 订单号:{order.orderNo} + {getStatusText(order.isSettled, order.isInvalid)} + + + 订单金额:¥{order.orderPrice || '0.00'} + + + 我的佣金:¥{order.userCommission} + + + - - - 订单金额:¥{order.orderPrice || '0.00'} - - - 我的佣金:¥{order.userCommission} - - - 总佣金:¥{order.totalCommission} - - - + + 客户:{order.customerName} + + {order.createTime} ) - // 根据状态和层级过滤订单 - 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 ( - + 加载中... ) } return ( - - {/* 统计卡片 */} - - {/* 总体统计 */} - - - {statistics.totalOrders} - 总订单 - - - ¥{statistics.totalCommission} - 总佣金 - - - ¥{statistics.pendingCommission} - 待结算 - - - - {/* 分层统计 */} - - 分层统计 - - - {statistics.level1.orders} - 一级订单 - ¥{statistics.level1.commission} - - - {statistics.level2.orders} - 二级订单 - ¥{statistics.level2.commission} - - - {statistics.level3.orders} - 三级订单 - ¥{statistics.level3.commission} - - - - - - {/* 订单列表 */} - setActiveTab}> - - - - {loading ? ( - - - 加载中... - - ) : getFilteredOrders('0').length > 0 ? ( - getFilteredOrders('0').map(renderOrderItem) - ) : ( - - )} - - - - - + + + - {getFilteredOrders('1').length > 0 ? ( - getFilteredOrders('1').map(renderOrderItem) + {loading && orders.length === 0 ? ( + + + 加载中... + + ) : orders.length > 0 ? ( + <> + {orders.map(renderOrderItem)} + {loadingMore && ( + + + 加载更多... + + )} + {!hasMore && orders.length > 0 && ( + + 没有更多数据了 + + )} + ) : ( - + )} - - - - - {getFilteredOrders('2').length > 0 ? ( - getFilteredOrders('2').map(renderOrderItem) - ) : ( - - )} - - - - - - {getFilteredOrders('3').length > 0 ? ( - getFilteredOrders('3').map(renderOrderItem) - ) : ( - - )} - - - - - - {getFilteredOrders('4').length > 0 ? ( - getFilteredOrders('4').map(renderOrderItem) - ) : ( - - )} - - - - - - {getFilteredOrders('5').length > 0 ? ( - getFilteredOrders('5').map(renderOrderItem) - ) : ( - - )} - - - - - - {getFilteredOrders('6').length > 0 ? ( - getFilteredOrders('6').map(renderOrderItem) - ) : ( - - )} - - - + + ) } diff --git a/src/dealer/qrcode/index.tsx b/src/dealer/qrcode/index.tsx index c8d376a..90edb58 100644 --- a/src/dealer/qrcode/index.tsx +++ b/src/dealer/qrcode/index.tsx @@ -1,7 +1,7 @@ import React, {useState, useEffect} from 'react' import {View, Text, Image} from '@tarojs/components' import {Button, Loading} from '@nutui/nutui-react-taro' -import {Share, Download, Copy, QrCode} from '@nutui/icons-react-taro' +import {Download, QrCode} from '@nutui/icons-react-taro' import Taro from '@tarojs/taro' import {useDealerUser} from '@/hooks/useDealerUser' import {generateInviteCode} from '@/api/invite' @@ -115,52 +115,52 @@ const DealerQrcode: React.FC = () => { } // 复制邀请信息 - const copyInviteInfo = () => { - if (!dealerUser?.userId) { - Taro.showToast({ - title: '用户信息未加载', - icon: 'error' - }) - return - } - - const inviteText = `🎉 邀请您加入我的团队! - -扫描小程序码或搜索"通源堂健康生态平台"小程序,即可享受优质商品和服务! - -💰 成为我的团队成员,一起赚取丰厚佣金 -🎁 新用户专享优惠等你来拿 - -邀请码:${dealerUser.userId} -快来加入我们吧!` - - Taro.setClipboardData({ - data: inviteText, - success: () => { - Taro.showToast({ - title: '邀请信息已复制', - icon: 'success' - }) - } - }) - } +// const copyInviteInfo = () => { +// if (!dealerUser?.userId) { +// Taro.showToast({ +// title: '用户信息未加载', +// icon: 'error' +// }) +// return +// } +// +// const inviteText = `🎉 邀请您加入我的团队! +// +// 扫描小程序码或搜索"时里院子市集"小程序,即可享受优质商品和服务! +// +// 💰 成为我的团队成员,一起赚取丰厚佣金 +// 🎁 新用户专享优惠等你来拿 +// +// 邀请码:${dealerUser.userId} +// 快来加入我们吧!` +// +// Taro.setClipboardData({ +// data: inviteText, +// success: () => { +// Taro.showToast({ +// title: '邀请信息已复制', +// icon: 'success' +// }) +// } +// }) +// } // 分享小程序码 - const shareMiniProgramCode = () => { - if (!dealerUser?.userId) { - Taro.showToast({ - title: '用户信息未加载', - icon: 'error' - }) - return - } - - // 小程序分享 - Taro.showShareMenu({ - withShareTicket: true, - showShareItems: ['shareAppMessage', 'shareTimeline'] - }) - } + // const shareMiniProgramCode = () => { + // if (!dealerUser?.userId) { + // Taro.showToast({ + // title: '用户信息未加载', + // icon: 'error' + // }) + // return + // } + // + // // 小程序分享 + // Taro.showShareMenu({ + // withShareTicket: true, + // showShareItems: ['shareAppMessage'] + // }) + // } if (!dealerUser) { return ( @@ -263,29 +263,29 @@ const DealerQrcode: React.FC = () => { 保存小程序码到相册 - - - - - - + {/**/} + {/* }*/} + {/* onClick={copyInviteInfo}*/} + {/* disabled={!dealerUser?.userId || loading}*/} + {/* >*/} + {/* 复制邀请信息*/} + {/* */} + {/**/} + {/**/} + {/* }*/} + {/* onClick={shareMiniProgramCode}*/} + {/* disabled={!dealerUser?.userId || loading}*/} + {/* >*/} + {/* 分享给好友*/} + {/* */} + {/**/} {/* 推广说明 */} diff --git a/src/dealer/team/index.config.ts b/src/dealer/team/index.config.ts index 926f186..ddd6b66 100644 --- a/src/dealer/team/index.config.ts +++ b/src/dealer/team/index.config.ts @@ -1,3 +1,3 @@ export default definePageConfig({ - navigationBarTitleText: '我的团队' + navigationBarTitleText: '邀请推广' }) diff --git a/src/dealer/team/index.tsx b/src/dealer/team/index.tsx index 923aa17..af7d8e7 100644 --- a/src/dealer/team/index.tsx +++ b/src/dealer/team/index.tsx @@ -1,56 +1,151 @@ -import React, { useState, useEffect, useCallback } from 'react' -import { View, Text } from '@tarojs/components' -import { Empty, Tabs, Avatar, Tag, Progress, Loading, PullToRefresh } from '@nutui/nutui-react-taro' -import { User, Star, StarFill } from '@nutui/icons-react-taro' +import React, {useState, useEffect, useCallback} from 'react' +import {View, Text} from '@tarojs/components' +import {Phone, Edit, Message} from '@nutui/icons-react-taro' +import {Space, Empty, Avatar, Button} from '@nutui/nutui-react-taro' import Taro from '@tarojs/taro' -import { useDealerUser } from '@/hooks/useDealerUser' -import { listShopDealerReferee } from '@/api/shop/shopDealerReferee' -import { pageShopDealerOrder } from '@/api/shop/shopDealerOrder' -import type { ShopDealerReferee } from '@/api/shop/shopDealerReferee/model' +import {useDealerUser} from '@/hooks/useDealerUser' +import {listShopDealerReferee} from '@/api/shop/shopDealerReferee' +import {pageShopDealerOrder} from '@/api/shop/shopDealerOrder' +import type {ShopDealerReferee} from '@/api/shop/shopDealerReferee/model' +import FixedButton from "@/components/FixedButton"; +import navTo from "@/utils/common"; +import {updateUser} from "@/api/system/user"; interface TeamMemberWithStats extends ShopDealerReferee { name?: string avatar?: string + nickname?: string; + alias?: string; + phone?: string; orderCount?: number commission?: string status?: 'active' | 'inactive' subMembers?: number joinTime?: string + dealerAvatar?: string; + dealerName?: string; + dealerPhone?: string; +} + +// 层级信息接口 +interface LevelInfo { + dealerId: number + dealerName?: string + level: number } const DealerTeam: React.FC = () => { - const [activeTab, setActiveTab] = useState('0') - const [loading, setLoading] = useState(false) - const [refreshing, setRefreshing] = useState(false) const [teamMembers, setTeamMembers] = useState([]) - const [teamStats, setTeamStats] = useState({ - total: 0, - firstLevel: 0, - secondLevel: 0, - thirdLevel: 0, - monthlyCommission: '0.00' - }) + const {dealerUser} = useDealerUser() + const [dealerId, setDealerId] = useState() + // 层级栈,用于支持返回上一层 + const [levelStack, setLevelStack] = useState([]) + const [loading, setLoading] = useState(false) + // 当前查看的用户名称 + const [currentDealerName, setCurrentDealerName] = useState('') - const { dealerUser } = useDealerUser() + // 异步加载成员统计数据 + const loadMemberStats = async (members: TeamMemberWithStats[]) => { + // 分批处理,避免过多并发请求 + const batchSize = 3 + for (let i = 0; i < members.length; i += batchSize) { + const batch = members.slice(i, i + batchSize) + + const batchStats = await Promise.all( + batch.map(async (member) => { + try { + // 并行获取订单统计和下级成员数量 + const [orderResult, subMembersResult] = await Promise.all([ + pageShopDealerOrder({ + page: 1, + userId: member.userId + }), + listShopDealerReferee({ + dealerId: member.userId, + deleted: 0 + }) + ]) + + let orderCount = 0 + let commission = '0.00' + let status: 'active' | 'inactive' = 'inactive' + + if (orderResult?.list) { + const orders = orderResult.list + orderCount = orders.length + commission = orders.reduce((sum, order) => { + const levelCommission = member.level === 1 ? order.firstMoney : + member.level === 2 ? order.secondMoney : + order.thirdMoney + return sum + parseFloat(levelCommission || '0') + }, 0).toFixed(2) + + // 判断活跃状态(30天内有订单为活跃) + const thirtyDaysAgo = new Date() + thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30) + const hasRecentOrder = orders.some(order => + new Date(order.createTime || '') > thirtyDaysAgo + ) + status = hasRecentOrder ? 'active' : 'inactive' + } + + return { + ...member, + orderCount, + commission, + status, + subMembers: subMembersResult?.length || 0 + } + } catch (error) { + console.error(`获取成员${member.userId}数据失败:`, error) + return { + ...member, + orderCount: 0, + commission: '0.00', + status: 'inactive' as const, + subMembers: 0 + } + } + }) + ) + + // 更新这一批成员的数据 + setTeamMembers(prevMembers => { + const updatedMembers = [...prevMembers] + batchStats.forEach(updatedMember => { + const index = updatedMembers.findIndex(m => m.userId === updatedMember.userId) + if (index !== -1) { + updatedMembers[index] = updatedMember + } + }) + return updatedMembers + }) + + // 添加小延迟,避免请求过于密集 + if (i + batchSize < members.length) { + await new Promise(resolve => setTimeout(resolve, 100)) + } + } + } // 获取团队数据 const fetchTeamData = useCallback(async () => { - if (!dealerUser?.userId) return + if (!dealerUser?.userId && !dealerId) return try { setLoading(true) - + console.log(dealerId, 'dealerId>>>>>>>>>') // 获取团队成员关系 const refereeResult = await listShopDealerReferee({ - dealerId: dealerUser.userId + dealerId: dealerId ? dealerId : dealerUser?.userId }) if (refereeResult) { + console.log('团队成员原始数据:', refereeResult) // 处理团队成员数据 const processedMembers: TeamMemberWithStats[] = refereeResult.map(member => ({ ...member, - name: `用户${member.userId}`, - avatar: '', + name: `${member.userId}`, orderCount: 0, commission: '0.00', status: 'active' as const, @@ -58,62 +153,13 @@ const DealerTeam: React.FC = () => { joinTime: member.createTime })) - // 并行获取每个成员的订单统计 - const memberStats = await Promise.all( - processedMembers.map(async (member) => { - try { - const orderResult = await pageShopDealerOrder({ - page: 1, - limit: 100, - userId: member.userId - }) + // 先显示基础数据,然后异步加载详细统计 + setTeamMembers(processedMembers) + setLoading(false) - if (orderResult?.list) { - const orders = orderResult.list - const orderCount = orders.length - const commission = orders.reduce((sum, order) => { - const levelCommission = member.level === 1 ? order.firstMoney : - member.level === 2 ? order.secondMoney : - order.thirdMoney - return sum + parseFloat(levelCommission || '0') - }, 0).toFixed(2) + // 异步加载每个成员的详细统计数据 + loadMemberStats(processedMembers) - // 判断活跃状态(30天内有订单为活跃) - const thirtyDaysAgo = new Date() - thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30) - const hasRecentOrder = orders.some(order => - new Date(order.createTime || '') > thirtyDaysAgo - ) - - return { - ...member, - orderCount, - commission, - status: hasRecentOrder ? 'active' as const : 'inactive' as const - } - } - return member - } catch (error) { - console.error(`获取成员${member.userId}订单失败:`, error) - return member - } - }) - ) - - setTeamMembers(memberStats) - - // 计算统计数据 - const stats = { - total: memberStats.length, - firstLevel: memberStats.filter(m => m.level === 1).length, - secondLevel: memberStats.filter(m => m.level === 2).length, - thirdLevel: memberStats.filter(m => m.level === 3).length, - monthlyCommission: memberStats.reduce((sum, member) => - sum + parseFloat(member.commission || '0'), 0 - ).toFixed(2) - } - - setTeamStats(stats) } } catch (error) { console.error('获取团队数据失败:', error) @@ -124,244 +170,270 @@ const DealerTeam: React.FC = () => { } finally { setLoading(false) } - }, [dealerUser?.userId]) + }, [dealerUser?.userId, dealerId]) - // 刷新数据 - const handleRefresh = async () => { - setRefreshing(true) - await fetchTeamData() - setRefreshing(false) + // 查看下级成员 + const getNextUser = (item: TeamMemberWithStats) => { + // 检查层级限制:最多只能查看2层(levelStack.length >= 1 表示已经是第2层了) + if (levelStack.length >= 1) { + return + } + + // 如果没有下级成员,不允许点击 + if (!item.subMembers || item.subMembers === 0) { + return + } + + console.log('点击用户:', item.userId, item.name) + + // 将当前层级信息推入栈中 + const currentLevel: LevelInfo = { + dealerId: dealerId || dealerUser?.userId || 0, + dealerName: currentDealerName || (dealerId ? '上级' : dealerUser?.realName || '我'), + level: levelStack.length + } + setLevelStack(prev => [...prev, currentLevel]) + + // 切换到下级 + setDealerId(item.userId) + setCurrentDealerName(item.nickname || item.dealerName || `用户${item.userId}`) } - // 初始化加载数据 + // 返回上一层 + const goBack = () => { + if (levelStack.length === 0) { + // 如果栈为空,返回首页或上一页 + Taro.navigateBack() + return + } + + // 从栈中弹出上一层信息 + const prevLevel = levelStack[levelStack.length - 1] + setLevelStack(prev => prev.slice(0, -1)) + + if (prevLevel.dealerId === (dealerUser?.userId || 0)) { + // 返回到根层级 + setDealerId(undefined) + setCurrentDealerName('') + } else { + setDealerId(prevLevel.dealerId) + setCurrentDealerName(prevLevel.dealerName || '') + } + } + + // 一键拨打 + const makePhoneCall = (phone: string) => { + Taro.makePhoneCall({ + phoneNumber: phone, + fail: () => { + Taro.showToast({ + title: '拨打取消', + icon: 'error' + }); + } + }); + }; + + // 别名备注 + const editAlias = (item: any, index: number) => { + Taro.showModal({ + title: '备注', + // @ts-ignore + editable: true, + placeholderText: '真实姓名', + content: item.alias || '', + success: async (res: any) => { + if (res.confirm && res.content !== undefined) { + try { + // 更新跟进情况 + await updateUser({ + userId: item.userId, + alias: res.content.trim() + }); + teamMembers[index].alias = res.content.trim() + setTeamMembers(teamMembers) + } catch (error) { + console.error('备注失败:', error); + Taro.showToast({ + title: '备注失败,请重试', + icon: 'error' + }); + } + } + } + }); + }; + + // 发送消息 + const sendMessage = (item: TeamMemberWithStats) => { + return navTo(`/user/chat/message/add?id=${item.userId}`, true) + } + + // 监听数据变化,获取团队数据 useEffect(() => { - if (dealerUser?.userId) { + if (dealerUser?.userId || dealerId) { fetchTeamData().then() } }, [fetchTeamData]) - const getLevelColor = (level: number) => { - switch (level) { - case 1: return '#f59e0b' - case 2: return '#8b5cf6' - case 3: return '#ec4899' - default: return '#6b7280' + // 初始化当前用户名称 + useEffect(() => { + if (!dealerId && dealerUser?.realName && !currentDealerName) { + setCurrentDealerName(dealerUser.realName) } - } + }, [dealerUser, dealerId, currentDealerName]) - const getLevelIcon = (level: number) => { - switch (level) { - case 1: return - case 2: return - case 3: return - default: return - } - } + const renderMemberItem = (member: TeamMemberWithStats, index: number) => { + // 判断是否可以点击:有下级成员且未达到层级限制 + const canClick = member.subMembers && member.subMembers > 0 && levelStack.length < 1 + // 判断是否显示手机号:只有本级(levelStack.length === 0)才显示 + const showPhone = levelStack.length === 0 + // 判断数据是否还在加载中(初始值都是0或'0.00') + const isStatsLoading = member.orderCount === 0 && member.commission === '0.00' && member.subMembers === 0 - const renderMemberItem = (member: TeamMemberWithStats) => ( - - - } - className="mr-3" - /> - - - - {member.name} - - {getLevelIcon(Number(member.level))} - - {member.level}级 - - - - 加入时间:{member.joinTime} - - - - - {member.status === 'active' ? '活跃' : '沉默'} - - - - - - - - {member.orderCount} - - 订单数 - - - - ¥{member.commission} - - 贡献佣金 - - - - {member.subMembers} - - 团队成员 - - - - ) - - const renderOverview = () => ( - - {/* 团队统计卡片 */} - - {/* 装饰背景 - 小程序兼容版本 */} - - - - - 团队总览 - - - {teamStats.total} - 团队总人数 - - - ¥{teamStats.monthlyCommission} - 本月团队佣金 - - - - - - {/* 层级分布 */} - - 层级分布 - - - - - 一级成员 - - - {teamStats.firstLevel} - - - - - - - - 二级成员 - - - {teamStats.secondLevel} - - - - - - - - 三级成员 - - - {teamStats.thirdLevel} - - - - - - - {/* 最新成员 */} - - 最新成员 - {teamMembers.slice(0, 3).map(renderMemberItem)} - - - ) - - const renderMemberList = (level?: number) => ( - - - {loading ? ( - - - 加载中... - - ) : teamMembers - .filter(member => !level || member.level === level) - .length > 0 ? ( - teamMembers - .filter(member => !level || member.level === level) - .map(renderMemberItem) - ) : ( - - )} - - - ) - - if (!dealerUser) { return ( - - - 加载中... + getNextUser(member)} + > + + + + + + + {member.alias ? {member.alias} : + {member.nickname}} + {/*别名备注*/} + { + e.stopPropagation() + editAlias(member, index) + }}/> + {/*发送消息*/} + { + e.stopPropagation() + sendMessage(member) + }}/> + + + {/* 显示手机号(仅本级可见) */} + {showPhone && member.phone && ( + { + e.stopPropagation(); + makePhoneCall(member.phone || ''); + }}> + {member.phone} + + + )} + + + 加入时间:{member.joinTime} + + + + + + + 订单数 + + {isStatsLoading ? '-' : member.orderCount} + + + + 贡献佣金 + + {isStatsLoading ? '-' : `¥${member.commission}`} + + + + 团队成员 + + {isStatsLoading ? '-' : (member.subMembers || 0)} + + + ) } - return ( - - setActiveTab}> - - {renderOverview()} - - - - {renderMemberList(1)} - - - - {renderMemberList(2)} - - - - {renderMemberList(3)} - - + const renderOverview = () => ( + + + 我的团队成员 + 成员数:{teamMembers.length} + + {teamMembers.map(renderMemberItem)} ) + + // 渲染顶部导航栏 + const renderHeader = () => { + if (levelStack.length === 0) return null + + return ( + + + + + {currentDealerName}的团队成员 + + + + + + ) + } + + if (!dealerUser) { + return ( + + navTo(`/dealer/apply/add`, true)}]} + /> + + ) + } + + return ( + <> + {renderHeader()} + + {loading ? ( + + 加载中... + + ) : teamMembers.length > 0 ? ( + renderOverview() + ) : ( + + + + )} + + navTo(`/dealer/qrcode/index`, true)}/> + + ) } -export default DealerTeam +export default DealerTeam; diff --git a/src/dealer/wechat/index.config.ts b/src/dealer/wechat/index.config.ts new file mode 100644 index 0000000..a3d18db --- /dev/null +++ b/src/dealer/wechat/index.config.ts @@ -0,0 +1,3 @@ +export default definePageConfig({ + navigationBarTitleText: '微信客服' +}) diff --git a/src/dealer/wechat/index.scss b/src/dealer/wechat/index.scss new file mode 100644 index 0000000..ea3fb82 --- /dev/null +++ b/src/dealer/wechat/index.scss @@ -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; + } + } + } + } + } +} diff --git a/src/dealer/wechat/index.tsx b/src/dealer/wechat/index.tsx new file mode 100644 index 0000000..1132e6e --- /dev/null +++ b/src/dealer/wechat/index.tsx @@ -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([]) + + // 长按保存二维码到相册 + 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]) => ( + + + + saveQRCodeToAlbum(`${data.value}`)} + /> + {data.style && 联系电话:{data.style}} + + + + 使用说明: + 1. 长按二维码保存到相册 + 2. 打开微信扫一扫 + 3. 选择相册中的二维码图片 + 4. 添加好友并发送验证消息 + + + + ) + + useEffect(() => { + listCmsWebsiteField({name: 'kefu'}).then(data => { + if (data) { + setCodes(data) + } + }) + }, []); + + return ( + + setActiveTab(`${value}`)} + className="service-tabs" + > + {codes.map((item) => ( + + {renderQRCode(item)} + + ))} + + + ) +} + +export default WechatService diff --git a/src/dealer/withdraw/__tests__/withdraw.test.tsx b/src/dealer/withdraw/__tests__/withdraw.test.tsx new file mode 100644 index 0000000..c3aeab9 --- /dev/null +++ b/src/dealer/withdraw/__tests__/withdraw.test.tsx @@ -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 +const mockAddShopDealerWithdraw = withdrawAPI.addShopDealerWithdraw as jest.MockedFunction +const mockPageShopDealerWithdraw = withdrawAPI.pageShopDealerWithdraw as jest.MockedFunction + +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() + expect(getByText('10000.00')).toBeInTheDocument() + expect(getByText('可提现余额')).toBeInTheDocument() + }) + + test('应该验证最低提现金额', async () => { + mockAddShopDealerWithdraw.mockResolvedValue('success') + + const { getByPlaceholderText, getByText } = render() + + // 输入低于最低金额的数值 + 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() + + // 输入超过可用余额的金额 + 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() + + // 输入有效金额 + 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() + + // 输入有效金额 + 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() + + // 点击快捷金额按钮 + const quickAmountButton = getByText('500') + fireEvent.click(quickAmountButton) + + // 验证金额输入框的值 + const amountInput = getByPlaceholderText('请输入提现金额') as HTMLInputElement + expect(amountInput.value).toBe('500') + }) + + test('全部按钮应该设置为可用余额', () => { + const { getByText, getByPlaceholderText } = render() + + // 点击全部按钮 + const allButton = getByText('全部') + fireEvent.click(allButton) + + // 验证金额输入框的值 + const amountInput = getByPlaceholderText('请输入提现金额') as HTMLInputElement + expect(amountInput.value).toBe('10000.00') + }) +}) diff --git a/src/dealer/withdraw/debug.tsx b/src/dealer/withdraw/debug.tsx new file mode 100644 index 0000000..167d7ba --- /dev/null +++ b/src/dealer/withdraw/debug.tsx @@ -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('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 ( + + + 调试信息 + 当前Tab: {String(activeTab)} + 切换次数: {clickCount} + Tab类型: {typeof activeTab} + + + + 手动切换测试 + + + + + + + + + + + 申请提现页面内容 + + 当前Tab值: {String(activeTab)} + + + + + + + 提现记录页面内容 + + 当前Tab值: {String(activeTab)} + + + + + + + + 事件日志 + + 请查看控制台输出以获取详细的切换日志 + + + + ) +} + +export default WithdrawDebug diff --git a/src/dealer/withdraw/index.tsx b/src/dealer/withdraw/index.tsx index 9ef56f5..04a3cef 100644 --- a/src/dealer/withdraw/index.tsx +++ b/src/dealer/withdraw/index.tsx @@ -1,7 +1,8 @@ -import React, { useState, useRef, useEffect, useCallback } from 'react' -import { View, Text } from '@tarojs/components' +import React, {useState, useRef, useEffect, useCallback} from 'react' +import {View, Text} from '@tarojs/components' import { Cell, + Space, Button, Form, Input, @@ -13,19 +14,19 @@ import { Loading, PullToRefresh } from '@nutui/nutui-react-taro' -import { Wallet } from '@nutui/icons-react-taro' -import { businessGradients } from '@/styles/gradients' +import {Wallet} from '@nutui/icons-react-taro' +import {businessGradients} from '@/styles/gradients' import Taro from '@tarojs/taro' -import { useDealerUser } from '@/hooks/useDealerUser' -import { pageShopDealerWithdraw, addShopDealerWithdraw } from '@/api/shop/shopDealerWithdraw' -import type { ShopDealerWithdraw } from '@/api/shop/shopDealerWithdraw/model' +import {useDealerUser} from '@/hooks/useDealerUser' +import {pageShopDealerWithdraw, addShopDealerWithdraw} from '@/api/shop/shopDealerWithdraw' +import type {ShopDealerWithdraw} from '@/api/shop/shopDealerWithdraw/model' interface WithdrawRecordWithDetails extends ShopDealerWithdraw { accountDisplay?: string } const DealerWithdraw: React.FC = () => { - const [activeTab, setActiveTab] = useState('0') + const [activeTab, setActiveTab] = useState('0') const [selectedAccount, setSelectedAccount] = useState('') const [loading, setLoading] = useState(false) const [refreshing, setRefreshing] = useState(false) @@ -34,16 +35,28 @@ const DealerWithdraw: React.FC = () => { const [withdrawRecords, setWithdrawRecords] = useState([]) const formRef = useRef(null) - const { dealerUser } = useDealerUser() + const {dealerUser} = useDealerUser() + + // Tab 切换处理函数 + const handleTabChange = (value: string | number) => { + console.log('Tab切换到:', value) + setActiveTab(value) + + // 如果切换到提现记录页面,刷新数据 + if (String(value) === '1') { + fetchWithdrawRecords() + } + } // 获取可提现余额 const fetchBalance = useCallback(async () => { + console.log(dealerUser, 'dealerUser...') try { - setAvailableAmount(dealerUser?.money || '0.00') + setAvailableAmount(dealerUser?.money || '0.00') } catch (error) { console.error('获取余额失败:', error) } - }, []) + }, [dealerUser]) // 获取提现记录 const fetchWithdrawRecords = useCallback(async () => { @@ -104,21 +117,31 @@ const DealerWithdraw: React.FC = () => { const getStatusText = (status?: number) => { switch (status) { - case 40: return '已到账' - case 20: return '审核通过' - case 10: return '待审核' - case 30: return '已驳回' - default: return '未知' + case 40: + return '已到账' + case 20: + return '审核通过' + case 10: + return '待审核' + case 30: + return '已驳回' + default: + return '未知' } } const getStatusColor = (status?: number) => { switch (status) { - case 40: return 'success' - case 20: return 'success' - case 10: return 'warning' - case 30: return 'danger' - default: return 'default' + case 40: + return 'success' + case 20: + return 'success' + case 10: + return 'warning' + case 30: + return 'danger' + default: + return 'default' } } @@ -131,9 +154,25 @@ const DealerWithdraw: React.FC = () => { return } + if (!values.accountType) { + Taro.showToast({ + title: '请选择提现方式', + icon: 'error' + }) + return + } + // 验证提现金额 const amount = parseFloat(values.amount) - const available = parseFloat(availableAmount.replace(',', '')) + const available = parseFloat(availableAmount.replace(/,/g, '')) + + if (isNaN(amount) || amount <= 0) { + Taro.showToast({ + title: '请输入有效的提现金额', + icon: 'error' + }) + return + } if (amount < 100) { Taro.showToast({ @@ -151,6 +190,25 @@ const DealerWithdraw: React.FC = () => { return } + // 验证账户信息 + if (values.accountType === 'alipay') { + if (!values.account || !values.accountName) { + Taro.showToast({ + title: '请填写完整的支付宝信息', + icon: 'error' + }) + return + } + } else if (values.accountType === 'bank') { + if (!values.account || !values.accountName || !values.bankName) { + Taro.showToast({ + title: '请填写完整的银行卡信息', + icon: 'error' + }) + return + } + } + try { setSubmitting(true) @@ -158,7 +216,7 @@ const DealerWithdraw: React.FC = () => { userId: dealerUser.userId, money: values.amount, payType: values.accountType === 'wechat' ? 10 : - values.accountType === 'alipay' ? 20 : 30, + values.accountType === 'alipay' ? 20 : 30, applyStatus: 10, // 待审核 platform: 'MiniProgram' } @@ -204,15 +262,21 @@ const DealerWithdraw: React.FC = () => { const quickAmounts = ['100', '300', '500', '1000'] const setQuickAmount = (amount: string) => { - formRef.current?.setFieldsValue({ amount }) + formRef.current?.setFieldsValue({amount}) } const setAllAmount = () => { - formRef.current?.setFieldsValue({ amount: availableAmount.replace(',', '') }) + formRef.current?.setFieldsValue({amount: availableAmount.replace(/,/g, '')}) + } + + // 格式化金额 + const formatMoney = (money?: string) => { + if (!money) return '0.00' + return parseFloat(money).toFixed(2) } const renderWithdrawForm = () => ( - + {/* 余额卡片 */} { }}> - + + {formatMoney(dealerUser?.money)} 可提现余额 - ¥{availableAmount} - + { { + // 实时验证提现金额 + const amount = parseFloat(value) + const available = parseFloat(availableAmount.replace(/,/g, '')) + if (!isNaN(amount) && amount > available) { + // 可以在这里添加实时提示,但不阻止输入 + } + }} /> @@ -301,10 +372,10 @@ const DealerWithdraw: React.FC = () => { {selectedAccount === 'alipay' && ( <> - + - + )} @@ -312,13 +383,13 @@ const DealerWithdraw: React.FC = () => { {selectedAccount === 'bank' && ( <> - + - + - + )} @@ -347,60 +418,64 @@ const DealerWithdraw: React.FC = () => { ) - const renderWithdrawRecords = () => ( - - - {loading ? ( - - - 加载中... - - ) : withdrawRecords.length > 0 ? ( - withdrawRecords.map(record => ( - - - - - 提现金额:¥{record.money} - - - 提现账户:{record.accountDisplay} - - - - {getStatusText(record.applyStatus)} - - + const renderWithdrawRecords = () => { + console.log('渲染提现记录:', {loading, recordsCount: withdrawRecords.length, dealerUserId: dealerUser?.userId}) - - 申请时间:{record.createTime} - {record.auditTime && ( - - 审核时间:{new Date(record.auditTime).toLocaleString()} - - )} - {record.rejectReason && ( - - 驳回原因:{record.rejectReason} - - )} - + return ( + + + {loading ? ( + + + 加载中... - )) - ) : ( - - )} - - - ) + ) : withdrawRecords.length > 0 ? ( + withdrawRecords.map(record => ( + + + + + 提现金额:¥{record.money} + + + 提现账户:{record.accountDisplay} + + + + {getStatusText(record.applyStatus)} + + + + + 申请时间:{record.createTime} + {record.auditTime && ( + + 审核时间:{new Date(record.auditTime).toLocaleString()} + + )} + {record.rejectReason && ( + + 驳回原因:{record.rejectReason} + + )} + + + )) + ) : ( + + )} + + + ) + } if (!dealerUser) { return ( - + 加载中... ) @@ -408,7 +483,7 @@ const DealerWithdraw: React.FC = () => { return ( - setActiveTab}> + {renderWithdrawForm()} diff --git a/src/doctor/apply/add.config.ts b/src/doctor/apply/add.config.ts new file mode 100644 index 0000000..3f7f9ad --- /dev/null +++ b/src/doctor/apply/add.config.ts @@ -0,0 +1,4 @@ +export default definePageConfig({ + navigationBarTitleText: '医生入驻申请通道', + navigationBarTextStyle: 'black' +}) diff --git a/src/doctor/apply/add.tsx b/src/doctor/apply/add.tsx new file mode 100644 index 0000000..08c2f15 --- /dev/null +++ b/src/doctor/apply/add.tsx @@ -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(true) + const [FormData, setFormData] = useState() + const formRef = useRef(null) + const [isEditMode, setIsEditMode] = useState(false) + const [existingApply, setExistingApply] = useState(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 加载中 + } + + return ( + <> +
submitSucceed(values)} + onFinishFailed={(errors) => submitFailed(errors)} + > + + + + + + + + + + + + +
+ {/* 审核状态显示(仅在编辑模式下显示) */} + {isEditMode && ( + + + {getApplyStatusText(FormData?.applyStatus)} + + } + /> + {FormData?.applyStatus === 20 && ( + + )} + {FormData?.applyStatus === 30 && ( + + )} + + )} + + + {/* 底部浮动按钮 */} + {(!isEditMode || FormData?.applyStatus === 10 || FormData?.applyStatus === 30) && ( + } + text={isEditMode ? '保存修改' : '提交申请'} + disabled={FormData?.applyStatus === 10} + onClick={handleFixedButtonClick} + /> + )} + + + ); +}; + +export default AddUserAddress; diff --git a/src/doctor/index.config.ts b/src/doctor/index.config.ts new file mode 100644 index 0000000..bbd5ebc --- /dev/null +++ b/src/doctor/index.config.ts @@ -0,0 +1,3 @@ +export default definePageConfig({ + navigationBarTitleText: '医生版' +}) diff --git a/src/doctor/index.scss b/src/doctor/index.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/doctor/index.tsx b/src/doctor/index.tsx new file mode 100644 index 0000000..da6ce5d --- /dev/null +++ b/src/doctor/index.tsx @@ -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 ( + + + {error} + + + + ) + } + + return ( + + + {/*头部信息*/} + {dealerUser && ( + + {/* 装饰性背景元素 - 小程序兼容版本 */} + + + + + } + className="mr-4" + style={{ + border: '2px solid rgba(255, 255, 255, 0.3)' + }} + /> + + + {dealerUser?.realName || '分销商'} + + + ID: {dealerUser.userId} | 推荐人: {dealerUser.refereeId || '无'} + + + + 加入时间 + + {formatTime(dealerUser.createTime)} + + + + + )} + + {/* 佣金统计卡片 */} + {dealerUser && ( + + + 佣金统计 + + + + + ¥{formatMoney(dealerUser.money)} + + 可提现 + + + + ¥{formatMoney(dealerUser.freezeMoney)} + + 冻结中 + + + + ¥{formatMoney(dealerUser.totalMoney)} + + 累计收益 + + + + )} + + {/* 团队统计 */} + {dealerUser && ( + + + 我的邀请 + navigateToPage('/dealer/team/index')} + > + 查看详情 + + + + + + + {dealerUser.firstNum || 0} + + 一级成员 + + + + {dealerUser.secondNum || 0} + + 二级成员 + + + + {dealerUser.thirdNum || 0} + + 三级成员 + + + + )} + + {/* 功能导航 */} + + 分销工具 + + + navigateToPage('/dealer/orders/index')}> + + + + + + + + navigateToPage('/dealer/withdraw/index')}> + + + + + + + + navigateToPage('/dealer/team/index')}> + + + + + + + + navigateToPage('/dealer/qrcode/index')}> + + + + + + + + + {/* 第二行功能 */} + {/**/} + {/* navigateToPage('/dealer/invite-stats/index')}>*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + + {/* /!* 预留其他功能位置 *!/*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/**/} + + + + + {/* 底部安全区域 */} + + + ) +} + +export default DealerIndex diff --git a/src/doctor/info.tsx b/src/doctor/info.tsx new file mode 100644 index 0000000..0f18841 --- /dev/null +++ b/src/doctor/info.tsx @@ -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 ( + + + {error} + + + + ) + } + + return ( + + {/* 页面标题 */} + + + 经销商信息 + + + + {!dealerUser ? ( + // 非经销商状态 + + + 您还不是经销商 + + 成为经销商后可享受专属价格和佣金收益 + + + + + ) : ( + // 经销商信息展示 + + {/* 状态卡片 */} + + + 经销商状态 + + {dealerUser.realName} + + + + {/* 基本信息 */} + + + + + + + + {/* 操作按钮 */} + + + + + + {/* 经销商权益 */} + + 经销商权益 + + + • 享受经销商专属价格 + + + • 获得推广佣金收益 + + + • 优先获得新品信息 + + + • 专属客服支持 + + + + + {/* 佣金统计 */} + + 佣金统计 + + + 0 + 今日佣金 + + + 0 + 本月佣金 + + + 0 + 累计佣金 + + + + + )} + + {/* 刷新按钮 */} + + + 点击刷新数据 + + + + ) +} + +export default DealerInfo diff --git a/src/doctor/invite-stats/index.config.ts b/src/doctor/invite-stats/index.config.ts new file mode 100644 index 0000000..246a9aa --- /dev/null +++ b/src/doctor/invite-stats/index.config.ts @@ -0,0 +1,7 @@ +export default definePageConfig({ + navigationBarTitleText: '邀请统计', + navigationBarBackgroundColor: '#ffffff', + navigationBarTextStyle: 'black', + backgroundColor: '#f5f5f5', + enablePullDownRefresh: true +}) diff --git a/src/doctor/invite-stats/index.tsx b/src/doctor/invite-stats/index.tsx new file mode 100644 index 0000000..3139ad4 --- /dev/null +++ b/src/doctor/invite-stats/index.tsx @@ -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('stats') + const [loading, setLoading] = useState(false) + const [inviteStats, setInviteStats] = useState(null) + const [inviteRecords, setInviteRecords] = useState([]) + const [ranking, setRanking] = useState([]) + const [dateRange, setDateRange] = useState('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 = { + 'pending': '待注册', + 'registered': '已注册', + 'activated': '已激活' + } + return statusMap[status] || status + } + + // 获取状态颜色 + const getStatusColor = (status: string) => { + const colorMap: Record = { + 'pending': 'text-orange-500', + 'registered': 'text-blue-500', + 'activated': 'text-green-500' + } + return colorMap[status] || 'text-gray-500' + } + + // 渲染统计概览 + const renderStatsOverview = () => ( + + {/* 核心数据卡片 */} + + + 邀请概览 + {loading ? ( + + + + ) : inviteStats ? ( + + + + + {inviteStats.totalInvites || 0} + + 总邀请数 + + + + + + {inviteStats.successfulRegistrations || 0} + + 成功注册 + + + + + + {inviteStats.conversionRate ? `${(inviteStats.conversionRate * 100).toFixed(1)}%` : '0%'} + + 转化率 + + + + + + {inviteStats.todayInvites || 0} + + 今日邀请 + + + ) : ( + + 暂无统计数据 + + )} + + + + {/* 邀请来源分析 */} + {inviteStats?.sourceStats && inviteStats.sourceStats.length > 0 && ( + + + 邀请来源分析 + + {inviteStats.sourceStats.map((source, index) => ( + + + + {source.source} + + + {source.count} + + 转化率 {source.conversionRate ? `${(source.conversionRate * 100).toFixed(1)}%` : '0%'} + + + + ))} + + + + )} + + ) + + // 渲染邀请记录 + const renderInviteRecords = () => ( + + {inviteRecords.length > 0 ? ( + + {inviteRecords.map((record, index) => ( + + + + + {record.inviteeName || `用户${record.inviteeId}`} + + + {getStatusText(record.status || 'pending')} + + + + + 来源: {record.source || '未知'} + {record.inviteTime ? new Date(record.inviteTime).toLocaleDateString() : ''} + + + {record.registerTime && ( + + 注册时间: {new Date(record.registerTime).toLocaleString()} + + )} + + + ))} + + ) : ( + + )} + + ) + + // 渲染排行榜 + const renderRanking = () => ( + + + setDateRange}> + + + + + + + {ranking.length > 0 ? ( + + {ranking.map((item, index) => ( + + + + {index < 3 ? ( + + ) : ( + {index + 1} + )} + + + + {item.inviterName} + + 邀请 {item.inviteCount} 人 · 转化率 {item.conversionRate ? `${(item.conversionRate * 100).toFixed(1)}%` : '0%'} + + + + {item.successCount} + + + ))} + + ) : ( + + )} + + ) + + if (!dealerUser) { + return ( + + + 加载中... + + ) + } + + return ( + + {/* 头部 */} + + + + + 邀请统计 + + 查看您的邀请效果和推广数据 + + + + + {/* 标签页 */} + + setActiveTab}> + + + + + + + {/* 内容区域 */} + + + {activeTab === 'stats' && renderStatsOverview()} + {activeTab === 'records' && renderInviteRecords()} + {activeTab === 'ranking' && renderRanking()} + + + + ) +} + +export default InviteStatsPage diff --git a/src/doctor/orders/index.config.ts b/src/doctor/orders/index.config.ts new file mode 100644 index 0000000..3bb5694 --- /dev/null +++ b/src/doctor/orders/index.config.ts @@ -0,0 +1,3 @@ +export default definePageConfig({ + navigationBarTitleText: '分销订单' +}) diff --git a/src/doctor/orders/index.tsx b/src/doctor/orders/index.tsx new file mode 100644 index 0000000..1cc459b --- /dev/null +++ b/src/doctor/orders/index.tsx @@ -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('0') + const [loading, setLoading] = useState(false) + const [orders, setOrders] = useState([]) + 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) => ( + + + + + 订单号:{order.orderNo} + + + 客户:{order.customerName} + + {/* 显示用户在此订单中的层级 */} + + {order.userLevel === 1 && '一级分销'} + {order.userLevel === 2 && '二级分销'} + {order.userLevel === 3 && '三级分销'} + + + + {getStatusText(order.isSettled, order.isInvalid)} + + + + + + + 订单金额:¥{order.orderPrice || '0.00'} + + + 我的佣金:¥{order.userCommission} + + + 总佣金:¥{order.totalCommission} + + + + {order.createTime} + + + + ) + + // 根据状态和层级过滤订单 + 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 ( + + + 加载中... + + ) + } + + return ( + + {/* 统计卡片 */} + + {/* 总体统计 */} + + + {statistics.totalOrders} + 总订单 + + + ¥{statistics.totalCommission} + 总佣金 + + + ¥{statistics.pendingCommission} + 待结算 + + + + {/* 分层统计 */} + + 分层统计 + + + {statistics.level1.orders} + 一级订单 + ¥{statistics.level1.commission} + + + {statistics.level2.orders} + 二级订单 + ¥{statistics.level2.commission} + + + {statistics.level3.orders} + 三级订单 + ¥{statistics.level3.commission} + + + + + + {/* 订单列表 */} + setActiveTab}> + + + + {loading ? ( + + + 加载中... + + ) : getFilteredOrders('0').length > 0 ? ( + getFilteredOrders('0').map(renderOrderItem) + ) : ( + + )} + + + + + + + {getFilteredOrders('1').length > 0 ? ( + getFilteredOrders('1').map(renderOrderItem) + ) : ( + + )} + + + + + + {getFilteredOrders('2').length > 0 ? ( + getFilteredOrders('2').map(renderOrderItem) + ) : ( + + )} + + + + + + {getFilteredOrders('3').length > 0 ? ( + getFilteredOrders('3').map(renderOrderItem) + ) : ( + + )} + + + + + + {getFilteredOrders('4').length > 0 ? ( + getFilteredOrders('4').map(renderOrderItem) + ) : ( + + )} + + + + + + {getFilteredOrders('5').length > 0 ? ( + getFilteredOrders('5').map(renderOrderItem) + ) : ( + + )} + + + + + + {getFilteredOrders('6').length > 0 ? ( + getFilteredOrders('6').map(renderOrderItem) + ) : ( + + )} + + + + + ) +} + +export default DealerOrders diff --git a/src/doctor/qrcode/index.config.ts b/src/doctor/qrcode/index.config.ts new file mode 100644 index 0000000..b075b21 --- /dev/null +++ b/src/doctor/qrcode/index.config.ts @@ -0,0 +1,3 @@ +export default definePageConfig({ + navigationBarTitleText: '推广二维码' +}) diff --git a/src/doctor/qrcode/index.tsx b/src/doctor/qrcode/index.tsx new file mode 100644 index 0000000..c8d376a --- /dev/null +++ b/src/doctor/qrcode/index.tsx @@ -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('') + const [loading, setLoading] = useState(false) + // const [inviteStats, setInviteStats] = useState(null) + // const [statsLoading, setStatsLoading] = useState(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 ( + + + 加载中... + + ) + } + + return ( + + {/* 头部卡片 */} + + {/* 装饰背景 */} + + + + 我的邀请小程序码 + + 分享小程序码邀请好友,获得丰厚佣金奖励 + + + + + + {/* 小程序码展示区 */} + + + {loading ? ( + + + 生成中... + + ) : miniProgramCodeUrl ? ( + + { + Taro.showModal({ + title: '二维码加载失败', + content: '请检查网络连接或联系管理员', + showCancel: true, + confirmText: '重新生成', + success: (res) => { + if (res.confirm) { + generateMiniProgramCode(); + } + } + }); + }} + + /> + + ) : ( + + + 小程序码生成失败 + + + )} + + + 扫码加入我的团队 + + + 好友扫描小程序码即可直接进入小程序并建立邀请关系 + + + + + + + {/* 操作按钮 */} + + + + + + + + + + + + + {/* 推广说明 */} + + 推广说明 + + + + + 好友通过您的二维码或链接注册成为您的团队成员 + + + + + + 好友购买商品时,您可获得相应层级的分销佣金 + + + + + + 支持三级分销,团队越大收益越多 + + + + + + {/* 邀请统计数据 */} + {/**/} + {/* 我的邀请数据*/} + {/* {statsLoading ? (*/} + {/* */} + {/* */} + {/* 加载中...*/} + {/* */} + {/* ) : inviteStats ? (*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* {inviteStats.totalInvites || 0}*/} + {/* */} + {/* 总邀请数*/} + {/* */} + {/* */} + {/* */} + {/* {inviteStats.successfulRegistrations || 0}*/} + {/* */} + {/* 成功注册*/} + {/* */} + {/* */} + + {/* */} + {/* */} + {/* */} + {/* {inviteStats.conversionRate ? `${(inviteStats.conversionRate * 100).toFixed(1)}%` : '0%'}*/} + {/* */} + {/* 转化率*/} + {/* */} + {/* */} + {/* */} + {/* {inviteStats.todayInvites || 0}*/} + {/* */} + {/* 今日邀请*/} + {/* */} + {/* */} + + {/* /!* 邀请来源统计 *!/*/} + {/* {inviteStats.sourceStats && inviteStats.sourceStats.length > 0 && (*/} + {/* */} + {/* 邀请来源分布*/} + {/* */} + {/* {inviteStats.sourceStats.map((source, index) => (*/} + {/* */} + {/* */} + {/* */} + {/* {source.source}*/} + {/* */} + {/* */} + {/* {source.count}*/} + {/* */} + {/* {source.conversionRate ? `${(source.conversionRate * 100).toFixed(1)}%` : '0%'}*/} + {/* */} + {/* */} + {/* */} + {/* ))}*/} + {/* */} + {/* */} + {/* )}*/} + {/* */} + {/* ) : (*/} + {/* */} + {/* 暂无邀请数据*/} + {/* */} + {/* 刷新数据*/} + {/* */} + {/* */} + {/* )}*/} + {/**/} + + + ) +} + +export default DealerQrcode diff --git a/src/doctor/team/index.config.ts b/src/doctor/team/index.config.ts new file mode 100644 index 0000000..926f186 --- /dev/null +++ b/src/doctor/team/index.config.ts @@ -0,0 +1,3 @@ +export default definePageConfig({ + navigationBarTitleText: '我的团队' +}) diff --git a/src/doctor/team/index.tsx b/src/doctor/team/index.tsx new file mode 100644 index 0000000..923aa17 --- /dev/null +++ b/src/doctor/team/index.tsx @@ -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(false) + const [refreshing, setRefreshing] = useState(false) + const [teamMembers, setTeamMembers] = useState([]) + 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 + case 2: return + case 3: return + default: return + } + } + + const renderMemberItem = (member: TeamMemberWithStats) => ( + + + } + className="mr-3" + /> + + + + {member.name} + + {getLevelIcon(Number(member.level))} + + {member.level}级 + + + + 加入时间:{member.joinTime} + + + + + {member.status === 'active' ? '活跃' : '沉默'} + + + + + + + + {member.orderCount} + + 订单数 + + + + ¥{member.commission} + + 贡献佣金 + + + + {member.subMembers} + + 团队成员 + + + + ) + + const renderOverview = () => ( + + {/* 团队统计卡片 */} + + {/* 装饰背景 - 小程序兼容版本 */} + + + + + 团队总览 + + + {teamStats.total} + 团队总人数 + + + ¥{teamStats.monthlyCommission} + 本月团队佣金 + + + + + + {/* 层级分布 */} + + 层级分布 + + + + + 一级成员 + + + {teamStats.firstLevel} + + + + + + + + 二级成员 + + + {teamStats.secondLevel} + + + + + + + + 三级成员 + + + {teamStats.thirdLevel} + + + + + + + {/* 最新成员 */} + + 最新成员 + {teamMembers.slice(0, 3).map(renderMemberItem)} + + + ) + + const renderMemberList = (level?: number) => ( + + + {loading ? ( + + + 加载中... + + ) : teamMembers + .filter(member => !level || member.level === level) + .length > 0 ? ( + teamMembers + .filter(member => !level || member.level === level) + .map(renderMemberItem) + ) : ( + + )} + + + ) + + if (!dealerUser) { + return ( + + + 加载中... + + ) + } + + return ( + + setActiveTab}> + + {renderOverview()} + + + + {renderMemberList(1)} + + + + {renderMemberList(2)} + + + + {renderMemberList(3)} + + + + ) +} + +export default DealerTeam diff --git a/src/doctor/withdraw/index.config.ts b/src/doctor/withdraw/index.config.ts new file mode 100644 index 0000000..00a9f9b --- /dev/null +++ b/src/doctor/withdraw/index.config.ts @@ -0,0 +1,3 @@ +export default definePageConfig({ + navigationBarTitleText: '提现申请' +}) diff --git a/src/doctor/withdraw/index.tsx b/src/doctor/withdraw/index.tsx new file mode 100644 index 0000000..9ef56f5 --- /dev/null +++ b/src/doctor/withdraw/index.tsx @@ -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(false) + const [refreshing, setRefreshing] = useState(false) + const [submitting, setSubmitting] = useState(false) + const [availableAmount, setAvailableAmount] = useState('0.00') + const [withdrawRecords, setWithdrawRecords] = useState([]) + const formRef = useRef(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 = () => ( + + {/* 余额卡片 */} + + {/* 装饰背景 - 小程序兼容版本 */} + + + + + 可提现余额 + ¥{availableAmount} + + + + + + + + 最低提现金额:¥100 | 手续费:免费 + + + + +
+ + + + + + {/* 快捷金额 */} + + 快捷金额 + + {quickAmounts.map(amount => ( + + ))} + + + + + + setSelectedAccount}> + + + 微信钱包 + + + 支付宝 + + + 银行卡 + + + + + + {selectedAccount === 'alipay' && ( + <> + + + + + + + + )} + + {selectedAccount === 'bank' && ( + <> + + + + + + + + + + + )} + + {selectedAccount === 'wechat' && ( + + + 微信钱包提现将直接转入您的微信零钱 + + + )} + + + + + +
+
+ ) + + const renderWithdrawRecords = () => ( + + + {loading ? ( + + + 加载中... + + ) : withdrawRecords.length > 0 ? ( + withdrawRecords.map(record => ( + + + + + 提现金额:¥{record.money} + + + 提现账户:{record.accountDisplay} + + + + {getStatusText(record.applyStatus)} + + + + + 申请时间:{record.createTime} + {record.auditTime && ( + + 审核时间:{new Date(record.auditTime).toLocaleString()} + + )} + {record.rejectReason && ( + + 驳回原因:{record.rejectReason} + + )} + + + )) + ) : ( + + )} + + + ) + + if (!dealerUser) { + return ( + + + 加载中... + + ) + } + + return ( + + setActiveTab}> + + {renderWithdrawForm()} + + + + {renderWithdrawRecords()} + + + + ) +} + +export default DealerWithdraw diff --git a/src/hooks/useAdminMode.ts b/src/hooks/useAdminMode.ts new file mode 100644 index 0000000..f20b96b --- /dev/null +++ b/src/hooks/useAdminMode.ts @@ -0,0 +1,66 @@ +import { useState, useCallback, useEffect } from 'react'; +import Taro from '@tarojs/taro'; + +/** + * 管理员模式Hook + * 用于管理管理员用户的模式切换(普通用户模式 vs 管理员模式) + */ +export function useAdminMode() { + const [isAdminMode, setIsAdminMode] = useState(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 + }; +} diff --git a/src/hooks/useOrderStats.ts b/src/hooks/useOrderStats.ts index 14b4c05..a26c29e 100644 --- a/src/hooks/useOrderStats.ts +++ b/src/hooks/useOrderStats.ts @@ -28,6 +28,9 @@ export const useOrderStats = () => { setLoading(true); setError(null); + if(!Taro.getStorageSync('UserId')){ + return false; + } // TODO 读取订单数量 const pending = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 0}) const paid = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 1}) diff --git a/src/hooks/useQRLogin.ts b/src/hooks/useQRLogin.ts new file mode 100644 index 0000000..717a968 --- /dev/null +++ b/src/hooks/useQRLogin.ts @@ -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.IDLE); + const [error, setError] = useState(''); + const [result, setResult] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + // 用于取消操作的引用 + const cancelRef = useRef(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((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 + }; +} diff --git a/src/hooks/useUnifiedQRScan.ts b/src/hooks/useUnifiedQRScan.ts new file mode 100644 index 0000000..5468046 --- /dev/null +++ b/src/hooks/useUnifiedQRScan.ts @@ -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.IDLE); + const [error, setError] = useState(''); + const [result, setResult] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [scanType, setScanType] = useState(ScanType.UNKNOWN); + + // 用于取消操作的引用 + const cancelRef = useRef(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 => { + 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 => { + 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 => { + try { + reset(); + setState(UnifiedScanState.SCANNING); + + // 调用扫码API + const scanResult = await new Promise((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 + }; +} diff --git a/src/hooks/useUser.ts b/src/hooks/useUser.ts index 4bd8304..479a0f3 100644 --- a/src/hooks/useUser.ts +++ b/src/hooks/useUser.ts @@ -1,7 +1,9 @@ import { useState, useEffect } from 'react'; import Taro from '@tarojs/taro'; import { User } from '@/api/system/user/model'; -import { getUserInfo, updateUserInfo } from '@/api/layout'; +import { getUserInfo, updateUserInfo, loginByOpenId } from '@/api/layout'; +import { TenantId } from '@/config/app'; +import {getStoredInviteParams, handleInviteRelation} from '@/utils/invite'; // 用户Hook export const useUser = () => { @@ -9,8 +11,62 @@ export const useUser = () => { const [isLoggedIn, setIsLoggedIn] = useState(false); const [loading, setLoading] = useState(true); + // 自动登录(通过OpenID) + const autoLoginByOpenId = async () => { + try { + const res = await new Promise((resolve, reject) => { + Taro.login({ + success: (loginRes) => { + loginByOpenId({ + code: loginRes.code, + tenantId: TenantId + }).then(async (data) => { + if (data) { + // 保存登录信息 + saveUserToStorage(data.access_token, data.user); + setUser(data.user); + setIsLoggedIn(true); + + // 处理邀请关系 + if (data.user?.userId) { + try { + const inviteSuccess = await handleInviteRelation(data.user.userId); + if (inviteSuccess) { + console.log('自动登录时邀请关系建立成功'); + } + } catch (error) { + console.error('自动登录时处理邀请关系失败:', error); + } + } + + resolve(data.user); + } else { + reject(new Error('自动登录失败')); + } + }).catch(_ => { + // 首次注册,跳转到邀请注册页面 + const pages = Taro.getCurrentPages(); + const currentPage = pages[pages.length - 1]; + const inviteParams = getStoredInviteParams() + if (currentPage?.route !== 'dealer/apply/add' && inviteParams?.inviter) { + return Taro.navigateTo({ + url: '/dealer/apply/add' + }); + } + }); + }, + fail: reject + }); + }); + return res; + } catch (error) { + console.error('自动登录失败:', error); + return null; + } + }; + // 从本地存储加载用户数据 - const loadUserFromStorage = () => { + const loadUserFromStorage = async () => { try { const token = Taro.getStorageSync('access_token'); const userData = Taro.getStorageSync('User'); @@ -26,8 +82,13 @@ export const useUser = () => { setIsLoggedIn(true); setUser({ userId, tenantId } as User); } else { - setUser(null); - setIsLoggedIn(false); + // 没有本地登录信息,尝试自动登录 + console.log('没有本地登录信息,尝试自动登录...'); + const autoLoginResult = await autoLoginByOpenId(); + if (!autoLoginResult) { + setUser(null); + setIsLoggedIn(false); + } } } catch (error) { console.error('加载用户数据失败:', error); @@ -43,9 +104,24 @@ export const useUser = () => { try { Taro.setStorageSync('access_token', token); Taro.setStorageSync('User', userInfo); - Taro.setStorageSync('UserId', userInfo.userId); - Taro.setStorageSync('TenantId', userInfo.tenantId); - Taro.setStorageSync('Phone', userInfo.phone); + + // 确保关键字段不为空时才保存,避免覆盖现有数据 + if (userInfo.userId) { + Taro.setStorageSync('UserId', userInfo.userId); + } + if (userInfo.tenantId) { + Taro.setStorageSync('TenantId', userInfo.tenantId); + } + if (userInfo.phone) { + Taro.setStorageSync('Phone', userInfo.phone); + } + // 保存头像和昵称信息 + if (userInfo.avatar) { + Taro.setStorageSync('Avatar', userInfo.avatar); + } + if (userInfo.nickname) { + Taro.setStorageSync('Nickname', userInfo.nickname); + } } catch (error) { console.error('保存用户数据失败:', error); } @@ -114,9 +190,16 @@ export const useUser = () => { } try { - const updatedUser = { ...user, ...userData }; + // 先获取最新的用户信息,确保我们有完整的数据 + const latestUserInfo = await getUserInfo(); + + // 合并最新的用户信息和要更新的数据 + const updatedUser = { ...latestUserInfo, ...userData }; + + // 调用API更新用户信息 await updateUserInfo(updatedUser); + // 更新本地状态 setUser(updatedUser); // 更新本地存储 @@ -216,7 +299,10 @@ export const useUser = () => { // 初始化时加载用户数据 useEffect(() => { - loadUserFromStorage(); + loadUserFromStorage().catch(error => { + console.error('初始化用户数据失败:', error); + setLoading(false); + }); }, []); return { @@ -231,6 +317,7 @@ export const useUser = () => { fetchUserInfo, updateUser, loadUserFromStorage, + autoLoginByOpenId, // 工具方法 hasPermission, diff --git a/src/hooks/useUserData.ts b/src/hooks/useUserData.ts index 33a1bef..fcc0423 100644 --- a/src/hooks/useUserData.ts +++ b/src/hooks/useUserData.ts @@ -39,6 +39,10 @@ export const useUserData = (): UseUserDataReturn => { setLoading(true) setError(null) + if(!Taro.getStorageSync('UserId')){ + return; + } + // 并发请求所有数据 const [userDataRes, couponsRes, giftCardsRes] = await Promise.all([ getUserInfo(), diff --git a/src/pages/cart/cart.tsx b/src/pages/cart/cart.tsx index 7a48947..e415a03 100644 --- a/src/pages/cart/cart.tsx +++ b/src/pages/cart/cart.tsx @@ -1,5 +1,5 @@ import {useEffect, useState} from "react"; -import Taro, {useShareAppMessage, useShareTimeline, useDidShow} from '@tarojs/taro'; +import Taro, {useShareAppMessage, useDidShow} from '@tarojs/taro'; import { NavBar, Checkbox, @@ -39,15 +39,9 @@ function Cart() { nutuiInputnumberButtonBorderRadius: '4px', } - useShareTimeline(() => { - return { - title: '购物车 - 网宿小店' - }; - }); - useShareAppMessage(() => { return { - title: '购物车 - 网宿小店', + title: '购物车 - 时里院子市集', success: function () { console.log('分享成功'); }, diff --git a/src/pages/cms/category/components/ArticleList.tsx b/src/pages/cms/category/components/ArticleList.tsx new file mode 100644 index 0000000..3f46857 --- /dev/null +++ b/src/pages/cms/category/components/ArticleList.tsx @@ -0,0 +1,25 @@ +import {Image, Cell} from '@nutui/nutui-react-taro' +import Taro from '@tarojs/taro' + +const ArticleList = (props: any) => { + + return ( + <> +
+ {props.data.map((item, index) => { + return ( + + } + key={index} + onClick={() => Taro.navigateTo({url: '/cms/detail/index?id=' + item.articleId})} + /> + ) + })} +
+ + ) +} +export default ArticleList diff --git a/src/pages/cms/category/components/ArticleTabs.tsx b/src/pages/cms/category/components/ArticleTabs.tsx new file mode 100644 index 0000000..a995345 --- /dev/null +++ b/src/pages/cms/category/components/ArticleTabs.tsx @@ -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(true) + const [tab1value, setTab1value] = useState('0') + const [list, setList] = useState([]) + + 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 ( + 加载中 + ) + } + + return ( + <> + { + reload(value).then() + }} + > + {props.data?.map((item, index) => { + return ( + + ) + })} + + + + ) +} +export default ArticleTabs diff --git a/src/pages/cms/category/components/Banner.tsx b/src/pages/cms/category/components/Banner.tsx new file mode 100644 index 0000000..7f3942d --- /dev/null +++ b/src/pages/cms/category/components/Banner.tsx @@ -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() + const reload = () => { + getCmsAd(439).then(data => { + setItem(data) + }) + } + + useEffect(() => { + reload() + }, []) + + return ( + <> + + {item?.imageList?.map((item) => ( + + + + ))} + + + ) +} +export default MyPage diff --git a/src/pages/cms/category/index.config.ts b/src/pages/cms/category/index.config.ts new file mode 100644 index 0000000..689ba07 --- /dev/null +++ b/src/pages/cms/category/index.config.ts @@ -0,0 +1,4 @@ +export default definePageConfig({ + navigationBarTitleText: '文章列表', + navigationBarTextStyle: 'black' +}) diff --git a/src/pages/cms/category/index.scss b/src/pages/cms/category/index.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/cms/category/index.tsx b/src/pages/cms/category/index.tsx new file mode 100644 index 0000000..6886cc3 --- /dev/null +++ b/src/pages/cms/category/index.tsx @@ -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(0) + const [category, setCategory] = useState([]) + const [loading, setLoading] = useState(true) + const [nav, setNav] = useState() + const [list, setList] = useState([]) + + 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 ( + 加载中 + ) + } + + if(category.length > 0){ + return + } + + return +} + +export default Category diff --git a/src/pages/cms/detail/index.config.ts b/src/pages/cms/detail/index.config.ts new file mode 100644 index 0000000..d74c9f2 --- /dev/null +++ b/src/pages/cms/detail/index.config.ts @@ -0,0 +1,3 @@ +export default definePageConfig({ + navigationBarTitleText: '文章详情' +}) diff --git a/src/pages/cms/detail/index.scss b/src/pages/cms/detail/index.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/cms/detail/index.tsx b/src/pages/cms/detail/index.tsx new file mode 100644 index 0000000..f92c890 --- /dev/null +++ b/src/pages/cms/detail/index.tsx @@ -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(true) + // 文章详情 + const [item, setItem] = useState() + 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 ( + 加载中 + ) + } + + return ( +
+
{item?.title}
+
{item?.createTime}
+ + + + +
+ ) +} + +export default Detail diff --git a/src/pages/index/Banner.tsx b/src/pages/index/Banner.tsx index 7f3942d..9a79828 100644 --- a/src/pages/index/Banner.tsx +++ b/src/pages/index/Banner.tsx @@ -1,31 +1,123 @@ -import { useEffect, useState } from 'react' -import { Swiper } from '@nutui/nutui-react-taro' +import {useEffect, useState} from 'react' +import {View} from '@tarojs/components' +import {Swiper} from '@nutui/nutui-react-taro' import {CmsAd} from "@/api/cms/cmsAd/model"; import {Image} from '@nutui/nutui-react-taro' -import {getCmsAd} from "@/api/cms/cmsAd"; +import {getCmsAdByCode} from "@/api/cms/cmsAd"; +import navTo from "@/utils/common"; +import {ShopGoods} from "@/api/shop/shopGoods/model"; +import {listShopGoods} from "@/api/shop/shopGoods"; + const MyPage = () => { - const [item, setItem] = useState() - const reload = () => { - getCmsAd(439).then(data => { - setItem(data) + const [carouselData, setCarouselData] = useState() + // const [hotToday, setHotToday] = useState() + // const [groupBuy, setGroupBuy] = useState() + const [hotGoods, setHotGoods] = useState([]) + + // 加载数据 + const loadData = () => { + // 轮播图 + getCmsAdByCode('flash').then(data => { + setCarouselData(data) + }) + // 今日热卖素材(上层图片) + // getCmsAd(444).then(data => { + // setHotToday(data) + // }) + // 社区拼团素材(下层图片) + // getCmsAd(445).then(data => { + // setGroupBuy(data) + // }) + // 今日热卖 + listShopGoods({categoryId: 4424, limit: 2}).then(data => { + setHotGoods(data) }) } useEffect(() => { - reload() + loadData() }, []) + // 轮播图高度,默认200px + const carouselHeight = carouselData?.height || 200; + return ( - <> - - {item?.imageList?.map((item) => ( - - - - ))} - - + + {/* 左侧轮播图区域 */} + + + {carouselData?.imageList?.map((img, index) => ( + + navTo(`${img.path}`)} + lazyLoad={false} + style={{height: `${carouselHeight}px`, borderRadius: '4px'}} + /> + + ))} + + + + {/* 右侧上下图片区域 - 从API获取数据 */} + + {/* 上层图片 - 使用今日热卖素材 */} + + 今日热卖 + + { + hotGoods.map(item => ( + + navTo('/shop/category/index?id=4424')} + /> + 到手价¥{item.price} + + )) + } + + + + {/* 下层图片 - 使用社区拼团素材 */} + + 走进社区 + + navTo('cms/detail/index?id=10109')} + /> + + + + ) } + export default MyPage + diff --git a/src/pages/index/BestSellers.tsx b/src/pages/index/BestSellers.tsx index 8e14c48..7c2c898 100644 --- a/src/pages/index/BestSellers.tsx +++ b/src/pages/index/BestSellers.tsx @@ -1,21 +1,56 @@ import {useEffect, useState} from "react"; -import {Image} from '@nutui/nutui-react-taro' +import {Image, Swiper, SwiperItem, Empty} from '@nutui/nutui-react-taro' import {Share} from '@nutui/icons-react-taro' import {View, Text} from '@tarojs/components'; -import Taro, {useShareAppMessage, useShareTimeline} from "@tarojs/taro"; +import Taro from "@tarojs/taro"; +import {Tabs} from '@nutui/nutui-react-taro' import {ShopGoods} from "@/api/shop/shopGoods/model"; import {pageShopGoods} from "@/api/shop/shopGoods"; -import './BestSellers.scss' const BestSellers = () => { + const [tab1value, setTab1value] = useState('0') const [list, setList] = useState([]) - const [goods, setGoods] = useState() + const [goods, setGoods] = useState(null) + // 轮播图固定高度,可根据需求调整 + const SWIPER_HEIGHT = 180; const reload = () => { pageShopGoods({}).then(res => { - setList(res?.list || []); - }) + const processGoodsItem = (item: ShopGoods) => { + const pics: string[] = []; + // 添加主图 + if (item.image) { + pics.push(item.image); + } + // 处理附加图片 + if (item.files) { + try { + // 解析文件字符串为对象 + const files = typeof item.files === "string" + ? JSON.parse(item.files) + : item.files; + + // 收集所有图片URL + Object.values(files).forEach(file => { + if (file?.url) { + pics.push(file.url); + } + }); + } catch (error) { + console.error('解析文件失败:', error); + } + } + // 返回新对象,避免直接修改原对象 + return {...item, pics}; + }; + + // 处理商品列表 + const goods = (res?.list || []).map(processGoodsItem); + setList(goods); + }).catch(err => { + console.error('获取商品列表失败:', err); + }); } // 处理分享点击 @@ -24,14 +59,12 @@ const BestSellers = () => { // 显示分享选项菜单 Taro.showActionSheet({ - itemList: ['分享给好友', '分享到朋友圈'], + itemList: ['分享给好友'], success: (res) => { if (res.tapIndex === 0) { - // 分享给好友 - 触发转发 Taro.showShareMenu({ withShareTicket: true, success: () => { - // 提示用户点击右上角分享 Taro.showToast({ title: '请点击右上角分享给好友', icon: 'none', @@ -39,13 +72,6 @@ const BestSellers = () => { }); } }); - } else if (res.tapIndex === 1) { - // 分享到朋友圈 - Taro.showToast({ - title: '请点击右上角分享到朋友圈', - icon: 'none', - duration: 2000 - }); } }, fail: (err) => { @@ -55,87 +81,135 @@ const BestSellers = () => { } useEffect(() => { - reload() - }, []) + reload(); + }, []); - // 分享给好友 - useShareAppMessage(() => { + // 配置分享内容 + Taro.useShareAppMessage(() => { + if (goods) { + return { + title: goods.name, + path: `/shop/goodsDetail/index?id=${goods.goodsId}`, + imageUrl: goods.image || '' + }; + } return { - title: goods?.name || '精选商品', - path: `/shop/goodsDetail/index?id=${goods?.goodsId}`, - imageUrl: goods?.image, // 分享图片 - success: function (res: any) { - console.log('分享成功', res); - Taro.showToast({ - title: '分享成功', - icon: 'success', - duration: 2000 - }); - }, - fail: function (res: any) { - console.log('分享失败', res); - Taro.showToast({ - title: '分享失败', - icon: 'none', - duration: 2000 - }); - } - }; - }); - - // 分享到朋友圈 - useShareTimeline(() => { - return { - title: `${goods?.name || '精选商品'} - 网宿小店`, - path: `/shop/goodsDetail/index?id=${goods?.goodsId}`, - imageUrl: goods?.image + title: '热销商品', + path: '/pages/index/index' }; }); return ( - <> - - - {list?.map((item, index) => { - return ( - - Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}/> - - - {item.name} - - {item.comments} - 已售 {item.sales} - - - - - {item.price} - - - - handleShare(item)} - > - - - - Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}>购买 - + + + { + setTab1value(value) + }} + style={{ + backgroundColor: '#fff', + }} + activeType="smile" + > + + + + + + + + + {tab1value == '0' && list?.map((item) => ( + + {/* 轮播图组件 */} + {item.pics && item.pics.length > 0 ? ( + + {item.pics.map((pic, picIndex) => ( + + Taro.navigateTo({ + url: `/shop/goodsDetail/index?id=${item.goodsId}` + })} + className="swiper-image" + /> + + ))} + + ) : ( + // 没有图片时显示占位图 + + 暂无图片 + + )} + + + + {item.name} + + {item.comments} + 已售 {item.sales} + + + + + {item.price} + + + + handleShare(item)} + > + + Taro.navigateTo({ + url: `/shop/goodsDetail/index?id=${item.goodsId}` + })} + > + 购买 + - ) - })} - + + + ))} + + { + tab1value == '1' && + } + + { + tab1value == '2' && + } + - + ) } + export default BestSellers diff --git a/src/pages/index/Header.tsx b/src/pages/index/Header.tsx index be5a673..42d467b 100644 --- a/src/pages/index/Header.tsx +++ b/src/pages/index/Header.tsx @@ -8,7 +8,7 @@ import {TenantId} from "@/config/app"; import {getOrganization} from "@/api/system/organization"; import {myUserVerify} from "@/api/system/userVerify"; import { useShopInfo } from '@/hooks/useShopInfo'; -import {handleInviteRelation} from "@/utils/invite"; +import {handleInviteRelation, getStoredInviteParams} from "@/utils/invite"; import {View,Text} from '@tarojs/components' import MySearch from "./MySearch"; import './Header.scss'; @@ -88,6 +88,22 @@ const Header = (props: any) => { /* 获取用户手机号 */ const handleGetPhoneNumber = ({detail}: {detail: {code?: string, encryptedData?: string, iv?: string}}) => { const {code, encryptedData, iv} = detail + + // 防重复登录检查 + const loginKey = 'login_in_progress' + const loginInProgress = Taro.getStorageSync(loginKey) + + if (loginInProgress && Date.now() - loginInProgress < 5000) { // 5秒内防重 + return + } + + // 标记登录开始 + Taro.setStorageSync(loginKey, Date.now()) + + // 获取存储的邀请参数 + const inviteParams = getStoredInviteParams() + const refereeId = inviteParams?.inviter ? parseInt(inviteParams.inviter) : 0 + Taro.login({ success: function () { if (code) { @@ -99,7 +115,7 @@ const Header = (props: any) => { encryptedData, iv, notVerifyPhone: true, - refereeId: 0, + refereeId: refereeId, // 使用解析出的推荐人ID sceneType: 'save_referee', tenantId: TenantId }, @@ -108,6 +124,9 @@ const Header = (props: any) => { TenantId }, success: async function (res) { + // 清除登录防重标记 + Taro.removeStorageSync('login_in_progress') + if (res.data.code == 1) { Taro.showToast({ title: res.data.message, @@ -124,14 +143,7 @@ const Header = (props: any) => { // 处理邀请关系 if (res.data.data.user?.userId) { try { - const inviteSuccess = await handleInviteRelation(res.data.data.user.userId) - if (inviteSuccess) { - Taro.showToast({ - title: '邀请关系建立成功', - icon: 'success', - duration: 2000 - }) - } + await handleInviteRelation(res.data.data.user.userId) } catch (error) { console.error('处理邀请关系失败:', error) } @@ -141,6 +153,10 @@ const Header = (props: any) => { Taro.reLaunch({ url: '/pages/index/index' }) + }, + fail: function() { + // 清除登录防重标记 + Taro.removeStorageSync('login_in_progress') } }) } else { @@ -157,9 +173,10 @@ const Header = (props: any) => { return ( <> - + {/*{!props.stickyStatus && }*/} { )}> + {/**/} ) diff --git a/src/pages/index/Login.tsx b/src/pages/index/Login.tsx index f4a0e0d..9c88ef7 100644 --- a/src/pages/index/Login.tsx +++ b/src/pages/index/Login.tsx @@ -4,7 +4,7 @@ import {Input, Radio, Button} from '@nutui/nutui-react-taro' import {TenantId} from "@/config/app"; import './login.scss'; import {saveStorageByLoginUser} from "@/utils/server"; -import {handleInviteRelation} from "@/utils/invite"; +import {handleInviteRelation, getStoredInviteParams} from "@/utils/invite"; // 微信获取手机号回调参数类型 interface GetPhoneNumberDetail { @@ -40,6 +40,11 @@ const Login = (props: LoginProps) => { /* 获取用户手机号 */ const handleGetPhoneNumber = ({detail}: GetPhoneNumberEvent) => { const {code, encryptedData, iv} = detail + + // 获取存储的邀请参数 + const inviteParams = getStoredInviteParams() + const refereeId = inviteParams?.inviter ? parseInt(inviteParams.inviter) : 0 + Taro.login({ success: function () { if (code) { @@ -51,7 +56,7 @@ const Login = (props: LoginProps) => { encryptedData, iv, notVerifyPhone: true, - refereeId: 0, + refereeId: refereeId, // 使用解析出的推荐人ID sceneType: 'save_referee', tenantId: TenantId }, diff --git a/src/pages/index/MySearch.tsx b/src/pages/index/MySearch.tsx index b0d43fb..3c6d9a5 100644 --- a/src/pages/index/MySearch.tsx +++ b/src/pages/index/MySearch.tsx @@ -4,7 +4,7 @@ import {useState} from "react"; import Taro from '@tarojs/taro'; import { goTo } from '@/utils/navigation'; -function MySearch() { +function MySearch(props: any) { const [keywords, setKeywords] = useState('') const onKeywords = (keywords: string) => { @@ -39,7 +39,7 @@ function MySearch() { background: '#ffffff', padding: '0 5px', borderRadius: '20px', - marginTop: '100px', + marginTop: `${props.statusBarHeight + 50}px`, }} > diff --git a/src/pages/index/index.tsx b/src/pages/index/index.tsx index 5e7dba9..3aee534 100644 --- a/src/pages/index/index.tsx +++ b/src/pages/index/index.tsx @@ -1,7 +1,7 @@ import Header from './Header'; import BestSellers from './BestSellers'; import Taro from '@tarojs/taro'; -import {useShareAppMessage, useShareTimeline} from "@tarojs/taro" +import {useShareAppMessage} from "@tarojs/taro" import {useEffect, useState} from "react"; import {getShopInfo} from "@/api/layout"; import {Sticky} from '@nutui/nutui-react-taro' @@ -16,22 +16,28 @@ function Home() { // 吸顶状态 const [stickyStatus, setStickyStatus] = useState(false) - useShareTimeline(() => { - return { - title: '网宿小店 - 网宿软件', - path: `/pages/index/index` - }; - }); - useShareAppMessage(() => { + // 获取当前用户ID,用于生成邀请链接 + const userId = Taro.getStorageSync('UserId'); + return { title: '网宿小店 - 网宿软件', - path: `/pages/index/index`, + path: userId ? `/pages/index/index?inviter=${userId}&source=share&t=${Date.now()}` : `/pages/index/index`, success: function () { - console.log('分享成功'); + console.log('首页分享成功'); + Taro.showToast({ + title: '分享成功', + icon: 'success', + duration: 2000 + }); }, fail: function () { - console.log('分享失败'); + console.log('首页分享失败'); + Taro.showToast({ + title: '分享失败', + icon: 'none', + duration: 2000 + }); } }; }); @@ -89,18 +95,32 @@ function Home() { }) - // 检查是否有待处理的邀请关系 + // 检查是否有待处理的邀请关系 - 异步处理,不阻塞页面加载 if (hasPendingInvite()) { console.log('检测到待处理的邀请关系') - // 延迟处理,确保用户信息已加载 + // 延迟处理,确保用户信息已加载,并设置超时保护 setTimeout(async () => { try { - const success = await checkAndHandleInviteRelation() + // 设置超时保护,避免长时间等待 + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error('邀请关系处理超时')), 8000) + ); + + const invitePromise = checkAndHandleInviteRelation(); + + const success = await Promise.race([invitePromise, timeoutPromise]); if (success) { console.log('首页邀请关系处理成功') } } catch (error) { console.error('首页邀请关系处理失败:', error) + // 邀请关系处理失败不应该影响页面正常显示 + // 可以选择清除邀请参数,避免重复尝试 + const errorMessage = error instanceof Error ? error.message : String(error) + if (errorMessage?.includes('超时')) { + console.log('邀请关系处理超时,清除邀请参数') + // 可以选择清除邀请参数或稍后重试 + } } }, 2000) } diff --git a/src/pages/menu/menu.config.ts b/src/pages/menu/menu.config.ts new file mode 100644 index 0000000..2cb081e --- /dev/null +++ b/src/pages/menu/menu.config.ts @@ -0,0 +1,3 @@ +export default definePageConfig({ + navigationBarTitleText: '菜单' +}) diff --git a/src/pages/menu/menu.tsx b/src/pages/menu/menu.tsx new file mode 100644 index 0000000..325e9cf --- /dev/null +++ b/src/pages/menu/menu.tsx @@ -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() + const [loading, setLoading] = useState(false) + const [list, setList] = useState() + + 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 && (
暂无数据
)} + { + }} + > + 发现 + + {list && ( + <> + + + 好物推荐 + + } extra={}/> + + + 权益中心 + + } + extra={} + onClick={() => { + navTo('/shop/shopArticle/index', true) + }} + /> + + + 我的收藏 + + } extra={}/> + + )} + + ) +} +export default Menu diff --git a/src/pages/user/components/IsDealer.tsx b/src/pages/user/components/IsDealer.tsx index 1a18b1d..3d4ef65 100644 --- a/src/pages/user/components/IsDealer.tsx +++ b/src/pages/user/components/IsDealer.tsx @@ -6,7 +6,7 @@ import {useUser} from '@/hooks/useUser' import {useEffect} from "react"; import {useDealerUser} from "@/hooks/useDealerUser"; -const UserCell = () => { +const IsDealer = () => { const {isSuperAdmin} = useUser(); const {dealerUser} = useDealerUser() @@ -20,7 +20,7 @@ const UserCell = () => { if (isSuperAdmin()) { return ( <> - + { if (dealerUser) { return ( <> - + { 医生管理端 + className={'pl-3 text-orange-100 font-medium'}>分销中心 {/*门店核销*/} } @@ -73,7 +73,7 @@ const UserCell = () => { */ return ( <> - + { title={ - 医生入驻 + 开通VIP + 享优惠 } - extra={ - <> - 需医师资格证 - - - } + extra={} onClick={() => navTo('/dealer/apply/add', true)} /> ) } -export default UserCell +export default IsDealer diff --git a/src/pages/user/components/UserCard.tsx b/src/pages/user/components/UserCard.tsx index 4ca426d..f0f0268 100644 --- a/src/pages/user/components/UserCard.tsx +++ b/src/pages/user/components/UserCard.tsx @@ -1,28 +1,21 @@ -import {Button} from '@nutui/nutui-react-taro' -import {Avatar, Tag} from '@nutui/nutui-react-taro' -import {View, Text} from '@tarojs/components' -import {Scan} from '@nutui/icons-react-taro'; +import {Avatar, Tag, Space, Button} from '@nutui/nutui-react-taro' +import {View, Text, Image} from '@tarojs/components' import {getUserInfo, getWxOpenId} from '@/api/layout'; import Taro from '@tarojs/taro'; -import {useEffect, useState} from "react"; +import {useEffect, useState, forwardRef, useImperativeHandle} from "react"; import {User} from "@/api/system/user/model"; import navTo from "@/utils/common"; import {TenantId} from "@/config/app"; -import {getMyAvailableCoupons} from "@/api/shop/shopUserCoupon"; import {useUser} from "@/hooks/useUser"; import {useUserData} from "@/hooks/useUserData"; +import {getStoredInviteParams} from "@/utils/invite"; +import UnifiedQRButton from "@/components/UnifiedQRButton"; -function UserCard() { - const { - isAdmin - } = useUser(); - const { data, refresh } = useUserData() +const UserCard = forwardRef((_, ref) => { + const {data, refresh} = useUserData() const {getDisplayName, getRoleName} = useUser(); const [IsLogin, setIsLogin] = useState(false) const [userInfo, setUserInfo] = useState() - const [couponCount, setCouponCount] = useState(0) - const [pointsCount, setPointsCount] = useState(0) - const [giftCount, setGiftCount] = useState(0) // 下拉刷新 const handleRefresh = async () => { @@ -33,6 +26,11 @@ function UserCard() { }) } + // 暴露方法给父组件 + useImperativeHandle(ref, () => ({ + handleRefresh + })) + useEffect(() => { // Taro.getSetting:获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限。 Taro.getSetting({ @@ -50,33 +48,6 @@ function UserCard() { }); }, []); - const loadUserStats = (userId: number) => { - // 加载优惠券数量 - getMyAvailableCoupons() - .then((coupons: any) => { - setCouponCount(coupons?.length || 0) - }) - .catch((error: any) => { - console.error('Coupon count error:', error) - }) - - // 加载积分数量 - console.log(userId) - setPointsCount(0) - // getUserPointsStats(userId) - // .then((res: any) => { - // setPointsCount(res.currentPoints || 0) - // }) - // .catch((error: any) => { - // console.error('Points stats error:', error) - // }) - // 加载礼品劵数量 - setGiftCount(0) - // pageUserGiftLog({userId, page: 1, limit: 1}).then(res => { - // setGiftCount(res.count || 0) - // }) - } - const reload = () => { Taro.getUserInfo({ success: (res) => { @@ -92,11 +63,6 @@ function UserCard() { setIsLogin(true); Taro.setStorageSync('UserId', data.userId) - // 加载用户统计数据 - if (data.userId) { - loadUserStats(data.userId) - } - // 获取openId if (!data.openid) { Taro.login({ @@ -149,8 +115,13 @@ function UserCard() { }; /* 获取用户手机号 */ - const handleGetPhoneNumber = ({detail}: {detail: {code?: string, encryptedData?: string, iv?: string}}) => { + const handleGetPhoneNumber = ({detail}: { detail: { code?: string, encryptedData?: string, iv?: string } }) => { const {code, encryptedData, iv} = detail + + // 获取存储的邀请参数 + const inviteParams = getStoredInviteParams() + const refereeId = inviteParams?.inviter ? parseInt(inviteParams.inviter) : 0 + Taro.login({ success: function () { if (code) { @@ -162,7 +133,7 @@ function UserCard() { encryptedData, iv, notVerifyPhone: true, - refereeId: 0, + refereeId: refereeId, // 使用解析出的推荐人ID sceneType: 'save_referee', tenantId: TenantId }, @@ -194,74 +165,114 @@ function UserCard() { } return ( - + - - - - { - IsLogin ? ( - - ) : ( - - ) - } - - {getDisplayName()} - {IsLogin ? ( - - - - {getRoleName()} - - - - ) : ''} + {/* 使用相对定位容器,让个人资料图片可以绝对定位在右上角 */} + + + + + { + IsLogin ? ( + + ) : ( + + ) + } + + {getDisplayName()} + {IsLogin ? ( + + + + {getRoleName()} + + + + ) : ''} + + + + {/*统一扫码入口 - 支持登录和核销*/} + { + console.log('统一扫码成功:', result); + // 根据扫码类型给出不同的提示 + if (result.type === 'verification') { + // 核销成功,可以显示更多信息或跳转到详情页 + Taro.showModal({ + title: '核销成功', + content: `已成功核销的品类:${result.data.goodsName || '礼品卡'},面值¥${result.data.faceValue}` + }); + } + }} + onError={(error) => { + console.error('统一扫码失败:', error); + }} + /> + + + + navTo('/user/wallet/wallet', true)}> + 余额 + {data?.balance || '0.00'} + + + 积分 + {data?.points || 0} + + navTo('/user/coupon/index', true)}> + 优惠券 + {data?.coupons || 0} + + navTo('/user/gift/index', true)}> + 礼品卡 + {data?.giftCards || 0} - {isAdmin() && navTo('/user/store/verification', true)} />} - navTo('/user/profile/profile', true)}> - {'个人资料'} - - - navTo('/user/wallet/wallet', true)}> - 余额 - {data?.balance || '0.00'} - - - 积分 - {data?.points || 0} - - navTo('/user/coupon/index', true)}> - 优惠券 - {data?.coupons || 0} - - navTo('/user/gift/index', true)}> - 礼品卡 - {data?.giftCards || 0} - + + {/* 个人资料图片,定位在右上角 */} + navTo('/user/profile/profile', true)} + > + - ) -} +}) export default UserCard; diff --git a/src/pages/user/components/UserCell.tsx b/src/pages/user/components/UserCell.tsx index 7aa218d..46a0882 100644 --- a/src/pages/user/components/UserCell.tsx +++ b/src/pages/user/components/UserCell.tsx @@ -26,7 +26,7 @@ const UserCell = () => { return ( <> - + diff --git a/src/pages/user/components/UserGrid.tsx b/src/pages/user/components/UserGrid.tsx new file mode 100644 index 0000000..5c3e5da --- /dev/null +++ b/src/pages/user/components/UserGrid.tsx @@ -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 ( + <> + + 我的服务 + + + navTo('/user/poster/poster', true)}> + + + + + + + + {/* 修改联系我们为微信客服 */} + + + + + navTo('/user/address/index', true)}> + + + + + + + + navTo('/user/userVerify/index', true)}> + + + + + + + + navTo('/dealer/team/index', true)}> + + + + + + + + {/* navTo('/dealer/qrcode/index', true)}>*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/**/} + + {/* navTo('/admin/index', true)}>*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/**/} + + navTo('/user/help/index')}> + + + + + + + + navTo('/user/about/index')}> + + + + + + + + + + + + + + + + + + + + ) +} +export default UserCell + diff --git a/src/pages/user/components/UserOrder.tsx b/src/pages/user/components/UserOrder.tsx index cc0f58d..0a17847 100644 --- a/src/pages/user/components/UserOrder.tsx +++ b/src/pages/user/components/UserOrder.tsx @@ -14,7 +14,7 @@ function UserOrder() { return ( <> - + () + const themeStyles = useThemeStyles(); + + // 下拉刷新处理 + const handleRefresh = async () => { + await refresh() + // 如果 UserCard 组件有自己的刷新方法,也可以调用 + if (userCardRef.current?.handleRefresh) { + await userCardRef.current.handleRefresh() + } + } useEffect(() => { }, []); - /** - * 门店核销管理 - */ - if (isAdmin()) { - return <> -
- - - - - -
- - } - return ( - <> -
- - - - - -
- + + + + + + + + ) } diff --git a/src/passport/qr-confirm/index.config.ts b/src/passport/qr-confirm/index.config.ts new file mode 100644 index 0000000..9a9855a --- /dev/null +++ b/src/passport/qr-confirm/index.config.ts @@ -0,0 +1,5 @@ +export default { + navigationBarTitleText: '确认登录', + navigationBarTextStyle: 'black', + navigationBarBackgroundColor: '#ffffff' +} diff --git a/src/passport/qr-confirm/index.tsx b/src/passport/qr-confirm/index.tsx new file mode 100644 index 0000000..184b27b --- /dev/null +++ b/src/passport/qr-confirm/index.tsx @@ -0,0 +1,239 @@ +import React, { useState, useEffect } from 'react'; +import { View, Text } from '@tarojs/components'; +import { Button, Loading, Card } from '@nutui/nutui-react-taro'; +import { Success, Failure, Tips, User } from '@nutui/icons-react-taro'; +import Taro, { useRouter } from '@tarojs/taro'; +import { confirmQRLogin } from '@/api/passport/qr-login'; +import { useUser } from '@/hooks/useUser'; + +/** + * 扫码登录确认页面 + * 用于处理从二维码跳转过来的登录确认 + */ +const QRConfirmPage: React.FC = () => { + const router = useRouter(); + const { user, getDisplayName } = useUser(); + const [loading, setLoading] = useState(false); + const [confirmed, setConfirmed] = useState(false); + const [error, setError] = useState(''); + const [token, setToken] = useState(''); + + useEffect(() => { + // 从URL参数中获取token + const { qrCodeKey, token: urlToken } = router.params; + const loginToken = qrCodeKey || urlToken; + + if (loginToken) { + setToken(loginToken); + } else { + setError('无效的登录链接'); + } + }, [router.params]); + + // 确认登录 + const handleConfirmLogin = async () => { + if (!token) { + setError('缺少登录token'); + return; + } + + if (!user?.userId) { + setError('请先登录小程序'); + return; + } + + try { + setLoading(true); + setError(''); + + const result = await confirmQRLogin({ + token, + userId: user.userId, + platform: 'wechat', + wechatInfo: { + nickname: user.nickname, + avatar: user.avatar + } + }); + + if (result.success) { + setConfirmed(true); + Taro.showToast({ + title: '登录确认成功', + icon: 'success', + duration: 2000 + }); + + // 3秒后自动返回 + setTimeout(() => { + Taro.navigateBack(); + }, 3000); + } else { + setError(result.message || '登录确认失败'); + } + } catch (err: any) { + setError(err.message || '登录确认失败'); + } finally { + setLoading(false); + } + }; + + // 取消登录 + const handleCancel = () => { + Taro.navigateBack(); + }; + + // 重试 + const handleRetry = () => { + setError(''); + setConfirmed(false); + handleConfirmLogin(); + }; + + return ( + + + {/* 主要内容卡片 */} + + + {/* 图标 */} + + {loading ? ( + + + + ) : confirmed ? ( + + + + ) : error ? ( + + + + ) : ( + + + + )} + + + {/* 标题 */} + + {loading ? '正在确认登录...' : + confirmed ? '登录确认成功' : + error ? '登录确认失败' : '确认登录'} + + + {/* 描述 */} + + {loading ? '请稍候,正在为您确认登录' : + confirmed ? '您已成功确认登录,网页端将自动登录' : + error ? error : + `确认使用 ${getDisplayName()} 登录网页端?`} + + + {/* 用户信息 */} + {!loading && !confirmed && !error && user && ( + + + + + + + + {user.nickname || user.username || '用户'} + + + ID: {user.userId} + + + + + )} + + {/* 操作按钮 */} + + {loading ? ( + + ) : confirmed ? ( + + ) : error ? ( + + + + + ) : ( + + + + + )} + + + + + {/* 安全提示 */} + + + + + + + 安全提示 + + + 请确认这是您本人的登录操作。如果不是,请点击取消并检查账户安全。 + + + + + + + + ); +}; + +export default QRConfirmPage; diff --git a/src/passport/qr-login/index.config.ts b/src/passport/qr-login/index.config.ts new file mode 100644 index 0000000..54abd28 --- /dev/null +++ b/src/passport/qr-login/index.config.ts @@ -0,0 +1,5 @@ +export default { + navigationBarTitleText: '扫码登录', + navigationBarTextStyle: 'black', + navigationBarBackgroundColor: '#ffffff' +} diff --git a/src/passport/qr-login/index.tsx b/src/passport/qr-login/index.tsx new file mode 100644 index 0000000..c2b4321 --- /dev/null +++ b/src/passport/qr-login/index.tsx @@ -0,0 +1,193 @@ +import React, { useState } from 'react'; +import { View, Text } from '@tarojs/components'; +import { Card, Divider, Button } from '@nutui/nutui-react-taro'; +import { Scan, Success, Failure, Tips } from '@nutui/icons-react-taro'; +import Taro from '@tarojs/taro'; +import QRLoginScanner from '@/components/QRLoginScanner'; +import { useUser } from '@/hooks/useUser'; + +/** + * 扫码登录页面 + */ +const QRLoginPage: React.FC = () => { + const [loginHistory, setLoginHistory] = useState([]); + const { getDisplayName } = useUser(); + + // 处理扫码成功 + const handleScanSuccess = (result: any) => { + console.log('扫码登录成功:', result); + + // 添加到登录历史 + const newRecord = { + id: Date.now(), + time: new Date().toLocaleString(), + userInfo: result.userInfo, + success: true + }; + setLoginHistory(prev => [newRecord, ...prev.slice(0, 4)]); // 只保留最近5条记录 + + // 显示成功提示 + Taro.showToast({ + title: '登录确认成功', + icon: 'success', + duration: 2000 + }); + }; + + // 处理扫码失败 + const handleScanError = (error: string) => { + console.error('扫码登录失败:', error); + + // 添加到登录历史 + const newRecord = { + id: Date.now(), + time: new Date().toLocaleString(), + error, + success: false + }; + setLoginHistory(prev => [newRecord, ...prev.slice(0, 4)]); + }; + + // 返回上一页 + // const handleBack = () => { + // Taro.navigateBack(); + // }; + + // 清除历史记录 + const clearHistory = () => { + setLoginHistory([]); + Taro.showToast({ + title: '已清除历史记录', + icon: 'success' + }); + }; + + return ( + + {/* 导航栏 */} + {/*}*/} + {/* className="bg-white"*/} + {/*/>*/} + + {/* 主要内容 */} + + {/* 用户信息卡片 */} + + + + + + + + + {getDisplayName()} + + + 使用小程序扫码快速登录网页端 + + + + + {/* 扫码登录组件 */} + + + + + {/* 使用说明 */} + + + + + 使用说明 + + + 1. 在电脑或其他设备上打开网页端登录页面 + 2. 点击"扫码登录"按钮,显示登录二维码 + 3. 使用此功能扫描二维码即可快速登录 + 4. 扫码成功后,网页端将自动完成登录 + + + + + {/* 登录历史 */} + {loginHistory.length > 0 && ( + + + + 最近登录记录 + + + + + {loginHistory.map((record, index) => ( + + + + {record.success ? ( + + ) : ( + + )} + + + {record.success ? '登录成功' : '登录失败'} + + {record.error && ( + + {record.error} + + )} + + + + {record.time} + + + {index < loginHistory.length - 1 && ( + + )} + + ))} + + + + )} + + {/* 安全提示 */} + + + + + + + 安全提示 + + + 请确保只扫描来自官方网站的登录二维码,避免扫描来源不明的二维码,保护账户安全。 + + + + + + + + ); +}; + +export default QRLoginPage; diff --git a/src/passport/unified-qr/index.config.ts b/src/passport/unified-qr/index.config.ts new file mode 100644 index 0000000..3a5a194 --- /dev/null +++ b/src/passport/unified-qr/index.config.ts @@ -0,0 +1,4 @@ +export default { + navigationBarTitleText: '统一扫码', + navigationBarTextStyle: 'black' +} \ No newline at end of file diff --git a/src/passport/unified-qr/index.tsx b/src/passport/unified-qr/index.tsx new file mode 100644 index 0000000..9e87c7f --- /dev/null +++ b/src/passport/unified-qr/index.tsx @@ -0,0 +1,320 @@ +import React, { useState } from 'react'; +import { View, Text } from '@tarojs/components'; +import { Card, Button, Tag } from '@nutui/nutui-react-taro'; +import { Scan, Success, Failure, Tips, ArrowLeft } from '@nutui/icons-react-taro'; +import Taro from '@tarojs/taro'; +import { useUnifiedQRScan, ScanType, type UnifiedScanResult } from '@/hooks/useUnifiedQRScan'; + +/** + * 统一扫码页面 + * 支持登录和核销两种类型的二维码扫描 + */ +const UnifiedQRPage: React.FC = () => { + const [scanHistory, setScanHistory] = useState([]); + const { + startScan, + isLoading, + canScan, + state, + result, + error, + scanType, + reset + } = useUnifiedQRScan(); + + // 处理扫码成功 + const handleScanSuccess = (result: UnifiedScanResult) => { + console.log('扫码成功:', result); + + // 添加到扫码历史 + const newRecord = { + id: Date.now(), + time: new Date().toLocaleString(), + type: result.type, + data: result.data, + message: result.message, + success: true + }; + setScanHistory(prev => [newRecord, ...prev.slice(0, 4)]); // 只保留最近5条记录 + + // 根据类型给出不同提示 + if (result.type === ScanType.VERIFICATION) { + // 核销成功后询问是否继续扫码 + setTimeout(() => { + Taro.showModal({ + title: '核销成功', + content: '是否继续扫码核销其他礼品卡?', + success: (res) => { + if (res.confirm) { + handleStartScan(); + } + } + }); + }, 2000); + } + }; + + // 处理扫码失败 + const handleScanError = (error: string) => { + console.error('扫码失败:', error); + + // 添加到扫码历史 + const newRecord = { + id: Date.now(), + time: new Date().toLocaleString(), + error, + success: false + }; + setScanHistory(prev => [newRecord, ...prev.slice(0, 4)]); // 只保留最近5条记录 + }; + + // 开始扫码 + const handleStartScan = async () => { + try { + const scanResult = await startScan(); + if (scanResult) { + handleScanSuccess(scanResult); + } + } catch (error: any) { + handleScanError(error.message || '扫码失败'); + } + }; + + // 返回上一页 + const handleGoBack = () => { + Taro.navigateBack(); + }; + + // 获取状态图标 + const getStatusIcon = (success: boolean, type?: ScanType) => { + console.log(type,'获取状态图标') + if (success) { + return ; + } else { + return ; + } + }; + + // 获取类型标签 + const getTypeTag = (type: ScanType) => { + switch (type) { + case ScanType.LOGIN: + return 登录; + case ScanType.VERIFICATION: + return 核销; + default: + return 未知; + } + }; + + return ( + + {/* 页面头部 */} + + + + 统一扫码 + + 支持登录和核销功能 + + + + + {/* 主要扫码区域 */} + + + {/* 状态显示 */} + {state === 'idle' && ( + <> + + + 智能扫码 + + + 自动识别登录和核销二维码 + + + + )} + + {state === 'scanning' && ( + <> + + + 扫码中... + + + 请对准二维码 + + + + )} + + {state === 'processing' && ( + <> + + + + + 处理中... + + + {scanType === ScanType.LOGIN ? '正在确认登录' : + scanType === ScanType.VERIFICATION ? '正在核销礼品卡' : '正在处理'} + + + )} + + {state === 'success' && result && ( + <> + + + {result.message} + + {result.type === ScanType.VERIFICATION && result.data && ( + + + 礼品卡:{result.data.goodsName || '未知商品'} + + + 面值:¥{result.data.faceValue} + + + )} + + + + + + )} + + {state === 'error' && ( + <> + + + 操作失败 + + + {error || '未知错误'} + + + + + + + )} + + + + {/* 扫码历史 */} + {scanHistory.length > 0 && ( + + + + 最近扫码记录 + + + {scanHistory.map((record, index) => ( + + + {getStatusIcon(record.success, record.type)} + + + {record.type && getTypeTag(record.type)} + + {record.time} + + + + {record.success ? record.message : record.error} + + {record.success && record.type === ScanType.VERIFICATION && record.data && ( + + {record.data.goodsName} - ¥{record.data.faceValue} + + )} + + + + ))} + + + )} + + {/* 功能说明 */} + + + + + + + 功能说明 + + + • 登录二维码:自动确认网页端登录 + + + • 核销二维码:门店核销用户礼品卡 + + + • 系统会自动识别二维码类型并执行相应操作 + + + + + + + ); +}; + +export default UnifiedQRPage; diff --git a/src/shop/category/index.tsx b/src/shop/category/index.tsx index 6e4c5a4..4f81fd7 100644 --- a/src/shop/category/index.tsx +++ b/src/shop/category/index.tsx @@ -1,6 +1,6 @@ import Taro from '@tarojs/taro' import GoodsList from './components/GoodsList' -import {useShareAppMessage, useShareTimeline} from "@tarojs/taro" +import {useShareAppMessage} from "@tarojs/taro" import {Loading} from '@nutui/nutui-react-taro' import {useEffect, useState} from "react" import {useRouter} from '@tarojs/taro' @@ -40,16 +40,9 @@ function Category() { }) }, []); - useShareTimeline(() => { - return { - title: `${nav?.categoryName}_通源堂健康生态平台`, - path: `/shop/category/index?id=${categoryId}` - }; - }); - useShareAppMessage(() => { return { - title: `${nav?.categoryName}_通源堂健康生态平台`, + title: `${nav?.categoryName}_时里院子市集`, path: `/shop/category/index?id=${categoryId}`, success: function () { console.log('分享成功'); diff --git a/src/shop/goodsDetail/index.tsx b/src/shop/goodsDetail/index.tsx index 75f8279..5bedda4 100644 --- a/src/shop/goodsDetail/index.tsx +++ b/src/shop/goodsDetail/index.tsx @@ -1,7 +1,7 @@ import {useEffect, useState} from "react"; import {Image, Divider, Badge} from "@nutui/nutui-react-taro"; import {ArrowLeft, Headphones, Share, Cart} from "@nutui/icons-react-taro"; -import Taro, {useShareAppMessage, useShareTimeline} from "@tarojs/taro"; +import Taro, {useShareAppMessage} from "@tarojs/taro"; import {RichText, View} from '@tarojs/components' import {ShopGoods} from "@/api/shop/shopGoods/model"; import {getShopGoods} from "@/api/shop/shopGoods"; @@ -186,15 +186,6 @@ const GoodsDetail = () => { }; }); - // 分享到朋友圈 - useShareTimeline(() => { - return { - title: `${goods?.name || '精选商品'} - 网宿小店`, - path: `/shop/goodsDetail/index?id=${goodsId}`, - imageUrl: goods?.image - }; - }); - if (!goods || loading) { return
加载中...
; } @@ -282,12 +273,14 @@ const GoodsDetail = () => { - + + + diff --git a/src/shop/orderConfirm/index.tsx b/src/shop/orderConfirm/index.tsx index 399e0c4..58fe1c1 100644 --- a/src/shop/orderConfirm/index.tsx +++ b/src/shop/orderConfirm/index.tsx @@ -73,7 +73,21 @@ const OrderConfirm = () => { // 计算商品总价 const getGoodsTotal = () => { if (!goods) return 0 - return parseFloat(goods.price || '0') * quantity + const price = parseFloat(goods.price || '0') + const total = price * quantity + + // 🔍 详细日志,用于排查数值精度问题 + console.log('💵 商品总价计算:', { + goodsPrice: goods.price, + goodsPriceType: typeof goods.price, + parsedPrice: price, + quantity: quantity, + total: total, + totalFixed2: total.toFixed(2), + totalString: total.toString() + }) + + return total } // 计算优惠券折扣 @@ -106,6 +120,7 @@ const OrderConfirm = () => { if (availableCoupons.length > 0) { const newTotal = parseFloat(goods?.price || '0') * finalQuantity const sortedCoupons = sortCoupons(availableCoupons, newTotal) + const usableCoupons = filterUsableCoupons(sortedCoupons, newTotal) setAvailableCoupons(sortedCoupons) // 检查当前选中的优惠券是否还可用 @@ -115,6 +130,56 @@ const OrderConfirm = () => { title: '当前优惠券不满足使用条件,已自动取消', icon: 'none' }) + + // 🎯 自动推荐新的最优优惠券 + if (usableCoupons.length > 0) { + const bestCoupon = usableCoupons[0] + const discount = calculateCouponDiscount(bestCoupon, newTotal) + + if (discount > 0) { + setSelectedCoupon(bestCoupon) + Taro.showToast({ + title: `已为您重新推荐最优优惠券,可省¥${discount.toFixed(2)}`, + icon: 'success', + duration: 3000 + }) + } + } + } else if (!selectedCoupon && usableCoupons.length > 0) { + // 🔔 如果没有选中优惠券但有可用的,推荐最优的 + const bestCoupon = usableCoupons[0] + const discount = calculateCouponDiscount(bestCoupon, newTotal) + + if (discount > 0) { + setSelectedCoupon(bestCoupon) + Taro.showToast({ + title: `已为您推荐最优优惠券,可省¥${discount.toFixed(2)}`, + icon: 'success', + duration: 3000 + }) + } + } else if (selectedCoupon && usableCoupons.length > 0) { + // 🔍 检查是否有更好的优惠券 + const bestCoupon = usableCoupons[0] + const currentDiscount = calculateCouponDiscount(selectedCoupon, newTotal) + const bestDiscount = calculateCouponDiscount(bestCoupon, newTotal) + + // 如果有更好的优惠券(优惠超过0.01元) + if (bestDiscount > currentDiscount + 0.01 && bestCoupon.id !== selectedCoupon.id) { + Taro.showModal({ + title: '发现更优惠的优惠券', + content: `有更好的优惠券可用,额外节省¥${(bestDiscount - currentDiscount).toFixed(2)},是否更换?`, + success: (res) => { + if (res.confirm) { + setSelectedCoupon(bestCoupon) + Taro.showToast({ + title: '优惠券已更换', + icon: 'success' + }) + } + } + }) + } } } } @@ -123,9 +188,45 @@ const OrderConfirm = () => { const handleCouponSelect = (coupon: CouponCardProps) => { const total = getGoodsTotal() + // 🔍 详细日志记录,用于排查问题 + console.log('🎫 手动选择优惠券详细信息:', { + coupon: { + id: coupon.id, + title: coupon.title, + type: coupon.type, + amount: coupon.amount, + minAmount: coupon.minAmount, + status: coupon.status + }, + orderInfo: { + goodsPrice: goods?.price, + quantity: quantity, + total: total, + totalFixed: total.toFixed(2) + }, + validation: { + isUsable: isCouponUsable(coupon, total), + discount: calculateCouponDiscount(coupon, total), + reason: getCouponUnusableReason(coupon, total) + } + }) + // 检查是否可用 if (!isCouponUsable(coupon, total)) { const reason = getCouponUnusableReason(coupon, total) + + // 🚨 记录手动选择失败的详细信息 + console.error('🚨 手动选择优惠券失败:', { + reason, + coupon, + total, + minAmount: coupon.minAmount, + comparison: { + totalVsMinAmount: `${total} < ${coupon.minAmount}`, + result: total < (coupon.minAmount || 0) + } + }) + Taro.showToast({ title: reason || '优惠券不可用', icon: 'none' @@ -165,13 +266,61 @@ const OrderConfirm = () => { // 按优惠金额排序 const total = getGoodsTotal() const sortedCoupons = sortCoupons(transformedCoupons, total) + const usableCoupons = filterUsableCoupons(sortedCoupons, total) setAvailableCoupons(sortedCoupons) + // 🎯 智能推荐:自动应用最优惠的可用优惠券 + if (usableCoupons.length > 0 && !selectedCoupon) { + const bestCoupon = usableCoupons[0] // 已经按优惠金额排序,第一个就是最优的 + const discount = calculateCouponDiscount(bestCoupon, total) + + // 🔍 详细日志记录自动推荐的信息 + console.log('🤖 自动推荐优惠券详细信息:', { + coupon: { + id: bestCoupon.id, + title: bestCoupon.title, + type: bestCoupon.type, + amount: bestCoupon.amount, + minAmount: bestCoupon.minAmount, + status: bestCoupon.status + }, + orderInfo: { + goodsPrice: goods?.price, + quantity: quantity, + total: total, + totalFixed: total.toFixed(2) + }, + validation: { + isUsable: isCouponUsable(bestCoupon, total), + discount: discount, + reason: getCouponUnusableReason(bestCoupon, total) + } + }) + + if (discount > 0) { + setSelectedCoupon(bestCoupon) + + // 显示智能推荐提示 + Taro.showToast({ + title: `已为您推荐最优优惠券,可省¥${discount.toFixed(2)}`, + icon: 'success', + duration: 3000 + }) + } + } + + // 🔔 优惠券提示:如果有可用优惠券,显示提示 + if (usableCoupons.length > 0) { + console.log(`发现${usableCoupons.length}张可用优惠券,已为您推荐最优惠券`) + } + console.log('加载优惠券成功:', { originalData: res, transformedData: transformedCoupons, - sortedData: sortedCoupons + sortedData: sortedCoupons, + usableCoupons: usableCoupons, + recommendedCoupon: usableCoupons[0] || null }) } else { setAvailableCoupons([]) @@ -233,6 +382,53 @@ const OrderConfirm = () => { }) return; } + } else { + // 🔔 支付前最后一次检查:提醒用户是否有可用优惠券 + const total = getGoodsTotal() + const usableCoupons = filterUsableCoupons(availableCoupons, total) + + if (usableCoupons.length > 0) { + const bestCoupon = usableCoupons[0] + const discount = calculateCouponDiscount(bestCoupon, total) + + if (discount > 0) { + // 用模态框提醒用户 + const confirmResult = await new Promise((resolve) => { + Taro.showModal({ + title: '发现可用优惠券', + content: `您有优惠券可使用,可省¥${discount.toFixed(2)},是否使用?`, + success: (res) => resolve(res.confirm), + fail: () => resolve(false) + }) + }) + + if (confirmResult) { + setSelectedCoupon(bestCoupon) + // 🔄 使用优惠券后需要重新构建订单数据,这里直接递归调用支付函数 + // 但要确保传递最新的优惠券信息 + const currentPaymentType = payment.type === 0 ? PaymentType.BALANCE : PaymentType.WECHAT; + const updatedOrderData = buildSingleGoodsOrder( + goods.goodsId!, + quantity, + address.id, + { + comments: goods.name, + deliveryType: 0, + buyerRemarks: orderRemark, + couponId: parseInt(String(bestCoupon.id), 10) + } + ); + + console.log('🎯 使用推荐优惠券的订单数据:', updatedOrderData); + + // 执行支付 + await PaymentHandler.pay(updatedOrderData, currentPaymentType); + return; // 提前返回,避免重复执行支付 + } else { + // 用户选择不使用优惠券,继续支付 + } + } + } } // 构建订单数据 @@ -244,23 +440,37 @@ const OrderConfirm = () => { comments: goods.name, deliveryType: 0, buyerRemarks: orderRemark, - // 确保couponId是数字类型 - couponId: selectedCoupon ? Number(selectedCoupon.id) : undefined + // 🔧 确保 couponId 是正确的数字类型,且不传递 undefined + couponId: selectedCoupon ? parseInt(String(selectedCoupon.id), 10) : undefined } ); // 根据支付方式选择支付类型 const paymentType = payment.type === 0 ? PaymentType.BALANCE : PaymentType.WECHAT; - console.log('开始支付:', { + // 🔍 支付前的详细信息记录 + console.log('💰 开始支付 - 详细信息:', { orderData, paymentType, selectedCoupon: selectedCoupon ? { id: selectedCoupon.id, title: selectedCoupon.title, + type: selectedCoupon.type, + amount: selectedCoupon.amount, + minAmount: selectedCoupon.minAmount, discount: getCouponDiscount() } : null, - finalPrice: getFinalPrice() + priceCalculation: { + goodsPrice: goods?.price, + quantity: quantity, + goodsTotal: getGoodsTotal(), + couponDiscount: getCouponDiscount(), + finalPrice: getFinalPrice() + }, + couponValidation: selectedCoupon ? { + isUsable: isCouponUsable(selectedCoupon, getGoodsTotal()), + reason: getCouponUnusableReason(selectedCoupon, getGoodsTotal()) + } : null }); // 执行支付 - 移除这里的成功提示,让PaymentHandler统一处理 @@ -422,7 +632,7 @@ const OrderConfirm = () => { height: '80px', }} lazyLoad={false}/>
- + {goods.name} 80g/袋 @@ -474,7 +684,31 @@ const OrderConfirm = () => { {selectedCoupon ? `-¥${getCouponDiscount().toFixed(2)}` : '暂未使用'} - + {(() => { + const usableCoupons = filterUsableCoupons(availableCoupons, getGoodsTotal()) + if (usableCoupons.length > 0 && !selectedCoupon) { + return ( + + + {usableCoupons.length}张可用 + + + + ) + } else if (usableCoupons.length > 0) { + return ( + + + 已选择 + + + + ) + } else { + return + } + })() + } )} onClick={() => setCouponVisible(true)} diff --git a/src/shop/search/components/GoodsItem.tsx b/src/shop/search/components/GoodsItem.tsx index e7e22e7..ae7d29b 100644 --- a/src/shop/search/components/GoodsItem.tsx +++ b/src/shop/search/components/GoodsItem.tsx @@ -18,7 +18,7 @@ const GoodsItem = ({ goods }: GoodsItemProps) => { } return ( -
+ { height="180" onClick={goToDetail} /> -
-
-
{goods.name || goods.goodsName}
-
+ + + {goods.name || goods.goodsName} + {goods.comments || ''} 已售 {goods.sales || 0} -
-
-
+ + + {goods.price || '0.00'} -
-
-
+ + + -
-
+ 购买 -
-
-
-
-
-
+
+
+
+
+
+
) } diff --git a/src/user/address/add.tsx b/src/user/address/add.tsx index 014f765..9ec01f5 100644 --- a/src/user/address/add.tsx +++ b/src/user/address/add.tsx @@ -365,7 +365,15 @@ const AddUserAddress = () => { /> {/* 底部浮动按钮 */} - submitSucceed} /> + { + // 触发表单提交 + if (formRef.current) { + formRef.current.submit(); + } + }} + /> ); }; diff --git a/src/user/chat/conversation/index.config.ts b/src/user/chat/conversation/index.config.ts new file mode 100644 index 0000000..93dd2b9 --- /dev/null +++ b/src/user/chat/conversation/index.config.ts @@ -0,0 +1,3 @@ +export default definePageConfig({ + navigationBarTitleText: '站内消息' +}) diff --git a/src/user/chat/conversation/index.tsx b/src/user/chat/conversation/index.tsx new file mode 100644 index 0000000..90b3e6f --- /dev/null +++ b/src/user/chat/conversation/index.tsx @@ -0,0 +1,167 @@ +import {useState, useCallback, useEffect} from 'react' +import {View, Text} from '@tarojs/components' +import Taro from '@tarojs/taro' +import {Loading, InfiniteLoading, Empty, Space, Tag} from '@nutui/nutui-react-taro' +import {pageShopChatConversation} from "@/api/shop/shopChatConversation"; +import FixedButton from "@/components/FixedButton"; + +const Index = () => { + const [list, setList] = useState([]) + const [loading, setLoading] = useState(false) + const [page, setPage] = useState(1) + const [hasMore, setHasMore] = useState(true) + + // 获取消息数据 + const fetchMessageData = useCallback(async (resetPage = false, targetPage?: number, searchKeyword?: string) => { + setLoading(true); + try { + const currentPage = resetPage ? 1 : (targetPage || page); + + // 构建API参数,根据状态筛选 + const params: any = { + page: currentPage + }; + + // 添加搜索关键词 + if (searchKeyword && searchKeyword.trim()) { + params.keywords = searchKeyword.trim(); + } + + const res = await pageShopChatConversation(params); + + if (res?.list && res.list.length > 0) { + // 正确映射状态 + const mappedList = res.list.map(customer => ({ + ...customer + })); + + // 如果是重置页面或第一页,直接设置新数据;否则追加数据 + 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 fetchMessageData(false, nextPage); + } + + + // 获取列表数据(现在使用服务端搜索,不需要消息端过滤) + const getFilteredList = () => { + return list; + }; + + useEffect(() => { + // 初始化时加载数据 + fetchMessageData(true, 1, ''); + }, []); + + // 渲染消息项 + const renderMessageItem = (customer: any) => ( + + + + + + 关于XXXX的通知 + + 未读 + {/*已读*/} + + + {/*统一代码:{customer.dealerCode}*/} + + 创建时间:{customer.createTime} + + + + + + ); + + // 渲染消息列表 + const renderMessageList = () => { + const filteredList = getFilteredList(); + + return ( + + { + // 滚动事件处理 + }} + onScrollToUpper={() => { + // 滚动到顶部事件处理 + }} + loadingText={ + <> + 加载中... + + } + loadMoreText={ + filteredList.length === 0 ? ( + + ) : ( + + 没有更多了 + + ) + } + > + {loading && filteredList.length === 0 ? ( + + + 加载中... + + ) : ( + filteredList.map(renderMessageItem) + )} + + + ); + }; + + return ( + + {/* 消息列表 */} + {renderMessageList()} + + + ); +}; + +export default Index; diff --git a/src/user/chat/message/add.config.ts b/src/user/chat/message/add.config.ts new file mode 100644 index 0000000..f59b48a --- /dev/null +++ b/src/user/chat/message/add.config.ts @@ -0,0 +1,4 @@ +export default definePageConfig({ + navigationBarTitleText: '发送消息', + navigationBarTextStyle: 'black' +}) diff --git a/src/user/chat/message/add.tsx b/src/user/chat/message/add.tsx new file mode 100644 index 0000000..a455fd0 --- /dev/null +++ b/src/user/chat/message/add.tsx @@ -0,0 +1,135 @@ +import {useEffect, useState, useRef} from "react"; +import {useRouter} from '@tarojs/taro' +import {Loading, CellGroup, Input, Form, Cell, Avatar} from '@nutui/nutui-react-taro' +import {ArrowRight} from '@nutui/icons-react-taro' +import {View, Text} from '@tarojs/components' +import Taro from '@tarojs/taro' +import FixedButton from "@/components/FixedButton"; +import {addShopChatMessage} from "@/api/shop/shopChatMessage"; +import {ShopChatMessage} from "@/api/shop/shopChatMessage/model"; +import navTo from "@/utils/common"; +import {getUser} from "@/api/system/user"; +import {User} from "@/api/system/user/model"; + +const AddMessage = () => { + const {params} = useRouter(); + const [toUser, setToUser] = useState() + const [loading, setLoading] = useState(true) + const [FormData, _] = useState() + const formRef = useRef(null) + + // 判断是编辑还是新增模式 + const isEditMode = !!params.id + const toUserId = params.id ? Number(params.id) : undefined + + const reload = async () => { + if(toUserId){ + getUser(Number(toUserId)).then(data => { + setToUser(data) + }) + } + } + + // 提交表单 + const submitSucceed = async (values: any) => { + try { + // 准备提交的数据 + const submitData = { + ...values + }; + + console.log('提交数据:', submitData) + + // 参数校验 + if(!toUser){ + Taro.showToast({ + title: `请选择发送对象`, + icon: 'error' + }); + return false; + } + + // 判断内容是否为空 + if (!values.content) { + Taro.showToast({ + title: `请输入内容`, + icon: 'error' + }); + return false; + } + // 执行新增或更新操作 + await addShopChatMessage({ + toUserId: toUserId, + formUserId: Taro.getStorageSync('UserId'), + type: 'text', + content: values.content + }); + + Taro.showToast({ + title: `发送成功`, + icon: 'success' + }); + + setTimeout(() => { + Taro.navigateBack(); + }, 1000); + + } catch (error) { + console.error('发送失败:', error); + Taro.showToast({ + title: `发送失败`, + icon: 'error' + }); + } + } + + const submitFailed = (error: any) => { + console.log(error, 'err...') + } + + useEffect(() => { + reload().then(() => { + setLoading(false) + }) + }, [isEditMode]); + + if (loading) { + return 加载中 + } + + return ( + <> + + + + {toUser.alias || toUser.nickname} + {toUser.mobile} + +
+ ) : '选择发送对象'} extra={( + + )} + onClick={() => navTo(`/dealer/team/index`, true)}/> +
submitSucceed(values)} + onFinishFailed={(errors) => submitFailed(errors)} + > + + + + + +
+ + {/* 底部浮动按钮 */} + formRef.current?.submit()}/> + + ); +}; + +export default AddMessage; diff --git a/src/user/chat/message/detail.config.ts b/src/user/chat/message/detail.config.ts new file mode 100644 index 0000000..5072884 --- /dev/null +++ b/src/user/chat/message/detail.config.ts @@ -0,0 +1,4 @@ +export default definePageConfig({ + navigationBarTitleText: '查看消息', + navigationBarTextStyle: 'black' +}) diff --git a/src/user/chat/message/detail.tsx b/src/user/chat/message/detail.tsx new file mode 100644 index 0000000..a7b9c78 --- /dev/null +++ b/src/user/chat/message/detail.tsx @@ -0,0 +1,77 @@ +import {useEffect, useState} from "react"; +import {useRouter} from '@tarojs/taro' +import {CellGroup, Cell, Loading, Avatar} from '@nutui/nutui-react-taro' +import {View,Text} from '@tarojs/components' +import {ArrowRight} from '@nutui/icons-react-taro' +import {getShopChatMessage, updateShopChatMessage} from "@/api/shop/shopChatMessage"; +import {ShopChatMessage} from "@/api/shop/shopChatMessage/model"; +import navTo from "@/utils/common"; + +const AddMessageDetail = () => { + const {params} = useRouter(); + const [loading, setLoading] = useState(true) + const [item, setItem] = useState() + + const reload = () => { + const id = params.id ? Number(params.id) : undefined + if (id) { + getShopChatMessage(id).then(data => { + setItem(data) + setLoading(false) + updateShopChatMessage({ + ...data, + status: 1 + }).then(() => { + console.log('设为已读') + }) + }) + } + } + + useEffect(() => { + reload() + }, []); + + if (loading) { + return 加载中 + } + + return ( + <> + + + + {item.formUserAlias || item.formUserName} + {item.formUserPhone} + +
+ ) : '选择发送对象'} extra={( + + )} + onClick={() => navTo(`/dealer/team/index`, true)}/> + + + + + {/**/} + {/* {'消息内容:'}*/} + {/* {item?.content}*/} + {/* */} + {/*)} />*/} + + + {item?.content} + )} /> + + + ); +}; + +export default AddMessageDetail; diff --git a/src/user/chat/message/index.config.ts b/src/user/chat/message/index.config.ts new file mode 100644 index 0000000..8c6bf6a --- /dev/null +++ b/src/user/chat/message/index.config.ts @@ -0,0 +1,3 @@ +export default definePageConfig({ + navigationBarTitleText: '我的消息' +}) diff --git a/src/user/chat/message/index.tsx b/src/user/chat/message/index.tsx new file mode 100644 index 0000000..479e7a5 --- /dev/null +++ b/src/user/chat/message/index.tsx @@ -0,0 +1,179 @@ +import {useState, useCallback, useEffect} from 'react' +import {View, Text} from '@tarojs/components' +import Taro from '@tarojs/taro' +import {Loading, InfiniteLoading, Empty, Avatar, Badge} from '@nutui/nutui-react-taro' +import FixedButton from "@/components/FixedButton"; +import {ShopChatMessage} from "@/api/shop/shopChatMessage/model"; +import {pageShopChatMessage} from "@/api/shop/shopChatMessage"; +import navTo from "@/utils/common"; + +const MessageIndex = () => { + const [list, setList] = useState([]) + const [loading, setLoading] = useState(false) + const [page, setPage] = useState(1) + const [hasMore, setHasMore] = useState(true) + + // 获取消息数据 + const fetchMessageData = useCallback(async (resetPage = false, targetPage?: number, searchKeyword?: string) => { + setLoading(true); + try { + const currentPage = resetPage ? 1 : (targetPage || page); + + // 构建API参数,根据状态筛选 + const params: any = { + type: 'text', + page: currentPage, + toUserId: Taro.getStorageSync('UserId') + }; + + // 添加搜索关键词 + if (searchKeyword && searchKeyword.trim()) { + params.keywords = searchKeyword.trim(); + } + + const res = await pageShopChatMessage(params); + + if (res?.list && res.list.length > 0) { + // 正确映射状态 + const mappedList = res.list.map(customer => ({ + ...customer + })); + + // 如果是重置页面或第一页,直接设置新数据;否则追加数据 + 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 fetchMessageData(false, nextPage); + } + + + // 获取列表数据(现在使用服务端搜索,不需要消息端过滤) + const getFilteredList = () => { + return list; + }; + + useEffect(() => { + // 初始化时加载数据 + fetchMessageData(true, 1, ''); + }, []); + + // 渲染消息项 + const renderMessageItem = (item: any) => ( + + navTo(`/user/chat/message/detail?id=${item.id}`,true)}> + + + + + + + + + {item.formUserAlias || item.formUserName} + + {item.createTime} + + + + {item.content} + + + + + + + + ); + + // 渲染消息列表 + const renderMessageList = () => { + const filteredList = getFilteredList(); + + return ( + + { + // 滚动事件处理 + }} + onScrollToUpper={() => { + // 滚动到顶部事件处理 + }} + loadingText={ + <> + 加载中... + + } + loadMoreText={ + filteredList.length === 0 ? ( + + ) : ( + + 没有更多了 + + ) + } + > + {loading && filteredList.length === 0 ? ( + + + 加载中... + + ) : ( + filteredList.map(renderMessageItem) + )} + + + ); + }; + + return ( + + {/* 消息列表 */} + {renderMessageList()} + navTo(`/user/chat/message/add`,true)}/> + + ); +}; + +export default MessageIndex; diff --git a/src/user/order/components/OrderList.tsx b/src/user/order/components/OrderList.tsx index e00380c..1ccbac2 100644 --- a/src/user/order/components/OrderList.tsx +++ b/src/user/order/components/OrderList.tsx @@ -11,7 +11,7 @@ import {ShopOrderGoods} from "@/api/shop/shopOrderGoods/model"; import {copyText} from "@/utils/common"; import PaymentCountdown from "@/components/PaymentCountdown"; import {PaymentType} from "@/utils/payment"; -import {goTo, switchTab} from "@/utils/navigation"; +import {goTo} from "@/utils/navigation"; // 判断订单是否支付已过期 const isPaymentExpired = (createTime: string, timeoutHours: number = 24): boolean => { @@ -738,20 +738,20 @@ function OrderList(props: OrderListProps) { )} {/* 待发货状态:显示申请退款 */} - {item.payStatus && item.deliveryStatus === 10 && item.orderStatus !== 2 && item.orderStatus !== 4 && ( - - )} + {/*{item.payStatus && item.deliveryStatus === 10 && item.orderStatus !== 2 && item.orderStatus !== 4 && (*/} + {/* */} + {/*)}*/} {/* 待收货状态:显示查看物流和确认收货 */} {item.deliveryStatus === 20 && item.orderStatus !== 2 && ( - + {/**/} - - + {/**/} + {/**/} )} {/* 退款/售后状态:显示查看进度和撤销申请 */} {(item.orderStatus === 4 || item.orderStatus === 7) && ( - + {/**/} )} diff --git a/src/user/poster/poster.config.ts b/src/user/poster/poster.config.ts new file mode 100644 index 0000000..c61c176 --- /dev/null +++ b/src/user/poster/poster.config.ts @@ -0,0 +1,4 @@ +export default definePageConfig({ + navigationBarTitleText: '企业采购', + navigationBarTextStyle: 'black' +}) diff --git a/src/user/poster/poster.tsx b/src/user/poster/poster.tsx new file mode 100644 index 0000000..a95fee6 --- /dev/null +++ b/src/user/poster/poster.tsx @@ -0,0 +1,115 @@ +import { useEffect, useState, useRef } from 'react' +import { Image } from '@nutui/nutui-react-taro' +import { CmsAd } from "@/api/cms/cmsAd/model"; +import { getCmsAd } from "@/api/cms/cmsAd"; +import navTo from "@/utils/common"; + +const NaturalFullscreenBanner = () => { + const [bannerData, setBannerData] = useState(null) + const [isLoading, setIsLoading] = useState(true) + const containerRef = useRef(null) + const imageRef = useRef(null) + + // 加载图片数据 + const loadBannerData = () => { + setIsLoading(true) + getCmsAd(447) + .then(data => { + setBannerData(data) + setIsLoading(false) + }) + .catch(error => { + console.error('图片数据加载失败:', error) + setIsLoading(false) + }) + } + + // 处理图片加载完成后调整显示方式 + const handleImageLoad = () => { + if (imageRef.current && containerRef.current) { + // 获取图片原始宽高比 + const imgRatio = imageRef.current.naturalWidth / imageRef.current.naturalHeight; + // 获取容器宽高比 + const containerRatio = containerRef.current.offsetWidth / containerRef.current.offsetHeight; + + // 根据比例差异微调显示方式 + if (imgRatio > containerRatio) { + // 图片更宽,适当调整以显示更多垂直内容 + imageRef.current.style.objectPosition = 'center'; + } else { + // 图片更高,适当调整以显示更多水平内容 + imageRef.current.style.objectPosition = 'center'; + } + } + } + + // 设置全屏尺寸 + useEffect(() => { + const setFullscreenSize = () => { + if (containerRef.current) { + // 减去可能存在的导航栏高度,使显示更自然 + const windowHeight = window.innerHeight - 48; // 假设导航栏高度为48px + const windowWidth = window.innerWidth; + + containerRef.current.style.height = `${windowHeight}px`; + containerRef.current.style.width = `${windowWidth}px`; + } + }; + + // 初始化尺寸 + setFullscreenSize(); + + // 监听窗口大小变化 + const resizeHandler = () => setFullscreenSize(); + window.addEventListener('resize', resizeHandler); + return () => window.removeEventListener('resize', resizeHandler); + }, []); + + useEffect(() => { + loadBannerData() + }, []) + + if (isLoading) { + return ( +
+ 加载中... +
+ ) + } + + // 获取第一张图片,如果有 + const firstImage = bannerData?.imageList?.[0]; + + if (!firstImage) { + return ( +
+ 暂无图片数据 +
+ ) + } + + return ( +
+ firstImage.path && navTo(firstImage.path)} + lazyLoad={false} + alt="全屏 banner 图" + onLoad={handleImageLoad} + style={{ + width: '100%', + height: '100%', + // 优先保持比例,只裁剪必要部分 + objectFit: 'cover', + // 默认居中显示,保留图片主体内容 + objectPosition: 'center center' + }} + /> +
+ ) +} + +export default NaturalFullscreenBanner diff --git a/src/user/store/verification.config.ts b/src/user/store/verification.config.ts index 77afd68..729a6f6 100644 --- a/src/user/store/verification.config.ts +++ b/src/user/store/verification.config.ts @@ -1,4 +1,4 @@ -export default definePageConfig({ +export default { navigationBarTitleText: '门店核销', navigationBarTextStyle: 'black' -}) +} diff --git a/src/utils/common.ts b/src/utils/common.ts index 1137728..5c2b526 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -78,18 +78,6 @@ export function shareGoodsLink(goodsId: string | number) { copyText(shareUrl); } -/** - * 显示分享引导提示 - */ -export function showShareGuide() { - Taro.showModal({ - title: '分享提示', - content: '请点击右上角的"..."按钮,然后选择"转发"来分享给好友,或选择"分享到朋友圈"', - showCancel: false, - confirmText: '知道了' - }); -} - /** * 截取字符串,确保不超过指定的汉字长度 * @param text 原始文本 diff --git a/src/utils/couponUtils.ts b/src/utils/couponUtils.ts index 56fd9e3..b39c46c 100644 --- a/src/utils/couponUtils.ts +++ b/src/utils/couponUtils.ts @@ -25,7 +25,7 @@ export const transformCouponData = (coupon: ShopUserCoupon): CouponCardProps => const getTheme = (type?: number): CouponCardProps['theme'] => { switch (type) { case 10: return 'red' // 满减券-红色 - case 20: return 'orange' // 折扣券-橙色 + case 20: return 'orange' // 折扣券-橙色 case 30: return 'green' // 免费券-绿色 default: return 'blue' } @@ -53,7 +53,7 @@ export const transformCouponData = (coupon: ShopUserCoupon): CouponCardProps => * 计算优惠券折扣金额 */ export const calculateCouponDiscount = ( - coupon: CouponCardProps, + coupon: CouponCardProps, totalAmount: number ): number => { // 检查是否满足使用条件 @@ -82,7 +82,7 @@ export const calculateCouponDiscount = ( * 检查优惠券是否可用 */ export const isCouponUsable = ( - coupon: CouponCardProps, + coupon: CouponCardProps, totalAmount: number ): boolean => { // 状态检查 @@ -102,13 +102,13 @@ export const isCouponUsable = ( * 获取优惠券不可用原因 */ export const getCouponUnusableReason = ( - coupon: CouponCardProps, + coupon: CouponCardProps, totalAmount: number ): string => { if (coupon.status === 1) { return '优惠券已使用' } - + if (coupon.status === 2) { return '优惠券已过期' } @@ -151,30 +151,30 @@ export const formatCouponTitle = (coupon: CouponCardProps): string => { * 按照优惠金额从大到小排序,同等优惠金额按过期时间排序 */ export const sortCoupons = ( - coupons: CouponCardProps[], + coupons: CouponCardProps[], totalAmount: number ): CouponCardProps[] => { return [...coupons].sort((a, b) => { // 先按可用性排序 const aUsable = isCouponUsable(a, totalAmount) const bUsable = isCouponUsable(b, totalAmount) - + if (aUsable && !bUsable) return -1 if (!aUsable && bUsable) return 1 - + // 都可用或都不可用时,按优惠金额排序 const aDiscount = calculateCouponDiscount(a, totalAmount) const bDiscount = calculateCouponDiscount(b, totalAmount) - + if (aDiscount !== bDiscount) { return bDiscount - aDiscount // 优惠金额大的在前 } - + // 优惠金额相同时,按过期时间排序(即将过期的在前) if (a.endTime && b.endTime) { return new Date(a.endTime).getTime() - new Date(b.endTime).getTime() } - + return 0 }) } @@ -183,7 +183,7 @@ export const sortCoupons = ( * 过滤可用优惠券 */ export const filterUsableCoupons = ( - coupons: CouponCardProps[], + coupons: CouponCardProps[], totalAmount: number ): CouponCardProps[] => { return coupons.filter(coupon => isCouponUsable(coupon, totalAmount)) @@ -193,7 +193,7 @@ export const filterUsableCoupons = ( * 过滤不可用优惠券 */ export const filterUnusableCoupons = ( - coupons: CouponCardProps[], + coupons: CouponCardProps[], totalAmount: number ): CouponCardProps[] => { return coupons.filter(coupon => !isCouponUsable(coupon, totalAmount)) diff --git a/src/utils/invite.ts b/src/utils/invite.ts index f4c901d..34975d6 100644 --- a/src/utils/invite.ts +++ b/src/utils/invite.ts @@ -15,17 +15,22 @@ export interface InviteParams { */ export function parseInviteParams(options: any): InviteParams | null { try { - // 从 scene 参数中解析邀请信息 - if (options.scene) { - // 确保 scene 是字符串类型 - const sceneStr = typeof options.scene === 'string' ? options.scene : String(options.scene) - console.log('解析scene参数:', sceneStr) + // 优先从 query.scene 参数中解析邀请信息 + let sceneStr = null + if (options.query && options.query.scene) { + sceneStr = typeof options.query.scene === 'string' ? options.query.scene : String(options.query.scene) + } else if (options.scene) { + // 兼容直接从 scene 参数解析 + sceneStr = typeof options.scene === 'string' ? options.scene : String(options.scene) + } + // 从 scene 参数中解析邀请信息 + if (sceneStr) { // 处理 uid_xxx 格式的邀请码 if (sceneStr.startsWith('uid_')) { const inviterId = sceneStr.replace('uid_', '') + if (inviterId && !isNaN(parseInt(inviterId))) { - console.log('检测到uid格式邀请码:', inviterId) return { inviter: inviterId, source: 'qrcode', @@ -56,16 +61,27 @@ export function parseInviteParams(options: any): InviteParams | null { }) if (params.inviter) { - console.log('检测到传统格式邀请码:', params) return params } } - // 从 query 参数中解析邀请信息(兼容旧版本) - if (options.referrer) { - return { - inviter: options.referrer, - source: 'link' + // 从 query 参数中解析邀请信息(处理首页分享链接) + if (options.query) { + const query = options.query + if (query.inviter) { + return { + inviter: query.inviter, + source: query.source || 'share', + t: query.t + } + } + + // 兼容旧版本 + if (query.referrer) { + return { + inviter: query.referrer, + source: 'link' + } } } @@ -81,11 +97,12 @@ export function parseInviteParams(options: any): InviteParams | null { */ export function saveInviteParams(params: InviteParams) { try { - Taro.setStorageSync('invite_params', { + const saveData = { ...params, timestamp: Date.now() - }) - console.log('邀请参数已保存:', params) + } + + Taro.setStorageSync('invite_params', saveData) } catch (error) { console.error('保存邀请参数失败:', error) } @@ -97,6 +114,7 @@ export function saveInviteParams(params: InviteParams) { export function getStoredInviteParams(): InviteParams | null { try { const stored = Taro.getStorageSync('invite_params') + if (stored && stored.inviter) { // 检查是否过期(24小时) const now = Date.now() @@ -126,7 +144,6 @@ export function getStoredInviteParams(): InviteParams | null { export function clearInviteParams() { try { Taro.removeStorageSync('invite_params') - console.log('邀请参数已清除') } catch (error) { console.error('清除邀请参数失败:', error) } @@ -137,61 +154,67 @@ export function clearInviteParams() { */ export async function handleInviteRelation(userId: number): Promise { try { - console.log('开始处理邀请关系,当前用户ID:', userId) - const inviteParams = getStoredInviteParams() if (!inviteParams || !inviteParams.inviter) { - console.log('没有找到邀请参数,跳过邀请关系建立') return false } - console.log('找到邀请参数:', inviteParams) - const inviterId = parseInt(inviteParams.inviter) if (isNaN(inviterId) || inviterId === userId) { // 邀请人ID无效或自己邀请自己 - console.log('邀请人ID无效或自己邀请自己,清除邀请参数') clearInviteParams() return false } - console.log(`准备建立邀请关系: ${inviterId} -> ${userId}`) + // 防重复检查:检查是否已经处理过这个邀请关系 + const relationKey = `invite_relation_${inviterId}_${userId}` + const existingRelation = Taro.getStorageSync(relationKey) + + if (existingRelation) { + clearInviteParams() // 清除邀请参数 + return true // 返回true表示关系已存在 + } + + // 设置API调用超时 + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error('API调用超时')), 5000) + ); // 使用新的绑定推荐关系接口 - await bindRefereeRelation({ - refereeId: inviterId, + const apiPromise = bindRefereeRelation({ + dealerId: inviterId, userId: userId, source: inviteParams.source || 'qrcode', scene: inviteParams.source === 'qrcode' ? `uid_${inviterId}` : `inviter=${inviterId}&source=${inviteParams.source}&t=${inviteParams.t}` + }); + + // 等待API调用完成或超时 + await Promise.race([apiPromise, timeoutPromise]); + + // 标记邀请关系已处理(设置过期时间为7天) + Taro.setStorageSync(relationKey, { + inviterId, + userId, + timestamp: Date.now(), + source: inviteParams.source || 'qrcode' }) // 清除本地存储的邀请参数 clearInviteParams() - console.log(`邀请关系建立成功: ${inviterId} -> ${userId}`) - - // 显示成功提示 - setTimeout(() => { - Taro.showToast({ - title: '邀请关系建立成功', - icon: 'success', - duration: 2000 - }) - }, 500) - return true } catch (error) { console.error('建立邀请关系失败:', error) - // 显示错误提示 - setTimeout(() => { - Taro.showToast({ - title: '邀请关系建立失败', - icon: 'error', - duration: 2000 - }) - }, 500) + // 如果是网络错误或超时,不清除邀请参数,允许稍后重试 + const errorMessage = error instanceof Error ? error.message : String(error) + if (errorMessage.includes('超时') || errorMessage.includes('网络')) { + console.log('网络问题,保留邀请参数供稍后重试') + return false + } + // 其他错误(如业务逻辑错误),清除邀请参数 + clearInviteParams() return false } } @@ -275,22 +298,91 @@ export function trackInviteSource(source: string, inviterId?: number) { } } +/** + * 调试工具:打印所有邀请相关的存储信息 + */ +export function debugInviteInfo() { + try { + console.log('=== 邀请参数调试信息 ===') + + // 获取启动参数 + const launchOptions = Taro.getLaunchOptionsSync() + console.log('启动参数:', JSON.stringify(launchOptions, null, 2)) + + // 获取存储的邀请参数 + const storedParams = Taro.getStorageSync('invite_params') + console.log('存储的邀请参数:', JSON.stringify(storedParams, null, 2)) + + // 获取用户信息 + const userId = Taro.getStorageSync('UserId') + const userInfo = Taro.getStorageSync('userInfo') + console.log('用户ID:', userId) + console.log('用户信息:', JSON.stringify(userInfo, null, 2)) + + // 获取邀请统计 + const inviteTracks = Taro.getStorageSync('invite_tracks') + console.log('邀请统计:', JSON.stringify(inviteTracks, null, 2)) + + console.log('=== 调试信息结束 ===') + + return { + launchOptions, + storedParams, + userId, + userInfo, + inviteTracks + } + } catch (error) { + console.error('获取调试信息失败:', error) + return null + } +} + /** * 检查并处理当前用户的邀请关系 * 用于在用户登录后立即检查是否需要建立邀请关系 */ export async function checkAndHandleInviteRelation(): Promise { try { + // 清理过期的防重记录 + cleanExpiredInviteRelations() + // 获取当前用户信息 const userInfo = Taro.getStorageSync('userInfo') - if (!userInfo || !userInfo.userId) { + const userId = Taro.getStorageSync('UserId') + + const finalUserId = userId || userInfo?.userId + + if (!finalUserId) { console.log('用户未登录,无法处理邀请关系') return false } - return await handleInviteRelation(userInfo.userId) + console.log('使用用户ID处理邀请关系:', finalUserId) + + // 设置整体超时保护 + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error('邀请关系处理整体超时')), 6000) + ); + + const handlePromise = handleInviteRelation(parseInt(finalUserId)); + + return await Promise.race([handlePromise, timeoutPromise]); } catch (error) { console.error('检查邀请关系失败:', error) + + // 记录失败次数,避免无限重试 + const failKey = 'invite_handle_fail_count' + const failCount = Taro.getStorageSync(failKey) || 0 + + if (failCount >= 3) { + console.log('邀请关系处理失败次数过多,清除邀请参数') + clearInviteParams() + Taro.removeStorageSync(failKey) + } else { + Taro.setStorageSync(failKey, failCount + 1) + } + return false } } @@ -328,14 +420,39 @@ export async function manualHandleInviteRelation(userId: number): Promise { + if (key.startsWith('invite_relation_')) { + try { + const data = Taro.getStorageSync(key) + if (data && data.timestamp && (now - data.timestamp > expireTime)) { + Taro.removeStorageSync(key) + } + } catch (error) { + // 如果读取失败,直接删除 + Taro.removeStorageSync(key) + } + } + }) + } catch (error) { + console.error('清理过期邀请关系记录失败:', error) + } +} + /** * 直接绑定推荐关系 * 用于直接调用绑定推荐关系接口 */ export async function bindReferee(refereeId: number, userId?: number, source: string = 'qrcode'): Promise { try { - console.log('直接绑定推荐关系:', { refereeId, userId, source }) - // 如果没有传入userId,尝试从本地存储获取 let targetUserId = userId if (!targetUserId) { @@ -353,32 +470,15 @@ export async function bindReferee(refereeId: number, userId?: number, source: st } await bindRefereeRelation({ - refereeId: refereeId, + dealerId: refereeId, userId: targetUserId, source: source, scene: source === 'qrcode' ? `uid_${refereeId}` : undefined }) - console.log(`推荐关系绑定成功: ${refereeId} -> ${targetUserId}`) - - // 显示成功提示 - Taro.showToast({ - title: '推荐关系绑定成功', - icon: 'success', - duration: 2000 - }) - return true } catch (error: any) { console.error('绑定推荐关系失败:', error) - - // 显示错误提示 - Taro.showToast({ - title: error.message || '绑定推荐关系失败', - icon: 'error', - duration: 2000 - }) - return false } } diff --git a/src/utils/networkCheck.ts b/src/utils/networkCheck.ts new file mode 100644 index 0000000..d7eb3bc --- /dev/null +++ b/src/utils/networkCheck.ts @@ -0,0 +1,155 @@ +import Taro from '@tarojs/taro'; + +/** + * 网络连接检测工具 + */ +export class NetworkChecker { + /** + * 检查网络连接状态 + */ + static async checkNetworkStatus(): Promise<{ + isConnected: boolean; + networkType: string; + message: string; + }> { + try { + const networkInfo = await Taro.getNetworkType(); + const isConnected = networkInfo.networkType !== 'none'; + + return { + isConnected, + networkType: networkInfo.networkType, + message: isConnected + ? `网络连接正常 (${networkInfo.networkType})` + : '网络连接异常' + }; + } catch (error) { + console.error('检查网络状态失败:', error); + return { + isConnected: false, + networkType: 'unknown', + message: '无法检测网络状态' + }; + } + } + + /** + * 测试API连接 + */ + static async testAPIConnection(baseUrl: string): Promise<{ + success: boolean; + responseTime: number; + message: string; + }> { + const startTime = Date.now(); + + try { + const response = await Taro.request({ + url: `${baseUrl}/health`, + method: 'GET', + timeout: 5000 + }); + + const responseTime = Date.now() - startTime; + + return { + success: response.statusCode === 200, + responseTime, + message: `API连接${response.statusCode === 200 ? '正常' : '异常'} (${responseTime}ms)` + }; + } catch (error) { + const responseTime = Date.now() - startTime; + console.error('API连接测试失败:', error); + + return { + success: false, + responseTime, + message: `API连接失败 (${responseTime}ms): ${error}` + }; + } + } + + /** + * 综合网络诊断 + */ + static async diagnoseNetwork(baseUrl: string): Promise<{ + network: any; + api: any; + suggestions: string[]; + }> { + console.log('🔍 开始网络诊断...'); + + const network = await this.checkNetworkStatus(); + const api = await this.testAPIConnection(baseUrl); + + const suggestions: string[] = []; + + if (!network.isConnected) { + suggestions.push('请检查网络连接'); + suggestions.push('尝试切换网络环境(WiFi/移动数据)'); + } + + if (!api.success) { + suggestions.push('服务器可能暂时不可用'); + suggestions.push('请稍后重试'); + if (api.responseTime > 10000) { + suggestions.push('网络响应较慢,建议检查网络质量'); + } + } + + if (network.networkType === 'wifi') { + suggestions.push('WiFi连接正常,如仍有问题请检查路由器'); + } else if (network.networkType === '4g' || network.networkType === '5g') { + suggestions.push('移动网络连接,请确保有足够的流量'); + } + + console.log('📊 网络诊断结果:', { network, api, suggestions }); + + return { network, api, suggestions }; + } + + /** + * 显示网络诊断结果 + */ + static async showNetworkDiagnosis(baseUrl: string) { + Taro.showLoading({ title: '诊断网络中...', mask: true }); + + try { + const diagnosis = await this.diagnoseNetwork(baseUrl); + Taro.hideLoading(); + + const content = [ + `网络状态: ${diagnosis.network.message}`, + `API连接: ${diagnosis.api.message}`, + '', + '建议:', + ...diagnosis.suggestions.map(s => `• ${s}`) + ].join('\n'); + + Taro.showModal({ + title: '网络诊断结果', + content, + showCancel: false, + confirmText: '知道了' + }); + } catch (error) { + Taro.hideLoading(); + console.error('网络诊断失败:', error); + + Taro.showModal({ + title: '诊断失败', + content: '无法完成网络诊断,请检查网络连接后重试', + showCancel: false, + confirmText: '知道了' + }); + } + } +} + +/** + * 便捷方法 + */ +export const checkNetwork = () => NetworkChecker.checkNetworkStatus(); +export const testAPI = (baseUrl: string) => NetworkChecker.testAPIConnection(baseUrl); +export const diagnoseNetwork = (baseUrl: string) => NetworkChecker.diagnoseNetwork(baseUrl); +export const showNetworkDiagnosis = (baseUrl: string) => NetworkChecker.showNetworkDiagnosis(baseUrl); diff --git a/src/utils/test-invite.ts b/src/utils/test-invite.ts new file mode 100644 index 0000000..f338161 --- /dev/null +++ b/src/utils/test-invite.ts @@ -0,0 +1,166 @@ +/** + * 邀请参数解析测试工具 + */ + +import { parseInviteParams } from './invite' + +/** + * 测试不同格式的邀请参数解析 + */ +export function testInviteParamsParsing() { + console.log('=== 开始测试邀请参数解析 ===') + + // 测试用例1: uid_格式 + const testCase1 = { + scene: 'uid_33103', + path: 'pages/index/index' + } + console.log('测试用例1 - uid格式:') + console.log('输入:', testCase1) + const result1 = parseInviteParams(testCase1) + console.log('输出:', result1) + console.log('预期: { inviter: "33103", source: "qrcode", t: "..." }') + console.log('结果:', result1?.inviter === '33103' && result1?.source === 'qrcode' ? '✅ 通过' : '❌ 失败') + console.log('') + + // 测试用例2: 传统格式 + const testCase2 = { + scene: 'inviter=12345&source=share&t=1640995200000', + path: 'pages/index/index' + } + console.log('测试用例2 - 传统格式:') + console.log('输入:', testCase2) + const result2 = parseInviteParams(testCase2) + console.log('输出:', result2) + console.log('预期: { inviter: "12345", source: "share", t: "1640995200000" }') + console.log('结果:', result2?.inviter === '12345' && result2?.source === 'share' ? '✅ 通过' : '❌ 失败') + console.log('') + + // 测试用例3: 数字类型的scene + const testCase3 = { + scene: 1047, // 数字类型 + path: 'pages/index/index' + } + console.log('测试用例3 - 数字类型scene:') + console.log('输入:', testCase3) + const result3 = parseInviteParams(testCase3) + console.log('输出:', result3) + console.log('预期: null (因为不是uid_格式)') + console.log('结果:', result3 === null ? '✅ 通过' : '❌ 失败') + console.log('') + + // 测试用例4: 空参数 + const testCase4 = {} + console.log('测试用例4 - 空参数:') + console.log('输入:', testCase4) + const result4 = parseInviteParams(testCase4) + console.log('输出:', result4) + console.log('预期: null') + console.log('结果:', result4 === null ? '✅ 通过' : '❌ 失败') + console.log('') + + // 测试用例5: 无效的uid格式 + const testCase5 = { + scene: 'uid_abc', + path: 'pages/index/index' + } + console.log('测试用例5 - 无效uid格式:') + console.log('输入:', testCase5) + const result5 = parseInviteParams(testCase5) + console.log('输出:', result5) + console.log('预期: null (因为abc不是数字)') + console.log('结果:', result5 === null ? '✅ 通过' : '❌ 失败') + console.log('') + + // 测试用例6: referrer参数 + const testCase6 = { + referrer: '99999', + path: 'pages/index/index' + } + console.log('测试用例6 - referrer参数:') + console.log('输入:', testCase6) + const result6 = parseInviteParams(testCase6) + console.log('输出:', result6) + console.log('预期: { inviter: "99999", source: "link" }') + console.log('结果:', result6?.inviter === '99999' && result6?.source === 'link' ? '✅ 通过' : '❌ 失败') + console.log('') + + console.log('=== 邀请参数解析测试完成 ===') +} + +/** + * 模拟小程序启动场景测试 + */ +export function simulateMiniProgramLaunch() { + console.log('=== 模拟小程序启动场景 ===') + + // 模拟通过小程序码启动 + const qrcodeOptions = { + path: 'pages/index/index', + scene: 'uid_33103', + shareTicket: undefined, + referrerInfo: {} + } + + console.log('模拟小程序码启动:') + console.log('启动参数:', qrcodeOptions) + + const qrcodeResult = parseInviteParams(qrcodeOptions) + console.log('解析结果:', qrcodeResult) + + if (qrcodeResult && qrcodeResult.inviter === '33103') { + console.log('✅ 小程序码邀请解析成功') + return qrcodeResult + } else { + console.log('❌ 小程序码邀请解析失败') + return null + } +} + +/** + * 验证邀请参数格式 + */ +export function validateInviteParams(params: any) { + console.log('=== 验证邀请参数格式 ===') + console.log('参数:', params) + + if (!params) { + console.log('❌ 参数为空') + return false + } + + if (!params.inviter) { + console.log('❌ 缺少inviter字段') + return false + } + + if (isNaN(parseInt(params.inviter))) { + console.log('❌ inviter不是有效数字') + return false + } + + if (!params.source) { + console.log('❌ 缺少source字段') + return false + } + + console.log('✅ 邀请参数格式验证通过') + return true +} + +/** + * 运行所有测试 + */ +export function runAllTests() { + console.log('🚀 开始运行所有邀请参数测试') + + testInviteParamsParsing() + + const simulationResult = simulateMiniProgramLaunch() + + if (simulationResult) { + validateInviteParams(simulationResult) + } + + console.log('🎉 所有测试完成') +} diff --git a/tailwind.config.js b/tailwind.config.js index cfc1db9..55ac80c 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -2,11 +2,12 @@ module.exports = { content: ['./src/**/*.{js,jsx,ts,tsx}'], darkMode: 'media', // or 'media' or 'class' + // 禁用 important 语法,微信小程序不支持 .\! 这样的选择器 theme: { extend: {}, }, variants: { - extend: {}, + // 禁用所有变体,避免生成微信小程序不支持的选择器 }, plugins: [], corePlugins: { @@ -18,5 +19,17 @@ module.exports = { divideColor: false, divideStyle: false, divideOpacity: false, + // 新增禁用项,解决微信小程序兼容性问题 + gap: true, // 禁用 gap 类,因为微信小程序不支持 gap 属性 + lineClamp: false, // 禁用 line-clamp 类,微信小程序不支持 + textIndent: false, // 禁用 text-indent + writingMode: false, // 禁用 writing-mode + hyphens: false, // 禁用 hyphens + // 禁用所有可能包含问题的变体 + visibility: false, // 禁用 visibility 相关类,避免生成 .\!visible + // 禁用伪类和交互变体,避免生成 .active\: 等选择器 + scale: false, // 禁用 scale 相关类,避免生成问题选择器 + transform: false, // 禁用 transform 相关类 + transitionProperty: false, // 禁用 transition 相关类 }, };