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` - 完整重构邀请流程
|
- `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": {
|
"miniprogram": {
|
||||||
"list": [
|
"list": [
|
||||||
{
|
{
|
||||||
"name": "passport/invite/index",
|
"name": "passport/login",
|
||||||
"pathName": "passport/invite/index",
|
"pathName": "passport/login",
|
||||||
"query": "",
|
"query": "",
|
||||||
"scene": null,
|
"scene": null,
|
||||||
"launchMode": "default"
|
"launchMode": "default"
|
||||||
@@ -18,6 +18,13 @@
|
|||||||
"launchMode": "default",
|
"launchMode": "default",
|
||||||
"scene": null
|
"scene": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "passport/invite/index",
|
||||||
|
"pathName": "passport/invite/index",
|
||||||
|
"query": "",
|
||||||
|
"launchMode": "default",
|
||||||
|
"scene": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "passport/invite",
|
"name": "passport/invite",
|
||||||
"pathName": "passport/invite",
|
"pathName": "passport/invite",
|
||||||
|
|||||||
@@ -1,39 +1,24 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
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 { TenantId } from "@/config/app";
|
||||||
|
|
||||||
// 邀请相关接口使用独立的 API 域名
|
// 邀请相关接口使用独立的 API 域名
|
||||||
// 注意:使用 /api/_app 前缀表示小程序专用接口(免登录)
|
|
||||||
const INVITE_API_URL = 'https://websopy-api.websoft.top';
|
const INVITE_API_URL = 'https://websopy-api.websoft.top';
|
||||||
import { TenantId } from "@/config/app";
|
|
||||||
import { getStoredInviteParams, saveInviteParams, clearInviteParams } from "@/utils/invite";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 邀请加入确认页面
|
* 邀请加入确认页面
|
||||||
*
|
*
|
||||||
* 用户扫描邀请二维码后,打开此小程序页面确认加入应用
|
|
||||||
*
|
|
||||||
* 流程:
|
* 流程:
|
||||||
* 1. 用户扫码进入页面,解析 token 参数
|
* 1. 扫码进入页面
|
||||||
* 2. 获取邀请信息展示给用户
|
* 2. 调用 wx.login() 获取 code
|
||||||
* 3. 用户点击"微信手机号快速加入"
|
* 3. 调用 loginByOpenId 判断用户是否已注册
|
||||||
* 4. 如果用户未登录,保存邀请信息并引导到登录页
|
* 4. 已注册:显示邀请页面,用户点击加入
|
||||||
* 5. 登录成功后返回此页面,自动执行加入操作
|
* 5. 未注册:跳转到 passport/login 页面完成登录/注册
|
||||||
* 6. 已登录用户直接执行加入操作
|
* 6. 登录成功后返回,自动执行加入操作
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// 微信获取手机号回调参数类型
|
|
||||||
interface GetPhoneNumberDetail {
|
|
||||||
code?: string;
|
|
||||||
encryptedData?: string;
|
|
||||||
iv?: string;
|
|
||||||
errMsg: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GetPhoneNumberEvent {
|
|
||||||
detail: GetPhoneNumberDetail;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 邀请信息类型
|
// 邀请信息类型
|
||||||
interface InviteInfo {
|
interface InviteInfo {
|
||||||
appId: string;
|
appId: string;
|
||||||
@@ -46,9 +31,12 @@ interface InviteInfo {
|
|||||||
// 协议类型
|
// 协议类型
|
||||||
type AgreementType = 'service' | 'privacy';
|
type AgreementType = 'service' | 'privacy';
|
||||||
|
|
||||||
|
// 页面状态
|
||||||
|
type PageStatus = 'loading' | 'checking' | 'invite' | 'login' | 'error';
|
||||||
|
|
||||||
const InvitePage: React.FC = () => {
|
const InvitePage: React.FC = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [loading, setLoading] = useState(true);
|
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);
|
||||||
@@ -57,14 +45,15 @@ const InvitePage: React.FC = () => {
|
|||||||
const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);
|
const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 检查用户是否已登录
|
initPage().then();
|
||||||
const accessToken = Taro.getStorageSync('access_token');
|
}, [router.params]);
|
||||||
setIsLoggedIn(!!accessToken);
|
|
||||||
|
|
||||||
|
// 页面初始化
|
||||||
|
const initPage = async () => {
|
||||||
// 从 URL 参数中获取 token
|
// 从 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)
|
// 兼容 q 参数(URL 编码的完整 URL)
|
||||||
if (params.q && !inviteToken) {
|
if (params.q && !inviteToken) {
|
||||||
try {
|
try {
|
||||||
@@ -76,58 +65,82 @@ const InvitePage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setToken(inviteToken);
|
if (!inviteToken) {
|
||||||
|
|
||||||
// 获取邀请信息
|
|
||||||
if (inviteToken) {
|
|
||||||
// 保存邀请 token 到本地存储,供登录后使用
|
|
||||||
saveInviteParams({
|
|
||||||
inviter: inviteToken,
|
|
||||||
source: 'app_invite',
|
|
||||||
t: Date.now().toString()
|
|
||||||
});
|
|
||||||
fetchInviteInfo(inviteToken);
|
|
||||||
} else {
|
|
||||||
setError('无效的邀请链接');
|
setError('无效的邀请链接');
|
||||||
setLoading(false);
|
setPageStatus('error');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}, [router.params]);
|
|
||||||
|
|
||||||
// 页面显示时检查是否需要自动执行加入操作(从登录页返回)
|
setToken(inviteToken);
|
||||||
useEffect(() => {
|
// 保存邀请 token,供登录后使用
|
||||||
const handleShow = () => {
|
Taro.setStorageSync('invite_token', inviteToken);
|
||||||
// 检查是否有 pending 的邀请数据且用户已登录
|
|
||||||
const pendingToken = Taro.getStorageSync('pending_invite_token');
|
|
||||||
const accessToken = Taro.getStorageSync('access_token');
|
|
||||||
|
|
||||||
if (pendingToken && accessToken) {
|
// 检查用户登录状态
|
||||||
console.log('检测到登录后返回,自动执行加入应用操作');
|
await checkLoginStatus(inviteToken);
|
||||||
// 更新登录状态
|
console.log('检查登录状态完成', inviteToken)
|
||||||
setIsLoggedIn(true);
|
};
|
||||||
// 自动执行加入操作
|
|
||||||
handleJoinApp();
|
// 检查用户登录状态
|
||||||
|
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 判断用户是否已注册
|
||||||
Taro.eventCenter.on('AppShow', handleShow);
|
const loginRes = await loginByOpenId({
|
||||||
|
code: wxLoginRes.code,
|
||||||
|
tenantId: parseInt(TenantId) || 1
|
||||||
|
});
|
||||||
|
|
||||||
// 立即检查一次(处理页面首次加载时已经是登录状态的情况)
|
console.log('loginByOpenId 结果:', loginRes);
|
||||||
handleShow();
|
|
||||||
|
|
||||||
return () => {
|
if (loginRes.success && loginRes.data?.access_token) {
|
||||||
Taro.eventCenter.off('AppShow', handleShow);
|
// 用户已注册,保存登录信息
|
||||||
};
|
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`
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取邀请信息
|
||||||
const fetchInviteInfo = async (inviteToken: string) => {
|
const fetchInviteInfo = async (inviteToken: string) => {
|
||||||
try {
|
try {
|
||||||
console.log('开始获取邀请信息, token:', inviteToken);
|
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({
|
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)}`,
|
||||||
@@ -139,30 +152,26 @@ const InvitePage: React.FC = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
console.log('邀请信息接口响应:', res);
|
console.log('邀请信息接口响应:', res);
|
||||||
console.log('响应数据:', res.data);
|
|
||||||
|
|
||||||
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, 'code:', res.data.code);
|
console.error('接口返回错误:', res.data.message);
|
||||||
setError(res.data.message || '邀请信息获取失败');
|
setError(res.data.message || '邀请信息获取失败');
|
||||||
|
setPageStatus('error');
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('获取邀请信息异常:', err);
|
console.error('获取邀请信息异常:', err);
|
||||||
setError(err.message || '网络请求失败');
|
setError(err.message || '网络请求失败');
|
||||||
} finally {
|
setPageStatus('error');
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
// 处理微信手机号授权
|
||||||
* 处理微信手机号授权
|
const handleGetPhoneNumber = async (e: any) => {
|
||||||
*
|
const { code, encryptedData, iv, errMsg } = e.detail;
|
||||||
* 如果用户未登录,先引导到登录页面完成注册/登录
|
|
||||||
* 登录成功后返回此页面自动执行加入操作
|
console.log('handleGetPhoneNumber:', { code, errMsg });
|
||||||
*/
|
|
||||||
const handleGetPhoneNumber = async ({ detail }: GetPhoneNumberEvent) => {
|
|
||||||
const { code, encryptedData, iv, errMsg } = detail;
|
|
||||||
|
|
||||||
// 检查协议是否勾选
|
// 检查协议是否勾选
|
||||||
if (!agreementChecked) {
|
if (!agreementChecked) {
|
||||||
@@ -181,57 +190,27 @@ const InvitePage: React.FC = () => {
|
|||||||
return;
|
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);
|
await handleJoinApp(code, encryptedData, iv);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
// 加入应用
|
||||||
* 加入应用
|
const handleJoinApp = async (phoneCode: string, encryptedData?: string, iv?: string) => {
|
||||||
*
|
|
||||||
* 支持两种调用方式:
|
|
||||||
* 1. 新用户:通过手机号授权码完成注册并加入(phoneCode 必传)
|
|
||||||
* 2. 已登录用户:直接加入应用(使用存储的 token)
|
|
||||||
*/
|
|
||||||
const handleJoinApp = async (phoneCode?: string, encryptedData?: string, iv?: string) => {
|
|
||||||
try {
|
try {
|
||||||
setAuthLoading(true);
|
setAuthLoading(true);
|
||||||
|
|
||||||
// 获取 pending 的邀请信息(如果是从登录页返回)
|
const inviteToken = Taro.getStorageSync('invite_token') || token;
|
||||||
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');
|
|
||||||
|
|
||||||
console.log('开始接受邀请, token:', pendingToken);
|
console.log('开始接受邀请, token:', inviteToken);
|
||||||
console.log('请求URL:', `${INVITE_API_URL}/api/_app/developer/invite/accept`);
|
|
||||||
console.log('请求参数:', { token: pendingToken, code: pendingPhoneCode ? '***' : null });
|
|
||||||
|
|
||||||
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: pendingToken,
|
token: inviteToken,
|
||||||
code: pendingPhoneCode,
|
code: phoneCode,
|
||||||
encryptedData: pendingEncryptedData,
|
encryptedData,
|
||||||
iv: pendingIv
|
iv
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
'content-type': 'application/json',
|
'content-type': 'application/json',
|
||||||
@@ -240,47 +219,18 @@ const InvitePage: React.FC = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
console.log('接受邀请接口响应:', res);
|
console.log('接受邀请接口响应:', res);
|
||||||
console.log('响应数据:', res.data);
|
|
||||||
|
|
||||||
if (res.data.code === 200 || res.data.code === 0) {
|
if (res.data.code === 200 || res.data.code === 0) {
|
||||||
// 清除 pending 的邀请信息
|
// 清理邀请信息
|
||||||
clearPendingInviteData();
|
Taro.removeStorageSync('invite_token');
|
||||||
clearInviteParams();
|
|
||||||
|
|
||||||
Taro.showToast({ title: '加入成功', icon: 'success', duration: 1500 });
|
Taro.showToast({ title: '加入成功', icon: 'success', duration: 1500 });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// 跳转到应用页面或首页
|
|
||||||
Taro.switchTab({ url: '/pages/index/index' });
|
Taro.switchTab({ url: '/pages/index/index' });
|
||||||
}, 1500);
|
}, 1500);
|
||||||
} else {
|
} else {
|
||||||
console.error('接受邀请失败:', res.data.message, 'code:', res.data.code);
|
console.error('接受邀请失败:', res.data.message);
|
||||||
const errorMsg = res.data.message || '';
|
Taro.showToast({ title: res.data.message || '加入失败', icon: 'none' });
|
||||||
|
|
||||||
// 用户不存在,引导去登录注册
|
|
||||||
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' });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('接受邀请异常:', err);
|
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 handleReject = () => {
|
||||||
const pages = Taro.getCurrentPages();
|
Taro.switchTab({ url: '/pages/index/index' });
|
||||||
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}` });
|
Taro.navigateTo({ url: `/passport/webview/index?url=${targetUrl}` });
|
||||||
};
|
};
|
||||||
|
|
||||||
// 加载中
|
// 加载中状态
|
||||||
if (loading) {
|
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">
|
||||||
@@ -332,14 +265,33 @@ const InvitePage: React.FC = () => {
|
|||||||
borderTopColor: '#3b82f6', borderRadius: '50%',
|
borderTopColor: '#3b82f6', borderRadius: '50%',
|
||||||
animation: 'spin 1s linear infinite',
|
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>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 错误状态
|
// 错误状态
|
||||||
if (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">
|
||||||
|
|||||||
@@ -1,56 +1,359 @@
|
|||||||
import {useEffect, useState} from "react";
|
import React, { useState, useEffect } from 'react';
|
||||||
import Taro from '@tarojs/taro'
|
import { View, Text, Button } from '@tarojs/components';
|
||||||
import {Input, Radio, Button} from '@nutui/nutui-react-taro'
|
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(() => {
|
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 (
|
return (
|
||||||
<>
|
<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-center px-5'}>
|
|
||||||
<div className={'text-3xl text-center py-5 font-normal my-10'}>账号登录</div>
|
|
||||||
|
|
||||||
<>
|
{/* 背景科技元素 */}
|
||||||
<div className={'flex flex-col justify-between items-center my-2'}>
|
{/* 网格背景 */}
|
||||||
<Input type="text" placeholder="手机号" maxLength={11}
|
<View style={{
|
||||||
style={{backgroundColor: '#ffffff', borderRadius: '8px'}}/>
|
position: 'absolute', top: 0, left: 0, right: 0, bottom: 0,
|
||||||
</div>
|
backgroundImage: `linear-gradient(rgba(59, 130, 246, 0.08) 1px, transparent 1px), linear-gradient(90deg, rgba(59, 130, 246, 0.08) 1px, transparent 1px)`,
|
||||||
<div className={'flex flex-col justify-between items-center my-2'}>
|
backgroundSize: '50px 50px', opacity: 0.5,
|
||||||
<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>*/}
|
|
||||||
</>
|
|
||||||
|
|
||||||
<div className={'my-2 flex text-sm items-center px-1'}>
|
{/* 渐变光晕 - 左上 */}
|
||||||
<Radio style={{color: '#333333'}} checked={isAgree} onClick={() => setIsAgree(!isAgree)}></Radio>
|
<View style={{
|
||||||
<span className={'text-gray-400'} onClick={() => setIsAgree(!isAgree)}>勾选表示您已阅读并同意</span><a
|
position: 'absolute', top: '-30%', left: '-20%', width: '60%', height: '60%',
|
||||||
onClick={() => Taro.navigateTo({url: '/passport/agreement'})}
|
background: 'radial-gradient(circle, rgba(59, 130, 246, 0.25) 0%, transparent 70%)',
|
||||||
className={'text-blue-600'}>《服务协议及隐私政策》</a>
|
filter: 'blur(40px)',
|
||||||
</div>
|
}} />
|
||||||
</div>
|
|
||||||
</>
|
{/* 渐变光晕 - 右下 */}
|
||||||
)
|
<View style={{
|
||||||
}
|
position: 'absolute', bottom: '-20%', right: '-20%', width: '50%', height: '50%',
|
||||||
export default Login
|
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