feat(invite): 重构邀请加入流程,实现已登录用户免授权直接加入
- 完全重写邀请页逻辑,区分已登录与未注册用户按钮显示 - 已登录用户显示“确认加入”按钮,点击用 wx.login 获取 code 加入 - 未注册用户显示“微信手机号快速加入”按钮,授权手机号后完成注册登录再加入 - 移除跳转登录页面逻辑,所有流程在邀请页内完成 - 加入接口统一使用 Authorization 头携带 token,确保身份双重验证 - 优化背景、粒子和扫描线效果,提升界面视觉体验 - 增加错误处理提示,加入失败时显示详细原因并可返回首页 - 协议勾选仅在未注册用户显示,确保合规授权流程
This commit is contained in:
@@ -35,5 +35,5 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"lastUpdated": 1775923978885
|
||||
"lastUpdated": 1775965240928
|
||||
}
|
||||
84
.workbuddy/memory/2026-04-12.md
Normal file
84
.workbuddy/memory/2026-04-12.md
Normal 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
|
||||
@@ -2,7 +2,8 @@ import React, { useState, useEffect } from 'react';
|
||||
import { View, Text, Button, Image } from '@tarojs/components';
|
||||
import Taro, { useRouter } from '@tarojs/taro';
|
||||
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 域名
|
||||
const INVITE_API_URL = 'https://websopy-api.websoft.top';
|
||||
@@ -11,15 +12,12 @@ const INVITE_API_URL = 'https://websopy-api.websoft.top';
|
||||
* 邀请加入确认页面
|
||||
*
|
||||
* 流程:
|
||||
* 1. 扫码进入页面
|
||||
* 2. 调用 wx.login() 获取 code
|
||||
* 3. 调用 loginByOpenId 判断用户是否已注册
|
||||
* 4. 已注册:显示邀请页面,用户点击加入
|
||||
* 5. 未注册:跳转到 passport/login 页面完成登录/注册
|
||||
* 6. 登录成功后返回,自动执行加入操作
|
||||
* 1. 扫码进入 → 调用 loginByOpenId 判断登录状态
|
||||
* 2. 已注册 → isLoggedIn=true → 显示「确认加入」按钮 → 点击直接加入(带 access_token)
|
||||
* 3. 未注册 → isLoggedIn=false → 显示「微信手机号授权」按钮
|
||||
* → 授权成功 → 注册/登录 → isLoggedIn=true → 自动执行加入
|
||||
*/
|
||||
|
||||
// 邀请信息类型
|
||||
interface InviteInfo {
|
||||
appId: string;
|
||||
appName: string;
|
||||
@@ -28,39 +26,39 @@ interface InviteInfo {
|
||||
roleName: string;
|
||||
}
|
||||
|
||||
// 协议类型
|
||||
type AgreementType = 'service' | 'privacy';
|
||||
|
||||
// 页面状态
|
||||
type PageStatus = 'loading' | 'checking' | 'invite' | 'login' | 'error';
|
||||
type PageStatus = 'loading' | 'checking' | 'invite' | 'error';
|
||||
|
||||
const InvitePage: React.FC = () => {
|
||||
const router = useRouter();
|
||||
|
||||
const [pageStatus, setPageStatus] = useState<PageStatus>('loading');
|
||||
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>('');
|
||||
/**
|
||||
* true = 已注册已登录,按钮文字「确认加入」(仍走 getPhoneNumber 获取 code 传给后端)
|
||||
* false = 未注册,按钮文字「微信手机号快速加入」,授权后先注册登录再加入
|
||||
*/
|
||||
const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
initPage().then();
|
||||
initPage();
|
||||
}, [router.params]);
|
||||
|
||||
// 页面初始化
|
||||
// ── 初始化 ──────────────────────────────────────────
|
||||
const initPage = async () => {
|
||||
// 从 URL 参数中获取 token
|
||||
const params = router.params;
|
||||
let inviteToken = params.scene || params.token || params.qrCodeKey || '';
|
||||
console.log(params,'URL参数')
|
||||
// 兼容 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) {
|
||||
} catch {
|
||||
inviteToken = decodeURIComponent(params.q);
|
||||
}
|
||||
}
|
||||
@@ -72,196 +70,211 @@ const InvitePage: React.FC = () => {
|
||||
}
|
||||
|
||||
setToken(inviteToken);
|
||||
// 保存邀请 token,供登录后使用
|
||||
Taro.setStorageSync('invite_token', inviteToken);
|
||||
|
||||
// 检查用户登录状态
|
||||
// 先获取邀请信息,再检查登录状态(并行更好,但 inviteInfo 失败要 error)
|
||||
await checkLoginStatus(inviteToken);
|
||||
console.log('检查登录状态完成', inviteToken)
|
||||
};
|
||||
|
||||
// 检查用户登录状态
|
||||
// ── loginByOpenId 检查登录状态 ──────────────────────
|
||||
const checkLoginStatus = async (inviteToken: string) => {
|
||||
setPageStatus('checking');
|
||||
|
||||
try {
|
||||
// 调用 wx.login 获取 code
|
||||
const wxLoginRes = await Taro.login();
|
||||
console.log('wx.login 结果:', wxLoginRes);
|
||||
const wxRes = await Taro.login();
|
||||
if (!wxRes.code) throw new Error('获取微信登录凭证失败');
|
||||
|
||||
if (!wxLoginRes.code) {
|
||||
throw new Error('获取微信登录凭证失败');
|
||||
}
|
||||
|
||||
// 调用 loginByOpenId 判断用户是否已注册
|
||||
const loginRes = await loginByOpenId({
|
||||
code: wxLoginRes.code,
|
||||
tenantId: parseInt(TenantId) || 1
|
||||
code: wxRes.code,
|
||||
tenantId: parseInt(TenantId) || 1,
|
||||
});
|
||||
|
||||
console.log('loginByOpenId 结果:', loginRes);
|
||||
|
||||
if (loginRes.success && loginRes.data?.access_token) {
|
||||
// 用户已注册,保存登录信息
|
||||
Taro.setStorageSync('access_token', loginRes.data.access_token);
|
||||
if (loginRes.data.user) {
|
||||
Taro.setStorageSync('user_info', JSON.stringify(loginRes.data.user));
|
||||
}
|
||||
// ✅ 已注册:保存登录信息,显示「确认加入」
|
||||
saveStorageByLoginUser(loginRes.data.access_token, loginRes.data.user as any);
|
||||
setIsLoggedIn(true);
|
||||
// 获取邀请信息并显示邀请页面
|
||||
await fetchInviteInfo(inviteToken);
|
||||
setPageStatus('invite');
|
||||
} else {
|
||||
// 用户未注册,跳转到登录页面
|
||||
console.log('用户未注册,跳转到登录页面');
|
||||
setPageStatus('login');
|
||||
// 延迟跳转,让用户看到提示
|
||||
setTimeout(() => {
|
||||
navigateToLogin(inviteToken);
|
||||
}, 500);
|
||||
// ❌ 未注册:显示手机号授权按钮
|
||||
console.log('用户未注册,显示手机号授权按钮');
|
||||
setIsLoggedIn(false);
|
||||
}
|
||||
|
||||
// 两种情况都需要获取邀请信息
|
||||
await fetchInviteInfo(inviteToken);
|
||||
setPageStatus('invite');
|
||||
} catch (err: any) {
|
||||
console.error('检查登录状态失败:', err);
|
||||
// 出错时也跳转到登录页面
|
||||
setPageStatus('login');
|
||||
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) => {
|
||||
try {
|
||||
console.log('开始获取邀请信息, token:', inviteToken);
|
||||
|
||||
const res = await Taro.request({
|
||||
url: `${INVITE_API_URL}/api/_app/developer/invite/info?token=${encodeURIComponent(inviteToken)}`,
|
||||
method: 'GET',
|
||||
header: {
|
||||
'content-type': 'application/json',
|
||||
TenantId
|
||||
}
|
||||
});
|
||||
|
||||
console.log('邀请信息接口响应:', res);
|
||||
|
||||
if (res.data.code === 200 || res.data.code === 0) {
|
||||
setInviteInfo(res.data.data);
|
||||
} else {
|
||||
console.error('接口返回错误:', res.data.message);
|
||||
setError(res.data.message || '邀请信息获取失败');
|
||||
setPageStatus('error');
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('获取邀请信息异常:', err);
|
||||
setError(err.message || '网络请求失败');
|
||||
setError(err.message || '初始化失败,请重试');
|
||||
setPageStatus('error');
|
||||
}
|
||||
};
|
||||
|
||||
// 处理微信手机号授权
|
||||
const handleGetPhoneNumber = async (e: any) => {
|
||||
const { code, encryptedData, iv, errMsg } = e.detail;
|
||||
// ── 获取邀请信息 ─────────────────────────────────────
|
||||
const fetchInviteInfo = async (inviteToken: string) => {
|
||||
const res = await Taro.request({
|
||||
url: `${INVITE_API_URL}/api/_app/developer/invite/info?token=${encodeURIComponent(inviteToken)}`,
|
||||
method: 'GET',
|
||||
header: { 'content-type': 'application/json', TenantId },
|
||||
});
|
||||
|
||||
console.log('handleGetPhoneNumber:', { code, errMsg });
|
||||
console.log('邀请信息接口响应:', res);
|
||||
|
||||
// 检查协议是否勾选
|
||||
if (res.data.code === 200 || res.data.code === 0) {
|
||||
setInviteInfo(res.data.data);
|
||||
} else {
|
||||
throw new Error(res.data.message || '邀请信息获取失败');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 已登录用户:点击「确认加入」
|
||||
* 不弹手机号授权,直接用 wx.login() 获取 code 调加入接口
|
||||
*/
|
||||
const handleConfirmJoin = async () => {
|
||||
if (!agreementChecked) {
|
||||
Taro.showToast({ title: '请先同意服务协议和隐私政策', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
// 用户拒绝授权
|
||||
if (errMsg && errMsg.includes('fail')) {
|
||||
Taro.showToast({ title: '需要授权手机号才能加入', icon: 'none' });
|
||||
const accessToken = Taro.getStorageSync('access_token');
|
||||
if (!accessToken) {
|
||||
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 inviteToken = Taro.getStorageSync('invite_token') || token;
|
||||
|
||||
console.log('开始接受邀请, token:', inviteToken);
|
||||
|
||||
const res = await Taro.request({
|
||||
url: `${INVITE_API_URL}/api/_app/developer/invite/accept`,
|
||||
method: 'POST',
|
||||
data: {
|
||||
token: inviteToken,
|
||||
code: phoneCode,
|
||||
encryptedData,
|
||||
iv
|
||||
},
|
||||
header: {
|
||||
'content-type': 'application/json',
|
||||
TenantId
|
||||
}
|
||||
});
|
||||
|
||||
console.log('接受邀请接口响应:', res);
|
||||
|
||||
if (res.data.code === 200 || res.data.code === 0) {
|
||||
// 清理邀请信息
|
||||
Taro.removeStorageSync('invite_token');
|
||||
|
||||
Taro.showToast({ title: '加入成功', icon: 'success', duration: 1500 });
|
||||
setTimeout(() => {
|
||||
Taro.switchTab({ url: '/pages/index/index' });
|
||||
}, 1500);
|
||||
} else {
|
||||
console.error('接受邀请失败:', res.data.message);
|
||||
Taro.showToast({ title: res.data.message || '加入失败', icon: 'none' });
|
||||
}
|
||||
// 用 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: 'error' });
|
||||
console.error('确认加入失败:', err);
|
||||
Taro.showToast({ title: err.message || '加入失败,请重试', icon: 'none' });
|
||||
} finally {
|
||||
setAuthLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 拒绝邀请
|
||||
const handleReject = () => {
|
||||
Taro.switchTab({ url: '/pages/index/index' });
|
||||
/**
|
||||
* 未注册用户:手机号授权回调
|
||||
* 先用 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) {
|
||||
Taro.showToast({ title: '获取授权信息失败,请重试', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setAuthLoading(true);
|
||||
|
||||
// 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('手机号注册/登录结果:', 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({
|
||||
url: `${INVITE_API_URL}/api/_app/developer/invite/accept`,
|
||||
method: 'POST',
|
||||
data: {
|
||||
token: inviteToken,
|
||||
code: wxCode,
|
||||
},
|
||||
header: {
|
||||
'content-type': 'application/json',
|
||||
TenantId,
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
console.log('加入应用接口响应:', res);
|
||||
|
||||
if (res.data.code === 200 || res.data.code === 0) {
|
||||
Taro.removeStorageSync('invite_token');
|
||||
Taro.showToast({ title: '加入成功', icon: 'success', duration: 1500 });
|
||||
setTimeout(() => Taro.switchTab({ url: '/pages/index/index' }), 1500);
|
||||
} else {
|
||||
throw new Error(res.data.message || '加入失败');
|
||||
}
|
||||
};
|
||||
|
||||
// ── 拒绝 / 打开协议 ─────────────────────────────────
|
||||
const handleReject = () => 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}` });
|
||||
Taro.navigateTo({ url: `/passport/webview/index?url=${encodeURIComponent(urlMap[type])}` });
|
||||
};
|
||||
|
||||
// 加载中状态
|
||||
// ── Loading / Checking 状态 ──────────────────────────
|
||||
if (pageStatus === 'loading' || pageStatus === 'checking') {
|
||||
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 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%',
|
||||
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') {
|
||||
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">
|
||||
<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'
|
||||
borderRadius: '20px', fontSize: '14px', padding: '8px 24px',
|
||||
}}>
|
||||
返回首页
|
||||
</Button>
|
||||
@@ -308,34 +305,69 @@ const InvitePage: React.FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
// 邀请确认页面
|
||||
// ── 邀请确认页面 ──────────────────────────────────────
|
||||
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={{
|
||||
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 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 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)',
|
||||
padding: '32px 24px', backdropFilter: 'blur(20px)',
|
||||
}}>
|
||||
{/* 应用信息 */}
|
||||
|
||||
{/* 应用 Logo + 名称 */}
|
||||
<View className="flex flex-col items-center mb-8">
|
||||
<View style={{
|
||||
width: '80px', height: '80px', borderRadius: '20px',
|
||||
@@ -344,28 +376,23 @@ const InvitePage: React.FC = () => {
|
||||
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>
|
||||
)}
|
||||
{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',
|
||||
}}>
|
||||
<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>
|
||||
<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 || '某位用户'}
|
||||
@@ -374,50 +401,71 @@ const InvitePage: React.FC = () => {
|
||||
|
||||
{/* 分隔线 */}
|
||||
<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',
|
||||
}} />
|
||||
|
||||
{/* 协议勾选 */}
|
||||
<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>}
|
||||
{!isLoggedIn && (
|
||||
<View className="flex items-center justify-center mb-6">
|
||||
<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>
|
||||
<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%)',
|
||||
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>
|
||||
{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
|
||||
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}>
|
||||
{/* 拒绝 */}
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user