feat(invite): 新增邀请功能及二维码扫码登录支持
- 添加邀请记录、统计、来源统计、小程序码等数据模型 - 实现小程序码生成、邀请关系绑定、邀请场景处理等接口 - 新增扫码登录相关接口,支持生成二维码、检查状态、确认登录等操作 - 实现二维码内容解析和设备信息获取工具函数 - 添加礼品卡核销相关接口和解密工具函数 - 集成环境配置管理,支持开发、生产、测试环境切换 - 在过期时间页面集成登录二维码和核销二维码处理逻辑 - 添加邀请参数解析工具函数,支持从小程序启动参数中提取邀请信息
This commit is contained in:
10
config/app.ts
Normal file
10
config/app.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { API_BASE_URL } from './env'
|
||||
|
||||
// 租户ID - 请根据实际情况修改
|
||||
export const TenantId = '10519';
|
||||
// 接口地址 - 请根据实际情况修改
|
||||
export const BaseUrl = API_BASE_URL;
|
||||
// 当前版本
|
||||
export const Version = 'v3.0.8';
|
||||
// 版权信息
|
||||
export const Copyright = 'WebSoft Inc.';
|
||||
42
config/env.ts
Normal file
42
config/env.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
// 环境变量配置
|
||||
export const ENV_CONFIG = {
|
||||
// 开发环境
|
||||
development: {
|
||||
API_BASE_URL: 'https://cms-api.websoft.top/api',
|
||||
APP_NAME: '开发环境',
|
||||
DEBUG: 'true',
|
||||
},
|
||||
// 生产环境
|
||||
production: {
|
||||
API_BASE_URL: 'https://cms-api.websoft.top/api',
|
||||
APP_NAME: '时里院子市集',
|
||||
DEBUG: 'false',
|
||||
},
|
||||
// 测试环境
|
||||
test: {
|
||||
API_BASE_URL: 'https://cms-api.s209.websoft.top/api',
|
||||
APP_NAME: '测试环境',
|
||||
DEBUG: 'true',
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前环境配置
|
||||
export function getEnvConfig() {
|
||||
const env = process.env.NODE_ENV || 'development'
|
||||
if (env === 'production') {
|
||||
return ENV_CONFIG.production
|
||||
} else { // @ts-ignore
|
||||
if (env === 'test') {
|
||||
return ENV_CONFIG.test
|
||||
} else {
|
||||
return ENV_CONFIG.development
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出环境变量
|
||||
export const {
|
||||
API_BASE_URL,
|
||||
APP_NAME,
|
||||
DEBUG
|
||||
} = getEnvConfig()
|
||||
277
src/api/invite/index.ts
Normal file
277
src/api/invite/index.ts
Normal file
@@ -0,0 +1,277 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api/index';
|
||||
|
||||
/**
|
||||
* 小程序码生成参数
|
||||
*/
|
||||
export interface MiniProgramCodeParam {
|
||||
// 小程序页面路径
|
||||
page?: string;
|
||||
// 场景值,最大32个可见字符
|
||||
scene: string;
|
||||
// 二维码宽度,单位 px,最小 280px,最大 1280px
|
||||
width?: number;
|
||||
// 是否检查页面是否存在
|
||||
checkPath?: boolean;
|
||||
// 环境版本
|
||||
envVersion?: 'release' | 'trial' | 'develop';
|
||||
}
|
||||
|
||||
/**
|
||||
* 邀请关系参数
|
||||
*/
|
||||
export interface InviteRelationParam {
|
||||
// 邀请人ID
|
||||
inviterId: number;
|
||||
// 被邀请人ID
|
||||
inviteeId: number;
|
||||
// 邀请来源
|
||||
source: string;
|
||||
// 场景值
|
||||
scene?: string;
|
||||
// 邀请时间
|
||||
inviteTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定推荐关系参数
|
||||
*/
|
||||
export interface BindRefereeParam {
|
||||
// 推荐人ID
|
||||
dealerId: number;
|
||||
// 被推荐人ID (可选,如果不传则使用当前登录用户)
|
||||
userId?: number;
|
||||
// 推荐来源
|
||||
source?: string;
|
||||
// 场景值
|
||||
scene?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 邀请统计数据
|
||||
*/
|
||||
export interface InviteStats {
|
||||
// 总邀请数
|
||||
totalInvites: number;
|
||||
// 成功注册数
|
||||
successfulRegistrations: number;
|
||||
// 转化率
|
||||
conversionRate: number;
|
||||
// 今日邀请数
|
||||
todayInvites: number;
|
||||
// 本月邀请数
|
||||
monthlyInvites: number;
|
||||
// 邀请来源统计
|
||||
sourceStats: InviteSourceStat[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 邀请来源统计
|
||||
*/
|
||||
export interface InviteSourceStat {
|
||||
source: string;
|
||||
count: number;
|
||||
successCount: number;
|
||||
conversionRate: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 邀请记录
|
||||
*/
|
||||
export interface InviteRecord {
|
||||
id?: number;
|
||||
inviterId?: number;
|
||||
inviteeId?: number;
|
||||
inviterName?: string;
|
||||
inviteeName?: string;
|
||||
source?: string;
|
||||
scene?: string;
|
||||
status?: 'pending' | 'registered' | 'activated';
|
||||
inviteTime?: string;
|
||||
registerTime?: string;
|
||||
activateTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 邀请记录查询参数
|
||||
*/
|
||||
export interface InviteRecordParam {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
inviterId?: number;
|
||||
status?: string;
|
||||
source?: string;
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成小程序码
|
||||
*/
|
||||
export async function generateMiniProgramCode(data: MiniProgramCodeParam) {
|
||||
try {
|
||||
const url = 'https://server.websoft.top/api/wx-login/getOrderQRCodeUnlimited/' + data.scene;
|
||||
// 由于接口直接返回图片buffer,我们直接构建完整的URL
|
||||
return `${url}`;
|
||||
} catch (error: any) {
|
||||
throw new Error(error.message || '生成小程序码失败');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成邀请小程序码
|
||||
*/
|
||||
export async function generateInviteCode(inviterId: number) {
|
||||
const scene = `uid_${inviterId}`;
|
||||
|
||||
return generateMiniProgramCode({
|
||||
page: 'pages/index/index',
|
||||
scene: scene,
|
||||
width: 180,
|
||||
checkPath: true,
|
||||
envVersion: 'trial'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 建立邀请关系 (旧接口,保留兼容性)
|
||||
*/
|
||||
export async function createInviteRelation(data: InviteRelationParam) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/invite/create-relation',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定推荐关系 (新接口)
|
||||
*/
|
||||
export async function bindRefereeRelation(data: BindRefereeParam) {
|
||||
try {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/shop/shop-dealer-referee',
|
||||
{
|
||||
dealerId: data.dealerId,
|
||||
userId: data.userId,
|
||||
source: data.source || 'qrcode',
|
||||
scene: data.scene
|
||||
}
|
||||
);
|
||||
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
|
||||
throw new Error(res.message || '绑定推荐关系失败');
|
||||
} catch (error: any) {
|
||||
console.error('绑定推荐关系API调用失败:', error);
|
||||
throw new Error(error.message || '绑定推荐关系失败');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理邀请场景值
|
||||
*/
|
||||
export async function processInviteScene(scene: string, userId: number) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/invite/process-scene',
|
||||
{ scene, userId }
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取邀请统计数据
|
||||
*/
|
||||
export async function getInviteStats(inviterId: number) {
|
||||
const res = await request.get<ApiResult<InviteStats>>(
|
||||
`/invite/stats/${inviterId}`
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询邀请记录
|
||||
*/
|
||||
export async function pageInviteRecords(params: InviteRecordParam) {
|
||||
const res = await request.get<ApiResult<PageResult<InviteRecord>>>(
|
||||
'/invite/records/page',
|
||||
params
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取我的邀请记录
|
||||
*/
|
||||
export async function getMyInviteRecords(params: InviteRecordParam) {
|
||||
const res = await request.get<ApiResult<PageResult<InviteRecord>>>(
|
||||
'/invite/my-records',
|
||||
params
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证邀请码有效性
|
||||
*/
|
||||
export async function validateInviteCode(scene: string) {
|
||||
const res = await request.post<ApiResult<{ valid: boolean; inviterId?: number; source?: string }>>(
|
||||
'/invite/validate-code',
|
||||
{ scene }
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新邀请状态
|
||||
*/
|
||||
export async function updateInviteStatus(inviteId: number, status: 'registered' | 'activated') {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
`/invite/update-status/${inviteId}`,
|
||||
{ status }
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取邀请排行榜
|
||||
*/
|
||||
export async function getInviteRanking(params?: { limit?: number; period?: 'day' | 'week' | 'month' }) {
|
||||
const res = await request.get<ApiResult<Array<{
|
||||
inviterId: number;
|
||||
inviterName: string;
|
||||
inviteCount: number;
|
||||
successCount: number;
|
||||
conversionRate: number;
|
||||
}>>>(
|
||||
'/invite/ranking',
|
||||
params
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
279
src/api/invite/model/index.ts
Normal file
279
src/api/invite/model/index.ts
Normal file
@@ -0,0 +1,279 @@
|
||||
import type { PageParam } from '@/api/index';
|
||||
|
||||
/**
|
||||
* 邀请记录表
|
||||
*/
|
||||
export interface InviteRecord {
|
||||
// 主键ID
|
||||
id?: number;
|
||||
// 邀请人ID
|
||||
inviterId?: number;
|
||||
// 被邀请人ID
|
||||
inviteeId?: number;
|
||||
// 邀请人姓名
|
||||
inviterName?: string;
|
||||
// 被邀请人姓名
|
||||
inviteeName?: string;
|
||||
// 邀请来源 (qrcode, link, share等)
|
||||
source?: string;
|
||||
// 场景值
|
||||
scene?: string;
|
||||
// 邀请状态: pending-待注册, registered-已注册, activated-已激活
|
||||
status?: 'pending' | 'registered' | 'activated';
|
||||
// 邀请时间
|
||||
inviteTime?: string;
|
||||
// 注册时间
|
||||
registerTime?: string;
|
||||
// 激活时间
|
||||
activateTime?: string;
|
||||
// 备注
|
||||
comments?: string;
|
||||
// 是否删除, 0否, 1是
|
||||
deleted?: number;
|
||||
// 租户id
|
||||
tenantId?: number;
|
||||
// 创建时间
|
||||
createTime?: string;
|
||||
// 修改时间
|
||||
updateTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 邀请统计表
|
||||
*/
|
||||
export interface InviteStats {
|
||||
// 主键ID
|
||||
id?: number;
|
||||
// 邀请人ID
|
||||
inviterId?: number;
|
||||
// 统计日期
|
||||
statDate?: string;
|
||||
// 总邀请数
|
||||
totalInvites?: number;
|
||||
// 成功注册数
|
||||
successfulRegistrations?: number;
|
||||
// 激活用户数
|
||||
activatedUsers?: number;
|
||||
// 转化率
|
||||
conversionRate?: number;
|
||||
// 今日邀请数
|
||||
todayInvites?: number;
|
||||
// 本周邀请数
|
||||
weeklyInvites?: number;
|
||||
// 本月邀请数
|
||||
monthlyInvites?: number;
|
||||
// 租户id
|
||||
tenantId?: number;
|
||||
// 创建时间
|
||||
createTime?: string;
|
||||
// 修改时间
|
||||
updateTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 邀请来源统计表
|
||||
*/
|
||||
export interface InviteSourceStats {
|
||||
// 主键ID
|
||||
id?: number;
|
||||
// 邀请人ID
|
||||
inviterId?: number;
|
||||
// 来源类型
|
||||
source?: string;
|
||||
// 来源名称
|
||||
sourceName?: string;
|
||||
// 邀请数量
|
||||
inviteCount?: number;
|
||||
// 成功数量
|
||||
successCount?: number;
|
||||
// 转化率
|
||||
conversionRate?: number;
|
||||
// 统计日期
|
||||
statDate?: string;
|
||||
// 租户id
|
||||
tenantId?: number;
|
||||
// 创建时间
|
||||
createTime?: string;
|
||||
// 修改时间
|
||||
updateTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 小程序码记录表
|
||||
*/
|
||||
export interface MiniProgramCode {
|
||||
// 主键ID
|
||||
id?: number;
|
||||
// 邀请人ID
|
||||
inviterId?: number;
|
||||
// 场景值
|
||||
scene?: string;
|
||||
// 小程序码URL
|
||||
codeUrl?: string;
|
||||
// 页面路径
|
||||
pagePath?: string;
|
||||
// 二维码宽度
|
||||
width?: number;
|
||||
// 环境版本
|
||||
envVersion?: string;
|
||||
// 过期时间
|
||||
expireTime?: string;
|
||||
// 使用次数
|
||||
useCount?: number;
|
||||
// 最后使用时间
|
||||
lastUseTime?: string;
|
||||
// 状态: active-有效, expired-过期, disabled-禁用
|
||||
status?: 'active' | 'expired' | 'disabled';
|
||||
// 租户id
|
||||
tenantId?: number;
|
||||
// 创建时间
|
||||
createTime?: string;
|
||||
// 修改时间
|
||||
updateTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 邀请记录搜索条件
|
||||
*/
|
||||
export interface InviteRecordParam extends PageParam {
|
||||
// 邀请人ID
|
||||
inviterId?: number;
|
||||
// 被邀请人ID
|
||||
inviteeId?: number;
|
||||
// 邀请状态
|
||||
status?: string;
|
||||
// 邀请来源
|
||||
source?: string;
|
||||
// 开始时间
|
||||
startTime?: string;
|
||||
// 结束时间
|
||||
endTime?: string;
|
||||
// 关键词搜索
|
||||
keywords?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 邀请统计搜索条件
|
||||
*/
|
||||
export interface InviteStatsParam extends PageParam {
|
||||
// 邀请人ID
|
||||
inviterId?: number;
|
||||
// 统计开始日期
|
||||
startDate?: string;
|
||||
// 统计结束日期
|
||||
endDate?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 邀请来源统计搜索条件
|
||||
*/
|
||||
export interface InviteSourceStatsParam extends PageParam {
|
||||
// 邀请人ID
|
||||
inviterId?: number;
|
||||
// 来源类型
|
||||
source?: string;
|
||||
// 统计开始日期
|
||||
startDate?: string;
|
||||
// 统计结束日期
|
||||
endDate?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 小程序码搜索条件
|
||||
*/
|
||||
export interface MiniProgramCodeParam extends PageParam {
|
||||
// 邀请人ID
|
||||
inviterId?: number;
|
||||
// 状态
|
||||
status?: string;
|
||||
// 场景值
|
||||
scene?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 邀请排行榜数据
|
||||
*/
|
||||
export interface InviteRanking {
|
||||
// 邀请人ID
|
||||
inviterId?: number;
|
||||
// 邀请人姓名
|
||||
inviterName?: string;
|
||||
// 邀请人头像
|
||||
inviterAvatar?: string;
|
||||
// 邀请数量
|
||||
inviteCount?: number;
|
||||
// 成功数量
|
||||
successCount?: number;
|
||||
// 转化率
|
||||
conversionRate?: number;
|
||||
// 排名
|
||||
rank?: number;
|
||||
// 奖励金额
|
||||
rewardAmount?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 邀请奖励配置
|
||||
*/
|
||||
export interface InviteRewardConfig {
|
||||
// 主键ID
|
||||
id?: number;
|
||||
// 奖励类型: register-注册奖励, activate-激活奖励, order-订单奖励
|
||||
rewardType?: string;
|
||||
// 奖励名称
|
||||
rewardName?: string;
|
||||
// 奖励金额
|
||||
rewardAmount?: number;
|
||||
// 奖励积分
|
||||
rewardPoints?: number;
|
||||
// 奖励优惠券ID
|
||||
couponId?: number;
|
||||
// 是否启用
|
||||
enabled?: boolean;
|
||||
// 生效时间
|
||||
effectTime?: string;
|
||||
// 失效时间
|
||||
expireTime?: string;
|
||||
// 备注
|
||||
comments?: string;
|
||||
// 租户id
|
||||
tenantId?: number;
|
||||
// 创建时间
|
||||
createTime?: string;
|
||||
// 修改时间
|
||||
updateTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 邀请奖励记录
|
||||
*/
|
||||
export interface InviteRewardRecord {
|
||||
// 主键ID
|
||||
id?: number;
|
||||
// 邀请记录ID
|
||||
inviteRecordId?: number;
|
||||
// 邀请人ID
|
||||
inviterId?: number;
|
||||
// 被邀请人ID
|
||||
inviteeId?: number;
|
||||
// 奖励类型
|
||||
rewardType?: string;
|
||||
// 奖励金额
|
||||
rewardAmount?: number;
|
||||
// 奖励积分
|
||||
rewardPoints?: number;
|
||||
// 优惠券ID
|
||||
couponId?: number;
|
||||
// 发放状态: pending-待发放, issued-已发放, failed-发放失败
|
||||
status?: 'pending' | 'issued' | 'failed';
|
||||
// 发放时间
|
||||
issueTime?: string;
|
||||
// 失败原因
|
||||
failReason?: string;
|
||||
// 租户id
|
||||
tenantId?: number;
|
||||
// 创建时间
|
||||
createTime?: string;
|
||||
// 修改时间
|
||||
updateTime?: string;
|
||||
}
|
||||
246
src/api/passport/qr-login/index.ts
Normal file
246
src/api/passport/qr-login/index.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult } from '@/api/index';
|
||||
import Taro from '@tarojs/taro';
|
||||
import {SERVER_API_URL} from "@/utils/server";
|
||||
import {getUserInfo} from "@/api/layout";
|
||||
|
||||
/**
|
||||
* 扫码登录相关接口
|
||||
*/
|
||||
|
||||
// 生成扫码token请求参数
|
||||
export interface GenerateQRTokenParam {
|
||||
// 客户端类型:web, app, wechat
|
||||
clientType?: string;
|
||||
// 设备信息
|
||||
deviceInfo?: string;
|
||||
// 过期时间(分钟)
|
||||
expireMinutes?: number;
|
||||
}
|
||||
|
||||
// 生成扫码token响应
|
||||
export interface GenerateQRTokenResult {
|
||||
// 扫码token
|
||||
token: string;
|
||||
// 二维码内容(通常是包含token的URL或JSON)
|
||||
qrContent: string;
|
||||
// 过期时间戳
|
||||
expireTime: number;
|
||||
// 二维码图片URL(可选)
|
||||
qrImageUrl?: string;
|
||||
}
|
||||
|
||||
// 扫码状态枚举
|
||||
export enum QRLoginStatus {
|
||||
PENDING = 'pending', // 等待扫码
|
||||
SCANNED = 'scanned', // 已扫码,等待确认
|
||||
CONFIRMED = 'confirmed', // 已确认登录
|
||||
EXPIRED = 'expired', // 已过期
|
||||
CANCELLED = 'cancelled' // 已取消
|
||||
}
|
||||
|
||||
// 检查扫码状态响应
|
||||
export interface QRLoginStatusResult {
|
||||
// 当前状态
|
||||
status: QRLoginStatus;
|
||||
// 状态描述
|
||||
message?: string;
|
||||
// 如果已确认登录,返回JWT token
|
||||
accessToken?: string;
|
||||
// 用户信息
|
||||
userInfo?: {
|
||||
userId: number;
|
||||
nickname?: string;
|
||||
avatar?: string;
|
||||
phone?: string;
|
||||
};
|
||||
// 剩余有效时间(秒)
|
||||
remainingTime?: number;
|
||||
}
|
||||
|
||||
// 确认登录请求参数
|
||||
export interface ConfirmLoginParam {
|
||||
// 扫码token
|
||||
token: string;
|
||||
// 用户ID
|
||||
userId: number;
|
||||
// 登录平台:web, app, wechat
|
||||
platform?: string;
|
||||
// 微信用户信息(当platform为wechat时)
|
||||
wechatInfo?: {
|
||||
openid?: string;
|
||||
unionid?: string;
|
||||
nickname?: string;
|
||||
avatar?: string;
|
||||
gender?: string;
|
||||
};
|
||||
// 设备信息
|
||||
deviceInfo?: string;
|
||||
}
|
||||
|
||||
// 确认登录响应
|
||||
export interface ConfirmLoginResult {
|
||||
// 是否成功
|
||||
success: boolean;
|
||||
// 消息
|
||||
message: string;
|
||||
// 登录用户信息
|
||||
userInfo?: {
|
||||
userId: number;
|
||||
nickname?: string;
|
||||
avatar?: string;
|
||||
phone?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成扫码登录token
|
||||
*/
|
||||
export async function generateQRToken(data?: GenerateQRTokenParam) {
|
||||
const res = await request.post<ApiResult<GenerateQRTokenResult>>(
|
||||
SERVER_API_URL + '/qr-login/generate',
|
||||
{
|
||||
clientType: 'wechat',
|
||||
expireMinutes: 5,
|
||||
...data
|
||||
}
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查扫码登录状态
|
||||
*/
|
||||
export async function checkQRLoginStatus(token: string) {
|
||||
const res = await request.get<ApiResult<QRLoginStatusResult>>(
|
||||
SERVER_API_URL + `/qr-login/status/${token}`
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认扫码登录(通用接口)
|
||||
*/
|
||||
export async function confirmQRLogin(data: ConfirmLoginParam) {
|
||||
const res = await request.post<ApiResult<ConfirmLoginResult>>(
|
||||
SERVER_API_URL + '/qr-login/confirm',
|
||||
data
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信小程序扫码登录确认(便捷接口)
|
||||
*/
|
||||
export async function confirmWechatQRLogin(token: string, userId: number) {
|
||||
try {
|
||||
// 获取微信用户信息
|
||||
const userInfo = await getUserInfo();
|
||||
|
||||
const data: ConfirmLoginParam = {
|
||||
token,
|
||||
userId,
|
||||
platform: 'wechat',
|
||||
wechatInfo: {
|
||||
nickname: userInfo?.nickname,
|
||||
avatar: userInfo?.avatar,
|
||||
gender: userInfo?.sex
|
||||
},
|
||||
deviceInfo: await getDeviceInfo()
|
||||
};
|
||||
|
||||
const res = await request.post<ApiResult<any>>(
|
||||
SERVER_API_URL + '/qr-login/confirm',
|
||||
data
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
} catch (error: any) {
|
||||
return Promise.reject(new Error(error.message || '确认登录失败'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备信息
|
||||
*/
|
||||
async function getDeviceInfo() {
|
||||
return new Promise<string>((resolve) => {
|
||||
Taro.getSystemInfo({
|
||||
success: (res) => {
|
||||
const deviceInfo = {
|
||||
platform: res.platform,
|
||||
system: res.system,
|
||||
version: res.version,
|
||||
model: res.model,
|
||||
brand: res.brand,
|
||||
screenWidth: res.screenWidth,
|
||||
screenHeight: res.screenHeight
|
||||
};
|
||||
resolve(JSON.stringify(deviceInfo));
|
||||
},
|
||||
fail: () => {
|
||||
resolve('unknown');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析二维码内容,提取token
|
||||
*/
|
||||
export function parseQRContent(qrContent: string): string | null {
|
||||
try {
|
||||
console.log('解析二维码内容1:', qrContent);
|
||||
|
||||
// 尝试解析JSON格式
|
||||
if (qrContent.startsWith('{')) {
|
||||
const parsed = JSON.parse(qrContent);
|
||||
return parsed.token || parsed.qrCodeKey || null;
|
||||
}
|
||||
|
||||
// 尝试解析URL格式
|
||||
if (qrContent.includes('http')) {
|
||||
const url = new URL(qrContent);
|
||||
// 支持多种参数名
|
||||
return url.searchParams.get('token') ||
|
||||
url.searchParams.get('qrCodeKey') ||
|
||||
url.searchParams.get('qr_code_key') ||
|
||||
null;
|
||||
}
|
||||
|
||||
// 尝试解析简单的key=value格式
|
||||
if (qrContent.includes('=')) {
|
||||
const params = new URLSearchParams(qrContent);
|
||||
return params.get('token') ||
|
||||
params.get('qrCodeKey') ||
|
||||
params.get('qr_code_key') ||
|
||||
null;
|
||||
}
|
||||
|
||||
// 如果是以qr-login:开头的格式
|
||||
if (qrContent.startsWith('qr-login:')) {
|
||||
return qrContent.replace('qr-login:', '');
|
||||
}
|
||||
|
||||
// 直接返回内容作为token(如果是32位以上的字符串)
|
||||
if (qrContent.length >= 32) {
|
||||
return qrContent;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('解析二维码内容失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
260
src/api/shop/shopGift/index.ts
Normal file
260
src/api/shop/shopGift/index.ts
Normal file
@@ -0,0 +1,260 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import {ShopGift, ShopGiftParam, GiftRedeemParam, GiftUseParam, QRCodeParam} from './model';
|
||||
|
||||
/**
|
||||
* 分页查询礼品卡
|
||||
*/
|
||||
export async function pageShopGift(params: ShopGiftParam) {
|
||||
const res = await request.get<ApiResult<PageResult<ShopGift>>>(
|
||||
'/shop/shop-gift/page',
|
||||
params
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询礼品卡列表
|
||||
*/
|
||||
export async function listShopGift(params?: ShopGiftParam) {
|
||||
const res = await request.get<ApiResult<ShopGift[]>>(
|
||||
'/shop/shop-gift',
|
||||
params
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加礼品卡
|
||||
*/
|
||||
export async function addShopGift(data: ShopGift) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/shop/shop-gift',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成礼品卡
|
||||
*/
|
||||
export async function makeShopGift(data: ShopGift) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/shop/shop-gift/make',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改礼品卡
|
||||
*/
|
||||
export async function updateShopGift(data: ShopGift) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
'/shop/shop-gift',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除礼品卡
|
||||
*/
|
||||
export async function removeShopGift(id?: number) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/shop/shop-gift/' + id
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除礼品卡
|
||||
*/
|
||||
export async function removeBatchShopGift(data: (number | undefined)[]) {
|
||||
const res = await request.del<ApiResult<unknown>>(
|
||||
'/shop/shop-gift/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据id查询礼品卡
|
||||
*/
|
||||
export async function getShopGift(id: number) {
|
||||
const res = await request.get<ApiResult<ShopGift>>(
|
||||
'/shop/shop-gift/' + id
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据code查询礼品卡
|
||||
* @param code
|
||||
*/
|
||||
export async function getShopGiftByCode(code: string) {
|
||||
const res = await request.get<ApiResult<ShopGift>>(
|
||||
'/shop/shop-gift/by-code/' + code
|
||||
);
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 兑换礼品卡
|
||||
*/
|
||||
export async function redeemGift(params: GiftRedeemParam) {
|
||||
const res = await request.post<ApiResult<ShopGift>>(
|
||||
'/shop/shop-gift/redeem',
|
||||
params
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用礼品卡
|
||||
*/
|
||||
export async function useGift(params: GiftUseParam) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/shop/shop-gift/use',
|
||||
params
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户的礼品卡列表
|
||||
*/
|
||||
export async function getUserGifts(params: ShopGiftParam) {
|
||||
const res = await request.get<ApiResult<PageResult<ShopGift>>>(
|
||||
'/shop/shop-gift/page',
|
||||
params
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证礼品卡兑换码
|
||||
*/
|
||||
export async function validateGiftCode(code: string) {
|
||||
const res = await request.get<ApiResult<ShopGift>>(
|
||||
`/shop/shop-gift/validate/${code}`
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
export async function exportShopGift(ids?: number[]) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/shop/shop-gift/export',
|
||||
ids
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成礼品卡核销码(可用)
|
||||
*/
|
||||
export async function generateVerificationCode(data: QRCodeParam) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/qr-code/create-encrypted-qr-code',
|
||||
data
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证核销码
|
||||
*/
|
||||
export async function verifyGiftCard(params: { verificationCode?: string; giftCode?: string }) {
|
||||
const res = await request.post<ApiResult<ShopGift>>(
|
||||
'/shop/shop-gift/verify',
|
||||
params
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成礼品卡核销
|
||||
*/
|
||||
export async function completeVerification(params: {
|
||||
giftId: number;
|
||||
verificationCode: string;
|
||||
storeId?: number;
|
||||
storeName?: string;
|
||||
operatorId?: number;
|
||||
operatorName?: string;
|
||||
}) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/shop/shop-gift/complete-verification',
|
||||
params
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密二维码数据
|
||||
*/
|
||||
export async function decryptQrData(params: { token: string; encryptedData: string }) {
|
||||
const res = await request.post<ApiResult<string>>(
|
||||
'/qr-code/decrypt-qr-data',
|
||||
params
|
||||
);
|
||||
if (res.code === 0) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
127
src/api/shop/shopGift/model/index.ts
Normal file
127
src/api/shop/shopGift/model/index.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import type { PageParam } from '@/api';
|
||||
|
||||
/**
|
||||
* 礼品卡
|
||||
*/
|
||||
export interface ShopGift {
|
||||
// 礼品卡ID
|
||||
id?: number;
|
||||
// 礼品卡名称
|
||||
name?: string;
|
||||
// 礼品卡描述
|
||||
description?: string;
|
||||
// 礼品卡兑换码
|
||||
code?: string;
|
||||
// 关联商品ID
|
||||
goodsId?: number;
|
||||
// 商品名称
|
||||
goodsName?: string;
|
||||
// 商品图片
|
||||
goodsImage?: string;
|
||||
// 礼品卡面值
|
||||
faceValue?: string;
|
||||
// 礼品卡类型 (10实物礼品卡 20虚拟礼品卡 30服务礼品卡)
|
||||
type?: number;
|
||||
// 领取时间
|
||||
takeTime?: string;
|
||||
// 过期时间
|
||||
expireTime?: string;
|
||||
// 有效期天数
|
||||
validDays?: number;
|
||||
// 操作人
|
||||
operatorUserId?: number;
|
||||
// 操作人名称
|
||||
operatorUserName?: string;
|
||||
// 是否展示
|
||||
isShow?: string;
|
||||
// 状态 (0未使用 1已使用 2已过期 3已失效)
|
||||
status?: number;
|
||||
// 备注
|
||||
comments?: string;
|
||||
// 使用说明
|
||||
instructions?: string;
|
||||
// 排序号
|
||||
sortNumber?: number;
|
||||
// 拥有者用户ID
|
||||
userId?: number;
|
||||
// 发放者用户ID
|
||||
issuerUserId?: number;
|
||||
// 是否删除, 0否, 1是
|
||||
deleted?: number;
|
||||
// 租户id
|
||||
tenantId?: number;
|
||||
// 创建时间
|
||||
createTime?: string;
|
||||
// 修改时间
|
||||
updateTime?: string;
|
||||
// 数量
|
||||
num?: number;
|
||||
// 已发放数量
|
||||
issuedCount?: number;
|
||||
// 总发放数量
|
||||
totalCount?: number;
|
||||
// 使用门店/地址
|
||||
useLocation?: string;
|
||||
// 客服联系方式
|
||||
contactInfo?: string;
|
||||
// 核销时间
|
||||
verificationTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 礼品卡搜索条件
|
||||
*/
|
||||
export interface ShopGiftParam extends PageParam {
|
||||
id?: number;
|
||||
keywords?: string;
|
||||
code?: string;
|
||||
// 礼品卡类型筛选
|
||||
type?: number;
|
||||
// 状态筛选 (0未使用 1已使用 2失效)
|
||||
status?: number;
|
||||
// 用户ID筛选
|
||||
userId?: number;
|
||||
// 商品ID筛选
|
||||
goodsId?: number;
|
||||
// 是否过期筛选
|
||||
isExpired?: boolean;
|
||||
// 排序字段
|
||||
sortBy?: 'createTime' | 'expireTime' | 'faceValue' | 'takeTime';
|
||||
// 排序方向
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
/**
|
||||
* 礼品卡兑换参数
|
||||
*/
|
||||
export interface GiftRedeemParam {
|
||||
// 兑换码
|
||||
code: string;
|
||||
// 用户ID
|
||||
userId?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 礼品卡使用参数
|
||||
*/
|
||||
export interface GiftUseParam {
|
||||
// 礼品卡ID
|
||||
giftId?: number;
|
||||
// 使用地址/门店
|
||||
useLocation?: string;
|
||||
// 使用备注
|
||||
useNote?: string;
|
||||
}
|
||||
|
||||
export interface QRCodeParam {
|
||||
// 二维码数据
|
||||
data?: string;
|
||||
// 二维码尺寸
|
||||
width?: number;
|
||||
// 二维码高度
|
||||
height?: number;
|
||||
// 二维码过期时间
|
||||
expireMinutes?: number;
|
||||
// 业务类型
|
||||
businessType?: string;
|
||||
}
|
||||
126
src/components/UnifiedQRButton.tsx
Normal file
126
src/components/UnifiedQRButton.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import React from 'react';
|
||||
import { Button } from '@nutui/nutui-react-taro';
|
||||
import { View } from '@tarojs/components';
|
||||
import { Scan } from '@nutui/icons-react-taro';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { useUnifiedQRScan, ScanType, type UnifiedScanResult } from '@/hooks/useUnifiedQRScan';
|
||||
|
||||
export interface UnifiedQRButtonProps {
|
||||
/** 按钮类型 */
|
||||
type?: 'primary' | 'success' | 'warning' | 'danger' | 'default';
|
||||
/** 按钮大小 */
|
||||
size?: 'large' | 'normal' | 'small';
|
||||
/** 按钮文本 */
|
||||
text?: string;
|
||||
/** 是否显示图标 */
|
||||
showIcon?: boolean;
|
||||
/** 自定义样式类名 */
|
||||
className?: string;
|
||||
/** 扫码成功回调 */
|
||||
onSuccess?: (result: UnifiedScanResult) => void;
|
||||
/** 扫码失败回调 */
|
||||
onError?: (error: string) => void;
|
||||
/** 是否使用页面模式(跳转到专门页面) */
|
||||
usePageMode?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一扫码按钮组件
|
||||
* 支持登录和核销两种类型的二维码扫描
|
||||
*/
|
||||
const UnifiedQRButton: React.FC<UnifiedQRButtonProps> = ({
|
||||
type = 'default',
|
||||
size = 'small',
|
||||
text = '扫码',
|
||||
showIcon = true,
|
||||
onSuccess,
|
||||
onError,
|
||||
usePageMode = false
|
||||
}) => {
|
||||
const { startScan, isLoading, canScan, state, result } = useUnifiedQRScan();
|
||||
console.log(result,'useUnifiedQRScan>>result')
|
||||
// 处理点击事件
|
||||
const handleClick = async () => {
|
||||
if (usePageMode) {
|
||||
// 跳转到专门的统一扫码页面
|
||||
if (canScan()) {
|
||||
Taro.navigateTo({
|
||||
url: '/passport/unified-qr/index'
|
||||
});
|
||||
} else {
|
||||
Taro.showToast({
|
||||
title: '请先登录小程序',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 直接执行扫码
|
||||
try {
|
||||
const scanResult = await startScan();
|
||||
if (scanResult) {
|
||||
onSuccess?.(scanResult);
|
||||
|
||||
// 根据扫码类型给出不同的后续提示
|
||||
if (scanResult.type === ScanType.VERIFICATION) {
|
||||
// 核销成功后可以继续扫码
|
||||
setTimeout(() => {
|
||||
Taro.showModal({
|
||||
title: '核销成功',
|
||||
content: '是否继续扫码核销其他礼品卡?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
handleClick(); // 递归调用继续扫码
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
onError?.(error.message || '扫码失败');
|
||||
}
|
||||
};
|
||||
|
||||
const disabled = !canScan() || isLoading;
|
||||
|
||||
// 根据当前状态动态显示文本
|
||||
const getButtonText = () => {
|
||||
if (isLoading) {
|
||||
switch (state) {
|
||||
case 'scanning':
|
||||
return '扫码中...';
|
||||
case 'processing':
|
||||
return '处理中...';
|
||||
default:
|
||||
return '扫码中...';
|
||||
}
|
||||
}
|
||||
|
||||
if (disabled && !canScan()) {
|
||||
return '请先登录';
|
||||
}
|
||||
|
||||
return text;
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
type={type}
|
||||
size={size}
|
||||
loading={isLoading}
|
||||
disabled={disabled}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<View className="flex items-center justify-center">
|
||||
{showIcon && !isLoading && (
|
||||
<Scan className="mr-1" />
|
||||
)}
|
||||
{getButtonText()}
|
||||
</View>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default UnifiedQRButton;
|
||||
@@ -126,6 +126,13 @@ const Query = () => {
|
||||
});
|
||||
return false
|
||||
}
|
||||
if (!FormData.vinCode) {
|
||||
Taro.showToast({
|
||||
title: '请填入车架号',
|
||||
icon: 'error'
|
||||
});
|
||||
return false
|
||||
}
|
||||
|
||||
// 安装车辆
|
||||
updateHjmCar({
|
||||
@@ -760,6 +767,9 @@ const Query = () => {
|
||||
<Cell className={'car-info-item-content'}>
|
||||
GPS编号:{FormData?.gpsNo}
|
||||
</Cell>
|
||||
<Cell className={'car-info-item-content'}>
|
||||
车架号:{FormData?.vinCode}
|
||||
</Cell>
|
||||
<Cell className={'car-info-item-content'}>
|
||||
电子围栏:{FormData.fenceName}
|
||||
</Cell>
|
||||
|
||||
66
src/hooks/useAdminMode.ts
Normal file
66
src/hooks/useAdminMode.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import Taro from '@tarojs/taro';
|
||||
|
||||
/**
|
||||
* 管理员模式Hook
|
||||
* 用于管理管理员用户的模式切换(普通用户模式 vs 管理员模式)
|
||||
*/
|
||||
export function useAdminMode() {
|
||||
const [isAdminMode, setIsAdminMode] = useState<boolean>(false);
|
||||
|
||||
// 从本地存储加载管理员模式状态
|
||||
useEffect(() => {
|
||||
try {
|
||||
const savedMode = Taro.getStorageSync('admin_mode');
|
||||
if (savedMode !== undefined) {
|
||||
setIsAdminMode(savedMode === 'true' || savedMode === true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load admin mode from storage:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 切换管理员模式
|
||||
const toggleAdminMode = useCallback(() => {
|
||||
const newMode = !isAdminMode;
|
||||
setIsAdminMode(newMode);
|
||||
|
||||
try {
|
||||
// 保存到本地存储
|
||||
Taro.setStorageSync('admin_mode', newMode);
|
||||
|
||||
// 显示切换提示
|
||||
Taro.showToast({
|
||||
title: newMode ? '已切换到管理员模式' : '已切换到普通用户模式',
|
||||
icon: 'success',
|
||||
duration: 1500
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to save admin mode to storage:', error);
|
||||
}
|
||||
}, [isAdminMode]);
|
||||
|
||||
// 设置管理员模式
|
||||
const setAdminMode = useCallback((mode: boolean) => {
|
||||
if (mode !== isAdminMode) {
|
||||
setIsAdminMode(mode);
|
||||
try {
|
||||
Taro.setStorageSync('admin_mode', mode);
|
||||
} catch (error) {
|
||||
console.error('Failed to save admin mode to storage:', error);
|
||||
}
|
||||
}
|
||||
}, [isAdminMode]);
|
||||
|
||||
// 重置为普通用户模式
|
||||
const resetToUserMode = useCallback(() => {
|
||||
setAdminMode(false);
|
||||
}, [setAdminMode]);
|
||||
|
||||
return {
|
||||
isAdminMode,
|
||||
toggleAdminMode,
|
||||
setAdminMode,
|
||||
resetToUserMode
|
||||
};
|
||||
}
|
||||
161
src/hooks/useCart.ts
Normal file
161
src/hooks/useCart.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import Taro from '@tarojs/taro';
|
||||
|
||||
// 购物车商品接口
|
||||
export interface CartItem {
|
||||
goodsId: number;
|
||||
name: string;
|
||||
price: string;
|
||||
image: string;
|
||||
quantity: number;
|
||||
addTime: number;
|
||||
skuId?: number;
|
||||
specInfo?: string;
|
||||
}
|
||||
|
||||
// 购物车Hook
|
||||
export const useCart = () => {
|
||||
const [cartItems, setCartItems] = useState<CartItem[]>([]);
|
||||
const [cartCount, setCartCount] = useState(0);
|
||||
|
||||
// 从本地存储加载购物车数据
|
||||
const loadCartFromStorage = () => {
|
||||
try {
|
||||
const cartData = Taro.getStorageSync('cart_items');
|
||||
if (cartData) {
|
||||
const items = JSON.parse(cartData) as CartItem[];
|
||||
setCartItems(items);
|
||||
updateCartCount(items);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载购物车数据失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 保存购物车数据到本地存储
|
||||
const saveCartToStorage = (items: CartItem[]) => {
|
||||
try {
|
||||
Taro.setStorageSync('cart_items', JSON.stringify(items));
|
||||
} catch (error) {
|
||||
console.error('保存购物车数据失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 更新购物车数量
|
||||
const updateCartCount = (items: CartItem[]) => {
|
||||
const count = items.reduce((total, item) => total + item.quantity, 0);
|
||||
setCartCount(count);
|
||||
};
|
||||
|
||||
// 添加商品到购物车
|
||||
const addToCart = (goods: {
|
||||
goodsId: number;
|
||||
name: string;
|
||||
price: string;
|
||||
image: string;
|
||||
skuId?: number;
|
||||
specInfo?: string;
|
||||
}, quantity: number = 1) => {
|
||||
const newItems = [...cartItems];
|
||||
// 如果有SKU,需要根据goodsId和skuId来判断是否为同一商品
|
||||
const existingItemIndex = newItems.findIndex(item =>
|
||||
item.goodsId === goods.goodsId &&
|
||||
(goods.skuId ? item.skuId === goods.skuId : !item.skuId)
|
||||
);
|
||||
|
||||
if (existingItemIndex >= 0) {
|
||||
// 如果商品已存在,增加数量
|
||||
newItems[existingItemIndex].quantity += quantity;
|
||||
} else {
|
||||
// 如果商品不存在,添加新商品
|
||||
const newItem: CartItem = {
|
||||
goodsId: goods.goodsId,
|
||||
name: goods.name,
|
||||
price: goods.price,
|
||||
image: goods.image,
|
||||
quantity,
|
||||
addTime: Date.now(),
|
||||
skuId: goods.skuId,
|
||||
specInfo: goods.specInfo
|
||||
};
|
||||
newItems.push(newItem);
|
||||
}
|
||||
|
||||
setCartItems(newItems);
|
||||
updateCartCount(newItems);
|
||||
saveCartToStorage(newItems);
|
||||
|
||||
// 显示成功提示
|
||||
Taro.showToast({
|
||||
title: '加入购物车成功',
|
||||
icon: 'success',
|
||||
duration: 1500
|
||||
});
|
||||
};
|
||||
|
||||
// 从购物车移除商品
|
||||
const removeFromCart = (goodsId: number) => {
|
||||
const newItems = cartItems.filter(item => item.goodsId !== goodsId);
|
||||
setCartItems(newItems);
|
||||
updateCartCount(newItems);
|
||||
saveCartToStorage(newItems);
|
||||
};
|
||||
|
||||
// 更新商品数量
|
||||
const updateQuantity = (goodsId: number, quantity: number) => {
|
||||
if (quantity <= 0) {
|
||||
removeFromCart(goodsId);
|
||||
return;
|
||||
}
|
||||
|
||||
const newItems = cartItems.map(item =>
|
||||
item.goodsId === goodsId ? { ...item, quantity } : item
|
||||
);
|
||||
setCartItems(newItems);
|
||||
updateCartCount(newItems);
|
||||
saveCartToStorage(newItems);
|
||||
};
|
||||
|
||||
// 清空购物车
|
||||
const clearCart = () => {
|
||||
setCartItems([]);
|
||||
setCartCount(0);
|
||||
Taro.removeStorageSync('cart_items');
|
||||
};
|
||||
|
||||
// 获取购物车总价
|
||||
const getTotalPrice = () => {
|
||||
return cartItems.reduce((total, item) => {
|
||||
return total + (parseFloat(item.price) * item.quantity);
|
||||
}, 0).toFixed(2);
|
||||
};
|
||||
|
||||
// 检查商品是否在购物车中
|
||||
const isInCart = (goodsId: number) => {
|
||||
return cartItems.some(item => item.goodsId === goodsId);
|
||||
};
|
||||
|
||||
// 获取商品在购物车中的数量
|
||||
const getItemQuantity = (goodsId: number) => {
|
||||
const item = cartItems.find(item => item.goodsId === goodsId);
|
||||
return item ? item.quantity : 0;
|
||||
};
|
||||
|
||||
// 初始化时加载购物车数据
|
||||
useEffect(() => {
|
||||
loadCartFromStorage();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
cartItems,
|
||||
cartCount,
|
||||
addToCart,
|
||||
removeFromCart,
|
||||
updateQuantity,
|
||||
clearCart,
|
||||
getTotalPrice,
|
||||
isInCart,
|
||||
getItemQuantity,
|
||||
loadCartFromStorage
|
||||
};
|
||||
};
|
||||
81
src/hooks/useDealerApply.ts
Normal file
81
src/hooks/useDealerApply.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import {useState, useEffect, useCallback} from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {getShopDealerApply} from '@/api/shop/shopDealerApply'
|
||||
import type {ShopDealerApply} from '@/api/shop/shopDealerApply/model'
|
||||
|
||||
// Hook 返回值接口
|
||||
export interface UseDealerApplyReturn {
|
||||
// 经销商用户信息
|
||||
dealerApply: ShopDealerApply | null
|
||||
// 加载状态
|
||||
loading: boolean
|
||||
// 错误信息
|
||||
error: string | null
|
||||
// 刷新数据
|
||||
refresh: () => Promise<void>
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 经销商用户 Hook - 简化版本
|
||||
* 只查询经销商用户信息和判断是否存在
|
||||
*/
|
||||
export const useDealerApply = (): UseDealerApplyReturn => {
|
||||
const [dealerApply, setDealerApply] = useState<ShopDealerApply | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const userId = Taro.getStorageSync('UserId');
|
||||
|
||||
// 获取经销商用户数据
|
||||
const fetchDealerData = useCallback(async () => {
|
||||
|
||||
if (!userId) {
|
||||
console.log('🔍 用户未登录,提前返回')
|
||||
setDealerApply(null)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
// 查询当前用户的经销商信息
|
||||
const dealer = await getShopDealerApply(userId)
|
||||
|
||||
if (dealer) {
|
||||
setDealerApply(dealer)
|
||||
} else {
|
||||
setDealerApply(null)
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : '获取经销商信息失败'
|
||||
setError(errorMessage)
|
||||
setDealerApply(null)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [userId])
|
||||
|
||||
// 刷新数据
|
||||
const refresh = useCallback(async () => {
|
||||
await fetchDealerData()
|
||||
}, [fetchDealerData])
|
||||
|
||||
// 初始化加载数据
|
||||
useEffect(() => {
|
||||
if (userId) {
|
||||
console.log('🔍 调用 fetchDealerData')
|
||||
fetchDealerData()
|
||||
} else {
|
||||
console.log('🔍 用户ID不存在,不调用 fetchDealerData')
|
||||
}
|
||||
}, [fetchDealerData, userId])
|
||||
|
||||
return {
|
||||
dealerApply,
|
||||
loading,
|
||||
error,
|
||||
refresh
|
||||
}
|
||||
}
|
||||
81
src/hooks/useDealerUser.ts
Normal file
81
src/hooks/useDealerUser.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import {useState, useEffect, useCallback} from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import {getShopDealerUser} from '@/api/shop/shopDealerUser'
|
||||
import type {ShopDealerUser} from '@/api/shop/shopDealerUser/model'
|
||||
|
||||
// Hook 返回值接口
|
||||
export interface UseDealerUserReturn {
|
||||
// 经销商用户信息
|
||||
dealerUser: ShopDealerUser | null
|
||||
// 加载状态
|
||||
loading: boolean
|
||||
// 错误信息
|
||||
error: string | null
|
||||
// 刷新数据
|
||||
refresh: () => Promise<void>
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 经销商用户 Hook - 简化版本
|
||||
* 只查询经销商用户信息和判断是否存在
|
||||
*/
|
||||
export const useDealerUser = (): UseDealerUserReturn => {
|
||||
const [dealerUser, setDealerUser] = useState<ShopDealerUser | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const userId = Taro.getStorageSync('UserId');
|
||||
|
||||
// 获取经销商用户数据
|
||||
const fetchDealerData = useCallback(async () => {
|
||||
|
||||
if (!userId) {
|
||||
console.log('🔍 用户未登录,提前返回')
|
||||
setDealerUser(null)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
// 查询当前用户的经销商信息
|
||||
const dealer = await getShopDealerUser(userId)
|
||||
|
||||
if (dealer) {
|
||||
setDealerUser(dealer)
|
||||
} else {
|
||||
setDealerUser(null)
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : '获取经销商信息失败'
|
||||
setError(errorMessage)
|
||||
setDealerUser(null)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [userId])
|
||||
|
||||
// 刷新数据
|
||||
const refresh = useCallback(async () => {
|
||||
await fetchDealerData()
|
||||
}, [fetchDealerData])
|
||||
|
||||
// 初始化加载数据
|
||||
useEffect(() => {
|
||||
if (userId) {
|
||||
console.log('🔍 调用 fetchDealerData')
|
||||
fetchDealerData()
|
||||
} else {
|
||||
console.log('🔍 用户ID不存在,不调用 fetchDealerData')
|
||||
}
|
||||
}, [fetchDealerData, userId])
|
||||
|
||||
return {
|
||||
dealerUser,
|
||||
loading,
|
||||
error,
|
||||
refresh
|
||||
}
|
||||
}
|
||||
120
src/hooks/useOrderStats.ts
Normal file
120
src/hooks/useOrderStats.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { UserOrderStats } from '@/api/user';
|
||||
import Taro from '@tarojs/taro';
|
||||
import {pageShopOrder} from "@/api/shop/shopOrder";
|
||||
|
||||
/**
|
||||
* 订单统计Hook
|
||||
* 用于管理用户订单各状态的数量统计
|
||||
*/
|
||||
export const useOrderStats = () => {
|
||||
const [orderStats, setOrderStats] = useState<UserOrderStats>({
|
||||
pending: 0, // 待付款
|
||||
paid: 0, // 待发货
|
||||
shipped: 0, // 待收货
|
||||
completed: 0, // 已完成
|
||||
refund: 0, // 退货/售后
|
||||
total: 0 // 总订单数
|
||||
});
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
/**
|
||||
* 获取订单统计数据
|
||||
*/
|
||||
const fetchOrderStats = useCallback(async (showToast = false) => {
|
||||
try {
|
||||
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})
|
||||
const shipped = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 3})
|
||||
const completed = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 5})
|
||||
const refund = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 6})
|
||||
const total = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId')})
|
||||
setOrderStats({
|
||||
pending: pending?.count || 0,
|
||||
paid: paid?.count || 0,
|
||||
shipped: shipped?.count || 0,
|
||||
completed: completed?.count || 0,
|
||||
refund: refund?.count || 0,
|
||||
total: total?.count || 0
|
||||
})
|
||||
|
||||
if (showToast) {
|
||||
Taro.showToast({
|
||||
title: '数据已更新',
|
||||
icon: 'success',
|
||||
duration: 1500
|
||||
});
|
||||
}
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.message || '获取订单统计失败';
|
||||
setError(errorMessage);
|
||||
|
||||
console.error('获取订单统计失败:', err);
|
||||
|
||||
if (showToast) {
|
||||
Taro.showToast({
|
||||
title: errorMessage,
|
||||
icon: 'error',
|
||||
duration: 2000
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 刷新订单统计数据
|
||||
*/
|
||||
const refreshOrderStats = useCallback(() => {
|
||||
return fetchOrderStats(true);
|
||||
}, [fetchOrderStats]);
|
||||
|
||||
/**
|
||||
* 获取指定状态的订单数量
|
||||
*/
|
||||
const getOrderCount = useCallback((status: keyof UserOrderStats) => {
|
||||
return orderStats[status] || 0;
|
||||
}, [orderStats]);
|
||||
|
||||
/**
|
||||
* 检查是否有待处理的订单
|
||||
*/
|
||||
const hasPendingOrders = useCallback(() => {
|
||||
return orderStats.pending > 0 || orderStats.paid > 0 || orderStats.shipped > 0;
|
||||
}, [orderStats]);
|
||||
|
||||
/**
|
||||
* 获取总的待处理订单数量
|
||||
*/
|
||||
const getTotalPendingCount = useCallback(() => {
|
||||
return orderStats.pending + orderStats.paid + orderStats.shipped;
|
||||
}, [orderStats]);
|
||||
|
||||
// 组件挂载时自动获取数据
|
||||
useEffect(() => {
|
||||
fetchOrderStats();
|
||||
}, [fetchOrderStats]);
|
||||
|
||||
return {
|
||||
orderStats,
|
||||
loading,
|
||||
error,
|
||||
fetchOrderStats,
|
||||
refreshOrderStats,
|
||||
getOrderCount,
|
||||
hasPendingOrders,
|
||||
getTotalPendingCount
|
||||
};
|
||||
};
|
||||
|
||||
export default useOrderStats;
|
||||
163
src/hooks/usePaymentCountdown.ts
Normal file
163
src/hooks/usePaymentCountdown.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import duration from 'dayjs/plugin/duration';
|
||||
|
||||
// 扩展dayjs支持duration
|
||||
dayjs.extend(duration);
|
||||
|
||||
export interface CountdownTime {
|
||||
hours: number;
|
||||
minutes: number;
|
||||
seconds: number;
|
||||
isExpired: boolean;
|
||||
totalMinutes: number; // 总剩余分钟数
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付倒计时Hook
|
||||
* @param createTime 订单创建时间
|
||||
* @param payStatus 支付状态
|
||||
* @param realTime 是否实时更新(详情页用true,列表页用false)
|
||||
* @param timeoutHours 超时小时数,默认24小时
|
||||
*/
|
||||
export const usePaymentCountdown = (
|
||||
createTime?: string,
|
||||
payStatus?: boolean,
|
||||
realTime: boolean = false,
|
||||
timeoutHours: number = 24
|
||||
): CountdownTime => {
|
||||
const [timeLeft, setTimeLeft] = useState<CountdownTime>({
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
isExpired: false,
|
||||
totalMinutes: 0
|
||||
});
|
||||
|
||||
// 计算剩余时间的函数
|
||||
const calculateTimeLeft = useMemo(() => {
|
||||
return (): CountdownTime => {
|
||||
if (!createTime || payStatus) {
|
||||
return {
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
isExpired: false,
|
||||
totalMinutes: 0
|
||||
};
|
||||
}
|
||||
|
||||
const createTimeObj = dayjs(createTime);
|
||||
const expireTime = createTimeObj.add(timeoutHours, 'hour');
|
||||
const now = dayjs();
|
||||
const diff = expireTime.diff(now);
|
||||
|
||||
if (diff <= 0) {
|
||||
return {
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
isExpired: true,
|
||||
totalMinutes: 0
|
||||
};
|
||||
}
|
||||
|
||||
const durationObj = dayjs.duration(diff);
|
||||
const hours = Math.floor(durationObj.asHours());
|
||||
const minutes = durationObj.minutes();
|
||||
const seconds = durationObj.seconds();
|
||||
const totalMinutes = Math.floor(durationObj.asMinutes());
|
||||
|
||||
return {
|
||||
hours,
|
||||
minutes,
|
||||
seconds,
|
||||
isExpired: false,
|
||||
totalMinutes
|
||||
};
|
||||
};
|
||||
}, [createTime, payStatus, timeoutHours]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!createTime || payStatus) {
|
||||
setTimeLeft({
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
isExpired: false,
|
||||
totalMinutes: 0
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 立即计算一次
|
||||
const initialTime = calculateTimeLeft();
|
||||
setTimeLeft(initialTime);
|
||||
|
||||
// 如果不需要实时更新,直接返回
|
||||
if (!realTime) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果需要实时更新,设置定时器
|
||||
const timer = setInterval(() => {
|
||||
const newTimeLeft = calculateTimeLeft();
|
||||
setTimeLeft(newTimeLeft);
|
||||
|
||||
// 如果已过期,清除定时器
|
||||
if (newTimeLeft.isExpired) {
|
||||
clearInterval(timer);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [createTime, payStatus, realTime, calculateTimeLeft]);
|
||||
|
||||
return timeLeft;
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化倒计时文本
|
||||
* @param timeLeft 倒计时时间对象
|
||||
* @param showSeconds 是否显示秒数
|
||||
*/
|
||||
export const formatCountdownText = (
|
||||
timeLeft: CountdownTime,
|
||||
showSeconds: boolean = false
|
||||
): string => {
|
||||
if (timeLeft.isExpired) {
|
||||
return '已过期';
|
||||
}
|
||||
|
||||
if (timeLeft.hours > 0) {
|
||||
if (showSeconds) {
|
||||
return `${timeLeft.hours}小时${timeLeft.minutes}分${timeLeft.seconds}秒`;
|
||||
} else {
|
||||
return `${timeLeft.hours}小时${timeLeft.minutes}分钟`;
|
||||
}
|
||||
} else if (timeLeft.minutes > 0) {
|
||||
if (showSeconds) {
|
||||
return `${timeLeft.minutes}分${timeLeft.seconds}秒`;
|
||||
} else {
|
||||
return `${timeLeft.minutes}分钟`;
|
||||
}
|
||||
} else {
|
||||
return `${timeLeft.seconds}秒`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 判断是否为紧急状态(剩余时间少于1小时)
|
||||
*/
|
||||
export const isUrgentCountdown = (timeLeft: CountdownTime): boolean => {
|
||||
return !timeLeft.isExpired && timeLeft.totalMinutes < 60;
|
||||
};
|
||||
|
||||
/**
|
||||
* 判断是否为非常紧急状态(剩余时间少于10分钟)
|
||||
*/
|
||||
export const isCriticalCountdown = (timeLeft: CountdownTime): boolean => {
|
||||
return !timeLeft.isExpired && timeLeft.totalMinutes < 10;
|
||||
};
|
||||
|
||||
export default usePaymentCountdown;
|
||||
228
src/hooks/useQRLogin.ts
Normal file
228
src/hooks/useQRLogin.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import Taro from '@tarojs/taro';
|
||||
import {
|
||||
confirmWechatQRLogin,
|
||||
parseQRContent,
|
||||
type ConfirmLoginResult
|
||||
} from '@/api/passport/qr-login';
|
||||
|
||||
/**
|
||||
* 扫码登录状态
|
||||
*/
|
||||
export enum ScanLoginState {
|
||||
IDLE = 'idle', // 空闲状态
|
||||
SCANNING = 'scanning', // 正在扫码
|
||||
CONFIRMING = 'confirming', // 正在确认登录
|
||||
SUCCESS = 'success', // 登录成功
|
||||
ERROR = 'error' // 登录失败
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫码登录Hook
|
||||
*/
|
||||
export function useQRLogin() {
|
||||
const [state, setState] = useState<ScanLoginState>(ScanLoginState.IDLE);
|
||||
const [error, setError] = useState<string>('');
|
||||
const [result, setResult] = useState<ConfirmLoginResult | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// 用于取消操作的引用
|
||||
const cancelRef = useRef<boolean>(false);
|
||||
|
||||
/**
|
||||
* 重置状态
|
||||
*/
|
||||
const reset = useCallback(() => {
|
||||
setState(ScanLoginState.IDLE);
|
||||
setError('');
|
||||
setResult(null);
|
||||
setIsLoading(false);
|
||||
cancelRef.current = false;
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 开始扫码登录
|
||||
*/
|
||||
const startScan = useCallback(async () => {
|
||||
try {
|
||||
reset();
|
||||
setState(ScanLoginState.SCANNING);
|
||||
|
||||
// 检查用户是否已登录
|
||||
const userId = Taro.getStorageSync('UserId');
|
||||
if (!userId) {
|
||||
throw new Error('请先登录小程序');
|
||||
}
|
||||
|
||||
// 调用扫码API
|
||||
const scanResult = await new Promise<string>((resolve, reject) => {
|
||||
Taro.scanCode({
|
||||
onlyFromCamera: true,
|
||||
scanType: ['qrCode'],
|
||||
success: (res) => {
|
||||
if (res.result) {
|
||||
resolve(res.result);
|
||||
} else {
|
||||
reject(new Error('扫码结果为空'));
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(new Error(err.errMsg || '扫码失败'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 检查是否被取消
|
||||
if (cancelRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 解析二维码内容
|
||||
const token = parseQRContent(scanResult);
|
||||
console.log('解析二维码内容2:',token)
|
||||
if (!token) {
|
||||
throw new Error('无效的登录二维码');
|
||||
}
|
||||
|
||||
// 确认登录
|
||||
setState(ScanLoginState.CONFIRMING);
|
||||
setIsLoading(true);
|
||||
|
||||
const confirmResult = await confirmWechatQRLogin(token, parseInt(userId));
|
||||
console.log(confirmResult,'confirmResult>>>>')
|
||||
if (cancelRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (confirmResult.success) {
|
||||
setState(ScanLoginState.SUCCESS);
|
||||
setResult(confirmResult);
|
||||
|
||||
// 显示成功提示
|
||||
Taro.showToast({
|
||||
title: '登录确认成功',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
} else {
|
||||
throw new Error(confirmResult.message || '登录确认失败');
|
||||
}
|
||||
|
||||
} catch (err: any) {
|
||||
if (!cancelRef.current) {
|
||||
setState(ScanLoginState.ERROR);
|
||||
const errorMessage = err.message || '扫码登录失败';
|
||||
setError(errorMessage);
|
||||
|
||||
// 显示错误提示
|
||||
Taro.showToast({
|
||||
title: errorMessage,
|
||||
icon: 'error',
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [reset]);
|
||||
|
||||
/**
|
||||
* 取消扫码登录
|
||||
*/
|
||||
const cancel = useCallback(() => {
|
||||
cancelRef.current = true;
|
||||
reset();
|
||||
}, [reset]);
|
||||
|
||||
/**
|
||||
* 处理扫码结果(用于已有扫码结果的情况)
|
||||
*/
|
||||
const handleScanResult = useCallback(async (qrContent: string) => {
|
||||
try {
|
||||
reset();
|
||||
setState(ScanLoginState.CONFIRMING);
|
||||
setIsLoading(true);
|
||||
|
||||
// 检查用户是否已登录
|
||||
const userId = Taro.getStorageSync('UserId');
|
||||
if (!userId) {
|
||||
throw new Error('请先登录小程序');
|
||||
}
|
||||
|
||||
// 解析二维码内容
|
||||
const token = parseQRContent(qrContent);
|
||||
if (!token) {
|
||||
throw new Error('无效的登录二维码');
|
||||
}
|
||||
|
||||
// 确认登录
|
||||
const confirmResult = await confirmWechatQRLogin(token, parseInt(userId));
|
||||
|
||||
if (confirmResult.success) {
|
||||
setState(ScanLoginState.SUCCESS);
|
||||
setResult(confirmResult);
|
||||
|
||||
// 显示成功提示
|
||||
Taro.showToast({
|
||||
title: '登录确认成功',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
} else {
|
||||
throw new Error(confirmResult.message || '登录确认失败');
|
||||
}
|
||||
|
||||
} catch (err: any) {
|
||||
setState(ScanLoginState.ERROR);
|
||||
const errorMessage = err.message || '登录确认失败';
|
||||
setError(errorMessage);
|
||||
|
||||
// 显示错误提示
|
||||
Taro.showToast({
|
||||
title: errorMessage,
|
||||
icon: 'error',
|
||||
duration: 3000
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [reset]);
|
||||
|
||||
/**
|
||||
* 检查是否可以进行扫码登录
|
||||
*/
|
||||
const canScan = useCallback(() => {
|
||||
const userId = Taro.getStorageSync('UserId');
|
||||
const accessToken = Taro.getStorageSync('access_token');
|
||||
return !!(userId && accessToken);
|
||||
}, []);
|
||||
|
||||
// 组件卸载时取消操作
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
cancelRef.current = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
// 状态
|
||||
state,
|
||||
error,
|
||||
result,
|
||||
isLoading,
|
||||
|
||||
// 方法
|
||||
startScan,
|
||||
cancel,
|
||||
reset,
|
||||
handleScanResult,
|
||||
canScan,
|
||||
|
||||
// 便捷状态判断
|
||||
isIdle: state === ScanLoginState.IDLE,
|
||||
isScanning: state === ScanLoginState.SCANNING,
|
||||
isConfirming: state === ScanLoginState.CONFIRMING,
|
||||
isSuccess: state === ScanLoginState.SUCCESS,
|
||||
isError: state === ScanLoginState.ERROR
|
||||
};
|
||||
}
|
||||
323
src/hooks/useShopInfo.ts
Normal file
323
src/hooks/useShopInfo.ts
Normal file
@@ -0,0 +1,323 @@
|
||||
import {useState, useEffect, useCallback} from 'react';
|
||||
import Taro from '@tarojs/taro';
|
||||
import {AppInfo} from '@/api/cms/cmsWebsite/model';
|
||||
import {getShopInfo} from '@/api/layout';
|
||||
|
||||
// 本地存储键名
|
||||
const SHOP_INFO_STORAGE_KEY = 'shop_info';
|
||||
const SHOP_INFO_CACHE_TIME_KEY = 'shop_info_cache_time';
|
||||
|
||||
// 缓存有效期(毫秒)- 默认30分钟
|
||||
const CACHE_DURATION = 30 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* 商店信息Hook
|
||||
* 提供商店信息的获取、缓存和管理功能
|
||||
*/
|
||||
export const useShopInfo = () => {
|
||||
const [shopInfo, setShopInfo] = useState<AppInfo | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// 从本地存储加载商店信息
|
||||
const loadShopInfoFromStorage = useCallback(() => {
|
||||
try {
|
||||
const cachedData = Taro.getStorageSync(SHOP_INFO_STORAGE_KEY);
|
||||
const cacheTime = Taro.getStorageSync(SHOP_INFO_CACHE_TIME_KEY);
|
||||
|
||||
if (cachedData && cacheTime) {
|
||||
const now = Date.now();
|
||||
const timeDiff = now - cacheTime;
|
||||
|
||||
// 检查缓存是否过期
|
||||
if (timeDiff < CACHE_DURATION) {
|
||||
const shopData = typeof cachedData === 'string' ? JSON.parse(cachedData) : cachedData;
|
||||
setShopInfo(shopData);
|
||||
setLoading(false);
|
||||
return true; // 返回true表示使用了缓存
|
||||
} else {
|
||||
// 缓存过期,清除旧数据
|
||||
Taro.removeStorageSync(SHOP_INFO_STORAGE_KEY);
|
||||
Taro.removeStorageSync(SHOP_INFO_CACHE_TIME_KEY);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载商店信息缓存失败:', error);
|
||||
}
|
||||
return false; // 返回false表示没有使用缓存
|
||||
}, []);
|
||||
|
||||
// 保存商店信息到本地存储
|
||||
const saveShopInfoToStorage = useCallback((data: AppInfo) => {
|
||||
try {
|
||||
Taro.setStorageSync(SHOP_INFO_STORAGE_KEY, data);
|
||||
Taro.setStorageSync(SHOP_INFO_CACHE_TIME_KEY, Date.now());
|
||||
} catch (error) {
|
||||
console.error('保存商店信息缓存失败:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 从服务器获取商店信息
|
||||
const fetchShopInfo = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const data = await getShopInfo();
|
||||
setShopInfo(data);
|
||||
|
||||
// 保存到本地存储
|
||||
saveShopInfoToStorage(data);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error('获取商店信息失败:', error);
|
||||
setError(errorMessage);
|
||||
|
||||
// 如果网络请求失败,尝试使用缓存数据(即使过期)
|
||||
const cachedData = Taro.getStorageSync(SHOP_INFO_STORAGE_KEY);
|
||||
if (cachedData) {
|
||||
const shopData = typeof cachedData === 'string' ? JSON.parse(cachedData) : cachedData;
|
||||
setShopInfo(shopData);
|
||||
console.warn('网络请求失败,使用缓存数据');
|
||||
}
|
||||
|
||||
return null;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [saveShopInfoToStorage]);
|
||||
|
||||
// 刷新商店信息
|
||||
const refreshShopInfo = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const data = await getShopInfo();
|
||||
setShopInfo(data);
|
||||
|
||||
// 保存到本地存储
|
||||
saveShopInfoToStorage(data);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error('刷新商店信息失败:', error);
|
||||
setError(errorMessage);
|
||||
return null;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [saveShopInfoToStorage]);
|
||||
|
||||
// 清除缓存
|
||||
const clearCache = useCallback(() => {
|
||||
try {
|
||||
Taro.removeStorageSync(SHOP_INFO_STORAGE_KEY);
|
||||
Taro.removeStorageSync(SHOP_INFO_CACHE_TIME_KEY);
|
||||
setShopInfo(null);
|
||||
setError(null);
|
||||
} catch (error) {
|
||||
console.error('清除商店信息缓存失败:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 获取应用名称
|
||||
const getAppName = useCallback(() => {
|
||||
return shopInfo?.appName || '商城';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取网站名称(兼容旧方法名)
|
||||
const getWebsiteName = useCallback(() => {
|
||||
return shopInfo?.appName || '商城';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取应用Logo
|
||||
const getAppLogo = useCallback(() => {
|
||||
return shopInfo?.logo || shopInfo?.icon || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取网站Logo(兼容旧方法名)
|
||||
const getWebsiteLogo = useCallback(() => {
|
||||
return shopInfo?.logo || shopInfo?.icon || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取应用图标
|
||||
const getAppIcon = useCallback(() => {
|
||||
return shopInfo?.icon || shopInfo?.logo || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取深色模式Logo(AppInfo中无此字段,使用普通Logo)
|
||||
const getDarkLogo = useCallback(() => {
|
||||
return shopInfo?.logo || shopInfo?.icon || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取应用域名
|
||||
const getDomain = useCallback(() => {
|
||||
return shopInfo?.domain || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取应用描述
|
||||
const getDescription = useCallback(() => {
|
||||
return shopInfo?.description || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取应用关键词
|
||||
const getKeywords = useCallback(() => {
|
||||
return shopInfo?.keywords || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取应用标题
|
||||
const getTitle = useCallback(() => {
|
||||
return shopInfo?.title || shopInfo?.appName || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取小程序二维码
|
||||
const getMpQrCode = useCallback(() => {
|
||||
return shopInfo?.mpQrCode || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取联系电话(AppInfo中无此字段,从config中获取)
|
||||
const getPhone = useCallback(() => {
|
||||
return (shopInfo?.config as any)?.phone || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取邮箱(AppInfo中无此字段,从config中获取)
|
||||
const getEmail = useCallback(() => {
|
||||
return (shopInfo?.config as any)?.email || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取地址(AppInfo中无此字段,从config中获取)
|
||||
const getAddress = useCallback(() => {
|
||||
return (shopInfo?.config as any)?.address || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取ICP备案号(AppInfo中无此字段,从config中获取)
|
||||
const getIcpNo = useCallback(() => {
|
||||
return (shopInfo?.config as any)?.icpNo || '';
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取应用状态
|
||||
const getStatus = useCallback(() => {
|
||||
return {
|
||||
running: shopInfo?.running || 0,
|
||||
statusText: shopInfo?.statusText || '',
|
||||
statusIcon: shopInfo?.statusIcon || '',
|
||||
expired: shopInfo?.expired || false,
|
||||
expiredDays: shopInfo?.expiredDays || 0,
|
||||
soon: shopInfo?.soon || 0
|
||||
};
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取应用配置
|
||||
const getConfig = useCallback(() => {
|
||||
return shopInfo?.config || {};
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取应用设置
|
||||
const getSetting = useCallback(() => {
|
||||
return shopInfo?.setting || {};
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取服务器时间
|
||||
const getServerTime = useCallback(() => {
|
||||
return shopInfo?.serverTime || {};
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取导航菜单
|
||||
const getNavigation = useCallback(() => {
|
||||
return {
|
||||
topNavs: shopInfo?.topNavs || [],
|
||||
bottomNavs: shopInfo?.bottomNavs || []
|
||||
};
|
||||
}, [shopInfo]);
|
||||
|
||||
// 检查是否支持搜索(从config中获取)
|
||||
const isSearchEnabled = useCallback(() => {
|
||||
return (shopInfo?.config as any)?.search === true;
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取应用版本信息
|
||||
const getVersionInfo = useCallback(() => {
|
||||
return {
|
||||
version: shopInfo?.version || 10,
|
||||
expirationTime: shopInfo?.expirationTime || '',
|
||||
expired: shopInfo?.expired || false,
|
||||
expiredDays: shopInfo?.expiredDays || 0,
|
||||
soon: shopInfo?.soon || 0
|
||||
};
|
||||
}, [shopInfo]);
|
||||
|
||||
// 检查应用是否过期
|
||||
const isExpired = useCallback(() => {
|
||||
return shopInfo?.expired === true;
|
||||
}, [shopInfo]);
|
||||
|
||||
// 获取过期天数
|
||||
const getExpiredDays = useCallback(() => {
|
||||
return shopInfo?.expiredDays || 0;
|
||||
}, [shopInfo]);
|
||||
|
||||
// 检查是否即将过期
|
||||
const isSoonExpired = useCallback(() => {
|
||||
return (shopInfo?.soon || 0) > 0;
|
||||
}, [shopInfo]);
|
||||
|
||||
// 初始化时加载商店信息
|
||||
useEffect(() => {
|
||||
const initShopInfo = async () => {
|
||||
// 先尝试从缓存加载
|
||||
const hasCache = loadShopInfoFromStorage();
|
||||
|
||||
// 如果没有缓存或需要刷新,则从服务器获取
|
||||
if (!hasCache) {
|
||||
await fetchShopInfo();
|
||||
}
|
||||
};
|
||||
|
||||
initShopInfo();
|
||||
}, []); // 空依赖数组,只在组件挂载时执行一次
|
||||
|
||||
return {
|
||||
// 状态
|
||||
shopInfo,
|
||||
loading,
|
||||
error,
|
||||
|
||||
// 方法
|
||||
fetchShopInfo,
|
||||
refreshShopInfo,
|
||||
clearCache,
|
||||
|
||||
// 新的工具方法(基于AppInfo字段)
|
||||
getAppName,
|
||||
getAppLogo,
|
||||
getAppIcon,
|
||||
getDescription,
|
||||
getKeywords,
|
||||
getTitle,
|
||||
getMpQrCode,
|
||||
getDomain,
|
||||
getConfig,
|
||||
getSetting,
|
||||
getServerTime,
|
||||
getNavigation,
|
||||
getStatus,
|
||||
getVersionInfo,
|
||||
isExpired,
|
||||
getExpiredDays,
|
||||
isSoonExpired,
|
||||
|
||||
// 兼容旧方法名
|
||||
getWebsiteName,
|
||||
getWebsiteLogo,
|
||||
getDarkLogo,
|
||||
getPhone,
|
||||
getEmail,
|
||||
getAddress,
|
||||
getIcpNo,
|
||||
isSearchEnabled
|
||||
};
|
||||
};
|
||||
95
src/hooks/useTheme.ts
Normal file
95
src/hooks/useTheme.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { gradientThemes, GradientTheme, gradientUtils } from '@/styles/gradients'
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
export interface UseThemeReturn {
|
||||
currentTheme: GradientTheme
|
||||
setTheme: (themeName: string) => void
|
||||
isAutoTheme: boolean
|
||||
refreshTheme: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* 主题管理Hook
|
||||
* 提供主题切换和状态管理功能
|
||||
*/
|
||||
export const useTheme = (): UseThemeReturn => {
|
||||
const [currentTheme, setCurrentTheme] = useState<GradientTheme>(gradientThemes[0])
|
||||
const [isAutoTheme, setIsAutoTheme] = useState<boolean>(true)
|
||||
|
||||
// 获取当前主题
|
||||
const getCurrentTheme = (): GradientTheme => {
|
||||
const savedTheme = Taro.getStorageSync('user_theme') || 'auto'
|
||||
|
||||
if (savedTheme === 'auto') {
|
||||
// 自动主题:根据用户ID生成
|
||||
const userId = Taro.getStorageSync('userId') || '1'
|
||||
return gradientUtils.getThemeByUserId(userId)
|
||||
} else {
|
||||
// 手动选择的主题
|
||||
return gradientThemes.find(t => t.name === savedTheme) || gradientThemes[0]
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化主题
|
||||
useEffect(() => {
|
||||
const savedTheme = Taro.getStorageSync('user_theme') || 'auto'
|
||||
setIsAutoTheme(savedTheme === 'auto')
|
||||
setCurrentTheme(getCurrentTheme())
|
||||
}, [])
|
||||
|
||||
// 设置主题
|
||||
const setTheme = (themeName: string) => {
|
||||
try {
|
||||
Taro.setStorageSync('user_theme', themeName)
|
||||
setIsAutoTheme(themeName === 'auto')
|
||||
setCurrentTheme(getCurrentTheme())
|
||||
} catch (error) {
|
||||
console.error('保存主题失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新主题(用于自动主题模式下用户信息变更时)
|
||||
const refreshTheme = () => {
|
||||
setCurrentTheme(getCurrentTheme())
|
||||
}
|
||||
|
||||
return {
|
||||
currentTheme,
|
||||
setTheme,
|
||||
isAutoTheme,
|
||||
refreshTheme
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前主题的样式对象
|
||||
* 用于直接应用到组件样式中
|
||||
*/
|
||||
export const useThemeStyles = () => {
|
||||
const { currentTheme } = useTheme()
|
||||
|
||||
return {
|
||||
// 主要背景样式
|
||||
primaryBackground: {
|
||||
background: currentTheme.background,
|
||||
color: currentTheme.textColor
|
||||
},
|
||||
|
||||
// 按钮样式
|
||||
primaryButton: {
|
||||
background: currentTheme.background,
|
||||
border: 'none',
|
||||
color: currentTheme.textColor
|
||||
},
|
||||
|
||||
// 强调色
|
||||
accentColor: currentTheme.primary,
|
||||
|
||||
// 文字颜色
|
||||
textColor: currentTheme.textColor,
|
||||
|
||||
// 完整主题对象
|
||||
theme: currentTheme
|
||||
}
|
||||
}
|
||||
331
src/hooks/useUnifiedQRScan.ts
Normal file
331
src/hooks/useUnifiedQRScan.ts
Normal file
@@ -0,0 +1,331 @@
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import Taro from '@tarojs/taro';
|
||||
import {
|
||||
confirmWechatQRLogin,
|
||||
parseQRContent
|
||||
} from '@/api/passport/qr-login';
|
||||
import { getShopGiftByCode, updateShopGift, decryptQrData } from "@/api/shop/shopGift";
|
||||
import { useUser } from "@/hooks/useUser";
|
||||
import { isValidJSON } from "@/utils/jsonUtils";
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
/**
|
||||
* 统一扫码状态
|
||||
*/
|
||||
export enum UnifiedScanState {
|
||||
IDLE = 'idle', // 空闲状态
|
||||
SCANNING = 'scanning', // 正在扫码
|
||||
PROCESSING = 'processing', // 正在处理
|
||||
SUCCESS = 'success', // 处理成功
|
||||
ERROR = 'error' // 处理失败
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫码类型
|
||||
*/
|
||||
export enum ScanType {
|
||||
LOGIN = 'login', // 登录二维码
|
||||
VERIFICATION = 'verification', // 核销二维码
|
||||
UNKNOWN = 'unknown' // 未知类型
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一扫码结果
|
||||
*/
|
||||
export interface UnifiedScanResult {
|
||||
type: ScanType;
|
||||
data: any;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一扫码Hook
|
||||
* 可以处理登录和核销两种类型的二维码
|
||||
*/
|
||||
export function useUnifiedQRScan() {
|
||||
const { isAdmin } = useUser();
|
||||
const [state, setState] = useState<UnifiedScanState>(UnifiedScanState.IDLE);
|
||||
const [error, setError] = useState<string>('');
|
||||
const [result, setResult] = useState<UnifiedScanResult | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [scanType, setScanType] = useState<ScanType>(ScanType.UNKNOWN);
|
||||
|
||||
// 用于取消操作的引用
|
||||
const cancelRef = useRef<boolean>(false);
|
||||
|
||||
/**
|
||||
* 重置状态
|
||||
*/
|
||||
const reset = useCallback(() => {
|
||||
setState(UnifiedScanState.IDLE);
|
||||
setError('');
|
||||
setResult(null);
|
||||
setIsLoading(false);
|
||||
setScanType(ScanType.UNKNOWN);
|
||||
cancelRef.current = false;
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 检测二维码类型
|
||||
*/
|
||||
const detectScanType = useCallback((scanResult: string): ScanType => {
|
||||
try {
|
||||
// 1. 检查是否为JSON格式(核销二维码)
|
||||
if (isValidJSON(scanResult)) {
|
||||
const json = JSON.parse(scanResult);
|
||||
if (json.businessType === 'gift' && json.token && json.data) {
|
||||
return ScanType.VERIFICATION;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 检查是否为登录二维码
|
||||
const loginToken = parseQRContent(scanResult);
|
||||
if (loginToken) {
|
||||
return ScanType.LOGIN;
|
||||
}
|
||||
|
||||
// 3. 检查是否为纯文本核销码(6位数字)
|
||||
if (/^\d{6}$/.test(scanResult.trim())) {
|
||||
return ScanType.VERIFICATION;
|
||||
}
|
||||
|
||||
return ScanType.UNKNOWN;
|
||||
} catch (error) {
|
||||
console.error('检测二维码类型失败:', error);
|
||||
return ScanType.UNKNOWN;
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 处理登录二维码
|
||||
*/
|
||||
const handleLoginQR = useCallback(async (scanResult: string): Promise<UnifiedScanResult> => {
|
||||
const userId = Taro.getStorageSync('UserId');
|
||||
if (!userId) {
|
||||
throw new Error('请先登录小程序');
|
||||
}
|
||||
|
||||
const token = parseQRContent(scanResult);
|
||||
if (!token) {
|
||||
throw new Error('无效的登录二维码');
|
||||
}
|
||||
|
||||
const confirmResult = await confirmWechatQRLogin(token, parseInt(userId));
|
||||
if (confirmResult.status === 'confirmed') {
|
||||
return {
|
||||
type: ScanType.LOGIN,
|
||||
data: confirmResult,
|
||||
message: '登录成功'
|
||||
};
|
||||
} else {
|
||||
throw new Error(confirmResult.message || '登录确认失败');
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 处理核销二维码
|
||||
*/
|
||||
const handleVerificationQR = useCallback(async (scanResult: string): Promise<UnifiedScanResult> => {
|
||||
if (!isAdmin()) {
|
||||
throw new Error('您没有核销权限');
|
||||
}
|
||||
|
||||
let code = '';
|
||||
|
||||
// 判断是否为加密的JSON格式
|
||||
if (isValidJSON(scanResult)) {
|
||||
const json = JSON.parse(scanResult);
|
||||
if (json.businessType === 'gift' && json.token && json.data) {
|
||||
// 解密获取核销码
|
||||
const decryptedData = await decryptQrData({
|
||||
token: json.token,
|
||||
encryptedData: json.data
|
||||
});
|
||||
|
||||
if (decryptedData) {
|
||||
code = decryptedData.toString();
|
||||
} else {
|
||||
throw new Error('解密失败');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 直接使用扫码结果作为核销码
|
||||
code = scanResult.trim();
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
throw new Error('无法获取有效的核销码');
|
||||
}
|
||||
|
||||
// 验证核销码
|
||||
const gift = await getShopGiftByCode(code);
|
||||
|
||||
if (!gift) {
|
||||
throw new Error('核销码无效');
|
||||
}
|
||||
|
||||
if (gift.status === 1) {
|
||||
throw new Error('此礼品码已使用');
|
||||
}
|
||||
|
||||
if (gift.status === 2) {
|
||||
throw new Error('此礼品码已失效');
|
||||
}
|
||||
|
||||
if (gift.userId === 0) {
|
||||
throw new Error('此礼品码未认领');
|
||||
}
|
||||
|
||||
// 执行核销
|
||||
await updateShopGift({
|
||||
...gift,
|
||||
status: 1,
|
||||
operatorUserId: Number(Taro.getStorageSync('UserId')) || 0,
|
||||
takeTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
verificationTime: dayjs().format('YYYY-MM-DD HH:mm:ss')
|
||||
});
|
||||
|
||||
return {
|
||||
type: ScanType.VERIFICATION,
|
||||
data: gift,
|
||||
message: '核销成功'
|
||||
};
|
||||
}, [isAdmin]);
|
||||
|
||||
/**
|
||||
* 开始扫码
|
||||
*/
|
||||
const startScan = useCallback(async (): Promise<UnifiedScanResult | null> => {
|
||||
try {
|
||||
reset();
|
||||
setState(UnifiedScanState.SCANNING);
|
||||
|
||||
// 调用扫码API
|
||||
const scanResult = await new Promise<string>((resolve, reject) => {
|
||||
Taro.scanCode({
|
||||
onlyFromCamera: true,
|
||||
scanType: ['qrCode'],
|
||||
success: (res) => {
|
||||
if (res.result) {
|
||||
resolve(res.result);
|
||||
} else {
|
||||
reject(new Error('扫码结果为空'));
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(new Error(err.errMsg || '扫码失败'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 检查是否被取消
|
||||
if (cancelRef.current) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检测二维码类型
|
||||
const type = detectScanType(scanResult);
|
||||
setScanType(type);
|
||||
|
||||
if (type === ScanType.UNKNOWN) {
|
||||
throw new Error('不支持的二维码类型');
|
||||
}
|
||||
|
||||
// 开始处理
|
||||
setState(UnifiedScanState.PROCESSING);
|
||||
setIsLoading(true);
|
||||
|
||||
let result: UnifiedScanResult;
|
||||
|
||||
switch (type) {
|
||||
case ScanType.LOGIN:
|
||||
result = await handleLoginQR(scanResult);
|
||||
break;
|
||||
case ScanType.VERIFICATION:
|
||||
result = await handleVerificationQR(scanResult);
|
||||
break;
|
||||
default:
|
||||
throw new Error('未知的扫码类型');
|
||||
}
|
||||
|
||||
if (cancelRef.current) {
|
||||
return null;
|
||||
}
|
||||
|
||||
setState(UnifiedScanState.SUCCESS);
|
||||
setResult(result);
|
||||
|
||||
// 显示成功提示
|
||||
Taro.showToast({
|
||||
title: result.message,
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
|
||||
return result;
|
||||
|
||||
} catch (err: any) {
|
||||
if (!cancelRef.current) {
|
||||
setState(UnifiedScanState.ERROR);
|
||||
const errorMessage = err.message || '处理失败';
|
||||
setError(errorMessage);
|
||||
|
||||
// 显示错误提示
|
||||
Taro.showToast({
|
||||
title: errorMessage,
|
||||
icon: 'error',
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
return null;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [reset, detectScanType, handleLoginQR, handleVerificationQR]);
|
||||
|
||||
/**
|
||||
* 取消扫码
|
||||
*/
|
||||
const cancel = useCallback(() => {
|
||||
cancelRef.current = true;
|
||||
reset();
|
||||
}, [reset]);
|
||||
|
||||
/**
|
||||
* 检查是否可以进行扫码
|
||||
*/
|
||||
const canScan = useCallback(() => {
|
||||
const userId = Taro.getStorageSync('UserId');
|
||||
const accessToken = Taro.getStorageSync('access_token');
|
||||
return !!(userId && accessToken);
|
||||
}, []);
|
||||
|
||||
// 组件卸载时取消操作
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
cancelRef.current = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
// 状态
|
||||
state,
|
||||
error,
|
||||
result,
|
||||
isLoading,
|
||||
scanType,
|
||||
|
||||
// 方法
|
||||
startScan,
|
||||
cancel,
|
||||
reset,
|
||||
canScan,
|
||||
|
||||
// 便捷状态判断
|
||||
isIdle: state === UnifiedScanState.IDLE,
|
||||
isScanning: state === UnifiedScanState.SCANNING,
|
||||
isProcessing: state === UnifiedScanState.PROCESSING,
|
||||
isSuccess: state === UnifiedScanState.SUCCESS,
|
||||
isError: state === UnifiedScanState.ERROR
|
||||
};
|
||||
}
|
||||
334
src/hooks/useUser.ts
Normal file
334
src/hooks/useUser.ts
Normal file
@@ -0,0 +1,334 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { User } from '@/api/system/user/model';
|
||||
import { getUserInfo, updateUserInfo, loginByOpenId } from '@/api/layout';
|
||||
import {getStoredInviteParams, handleInviteRelation} from '@/utils/invite';
|
||||
|
||||
// 用户Hook
|
||||
export const useUser = () => {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// 自动登录(通过OpenID)
|
||||
const autoLoginByOpenId = async () => {
|
||||
try {
|
||||
const res = await new Promise<any>((resolve, reject) => {
|
||||
Taro.login({
|
||||
success: (loginRes) => {
|
||||
loginByOpenId({
|
||||
code: loginRes.code,
|
||||
tenantId: 10519
|
||||
}).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 = async () => {
|
||||
try {
|
||||
const token = Taro.getStorageSync('access_token');
|
||||
const userData = Taro.getStorageSync('User');
|
||||
const userId = Taro.getStorageSync('UserId');
|
||||
const tenantId = Taro.getStorageSync('TenantId');
|
||||
|
||||
if (token && userData) {
|
||||
const userInfo = typeof userData === 'string' ? JSON.parse(userData) : userData;
|
||||
setUser(userInfo);
|
||||
setIsLoggedIn(true);
|
||||
} else if (token && userId) {
|
||||
// 如果有token和userId但没有完整用户信息,标记为已登录但需要获取用户信息
|
||||
setIsLoggedIn(true);
|
||||
setUser({ userId, tenantId } as User);
|
||||
} else {
|
||||
// 没有本地登录信息,尝试自动登录
|
||||
console.log('没有本地登录信息,尝试自动登录...');
|
||||
const autoLoginResult = await autoLoginByOpenId();
|
||||
if (!autoLoginResult) {
|
||||
setUser(null);
|
||||
setIsLoggedIn(false);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载用户数据失败:', error);
|
||||
setUser(null);
|
||||
setIsLoggedIn(false);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 保存用户数据到本地存储
|
||||
const saveUserToStorage = (token: string, userInfo: User) => {
|
||||
try {
|
||||
Taro.setStorageSync('access_token', token);
|
||||
Taro.setStorageSync('User', userInfo);
|
||||
|
||||
// 确保关键字段不为空时才保存,避免覆盖现有数据
|
||||
if (userInfo.userId) {
|
||||
Taro.setStorageSync('UserId', userInfo.userId);
|
||||
}
|
||||
if (userInfo.tenantId) {
|
||||
Taro.setStorageSync('TenantId', userInfo.tenantId);
|
||||
}
|
||||
if (userInfo.phone) {
|
||||
Taro.setStorageSync('Phone', userInfo.phone);
|
||||
}
|
||||
// 保存头像和昵称信息
|
||||
if (userInfo.avatar) {
|
||||
Taro.setStorageSync('Avatar', userInfo.avatar);
|
||||
}
|
||||
if (userInfo.nickname) {
|
||||
Taro.setStorageSync('Nickname', userInfo.nickname);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存用户数据失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 登录用户
|
||||
const loginUser = (token: string, userInfo: User) => {
|
||||
setUser(userInfo);
|
||||
setIsLoggedIn(true);
|
||||
saveUserToStorage(token, userInfo);
|
||||
};
|
||||
|
||||
// 退出登录
|
||||
const logoutUser = () => {
|
||||
setUser(null);
|
||||
setIsLoggedIn(false);
|
||||
|
||||
// 清除本地存储
|
||||
try {
|
||||
Taro.removeStorageSync('access_token');
|
||||
Taro.removeStorageSync('User');
|
||||
Taro.removeStorageSync('UserId');
|
||||
Taro.removeStorageSync('TenantId');
|
||||
Taro.removeStorageSync('Phone');
|
||||
Taro.removeStorageSync('userInfo');
|
||||
} catch (error) {
|
||||
console.error('清除用户数据失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 从服务器获取最新用户信息
|
||||
const fetchUserInfo = async () => {
|
||||
if (!isLoggedIn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const userInfo = await getUserInfo();
|
||||
setUser(userInfo);
|
||||
|
||||
// 更新本地存储
|
||||
const token = Taro.getStorageSync('access_token');
|
||||
if (token) {
|
||||
saveUserToStorage(token, userInfo);
|
||||
}
|
||||
|
||||
return userInfo;
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error);
|
||||
// 如果获取失败,可能是token过期,清除登录状态
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
if (errorMessage?.includes('401') || errorMessage?.includes('未授权')) {
|
||||
logoutUser();
|
||||
}
|
||||
return null;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 更新用户信息
|
||||
const updateUser = async (userData: Partial<User>) => {
|
||||
if (!user) {
|
||||
throw new Error('用户未登录');
|
||||
}
|
||||
|
||||
try {
|
||||
// 先获取最新的用户信息,确保我们有完整的数据
|
||||
const latestUserInfo = await getUserInfo();
|
||||
|
||||
// 合并最新的用户信息和要更新的数据
|
||||
const updatedUser = { ...latestUserInfo, ...userData };
|
||||
|
||||
// 调用API更新用户信息
|
||||
await updateUserInfo(updatedUser);
|
||||
|
||||
// 更新本地状态
|
||||
setUser(updatedUser);
|
||||
|
||||
// 更新本地存储
|
||||
const token = Taro.getStorageSync('access_token');
|
||||
if (token) {
|
||||
saveUserToStorage(token, updatedUser);
|
||||
}
|
||||
|
||||
Taro.showToast({
|
||||
title: '更新成功',
|
||||
icon: 'success',
|
||||
duration: 1500
|
||||
});
|
||||
|
||||
return updatedUser;
|
||||
} catch (error) {
|
||||
console.error('更新用户信息失败:', error);
|
||||
Taro.showToast({
|
||||
title: '更新失败',
|
||||
icon: 'error',
|
||||
duration: 1500
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// 检查是否有特定权限
|
||||
const hasPermission = (permission: string) => {
|
||||
if (!user || !user.authorities) {
|
||||
return false;
|
||||
}
|
||||
return user.authorities.some(auth => auth.authority === permission);
|
||||
};
|
||||
|
||||
// 检查是否有特定角色
|
||||
const hasRole = (roleCode: string) => {
|
||||
if (!user || !user.roles) {
|
||||
return false;
|
||||
}
|
||||
return user.roles.some(role => role.roleCode === roleCode);
|
||||
};
|
||||
|
||||
// 获取用户头像URL
|
||||
const getAvatarUrl = () => {
|
||||
return user?.avatar || user?.avatarUrl || '';
|
||||
};
|
||||
|
||||
const getUserId = () => {
|
||||
return user?.userId;
|
||||
};
|
||||
|
||||
// 获取用户显示名称
|
||||
const getDisplayName = () => {
|
||||
return user?.nickname || user?.realName || user?.username || '未登录';
|
||||
};
|
||||
|
||||
// 获取用户显示的角色(同步版本)
|
||||
const getRoleName = () => {
|
||||
if(hasRole('superAdmin')){
|
||||
return '超级管理员';
|
||||
}
|
||||
if(hasRole('admin')){
|
||||
return '管理员';
|
||||
}
|
||||
if(hasRole('staff')){
|
||||
return '员工';
|
||||
}
|
||||
if(hasRole('vip')){
|
||||
return 'VIP会员';
|
||||
}
|
||||
return '注册用户';
|
||||
}
|
||||
|
||||
// 检查用户是否已实名认证
|
||||
const isCertified = () => {
|
||||
return user?.certification === true;
|
||||
};
|
||||
|
||||
// 检查用户是否是管理员
|
||||
const isAdmin = () => {
|
||||
return user?.isAdmin === true;
|
||||
};
|
||||
|
||||
const isSuperAdmin = () => {
|
||||
return user?.isSuperAdmin === true;
|
||||
};
|
||||
|
||||
// 获取用户余额
|
||||
const getBalance = () => {
|
||||
return user?.balance || 0;
|
||||
};
|
||||
|
||||
// 获取用户积分
|
||||
const getPoints = () => {
|
||||
return user?.points || 0;
|
||||
};
|
||||
|
||||
// 初始化时加载用户数据
|
||||
useEffect(() => {
|
||||
loadUserFromStorage().catch(error => {
|
||||
console.error('初始化用户数据失败:', error);
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
// 状态
|
||||
user,
|
||||
isLoggedIn,
|
||||
loading,
|
||||
|
||||
// 方法
|
||||
loginUser,
|
||||
logoutUser,
|
||||
fetchUserInfo,
|
||||
updateUser,
|
||||
loadUserFromStorage,
|
||||
autoLoginByOpenId,
|
||||
|
||||
// 工具方法
|
||||
hasPermission,
|
||||
hasRole,
|
||||
getAvatarUrl,
|
||||
getDisplayName,
|
||||
getRoleName,
|
||||
isCertified,
|
||||
isAdmin,
|
||||
getBalance,
|
||||
getPoints,
|
||||
getUserId,
|
||||
isSuperAdmin
|
||||
};
|
||||
};
|
||||
136
src/hooks/useUserData.ts
Normal file
136
src/hooks/useUserData.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import {pageShopUserCoupon} from "@/api/shop/shopUserCoupon";
|
||||
import {pageShopGift} from "@/api/shop/shopGift";
|
||||
import {useUser} from "@/hooks/useUser";
|
||||
import Taro from '@tarojs/taro'
|
||||
import {getUserInfo} from "@/api/layout";
|
||||
|
||||
interface UserData {
|
||||
balance: number
|
||||
points: number
|
||||
coupons: number
|
||||
giftCards: number
|
||||
orders: {
|
||||
pending: number
|
||||
paid: number
|
||||
shipped: number
|
||||
completed: number
|
||||
refund: number
|
||||
}
|
||||
}
|
||||
|
||||
interface UseUserDataReturn {
|
||||
data: UserData | null
|
||||
loading: boolean
|
||||
error: string | null
|
||||
refresh: () => Promise<void>
|
||||
updateBalance: (newBalance: number) => void
|
||||
updatePoints: (newPoints: number) => void
|
||||
}
|
||||
|
||||
export const useUserData = (): UseUserDataReturn => {
|
||||
const [data, setData] = useState<UserData | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// 获取用户数据
|
||||
const fetchUserData = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
if(!Taro.getStorageSync('UserId')){
|
||||
return;
|
||||
}
|
||||
|
||||
// 并发请求所有数据
|
||||
const [userDataRes, couponsRes, giftCardsRes] = await Promise.all([
|
||||
getUserInfo(),
|
||||
pageShopUserCoupon({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), status: 0}),
|
||||
pageShopGift({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), status: 0})
|
||||
])
|
||||
|
||||
const newData: UserData = {
|
||||
balance: userDataRes?.balance || 0.00,
|
||||
points: userDataRes?.points || 0,
|
||||
coupons: couponsRes?.count || 0,
|
||||
giftCards: giftCardsRes?.count || 0,
|
||||
orders: {
|
||||
pending: 0,
|
||||
paid: 0,
|
||||
shipped: 0,
|
||||
completed: 0,
|
||||
refund: 0
|
||||
}
|
||||
}
|
||||
|
||||
setData(newData)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '获取用户数据失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 刷新数据
|
||||
const refresh = useCallback(async () => {
|
||||
await fetchUserData()
|
||||
}, [fetchUserData])
|
||||
|
||||
// 更新余额(本地更新,避免频繁请求)
|
||||
const updateBalance = useCallback((newBalance: number) => {
|
||||
setData(prev => prev ? { ...prev, balance: newBalance } : null)
|
||||
}, [])
|
||||
|
||||
// 更新积分
|
||||
const updatePoints = useCallback((newPoints: number) => {
|
||||
setData(prev => prev ? { ...prev, points: newPoints } : null)
|
||||
}, [])
|
||||
|
||||
// 初始化加载
|
||||
useEffect(() => {
|
||||
fetchUserData().then()
|
||||
}, [fetchUserData])
|
||||
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
error,
|
||||
refresh,
|
||||
updateBalance,
|
||||
updatePoints
|
||||
}
|
||||
}
|
||||
|
||||
// 轻量级版本 - 只获取基础数据
|
||||
export const useUserBasicData = () => {
|
||||
const {user} = useUser()
|
||||
const [balance, setBalance] = useState<number>(0)
|
||||
const [points, setPoints] = useState<number>(0)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const fetchBasicData = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
setBalance(user?.balance || 0)
|
||||
setPoints(user?.points || 0)
|
||||
} catch (error) {
|
||||
console.error('获取基础数据失败:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
fetchBasicData().then()
|
||||
}, [fetchBasicData])
|
||||
|
||||
return {
|
||||
balance,
|
||||
points,
|
||||
loading,
|
||||
refresh: fetchBasicData,
|
||||
updateBalance: setBalance,
|
||||
updatePoints: setPoints
|
||||
}
|
||||
}
|
||||
@@ -5,17 +5,177 @@ import {Target, Scan, Truck} from '@nutui/icons-react-taro'
|
||||
import {getUserInfo} from "@/api/layout";
|
||||
import navTo from "@/utils/common";
|
||||
import {pageHjmCar} from "@/api/hjm/hjmCar";
|
||||
import { ScanType } from '@/hooks/useUnifiedQRScan';
|
||||
import { isValidJSON } from '@/utils/jsonUtils';
|
||||
import { parseQRContent, confirmWechatQRLogin } from '@/api/passport/qr-login';
|
||||
import { getShopGiftByCode, updateShopGift, decryptQrData } from "@/api/shop/shopGift";
|
||||
import { useUser } from '@/hooks/useUser';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const ExpirationTime = () => {
|
||||
const [isAdmin, setIsAdmin] = useState<boolean>(false)
|
||||
const [roleName, setRoleName] = useState<string>()
|
||||
const { isAdmin: isUserAdmin } = useUser();
|
||||
|
||||
// 检测二维码类型
|
||||
const detectScanType = (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 = async (scanResult: string) => {
|
||||
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.success || confirmResult.status === 'confirmed') {
|
||||
Taro.showToast({
|
||||
title: '登录确认成功',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(confirmResult.message || '登录确认失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 处理核销二维码
|
||||
const handleVerificationQR = async (scanResult: string) => {
|
||||
if (!isUserAdmin()) {
|
||||
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')
|
||||
});
|
||||
|
||||
Taro.showToast({
|
||||
title: '核销成功',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
const onScanCode = () => {
|
||||
Taro.scanCode({
|
||||
onlyFromCamera: true,
|
||||
scanType: ['qrCode'],
|
||||
success: (res) => {
|
||||
success: async (res) => {
|
||||
console.log(res, 'qrcode...')
|
||||
Taro.navigateTo({url: '/hjm/query?id=' + res.result})
|
||||
const scanContent = res.result;
|
||||
|
||||
// 检测二维码类型
|
||||
const scanType = detectScanType(scanContent);
|
||||
|
||||
try {
|
||||
if (scanType === ScanType.LOGIN) {
|
||||
// 处理登录二维码
|
||||
await handleLoginQR(scanContent);
|
||||
console.log('登录二维码处理成功');
|
||||
return;
|
||||
} else if (scanType === ScanType.VERIFICATION) {
|
||||
// 处理核销二维码
|
||||
await handleVerificationQR(scanContent);
|
||||
console.log('核销二维码处理成功');
|
||||
return;
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.log('特殊二维码处理失败:', error.message);
|
||||
Taro.showToast({
|
||||
title: error.message,
|
||||
icon: 'error',
|
||||
duration: 2000
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果不是特殊二维码,作为车辆查询处理
|
||||
console.log('作为车辆查询二维码处理:', scanContent);
|
||||
Taro.navigateTo({url: '/hjm/query?id=' + scanContent});
|
||||
},
|
||||
fail: (res) => {
|
||||
console.log(res, '扫码失败')
|
||||
|
||||
@@ -2,8 +2,13 @@ import {useEffect, useState} from "react";
|
||||
import Taro from '@tarojs/taro'
|
||||
import {Input, Radio, Button} from '@nutui/nutui-react-taro'
|
||||
import './login.scss';
|
||||
import {User} from "@/api/system/user/model";
|
||||
|
||||
const Login = () => {
|
||||
interface LoginProps {
|
||||
done?: (data: User) => void;
|
||||
}
|
||||
|
||||
const Login: React.FC<LoginProps> = ({ done }) => {
|
||||
const [isAgree, setIsAgree] = useState(false)
|
||||
const [env, setEnv] = useState<string>()
|
||||
|
||||
|
||||
484
src/utils/invite.ts
Normal file
484
src/utils/invite.ts
Normal file
@@ -0,0 +1,484 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
import { bindRefereeRelation } from '@/api/invite'
|
||||
|
||||
/**
|
||||
* 邀请参数接口
|
||||
*/
|
||||
export interface InviteParams {
|
||||
inviter?: string;
|
||||
source?: string;
|
||||
t?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析小程序启动参数中的邀请信息
|
||||
*/
|
||||
export function parseInviteParams(options: any): InviteParams | null {
|
||||
try {
|
||||
// 优先从 query.scene 参数中解析邀请信息
|
||||
let sceneStr: string | null = 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))) {
|
||||
return {
|
||||
inviter: inviterId,
|
||||
source: 'qrcode',
|
||||
t: Date.now().toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理传统的 key=value&key=value 格式
|
||||
const params: InviteParams = {}
|
||||
const pairs = sceneStr.split('&')
|
||||
|
||||
pairs.forEach((pair: string) => {
|
||||
const [key, value] = pair.split('=')
|
||||
if (key && value) {
|
||||
switch (key) {
|
||||
case 'inviter':
|
||||
params.inviter = decodeURIComponent(value)
|
||||
break
|
||||
case 'source':
|
||||
params.source = decodeURIComponent(value)
|
||||
break
|
||||
case 't':
|
||||
params.t = decodeURIComponent(value)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (params.inviter) {
|
||||
return params
|
||||
}
|
||||
}
|
||||
|
||||
// 从 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'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
} catch (error) {
|
||||
console.error('解析邀请参数失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存邀请信息到本地存储
|
||||
*/
|
||||
export function saveInviteParams(params: InviteParams) {
|
||||
try {
|
||||
const saveData = {
|
||||
...params,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
|
||||
Taro.setStorageSync('invite_params', saveData)
|
||||
} catch (error) {
|
||||
console.error('保存邀请参数失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取本地存储的邀请信息
|
||||
*/
|
||||
export function getStoredInviteParams(): InviteParams | null {
|
||||
try {
|
||||
const stored = Taro.getStorageSync('invite_params')
|
||||
|
||||
if (stored && stored.inviter) {
|
||||
// 检查是否过期(24小时)
|
||||
const now = Date.now()
|
||||
const expireTime = 24 * 60 * 60 * 1000 // 24小时
|
||||
|
||||
if (now - stored.timestamp < expireTime) {
|
||||
return {
|
||||
inviter: stored.inviter,
|
||||
source: stored.source || 'unknown',
|
||||
t: stored.t
|
||||
}
|
||||
} else {
|
||||
// 过期则清除
|
||||
clearInviteParams()
|
||||
}
|
||||
}
|
||||
return null
|
||||
} catch (error) {
|
||||
console.error('获取邀请参数失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除本地存储的邀请信息
|
||||
*/
|
||||
export function clearInviteParams() {
|
||||
try {
|
||||
Taro.removeStorageSync('invite_params')
|
||||
} catch (error) {
|
||||
console.error('清除邀请参数失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理邀请关系建立
|
||||
*/
|
||||
export async function handleInviteRelation(userId: number): Promise<boolean> {
|
||||
try {
|
||||
const inviteParams = getStoredInviteParams()
|
||||
if (!inviteParams || !inviteParams.inviter) {
|
||||
return false
|
||||
}
|
||||
|
||||
const inviterId = parseInt(inviteParams.inviter)
|
||||
if (isNaN(inviterId) || inviterId === userId) {
|
||||
// 邀请人ID无效或自己邀请自己
|
||||
clearInviteParams()
|
||||
return false
|
||||
}
|
||||
|
||||
// 防重复检查:检查是否已经处理过这个邀请关系
|
||||
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)
|
||||
);
|
||||
|
||||
// 使用新的绑定推荐关系接口
|
||||
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()
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('建立邀请关系失败:', error)
|
||||
|
||||
// 如果是网络错误或超时,不清除邀请参数,允许稍后重试
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
if (errorMessage.includes('超时') || errorMessage.includes('网络')) {
|
||||
console.log('网络问题,保留邀请参数供稍后重试')
|
||||
return false
|
||||
}
|
||||
|
||||
// 其他错误(如业务逻辑错误),清除邀请参数
|
||||
clearInviteParams()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有待处理的邀请
|
||||
*/
|
||||
export function hasPendingInvite(): boolean {
|
||||
const params = getStoredInviteParams()
|
||||
return !!(params && params.inviter)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取邀请来源的显示名称
|
||||
*/
|
||||
export function getSourceDisplayName(source: string): string {
|
||||
const sourceMap: Record<string, string> = {
|
||||
'qrcode': '小程序码',
|
||||
'link': '分享链接',
|
||||
'share': '好友分享',
|
||||
'poster': '海报分享',
|
||||
'unknown': '未知来源'
|
||||
}
|
||||
|
||||
return sourceMap[source] || source
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证邀请码格式
|
||||
*/
|
||||
export function validateInviteCode(scene: string): boolean {
|
||||
try {
|
||||
if (!scene) return false
|
||||
|
||||
// 检查是否包含必要的参数
|
||||
const hasInviter = scene.includes('inviter=')
|
||||
const hasSource = scene.includes('source=')
|
||||
|
||||
return hasInviter && hasSource
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成邀请场景值
|
||||
*/
|
||||
export function generateInviteScene(inviterId: number, source: string): string {
|
||||
const timestamp = Date.now()
|
||||
return `inviter=${inviterId}&source=${source}&t=${timestamp}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计邀请来源
|
||||
*/
|
||||
export function trackInviteSource(source: string, inviterId?: number) {
|
||||
try {
|
||||
// 记录邀请来源统计
|
||||
const trackData = {
|
||||
source,
|
||||
inviterId,
|
||||
timestamp: Date.now(),
|
||||
userAgent: Taro.getSystemInfoSync()
|
||||
}
|
||||
|
||||
// 可以发送到统计服务
|
||||
console.log('邀请来源统计:', trackData)
|
||||
|
||||
// 暂存到本地,后续可批量上报
|
||||
const existingTracks = Taro.getStorageSync('invite_tracks') || []
|
||||
existingTracks.push(trackData)
|
||||
|
||||
// 只保留最近100条记录
|
||||
if (existingTracks.length > 100) {
|
||||
existingTracks.splice(0, existingTracks.length - 100)
|
||||
}
|
||||
|
||||
Taro.setStorageSync('invite_tracks', existingTracks)
|
||||
} catch (error) {
|
||||
console.error('统计邀请来源失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调试工具:打印所有邀请相关的存储信息
|
||||
*/
|
||||
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<boolean> {
|
||||
try {
|
||||
// 清理过期的防重记录
|
||||
cleanExpiredInviteRelations()
|
||||
|
||||
// 获取当前用户信息
|
||||
const userInfo = Taro.getStorageSync('userInfo')
|
||||
const userId = Taro.getStorageSync('UserId')
|
||||
|
||||
const finalUserId = userId || userInfo?.userId
|
||||
|
||||
if (!finalUserId) {
|
||||
console.log('用户未登录,无法处理邀请关系')
|
||||
return false
|
||||
}
|
||||
|
||||
console.log('使用用户ID处理邀请关系:', finalUserId)
|
||||
|
||||
// 设置整体超时保护
|
||||
const timeoutPromise = new Promise<boolean>((_, 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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动触发邀请关系建立
|
||||
* 用于在特定页面或时机手动建立邀请关系
|
||||
*/
|
||||
export async function manualHandleInviteRelation(userId: number): Promise<boolean> {
|
||||
try {
|
||||
console.log('手动触发邀请关系建立,用户ID:', userId)
|
||||
|
||||
const inviteParams = getStoredInviteParams()
|
||||
if (!inviteParams || !inviteParams.inviter) {
|
||||
console.log('没有待处理的邀请参数')
|
||||
return false
|
||||
}
|
||||
|
||||
const result = await handleInviteRelation(userId)
|
||||
|
||||
if (result) {
|
||||
// 显示成功提示
|
||||
Taro.showModal({
|
||||
title: '邀请成功',
|
||||
content: '您已成功加入邀请人的团队!',
|
||||
showCancel: false,
|
||||
confirmText: '知道了'
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('手动处理邀请关系失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期的邀请关系防重记录
|
||||
*/
|
||||
export function cleanExpiredInviteRelations() {
|
||||
try {
|
||||
const keys = Taro.getStorageInfoSync().keys
|
||||
const expireTime = 7 * 24 * 60 * 60 * 1000 // 7天
|
||||
const now = Date.now()
|
||||
|
||||
keys.forEach(key => {
|
||||
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<boolean> {
|
||||
try {
|
||||
// 如果没有传入userId,尝试从本地存储获取
|
||||
let targetUserId = userId
|
||||
if (!targetUserId) {
|
||||
const userInfo = Taro.getStorageSync('userInfo')
|
||||
if (userInfo && userInfo.userId) {
|
||||
targetUserId = userInfo.userId
|
||||
} else {
|
||||
throw new Error('无法获取用户ID')
|
||||
}
|
||||
}
|
||||
|
||||
// 防止自己推荐自己
|
||||
if (refereeId === targetUserId) {
|
||||
throw new Error('不能推荐自己')
|
||||
}
|
||||
|
||||
await bindRefereeRelation({
|
||||
dealerId: refereeId,
|
||||
userId: targetUserId,
|
||||
source: source,
|
||||
scene: source === 'qrcode' ? `uid_${refereeId}` : undefined
|
||||
})
|
||||
|
||||
return true
|
||||
} catch (error: any) {
|
||||
console.error('绑定推荐关系失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
31
src/utils/jsonUtils.ts
Normal file
31
src/utils/jsonUtils.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 判断字符串是否为有效的JSON格式
|
||||
* @param str 要检测的字符串
|
||||
* @returns boolean
|
||||
*/
|
||||
export function isValidJSON(str: string): boolean {
|
||||
if (typeof str !== 'string' || str.trim() === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
JSON.parse(str);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全解析JSON,失败时返回默认值
|
||||
* @param str JSON字符串
|
||||
* @param defaultValue 默认值
|
||||
* @returns 解析结果或默认值
|
||||
*/
|
||||
export function safeJSONParse<T>(str: string, defaultValue: T): T {
|
||||
try {
|
||||
return JSON.parse(str);
|
||||
} catch (error) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,8 @@
|
||||
"@/components/*": ["./src/components/*"],
|
||||
"@/utils/*": ["./src/utils/*"],
|
||||
"@/assets/*": ["./src/assets/*"],
|
||||
"@/api/*": ["./src/api/*"]
|
||||
"@/api/*": ["./src/api/*"],
|
||||
"@/hooks/*": ["./src/hooks/*"]
|
||||
}
|
||||
},
|
||||
"include": ["./src", "./types"],
|
||||
|
||||
Reference in New Issue
Block a user