@@ -5,6 +5,9 @@ import { Success, Failure, Tips, User } 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" ;
/**
* 扫码登录确认页面
@@ -18,32 +21,52 @@ import { loginByOpenId } from '@/api/passport/wx-login';
* - 扫码后 URL: `https://websopy.websoft.top/wx-scan?token=xxx`
* - 小程序接收到参数后自动确认登录
*
* 登录流程( 2026-04-07 实现 ) :
* 登录流程( 2026-04-08 更新 ) :
* 1. 用户扫码 → 进入 qr-confirm 页面
* 2. 页面立即调用 wx.login() 获取 code
* 3. 用 code 调用 /api/wx-login/loginByOpenId 获取/验证用户身份
* 4. 如果用户不存在 → 跳转到用户页引导注册
* 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 ;
}
const QRConfirmPage : React.FC = ( ) = > {
const router = useRouter ( ) ;
// 移除 useUser 依赖,改用 wx.login() + loginByOpenId 方式验证用户身份
const [ loading , setLoading ] = useState ( false ) ;
const [ confirmed , setConfirmed ] = useState ( false ) ;
const [ error , setError ] = useState < string > ( '' ) ;
const [ token , setToken ] = useState < string > ( '' ) ;
const [ loginMethod , setLoginMethod ] = useState < 'scan' | 'url' > ( 'url' ) ;
const [ userInfo , setUserInfo ] = useState < any > ( null ) ;
const [ needAuth , setNeedAuth ] = useState ( false ) ; // 是否需要手机号授权
const [ authLoading , setAuthLoading ] = useState ( false ) ; // 授权中状态
useEffect ( ( ) = > {
// 从 URL 参数中获取 token
const params = router . params ;
// 兼容多种参数名
// 1. 小程序码场景:?scene=xxx( 微信会将 scene 参数透传到小程序)
// 2. 直接参数:?token=xxx
// 3. URL 编码参数:?q=xxx( 扫普通链接二维码场景)
// 4. 旧版参数:?qrCodeKey=xxx
let loginToken = params . scene || params . token || params . qrCodeKey || '' ;
// 如果是 q 参数( URL 编码的完整 URL) , 需要解析
@@ -52,7 +75,6 @@ const QRConfirmPage: React.FC = () => {
const decodedUrl = decodeURIComponent ( params . q ) ;
console . log ( '[QRConfirm] 解码后的 URL:' , decodedUrl ) ;
// 解析 token
const url = new URL ( decodedUrl ) ;
loginToken = url . searchParams . get ( 'token' ) ||
url . searchParams . get ( 'qrCodeKey' ) ||
@@ -61,7 +83,6 @@ const QRConfirmPage: React.FC = () => {
setLoginMethod ( 'url' ) ;
} catch ( e ) {
console . error ( '[QRConfirm] 解析 q 参数失败:' , e ) ;
// 尝试直接使用 q 作为 token
loginToken = decodeURIComponent ( params . q ) ;
setLoginMethod ( 'url' ) ;
}
@@ -74,7 +95,6 @@ const QRConfirmPage: React.FC = () => {
console . log ( '[QRConfirm] 获取到 token:' , loginToken ) ;
// 扫码场景:自动确认登录
// scene 参数说明是扫描小程序码进来的, token 参数说明是扫码跳转过来的
if ( params . scene || params . token || params . qrCodeKey || params . q ) {
console . log ( '[QRConfirm] 检测到扫码参数,自动确认登录' ) ;
setTimeout ( ( ) = > {
@@ -88,12 +108,6 @@ const QRConfirmPage: React.FC = () => {
/**
* 自动确认登录( URL 扫码场景)
*
* 新的登录流程( 2026-04-07) :
* 1. 调用 wx.login() 获取 code
* 2. 用 code 调用后端 loginByOpenId 接口验证用户身份
* 3. 用户存在 → 调用 confirmQRLogin 确认登录
* 4. 用户不存在 → 跳转到用户页引导注册
*/
const handleAutoConfirm = async ( loginToken : string ) = > {
try {
@@ -112,7 +126,7 @@ const QRConfirmPage: React.FC = () => {
console . log ( '[QRConfirm] 调用后端 loginByOpenId...' ) ;
const wxLoginResult = await loginByOpenId ( {
code : loginResult.code ,
tenantId : 10398 // 使用固定租户ID
tenantId : 5
} ) ;
console . log ( '[QRConfirm] loginByOpenId 结果:' , wxLoginResult ) ;
@@ -126,34 +140,115 @@ const QRConfirmPage: React.FC = () => {
// 调用确认登录
await handleConfirmLogin ( loginToken , wxLoginResult . data . user ) ;
} else {
// 用户未注册,跳转到 手机号授权登录页 面
console . log ( '[QRConfirm] 用户未注册,跳转到 手机号授权登录页 面' ) ;
Taro . showToast ( {
title : '请先授权登录小程序' ,
icon : 'none' ,
duration : 2000
} ) ;
setTimeout ( ( ) = > {
// 跳转到手机号授权登录页面,登录/注册成功后返回扫码确认页面
Taro . navigateTo ( {
url : '/passport/phone-auth/index?redirect=/passport/qr-confirm'
} ) ;
} , 2000 ) ;
// 用户未注册,显示 手机号授权界 面
console . log ( '[QRConfirm] 用户未注册,显示 手机号授权界 面' ) ;
setNeedAuth ( true ) ;
setLoading ( false ) ;
}
} catch ( err : any ) {
console . error ( '[QRConfirm] 自动确认登录失败:' , err ) ;
setError ( err . message || '自动确认登录失败' ) ;
} finally {
setLoading ( false ) ;
}
} ;
/**
* 处理微信手机号授权
*/
const handleGetPhoneNumber = async ( { detail } : GetPhoneNumberEvent ) = > {
const { code , encryptedData , iv , errMsg } = detail ;
// 用户拒绝授权
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 < LoginResponse > ( {
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 ) ;
}
} ;
/**
* 确认登录
* @param loginToken - 可选的登录token, 默认使用页面token
* @param wxUserInfo - 微信登录获取的用户信息(可选)
*/
const handleConfirmLogin = async ( loginToken? : string , wxUserInfo? : any ) = > {
const confirmToken = loginToken || token ;
@@ -163,24 +258,13 @@ const QRConfirmPage: React.FC = () => {
return ;
}
// 优先使用传入的用户信息,否则尝试从本地存储获取
const currentUser = wxUserInfo || userInfo ;
if ( ! currentUser ? . userId ) {
// 没有用户信息,尝试从本地存储获取
const userId = Taro . getStorageSync ( 'UserId' ) ;
if ( ! userId ) {
setError ( '请先登录小程序' ) ;
Taro . showToast ( {
title : '请先登录小程序' ,
icon : 'none'
} ) ;
setTimeout ( ( ) = > {
Taro . switchTab ( {
url : '/pages/user/user'
} ) ;
} , 1500 ) ;
setNeedAuth ( true ) ;
return ;
}
currentUser && ( currentUser . userId = Number ( userId ) ) ;
@@ -200,25 +284,22 @@ const QRConfirmPage: React.FC = () => {
}
} ) ;
// 根据 status 判断成功: confirmed 表示登录成功
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
} ) ;
// 3秒后自动关闭或返回
setTimeout ( ( ) = > {
// 尝试返回上一页,如果没有则关闭
const pages = Taro . getCurrentPages ( ) ;
if ( pages . length > 1 ) {
Taro . navigateBack ( ) ;
} else {
// 小程序场景下,提示用户回到 PC 端
Taro . showModal ( {
title : '登录成功' ,
content : '请回到电脑端刷新页面' ,
@@ -228,7 +309,6 @@ const QRConfirmPage: React.FC = () => {
}
} , 3000 ) ;
} else if ( result . status === 'bind_phone' || result . needBindPhone ) {
// 需要绑定手机号
Taro . showToast ( {
title : '请先绑定手机号' ,
icon : 'none'
@@ -274,7 +354,10 @@ const QRConfirmPage: React.FC = () => {
const handleRetry = ( ) = > {
setError ( '' ) ;
setConfirmed ( false ) ;
handleConfirmLogin ( ) ;
setNeedAuth ( false ) ;
if ( token ) {
handleAutoConfirm ( token ) ;
}
} ;
/**
@@ -285,12 +368,10 @@ const QRConfirmPage: React.FC = () => {
success : async ( res ) = > {
console . log ( '[QRConfirm] 扫码成功:' , res ) ;
// 解析二维码内容
let scanToken = '' ;
const qrContent = res . result ;
try {
// 尝试解析 URL
if ( qrContent . includes ( 'http' ) ) {
const url = new URL ( qrContent ) ;
scanToken = url . searchParams . get ( 'token' ) ||
@@ -298,13 +379,11 @@ const QRConfirmPage: React.FC = () => {
'' ;
}
// 尝试解析 JSON
if ( ! scanToken && qrContent . startsWith ( '{' ) ) {
const parsed = JSON . parse ( qrContent ) ;
scanToken = parsed . token || parsed . qrCodeKey || '' ;
}
// 直接作为 token
if ( ! scanToken && qrContent . length >= 32 ) {
scanToken = qrContent ;
}
@@ -312,6 +391,8 @@ const QRConfirmPage: React.FC = () => {
if ( scanToken ) {
setToken ( scanToken ) ;
setLoginMethod ( 'scan' ) ;
setNeedAuth ( false ) ;
setError ( '' ) ;
handleConfirmLogin ( scanToken ) ;
} else {
setError ( '无效的二维码内容' ) ;
@@ -330,8 +411,9 @@ const QRConfirmPage: React.FC = () => {
} ) ;
} ;
// 渲染状态:加载中
const renderLoading = ( ) = > (
// 渲染状态图标
const renderStatusIcon = ( ) = > {
if ( loading || authLoading ) return (
< View className = "mb-6" >
< View className = "w-16 h-16 mx-auto flex items-center justify-center" >
< Loading className = "text-blue-500" / >
@@ -339,8 +421,7 @@ const QRConfirmPage: React.FC = () => {
< / View >
) ;
// 渲染状态:成功
const renderSuccess = ( ) = > (
if ( confirmed ) return (
< View className = "mb-6" >
< View className = "w-16 h-16 mx-auto bg-green-100 rounded-full flex items-center justify-center" >
< Success className = "text-green-500" size = "32" / >
@@ -348,8 +429,7 @@ const QRConfirmPage: React.FC = () => {
< / View >
) ;
// 渲染状态:错误
const renderError = ( ) = > (
if ( error && ! needAuth ) return (
< View className = "mb-6" >
< View className = "w-16 h-16 mx-auto bg-red-100 rounded-full flex items-center justify-center" >
< Failure className = "text-red-500" size = "32" / >
@@ -357,19 +437,30 @@ const QRConfirmPage: React.FC = () => {
< / View >
) ;
// 渲染状态:初始(用户未扫码)
const renderInitial = ( ) = > (
if ( needAuth ) return (
< View className = "mb-6" >
< View className = "w-16 h-16 mx-auto bg-green-100 rounded-full flex items-center justify-center" >
< svg width = "32" height = "32" viewBox = "0 0 24 24" fill = "#22c55e" >
< path d = "M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 0 0 .167-.054l1.903-1.114a.864.864 0 0 1 .717-.098 10.16 10.16 0 0 0 2.837.403c.276 0 .543-.027.811-.05-.857-2.578.157-4.972 1.932-6.446 1.703-1.415 3.882-1.98 5.853-1.838-.576-3.583-4.196-6.348-8.596-6.348zM5.785 5.991c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178A1.17 1.17 0 0 1 4.623 7.17c0-.651.52-1.18 1.162-1.18zm5.813 0c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178 1.17 1.17 0 0 1-1.162-1.178c0-.651.52-1.18 1.162-1.18zm5.34 2.867c-1.797-.052-3.746.512-5.28 1.786-1.72 1.428-2.687 3.72-1.78 6.22.942 2.453 3.666 4.229 6.884 4.229.826 0 1.622-.12 2.361-.336a.722.722 0 0 1 .598.082l1.584.926a.272.272 0 0 0 .14.047c.134 0 .24-.111.24-.247 0-.06-.023-.12-.038-.177l-.327-1.233a.582.582 0 0 1-.023-.156.49.49 0 0 1 .201-.398C23.024 18.48 24 16.82 24 14.98c0-3.21-2.931-5.837-6.656-6.088V8.89c-.135-.01-.269-.03-.407-.03zm-2.53 3.274c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.97-.982zm4.844 0c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.969-.982z" / >
< / svg >
< / View >
< / View >
) ;
return (
< View className = "mb-6" >
< View className = "w-16 h-16 mx-auto bg-blue-100 rounded-full flex items-center justify-center" >
< User size = "32" className = "text-blue-500" / >
< / View >
< / View >
) ;
} ;
// 获取标题
const getTitle = ( ) = > {
if ( loading ) return '正在确认登录 ...' ;
if ( loading || authLoading ) return '正在处理 ...' ;
if ( confirmed ) return '登录确认成功' ;
if ( needAuth ) return '首次登录授权' ;
if ( error ) return '登录确认失败' ;
return loginMethod === 'url' ? '扫码登录确认' : '确认登录' ;
} ;
@@ -377,7 +468,9 @@ const QRConfirmPage: React.FC = () => {
// 获取描述
const getDescription = ( ) = > {
if ( loading ) return '请稍候,正在为您确认登录' ;
if ( authLoading ) return '正在授权登录...' ;
if ( confirmed ) return '您已成功确认登录,网页端将自动登录' ;
if ( needAuth ) return '检测到您是首次使用,请授权手机号完成注册并登录' ;
if ( error ) return error ;
if ( loginMethod === 'url' ) {
return '检测到登录请求,是否确认登录?' ;
@@ -386,71 +479,37 @@ const QRConfirmPage: React.FC = () => {
return ` 确认使用 ${ displayName } 登录网页端? ` ;
} ;
// 渲染操作按钮
const renderActions = ( ) = > {
// 需要授权登录
if ( needAuth ) {
return (
< View className = "qr-confirm-page min-h-screen bg-gradient-to-b from-blue-50 to-white" >
< View className = "p-4" >
{ /* Logo/品牌区域 */ }
< View className = "text-center pt-8 pb-6" >
< View className = "w-20 h-20 mx-auto bg-white rounded-2xl shadow-lg flex items-center justify-center mb-4" >
< Text className = "text-3xl" > 🔐 < / Text >
< / View >
< Text className = "text-gray-400 text-sm" > WebSoft Platform < / Text >
< / View >
{ /* 主要内容卡片 */ }
< Card className = "bg-white rounded-2xl shadow-xl -mt-4" >
< View className = "p-6 text-center" >
{ /* 状态图标 */ }
{ loading ? renderLoading ( ) : confirmed ? renderSuccess ( ) : error ? renderError ( ) : renderInitial ( ) }
{ /* 标题 */ }
< Text className = "text-xl font-bold text-gray-800 mb-2 block" >
{ getTitle ( ) }
< / Text >
{ /* 描述 */ }
< Text className = "text-gray-600 mb-6 block text-sm" >
{ getDescription ( ) }
< / Text >
{ /* 用户信息 */ }
{ ! loading && ! confirmed && ! error && userInfo && (
< View className = "bg-gray-50 rounded-xl p-4 mb-6" >
< View className = "flex items-center justify-center" >
{ userInfo . avatar ? (
< View
className = "w-12 h-12 rounded-full bg-blue-100 mr-3 overflow-hidden"
style = { { backgroundImage : ` url( ${ userInfo . avatar } ) ` , backgroundSize : 'cover' } }
/ >
) : (
< View className = "w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center mr-3" >
< User className = "text-blue-500" size = "20" / >
< / View >
) }
< View className = "text-left" >
< Text className = "text-sm font-medium text-gray-800 block" >
{ userInfo . nickname || userInfo . username || '用户' }
< / Text >
< Text className = "text-xs text-gray-500 block" >
ID : { userInfo . userId }
< / Text >
< / View >
< / View >
< / View >
) }
{ /* Token 信息 */ }
{ token && ! loading && ! confirmed && (
< View className = "bg-blue-50 rounded-lg p-3 mb-4" >
< Text className = "text-xs text-blue-600" >
登 录 令 牌 : { token . substring ( 0 , 20 ) } . . . { token . substring ( token . length - 10 ) }
< / Text >
< / View >
) }
{ /* 操作按钮 */ }
< View className = "space-y-3" >
{ loading ? (
< Button
type = "success"
size = "large"
className = "w-full rounded-xl"
open-type = "getPhoneNumber"
onGetPhoneNumber = { handleGetPhoneNumber }
disabled = { authLoading }
>
{ authLoading ? '授权中...' : '微信手机号一键授权' }
< / Button >
< Button
type = "default"
size = "large"
onClick = { handleCancel }
className = "w-full rounded-xl"
fill = "none"
>
取 消
< / Button >
< / View >
) ;
}
if ( loading ) {
return (
< Button
type = "default"
size = "large"
@@ -459,7 +518,11 @@ const QRConfirmPage: React.FC = () => {
>
确 认 中 . . .
< / Button >
) : confirmed ? (
) ;
}
if ( confirmed ) {
return (
< Button
type = "success"
size = "large"
@@ -468,7 +531,11 @@ const QRConfirmPage: React.FC = () => {
>
完 成
< / Button >
) : error ? (
) ;
}
if ( error ) {
return (
< View className = "space-y-2" >
< Button
type = "primary"
@@ -496,7 +563,11 @@ const QRConfirmPage: React.FC = () => {
取 消
< / Button >
< / View >
) : loginMethod === 'scan' ? (
) ;
}
if ( loginMethod === 'scan' ) {
return (
< View >
< Button
type = "primary"
@@ -517,8 +588,10 @@ const QRConfirmPage: React.FC = () => {
取 消
< / Button >
< / View >
) : (
// URL 扫码场景:自动确认中
) ;
}
return (
< Button
type = "primary"
size = "large"
@@ -527,12 +600,78 @@ const QRConfirmPage: React.FC = () => {
>
确 认 登 录
< / Button >
) }
) ;
} ;
return (
< View className = "qr-confirm-page min-h-screen bg-gradient-to-b from-blue-50 to-white" >
< View className = "p-4" >
{ /* Logo/品牌区域 */ }
< View className = "text-center pt-8 pb-6" >
< View className = "w-20 h-20 mx-auto bg-white rounded-2xl shadow-lg flex items-center justify-center mb-4" >
< Text className = "text-3xl" > 🔐 < / Text >
< / View >
< Text className = "text-gray-400 text-sm" > WebSoft Platform < / Text >
< / View >
{ /* 主要内容卡片 */ }
< Card className = "bg-white rounded-2xl shadow-xl -mt-4" >
< View className = "p-6 text-center" >
{ /* 状态图标 */ }
{ renderStatusIcon ( ) }
{ /* 标题 */ }
< Text className = "text-xl font-bold text-gray-800 mb-2 block" >
{ getTitle ( ) }
< / Text >
{ /* 描述 */ }
< Text className = "text-gray-600 mb-6 block text-sm" >
{ getDescription ( ) }
< / Text >
{ /* 用户信息 */ }
{ ! loading && ! confirmed && ! error && ! needAuth && userInfo && (
< View className = "bg-gray-50 rounded-xl p-4 mb-6" >
< View className = "flex items-center justify-center" >
{ userInfo . avatar ? (
< View
className = "w-12 h-12 rounded-full bg-blue-100 mr-3 overflow-hidden"
style = { { backgroundImage : ` url( ${ userInfo . avatar } ) ` , backgroundSize : 'cover' } }
/ >
) : (
< View className = "w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center mr-3" >
< User className = "text-blue-500" size = "20" / >
< / View >
) }
< View className = "text-left" >
< Text className = "text-sm font-medium text-gray-800 block" >
{ userInfo . nickname || userInfo . username || '用户' }
< / Text >
< Text className = "text-xs text-gray-500 block" >
ID : { userInfo . userId }
< / Text >
< / View >
< / View >
< / View >
) }
{ /* Token 信息 */ }
{ token && ! loading && ! confirmed && ! needAuth && (
< View className = "bg-blue-50 rounded-lg p-3 mb-4" >
< Text className = "text-xs text-blue-600" >
登 录 令 牌 : { token . substring ( 0 , 20 ) } . . . { token . substring ( token . length - 10 ) }
< / Text >
< / View >
) }
{ /* 操作按钮 */ }
{ renderActions ( ) }
< / View >
< / Card >
{ /* 安全提示 */ }
{ ! needAuth && (
< Card className = "bg-yellow-50 border border-yellow-200 rounded-xl mt-4" >
< View className = "p-4" >
< View className = "flex items-start" >
@@ -548,6 +687,7 @@ const QRConfirmPage: React.FC = () => {
< / View >
< / View >
< / Card >
) }
{ /* 底部说明 */ }
< View className = "text-center mt-6 pb-8" >