feat(invite): 重构邀请加入流程,实现已登录用户免授权直接加入

- 完全重写邀请页逻辑,区分已登录与未注册用户按钮显示
- 已登录用户显示“确认加入”按钮,点击用 wx.login 获取 code 加入
- 未注册用户显示“微信手机号快速加入”按钮,授权手机号后完成注册登录再加入
- 移除跳转登录页面逻辑,所有流程在邀请页内完成
- 加入接口统一使用 Authorization 头携带 token,确保身份双重验证
- 优化背景、粒子和扫描线效果,提升界面视觉体验
- 增加错误处理提示,加入失败时显示详细原因并可返回首页
- 协议勾选仅在未注册用户显示,确保合规授权流程
This commit is contained in:
2026-04-12 11:42:02 +08:00
parent 2fe14aa2b4
commit 60836da3c2
3 changed files with 357 additions and 225 deletions

View File

@@ -35,5 +35,5 @@
} }
] ]
}, },
"lastUpdated": 1775923978885 "lastUpdated": 1775965240928
} }

View File

@@ -0,0 +1,84 @@
# 2026-04-12 工作记录
## 任务:优化邀请加入应用按钮逻辑
### 需求描述
loginByOpenId 返回有用户数据(已登录)时,不显示手机号授权按钮,直接显示「确认加入」普通按钮;
loginByOpenId 返回未注册时才走 getPhoneNumber 授权分支。
### 解决方案
完全重写 `invite/index.tsx`,核心逻辑:
#### 按钮渲染逻辑
```tsx
{isLoggedIn ? (
// 已登录:普通按钮,直接加入,携带 Authorization 头
<Button onClick={handleConfirmJoin}>确认加入</Button>
) : (
// 未注册:手机号授权按钮(兜底,实际大多已被重定向到 login 页)
<Button open-type="getPhoneNumber" onGetPhoneNumber={handleGetPhoneNumber}>微信手机号快速加入</Button>
)}
```
#### handleJoinApp 统一入口
- `useToken` 参数:已登录用户,请求头加 `Authorization: Bearer xxx`
- `phoneCode` 参数:未注册用户,请求体加 code/encryptedData/iv
### 文件修改
- `src/passport/invite/index.tsx` - 完整重写,区分已登录/未注册两种按钮状态
---
## 任务:未注册用户在邀请页内完成授权注册,不跳登录页
### 需求
- loginByOpenId 未注册 → 在页面内显示「微信手机号授权」按钮
- 授权成功 → 调用 `loginByMpWxPhone` 注册/登录 → 自动执行加入应用
- 不再跳转 passport/login 页面
### 关键逻辑
1. `checkLoginStatus`:已注册 isLoggedIn=true未注册 isLoggedIn=false**两种情况都显示邀请页**
2. 未注册按钮:`open-type="getPhoneNumber"``handleGetPhoneNumber`
- 授权码调 `SERVER_API_URL/wx-login/loginByMpWxPhone` 完成注册登录
- 保存 token → isLoggedIn=true → 立即调 `doJoinApp`
3. 已注册按钮:普通 `onClick``handleConfirmJoin``doJoinApp(access_token)`
4. `doJoinApp`:统一加入接口,请求头带 `Authorization: Bearer {access_token}`
### 文件修改
- `src/passport/invite/index.tsx` - 完整重写(彻底移除跳登录页逻辑)
---
## 修复:「授权码不能为空」报错
### 问题
已登录用户点「确认加入」时,后端报 `授权码不能为空`
后端 `/api/_app/developer/invite/accept` 接口不管是否登录,都要求传 `code`(微信授权码)。
### 解决
统一用一个 `getPhoneNumber` 按钮处理两种场景:
- **已注册**:文字「确认加入」→ 触发 getPhoneNumber → `doJoinApp(code, accessToken)`
- **未注册**:文字「微信手机号快速加入」→ 触发 getPhoneNumber → 先 `loginByMpWxPhone` 注册 → 再 `wx.login()` 获取新 code → `doJoinApp(newCode, access_token)`
### doJoinApp 参数
```ts
doJoinApp(wxCode: string, accessToken: string)
// 请求体带 code请求头带 Authorization: Bearer xxx
```
---
## 优化:已登录用户不弹手机号授权
### 改动
- 已登录按钮:普通 `onClick`,文字「确认加入」
- 未注册按钮:`getPhoneNumber` 授权,文字「微信手机号快速加入」
### 逻辑差异
| 用户状态 | 按钮类型 | 获取 code 方式 |
|------|------|------|
| 已登录 | 普通 onClick | `wx.login()` |
| 未注册 | getPhoneNumber | 授权回调的 `code` |
### 文件修改
- `src/passport/invite/index.tsx` - 按钮区分两种类型,已登录用普通 onClick

