import React, { useState, useEffect } from 'react'; import { View, Text } from '@tarojs/components'; import { Loading, Card, Button } from '@nutui/nutui-react-taro'; import { Success, Failure, Tips, User, Checklist } from '@nutui/icons-react-taro'; import Taro, { useRouter } from '@tarojs/taro'; import { confirmQRLogin } from '@/api/passport/qr-login'; import { loginByOpenId } from '@/api/passport/wx-login'; import { TenantId } from "@/config/app"; import { saveStorageByLoginUser, SERVER_API_URL } from "@/utils/server"; import { checkAndHandleInviteRelation, hasPendingInvite, getStoredInviteParams } from "@/utils/invite"; /** * 扫码登录确认页面 * * 支持两种场景: * 1. 主动扫码:用户点击按钮调用微信扫码,扫描 PC 端二维码 * 2. URL 扫码(小程序码):用户扫描小程序码后,微信自动打开此页面 * * URL 扫码场景: * - 微信「扫普通链接二维码打开小程序」配置的二维码规则:`https://websopy.websoft.top/wx-scan/` * - 扫码后 URL:`https://websopy.websoft.top/wx-scan?token=xxx` * - 小程序接收到参数后自动确认登录 * * 登录流程(2026-04-08 更新): * 1. 用户扫码 → 进入 qr-confirm 页面 * 2. 页面立即调用 wx.login() 获取 code * 3. 用 code 调用 /api/wx-login/loginByOpenId 获取/验证用户身份 * 4. 如果用户不存在 → 显示微信手机号授权按钮(一键注册登录) * 5. 如果用户存在 → 自动调用 confirmQRLogin 确认登录 */ // 微信获取手机号回调参数类型 interface GetPhoneNumberDetail { code?: string; encryptedData?: string; iv?: string; errMsg: string; } interface GetPhoneNumberEvent { detail: GetPhoneNumberDetail; } // 登录接口返回数据类型 interface LoginResponse { data: { access_token: string; user: any; }; code: number; message: string; } // 协议弹窗类型 type AgreementType = 'service' | 'privacy' | null; const QRConfirmPage: React.FC = () => { const router = useRouter(); const [loading, setLoading] = useState(false); const [confirmed, setConfirmed] = useState(false); const [error, setError] = useState(''); const [token, setToken] = useState(''); const [loginMethod, setLoginMethod] = useState<'scan' | 'url'>('url'); const [userInfo, setUserInfo] = useState(null); const [needAuth, setNeedAuth] = useState(false); // 是否需要手机号授权 const [authLoading, setAuthLoading] = useState(false); // 授权中状态 const [agreementChecked, setAgreementChecked] = useState(false); // 协议勾选状态 const [showAgreementPopup, setShowAgreementPopup] = useState(null); // 协议弹窗 useEffect(() => { // 从 URL 参数中获取 token const params = router.params; // 兼容多种参数名 let loginToken = params.scene || params.token || params.qrCodeKey || ''; // 如果是 q 参数(URL 编码的完整 URL),需要解析 if (params.q && !loginToken) { try { const decodedUrl = decodeURIComponent(params.q); console.log('[QRConfirm] 解码后的 URL:', decodedUrl); const url = new URL(decodedUrl); loginToken = url.searchParams.get('token') || url.searchParams.get('qrCodeKey') || ''; setLoginMethod('url'); } catch (e) { console.error('[QRConfirm] 解析 q 参数失败:', e); loginToken = decodeURIComponent(params.q); setLoginMethod('url'); } } else if (loginToken) { setLoginMethod('url'); } // 扫码场景:直接显示授权登录界面 console.log('[QRConfirm] 显示授权登录界面'); setToken(loginToken); setNeedAuth(true); }, [router.params]); /** * 自动确认登录(URL 扫码场景) */ const handleAutoConfirm = async (loginToken: string) => { try { setLoading(true); // 1. 调用微信登录获取 code console.log('[QRConfirm] 调用 wx.login() 获取 code...'); const loginResult = await Taro.login(); if (!loginResult.code) { throw new Error('获取微信登录凭证失败'); } console.log('[QRConfirm] 获取到 code:', loginResult.code); // 2. 用 code 调用后端接口验证用户身份 console.log('[QRConfirm] 调用后端 loginByOpenId...'); const wxLoginResult = await loginByOpenId({ code: loginResult.code, tenantId: 5 }); console.log('[QRConfirm] loginByOpenId 结果:', wxLoginResult); // 3. 统一显示授权登录界面,让用户一键授权完成登录 // 无论用户是否注册、是否绑定手机号,都走授权登录流程 console.log('[QRConfirm] 显示手机号授权登录界面'); setNeedAuth(true); setLoading(false); } catch (err: any) { console.error('[QRConfirm] 自动确认登录失败:', err); setError(err.message || '自动确认登录失败'); setLoading(false); } }; /** * 处理微信手机号授权 */ const handleGetPhoneNumber = async ({ detail }: GetPhoneNumberEvent) => { const { code, encryptedData, iv, errMsg } = detail; // 检查协议是否勾选 if (!agreementChecked) { Taro.showToast({ title: '请先同意服务协议和隐私政策', icon: 'none' }); return; } // 用户拒绝授权 if (errMsg && errMsg.includes('fail')) { Taro.showToast({ title: '需要授权手机号才能完成登录', icon: 'none' }); return; } if (!code) { Taro.showToast({ title: '获取授权信息失败,请重试', icon: 'none' }); return; } // 执行授权登录 await handleAuthLogin(code, encryptedData, iv); }; /** * 授权登录(未注册用户) */ const handleAuthLogin = async (phoneCode: string, encryptedData?: string, iv?: string) => { try { setAuthLoading(true); // 获取存储的邀请参数 const inviteParams = getStoredInviteParams(); const refereeId = inviteParams?.inviter ? parseInt(inviteParams.inviter) : 0; const res = await Taro.request({ url: `${SERVER_API_URL}/wx-login/loginByMpWxPhone`, method: 'POST', data: { code: phoneCode, encryptedData, iv, tenantId: TenantId, notVerifyPhone: true, refereeId: refereeId, sceneType: 'save_referee' }, header: { 'content-type': 'application/json', 'TenantId': TenantId } }); if (res.data.code !== 0) { throw new Error(res.data.message || '登录失败'); } // 保存登录信息 if (res.data.data?.user) { saveStorageByLoginUser(res.data.data.access_token, res.data.data.user); setUserInfo(res.data.data.user); // 处理邀请关系 if (hasPendingInvite()) { try { await checkAndHandleInviteRelation(); } catch (e) { console.error('授权登录后处理邀请关系失败:', e); } } Taro.showToast({ title: '授权成功,正在确认登录...', icon: 'success' }); // 延迟后自动确认扫码登录 setTimeout(() => { handleConfirmLogin(token, res.data.data.user); }, 1500); } } catch (error: any) { Taro.showToast({ title: error.message || '授权失败', icon: 'error' }); } finally { setAuthLoading(false); } }; /** * 确认登录 */ const handleConfirmLogin = async (loginToken?: string, wxUserInfo?: any) => { const confirmToken = loginToken || token; if (!confirmToken) { setError('缺少登录token'); return; } const currentUser = wxUserInfo || userInfo; if (!currentUser?.userId) { const userId = Taro.getStorageSync('UserId'); if (!userId) { setError('请先登录小程序'); setNeedAuth(true); return; } currentUser && (currentUser.userId = Number(userId)); } try { setLoading(true); setError(''); const result = await confirmQRLogin({ token: confirmToken, userId: currentUser.userId, platform: 'wechat', wechatInfo: { nickname: currentUser.nickname || currentUser.username, avatar: currentUser.avatar } }); const isConfirmed = result.status === 'confirmed' || result.success === true; if (isConfirmed) { setConfirmed(true); setNeedAuth(false); Taro.showToast({ title: result.successMessage || result.message || '登录确认成功', icon: 'success', duration: 2000 }); setTimeout(() => { const pages = Taro.getCurrentPages(); if (pages.length > 1) { Taro.navigateBack(); } else { Taro.showModal({ title: '登录成功', content: '请回到电脑端刷新页面', showCancel: false, confirmText: '我知道了' }); } }, 3000); } else if (result.status === 'bind_phone' || result.needBindPhone) { Taro.showToast({ title: '请先绑定手机号', icon: 'none' }); setTimeout(() => { Taro.redirectTo({ url: '/passport/sms-login' }); }, 1500); } else { setError(result.message || '登录确认失败'); } } catch (err: any) { console.error('[QRConfirm] 确认登录失败:', err); setError(err.message || '登录确认失败'); } finally { setLoading(false); } }; /** * 手动确认登录(主动扫码场景) */ const handleManualConfirm = () => { handleConfirmLogin(); }; /** * 取消登录 */ const handleCancel = () => { const pages = Taro.getCurrentPages(); if (pages.length > 1) { Taro.navigateBack(); } else { Taro.switchTab({ url: '/pages/user/user' }); } }; /** * 重试 */ const handleRetry = () => { setError(''); setConfirmed(false); setNeedAuth(false); if (token) { handleAutoConfirm(token); } }; /** * 打开微信扫码 */ const handleScan = () => { Taro.scanCode({ success: async (res) => { console.log('[QRConfirm] 扫码成功:', res); let scanToken = ''; const qrContent = res.result; try { if (qrContent.includes('http')) { const url = new URL(qrContent); scanToken = url.searchParams.get('token') || url.searchParams.get('qrCodeKey') || ''; } if (!scanToken && qrContent.startsWith('{')) { const parsed = JSON.parse(qrContent); scanToken = parsed.token || parsed.qrCodeKey || ''; } if (!scanToken && qrContent.length >= 32) { scanToken = qrContent; } if (scanToken) { setToken(scanToken); setLoginMethod('scan'); setNeedAuth(false); setError(''); handleConfirmLogin(scanToken); } else { setError('无效的二维码内容'); } } catch (e) { console.error('[QRConfirm] 解析二维码失败:', e); setError('二维码解析失败'); } }, fail: (err) => { console.error('[QRConfirm] 扫码失败:', err); if (err.errMsg !== 'scanCode:fail cancel') { setError('扫码失败,请重试'); } } }); }; // 打开协议弹窗 const openAgreement = (type: AgreementType) => { setShowAgreementPopup(type); }; // 关闭协议弹窗 const closeAgreement = () => { setShowAgreementPopup(null); }; // 渲染协议弹窗内容 const renderAgreementContent = () => { if (showAgreementPopup === 'service') { return ( 服务协议 欢迎使用我们的服务! 1. 服务条款 本服务协议是您与我们之间关于使用我们提供的各项服务的协议。 2. 账号注册 您需要注册账号才能使用我们的部分服务。注册时您需要提供真实、准确的信息。 3. 服务使用 您同意遵守相关法律法规,不得利用我们的服务从事违法违规活动。 4. 隐私保护 我们重视您的隐私保护,具体请参见《隐私政策》。 5. 协议修改 我们有权在必要时修改本协议,修改后的协议将在公布时立即生效。 ); } if (showAgreementPopup === 'privacy') { return ( 隐私政策 我们非常重视您的隐私保护。 1. 信息收集 我们可能会收集您的手机号、微信信息等,用于提供服务和身份验证。 2. 信息使用 我们仅将您的信息用于提供服务、改进用户体验和保障账号安全。 3. 信息保护 我们采用行业标准的加密技术保护您的信息安全。 4. 信息共享 我们不会将您的个人信息出售或分享给第三方,除非获得您的同意或法律法规要求。 5. 联系我们 如有任何隐私相关问题,请联系我们。 ); } return null; }; // 授权登录页面(参考权大师风格) const renderAuthPage = () => { return ( {/* Logo 区域 */} {/* Logo */} {/* 品牌名 */} websopy {/* 标语 */} 构建下一代 AI 应用 {/* 底部操作区域 */} {/* 主按钮 - 手机号授权登录 */}
{/**/} {/* {authLoading ? '授权中...' : '手机号授权登录'}*/} {/**/} {/* 取消按钮 */} 取消 {/* 协议勾选 */} setAgreementChecked(!agreementChecked)} > {agreementChecked && ( )} 我已阅读并同意权大师的 { e.stopPropagation(); openAgreement('service'); }} > 《服务协议》 { e.stopPropagation(); openAgreement('privacy'); }} > 《隐私政策》
{/* 协议弹窗 */} {showAgreementPopup && ( {renderAgreementContent()} )}
); }; // 渲染状态图标 const renderStatusIcon = () => { if (loading || authLoading) return ( ); if (confirmed) return ( ); if (error && !needAuth) return ( ); if (needAuth) { // 需要授权时显示授权页面 return renderAuthPage(); } return ( ); }; // 获取标题 const getTitle = () => { if (loading || authLoading) return '正在处理...'; if (confirmed) return '登录确认成功'; if (needAuth) return ''; // 授权页面有自己的标题 if (error) return '登录确认失败'; return loginMethod === 'url' ? '扫码登录确认' : '确认登录'; }; // 获取描述 const getDescription = () => { if (loading) return '请稍候,正在为您确认登录'; if (authLoading) return '正在授权登录...'; if (confirmed) return '您已成功确认登录,网页端将自动登录'; if (needAuth) return ''; // 授权页面有自己的描述 if (error) return error; if (loginMethod === 'url') { return '检测到登录请求,是否确认登录?'; } const displayName = userInfo?.nickname || userInfo?.username || '当前用户'; return `确认使用 ${displayName} 登录网页端?`; }; // 渲染操作按钮 const renderActions = () => { // 需要授权登录时,按钮在授权页面内部渲染 if (needAuth) { return null; } if (loading) { return ( 确认中... ); } if (confirmed) { return ( 完成 ); } if (error) { return ( 重试 扫码其他二维码 取消 ); } if (loginMethod === 'scan') { return ( 确认登录 取消 ); } return ( 确认登录 ); }; // 如果是授权页面,直接返回授权页面 if (needAuth) { return renderAuthPage(); } return ( {/* Logo/品牌区域 */} 🔐 WebSoft Platform {/* 主要内容卡片 */} {/* 状态图标 */} {renderStatusIcon()} {/* 标题 */} {getTitle() && ( {getTitle()} )} {/* 描述 */} {getDescription() && ( {getDescription()} )} {/* 用户信息 */} {!loading && !confirmed && !error && !needAuth && userInfo && ( {userInfo.avatar ? ( ) : ( )} {userInfo.nickname || userInfo.username || '用户'} ID: {userInfo.userId} )} {/* Token 信息 */} {token && !loading && !confirmed && !needAuth && ( 登录令牌:{token.substring(0, 20)}...{token.substring(token.length - 10)} )} {/* 操作按钮 */} {renderActions()} {/* 安全提示 */} {!needAuth && ( 安全提示 请确认这是您本人的登录操作。如果不是,请点击取消并检查账户安全。 )} {/* 底部说明 */} 如有问题,请联系管理员 ); }; export default QRConfirmPage;