Files
websopy-taro/src/passport/invite/index.tsx
赵忠林 a568b9d9df refactor(api): 移除未使用的配置接口及相关Hook
- 从cmsWebsiteField接口中删除configWebsiteField方法
- 删除useConfig自定义Hook及其引用
- 更新about页面,去除对config对象和useConfig的使用
- 修正invite模块请求URL,统一加上/api前缀
2026-04-11 19:50:52 +08:00

357 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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";
// 邀请相关接口使用独立的 API 域名
const INVITE_API_URL = 'https://websopy-api.websoft.top';
import { TenantId } from "@/config/app";
/**
* 邀请加入确认页面
*
* 用户扫描邀请二维码后,打开此小程序页面确认加入应用
*/
// 微信获取手机号回调参数类型
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 {
console.log('开始获取邀请信息, token:', inviteToken);
console.log('请求URL:', `${INVITE_API_URL}/api/developer/invite/info?token=${encodeURIComponent(inviteToken)}`);
console.log('请求头:', { 'content-type': 'application/json', TenantId });
const res = await Taro.request({
url: `${INVITE_API_URL}/api/developer/invite/info?token=${encodeURIComponent(inviteToken)}`,
method: 'GET',
header: {
'content-type': 'application/json',
TenantId
}
});
console.log('邀请信息接口响应:', res);
console.log('响应数据:', res.data);
if (res.data.code === 200 || res.data.code === 0) {
setInviteInfo(res.data.data);
} else {
console.error('接口返回错误:', res.data.message, 'code:', res.data.code);
setError(res.data.message || '邀请信息获取失败');
}
} catch (err: any) {
console.error('获取邀请信息异常:', err);
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: `${INVITE_API_URL}/api/developer/invite/accept`,
method: 'POST',
data: {
token,
code: phoneCode,
encryptedData,
iv
},
header: {
'content-type': 'application/json',
TenantId
}
});
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;