feat(passport): 重写登录与邀请加入流程,支持微信手机号一键登录
- 完全重写 passport/login.tsx,实现微信手机号一键登录功能 - 支持登录页面携带 redirect 参数,登录后自动跳转回原页面 - 登录成功后自动处理邀请加入应用,支持邀请关系绑定 - 重写 passport/invite/index.tsx,实现扫码邀请加入应用完整流程 - 支持调用 wx.login 获取 code,使用 loginByOpenId 判断注册状态 - 未注册用户自动跳转登录页完成微信手机号登录再加入应用 - 已登录用户直接执行加入应用操作,加入接口支持 Authorization 头 -
This commit is contained in:
@@ -35,5 +35,5 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"lastUpdated": 1775921440281
|
||||
"lastUpdated": 1775923978885
|
||||
}
|
||||
@@ -42,3 +42,212 @@
|
||||
|
||||
### 文件修改
|
||||
- `src/passport/invite/index.tsx` - 完整重构邀请流程
|
||||
|
||||
---
|
||||
|
||||
## 任务:重写 passport/login 页面
|
||||
|
||||
### 问题描述
|
||||
`passport/login.tsx` 页面只有 UI 框架,没有实现登录逻辑:
|
||||
- 输入框没有绑定状态
|
||||
- 登录按钮没有点击事件
|
||||
- 不支持微信手机号登录
|
||||
- 无法处理邀请流程的重定向
|
||||
|
||||
### 解决方案
|
||||
基于 `phone-auth/index.tsx` 的实现,重写 `login.tsx`:
|
||||
|
||||
#### 1. 功能实现
|
||||
- 微信手机号一键登录(使用 `open-type="getPhoneNumber"`)
|
||||
- 支持 `redirect` 参数,登录后返回原页面
|
||||
- 未注册用户自动注册
|
||||
- 支持邀请关系绑定
|
||||
- 服务协议和隐私政策弹窗
|
||||
|
||||
#### 2. 跳转逻辑
|
||||
```
|
||||
有 redirect 参数:
|
||||
- TabBar 页面 → switchTab
|
||||
- 普通页面 → navigateBack 或 redirectTo
|
||||
|
||||
无 redirect 参数:
|
||||
- 跳转到首页 /pages/index/index
|
||||
```
|
||||
|
||||
#### 3. 统一登录入口
|
||||
- 将 `invite/index.tsx` 中的登录跳转统一改为 `/passport/login`
|
||||
- 保持与现有代码引用兼容(5处引用无需修改)
|
||||
|
||||
### 文件修改
|
||||
- `src/passport/login.tsx` - 完全重写为微信手机号登录页面
|
||||
- `src/passport/invite/index.tsx` - 更新登录跳转链接
|
||||
|
||||
---
|
||||
|
||||
## 任务:修复登录后加入应用流程
|
||||
|
||||
### 问题描述
|
||||
用户从邀请页面跳转到登录页面完成注册后,返回邀请页面提示"您尚未注册",无法成功加入应用。
|
||||
|
||||
### 问题分析
|
||||
1. 用户在邀请页面点击"微信手机号快速加入"时保存了微信授权码
|
||||
2. 跳转到登录页面后,用户**重新**进行了微信授权,获得了新的授权码
|
||||
3. 登录成功后返回邀请页面,但邀请页面使用的是旧的已失效的授权码
|
||||
4. 导致加入应用失败
|
||||
|
||||
### 解决方案
|
||||
在登录页面登录成功后,直接使用**当前获取的微信授权码**完成加入应用操作:
|
||||
|
||||
#### 1. 登录页面修改 (`login.tsx`)
|
||||
- 添加 `handleJoinAppAfterLogin` 方法
|
||||
- 登录成功后检测 `pending_invite_token`
|
||||
- 如果存在,使用当前授权码调用 `/api/_app/developer/invite/accept` 接口
|
||||
- 加入成功后清理 pending 数据并跳转到首页
|
||||
- 加入失败则继续正常登录流程
|
||||
|
||||
#### 2. 邀请页面修改 (`invite/index.tsx`)
|
||||
- 优化从登录页返回的处理逻辑
|
||||
- 检测 `pending_invite_phone_code` 是否存在
|
||||
- 延迟检查登录页面是否已处理加入操作
|
||||
- 如果登录页面已处理(token 被清除),则显示成功提示并跳转
|
||||
- 如果登录页面未处理,则自动执行加入操作
|
||||
|
||||
### 新的完整流程
|
||||
```
|
||||
新用户扫码加入流程:
|
||||
1. 扫码进入邀请页面
|
||||
2. 点击"微信手机号快速加入"
|
||||
3. 检测到未登录,保存 pending 数据,跳转到登录页面
|
||||
4. 在登录页面勾选协议,点击"微信手机号一键登录"
|
||||
5. 获取新的微信授权码,完成登录/注册
|
||||
6. 登录成功后检测到 pending_invite_token,自动执行加入应用
|
||||
7. 加入成功,清理数据,跳转到首页
|
||||
|
||||
已登录用户流程:
|
||||
1. 扫码进入邀请页面
|
||||
2. 点击"微信手机号快速加入"
|
||||
3. 直接执行加入操作
|
||||
4. 加入成功,跳转到首页
|
||||
```
|
||||
|
||||
### 文件修改
|
||||
- `src/passport/login.tsx` - 添加登录后自动加入应用逻辑
|
||||
- `src/passport/invite/index.tsx` - 优化从登录页返回的处理逻辑
|
||||
|
||||
---
|
||||
|
||||
## 任务:修复"用户创建失败"问题
|
||||
|
||||
### 问题描述
|
||||
后端返回 `"用户创建失败"`,原因是微信授权码失效。
|
||||
|
||||
### 问题分析
|
||||
1. 用户在邀请页面获取了微信授权码
|
||||
2. 跳转到登录页面后,用户**重新**点击授权按钮获取了新的授权码
|
||||
3. 但代码逻辑可能混淆了新旧授权码
|
||||
4. 或者授权码已过期(5分钟有效期)
|
||||
|
||||
### 解决方案
|
||||
1. **明确使用当前获取的授权码**:在登录页面登录成功后,使用**当前**获取的微信授权码执行加入应用操作
|
||||
2. **不使用旧的授权码**:从邀请页面带过来的授权码仅作为标记用途,实际使用登录时获取的新授权码
|
||||
3. **添加错误处理**:加入失败时显示错误信息并延迟跳转
|
||||
|
||||
### 关键修改
|
||||
- `login.tsx`:优化 `handleLogin` 中的邀请流程处理逻辑
|
||||
- 明确使用当前 `phoneCode` 执行加入操作
|
||||
- 添加错误处理和用户提示
|
||||
- 加入失败后延迟跳转,让用户看到错误信息
|
||||
|
||||
---
|
||||
|
||||
## 任务:修复已登录用户加入应用失败问题
|
||||
|
||||
### 问题描述
|
||||
已登录用户点击"微信手机号快速加入"时,后端返回 `"用户创建失败"`。
|
||||
|
||||
### 问题分析
|
||||
1. 用户已经是登录状态(有 `access_token`)
|
||||
2. 但后端接口 `/api/_app/developer/invite/accept` 仍然尝试"创建用户"
|
||||
3. 原因是后端无法识别当前已登录的用户身份
|
||||
|
||||
### 解决方案
|
||||
为已登录用户在请求头中添加 `Authorization: Bearer {access_token}`,让后端能正确识别用户身份:
|
||||
|
||||
#### 1. 邀请页面修改 (`invite/index.tsx`)
|
||||
- 在 `handleJoinApp` 方法中获取 `access_token`
|
||||
- 构建请求头时,如果用户已登录,添加 `Authorization` 头
|
||||
- 添加日志记录是否使用了认证头
|
||||
|
||||
#### 2. 登录页面修改 (`login.tsx`)
|
||||
- 在 `handleJoinAppAfterLogin` 方法中同样添加 `Authorization` 头
|
||||
- 确保登录成功后调用加入接口时携带认证信息
|
||||
|
||||
### 文件修改
|
||||
- `src/passport/invite/index.tsx` - 添加 Authorization 请求头支持
|
||||
- `src/passport/login.tsx` - 添加 Authorization 请求头支持
|
||||
|
||||
---
|
||||
|
||||
## 任务:修复 401 认证失败问题
|
||||
|
||||
### 问题描述
|
||||
后端返回 `401` 和 `"请退出重新登录"`,`error: "Username not found"`。
|
||||
|
||||
### 问题分析
|
||||
1. 请求带了 `Authorization: Bearer {token}` 头
|
||||
2. 但后端验证 token 时发现用户不存在
|
||||
3. 原因可能是:
|
||||
- token 已过期
|
||||
- token 对应的用户已被删除
|
||||
- `/api/_app` 前缀的接口设计为"免登录",使用 `Authorization` 头反而导致认证失败
|
||||
|
||||
### 解决方案
|
||||
`/api/_app` 前缀的接口是小程序专用免登录接口,应该优先使用微信授权码方式:
|
||||
|
||||
#### 策略调整
|
||||
- **有微信授权码**:使用授权码方式,不传 `Authorization` 头
|
||||
- **无授权码但已登录**:使用 `Authorization` 头
|
||||
|
||||
#### 修改内容
|
||||
- `invite/index.tsx`:调整 `handleJoinApp` 方法中的请求头逻辑
|
||||
- 优先使用微信授权码,避免使用可能过期的 token
|
||||
|
||||
### 文件修改
|
||||
- `src/passport/invite/index.tsx` - 调整认证策略,优先使用微信授权码
|
||||
|
||||
---
|
||||
|
||||
## 任务:重写邀请加入应用流程
|
||||
|
||||
### 新流程设计
|
||||
1. 扫码进入邀请页面
|
||||
2. 调用 `wx.login()` 获取 code
|
||||
3. 调用 `loginByOpenId` 判断用户是否已注册
|
||||
4. 已注册:显示邀请页面,用户点击加入
|
||||
5. 未注册:跳转到 `passport/login` 页面完成登录/注册
|
||||
6. 登录成功后自动执行加入应用操作
|
||||
|
||||
### 实现细节
|
||||
|
||||
#### 1. 邀请页面 (`invite/index.tsx`)
|
||||
- 页面状态管理:`loading` → `checking` → `invite`/`login`/`error`
|
||||
- `initPage()`:解析 token,保存到 storage
|
||||
- `checkLoginStatus()`:调用 `loginByOpenId` 检查登录状态
|
||||
- `navigateToLogin()`:未登录用户跳转到登录页
|
||||
- `fetchInviteInfo()`:获取邀请信息
|
||||
- `handleGetPhoneNumber()`:处理微信授权
|
||||
- `handleJoinApp()`:执行加入应用操作
|
||||
|
||||
#### 2. 登录页面 (`login.tsx`)
|
||||
- 登录成功后检查 `invite_token`
|
||||
- 如果存在,使用当前授权码自动执行加入应用
|
||||
- 加入成功后跳转到首页
|
||||
|
||||
### 简化点
|
||||
- 不再使用复杂的 `pending_invite_*` 系列 storage key
|
||||
- 只使用 `invite_token` 保存邀请标识
|
||||
- 登录页面直接使用当前获取的微信授权码
|
||||
|
||||
### 文件修改
|
||||
- `src/passport/invite/index.tsx` - 完全重写
|
||||
- `src/passport/login.tsx` - 更新为使用 `invite_token`
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
"miniprogram": {
|
||||
"list": [
|
||||
{
|
||||
"name": "passport/invite/index",
|
||||
"pathName": "passport/invite/index",
|
||||
"name": "passport/login",
|
||||
"pathName": "passport/login",
|
||||
"query": "",
|
||||
"scene": null,
|
||||
"launchMode": "default"
|
||||
@@ -18,6 +18,13 @@
|
||||
"launchMode": "default",
|
||||
"scene": null
|
||||
},
|
||||
{
|
||||
"name": "passport/invite/index",
|
||||
"pathName": "passport/invite/index",
|
||||
"query": "",
|
||||
"launchMode": "default",
|
||||
"scene": null
|
||||
},
|
||||
{
|
||||
"name": "passport/invite",
|
||||
"pathName": "passport/invite",
|
||||
|
||||
@@ -1,39 +1,24 @@
|
||||
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";
|
||||
|
||||
// 邀请相关接口使用独立的 API 域名
|
||||
// 注意:使用 /api/_app 前缀表示小程序专用接口(免登录)
|
||||
const INVITE_API_URL = 'https://websopy-api.websoft.top';
|
||||
import { TenantId } from "@/config/app";
|
||||
import { getStoredInviteParams, saveInviteParams, clearInviteParams } from "@/utils/invite";
|
||||
|
||||
/**
|
||||
* 邀请加入确认页面
|
||||
*
|
||||
* 用户扫描邀请二维码后,打开此小程序页面确认加入应用
|
||||
*
|
||||
* 流程:
|
||||
* 1. 用户扫码进入页面,解析 token 参数
|
||||
* 2. 获取邀请信息展示给用户
|
||||
* 3. 用户点击"微信手机号快速加入"
|
||||
* 4. 如果用户未登录,保存邀请信息并引导到登录页
|
||||
* 5. 登录成功后返回此页面,自动执行加入操作
|
||||
* 6. 已登录用户直接执行加入操作
|
||||
* 1. 扫码进入页面
|
||||
* 2. 调用 wx.login() 获取 code
|
||||
* 3. 调用 loginByOpenId 判断用户是否已注册
|
||||
* 4. 已注册:显示邀请页面,用户点击加入
|
||||
* 5. 未注册:跳转到 passport/login 页面完成登录/注册
|
||||
* 6. 登录成功后返回,自动执行加入操作
|
||||
*/
|
||||
|
||||
// 微信获取手机号回调参数类型
|
||||
interface GetPhoneNumberDetail {
|
||||
code?: string;
|
||||
encryptedData?: string;
|
||||
iv?: string;
|
||||
errMsg: string;
|
||||
}
|
||||
|
||||
interface GetPhoneNumberEvent {
|
||||
detail: GetPhoneNumberDetail;
|
||||
}
|
||||
|
||||
// 邀请信息类型
|
||||
interface InviteInfo {
|
||||
appId: string;
|
||||
@@ -46,9 +31,12 @@ interface InviteInfo {
|
||||
// 协议类型
|
||||
type AgreementType = 'service' | 'privacy';
|
||||
|
||||
// 页面状态
|
||||
type PageStatus = 'loading' | 'checking' | 'invite' | 'login' | 'error';
|
||||
|
||||
const InvitePage: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [pageStatus, setPageStatus] = useState<PageStatus>('loading');
|
||||
const [inviteInfo, setInviteInfo] = useState<InviteInfo | null>(null);
|
||||
const [authLoading, setAuthLoading] = useState(false);
|
||||
const [agreementChecked, setAgreementChecked] = useState(false);
|
||||
@@ -57,14 +45,15 @@ const InvitePage: React.FC = () => {
|
||||
const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
// 检查用户是否已登录
|
||||
const accessToken = Taro.getStorageSync('access_token');
|
||||
setIsLoggedIn(!!accessToken);
|
||||
initPage().then();
|
||||
}, [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 {
|
||||
@@ -76,58 +65,82 @@ const InvitePage: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (!inviteToken) {
|
||||
setError('无效的邀请链接');
|
||||
setPageStatus('error');
|
||||
return;
|
||||
}
|
||||
|
||||
setToken(inviteToken);
|
||||
// 保存邀请 token,供登录后使用
|
||||
Taro.setStorageSync('invite_token', inviteToken);
|
||||
|
||||
// 检查用户登录状态
|
||||
await checkLoginStatus(inviteToken);
|
||||
console.log('检查登录状态完成', inviteToken)
|
||||
};
|
||||
|
||||
// 检查用户登录状态
|
||||
const checkLoginStatus = async (inviteToken: string) => {
|
||||
setPageStatus('checking');
|
||||
|
||||
try {
|
||||
// 调用 wx.login 获取 code
|
||||
const wxLoginRes = await Taro.login();
|
||||
console.log('wx.login 结果:', wxLoginRes);
|
||||
|
||||
if (!wxLoginRes.code) {
|
||||
throw new Error('获取微信登录凭证失败');
|
||||
}
|
||||
|
||||
// 调用 loginByOpenId 判断用户是否已注册
|
||||
const loginRes = await loginByOpenId({
|
||||
code: wxLoginRes.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));
|
||||
}
|
||||
setIsLoggedIn(true);
|
||||
// 获取邀请信息并显示邀请页面
|
||||
await fetchInviteInfo(inviteToken);
|
||||
setPageStatus('invite');
|
||||
} else {
|
||||
// 用户未注册,跳转到登录页面
|
||||
console.log('用户未注册,跳转到登录页面');
|
||||
setPageStatus('login');
|
||||
// 延迟跳转,让用户看到提示
|
||||
setTimeout(() => {
|
||||
navigateToLogin(inviteToken);
|
||||
}, 500);
|
||||
}
|
||||
} 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`
|
||||
});
|
||||
};
|
||||
|
||||
// 获取邀请信息
|
||||
if (inviteToken) {
|
||||
// 保存邀请 token 到本地存储,供登录后使用
|
||||
saveInviteParams({
|
||||
inviter: inviteToken,
|
||||
source: 'app_invite',
|
||||
t: Date.now().toString()
|
||||
});
|
||||
fetchInviteInfo(inviteToken);
|
||||
} else {
|
||||
setError('无效的邀请链接');
|
||||
setLoading(false);
|
||||
}
|
||||
}, [router.params]);
|
||||
|
||||
// 页面显示时检查是否需要自动执行加入操作(从登录页返回)
|
||||
useEffect(() => {
|
||||
const handleShow = () => {
|
||||
// 检查是否有 pending 的邀请数据且用户已登录
|
||||
const pendingToken = Taro.getStorageSync('pending_invite_token');
|
||||
const accessToken = Taro.getStorageSync('access_token');
|
||||
|
||||
if (pendingToken && accessToken) {
|
||||
console.log('检测到登录后返回,自动执行加入应用操作');
|
||||
// 更新登录状态
|
||||
setIsLoggedIn(true);
|
||||
// 自动执行加入操作
|
||||
handleJoinApp();
|
||||
}
|
||||
};
|
||||
|
||||
// 监听页面显示事件
|
||||
Taro.eventCenter.on('AppShow', handleShow);
|
||||
|
||||
// 立即检查一次(处理页面首次加载时已经是登录状态的情况)
|
||||
handleShow();
|
||||
|
||||
return () => {
|
||||
Taro.eventCenter.off('AppShow', handleShow);
|
||||
};
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 获取邀请信息
|
||||
*/
|
||||
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/_app/developer/invite/info?token=${encodeURIComponent(inviteToken)}`,
|
||||
@@ -139,30 +152,26 @@ const InvitePage: React.FC = () => {
|
||||
});
|
||||
|
||||
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);
|
||||
console.error('接口返回错误:', res.data.message);
|
||||
setError(res.data.message || '邀请信息获取失败');
|
||||
setPageStatus('error');
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('获取邀请信息异常:', err);
|
||||
setError(err.message || '网络请求失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setPageStatus('error');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理微信手机号授权
|
||||
*
|
||||
* 如果用户未登录,先引导到登录页面完成注册/登录
|
||||
* 登录成功后返回此页面自动执行加入操作
|
||||
*/
|
||||
const handleGetPhoneNumber = async ({ detail }: GetPhoneNumberEvent) => {
|
||||
const { code, encryptedData, iv, errMsg } = detail;
|
||||
// 处理微信手机号授权
|
||||
const handleGetPhoneNumber = async (e: any) => {
|
||||
const { code, encryptedData, iv, errMsg } = e.detail;
|
||||
|
||||
console.log('handleGetPhoneNumber:', { code, errMsg });
|
||||
|
||||
// 检查协议是否勾选
|
||||
if (!agreementChecked) {
|
||||
@@ -181,57 +190,27 @@ const InvitePage: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查用户是否已登录
|
||||
const accessToken = Taro.getStorageSync('access_token');
|
||||
|
||||
if (!accessToken) {
|
||||
// 未登录用户:保存当前邀请信息,引导到登录页面
|
||||
// 保存邀请 token 和微信授权信息,供登录后使用
|
||||
Taro.setStorageSync('pending_invite_token', token);
|
||||
Taro.setStorageSync('pending_invite_phone_code', code);
|
||||
if (encryptedData) Taro.setStorageSync('pending_invite_encrypted_data', encryptedData);
|
||||
if (iv) Taro.setStorageSync('pending_invite_iv', iv);
|
||||
|
||||
// 引导到手机号授权登录页面
|
||||
Taro.navigateTo({
|
||||
url: `/passport/phone-auth/index?redirect=${encodeURIComponent('/passport/invite/index?token=' + token)}`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 已登录用户:直接执行加入操作
|
||||
// 执行加入操作
|
||||
await handleJoinApp(code, encryptedData, iv);
|
||||
};
|
||||
|
||||
/**
|
||||
* 加入应用
|
||||
*
|
||||
* 支持两种调用方式:
|
||||
* 1. 新用户:通过手机号授权码完成注册并加入(phoneCode 必传)
|
||||
* 2. 已登录用户:直接加入应用(使用存储的 token)
|
||||
*/
|
||||
const handleJoinApp = async (phoneCode?: string, encryptedData?: string, iv?: string) => {
|
||||
// 加入应用
|
||||
const handleJoinApp = async (phoneCode: string, encryptedData?: string, iv?: string) => {
|
||||
try {
|
||||
setAuthLoading(true);
|
||||
|
||||
// 获取 pending 的邀请信息(如果是从登录页返回)
|
||||
const pendingToken = Taro.getStorageSync('pending_invite_token') || token;
|
||||
const pendingPhoneCode = phoneCode || Taro.getStorageSync('pending_invite_phone_code');
|
||||
const pendingEncryptedData = encryptedData || Taro.getStorageSync('pending_invite_encrypted_data');
|
||||
const pendingIv = iv || Taro.getStorageSync('pending_invite_iv');
|
||||
const inviteToken = Taro.getStorageSync('invite_token') || token;
|
||||
|
||||
console.log('开始接受邀请, token:', pendingToken);
|
||||
console.log('请求URL:', `${INVITE_API_URL}/api/_app/developer/invite/accept`);
|
||||
console.log('请求参数:', { token: pendingToken, code: pendingPhoneCode ? '***' : null });
|
||||
console.log('开始接受邀请, token:', inviteToken);
|
||||
|
||||
const res = await Taro.request({
|
||||
url: `${INVITE_API_URL}/api/_app/developer/invite/accept`,
|
||||
method: 'POST',
|
||||
data: {
|
||||
token: pendingToken,
|
||||
code: pendingPhoneCode,
|
||||
encryptedData: pendingEncryptedData,
|
||||
iv: pendingIv
|
||||
token: inviteToken,
|
||||
code: phoneCode,
|
||||
encryptedData,
|
||||
iv
|
||||
},
|
||||
header: {
|
||||
'content-type': 'application/json',
|
||||
@@ -240,47 +219,18 @@ const InvitePage: React.FC = () => {
|
||||
});
|
||||
|
||||
console.log('接受邀请接口响应:', res);
|
||||
console.log('响应数据:', res.data);
|
||||
|
||||
if (res.data.code === 200 || res.data.code === 0) {
|
||||
// 清除 pending 的邀请信息
|
||||
clearPendingInviteData();
|
||||
clearInviteParams();
|
||||
// 清理邀请信息
|
||||
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, 'code:', res.data.code);
|
||||
const errorMsg = res.data.message || '';
|
||||
|
||||
// 用户不存在,引导去登录注册
|
||||
if (errorMsg.includes('用户不存在') || errorMsg.includes('用户创建失败') || errorMsg.includes('请先登录')) {
|
||||
// 保存当前邀请信息
|
||||
Taro.setStorageSync('pending_invite_token', pendingToken);
|
||||
if (pendingPhoneCode) Taro.setStorageSync('pending_invite_phone_code', pendingPhoneCode);
|
||||
if (pendingEncryptedData) Taro.setStorageSync('pending_invite_encrypted_data', pendingEncryptedData);
|
||||
if (pendingIv) Taro.setStorageSync('pending_invite_iv', pendingIv);
|
||||
|
||||
Taro.showModal({
|
||||
title: '需要登录',
|
||||
content: '您尚未注册,请先完成登录或注册后再加入应用',
|
||||
confirmText: '去登录',
|
||||
cancelText: '取消',
|
||||
success: (modalRes) => {
|
||||
if (modalRes.confirm) {
|
||||
// 跳转到手机号授权登录页面,携带token参数以便登录后返回
|
||||
Taro.navigateTo({
|
||||
url: `/passport/phone-auth/index?redirect=${encodeURIComponent('/passport/invite/index?token=' + pendingToken)}`
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Taro.showToast({ title: errorMsg || '加入失败', icon: 'none' });
|
||||
}
|
||||
console.error('接受邀请失败:', res.data.message);
|
||||
Taro.showToast({ title: res.data.message || '加入失败', icon: 'none' });
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('接受邀请异常:', err);
|
||||
@@ -290,26 +240,9 @@ const InvitePage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 清除 pending 的邀请数据
|
||||
*/
|
||||
const clearPendingInviteData = () => {
|
||||
Taro.removeStorageSync('pending_invite_token');
|
||||
Taro.removeStorageSync('pending_invite_phone_code');
|
||||
Taro.removeStorageSync('pending_invite_encrypted_data');
|
||||
Taro.removeStorageSync('pending_invite_iv');
|
||||
};
|
||||
|
||||
/**
|
||||
* 拒绝邀请
|
||||
*/
|
||||
// 拒绝邀请
|
||||
const handleReject = () => {
|
||||
const pages = Taro.getCurrentPages();
|
||||
if (pages.length > 1) {
|
||||
Taro.navigateBack();
|
||||
} else {
|
||||
Taro.switchTab({ url: '/pages/index/index' });
|
||||
}
|
||||
};
|
||||
|
||||
// 打开协议页面
|
||||
@@ -322,8 +255,8 @@ const InvitePage: React.FC = () => {
|
||||
Taro.navigateTo({ url: `/passport/webview/index?url=${targetUrl}` });
|
||||
};
|
||||
|
||||
// 加载中
|
||||
if (loading) {
|
||||
// 加载中状态
|
||||
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="flex flex-col items-center">
|
||||
@@ -332,14 +265,33 @@ const InvitePage: React.FC = () => {
|
||||
borderTopColor: '#3b82f6', borderRadius: '50%',
|
||||
animation: 'spin 1s linear infinite',
|
||||
}} />
|
||||
<Text style={{ color: 'rgba(255, 255, 255, 0.6)', marginTop: '16px', fontSize: '14px' }}>加载中...</Text>
|
||||
<Text style={{ color: 'rgba(255, 255, 255, 0.6)', marginTop: '16px', fontSize: '14px' }}>
|
||||
{pageStatus === 'checking' ? '检查登录状态...' : '加载中...'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// 跳转到登录中状态
|
||||
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 (error) {
|
||||
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="flex flex-col items-center px-8">
|
||||
|
||||
@@ -1,56 +1,359 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import Taro from '@tarojs/taro'
|
||||
import {Input, Radio, Button} from '@nutui/nutui-react-taro'
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { View, Text, Button } from '@tarojs/components';
|
||||
import Taro, { useRouter } from '@tarojs/taro';
|
||||
import { saveStorageByLoginUser, SERVER_API_URL } from "@/utils/server";
|
||||
import { checkAndHandleInviteRelation, hasPendingInvite, getStoredInviteParams } from "@/utils/invite";
|
||||
|
||||
const Login = () => {
|
||||
const [isAgree, setIsAgree] = useState(false)
|
||||
const reload = () => {
|
||||
Taro.hideTabBar()
|
||||
}
|
||||
/**
|
||||
* 扫码登录确认页面 - 科技风格授权页
|
||||
*
|
||||
* 用户扫描 PC 端二维码后,打开此小程序页面进行微信手机号授权登录
|
||||
*/
|
||||
|
||||
// 微信获取手机号回调参数类型
|
||||
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;
|
||||
}
|
||||
|
||||
// 协议类型
|
||||
type AgreementType = 'service' | 'privacy';
|
||||
|
||||
const QRConfirmPage: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const [authLoading, setAuthLoading] = useState(false); // 授权中状态
|
||||
const [agreementChecked, setAgreementChecked] = useState(false); // 协议勾选状态
|
||||
const [token, setToken] = useState<string>(''); // 登录 token
|
||||
|
||||
useEffect(() => {
|
||||
reload()
|
||||
}, [])
|
||||
// 从 URL 参数中获取 token
|
||||
const params = router.params;
|
||||
let loginToken = params.scene || params.token || params.qrCodeKey || '';
|
||||
|
||||
// 兼容 q 参数(URL 编码的完整 URL)
|
||||
if (params.q && !loginToken) {
|
||||
try {
|
||||
const decodedUrl = decodeURIComponent(params.q);
|
||||
const url = new URL(decodedUrl);
|
||||
loginToken = url.searchParams.get('token') || url.searchParams.get('qrCodeKey') || '';
|
||||
} catch (e) {
|
||||
loginToken = decodeURIComponent(params.q);
|
||||
}
|
||||
}
|
||||
|
||||
setToken(loginToken);
|
||||
}, [router.params]);
|
||||
|
||||
/**
|
||||
* 处理微信手机号授权
|
||||
*/
|
||||
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 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: 5,
|
||||
notVerifyPhone: true,
|
||||
refereeId,
|
||||
sceneType: 'save_referee'
|
||||
},
|
||||
header: { 'content-type': 'application/json', 'TenantId': 5 }
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
// 处理邀请关系
|
||||
if (hasPendingInvite()) {
|
||||
try {
|
||||
await checkAndHandleInviteRelation();
|
||||
} catch (e) {
|
||||
console.error('处理邀请关系失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
Taro.showToast({ title: '授权成功,正在确认登录...', icon: 'none' });
|
||||
|
||||
// 延迟确认扫码登录
|
||||
setTimeout(() => handleConfirmQRLogin(res.data.data.user), 1500);
|
||||
}
|
||||
} catch (error: any) {
|
||||
Taro.showToast({ title: error.message || '授权失败', icon: 'error' });
|
||||
} finally {
|
||||
setAuthLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 确认扫码登录
|
||||
*/
|
||||
const handleConfirmQRLogin = async (userInfo: any) => {
|
||||
if (!token) {
|
||||
Taro.showToast({ title: '缺少登录token', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await Taro.request({
|
||||
url: `${SERVER_API_URL}/qr-login/confirm`,
|
||||
method: 'POST',
|
||||
data: {
|
||||
token,
|
||||
userId: userInfo.userId,
|
||||
platform: 'wechat',
|
||||
wechatInfo: {
|
||||
nickname: userInfo.nickname || userInfo.username,
|
||||
avatar: userInfo.avatar
|
||||
}
|
||||
},
|
||||
header: { 'content-type': 'application/json', 'TenantId': 5 }
|
||||
});
|
||||
|
||||
if (res.data.success || res.data.status === 'confirmed') {
|
||||
Taro.showToast({ title: '登录确认成功', icon: 'success', duration: 2000 });
|
||||
setTimeout(() => {
|
||||
// 先隐藏 toast,避免影响页面跳转
|
||||
Taro.hideToast();
|
||||
Taro.switchTab({
|
||||
url: '/pages/user/user',
|
||||
success: () => {
|
||||
console.log('switchTab to /pages/user/user success');
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('switchTab fail:', err);
|
||||
Taro.showToast({ title: '页面跳转失败,请手动返回', icon: 'none' });
|
||||
}
|
||||
});
|
||||
}, 1800);
|
||||
} else {
|
||||
Taro.showToast({ title: res.data.message || '登录确认失败', icon: 'none' });
|
||||
}
|
||||
} catch (err: any) {
|
||||
Taro.showToast({ title: err.message || '确认登录失败', icon: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 取消登录
|
||||
*/
|
||||
const handleCancel = () => {
|
||||
const pages = Taro.getCurrentPages();
|
||||
if (pages.length > 1) {
|
||||
Taro.navigateBack();
|
||||
} else {
|
||||
Taro.switchTab({ url: '/pages/user/user' });
|
||||
}
|
||||
};
|
||||
|
||||
// 打开协议页面
|
||||
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}` });
|
||||
};
|
||||
|
||||
// 科技风格授权页面 - 蓝色主题
|
||||
return (
|
||||
<>
|
||||
<div className={'flex flex-col justify-center px-5'}>
|
||||
<div className={'text-3xl text-center py-5 font-normal my-10'}>账号登录</div>
|
||||
<View className="min-h-screen relative overflow-hidden" style={{ background: 'linear-gradient(135deg, #0a0a1a 0%, #0d1528 50%, #0a1520 100%)' }}>
|
||||
|
||||
<>
|
||||
<div className={'flex flex-col justify-between items-center my-2'}>
|
||||
<Input type="text" placeholder="手机号" maxLength={11}
|
||||
style={{backgroundColor: '#ffffff', borderRadius: '8px'}}/>
|
||||
</div>
|
||||
<div className={'flex flex-col justify-between items-center my-2'}>
|
||||
<Input type="password" placeholder="密码" style={{backgroundColor: '#ffffff', borderRadius: '8px'}}/>
|
||||
</div>
|
||||
<div className={'flex justify-between my-2 text-left px-1'}>
|
||||
<a href={'#'} className={'text-blue-600 text-sm'}
|
||||
onClick={() => Taro.navigateTo({url: '/passport/forget'})}>忘记密码</a>
|
||||
<a href={'#'} className={'text-blue-600 text-sm'}
|
||||
onClick={() => Taro.navigateTo({url: '/passport/register'})}>立即注册</a>
|
||||
</div>
|
||||
<div className={'flex justify-center my-5'}>
|
||||
<Button type="info" size={'large'} className={'w-full rounded-lg p-2'} disabled={!isAgree}>登录</Button>
|
||||
</div>
|
||||
<div className={'my-2 flex fixed justify-center bottom-20 left-0 text-sm items-center text-center w-full'}>
|
||||
<Button onClick={() => Taro.navigateTo({url: '/passport/setting'})}>服务配置</Button>
|
||||
</div>
|
||||
{/*<div className={'w-full fixed bottom-20 my-2 flex justify-center text-sm items-center text-center'}>*/}
|
||||
{/* 没有账号?<a href={''} onClick={() => Taro.navigateTo({url: '/passport/register'})}*/}
|
||||
{/* className={'text-blue-600'}>立即注册</a>*/}
|
||||
{/*</div>*/}
|
||||
</>
|
||||
{/* 背景科技元素 */}
|
||||
{/* 网格背景 */}
|
||||
<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,
|
||||
}} />
|
||||
|
||||
<div className={'my-2 flex text-sm items-center px-1'}>
|
||||
<Radio style={{color: '#333333'}} checked={isAgree} onClick={() => setIsAgree(!isAgree)}></Radio>
|
||||
<span className={'text-gray-400'} onClick={() => setIsAgree(!isAgree)}>勾选表示您已阅读并同意</span><a
|
||||
onClick={() => Taro.navigateTo({url: '/passport/agreement'})}
|
||||
className={'text-blue-600'}>《服务协议及隐私政策》</a>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default Login
|
||||
{/* 渐变光晕 - 左上 */}
|
||||
<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: '20%', size: 4, delay: '0s' },
|
||||
{ top: '25%', left: '80%', size: 3, delay: '0.5s' },
|
||||
{ top: '40%', left: '15%', size: 5, delay: '1s' },
|
||||
{ top: '35%', left: '85%', size: 3, delay: '1.5s' },
|
||||
{ top: '55%', left: '10%', size: 4, delay: '2s' },
|
||||
{ top: '60%', left: '90%', size: 3, delay: '0.3s' },
|
||||
{ top: '75%', left: '25%', size: 4, delay: '1.2s' },
|
||||
{ top: '80%', left: '75%', size: 5, delay: '0.8s' },
|
||||
].map((particle, index) => (
|
||||
<View key={index} className="particle-blue" style={{
|
||||
position: 'absolute', top: particle.top, left: particle.left,
|
||||
width: particle.size, height: particle.size, borderRadius: '50%',
|
||||
background: index % 2 === 0 ? '#3b82f6' : '#6366f1',
|
||||
boxShadow: index % 2 === 0
|
||||
? '0 0 10px rgba(59, 130, 246, 0.8), 0 0 20px rgba(59, 130, 246, 0.4)'
|
||||
: '0 0 10px rgba(99, 102, 241, 0.8), 0 0 20px rgba(99, 102, 241, 0.4)',
|
||||
animationDelay: particle.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">
|
||||
|
||||
{/* Logo 区域 */}
|
||||
<View className="flex flex-col items-center mb-12">
|
||||
<View style={{
|
||||
width: '100px', height: '100px', borderRadius: '24px',
|
||||
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: '20px', position: 'relative', overflow: 'hidden',
|
||||
}}>
|
||||
{/* Logo 内光效 */}
|
||||
<View style={{
|
||||
position: 'absolute', top: '-50%', left: '-50%', width: '200%', height: '200%',
|
||||
background: 'conic-gradient(from 0deg, transparent, rgba(59, 130, 246, 0.15), transparent, rgba(99, 102, 241, 0.15), transparent)',
|
||||
animation: 'rotate 4s linear infinite',
|
||||
}} />
|
||||
<Text style={{ fontSize: '48px', position: 'relative', zIndex: 1 }}>🔐</Text>
|
||||
</View>
|
||||
|
||||
<Text style={{
|
||||
fontSize: '28px', fontWeight: '700',
|
||||
background: 'linear-gradient(90deg, #3b82f6, #6366f1, #8b5cf6)',
|
||||
WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent',
|
||||
backgroundClip: 'text', marginBottom: '8px',
|
||||
}}>websopy</Text>
|
||||
|
||||
<View style={{
|
||||
width: '60px', height: '3px',
|
||||
background: 'linear-gradient(90deg, transparent, #3b82f6, transparent)',
|
||||
marginTop: '20px', borderRadius: '2px',
|
||||
}} />
|
||||
</View>
|
||||
|
||||
{/* 主按钮 - 渐变发光按钮 */}
|
||||
<View style={{
|
||||
width: '100%', maxWidth: '320px',
|
||||
background: 'linear-gradient(135deg, #1d4ed8 0%, #3b82f6 50%, #6366f1 100%)',
|
||||
borderRadius: '30px', padding: '2px',
|
||||
boxShadow: '0 0 30px rgba(59, 130, 246, 0.4), 0 0 60px rgba(59, 130, 246, 0.2)',
|
||||
}}>
|
||||
<Button
|
||||
open-type="getPhoneNumber"
|
||||
onGetPhoneNumber={handleGetPhoneNumber}
|
||||
disabled={authLoading}
|
||||
style={{
|
||||
width: '100%', height: '52px', fontSize: '17px', fontWeight: '600',
|
||||
color: '#fff', background: 'transparent', borderRadius: '28px',
|
||||
border: 'none', padding: '0', boxSizing: 'border-box', lineHeight: '52px',
|
||||
}}
|
||||
>
|
||||
{authLoading ? '授权中...' : '微信手机号登录'}
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
{/* 取消按钮 */}
|
||||
<View style={{ width: '100%', maxWidth: '320px', height: '44px', display: 'flex', alignItems: 'center', justifyContent: 'center', marginTop: '16px' }} onClick={handleCancel}>
|
||||
<Text style={{ color: 'rgba(255, 255, 255, 0.5)', fontSize: '14px' }}>取消</Text>
|
||||
</View>
|
||||
|
||||
{/* 协议勾选 */}
|
||||
<View className="flex items-center justify-center mt-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>
|
||||
|
||||
{/* 底部装饰 */}
|
||||
<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 QRConfirmPage;
|
||||
|
||||
Reference in New Issue
Block a user