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:
2025-09-23 11:11:20 +08:00
parent c5a38ab695
commit d12a0fbf11
7 changed files with 732 additions and 119 deletions

View 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
};
}