@@ -0,0 +1,338 @@
import React , { useState , useEffect } from 'react' ;
import { View , Text , Button , Image } from '@tarojs/components' ;
import Taro , { useRouter } from '@tarojs/taro' ;
import { SERVER_API_URL } from "@/utils/server" ;
/**
* 邀请加入确认页面
*
* 用户扫描邀请二维码后,打开此小程序页面确认加入应用
*/
// 微信获取手机号回调参数类型
interface GetPhoneNumberDetail {
code? : string ;
encryptedData? : string ;
iv? : string ;
errMsg : string ;
}
interface GetPhoneNumberEvent {
detail : GetPhoneNumberDetail ;
}
// 邀请信息类型
interface InviteInfo {
appId : string ;
appName : string ;
appLogo : string ;
inviterName : string ;
roleName : string ;
}
// 协议类型
type AgreementType = 'service' | 'privacy' ;
const InvitePage : React.FC = ( ) = > {
const router = useRouter ( ) ;
const [ loading , setLoading ] = useState ( true ) ;
const [ inviteInfo , setInviteInfo ] = useState < InviteInfo | null > ( null ) ;
const [ authLoading , setAuthLoading ] = useState ( false ) ;
const [ agreementChecked , setAgreementChecked ] = useState ( false ) ;
const [ token , setToken ] = useState < string > ( '' ) ;
const [ error , setError ] = useState < string > ( '' ) ;
useEffect ( ( ) = > {
// 从 URL 参数中获取 token
const params = router . params ;
let inviteToken = params . scene || params . token || params . qrCodeKey || '' ;
// 兼容 q 参数(URL 编码的完整 URL)
if ( params . q && ! inviteToken ) {
try {
const decodedUrl = decodeURIComponent ( params . q ) ;
const url = new URL ( decodedUrl ) ;
inviteToken = url . searchParams . get ( 'token' ) || url . searchParams . get ( 'qrCodeKey' ) || '' ;
} catch ( e ) {
inviteToken = decodeURIComponent ( params . q ) ;
}
}
setToken ( inviteToken ) ;
// 获取邀请信息
if ( inviteToken ) {
fetchInviteInfo ( inviteToken ) ;
} else {
setError ( '无效的邀请链接' ) ;
setLoading ( false ) ;
}
} , [ router . params ] ) ;
/**
* 获取邀请信息
*/
const fetchInviteInfo = async ( inviteToken : string ) = > {
try {
const res = await Taro . request ( {
url : ` ${ SERVER_API_URL } /api/_app/developer/invite/info ` ,
method : 'GET' ,
data : { token : inviteToken } ,
header : { 'content-type' : 'application/json' }
} ) ;
if ( res . data . code === 200 || res . data . code === 0 ) {
setInviteInfo ( res . data . data ) ;
} else {
setError ( res . data . message || '邀请信息获取失败' ) ;
}
} catch ( err : any ) {
setError ( err . message || '网络请求失败' ) ;
} finally {
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 handleJoinApp ( code , encryptedData , iv ) ;
} ;
/**
* 加入应用
*/
const handleJoinApp = async ( phoneCode : string , encryptedData? : string , iv? : string ) = > {
try {
setAuthLoading ( true ) ;
const res = await Taro . request ( {
url : ` ${ SERVER_API_URL } /api/_app/invite/accept ` ,
method : 'POST' ,
data : {
token ,
code : phoneCode ,
encryptedData ,
iv
} ,
header : { 'content-type' : 'application/json' }
} ) ;
if ( res . data . code === 200 || res . data . code === 0 ) {
Taro . showToast ( { title : '加入成功' , icon : 'success' , duration : 1500 } ) ;
setTimeout ( ( ) = > {
// 跳转到应用页面或首页
Taro . switchTab ( { url : '/pages/index/index' } ) ;
} , 1500 ) ;
} else {
Taro . showToast ( { title : res.data.message || '加入失败' , icon : 'none' } ) ;
}
} catch ( err : any ) {
Taro . showToast ( { title : err.message || '加入失败' , icon : 'error' } ) ;
} finally {
setAuthLoading ( false ) ;
}
} ;
/**
* 拒绝邀请
*/
const handleReject = ( ) = > {
const pages = Taro . getCurrentPages ( ) ;
if ( pages . length > 1 ) {
Taro . navigateBack ( ) ;
} else {
Taro . switchTab ( { url : '/pages/index/index' } ) ;
}
} ;
// 打开协议页面
const openAgreement = ( type : AgreementType ) = > {
const urlMap = {
service : 'https://websopy.websoft.top/agreement' ,
privacy : 'https://websopy.websoft.top/privacy' ,
} ;
const targetUrl = encodeURIComponent ( urlMap [ type ] ) ;
Taro . navigateTo ( { url : ` /passport/webview/index?url= ${ targetUrl } ` } ) ;
} ;
// 加载中
if ( loading ) {
return (
< View className = "min-h-screen flex items-center justify-center" style = { { background : 'linear-gradient(135deg, #0a0a1a 0%, #0d1528 50%, #0a1520 100%)' } } >
< View className = "flex flex-col items-center" >
< View style = { {
width : '40px' , height : '40px' , border : '3px solid rgba(59, 130, 246, 0.3)' ,
borderTopColor : '#3b82f6' , borderRadius : '50%' ,
animation : 'spin 1s linear infinite' ,
} } / >
< Text style = { { color : 'rgba(255, 255, 255, 0.6)' , marginTop : '16px' , fontSize : '14px' } } > 加 载 中 . . . < / Text >
< / View >
< / View >
) ;
}
// 错误状态
if ( error ) {
return (
< View className = "min-h-screen flex items-center justify-center" style = { { background : 'linear-gradient(135deg, #0a0a1a 0%, #0d1528 50%, #0a1520 100%)' } } >
< View className = "flex flex-col items-center px-8" >
< Text style = { { fontSize : '64px' , marginBottom : '20px' } } > ❌ < / Text >
< Text style = { { color : '#ef4444' , fontSize : '16px' , textAlign : 'center' , marginBottom : '24px' } } > { error } < / Text >
< Button onClick = { handleReject } style = { {
background : 'rgba(255, 255, 255, 0.1)' , color : '#fff' ,
borderRadius : '20px' , fontSize : '14px' , padding : '8px 24px'
} } >
返 回 首 页
< / Button >
< / View >
< / View >
) ;
}
// 邀请确认页面
return (
< View className = "min-h-screen relative overflow-hidden" style = { { background : 'linear-gradient(135deg, #0a0a1a 0%, #0d1528 50%, #0a1520 100%)' } } >
{ /* 背景效果 */ }
< View style = { {
position : 'absolute' , top : 0 , left : 0 , right : 0 , bottom : 0 ,
backgroundImage : ` linear-gradient(rgba(59, 130, 246, 0.08) 1px, transparent 1px), linear-gradient(90deg, rgba(59, 130, 246, 0.08) 1px, transparent 1px) ` ,
backgroundSize : '50px 50px' , opacity : 0.5 ,
} } / >
{ /* 渐变光晕 */ }
< View style = { {
position : 'absolute' , top : '-30%' , left : '-20%' , width : '60%' , height : '60%' ,
background : 'radial-gradient(circle, rgba(59, 130, 246, 0.25) 0%, transparent 70%)' ,
filter : 'blur(40px)' ,
} } / >
{ /* 主内容区域 */ }
< View className = "relative z-10 flex flex-col items-center justify-center min-h-screen px-8" >
{ /* 邀请卡片 */ }
< View style = { {
width : '100%' , maxWidth : '360px' , background : 'rgba(255, 255, 255, 0.05)' ,
borderRadius : '24px' , border : '1px solid rgba(255, 255, 255, 0.1)' ,
padding : '32px 24px' , backdropFilter : 'blur(20px)' ,
} } >
{ /* 应用信息 */ }
< View className = "flex flex-col items-center mb-8" >
< View style = { {
width : '80px' , height : '80px' , borderRadius : '20px' ,
background : 'linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(99, 102, 241, 0.2))' ,
border : '1px solid rgba(59, 130, 246, 0.3)' ,
display : 'flex' , alignItems : 'center' , justifyContent : 'center' ,
marginBottom : '16px' , overflow : 'hidden' ,
} } >
{ inviteInfo ? . appLogo ? (
< Image src = { inviteInfo . appLogo } style = { { width : '60px' , height : '60px' , borderRadius : '12px' } } / >
) : (
< Text style = { { fontSize : '36px' } } > 🚀 < / Text >
) }
< / View >
< Text style = { {
fontSize : '22px' , fontWeight : '700' , color : '#fff' , marginBottom : '8px' ,
} } >
{ inviteInfo ? . appName || 'Websopy' }
< / Text >
< View style = { {
padding : '4px 12px' , borderRadius : '12px' ,
background : 'rgba(59, 130, 246, 0.2)' , border : '1px solid rgba(59, 130, 246, 0.3)' ,
} } >
< Text style = { { color : '#3b82f6' , fontSize : '12px' } } >
邀 请 你 加 入 应 用
< / Text >
< / View >
< / View >
{ /* 邀请人信息 */ }
< View className = "flex items-center justify-center mb-6" >
< Text style = { { color : 'rgba(255, 255, 255, 0.5)' , fontSize : '13px' } } >
邀 请 人 : { inviteInfo ? . inviterName || '某位用户' }
< / Text >
< / View >
{ /* 分隔线 */ }
< View style = { {
height : '1px' , background : 'linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent)' ,
marginBottom : '24px' ,
} } / >
{ /* 协议勾选 */ }
< View className = "flex items-center justify-center mb-4" >
< View style = { { padding : '8px' , marginRight : '4px' } } onClick = { ( ) = > setAgreementChecked ( ! agreementChecked ) } >
< View style = { {
width : '18px' , height : '18px' , borderRadius : '4px' ,
border : agreementChecked ? 'none' : '1px solid rgba(255, 255, 255, 0.3)' ,
background : agreementChecked ? 'linear-gradient(135deg, #1d4ed8, #3b82f6)' : 'transparent' ,
display : 'flex' , alignItems : 'center' , justifyContent : 'center' ,
} } >
{ agreementChecked && < Text style = { { color : '#fff' , fontSize : '12px' } } > ✓ < / Text > }
< / View >
< / View >
< Text style = { { color : 'rgba(255, 255, 255, 0.5)' , fontSize : '12px' } } > 我 已 阅 读 并 同 意 < / Text >
< Text style = { { color : '#3b82f6' , fontSize : '12px' , padding : '4px 2px' } } onClick = { ( ) = > openAgreement ( 'service' ) } > 《 服 务 协 议 》 < / Text >
< Text style = { { color : 'rgba(255, 255, 255, 0.5)' , fontSize : '12px' } } > 和 < / Text >
< Text style = { { color : '#3b82f6' , fontSize : '12px' , padding : '4px 2px' } } onClick = { ( ) = > openAgreement ( 'privacy' ) } > 《 隐 私 政 策 》 < / Text >
< / View >
{ /* 主按钮 */ }
< View style = { {
width : '100%' , background : 'linear-gradient(135deg, #1d4ed8 0%, #3b82f6 50%, #6366f1 100%)' ,
borderRadius : '24px' , padding : '2px' ,
boxShadow : '0 0 30px rgba(59, 130, 246, 0.4)' ,
} } >
< Button
open-type = "getPhoneNumber"
onGetPhoneNumber = { handleGetPhoneNumber }
disabled = { authLoading }
style = { {
width : '100%' , height : '48px' , fontSize : '16px' , fontWeight : '600' ,
color : '#fff' , background : 'transparent' , borderRadius : '22px' ,
border : 'none' , padding : '0' , boxSizing : 'border-box' , lineHeight : '48px' ,
} }
>
{ authLoading ? '处理中...' : '微信手机号快速加入' }
< / Button >
< / View >
{ /* 拒绝按钮 */ }
< View style = { { display : 'flex' , alignItems : 'center' , justifyContent : 'center' , marginTop : '16px' } } onClick = { handleReject } >
< Text style = { { color : 'rgba(255, 255, 255, 0.4)' , fontSize : '14px' } } > 暂 不 加 入 < / Text >
< / View >
< / View >
{ /* 底部安全标识 */ }
< View style = { { position : 'absolute' , bottom : '40px' , display : 'flex' , alignItems : 'center' , gap : '8px' } } >
< View style = { { width : '6px' , height : '6px' , borderRadius : '50%' , background : '#3b82f6' , boxShadow : '0 0 10px rgba(59, 130, 246, 0.8)' } } / >
< Text style = { { color : 'rgba(255, 255, 255, 0.3)' , fontSize : '11px' } } > 安 全 加 密 连 接 < / Text >
< View style = { { width : '6px' , height : '6px' , borderRadius : '50%' , background : '#6366f1' , boxShadow : '0 0 10px rgba(99, 102, 241, 0.8)' } } / >
< / View >
< / View >
< / View >
) ;
} ;
export default InvitePage ;