feat(auth): 实现二维码登录功能

- 新增二维码登录相关接口和页面
- 实现二维码生成、状态检查、登录确认等逻辑
- 添加微信小程序登录支持- 优化用户信息展示和处理
This commit is contained in:
2025-09-05 22:49:41 +08:00
parent 0dfe3934a4
commit 408ff13590
18 changed files with 1558 additions and 10 deletions

View File

@@ -0,0 +1,303 @@
import React from 'react';
import Taro from '@tarojs/taro';
import { parseScanResult, wechatMiniProgramConfirm, scanQrCode } from '@/api/qrLogin';
import type { ScanResultParsed } from '@/api/qrLogin/model';
import navTo from '@/utils/common';
import { useUser } from '@/hooks/useUser';
/**
* 统一扫码处理组件
*/
export interface UniversalScannerProps {
/** 扫码成功回调 */
onScanSuccess?: (result: ScanResultParsed) => void;
/** 扫码失败回调 */
onScanError?: (error: string) => void;
/** 是否显示处理结果提示 */
showToast?: boolean;
}
/**
* 统一扫码处理Hook
*/
export function useUniversalScanner(props: UniversalScannerProps = {}) {
const {
onScanSuccess,
onScanError,
showToast = true
} = props;
const { user, isLoggedIn, isAdmin, loginUser } = useUser();
/**
* 启动扫码
*/
const startScan = () => {
Taro.scanCode({
onlyFromCamera: true,
scanType: ['qrCode', 'barCode'],
success: (res) => {
handleScanResult(res.result);
},
fail: (err) => {
console.error('扫码失败:', err);
const errorMsg = '扫码失败,请重试';
if (showToast) {
Taro.showToast({
title: errorMsg,
icon: 'error'
});
}
onScanError?.(errorMsg);
}
});
};
/**
* 处理扫码结果
*/
const handleScanResult = async (scanResult: string) => {
try {
console.log('扫码结果:', scanResult);
// 解析扫码结果
const parsed = parseScanResult(scanResult);
console.log('解析结果:', parsed);
// 权限检查
if (parsed.requireAuth && !isLoggedIn) {
if (showToast) {
Taro.showToast({
title: '请先登录',
icon: 'error'
});
}
onScanError?.('请先登录');
return;
}
if (parsed.requireAdmin && !isAdmin()) {
if (showToast) {
Taro.showToast({
title: '仅管理员可使用此功能',
icon: 'error'
});
}
onScanError?.('权限不足');
return;
}
// 根据类型处理
await handleByType(parsed);
// 回调
onScanSuccess?.(parsed);
} catch (error) {
console.error('处理扫码结果失败:', error);
const errorMsg = error.message || '处理扫码结果失败';
if (showToast) {
Taro.showToast({
title: errorMsg,
icon: 'error'
});
}
onScanError?.(errorMsg);
}
};
/**
* 根据类型处理扫码结果
*/
const handleByType = async (parsed: ScanResultParsed) => {
switch (parsed.type) {
case 'qr-login':
await handleQrLogin(parsed);
break;
case 'gift-verification':
handleGiftVerification(parsed);
break;
case 'gift-redeem':
handleGiftRedeem(parsed);
break;
case 'vehicle-query':
handleVehicleQuery(parsed);
break;
case 'unknown':
handleUnknownType(parsed);
break;
default:
throw new Error(`未支持的扫码类型: ${parsed.type}`);
}
};
/**
* 处理二维码登录
*/
const handleQrLogin = async (parsed: ScanResultParsed) => {
const { token } = parsed.data;
try {
if (showToast) {
Taro.showLoading({ title: '正在处理登录...' });
}
// 1. 先调用扫码接口,更新状态为已扫码
await scanQrCode(token);
// 2. 确认登录
const confirmData = {
token,
userId: user?.userId,
platform: 'miniprogram',
wechatInfo: {
openid: user?.openid,
unionid: user?.unionid,
nickname: user?.nickname || user?.realName,
avatar: user?.avatar
}
};
const result = await wechatMiniProgramConfirm(confirmData);
if (result.status === 'confirmed') {
if (showToast) {
Taro.hideLoading();
Taro.showToast({
title: '后台管理登录确认成功',
icon: 'success',
duration: 2000
});
}
// 显示成功提示弹窗
Taro.showModal({
title: '登录成功',
content: '您已成功确认后台管理系统登录,请在电脑端查看登录状态。',
showCancel: false,
confirmText: '知道了'
});
} else {
throw new Error(result.message || '登录确认失败');
}
} catch (error) {
if (showToast) {
Taro.hideLoading();
}
// 根据错误类型显示不同的提示
let errorMessage = '登录确认失败';
if (error.message?.includes('过期')) {
errorMessage = '二维码已过期,请重新生成';
} else if (error.message?.includes('无效')) {
errorMessage = '无效的登录二维码';
} else if (error.message) {
errorMessage = error.message;
}
if (showToast) {
Taro.showToast({
title: errorMessage,
icon: 'error',
duration: 3000
});
}
throw new Error(errorMessage);
}
};
/**
* 处理礼品卡核销
*/
const handleGiftVerification = (parsed: ScanResultParsed) => {
// 跳转到核销页面,并传递扫码数据
const encryptedData = encodeURIComponent(JSON.stringify(parsed.data));
navTo(`/user/store/verification?scanData=${encryptedData}`, true);
};
/**
* 处理礼品卡兑换
*/
const handleGiftRedeem = (parsed: ScanResultParsed) => {
const { code } = parsed.data;
navTo(`/user/gift/redeem?code=${encodeURIComponent(code)}`, true);
};
/**
* 处理车辆查询
*/
const handleVehicleQuery = (parsed: ScanResultParsed) => {
const { vehicleId } = parsed.data;
navTo(`/hjm/query?id=${vehicleId}`, true);
};
/**
* 处理未知类型
*/
const handleUnknownType = (parsed: ScanResultParsed) => {
// 显示选择弹窗,让用户选择如何处理
Taro.showActionSheet({
itemList: [
'复制内容',
'作为礼品卡兑换码',
'作为车辆查询码',
'取消'
],
success: (res) => {
const { tapIndex } = res;
switch (tapIndex) {
case 0:
// 复制内容
Taro.setClipboardData({
data: parsed.rawContent,
success: () => {
if (showToast) {
Taro.showToast({
title: '已复制到剪贴板',
icon: 'success'
});
}
}
});
break;
case 1:
// 作为礼品卡兑换码
navTo(`/user/gift/redeem?code=${encodeURIComponent(parsed.rawContent)}`, true);
break;
case 2:
// 作为车辆查询码
navTo(`/hjm/query?id=${parsed.rawContent}`, true);
break;
}
}
});
};
return {
startScan,
handleScanResult
};
}
/**
* 统一扫码处理组件(如果需要作为组件使用)
*/
const UniversalScanner: React.FC<UniversalScannerProps> = (props) => {
const { startScan } = useUniversalScanner(props);
// 这个组件主要提供Hook不渲染UI
// 如果需要可以返回一个扫码按钮
return null;
};
export default UniversalScanner;