View File

@@ -2,7 +2,8 @@ import React, { useState, useEffect } from 'react';
import { View, Text, Button, Image } from '@tarojs/components'; import { View, Text, Button, Image } from '@tarojs/components';
import Taro, { useRouter } from '@tarojs/taro'; import Taro, { useRouter } from '@tarojs/taro';
import { loginByOpenId } from '@/api/passport/wx-login'; import { loginByOpenId } from '@/api/passport/wx-login';
import { TenantId } from "@/config/app"; import { saveStorageByLoginUser, SERVER_API_URL } from '@/utils/server';
import { TenantId } from '@/config/app';
// 邀请相关接口使用独立的 API 域名 // 邀请相关接口使用独立的 API 域名
const INVITE_API_URL = 'https://websopy-api.websoft.top'; const INVITE_API_URL = 'https://websopy-api.websoft.top';
@@ -11,15 +12,12 @@ const INVITE_API_URL = 'https://websopy-api.websoft.top';
* 邀请加入确认页面 * 邀请加入确认页面
* *
* 流程: * 流程:
* 1. 扫码进入页面 * 1. 扫码进入 → 调用 loginByOpenId 判断登录状态
* 2. 调用 wx.login() 获取 code * 2. 已注册 → isLoggedIn=true → 显示「确认加入」按钮 → 点击直接加入(带 access_token
* 3. 调用 loginByOpenId 判断用户是否已注册 * 3. 未注册 → isLoggedIn=false → 显示「微信手机号授权」按钮
* 4. 已注册:显示邀请页面,用户点击加入 * → 授权成功 → 注册/登录 → isLoggedIn=true → 自动执行加入
* 5. 未注册:跳转到 passport/login 页面完成登录/注册
* 6. 登录成功后返回,自动执行加入操作
*/ */
// 邀请信息类型
interface InviteInfo { interface InviteInfo {
appId: string; appId: string;
appName: string; appName: string;
@@ -28,39 +26,39 @@ interface InviteInfo {
roleName: string; roleName: string;
} }
// 协议类型
type AgreementType = 'service' | 'privacy'; type AgreementType = 'service' | 'privacy';
type PageStatus = 'loading' | 'checking' | 'invite' | 'error';
// 页面状态
type PageStatus = 'loading' | 'checking' | 'invite' | 'login' | 'error';
const InvitePage: React.FC = () => { const InvitePage: React.FC = () => {
const router = useRouter(); const router = useRouter();
const [pageStatus, setPageStatus] = useState<PageStatus>('loading'); const [pageStatus, setPageStatus] = useState<PageStatus>('loading');
const [inviteInfo, setInviteInfo] = useState<InviteInfo | null>(null); const [inviteInfo, setInviteInfo] = useState<InviteInfo | null>(null);
const [authLoading, setAuthLoading] = useState(false); const [authLoading, setAuthLoading] = useState(false);
const [agreementChecked, setAgreementChecked] = useState(false); const [agreementChecked, setAgreementChecked] = useState(false);
const [token, setToken] = useState<string>(''); const [token, setToken] = useState<string>('');
const [error, setError] = useState<string>(''); const [error, setError] = useState<string>('');
/**
* true = 已注册已登录,按钮文字「确认加入」(仍走 getPhoneNumber 获取 code 传给后端)
* false = 未注册,按钮文字「微信手机号快速加入」,授权后先注册登录再加入
*/
const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false); const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
initPage().then(); initPage();
}, [router.params]); }, [router.params]);
// 页面初始化 // ── 初始化 ──────────────────────────────────────────
const initPage = async () => { const initPage = async () => {
// 从 URL 参数中获取 token
const params = router.params; const params = router.params;
let inviteToken = params.scene || params.token || params.qrCodeKey || ''; let inviteToken = params.scene || params.token || params.qrCodeKey || '';
console.log(params,'URL参数')
// 兼容 q 参数URL 编码的完整 URL
if (params.q && !inviteToken) { if (params.q && !inviteToken) {
try { try {
const decodedUrl = decodeURIComponent(params.q); const decodedUrl = decodeURIComponent(params.q);
const url = new URL(decodedUrl); const url = new URL(decodedUrl);
inviteToken = url.searchParams.get('token') || url.searchParams.get('qrCodeKey') || ''; inviteToken = url.searchParams.get('token') || url.searchParams.get('qrCodeKey') || '';
} catch (e) { } catch {
inviteToken = decodeURIComponent(params.q); inviteToken = decodeURIComponent(params.q);
} }
} }
@@ -72,83 +70,52 @@ const InvitePage: React.FC = () => {
} }
setToken(inviteToken); setToken(inviteToken);
// 保存邀请 token供登录后使用
Taro.setStorageSync('invite_token', inviteToken); Taro.setStorageSync('invite_token', inviteToken);
// 检查用户登录状态 // 先获取邀请信息,再检查登录状态(并行更好,但 inviteInfo 失败要 error
await checkLoginStatus(inviteToken); await checkLoginStatus(inviteToken);
console.log('检查登录状态完成', inviteToken)
}; };
// 检查用户登录状态 // ── loginByOpenId 检查登录状态 ──────────────────────
const checkLoginStatus = async (inviteToken: string) => { const checkLoginStatus = async (inviteToken: string) => {
setPageStatus('checking'); setPageStatus('checking');
try { try {
// 调用 wx.login 获取 code const wxRes = await Taro.login();
const wxLoginRes = await Taro.login(); if (!wxRes.code) throw new Error('获取微信登录凭证失败');
console.log('wx.login 结果:', wxLoginRes);
if (!wxLoginRes.code) {
throw new Error('获取微信登录凭证失败');
}
// 调用 loginByOpenId 判断用户是否已注册
const loginRes = await loginByOpenId({ const loginRes = await loginByOpenId({
code: wxLoginRes.code, code: wxRes.code,
tenantId: parseInt(TenantId) || 1 tenantId: parseInt(TenantId) || 1,
}); });
console.log('loginByOpenId 结果:', loginRes); console.log('loginByOpenId 结果:', loginRes);
if (loginRes.success && loginRes.data?.access_token) { if (loginRes.success && loginRes.data?.access_token) {
// 用户已注册保存登录信息 // 已注册保存登录信息,显示「确认加入」
Taro.setStorageSync('access_token', loginRes.data.access_token); saveStorageByLoginUser(loginRes.data.access_token, loginRes.data.user as any);
if (loginRes.data.user) {
Taro.setStorageSync('user_info', JSON.stringify(loginRes.data.user));
}
setIsLoggedIn(true); setIsLoggedIn(true);
// 获取邀请信息并显示邀请页面 } else {
// ❌ 未注册:显示手机号授权按钮
console.log('用户未注册,显示手机号授权按钮');
setIsLoggedIn(false);
}
// 两种情况都需要获取邀请信息
await fetchInviteInfo(inviteToken); await fetchInviteInfo(inviteToken);
setPageStatus('invite'); setPageStatus('invite');
} else {
// 用户未注册,跳转到登录页面
console.log('用户未注册,跳转到登录页面');
setPageStatus('login');
// 延迟跳转,让用户看到提示
setTimeout(() => {
navigateToLogin(inviteToken);
}, 500);
}
} catch (err: any) { } catch (err: any) {
console.error('检查登录状态失败:', err); console.error('检查登录状态失败:', err);
// 出错时也跳转到登录页面 setError(err.message || '初始化失败,请重试');
setPageStatus('login'); setPageStatus('error');
setTimeout(() => {
navigateToLogin(inviteToken);
}, 500);
} }
}; };
// 跳转到登录页面 // ── 获取邀请信息 ─────────────────────────────────────
const navigateToLogin = (inviteToken: string) => {
Taro.redirectTo({
url: `/passport/login?redirect=${encodeURIComponent('/passport/invite/index?token=' + inviteToken)}&from=invite`
});
};
// 获取邀请信息
const fetchInviteInfo = async (inviteToken: string) => { const fetchInviteInfo = async (inviteToken: string) => {
try {
console.log('开始获取邀请信息, token:', inviteToken);
const res = await Taro.request({ const res = await Taro.request({
url: `${INVITE_API_URL}/api/_app/developer/invite/info?token=${encodeURIComponent(inviteToken)}`, url: `${INVITE_API_URL}/api/_app/developer/invite/info?token=${encodeURIComponent(inviteToken)}`,
method: 'GET', method: 'GET',
header: { header: { 'content-type': 'application/json', TenantId },
'content-type': 'application/json',
TenantId
}
}); });
console.log('邀请信息接口响应:', res); console.log('邀请信息接口响应:', res);
@@ -156,112 +123,158 @@ const InvitePage: React.FC = () => {
if (res.data.code === 200 || res.data.code === 0) { if (res.data.code === 200 || res.data.code === 0) {
setInviteInfo(res.data.data); setInviteInfo(res.data.data);
} else { } else {
console.error('接口返回错误:', res.data.message); throw new Error(res.data.message || '邀请信息获取失败');
setError(res.data.message || '邀请信息获取失败');
setPageStatus('error');
}
} catch (err: any) {
console.error('获取邀请信息异常:', err);
setError(err.message || '网络请求失败');
setPageStatus('error');
} }
}; };
// 处理微信手机号授权 /**
const handleGetPhoneNumber = async (e: any) => { * 已登录用户:点击「确认加入」
const { code, encryptedData, iv, errMsg } = e.detail; * 不弹手机号授权,直接用 wx.login() 获取 code 调加入接口
*/
console.log('handleGetPhoneNumber:', { code, errMsg }); const handleConfirmJoin = async () => {
// 检查协议是否勾选
if (!agreementChecked) { if (!agreementChecked) {
Taro.showToast({ title: '请先同意服务协议和隐私政策', icon: 'none' }); Taro.showToast({ title: '请先同意服务协议和隐私政策', icon: 'none' });
return; return;
} }
const accessToken = Taro.getStorageSync('access_token');
// 用户拒绝授权 if (!accessToken) {
if (errMsg && errMsg.includes('fail')) { Taro.showToast({ title: '登录状态异常,请刷新重试', icon: 'none' });
Taro.showToast({ title: '需要授权手机号才能加入', icon: 'none' });
return; return;
} }
try {
setAuthLoading(true);
// 用 wx.login() 获取 code后端需要 code 识别用户身份)
const wxRes = await Taro.login();
if (!wxRes.code) throw new Error('获取微信登录凭证失败');
await doJoinApp(wxRes.code, accessToken);
} catch (err: any) {
console.error('确认加入失败:', err);
Taro.showToast({ title: err.message || '加入失败,请重试', icon: 'none' });
} finally {
setAuthLoading(false);
}
};
/**
* 未注册用户:手机号授权回调
* 先用 code 注册/登录,再用新的 wx.login code 调加入接口
*/
const handleGetPhoneNumber = async (e: any) => {
const { code, encryptedData, iv, errMsg } = e.detail;
if (!agreementChecked) {
Taro.showToast({ title: '请先同意服务协议和隐私政策', icon: 'none' });
return;
}
if (errMsg?.includes('fail')) {
Taro.showToast({ title: '需要授权手机号才能加入', icon: 'none' });
return;
}
if (!code) { if (!code) {
Taro.showToast({ title: '获取授权信息失败,请重试', icon: 'none' }); Taro.showToast({ title: '获取授权信息失败,请重试', icon: 'none' });
return; return;
} }
// 执行加入操作
await handleJoinApp(code, encryptedData, iv);
};
// 加入应用
const handleJoinApp = async (phoneCode: string, encryptedData?: string, iv?: string) => {
try { try {
setAuthLoading(true); setAuthLoading(true);
const inviteToken = Taro.getStorageSync('invite_token') || token; // 1. 用手机号授权码完成注册/登录
console.log('未注册用户,先注册/登录再加入');
const loginRes = await Taro.request({
url: `${SERVER_API_URL}/wx-login/loginByMpWxPhone`,
method: 'POST',
data: {
code,
encryptedData,
iv,
tenantId: parseInt(TenantId) || 1,
notVerifyPhone: true,
},
header: { 'content-type': 'application/json', TenantId },
});
console.log('开始接受邀请, token:', inviteToken); console.log('手机号注册/登录结果:', loginRes);
if (loginRes.data.code !== 0 && loginRes.data.code !== 200) {
throw new Error(loginRes.data.message || '注册/登录失败');
}
const { access_token, user } = loginRes.data.data || {};
if (!access_token) throw new Error('登录失败,未获取到 token');
// 2. 保存登录信息
saveStorageByLoginUser(access_token, user);
setIsLoggedIn(true);
// 3. 手机号授权码已被消耗,重新获取 wx.login code 调加入接口
console.log('注册成功,重新获取 wx.login code 调加入接口');
const wxRes = await Taro.login();
if (!wxRes.code) throw new Error('获取微信登录凭证失败');
await doJoinApp(wxRes.code, access_token);
} catch (err: any) {
console.error('手机号授权登录失败:', err);
Taro.showToast({ title: err.message || '授权失败,请重试', icon: 'none' });
} finally {
setAuthLoading(false);
}
};
/**
* 调用加入接口(统一入口)
* @param wxCode wx.login() 或 getPhoneNumber 获取的 code后端用于识别用户
* @param accessToken 登录 token加到 Authorization 头,双重验证)
*/
const doJoinApp = async (wxCode: string, accessToken: string) => {
const inviteToken = Taro.getStorageSync('invite_token') || token;
console.log('doJoinApp, inviteToken:', inviteToken, 'wxCode:', wxCode ? '有' : '无', 'accessToken:', accessToken ? '有' : '无');
const res = await Taro.request({ const res = await Taro.request({
url: `${INVITE_API_URL}/api/_app/developer/invite/accept`, url: `${INVITE_API_URL}/api/_app/developer/invite/accept`,
method: 'POST', method: 'POST',
data: { data: {
token: inviteToken, token: inviteToken,
code: phoneCode, code: wxCode,
encryptedData,
iv
}, },
header: { header: {
'content-type': 'application/json', 'content-type': 'application/json',
TenantId TenantId,
} Authorization: `Bearer ${accessToken}`,
},
}); });
console.log('接受邀请接口响应:', res); console.log('加入应用接口响应:', res);
if (res.data.code === 200 || res.data.code === 0) { if (res.data.code === 200 || res.data.code === 0) {
// 清理邀请信息
Taro.removeStorageSync('invite_token'); Taro.removeStorageSync('invite_token');
Taro.showToast({ title: '加入成功', icon: 'success', duration: 1500 }); Taro.showToast({ title: '加入成功', icon: 'success', duration: 1500 });
setTimeout(() => { setTimeout(() => Taro.switchTab({ url: '/pages/index/index' }), 1500);
Taro.switchTab({ url: '/pages/index/index' });
}, 1500);
} else { } else {
console.error('接受邀请失败:', res.data.message); throw new Error(res.data.message || '加入失败');
Taro.showToast({ title: res.data.message || '加入失败', icon: 'none' });
}
} catch (err: any) {
console.error('接受邀请异常:', err);
Taro.showToast({ title: err.message || '加入失败', icon: 'error' });
} finally {
setAuthLoading(false);
} }
}; };
// 拒绝邀请 // ── 拒绝 / 打开协议 ─────────────────────────────────
const handleReject = () => { const handleReject = () => Taro.switchTab({ url: '/pages/index/index' });
Taro.switchTab({ url: '/pages/index/index' });
};
// 打开协议页面
const openAgreement = (type: AgreementType) => { const openAgreement = (type: AgreementType) => {
const urlMap = { const urlMap = {
service: 'https://websopy.websoft.top/agreement', service: 'https://websopy.websoft.top/agreement',
privacy: 'https://websopy.websoft.top/privacy', privacy: 'https://websopy.websoft.top/privacy',
}; };
const targetUrl = encodeURIComponent(urlMap[type]); Taro.navigateTo({ url: `/passport/webview/index?url=${encodeURIComponent(urlMap[type])}` });
Taro.navigateTo({ url: `/passport/webview/index?url=${targetUrl}` });
}; };
// 加载中状态 // ── Loading / Checking 状态 ──────────────────────────
if (pageStatus === 'loading' || pageStatus === 'checking') { if (pageStatus === 'loading' || pageStatus === 'checking') {
return ( return (
<View className="min-h-screen flex items-center justify-center" style={{ background: 'linear-gradient(135deg, #0a0a1a 0%, #0d1528 50%, #0a1520 100%)' }}> <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 className="flex flex-col items-center">
<View style={{ <View style={{
width: '40px', height: '40px', border: '3px solid rgba(59, 130, 246, 0.3)', width: '40px', height: '40px',
border: '3px solid rgba(59, 130, 246, 0.3)',
borderTopColor: '#3b82f6', borderRadius: '50%', borderTopColor: '#3b82f6', borderRadius: '50%',
animation: 'spin 1s linear infinite', animation: 'spin 1s linear infinite',
}} /> }} />
@@ -273,33 +286,17 @@ const InvitePage: React.FC = () => {
); );
} }
// 跳转到登录中状态 // ── 错误状态 ─────────────────────────────────────────
if (pageStatus === 'login') {
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: '48px', marginBottom: '20px' }}>🔐</Text>
<Text style={{ color: '#fff', fontSize: '16px', textAlign: 'center', marginBottom: '12px' }}>
</Text>
<Text style={{ color: 'rgba(255, 255, 255, 0.5)', fontSize: '14px', textAlign: 'center', marginBottom: '24px' }}>
...
</Text>
</View>
</View>
);
}
// 错误状态
if (pageStatus === 'error') { if (pageStatus === 'error') {
return ( return (
<View className="min-h-screen flex items-center justify-center" style={{ background: 'linear-gradient(135deg, #0a0a1a 0%, #0d1528 50%, #0a1520 100%)' }}> <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"> <View className="flex flex-col items-center px-8">
<Text style={{ fontSize: '64px', marginBottom: '20px' }}></Text> <Text style={{ fontSize: '64px', marginBottom: '20px' }}></Text>
<Text style={{ color: '#ef4444', fontSize: '16px', textAlign: 'center', marginBottom: '24px' }}>{error}</Text> <Text style={{ color: '#ef4444', fontSize: '16px', textAlign: 'center', marginBottom: '24px' }}>{error}</Text>
<Button onClick={handleReject} style={{ <Button onClick={handleReject} style={{
background: 'rgba(255, 255, 255, 0.1)', color: '#fff', background: 'rgba(255, 255, 255, 0.1)', color: '#fff',
borderRadius: '20px', fontSize: '14px', padding: '8px 24px' borderRadius: '20px', fontSize: '14px', padding: '8px 24px',
}}> }}>
</Button> </Button>
@@ -308,34 +305,69 @@ const InvitePage: React.FC = () => {
); );
} }
// 邀请确认页面 // ── 邀请确认页面 ──────────────────────────────────────
return ( return (
<View className="min-h-screen relative overflow-hidden" style={{ background: 'linear-gradient(135deg, #0a0a1a 0%, #0d1528 50%, #0a1520 100%)' }}> <View className="min-h-screen relative overflow-hidden"
style={{ background: 'linear-gradient(135deg, #0a0a1a 0%, #0d1528 50%, #0a1520 100%)' }}>
{/* 背景效果 */} {/* 背景网格 */}
<View style={{ <View style={{
position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, 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)`, 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, backgroundSize: '50px 50px', opacity: 0.5,
}} /> }} />
{/* 渐变光晕 */} {/* 渐变光晕 - 左上 */}
<View style={{ <View style={{
position: 'absolute', top: '-30%', left: '-20%', width: '60%', height: '60%', position: 'absolute', top: '-30%', left: '-20%', width: '60%', height: '60%',
background: 'radial-gradient(circle, rgba(59, 130, 246, 0.25) 0%, transparent 70%)', background: 'radial-gradient(circle, rgba(59, 130, 246, 0.25) 0%, transparent 70%)',
filter: 'blur(40px)', filter: 'blur(40px)',
}} /> }} />
{/* 主内容区域 */} {/* 渐变光晕 - 右下 */}
<View style={{
position: 'absolute', bottom: '-20%', right: '-20%', width: '50%', height: '50%',
background: 'radial-gradient(circle, rgba(99, 102, 241, 0.2) 0%, transparent 70%)',
filter: 'blur(50px)',
}} />
{/* 动态粒子光点 */}
{[
{ top: '15%', left: '10%', size: 4, delay: '0s' },
{ top: '20%', left: '85%', size: 3, delay: '0.5s' },
{ top: '70%', left: '8%', size: 4, delay: '1s' },
{ top: '75%', left: '88%', size: 3, delay: '1.5s' },
].map((p, i) => (
<View key={i} style={{
position: 'absolute', top: p.top, left: p.left,
width: p.size, height: p.size, borderRadius: '50%',
background: i % 2 === 0 ? '#3b82f6' : '#6366f1',
boxShadow: i % 2 === 0
? '0 0 10px rgba(59, 130, 246, 0.8)'
: '0 0 10px rgba(99, 102, 241, 0.8)',
animationDelay: p.delay,
}} />
))}
{/* 扫描线 */}
<View style={{
position: 'absolute', top: 0, left: 0, right: 0, height: '2px',
background: 'linear-gradient(90deg, transparent, rgba(59, 130, 246, 0.6), transparent)',
animation: 'scanline-blue 3s ease-in-out infinite',
}} />
{/* 主内容 */}
<View className="relative z-10 flex flex-col items-center justify-center min-h-screen px-8"> <View className="relative z-10 flex flex-col items-center justify-center min-h-screen px-8">
{/* 邀请卡片 */} {/* 邀请卡片 */}
<View style={{ <View style={{
width: '100%', maxWidth: '360px', background: 'rgba(255, 255, 255, 0.05)', width: '100%', maxWidth: '360px',
background: 'rgba(255, 255, 255, 0.05)',
borderRadius: '24px', border: '1px solid rgba(255, 255, 255, 0.1)', borderRadius: '24px', border: '1px solid rgba(255, 255, 255, 0.1)',
padding: '32px 24px', backdropFilter: 'blur(20px)', padding: '32px 24px', backdropFilter: 'blur(20px)',
}}> }}>
{/* 应用信息 */}
{/* 应用 Logo + 名称 */}
<View className="flex flex-col items-center mb-8"> <View className="flex flex-col items-center mb-8">
<View style={{ <View style={{
width: '80px', height: '80px', borderRadius: '20px', width: '80px', height: '80px', borderRadius: '20px',
@@ -344,28 +376,23 @@ const InvitePage: React.FC = () => {
display: 'flex', alignItems: 'center', justifyContent: 'center', display: 'flex', alignItems: 'center', justifyContent: 'center',
marginBottom: '16px', overflow: 'hidden', marginBottom: '16px', overflow: 'hidden',
}}> }}>
{inviteInfo?.appLogo ? ( {inviteInfo?.appLogo
<Image src={inviteInfo.appLogo} style={{ width: '60px', height: '60px', borderRadius: '12px' }} /> ? <Image src={inviteInfo.appLogo} style={{ width: '60px', height: '60px', borderRadius: '12px' }} />
) : ( : <Text style={{ fontSize: '36px' }}>🚀</Text>
<Text style={{ fontSize: '36px' }}>🚀</Text> }
)}
</View> </View>
<Text style={{ <Text style={{ fontSize: '22px', fontWeight: '700', color: '#fff', marginBottom: '8px' }}>
fontSize: '22px', fontWeight: '700', color: '#fff', marginBottom: '8px',
}}>
{inviteInfo?.appName || 'Websopy'} {inviteInfo?.appName || 'Websopy'}
</Text> </Text>
<View style={{ <View style={{
padding: '4px 12px', borderRadius: '12px', padding: '4px 12px', borderRadius: '12px',
background: 'rgba(59, 130, 246, 0.2)', border: '1px solid rgba(59, 130, 246, 0.3)', background: 'rgba(59, 130, 246, 0.2)', border: '1px solid rgba(59, 130, 246, 0.3)',
}}> }}>
<Text style={{ color: '#3b82f6', fontSize: '12px' }}> <Text style={{ color: '#3b82f6', fontSize: '12px' }}></Text>
</Text>
</View> </View>
</View> </View>
{/* 邀请人信息 */} {/* 邀请人 */}
<View className="flex items-center justify-center mb-6"> <View className="flex items-center justify-center mb-6">
<Text style={{ color: 'rgba(255, 255, 255, 0.5)', fontSize: '13px' }}> <Text style={{ color: 'rgba(255, 255, 255, 0.5)', fontSize: '13px' }}>
{inviteInfo?.inviterName || '某位用户'} {inviteInfo?.inviterName || '某位用户'}
@@ -374,12 +401,14 @@ const InvitePage: React.FC = () => {
{/* 分隔线 */} {/* 分隔线 */}
<View style={{ <View style={{
height: '1px', background: 'linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent)', height: '1px',
background: 'linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent)',
marginBottom: '24px', marginBottom: '24px',
}} /> }} />
{/* 协议勾选 */} {/* 协议勾选 */}
<View className="flex items-center justify-center mb-4"> {!isLoggedIn && (
<View className="flex items-center justify-center mb-6">
<View style={{ padding: '8px', marginRight: '4px' }} onClick={() => setAgreementChecked(!agreementChecked)}> <View style={{ padding: '8px', marginRight: '4px' }} onClick={() => setAgreementChecked(!agreementChecked)}>
<View style={{ <View style={{
width: '18px', height: '18px', borderRadius: '4px', width: '18px', height: '18px', borderRadius: '4px',
@@ -395,13 +424,30 @@ const InvitePage: React.FC = () => {
<Text style={{ color: 'rgba(255, 255, 255, 0.5)', fontSize: '12px' }}></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> <Text style={{ color: '#3b82f6', fontSize: '12px', padding: '4px 2px' }} onClick={() => openAgreement('privacy')}></Text>
</View> </View>
)}
{/* 主按钮 */} {/* ===== 按钮区:根据登录状态切换按钮类型 ===== */}
<View style={{ <View style={{
width: '100%', background: 'linear-gradient(135deg, #1d4ed8 0%, #3b82f6 50%, #6366f1 100%)', width: '100%',
background: 'linear-gradient(135deg, #1d4ed8 0%, #3b82f6 50%, #6366f1 100%)',
borderRadius: '24px', padding: '2px', borderRadius: '24px', padding: '2px',
boxShadow: '0 0 30px rgba(59, 130, 246, 0.4)', boxShadow: '0 0 30px rgba(59, 130, 246, 0.4)',
}}> }}>
{isLoggedIn ? (
/* 已登录:普通按钮,不弹手机号授权,用 wx.login() 获取 code */
<Button
onClick={handleConfirmJoin}
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>
) : (
/* 未注册:手机号授权按钮 */
<Button <Button
open-type="getPhoneNumber" open-type="getPhoneNumber"
onGetPhoneNumber={handleGetPhoneNumber} onGetPhoneNumber={handleGetPhoneNumber}
@@ -414,10 +460,12 @@ const InvitePage: React.FC = () => {
> >
{authLoading ? '处理中...' : '微信手机号快速加入'} {authLoading ? '处理中...' : '微信手机号快速加入'}
</Button> </Button>
)}
</View> </View>
{/* 拒绝按钮 */} {/* 拒绝 */}
<View style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', marginTop: '16px' }} onClick={handleReject}> <View style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', marginTop: '16px' }}
onClick={handleReject}>
<Text style={{ color: 'rgba(255, 255, 255, 0.4)', fontSize: '14px' }}></Text> <Text style={{ color: 'rgba(255, 255, 255, 0.4)', fontSize: '14px' }}></Text>
</View> </View>
</View> </View>