```
feat(dealer): 将“确定签约”按钮文本修改为“立即提交” 将客户添加页面中的按钮文本从“确定签约”更改为“立即提交”,以更准确地反映用户操作意图。feat(config): 移除测试页面的路由配置 从 app.config.ts 中移除了 /pages/test/scan 路由配置,清理不再使用的测试页面路径。 feat(index): 添加统一扫码按钮并优化用户信息检查逻辑在首页头部添加了 UnifiedQRButton 组件,支持统一扫码入口,用于登录和核销功能。 同时优化了微信用户昵称判断条件的空格格式。 feat(api): 新增扫码登录相关接口及工具方法 新增 qr-login 模块,包含生成二维码 token、检查扫码状态、确认登录等接口。同时提供了解析二维码内容、获取设备信息等辅助函数。 feat(component): 新增统一扫码按钮组件 UnifiedQRButton 创建 UnifiedQRButton 组件,封装扫码逻辑,支持页面模式跳转与直接扫码两种方式, 并根据扫码结果展示不同反馈。 feat(hook): 新增 useUnifiedQRScan Hook 支持登录与核销扫码 实现 useUnifiedQRScan 自定义 Hook,统一处理登录二维码和礼品卡核销二维码的识别与处理流程,支持权限校验、解密、状态管理等功能。 ```
This commit is contained in:
246
src/api/passport/qr-login/index.ts
Normal file
246
src/api/passport/qr-login/index.ts
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
import type { ApiResult } from '@/api';
|
||||||
|
import Taro from '@tarojs/taro';
|
||||||
|
import {SERVER_API_URL} from "@/utils/server";
|
||||||
|
import {getUserInfo} from "@/api/layout";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫码登录相关接口
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 生成扫码token请求参数
|
||||||
|
export interface GenerateQRTokenParam {
|
||||||
|
// 客户端类型:web, app, wechat
|
||||||
|
clientType?: string;
|
||||||
|
// 设备信息
|
||||||
|
deviceInfo?: string;
|
||||||
|
// 过期时间(分钟)
|
||||||
|
expireMinutes?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成扫码token响应
|
||||||
|
export interface GenerateQRTokenResult {
|
||||||
|
// 扫码token
|
||||||
|
token: string;
|
||||||
|
// 二维码内容(通常是包含token的URL或JSON)
|
||||||
|
qrContent: string;
|
||||||
|
// 过期时间戳
|
||||||
|
expireTime: number;
|
||||||
|
// 二维码图片URL(可选)
|
||||||
|
qrImageUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 扫码状态枚举
|
||||||
|
export enum QRLoginStatus {
|
||||||
|
PENDING = 'pending', // 等待扫码
|
||||||
|
SCANNED = 'scanned', // 已扫码,等待确认
|
||||||
|
CONFIRMED = 'confirmed', // 已确认登录
|
||||||
|
EXPIRED = 'expired', // 已过期
|
||||||
|
CANCELLED = 'cancelled' // 已取消
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查扫码状态响应
|
||||||
|
export interface QRLoginStatusResult {
|
||||||
|
// 当前状态
|
||||||
|
status: QRLoginStatus;
|
||||||
|
// 状态描述
|
||||||
|
message?: string;
|
||||||
|
// 如果已确认登录,返回JWT token
|
||||||
|
accessToken?: string;
|
||||||
|
// 用户信息
|
||||||
|
userInfo?: {
|
||||||
|
userId: number;
|
||||||
|
nickname?: string;
|
||||||
|
avatar?: string;
|
||||||
|
phone?: string;
|
||||||
|
};
|
||||||
|
// 剩余有效时间(秒)
|
||||||
|
remainingTime?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认登录请求参数
|
||||||
|
export interface ConfirmLoginParam {
|
||||||
|
// 扫码token
|
||||||
|
token: string;
|
||||||
|
// 用户ID
|
||||||
|
userId: number;
|
||||||
|
// 登录平台:web, app, wechat
|
||||||
|
platform?: string;
|
||||||
|
// 微信用户信息(当platform为wechat时)
|
||||||
|
wechatInfo?: {
|
||||||
|
openid?: string;
|
||||||
|
unionid?: string;
|
||||||
|
nickname?: string;
|
||||||
|
avatar?: string;
|
||||||
|
gender?: string;
|
||||||
|
};
|
||||||
|
// 设备信息
|
||||||
|
deviceInfo?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认登录响应
|
||||||
|
export interface ConfirmLoginResult {
|
||||||
|
// 是否成功
|
||||||
|
success: boolean;
|
||||||
|
// 消息
|
||||||
|
message: string;
|
||||||
|
// 登录用户信息
|
||||||
|
userInfo?: {
|
||||||
|
userId: number;
|
||||||
|
nickname?: string;
|
||||||
|
avatar?: string;
|
||||||
|
phone?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成扫码登录token
|
||||||
|
*/
|
||||||
|
export async function generateQRToken(data?: GenerateQRTokenParam) {
|
||||||
|
const res = await request.post<ApiResult<GenerateQRTokenResult>>(
|
||||||
|
SERVER_API_URL + '/qr-login/generate',
|
||||||
|
{
|
||||||
|
clientType: 'wechat',
|
||||||
|
expireMinutes: 5,
|
||||||
|
...data
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查扫码登录状态
|
||||||
|
*/
|
||||||
|
export async function checkQRLoginStatus(token: string) {
|
||||||
|
const res = await request.get<ApiResult<QRLoginStatusResult>>(
|
||||||
|
SERVER_API_URL + `/qr-login/status/${token}`
|
||||||
|
);
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确认扫码登录(通用接口)
|
||||||
|
*/
|
||||||
|
export async function confirmQRLogin(data: ConfirmLoginParam) {
|
||||||
|
const res = await request.post<ApiResult<ConfirmLoginResult>>(
|
||||||
|
SERVER_API_URL + '/qr-login/confirm',
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信小程序扫码登录确认(便捷接口)
|
||||||
|
*/
|
||||||
|
export async function confirmWechatQRLogin(token: string, userId: number) {
|
||||||
|
try {
|
||||||
|
// 获取微信用户信息
|
||||||
|
const userInfo = await getUserInfo();
|
||||||
|
|
||||||
|
const data: ConfirmLoginParam = {
|
||||||
|
token,
|
||||||
|
userId,
|
||||||
|
platform: 'wechat',
|
||||||
|
wechatInfo: {
|
||||||
|
nickname: userInfo?.nickname,
|
||||||
|
avatar: userInfo?.avatar,
|
||||||
|
gender: userInfo?.sex
|
||||||
|
},
|
||||||
|
deviceInfo: await getDeviceInfo()
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await request.post<ApiResult<any>>(
|
||||||
|
SERVER_API_URL + '/qr-login/confirm',
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.message));
|
||||||
|
} catch (error: any) {
|
||||||
|
return Promise.reject(new Error(error.message || '确认登录失败'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取设备信息
|
||||||
|
*/
|
||||||
|
async function getDeviceInfo() {
|
||||||
|
return new Promise<string>((resolve) => {
|
||||||
|
Taro.getSystemInfo({
|
||||||
|
success: (res) => {
|
||||||
|
const deviceInfo = {
|
||||||
|
platform: res.platform,
|
||||||
|
system: res.system,
|
||||||
|
version: res.version,
|
||||||
|
model: res.model,
|
||||||
|
brand: res.brand,
|
||||||
|
screenWidth: res.screenWidth,
|
||||||
|
screenHeight: res.screenHeight
|
||||||
|
};
|
||||||
|
resolve(JSON.stringify(deviceInfo));
|
||||||
|
},
|
||||||
|
fail: () => {
|
||||||
|
resolve('unknown');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析二维码内容,提取token
|
||||||
|
*/
|
||||||
|
export function parseQRContent(qrContent: string): string | null {
|
||||||
|
try {
|
||||||
|
console.log('解析二维码内容1:', qrContent);
|
||||||
|
|
||||||
|
// 尝试解析JSON格式
|
||||||
|
if (qrContent.startsWith('{')) {
|
||||||
|
const parsed = JSON.parse(qrContent);
|
||||||
|
return parsed.token || parsed.qrCodeKey || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试解析URL格式
|
||||||
|
if (qrContent.includes('http')) {
|
||||||
|
const url = new URL(qrContent);
|
||||||
|
// 支持多种参数名
|
||||||
|
return url.searchParams.get('token') ||
|
||||||
|
url.searchParams.get('qrCodeKey') ||
|
||||||
|
url.searchParams.get('qr_code_key') ||
|
||||||
|
null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试解析简单的key=value格式
|
||||||
|
if (qrContent.includes('=')) {
|
||||||
|
const params = new URLSearchParams(qrContent);
|
||||||
|
return params.get('token') ||
|
||||||
|
params.get('qrCodeKey') ||
|
||||||
|
params.get('qr_code_key') ||
|
||||||
|
null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是以qr-login:开头的格式
|
||||||
|
if (qrContent.startsWith('qr-login:')) {
|
||||||
|
return qrContent.replace('qr-login:', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接返回内容作为token(如果是32位以上的字符串)
|
||||||
|
if (qrContent.length >= 32) {
|
||||||
|
return qrContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析二维码内容失败:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,12 +51,6 @@ export default defineAppConfig({
|
|||||||
"chat/message/detail"
|
"chat/message/detail"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"root": "pages/test",
|
|
||||||
"pages": [
|
|
||||||
"scan"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"root": "dealer",
|
"root": "dealer",
|
||||||
"pages": [
|
"pages": [
|
||||||
|
|||||||
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 = 'success',
|
||||||
|
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 text-white">
|
||||||
|
{showIcon && !isLoading && (
|
||||||
|
<Scan />
|
||||||
|
)}
|
||||||
|
{getButtonText()}
|
||||||
|
</View>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UnifiedQRButton;
|
||||||
@@ -385,7 +385,7 @@ const AddShopDealerApply = () => {
|
|||||||
{(!isEditMode || FormData?.applyStatus === 10) && (
|
{(!isEditMode || FormData?.applyStatus === 10) && (
|
||||||
<FixedButton
|
<FixedButton
|
||||||
icon={<Edit/>}
|
icon={<Edit/>}
|
||||||
text={'确定签约'}
|
text={'立即提交'}
|
||||||
onClick={handleFixedButtonClick}
|
onClick={handleFixedButtonClick}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
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
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import {View, Text} from '@tarojs/components'
|
|||||||
import MySearch from "./MySearch";
|
import MySearch from "./MySearch";
|
||||||
import './Header.scss';
|
import './Header.scss';
|
||||||
import navTo from "@/utils/common";
|
import navTo from "@/utils/common";
|
||||||
|
import UnifiedQRButton from "@/components/UnifiedQRButton";
|
||||||
|
|
||||||
const Header = (props: any) => {
|
const Header = (props: any) => {
|
||||||
// 使用新的useShopInfo Hook
|
// 使用新的useShopInfo Hook
|
||||||
@@ -194,7 +195,7 @@ const Header = (props: any) => {
|
|||||||
if (isLoggedIn && user) {
|
if (isLoggedIn && user) {
|
||||||
console.log('用户信息已更新:', user);
|
console.log('用户信息已更新:', user);
|
||||||
// 检查是否设置头像和昵称
|
// 检查是否设置头像和昵称
|
||||||
if(user.nickname === '微信用户'){
|
if (user.nickname === '微信用户') {
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
title: '请设置头像和昵称',
|
title: '请设置头像和昵称',
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
@@ -242,7 +243,32 @@ const Header = (props: any) => {
|
|||||||
<TriangleDown size={9} className={'text-white'}/>
|
<TriangleDown size={9} className={'text-white'}/>
|
||||||
</Space>
|
</Space>
|
||||||
</View>
|
</View>
|
||||||
)}>
|
)}
|
||||||
|
right={
|
||||||
|
<Space style={{
|
||||||
|
marginRight: '100px'
|
||||||
|
}}>
|
||||||
|
{/*统一扫码入口 - 支持登录和核销*/}
|
||||||
|
<UnifiedQRButton
|
||||||
|
size="small"
|
||||||
|
onSuccess={(result) => {
|
||||||
|
console.log('统一扫码成功:', result);
|
||||||
|
// 根据扫码类型给出不同的提示
|
||||||
|
if (result.type === 'verification') {
|
||||||
|
// 核销成功,可以显示更多信息或跳转到详情页
|
||||||
|
Taro.showModal({
|
||||||
|
title: '核销成功',
|
||||||
|
content: `已成功核销的品类:${result.data.goodsName || '礼品卡'},面值¥${result.data.faceValue}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onError={(error) => {
|
||||||
|
console.error('统一扫码失败:', error);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
</NavBar>
|
</NavBar>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { View, Text } from '@tarojs/components';
|
|
||||||
import { Button } from '@nutui/nutui-react-taro';
|
|
||||||
import { Scan } from '@nutui/icons-react-taro';
|
|
||||||
import Taro from '@tarojs/taro';
|
|
||||||
import { useUniversalScanner } from '@/components/UniversalScanner';
|
|
||||||
import { useUser } from '@/hooks/useUser';
|
|
||||||
|
|
||||||
const ScanTest: React.FC = () => {
|
|
||||||
const { user, isLoggedIn } = useUser();
|
|
||||||
|
|
||||||
const { startScan } = useUniversalScanner({
|
|
||||||
onScanSuccess: (result) => {
|
|
||||||
console.log('测试页面 - 扫码成功:', result);
|
|
||||||
Taro.showModal({
|
|
||||||
title: '扫码成功',
|
|
||||||
content: `类型: ${result.type}\n内容: ${result.rawContent}`,
|
|
||||||
showCancel: false
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onScanError: (error) => {
|
|
||||||
console.error('测试页面 - 扫码失败:', error);
|
|
||||||
Taro.showModal({
|
|
||||||
title: '扫码失败',
|
|
||||||
content: error,
|
|
||||||
showCancel: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleDirectScan = () => {
|
|
||||||
console.log('直接调用 Taro.scanCode');
|
|
||||||
Taro.scanCode({
|
|
||||||
success: (res) => {
|
|
||||||
console.log('直接扫码成功:', res.result);
|
|
||||||
Taro.showModal({
|
|
||||||
title: '直接扫码成功',
|
|
||||||
content: res.result,
|
|
||||||
showCancel: false
|
|
||||||
});
|
|
||||||
},
|
|
||||||
fail: (err) => {
|
|
||||||
console.error('直接扫码失败:', err);
|
|
||||||
Taro.showModal({
|
|
||||||
title: '直接扫码失败',
|
|
||||||
content: JSON.stringify(err),
|
|
||||||
showCancel: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View className="p-4">
|
|
||||||
<Text className="text-lg font-bold mb-4">扫码功能测试</Text>
|
|
||||||
|
|
||||||
<View className="mb-4">
|
|
||||||
<Text>登录状态: {isLoggedIn ? '已登录' : '未登录'}</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="mb-4">
|
|
||||||
<Text>用户信息: {user ? JSON.stringify(user, null, 2) : '无'}</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="space-y-4">
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
size="large"
|
|
||||||
block
|
|
||||||
icon={<Scan />}
|
|
||||||
onClick={() => {
|
|
||||||
console.log('点击了统一扫码按钮');
|
|
||||||
startScan();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
统一扫码测试
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="default"
|
|
||||||
size="large"
|
|
||||||
block
|
|
||||||
icon={<Scan />}
|
|
||||||
onClick={handleDirectScan}
|
|
||||||
>
|
|
||||||
直接扫码测试
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="warning"
|
|
||||||
size="large"
|
|
||||||
block
|
|
||||||
onClick={() => {
|
|
||||||
console.log('测试日志输出');
|
|
||||||
console.log('startScan 函数:', startScan);
|
|
||||||
console.log('startScan 类型:', typeof startScan);
|
|
||||||
Taro.showToast({
|
|
||||||
title: '查看控制台日志',
|
|
||||||
icon: 'none'
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
测试日志输出
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ScanTest;
|
|
||||||
Reference in New Issue
Block a user